├── .gitignore ├── .ignore ├── README.md ├── assets ├── screen-shot1.png ├── screen-shot2.png ├── screen-shot3-edit-mode.png ├── screen-shot4-edit-mode.png ├── screen-shot4.png ├── screen-shot5-edit-mode.png ├── screen-shot5.png ├── screen-shot6-edit-mode.png ├── screen-shot6.png ├── screen-shot7.png └── screen-shot8.png ├── autoload ├── ags.vim └── ags │ ├── buf.vim │ ├── cur.vim │ ├── edit.vim │ ├── log.vim │ ├── pat.vim │ └── run.vim ├── doc └── ags.txt ├── ftdetect ├── agse.vim └── agsv.vim ├── ftplugin ├── agse.vim └── agsv.vim ├── plugin └── ags.vim └── syntax ├── agse.vim └── agsv.vim /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.gz 4 | *.log 5 | *.out 6 | *.pid 7 | *.rdb 8 | *.seed 9 | 10 | .DS_Store 11 | .stat-test 12 | 13 | NERD_tree_* 14 | bower_components 15 | dist 16 | doc/tags 17 | lib-cov 18 | logs 19 | npm-debug.log 20 | pids 21 | public 22 | results 23 | testem.log 24 | tmp 25 | 26 | Highlight test 27 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | .git/* 2 | .gitignore 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | _____ ____ ______ 3 | \__ \ / ___\/ ___/ 4 | / __ \_/ /_/ >___ \ 5 | (____ /\___ /____ > 6 | \//_____/ \/ 7 | ``` 8 | 9 | # Silver searcher (AG) plugin for Vim 10 | *A Vim plugin for the [silver searcher](https://github.com/ggreer/the_silver_searcher) or [ripgrep](https://github.com/BurntSushi/ripgrep) that focuses on 11 | clear display and easy navigation of the search results* 12 | 13 | ### Installation 14 | Install via [pathogen](https://github.com/tpope/vim-pathogen), [vundle](https://github.com/gmarik/vundle), [plug](https://github.com/junegunn/vim-plug) or copy to the Vim directory 15 | The [ag](https://github.com/ggreer/the_silver_searcher) or [rg](https://github.com/BurntSushi/ripgrep) executable must be installed as well. 16 | 17 | ### Usage 18 | See the [docs](https://github.com/gabesoft/vim-ags/blob/master/doc/ags.txt) or press `u` (for usage) while in the search results window. 19 | 20 | ### Using [ripgrep](https://github.com/BurntSushi/ripgrep) instead of [ag](https://github.com/ggreer/the_silver_searcher) 21 | Despite the name `vim-ags` works with `ripgrep` as well if configured as below: 22 | ```vim 23 | let g:ags_agexe = 'rg' 24 | 25 | let g:ags_agargs = { 26 | \ '--column' : ['', ''], 27 | \ '--line-number' : ['', ''], 28 | \ '--context' : ['g:ags_agcontext', '-C'], 29 | \ '--max-count' : ['g:ags_agmaxcount', ''], 30 | \ '--heading' : ['',''], 31 | \ '--smart-case' : ['','-S'], 32 | \ '--color' : ['always',''], 33 | \ '--colors' : [['match:fg:green', 'match:bg:black', 'match:style:nobold', 'path:fg:red', 'path:style:bold', 'line:fg:black', 'line:style:bold'] ,''], 34 | \ } 35 | ``` 36 | 37 | ### Sample Shortcut Mappings 38 | ```vim 39 | " Search for the word under cursor 40 | nnoremap s :Ags=expand('') 41 | " Search for the visually selected text 42 | vnoremap s y:Ags='"' . escape(@", '"*?()[]{}.') . '"' 43 | " Run Ags 44 | nnoremap a :Ags 45 | " Quit Ags 46 | nnoremap a :AgsQuit 47 | ``` 48 | 49 | ### Notes 50 | Works with ag version >= 0.29.1 or ripgrep >= 11.0.2 51 | 52 | ### Screenshots 53 | Here are a couple of screenshots of the search results window 54 | 55 | #### View mode (with [lightline](https://github.com/itchyny/lightline.vim) integration) 56 | 57 | 58 | #### Edit mode 59 | 60 | 61 | ### Similar Plugins 62 | [ctrlsf](https://github.com/dyng/ctrlsf.vim) 63 | -------------------------------------------------------------------------------- /assets/screen-shot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot1.png -------------------------------------------------------------------------------- /assets/screen-shot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot2.png -------------------------------------------------------------------------------- /assets/screen-shot3-edit-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot3-edit-mode.png -------------------------------------------------------------------------------- /assets/screen-shot4-edit-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot4-edit-mode.png -------------------------------------------------------------------------------- /assets/screen-shot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot4.png -------------------------------------------------------------------------------- /assets/screen-shot5-edit-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot5-edit-mode.png -------------------------------------------------------------------------------- /assets/screen-shot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot5.png -------------------------------------------------------------------------------- /assets/screen-shot6-edit-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot6-edit-mode.png -------------------------------------------------------------------------------- /assets/screen-shot6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot6.png -------------------------------------------------------------------------------- /assets/screen-shot7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot7.png -------------------------------------------------------------------------------- /assets/screen-shot8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabesoft/vim-ags/4b1cb6cc66345919385e71eea5140001946914f4/assets/screen-shot8.png -------------------------------------------------------------------------------- /autoload/ags.vim: -------------------------------------------------------------------------------- 1 | 2 | " The position of the last highlighted search pattern 3 | let s:hlpos = [] 4 | 5 | " Search results statistics 6 | let s:stats = {} 7 | 8 | " Results accumulated during an async request 9 | let s:lines = [] 10 | 11 | " Last line of search results saved during an async request 12 | let s:lastLine = '' 13 | 14 | " Flag that indicates whether to add to the search results 15 | " window after a search 16 | let s:add = 0 17 | 18 | " Regex patterns cache 19 | let s:patt = { 20 | \ 'lineNo' : '^[1;30m\(\d\{1,}\)', 21 | \ 'lineNoCapture' : '\[1;30m\([ 0-9]\{-1,}\)\[0m\[K-', 22 | \ 'lineColNoCapture' : '\[1;30m\([ 0-9]\{-1,}\)\[0m\[K:\d\{-1,}:', 23 | \ 'file' : '^[1;31m.\{-}[0m[K$', 24 | \ 'fileCapture' : '[1;31m\(.\{-}\)[0m[K', 25 | \ 'result' : '[32;40m.\{-}[0m[K', 26 | \ 'resultCapture' : '\[32;40m\(.\{-}\)\[0m\[K', 27 | \ 'resultReplace' : '[32;40m\1[0m[K' 28 | \ } 29 | 30 | " Search results usage 31 | let s:usage = [ 32 | \ ' Search Results Key Bindings', 33 | \ ' ---------------------------', 34 | \ ' ', 35 | \ ' Results Window Commands', 36 | \ ' p - navigate file paths forward', 37 | \ ' P - navigate files paths backwards', 38 | \ ' r - navigate results forward', 39 | \ ' R - navigate results backwards', 40 | \ ' a - display the file path for current results', 41 | \ ' c - copy the file path for current results', 42 | \ ' E - enter edit mode', 43 | \ ' q - close the search results window', 44 | \ ' u - usage', 45 | \ ' ', 46 | \ ' Open Window Commands', 47 | \ ' - to maintain the focus in the search results window use', 48 | \ ' the uppercase variant of the mappings below', 49 | \ ' oa - open file above the results window', 50 | \ ' ob - open file below the results window', 51 | \ ' ol - open file to the left of the results window', 52 | \ ' or - open file to the right of the results window', 53 | \ ' os - open file in the results window', 54 | \ ' ou - open file in a previously opened window (alias Enter)', 55 | \ ' xu - open file in a previously opened window and close the search results', 56 | \ ' ', 57 | \ ] 58 | 59 | " Window position flags 60 | let s:wflags = { 't' : 'above', 'a' : 'above', 'b' : 'below', 'r' : 'right', 'l' : 'left' } 61 | 62 | " Executes a write command 63 | " 64 | function! s:execw(...) 65 | exec 'setlocal modifiable' 66 | for cmd in a:000 67 | if type(cmd) == type({}) 68 | call cmd.run() 69 | else 70 | exec cmd 71 | endif 72 | endfor 73 | exec 'setlocal nomodifiable' 74 | endfunction 75 | 76 | " Displays the search results from {lines} in the 77 | " search results window 78 | " 79 | function! s:show(lines, ...) 80 | let obj = { 'add': a:0 && a:1, 'lines': a:lines } 81 | 82 | function obj.run() 83 | if self.add && line('$') > 1 84 | call append('$', self.lines) 85 | else 86 | call ags#buf#replaceLines(self.lines) 87 | endif 88 | endfunction 89 | 90 | call ags#buf#setLastWinnr(winnr()) 91 | call ags#buf#openViewResultsBuffer() 92 | call s:execw(obj) 93 | call s:printStats(0, 0, 1) 94 | endfunction 95 | 96 | function! s:isNewLine(data) 97 | let data = substitute(a:data, '\e', '', 'g') 98 | let data = substitute(data, '["\(\d\{-};\d\{-}\)"m', '[\1m', 'g') 99 | return data =~ s:patt.lineNo || data =~ s:patt.file 100 | endfunction 101 | 102 | " If the search {data} comes from ripgrep make it look like it came from ag 103 | " 104 | function! s:cleanRipGrepData(data) 105 | let cmd = has_key(g:, 'ags_agexe') ? g:ags_agexe : '' 106 | if cmd !=? 'rg' 107 | return a:data 108 | endif 109 | 110 | let data = a:data 111 | let data = substitute(data, '[0m[1m[31m', '[1;31m', 'g') 112 | let data = substitute(data, '[0m[32m[40m', '[32;40m', 'g') 113 | let data = substitute(data, '[0m[1m[30m', '[1;30m', 'g') 114 | let data = substitute(data, '[0m', '[0m[K', 'g') 115 | let data = substitute(data, '[0m[K:[0m[K', '[0m[K-[0m[K', 'g') 116 | let data = substitute(data, '\(\d\{1,}\)[0m[K:', '\1:', 'g') 117 | let data = substitute(data, '[0m[K-[0m[K', '[0m[K:', 'g') 118 | 119 | return data 120 | endfunction 121 | 122 | " Prepares the search {data} for display 123 | " 124 | function! s:processSearchData(data) 125 | let data = a:data 126 | let data = substitute(data, '\e', '', 'g') 127 | let data = s:cleanRipGrepData(data) 128 | let data = substitute(data, '["\(\d\{-};\d\{-}\)"m', '[\1m', 'g') 129 | let lines = split(data, '\n') 130 | let lmaxlen = 9 131 | 132 | if !s:asyncEnabled() 133 | for line in lines 134 | let lmatch = matchstr(line, s:patt.lineNo) 135 | let lmaxlen = max([ strlen(lmatch), lmaxlen ]) 136 | endfor 137 | endif 138 | 139 | let results = [] 140 | for line in lines 141 | let llen = strlen(matchstr(line, s:patt.lineNo)) 142 | let wlen = lmaxlen - llen 143 | 144 | " right justify line numbers and add a space after 145 | let line = substitute(line, s:patt.lineNo, '[1;30m' . repeat(' ', wlen) . '\1 ', '') 146 | 147 | call add(results, line) 148 | endfor 149 | 150 | return results 151 | endfunction 152 | 153 | " Gathers statistics about the search results from {lines} 154 | " 155 | function! s:gatherStatistics(lines) 156 | if len(a:lines) > g:ags_stats_max_ln | return {} | endif 157 | 158 | let stats = {} 159 | let totalCount = 0 160 | let fileCount = 0 161 | let index = 0 162 | let llines = len(a:lines) 163 | let curFile = '' 164 | 165 | while index < llines 166 | let line = a:lines[index] 167 | let currCount = 0 168 | 169 | if line =~ s:patt.file 170 | let fileCount = fileCount + 1 171 | let curFile = substitute(line, s:patt.fileCapture, '\1', '') 172 | elseif line =~ s:patt.result 173 | let occurences = ags#pat#matchCount(line, s:patt.result, 0) 174 | let totalCount = totalCount + occurences 175 | let currCount = currCount + occurences 176 | endif 177 | 178 | let index = index + 1 179 | let stats[index] = { 180 | \ 'file' : fileCount, 181 | \ 'filePath' : curFile, 182 | \ 'result' : totalCount, 183 | \ 'matches' : currCount 184 | \ } 185 | endw 186 | 187 | return { 'data': stats, 'files': fileCount, 'results': totalCount } 188 | endfunction 189 | 190 | " Returns the cursor position when opening a file 191 | " from the {lineNo} in the search results window 192 | " 193 | function! s:resultPosition(lineNo) 194 | let line = getline(a:lineNo) 195 | let col = 0 196 | 197 | if line =~ s:patt.file 198 | let line = getline(a:lineNo + 1) 199 | endif 200 | 201 | if strlen(line) == 0 || line =~ '^--$' 202 | let line = getline(a:lineNo - 1) 203 | endif 204 | 205 | if line =~ '^[1;30m\s\{}\d\{1,}\s\{}[0m[K:\d\{-1,}:' 206 | let col = matchstr(line, ':\zs\d\{1,}:\@=') 207 | endif 208 | 209 | let row = matchstr(line, '^[1;30m\s\{}\zs\d\{1,}\ze\s\{}[\@=') 210 | 211 | return [0, row, col, 0] 212 | endfunction 213 | 214 | " Gets results statistics for the current cursor position 215 | " 216 | function! s:getCurrentStats() 217 | if empty(s:stats) | return {} | endif 218 | if !has_key(s:stats.data, line('.')) | return {} | endif 219 | 220 | let lnum = line('.') 221 | let file = s:stats.data[lnum].file 222 | let filePath = s:stats.data[lnum].filePath 223 | let fileName = strlen(filePath) ? fnamemodify(filePath, ':t') : filePath 224 | let lmatches = s:stats.data[lnum].matches 225 | let fileMsg = 'File(' . file . '/' . s:stats.files . ')' 226 | 227 | if lmatches <= 1 228 | let result = s:stats.data[lnum].result 229 | else 230 | let line = getline('.') 231 | let remaining = ags#pat#matchCount(getline('.'), s:patt.result, col('.') - 1) 232 | let result = s:stats.data[lnum].result - remaining + 1 233 | endif 234 | 235 | let resultMsg = 'Result(' . result . '/' . s:stats.results . ')' 236 | 237 | return { 'file': fileMsg, 'result': resultMsg, 'fileName': fileName } 238 | endfunction 239 | 240 | " Prints search results statistics 241 | " 242 | " {r} - print result info 243 | " {f} - print file info 244 | " {t} - print totals info 245 | function! s:printStats(r, f, t) 246 | if g:ags_no_stats | return | endif 247 | 248 | let msg = s:getCurrentStats() 249 | 250 | if empty(msg) | return | endif 251 | 252 | if a:r 253 | echohl None 254 | redraw | echon msg.result . ' ' 255 | endif 256 | 257 | if !a:r 258 | redraw 259 | endif 260 | 261 | if a:f 262 | echohl MoreMsg 263 | echon msg.file 264 | endif 265 | 266 | if a:t 267 | echohl Underlined 268 | redraw | echom s:stats.results . ' results found in ' . s:stats.files . ' files' 269 | endif 270 | 271 | echohl None 272 | endfunction 273 | 274 | " Sets the {text} into the copy registers 275 | " 276 | function! s:copyText(text) 277 | if &clipboard =~ '\' 278 | call setreg('*', a:text) 279 | call setreg('@', a:text) 280 | elseif &clipboard =~ '\' && has('unnamedplus') 281 | call setreg('+', a:text) 282 | call setreg('@', a:text) 283 | endif 284 | endfunction 285 | 286 | " Returns true if async operations are available and enabled 287 | " 288 | function! s:asyncEnabled() 289 | return g:ags_enable_async && has('nvim') 290 | endfunction 291 | 292 | " Returns the next lines to be displayed given the {data} received 293 | " during an stdout event 294 | " 295 | function! s:getAsyncLines(data) 296 | let first = '' 297 | let last = '' 298 | let lines = [] 299 | let hasLast = strlen(s:lastLine) > 0 300 | 301 | if len(a:data) == 1 302 | if hasLast 303 | let last = a:data[0] 304 | else 305 | let first = a:data[0] 306 | endif 307 | elseif len(a:data) > 1 308 | let first = a:data[0] 309 | let last = a:data[-1] 310 | let lines = a:data[1:-2] 311 | endif 312 | 313 | if s:isNewLine(first) 314 | let lines = [first] + lines 315 | if hasLast 316 | let lines = [s:lastLine] + lines 317 | endif 318 | else 319 | let lines = [s:lastLine . first] + lines 320 | endif 321 | 322 | let s:lastLine = last 323 | 324 | return lines 325 | endfunction 326 | 327 | " Displays an error when an async search fails 328 | " 329 | function! s:onSearchError(job_id, data, event) 330 | call ags#log#warn(string(a:data)) 331 | call ags#run#agAsyncUpdateJobId(a:job_id) 332 | endfunction 333 | 334 | " Populates the search results window during an async search 335 | " 336 | function! s:onSearchOut(job_id, data, event) 337 | let lines = s:getAsyncLines(a:data) 338 | let data = join(lines, "\n") 339 | let start = empty(s:lines) 340 | 341 | call s:showSearchResults(data) 342 | 343 | if start 344 | call ags#log#info('Search started') 345 | call ags#run#agAsyncUpdateJobId(a:job_id) 346 | redraw 347 | else 348 | if ags#run#agAsyncWasKilled() == 0 349 | call ags#log#info('Searching...') 350 | else 351 | call ags#log#info('Search aborted') 352 | endif 353 | call ags#run#agAsyncUpdateJobId(a:job_id) 354 | end 355 | 356 | let s:add = 1 357 | endfunction 358 | 359 | " Prints a message when an async search is done 360 | " 361 | function! s:onSearchDone(job_id, data, event) 362 | call ags#run#agAsyncUpdateJobId(0) 363 | call s:showSearchResults(s:lastLine) 364 | if ags#run#agAsyncWasKilled() == 0 365 | call ags#log#info('Search complete') 366 | endif 367 | call s:afterSearchDone() 368 | endfunction 369 | 370 | " Displays the search {data} in the results window 371 | " 372 | function! s:showSearchResults(data) 373 | let lines = s:processSearchData(a:data) 374 | let args = ags#run#getLastArgs() 375 | let s:lines = s:lines + lines 376 | 377 | if !s:asyncEnabled() && len(lines) == 1 378 | call ags#log#warn(lines[0]) 379 | elseif len(lines) > 1 380 | call s:show(lines, s:add) 381 | endif 382 | endfunction 383 | 384 | " Collects statistics after a search is done 385 | " 386 | function! s:afterSearchDone() 387 | if empty(s:lines) 388 | let args = ags#run#getLastArgs() 389 | call ags#log#warn('No matches for ' . string(args)) 390 | else 391 | let s:stats = s:gatherStatistics(s:lines) 392 | endif 393 | endfunction 394 | 395 | " Returns a string that could be used in the status line to indicate 396 | " the current cursor position within the search results 397 | " 398 | function! ags#get_status_string() 399 | let msg = s:getCurrentStats() 400 | return empty(msg) ? '' : msg.result . ' ' . msg.file . ' ' . msg.fileName 401 | endfunction 402 | 403 | " Performs a search with the specified {args} and according to {cmd}. 404 | " 405 | " {cmd|add} the new results will be added to previous results in the search window 406 | " {cmd|last} ignores {args} and runs the last search 407 | " 408 | function! ags#search(args, cmd) 409 | let last = a:cmd ==# 'last' 410 | let s:add = a:cmd ==# 'add' 411 | let args = '' 412 | let s:lines = [] 413 | 414 | if last && !ags#run#hasLastCmd() 415 | call ags#log#warn("There is no previous search") 416 | return 417 | elseif last 418 | let args = ags#run#getLastArgs() 419 | else 420 | let args = empty(a:args) ? expand('') : a:args 421 | endif 422 | 423 | if s:asyncEnabled() 424 | call ags#run#agAsync(args, 425 | \ function('s:onSearchOut'), 426 | \ function('s:onSearchDone'), 427 | \ function('s:onSearchError')) 428 | else 429 | let data = ags#run#ag(args) 430 | call s:showSearchResults(data) 431 | call s:afterSearchDone() 432 | endif 433 | endfunction 434 | 435 | " Returns the file path for the search results 436 | " relative to {lineNo} 437 | " 438 | function! ags#filePath(lineNo) 439 | let nr = a:lineNo 440 | 441 | while nr >= 0 && getline(nr) !~ s:patt.file 442 | let nr = nr - 1 443 | endw 444 | 445 | return substitute(getline(nr), s:patt.fileCapture, '\1', '') 446 | endfunction 447 | 448 | " Copies to clipboard the file path for the search results 449 | " relative to {lineNo} 450 | " 451 | function! ags#copyFilePath(lineNo, fullPath) 452 | let file = ags#filePath(a:lineNo) 453 | let file = a:fullPath ? fnamemodify(file, ':p') : file 454 | call s:copyText(file) 455 | return 'Copied ' . file 456 | endfunction 457 | 458 | " Removes any delimiters from the yanked text. This function 459 | " should be called from a TextYankPost event. 460 | " 461 | function! ags#cleanYankedText() 462 | let regname = v:event.regname 463 | let regcontents = getreg(regname) 464 | 465 | if empty(regcontents) 466 | return 467 | endif 468 | 469 | let regcontents = substitute(regcontents, s:patt.fileCapture, '\1', 'g') 470 | let regcontents = substitute(regcontents, s:patt.lineColNoCapture, '', 'g') 471 | let regcontents = substitute(regcontents, s:patt.lineNoCapture, '', 'g') 472 | let regcontents = substitute(regcontents, s:patt.resultCapture, '\1', 'g') 473 | 474 | call setreg(regname, regcontents) 475 | endfunction 476 | 477 | " Opens a results file 478 | " 479 | " {lineNo} the line number in the search results buffer 480 | " {flags} window location flags 481 | " {flags|s} opens the file in the search results window 482 | " {flags|a} opens the file above the search results window 483 | " {flags|b} opens the file below the search results window 484 | " {flags|r} opens the file to the right of the search results window 485 | " {flags|l} opens the file to the left of the search results window 486 | " {flags|u} opens the file to in a previously opened window 487 | " {preview} set to true to keep focus in the search results window 488 | " 489 | function! ags#openFile(lineNo, flags, preview) 490 | let path = ags#filePath(a:lineNo) 491 | let pos = s:resultPosition(a:lineNo) 492 | let flags = has_key(s:wflags, a:flags) ? s:wflags[a:flags] : 'above' 493 | let wpos = a:flags == 's' 494 | let reuse = a:flags == 'u' 495 | 496 | if filereadable(path) 497 | call ags#buf#openBuffer(fnameescape(path), flags, wpos, reuse) 498 | call setpos('.', pos) 499 | 500 | if a:preview 501 | exec 'normal zz' 502 | exec 'wincmd p' 503 | endif 504 | endif 505 | endfunction 506 | 507 | " Navigates the next result pattern on the same line 508 | " 509 | function! ags#navigateResultsOnLine() 510 | let line = getline('.') 511 | let result = s:patt.result 512 | if line =~ result 513 | let [bufnum, lnum, col, off] = getpos('.') 514 | call setpos('.', [bufnum, lnum, 0, off]) 515 | call ags#navigateResults() 516 | endif 517 | endfunction 518 | 519 | " Navigates the search results patterns 520 | " 521 | " {flags} search flags (b, B, w, W) 522 | " 523 | function! ags#navigateResults(...) 524 | let flags = a:0 > 0 ? a:1 : 'w' 525 | 526 | call search(s:patt.result, flags) 527 | call matchadd('agsvResultPatternOn', '\%#' . s:patt.result, 999) 528 | call s:printStats(1, 1, 0) 529 | endfunction 530 | 531 | " Navigates the search results file paths 532 | " 533 | " {flags} search flags (b, B, w, W) 534 | function! ags#navigateResultsFiles(...) 535 | let flags = a:0 > 0 ? a:1 : 'w' 536 | call search(s:patt.file, flags) 537 | exec 'normal zt' 538 | call s:printStats(0, 1, 0) 539 | endfunction 540 | 541 | function! ags#quit() 542 | call ags#buf#closeResultsBuffer() 543 | call ags#run#agAsyncStop() 544 | exec winnr('#') . 'wincmd w' 545 | endfunction 546 | 547 | function! ags#usage() 548 | for u in s:usage | echom u | endfor 549 | endfunction 550 | -------------------------------------------------------------------------------- /autoload/ags/buf.vim: -------------------------------------------------------------------------------- 1 | " Buffer name 2 | function! s:resBufName(name) 3 | if g:ags_results_per_tab == 1 4 | return tabpagenr() . '-' . a:name 5 | else 6 | return a:name 7 | endif 8 | endfunction 9 | 10 | " The search results buffer name (view mode) 11 | function! s:agsv() 12 | return s:resBufName('search-results.agsv') 13 | endfunction 14 | 15 | " The search results buffer name (edit mode) 16 | function! s:agse() 17 | return s:resBufName('search-results.agse') 18 | endfunction 19 | 20 | " The last window where a file from search results was opened 21 | let s:lastWin = 0 22 | 23 | " Open buffer commands 24 | let s:cmd = { 25 | \ 'top' : 'to', 26 | \ 'bottom' : 'bo', 27 | \ 'above' : 'abo', 28 | \ 'below' : 'bel', 29 | \ 'far-left' : 'vert to', 30 | \ 'far-right' : 'vert bo', 31 | \ 'left' : 'vert abo', 32 | \ 'right' : 'vert bel' 33 | \ } 34 | 35 | " Opens a window for the buffer with {name} positioned according to {cmd} 36 | " 37 | " {name} the buffer name or file path 38 | " {cmd} the position command 39 | " {winheight} the window heigth 40 | " 41 | function! s:openWin(name, cmd, winheight) 42 | let bufcmd = a:cmd == 'same' ? 'buffer ' : a:cmd . ' sbuffer ' 43 | let wincmd = a:cmd == 'same' ? 'edit ' : a:cmd . ' ' . a:winheight . 'new ' 44 | 45 | if bufexists(a:name) 46 | let nr = bufwinnr(a:name) 47 | if nr == -1 48 | exec bufcmd . bufnr(a:name) 49 | else 50 | call s:focus(nr) 51 | endif 52 | else 53 | execute wincmd . a:name 54 | endif 55 | endfunction 56 | 57 | " Opens a window for the buffer with {name} positioned according to {cmd} 58 | " 59 | " {name} the buffer name or file path 60 | " {cmd} one of the commands from s:cmd 61 | " {sameWin} true to open in the current window 62 | " {lastWin} true to reuse last window opened 63 | " {winheight} the window heigth 64 | " 65 | function! s:open(name, cmd, sameWin, lastWin, winheight) 66 | let cmd = s:cmd[a:cmd] 67 | let sameWin = a:sameWin 68 | let lastWin = a:lastWin 69 | 70 | if !s:lastWin 71 | let s:lastWin = winnr('#') 72 | endif 73 | 74 | if lastWin 75 | let searchWin = s:lastWin == bufwinnr(s:agsv()) || s:lastWin == bufwinnr(s:agse()) 76 | if !searchWin && s:lastWin <= winnr('$') 77 | call s:focus(s:lastWin) 78 | let sameWin = 1 79 | else 80 | let cmd = s:cmd.above 81 | let sameWin = 0 82 | endif 83 | endif 84 | 85 | call s:openWin(a:name, sameWin ? 'same' : cmd, a:winheight) 86 | 87 | if a:name != s:agsv() && a:name != s:agse() 88 | let s:lastWin = winnr() 89 | endif 90 | endfunction 91 | 92 | " Closes the buffer with {name} 93 | " 94 | function! s:close(name) 95 | if bufexists(a:name) 96 | let nr = bufnr(a:name) 97 | if nr > -1 98 | execute 'silent bw ' . nr 99 | endif 100 | endif 101 | endfunction 102 | 103 | function! ags#buf#openBuffer(name, cmd, sameWin, lastWin) 104 | call s:open(a:name, a:cmd, a:sameWin, a:lastWin, '') 105 | endfunction 106 | 107 | " Gets the edit or view search results bufwinnr 108 | " 109 | function! s:getSearchResultsBufwinnr() 110 | let nr = bufwinnr(s:agsv()) 111 | return nr == -1 ? bufwinnr(s:agse()) : nr 112 | endfunction 113 | 114 | " Focuses the window with {nr} 115 | " 116 | " {nr} the window number 117 | " 118 | function! s:focus(nr) 119 | exec a:nr . 'wincmd w' 120 | endfunction 121 | 122 | " Opens the search results buffer 123 | " 124 | function! s:openResultsBuffer(name) 125 | let nr = s:getSearchResultsBufwinnr() 126 | let winheight = g:ags_winheight 127 | 128 | if nr > 0 && nr <= winnr('$') 129 | call s:focus(nr) 130 | exec 'setlocal nomodified' 131 | call s:openWin(a:name, 'same', '') 132 | else 133 | call s:open(a:name, g:ags_winplace, 0, 0, winheight) 134 | endif 135 | endfunction 136 | 137 | " Sets the last window {nr} 138 | " 139 | function! ags#buf#setLastWinnr(nr) 140 | if !s:lastWin 141 | let s:lastWin = a:nr 142 | endif 143 | endfunction 144 | 145 | " Opens the view search results buffer and closes the edit search results 146 | " buffer 147 | " 148 | function! ags#buf#openViewResultsBuffer() 149 | call s:openResultsBuffer(s:agsv()) 150 | call s:close(s:agse()) 151 | endfunction 152 | 153 | " Opens the edit search results buffer and closes the view search results 154 | " buffer 155 | " 156 | function! ags#buf#openEditResultsBuffer() 157 | call s:openResultsBuffer(s:agse()) 158 | call s:close(s:agsv()) 159 | endfunction 160 | 161 | " Opens the edit results buffer if it exists and returns 1; otherwise, it returns 0 162 | " 163 | function! ags#buf#openEditResultsBufferIfExists() 164 | if bufwinnr(s:agse()) != -1 || bufnr(s:agse()) != -1 165 | call s:open(s:agse(), g:ags_winplace, 0, 0, '') 166 | return 1 167 | else 168 | return 0 169 | endif 170 | endfunction 171 | 172 | " Focuses the search results window 173 | " 174 | function! ags#buf#focusResultsWindow() 175 | let nr = s:getSearchResultsBufwinnr() 176 | call s:focus(nr) 177 | endfunction 178 | 179 | " Returns all lines from the view search results buffer 180 | " 181 | function! ags#buf#readViewResultsBuffer() 182 | let name = s:agsv() 183 | if bufexists(name) 184 | let nr = bufnr(name) 185 | return getbufline(nr, 0, '$') 186 | else 187 | return [] 188 | endif 189 | endfunction 190 | 191 | " Returns all lines from the edit search results buffer 192 | " 193 | function! ags#buf#readEditResultsBuffer() 194 | let name = s:agse() 195 | if bufexists(name) 196 | let nr = bufnr(name) 197 | return getbufline(nr, 0, '$') 198 | else 199 | return [] 200 | endif 201 | endfunction 202 | 203 | " Closes the search results buffer 204 | " 205 | function! ags#buf#closeResultsBuffer() 206 | let nr = s:getSearchResultsBufwinnr() 207 | if nr > 0 && nr <= winnr('$') 208 | call s:focus(nr) 209 | exec 'setlocal nomodified' 210 | endif 211 | call s:close(s:agsv()) 212 | call s:close(s:agse()) 213 | endfunction 214 | 215 | " Replaces all lines in buffer with the specified lines 216 | " and places the cursor at the first line 217 | " 218 | function! ags#buf#replaceLines(lines) 219 | exec '%delete' 220 | if len(a:lines) > 0 221 | call append(0, a:lines) 222 | exec 'normal dd' 223 | exec 'normal gg' 224 | endif 225 | endfunction 226 | -------------------------------------------------------------------------------- /autoload/ags/cur.vim: -------------------------------------------------------------------------------- 1 | let s:CUR = {} 2 | 3 | function! s:getCursorCol() 4 | return getpos('.')[2] 5 | endfunction 6 | 7 | function! s:setCursorCol(col) 8 | let pos = getpos('.') 9 | let pos[2] = a:col 10 | call setpos('.', pos) 11 | endfunction 12 | 13 | function! s:incCursorCol(offset) 14 | call s:setCursorCol(s:getCursorCol() + a:offset) 15 | endfunction 16 | 17 | function! s:CUR.New(offset, offsetPat, lineNumberPat) 18 | let curObj = copy(self) 19 | let curObj.offsetPat = a:offsetPat 20 | let curObj.offset = a:offset 21 | let curObj.lineNumberPat = a:lineNumberPat 22 | return curObj 23 | endfunction 24 | 25 | function! s:CUR.isLineNumber() 26 | let line = getline('.') 27 | let mlen = len(matchstr(line, self.lineNumberPat)) 28 | return mlen == self.offset 29 | endfunction 30 | 31 | function! s:CUR.isOutside() 32 | return s:getCursorCol() <= self.offset 33 | endfunction 34 | 35 | function! s:CUR.moveToStart() 36 | if self.isLineNumber() 37 | let line = getline('.') 38 | let line = substitute(line, self.offsetPat, '', '') 39 | let spaces = len(matchstr(line, '^\s\{}')) 40 | 41 | call s:setCursorCol(self.offset + spaces + 1) 42 | else 43 | exec 'normal ^' 44 | endif 45 | exec 'startinsert' 46 | endfunction 47 | 48 | function! s:CUR.moveToStartIfOutside(offset) 49 | if self.isOutside() && self.isLineNumber() 50 | call s:setCursorCol(self.offset + 1) 51 | else 52 | call s:incCursorCol(a:offset) 53 | endif 54 | exec 'startinsert' 55 | endfunction 56 | 57 | " Creates a cursor object 58 | " 59 | function! ags#cur#make(offset, offsetPat, lineNumberPat) 60 | return s:CUR.New(a:offset, a:offsetPat, a:lineNumberPat) 61 | endfunction 62 | -------------------------------------------------------------------------------- /autoload/ags/edit.vim: -------------------------------------------------------------------------------- 1 | " Search data map, used for determining the file and line number where a search 2 | " result line belongs 3 | let s:dataMap = {} 4 | 5 | " Last edited search results, used to determine which lines have changed 6 | let s:editLines = [] 7 | 8 | " Line number offset 9 | " The actual file line starts after the line number offset 10 | let s:offset = 0 11 | 12 | " Line number offset pattern 13 | " Used to remove the line numbers from the file line 14 | let s:offsetPat = '^' 15 | 16 | " Cursor operations 17 | let s:cur = {} 18 | 19 | " Regex pattern functions 20 | let s:pat = function('ags#pat#mkpat') 21 | let s:gsub = function('ags#pat#gsub') 22 | let s:sub = function('ags#pat#sub') 23 | 24 | " Clears undo history 25 | " 26 | function! s:clearUndoHistory() 27 | let prev = &undolevels 28 | set undolevels=-1 29 | exe "normal a \\" 30 | let &undolevels = prev 31 | endfunction 32 | 33 | " Prepares the search {lines} for display in editable mode 34 | " 35 | function! s:processLinesForEdit(lines) 36 | if empty(a:lines) | return [] | endif 37 | 38 | let lines = [] 39 | 40 | let lineColPat = s:pat('^:\lineStart:\([ 0-9]\{-1,}\):lineColEnd:') 41 | let linePat = s:pat('^:\lineStart:\([ 0-9]\{-1,}\):lineEnd:') 42 | let resultDelimPat = s:pat(':resultStart::hlDelim:\(.\{-1,}\):hlDelim::end:') 43 | let resultPat = s:pat(':resultStart:\(.\{-1,}\):end:') 44 | let lineSubst = g:ags_edit_show_line_numbers ? '\1' : '' 45 | 46 | for line in a:lines 47 | let line = substitute(line, lineColPat, lineSubst, '') 48 | let line = substitute(line, linePat, lineSubst, '') 49 | let line = substitute(line, resultDelimPat, '\1', 'g') 50 | let line = substitute(line, resultPat, '\1', 'g') 51 | call add(lines, line) 52 | endfor 53 | 54 | return lines 55 | endfunction 56 | 57 | " Calculates the offset from the begining of the edit line to the file line. 58 | " This is the same as the number line width. 59 | " 60 | function! s:calculateOffset(lines) 61 | return (len(a:lines) < 2 || !g:ags_edit_show_line_numbers) ? 0 : strlen(matchstr(a:lines[1], '^\s\{}\d\{}\s')) 62 | endfunction 63 | 64 | " Makes a data hash map from {lines} 65 | " returns a dictionary of the form searchLineNumber : [ filePath, fileLineNumer ] 66 | " 67 | function! s:makeDataMap(lines) 68 | let data = {} 69 | let file = '' 70 | let idx = 0 71 | let linePat = s:pat('^:lineStart:\s\{}\zs\d\{1,}\ze\s\{}[\@=') 72 | let filePat = s:pat('^:file:') 73 | 74 | for line in a:lines 75 | if line =~ filePat 76 | 77 | let file = substitute(line, filePat, '\1', '') 78 | let data[idx] = { 'file': file, 'row': 0 } 79 | elseif line =~ linePat 80 | 81 | let row = matchstr(line, linePat) 82 | let data[idx] = { 'file': file, 'row': row } 83 | else 84 | let data[idx] = { 'file': file, 'row': 0 } 85 | endif 86 | 87 | let idx = idx + 1 88 | endfor 89 | 90 | return data 91 | endfunction 92 | 93 | " Gets the changed lines from the search results window 94 | " 95 | " Returns a dictionary of the form { file: lineInfo } 96 | " 97 | function! s:changes() 98 | let olines = s:editLines 99 | let elines = ags#buf#readEditResultsBuffer() 100 | let changes = {} 101 | let idx = 0 102 | 103 | if len(olines) != len(elines) | return [ 1, changes ] | endif 104 | 105 | while idx < len(olines) 106 | let eline = elines[idx] 107 | let oline = olines[idx] 108 | 109 | if eline !=# oline 110 | let file = s:dataMap[idx].file 111 | 112 | if !has_key(changes, file) 113 | let changes[file] = [] 114 | endif 115 | 116 | call add(changes[file], { 117 | \ 'fileLine' : s:dataMap[idx].row, 118 | \ 'fileData' : substitute(eline, s:offsetPat, '', ''), 119 | \ 'fileDataPrev' : substitute(oline, s:offsetPat, '', ''), 120 | \ 'editLine' : idx, 121 | \ 'editData' : eline 122 | \ }) 123 | endif 124 | 125 | let idx = idx + 1 126 | endwhile 127 | 128 | return [ 0, changes ] 129 | endfunction 130 | 131 | " Writes the search results window changes to their corresponding files 132 | " 133 | function! ags#edit#write() 134 | let olines = s:editLines 135 | let elines = ags#buf#readEditResultsBuffer() 136 | let [ err, allChanges ] = s:changes() 137 | let fileCount = 0 138 | let lineCount = 0 139 | let skipFileCount = 0 140 | let skipLineCount = 0 141 | 142 | if err 143 | call ags#log#error('Original number of lines has changed. Write cancelled.') 144 | return 145 | endif 146 | 147 | for [file, fileChanges] in items(allChanges) 148 | let lines = readfile(file, 'b') 149 | let cnt = 0 150 | let skipCnt = 0 151 | let skip = g:ags_edit_skip_if_file_changed 152 | 153 | for change in fileChanges 154 | if change.fileLine == 0 | continue | endif 155 | 156 | if skip && lines[change.fileLine - 1] !=# change.fileDataPrev 157 | let skipCnt = skipCnt + 1 158 | 159 | let eline = getline(change.editLine + 1) 160 | let enum = matchstr(eline, s:offsetPat) 161 | let nline = enum . lines[change.fileLine - 1] 162 | 163 | let s:editLines[change.editLine] = nline 164 | call setline(change.editLine + 1, nline) 165 | else 166 | let lines[change.fileLine - 1] = change.fileData 167 | let cnt = cnt + 1 168 | let s:editLines[change.editLine] = change.editData 169 | endif 170 | endfor 171 | 172 | if skipCnt > 0 173 | let skipFileCount = skipFileCount + 1 174 | let skipLineCount = skipLineCount + skipCnt 175 | endif 176 | 177 | if cnt > 0 178 | let fileCount = fileCount + 1 179 | let lineCount = lineCount + cnt 180 | let path = fnameescape(file) 181 | 182 | if filewritable(path) 183 | execute 'silent doautocmd FileWritePre ' . path 184 | call writefile(lines, path, 'b') 185 | execute 'silent doautocmd FileWritePost ' . path 186 | endif 187 | endif 188 | endfor 189 | 190 | if lineCount == 0 191 | call ags#log#info('All files up to date') 192 | elseif skipLineCount == 0 193 | call ags#log#info('Updated ' . lineCount . ' lines in ' . fileCount . ' files') 194 | else 195 | call ags#log#info( 196 | \ 'Updated ' . 197 | \ lineCount . ' lines in ' . fileCount . ' files. ' . 198 | \ 'Skipped ' . 199 | \ skipLineCount . ' lines in ' . skipFileCount . ' files.') 200 | endif 201 | 202 | call ags#buf#focusResultsWindow() 203 | exec 'setlocal nomodified' 204 | endfunction 205 | 206 | " Makes the search results window editable 207 | " 208 | function! ags#edit#show() 209 | if ags#buf#openEditResultsBufferIfExists() | return | endif 210 | 211 | let lines = ags#buf#readViewResultsBuffer() 212 | let s:dataMap = s:makeDataMap(lines) 213 | 214 | let lines = s:processLinesForEdit(lines) 215 | let s:editLines = lines 216 | 217 | let s:offset = s:calculateOffset(lines) 218 | let s:offsetPat = '^.\{' . string(s:offset) . '}' 219 | let s:cur = ags#cur#make(s:offset, s:offsetPat, '^\s\{}\d\{}\s') 220 | 221 | if empty(lines) 222 | call ags#log#warn('There are no search results to edit') 223 | return 224 | endif 225 | 226 | let pos = getpos('.') 227 | call ags#buf#openEditResultsBuffer() 228 | call ags#buf#replaceLines(lines) 229 | call s:clearUndoHistory() 230 | exec 'setlocal nomodified' 231 | call setpos('.', pos) 232 | endfunction 233 | 234 | " Moves the cursor to the start of the file line 235 | " 236 | function! ags#edit#moveCursorToStart() 237 | call s:cur.moveToStart() 238 | endfunction 239 | 240 | " Moves the cursor to the start of the file line if it is outside of the 241 | " editable file line 242 | " 243 | function! ags#edit#moveCursorToStartIfOut(offset) 244 | call s:cur.moveToStartIfOutside(a:offset) 245 | endfunction 246 | -------------------------------------------------------------------------------- /autoload/ags/log.vim: -------------------------------------------------------------------------------- 1 | " Logs a {message} highlighted with {hl} 2 | " 3 | function! s:log(message, hl) 4 | exec 'echohl ' . a:hl 5 | echom a:message 6 | echohl None 7 | endfunction 8 | 9 | " Writes a {message} highlighted with {hl} 10 | " 11 | function! s:write(message, hl) 12 | exec 'echohl ' . a:hl 13 | redraw | echo a:message 14 | echohl None 15 | endfunction 16 | 17 | " Logs an error with {message} 18 | " 19 | function! ags#log#error(message) 20 | call s:log(a:message, 'Error') 21 | endfunction 22 | 23 | " Logs an info {message} 24 | " 25 | function! ags#log#info(message) 26 | call s:log(a:message, 'MoreMsg') 27 | endfunction 28 | 29 | " Logs a warning {message} 30 | " 31 | function! ags#log#warn(message) 32 | call s:log(a:message, 'WarningMsg') 33 | endfunction 34 | 35 | " Logs a plain {message} 36 | " 37 | function! ags#log#plain(message) 38 | call s:log(a:message, 'None') 39 | endfunction 40 | 41 | " Writes an error with {message} 42 | " 43 | function! ags#log#errorw(message) 44 | call s:write(a:message, 'Error') 45 | endfunction 46 | 47 | " Writes an info {message} 48 | " 49 | function! ags#log#infow(message) 50 | call s:write(a:message, 'MoreMsg') 51 | endfunction 52 | 53 | " Writes a warning {message} 54 | " 55 | function! ags#log#warnw(message) 56 | call s:write(a:message, 'WarningMsg') 57 | endfunction 58 | 59 | " Writes a plain {message} 60 | " 61 | function! ags#log#plainw(message) 62 | call s:write(a:message, 'None') 63 | endfunction 64 | -------------------------------------------------------------------------------- /autoload/ags/pat.vim: -------------------------------------------------------------------------------- 1 | " Types of patterns 2 | " - file path 3 | " - line number 4 | " - column number 5 | " - search result 6 | " - search result highlighted 7 | 8 | let s:end = '[0m[K' 9 | let s:col = ':\d\{-1,}:' 10 | let s:fileStart = '[1;31m' 11 | let s:file = s:fileStart . '\(.\{-1,}\)' . s:end 12 | let s:lineStart = '[1;30m' 13 | let s:lineEnd = s:end . '-' 14 | let s:lineColEnd = s:end . s:col 15 | let s:resultStart = '[32;40m' 16 | let s:hlDelim = '[#m' 17 | 18 | function! s:esc(pat) 19 | return substitute(a:pat, '\[', '\\[', 'g') 20 | endfunction 21 | 22 | function! s:val(name) 23 | return has_key(s:, a:name) ? s:[a:name] : a:name 24 | endfunction 25 | 26 | function! s:valEsc(name) 27 | return has_key(s:, a:name) ? s:esc(s:[a:name]) : a:name 28 | endfunction 29 | 30 | function! ags#pat#matchCount(expr, pat, index) 31 | if strlen(a:expr) == 0 | return 0 | endif 32 | 33 | let cnt = 0 34 | let idx = a:index 35 | 36 | while match(a:expr, a:pat, idx) > -1 && idx < strlen(a:expr) 37 | let cnt = cnt + 1 38 | let idx = match(a:expr, a:pat, idx) + strlen(matchstr(a:expr, a:pat, idx)) 39 | endw 40 | 41 | return cnt 42 | endfunction 43 | 44 | function! ags#pat#mkpat(...) 45 | let key = '' 46 | 47 | for p in a:000 48 | let key .= has_key(s:, p) ? s:[p] : p 49 | endfor 50 | 51 | if strlen(key) == 0 | return '' | endif 52 | 53 | let pat = key 54 | let pat = substitute(pat, ':\([a-zA-Z]\{-1,}\):', '\=s:val(submatch(1))', 'g') 55 | let pat = substitute(pat, ':\\\([a-zA-Z]\{-1,}\):', '\=s:valEsc(submatch(1))', 'g') 56 | 57 | return pat 58 | endfunction 59 | 60 | function! ags#pat#sub(expr, pat, sub) 61 | return substitute(a:expr, ags#pat#mkpat(a:pat), ags#pat#mkpat(a:sub), '') 62 | endfunction 63 | 64 | function! ags#pat#gsub(expr, pat, sub) 65 | return substitute(a:expr, ags#pat#mkpat(a:pat), ags#pat#mkpat(a:sub), 'g') 66 | endfunction 67 | -------------------------------------------------------------------------------- /autoload/ags/run.vim: -------------------------------------------------------------------------------- 1 | " Default ag executable path 2 | let s:exe = 'ag' 3 | 4 | " Last command 5 | let s:last = '' 6 | 7 | " Last args 8 | let s:lastArgs = '' 9 | 10 | let s:id = '' 11 | let s:job_killed = 0 12 | 13 | function! s:remove(args, name) 14 | return substitute(a:args, '\s\{}' . a:name . '\(=\S\{}\)\?', '', 'g') 15 | endfunction 16 | 17 | function! s:exists(args, name) 18 | return a:args =~ '\s\{1,}' . a:name . '\(=\|\s\)' 19 | endfunction 20 | 21 | function! s:cmd(args) 22 | let cmd = has_key(g:, 'ags_agexe') ? g:ags_agexe . ' ' : s:exe 23 | let args = a:args 24 | 25 | for [ key, arg ] in items(g:ags_agargs) 26 | let value = arg[0] 27 | let short = arg[1] 28 | let exists = s:exists(args, key) || (strlen(short) > 0 && s:exists(args, short)) 29 | 30 | if exists 31 | continue 32 | endif 33 | 34 | if type(value) == type([]) 35 | for v in value 36 | let cmd .= ' ' . key . '=' . v 37 | endfor 38 | else 39 | if value =~ '^g:' 40 | let value = substitute(value, '^g:', '', '') 41 | let value = get(g:, value, 0) 42 | endif 43 | 44 | let op = strlen(value) == 0 ? '' : '=' 45 | let cmd .= ' ' . key . op . value 46 | endif 47 | endfor 48 | 49 | let cmd = cmd . ' ' . args 50 | let s:last = cmd 51 | 52 | return cmd 53 | endfunction 54 | 55 | " Runs an ag search with the given {args} 56 | " 57 | function! ags#run#ag(args) 58 | let s:lastArgs = a:args 59 | return system(s:cmd(a:args)) 60 | endfunction 61 | 62 | " Runs an ag search with the givent {args} async. 63 | " The functions {onOut}, {onExit}, and, {onError} will be used to 64 | " communicate with the async process 65 | " 66 | function! ags#run#agAsync(args, onOut, onExit, onError) 67 | let s:lastArgs = a:args 68 | let pat = '"\([^"]\+\)"\|\([^ ]\+\)' 69 | let idx = 0 70 | let cmd = s:cmd(a:args) 71 | let len = strlen(cmd) 72 | let lst = [] 73 | 74 | while match(cmd, pat, idx) > -1 && idx < len 75 | let mat = matchlist(cmd, pat, idx) 76 | let cur = mat[1] 77 | 78 | if strlen(cur) == 0 79 | let cur = mat[0] 80 | endif 81 | 82 | let idx = match(cmd, pat, idx) + strlen(mat[0]) 83 | call add(lst, cur) 84 | endwhile 85 | 86 | if s:id 87 | silent! call jobstop(s:id) 88 | endif 89 | 90 | let s:job_killed = 0 91 | let s:id = jobstart(lst, { 92 | \ 'on_stderr': a:onError, 93 | \ 'on_stdout': a:onOut, 94 | \ 'on_exit': a:onExit 95 | \ }) 96 | endfunction 97 | 98 | function! ags#run#agAsyncWasKilled() 99 | if s:job_killed == 1 100 | return 1 101 | endif 102 | endfunction 103 | 104 | function! ags#run#agAsyncUpdateJobId(job_id) 105 | let s:id = a:job_id 106 | endfunction 107 | 108 | function! ags#run#agAsyncStop() 109 | if s:id != 0 110 | let s:job_killed = 1 111 | call jobstop(s:id) 112 | endif 113 | endfunction 114 | 115 | " Runs the last ag search 116 | " 117 | function! ags#run#runLastCmd() 118 | return ags#run#ag(s:lastArgs) 119 | endfunction 120 | 121 | " Returns true if an ag search has been performed 122 | " 123 | function! ags#run#hasLastCmd() 124 | return !empty(s:lastArgs) 125 | endfunction 126 | 127 | " Return the arguments of the last command 128 | " 129 | function! ags#run#getLastArgs() 130 | return s:lastArgs 131 | endfunction 132 | 133 | " Displays the last ag command executed 134 | " 135 | function! ags#run#getLastCmd() 136 | echom s:last 137 | endfunction 138 | -------------------------------------------------------------------------------- /doc/ags.txt: -------------------------------------------------------------------------------- 1 | *ags.txt* Ag search plugin for Vim *Ags* 2 | 3 | ============================================================= 4 | # # 5 | # _____ ____ ______ # 6 | # \__ \ / ___\/ ___/ # 7 | # / __ \_/ /_/ >___ \ # 8 | # (____ /\___ /____ > # 9 | # \//_____/ \/ # 10 | # # 11 | ============================================================= 12 | 13 | CONTENTS *ags-contents* 14 | 15 | 1. Intro.............................|ags-intro| 16 | 2. EditMode..........................|ags-edit-mode| 17 | 3. Options...........................|ags-options| 18 | 4. Commands..........................|ags-commands| 19 | 5. Mappings..........................|ags-mappings| 20 | 6. Statusline........................|ags-statusline| 21 | 7. Limitations.......................|ags-limitations| 22 | 23 | ============================================================= 24 | INTRO *ags-intro* 25 | 26 | Ags is a plugin that allows accessing the search 27 | functionality of the silver searcher (AG) from Vim. 28 | It focuses on displaying the search results in a clear 29 | format and provides several key mappings for easy 30 | navigation through them. In addition there is an edit 31 | mode that allows for search and replace in multiple 32 | files. 33 | 34 | ============================================================= 35 | EDIT MODE *ags-edit-mode* 36 | 37 | The edit mode can be triggered by pressing E in the search 38 | window after running a search, or by invoking the 39 | |:AgsEditSearchResults| command. This will make the results 40 | window editable and can be used for example to do a search 41 | and replace in multiple files. Writing the search 42 | buffer will cause the changes to be written to files. To undo 43 | any changes first undo them in the search window and then 44 | write the search buffer again. 45 | 46 | ============================================================= 47 | OPTIONS *ags-options* 48 | 49 | |ags_loaded|.....................Disable the plugin 50 | |ags_agexe|......................The ag executable path 51 | |ags_agmaxcount|.................The --max-count ag param 52 | |ags_agcontext|..................The --context ag param 53 | |ags_enable_async|...............Enable async search if supported 54 | |ags_no_stats|...................Disable showing statistics 55 | in the command bar 56 | |ags_results_per_tab|............Enable search result window per 57 | tab 58 | |ags_stats_max_ln|...............The maximum number of lines 59 | before disabling statistics 60 | |ags_agargs|.....................The predefined ag args 61 | |ags_edit_show_line_numbers|.....Show line numbers in edit mode 62 | |ags_edit_skip_if_file_changed|..In edit mode skip write of any 63 | lines that changed outside vim 64 | |ags_highlight|..................Highlight groups used when displaying 65 | the search results 66 | |g:ags_winheight|................The search results window 67 | height 68 | |g:ags_winplace|.................The placement of the search 69 | results window. One of 70 | 71 | ------------------------------------------------------------- 72 | Detailed options:~ 73 | 74 | *ags_agexe* 75 | This is the ag executable path 76 | > 77 | let g:ags_agexe = 'ag' 78 | < 79 | 80 | *ags_agmaxcount* 81 | This is the max-count parameter to be passed to ag 82 | > 83 | let g:ags_agmaxcount = 2000 84 | < 85 | 86 | *ags_agcontext* 87 | This is the context parameter to be passed to ag 88 | > 89 | let g:ags_agcontext = 3 90 | < 91 | 92 | *ags_enable_async* 93 | This is a flag that determines whether to run the ag search 94 | asynchronously when supported (currently only in neovim) 95 | > 96 | let g:ags_enable_async = 1 97 | < 98 | 99 | *ags_results_per_tab* 100 | This determines whether to work with a single results window or 101 | with one per tab. 102 | 103 | let g:ags_results_per_tab = 0 104 | 105 | 106 | *ags_no_stats* 107 | Flag to disable showing statistics in the command bar. This 108 | could be used when displaying statistics in a status line plugin 109 | such as lightline. In that case use ags#get_status_string to get 110 | status line information. 111 | > 112 | let g:ags_no_stats = 0 113 | < 114 | 115 | *ags_stats_max_ln* 116 | This is the maximum number of lines before disabling the 117 | statistics info that shows when navigating results in the 118 | search results window. When the number of lines returned by 119 | ag is larger than the value of this variable the statistics 120 | are disabled to improve performance. 121 | > 122 | let g:ags_stats_max_ln = 5000 123 | < 124 | 125 | *ags_edit_show_line_numbers* 126 | This enables showing file line numbers in edit mode as visual 127 | cues. The file numbers as well as file paths in edit mode are 128 | not meant to be edited and could result in unwanted changes if 129 | removed. 130 | > 131 | let g:ags_edit_show_line_numbers = 0 132 | < 133 | 134 | *ags_edit_skip_if_file_changed* 135 | When writing files from edit mode this allows to skip writing 136 | lines that have changed from outside vim. This could have 137 | mixed results as some lines will be changed and others not. 138 | However, the search results window will be updated with the 139 | file lines exactly as they appear in the file. 140 | > 141 | let g:ags_edit_skip_if_file_changed = 0 142 | < 143 | 144 | *ags_highlight* 145 | Ags uses these highlight groups: 146 | 147 | agsvFilePath 148 | File paths 149 | 150 | agsvLineNum 151 | Line numbers 152 | 153 | agsvLineNumMatch 154 | Line numbers for lines containing search matches 155 | 156 | agsvResultPattern 157 | Search match 158 | 159 | agsvResultPatternOn 160 | Current search match 161 | 162 | 163 | *ags_winheight* 164 | This determines the search results window height. It defaults 165 | to half the screen. 166 | > 167 | let g:ags_winheight = '' 168 | < 169 | 170 | *ags_winplace* 171 | This determines the placement of the search results window. It 172 | defaults to |bottom|. Possible values are: |top|, |bottom|, |above|, 173 | |below|, |far-left|, |far-right|, |left|, |right|. 174 | > 175 | let g:ags_winplace = 'bottom' 176 | < 177 | 178 | *ags_agargs* 179 | This variable contains the predefined search arguments 180 | used to ensure that the search results are highlighted 181 | properly in Vim. This can be customized but the color 182 | arguments should not be modified though since they are used by 183 | Ags to determine where the file paths and search results are 184 | within the data received from the search command. They are not 185 | used when displaying the search results. For changing the 186 | colors used to display the search results see |ags_highlight|. 187 | > 188 | let g:ags_agargs = { 189 | \ '--break' : [ '', '' ], 190 | \ '--color' : [ '', '' ], 191 | \ '--color-line-number' : [ '"1;30"', '' ], 192 | \ '--color-match' : [ '"32;40"', '' ], 193 | \ '--color-path' : [ '"1;31"', '' ], 194 | \ '--column' : [ '', '' ], 195 | \ '--context' : [ 'g:ags_agcontext', '-C' ], 196 | \ '--filename' : [ '', '' ], 197 | \ '--group' : [ '', '' ], 198 | \ '--heading' : [ '', '-H' ], 199 | \ '--max-count' : [ 'g:ags_agmaxcount', '-m' ], 200 | \ '--numbers' : [ '', '' ] 201 | \ } 202 | < 203 | 204 | ============================================================= 205 | COMMANDS *ags-commands* 206 | 207 | *:Ags* 208 | :Ags [{file-type}] [{options}] {pattern} [{directory}] 209 | 210 | Runs a recursive search in {directory} for 211 | {pattern}. If {pattern} is not specified it 212 | will be set to the word under cursor. The 213 | {directory} will default to the current directory. 214 | {options} and {file-type} are passed directly to 215 | the ag executable. In addition a few other options, 216 | as set in |ags_agargs|, will be specified for display purposes. 217 | 218 | For example 219 | :Ags --js Reducer 220 | *:AgsAdd* 221 | :AgsAdd [{file-type}] [{options}] {pattern} [{directory}] 222 | 223 | Like |:Ags| but the matches are appended to the 224 | current search results. 225 | 226 | *:AgsLast* 227 | :AgsLast 228 | Runs the last search. This could be used after entering 229 | edit mode to return to view mode. 230 | 231 | *:AgsQuit* 232 | :AgsQuit 233 | 234 | Closes the search results window whether in edit or 235 | view mode. 236 | 237 | *:AgsEditSearchResults* 238 | :AgsEditSearchResults 239 | 240 | Enters edit mode if the search results is open in 241 | view mode. 242 | 243 | *:AgsShowLastCommand* 244 | :AgsShowLastCommand 245 | 246 | Displays the command for the last search. 247 | 248 | ============================================================= 249 | MAPPINGS *ags-mappings* 250 | 251 | Once inside the search results window:~ 252 | 253 | p - navigate file paths forward 254 | P - navigate files paths backwards 255 | r - navigate results forward 256 | R - navigate results backwards 257 | a - display the file path for current results 258 | c - copy to clipboard the file path for current results 259 | E - enter edit mode 260 | 261 | oa - open file above the results window 262 | ob - open file below the results window 263 | ol - open file to the left of the results window 264 | or - open file to the right of the results window 265 | os - open file in the results window 266 | ou - open file in a previously opened window 267 | xu - open file in a previously opened window and close the search results 268 | - open file in a previously opened window 269 | 270 | q - close the search results window 271 | u - displays these key mappings 272 | 273 | ============================================================= 274 | STATUSLINE *ags-statusline* 275 | 276 | To display search results info in the status line use ags#get_status_string() 277 | See also |ags_no_stats| and |ags_stats_max_ln| 278 | 279 | ============================================================= 280 | LIMITATIONS *ags-limitations* 281 | 282 | Performing a search could be slow if the search returns too many 283 | lines. To improve performance in such cases set the context argument to 0 or 1. 284 | 285 | For example 286 | :Ags -C 0 search-pattern 287 | 288 | The default is 3 and can be overwritten via |g:ags_agcontext| setting. 289 | 290 | In edit mode lines can be changed but no lines can be added or removed. 291 | Also entering edit mode may lag if there are too many lines in the 292 | search results window. The context argument mentioned above will help here as well. 293 | 294 | ============================================================= 295 | vim:tw=78:et:ft=help:norl: 296 | -------------------------------------------------------------------------------- /ftdetect/agse.vim: -------------------------------------------------------------------------------- 1 | augroup agsResultsWindowEdit 2 | autocmd! 3 | autocmd BufNewFile,BufRead,BufEnter *.agse set filetype=agse 4 | autocmd BufWriteCmd *.agse call ags#edit#write() 5 | augroup END 6 | 7 | -------------------------------------------------------------------------------- /ftdetect/agsv.vim: -------------------------------------------------------------------------------- 1 | augroup agsResultsWindowView 2 | autocmd! 3 | autocmd BufNewFile,BufRead,BufEnter *.agsv set filetype=agsv 4 | autocmd BufEnter,BufWinEnter *.agsv call ags#navigateResultsOnLine() 5 | autocmd TextYankPost *.agsv call ags#cleanYankedText() 6 | augroup END 7 | -------------------------------------------------------------------------------- /ftplugin/agse.vim: -------------------------------------------------------------------------------- 1 | setlocal buftype=acwrite 2 | setlocal bufhidden=hide 3 | setlocal nolist 4 | setlocal noswapfile 5 | setlocal conceallevel=3 6 | setlocal concealcursor=nvic 7 | 8 | if g:ags_edit_show_line_numbers 9 | command! -buffer AgsEditMoveCursorToStartBefore call ags#edit#moveCursorToStartIfOut(0) 10 | command! -buffer AgsEditMoveCursorToStartAfter call ags#edit#moveCursorToStartIfOut(1) 11 | command! -buffer AgsEditMoveCursorToStart call ags#edit#moveCursorToStart() 12 | 13 | nnoremap i :AgsEditMoveCursorToStartBefore 14 | nnoremap a :AgsEditMoveCursorToStartAfter 15 | nnoremap I :AgsEditMoveCursorToStart 16 | endif 17 | -------------------------------------------------------------------------------- /ftplugin/agsv.vim: -------------------------------------------------------------------------------- 1 | setlocal buftype=nofile 2 | setlocal conceallevel=3 3 | setlocal concealcursor=nvic 4 | 5 | command! -buffer AgsNextResult call ags#navigateResults('W') 6 | command! -buffer AgsPrevResult call ags#navigateResults('bW') 7 | command! -buffer AgsNextFile call ags#navigateResultsFiles('W') 8 | command! -buffer AgsPrevFile call ags#navigateResultsFiles('bW') 9 | 10 | command! -buffer AgsFilePathShow echom ags#filePath(line('.')) 11 | command! -buffer AgsFilePathCopy echom ags#copyFilePath(line('.'), 1) 12 | 13 | command! -buffer AgsOpenFileAbove call ags#openFile(line('.'), 'a', 0) 14 | command! -buffer AgsOpenFileBelow call ags#openFile(line('.'), 'b', 0) 15 | command! -buffer AgsOpenFileLeft call ags#openFile(line('.'), 'l', 0) 16 | command! -buffer AgsOpenFileRight call ags#openFile(line('.'), 'r', 0) 17 | command! -buffer AgsOpenFileSame call ags#openFile(line('.'), 's', 0) 18 | command! -buffer AgsOpenFileReuse call ags#openFile(line('.'), 'u', 0) 19 | command! -buffer AgsOpenFileReuseAndQuit call ags#openFile(line('.'), 'u', 0) | call ags#quit() 20 | 21 | command! -buffer AgsViewFileAbove call ags#openFile(line('.'), 'a', 1) 22 | command! -buffer AgsViewFileBelow call ags#openFile(line('.'), 'b', 1) 23 | command! -buffer AgsViewFileLeft call ags#openFile(line('.'), 'l', 1) 24 | command! -buffer AgsViewFileRight call ags#openFile(line('.'), 'r', 1) 25 | command! -buffer AgsViewFileSame call ags#openFile(line('.'), 's', 1) 26 | command! -buffer AgsViewFileReuse call ags#openFile(line('.'), 'u', 1) 27 | 28 | command! -buffer AgsUsage call ags#usage() 29 | 30 | nnoremap r :AgsNextResult 31 | nnoremap R :AgsPrevResult 32 | nnoremap p :AgsNextFile 33 | nnoremap P :AgsPrevFile 34 | 35 | nnoremap a :AgsFilePathShow 36 | nnoremap c :AgsFilePathCopy 37 | nnoremap E :AgsEditSearchResults 38 | nnoremap u :AgsUsage 39 | nnoremap q :AgsQuit 40 | 41 | nnoremap oa : AgsOpenFileAbove 42 | nnoremap ob : AgsOpenFileBelow 43 | nnoremap ol : AgsOpenFileLeft 44 | nnoremap or : AgsOpenFileRight 45 | nnoremap os : AgsOpenFileSame 46 | nnoremap ou : AgsOpenFileReuse 47 | nnoremap : AgsOpenFileReuse 48 | 49 | nnoremap xu : AgsOpenFileReuseAndQuit 50 | 51 | nnoremap OA : AgsViewFileAbove 52 | nnoremap OB : AgsViewFileBelow 53 | nnoremap OL : AgsViewFileLeft 54 | nnoremap OR : AgsViewFileRight 55 | nnoremap OS : AgsOpenFileSame 56 | nnoremap OU : AgsViewFileReuse 57 | -------------------------------------------------------------------------------- /plugin/ags.vim: -------------------------------------------------------------------------------- 1 | if exists('g:ags_loaded') || &cp || v:version < 700 | finish | endif 2 | 3 | let g:ags_loaded = 1 4 | 5 | let g:ags_agexe = get(g:, 'ags_agexe', 'ag') 6 | let g:ags_agmaxcount = get(g:, 'ags_agmaxcount', 2000) 7 | let g:ags_agcontext = get(g:, 'ags_agcontext', 3) 8 | let g:ags_stats_max_ln = get(g:, 'ags_stats_max_ln', 5000) 9 | let g:ags_edit_skip_if_file_changed = get(g:, 'ags_edit_skip_if_file_changed', 0) 10 | let g:ags_edit_show_line_numbers = get(g:, 'ags_edit_show_line_numbers', 0) 11 | let g:ags_no_stats = get(g:, 'ags_no_stats', 0) 12 | let g:ags_winheight = get(g:, 'ags_winheight', '') 13 | let g:ags_winplace = get(g:, 'ags_winplace', 'bottom') 14 | let g:ags_enable_async = get(g:, 'ags_enable_async', 1) 15 | let g:ags_results_per_tab = get(g:, 'ags_results_per_tab', 0) 16 | 17 | if !exists('g:ags_agargs') 18 | " Predefined search arguments 19 | " arg : [ value, short-name ] 20 | let g:ags_agargs = { 21 | \ '--break' : [ '', '' ], 22 | \ '--color' : [ '', '' ], 23 | \ '--color-line-number' : [ '"1;30"', '' ], 24 | \ '--color-match' : [ '"32;40"', '' ], 25 | \ '--color-path' : [ '"1;31"', '' ], 26 | \ '--column' : [ '', '' ], 27 | \ '--context' : [ 'g:ags_agcontext', '-C' ], 28 | \ '--filename' : [ '', '' ], 29 | \ '--group' : [ '', '' ], 30 | \ '--heading' : [ '', '-H' ], 31 | \ '--max-count' : [ 'g:ags_agmaxcount', '-m' ], 32 | \ '--numbers' : [ '', '' ] 33 | \ } 34 | endif 35 | 36 | command! -nargs=* -complete=file Ags call ags#search(, '') 37 | command! -nargs=* -complete=file AgsAdd call ags#search(, 'add') 38 | command! -nargs=0 AgsLast call ags#search(, 'last') 39 | command! -nargs=0 AgsEditSearchResults call ags#edit#show() 40 | command! -nargs=0 AgsQuit call ags#quit() 41 | command! -nargs=0 AgsShowLastCommand call ags#run#getLastCmd() 42 | -------------------------------------------------------------------------------- /syntax/agse.vim: -------------------------------------------------------------------------------- 1 | syntax match agseLineNum /^\s\{}\d\{1,}\s\{1}/ 2 | syntax match agseLineNumMatch /^\s\{}\d\{1,}\s\{1}/ 3 | 4 | syntax region agseFilePath 5 | \ oneline 6 | \ concealends 7 | \ matchgroup=agsvFilePathSyn 8 | \ keepend 9 | \ start=/\[1;31m/ 10 | \ end=/\[0m\[K/ 11 | 12 | highlight default link agseFilePath DiffAdd 13 | highlight default link agseLineNum DiffText 14 | highlight default link agseLineNumMatch DiffText 15 | -------------------------------------------------------------------------------- /syntax/agsv.vim: -------------------------------------------------------------------------------- 1 | syntax region agsvFilePath 2 | \ oneline 3 | \ concealends 4 | \ matchgroup=agsvFilePathSyn 5 | \ keepend 6 | \ start=/\[1;31m/ 7 | \ end=/\[0m\[K/ 8 | syntax region agsvLineNum 9 | \ oneline 10 | \ concealends 11 | \ matchgroup=agsvLineNumSyn 12 | \ keepend 13 | \ start=/\[1;30m/ 14 | \ end=/\[0m\[K-/ 15 | syntax region agsvLineNumMatch 16 | \ oneline 17 | \ concealends 18 | \ matchgroup=agsvLineNumMatchSyn 19 | \ keepend 20 | \ start=/\[1;30m/ 21 | \ end=/\[0m\[K:\d\{-1,}:/ 22 | syntax region agsvResultPattern 23 | \ oneline 24 | \ concealends 25 | \ matchgroup=agsvResultPatternSyn 26 | \ start='\[32;40m' 27 | \ end=/\[0m\[K/ 28 | syntax region agsvResultPatternOn 29 | \ oneline 30 | \ concealends 31 | \ matchgroup=agsvResultPatternOnSyn 32 | \ keepend 33 | \ start=/\[32;40m\[#m/ 34 | \ end=/\[#m\[0m\[K/ 35 | 36 | highlight default link agsvFilePath Constant 37 | highlight default link agsvLineNum Identifier 38 | highlight default link agsvLineNumMatch Underlined 39 | highlight default link agsvResultPattern Title 40 | highlight default link agsvResultPatternOn lCursor 41 | --------------------------------------------------------------------------------