├── LICENSE.md ├── README.md ├── autoload └── gitgrep.vim ├── doc └── gitgrep.txt ├── images └── preview.gif ├── plugin └── gitgrep.vim └── syntax └── gitgrep.vim /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © 2021 Rico Sta. Cruz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | vim-gitgrep 3 |

4 | 5 |

6 | Searches the project using :GG <search> and show it in a new buffer. Inspired by Sublime Text's Find in Files feature. 7 |

8 | 9 | ![Screencast](images/preview.gif) 10 | 11 | ## Install 12 | 13 | Install using vim-plug: 14 | 15 | ```vim 16 | Plug 'rstacruz/vim-gitgrep' 17 | ``` 18 | 19 | Search using `:GG`: 20 | 21 | ``` 22 | :GG hello 23 | ``` 24 | 25 | Read the [documentation](./doc/gitgrep.txt): 26 | 27 | ```vim 28 | :help gitgrep 29 | ``` 30 | 31 | ## Tips 32 | 33 | This will bind leaderg to search the current word under the cursor, similar to pressing \*. 34 | 35 | ```vim 36 | nnoremap * :GG! 37 | vnoremap * y:GG! " 38 | ``` 39 | 40 | This will bend leader/ to put `:GG! ` in the command line, ready to accept input. 41 | 42 | ```vim 43 | nnoremap / :GG! 44 | ``` 45 | 46 | ## Thanks 47 | 48 | :heart: MIT 49 | -------------------------------------------------------------------------------- /autoload/gitgrep.vim: -------------------------------------------------------------------------------- 1 | " Include guard 2 | if exists('g:autoloaded_gitgrep') || &compatible | finish | endif 3 | let g:autoloaded_gitgrep = 1 4 | 5 | " Name of the gitgrep window 6 | if !exists('g:gitgrep_window') 7 | let g:gitgrep_window = 'GitGrep' 8 | endif 9 | 10 | " Position in :VG (right or left) 11 | if !exists('g:gitgrep_vertical_position') 12 | let g:gitgrep_vertical_position = 'right' 13 | endif 14 | 15 | if !exists('g:gitgrep_use_cursor_line') 16 | let g:gitgrep_use_cursor_line = 1 17 | endif 18 | 19 | " Position in :SG (top or bottom) 20 | if !exists('g:gitgrep_horizontal_position') 21 | let g:gitgrep_horizontal_position = 'bottom' 22 | endif 23 | 24 | " Height for the window 25 | if !exists('g:gitgrep_height') 26 | let g:gitgrep_height = 30 27 | endif 28 | 29 | " `winmode` can be [vertical]split, [split], [tab] or [max]imised 30 | if !exists('g:gitgrep_window_mode') 31 | let g:gitgrep_window_mode = 'vertical' 32 | endif 33 | 34 | " Store the last known search here 35 | if !exists('g:gitgrep_last_query') 36 | let g:gitgrep_last_query = {'keywords': '', 'win_mode': '', 'ignorecase': -1} 37 | endif 38 | 39 | if !exists('g:gitgrep_keep_focus') 40 | let g:gitgrep_keep_focus = 1 41 | endif 42 | 43 | " The main entrypoint 44 | function! gitgrep#run(win_mode, keywords, options) " {{{ 45 | let ignorecase = get(a:options, 'ignorecase', 0) 46 | let focusonly = get(a:options, 'focusonly', 0) 47 | let force = get(a:options, 'force', 0) 48 | 49 | if a:win_mode == '' 50 | let win_mode = g:gitgrep_window_mode 51 | else 52 | let win_mode = a:win_mode 53 | endif 54 | 55 | " What invoked it 56 | let source = winnr() 57 | 58 | " Get query 59 | if a:keywords == '' 60 | if g:gitgrep_last_query['keywords'] == '' 61 | echo "What do you want to search for?" 62 | return 63 | else 64 | " Repeat the last query 65 | let query = g:gitgrep_last_query 66 | end 67 | else 68 | " New query 69 | let query = { 'keywords': a:keywords, 'ignorecase': ignorecase, 'win_mode': win_mode } 70 | endif 71 | 72 | " Open and focus on the window 73 | let window_mode = gitgrep#prepare_window(win_mode, { 'focusonly': focusonly }) 74 | if window_mode > 0 75 | if g:gitgrep_last_query['keywords'] == query['keywords'] && force != 1 76 | " If it's just refocusing, and the query hasn't changed, 77 | " don't do anything. TODO: account for ignorecase 78 | return 79 | else 80 | " Otherwise, clear out the entire buffer 81 | setlocal noreadonly modifiable 82 | norm ggVG"_d 83 | endif 84 | endif 85 | 86 | " Perform an git grep search 87 | let escaped_query = shellescape(query['keywords']) 88 | 89 | let grep_params = '' 90 | if query['ignorecase'] == 1 91 | let grep_params = '-i ' 92 | endif 93 | 94 | silent! exec 'r!git grep --heading --line-number -E ' . grep_params . escaped_query 95 | 96 | " check line count to see if there are results 97 | if line('$') != 1 98 | try 99 | " Format lines 100 | silent! %s#^\d\+:# & #g 101 | 102 | " Format filenames 103 | silent! %s#^[^ ].\+$#\r&#g 104 | catch /./ 105 | endtry 106 | 107 | " Move cursor to top, remove first 2 lines 108 | normal gg 109 | normal "_2dd 110 | 111 | " Highlight currenty query 112 | call clearmatches() 113 | call matchadd('Search', (query['ignorecase'] == 1 ? '\c' : '') . query['keywords']) 114 | 115 | " Also allow pressing 'n' to move to next match 116 | let @/ = query['keywords'] 117 | else 118 | exec "normal a! No results found for `" . query['keywords'] . "`" 119 | endif 120 | 121 | " Prevent it from being written, and other stuff 122 | " (Setting the filetype will bind buffer keys) 123 | silent! setlocal 124 | \ nocursorcolumn nobuflisted foldcolumn=0 125 | \ nolist nonumber norelativenumber nospell noswapfile signcolumn=no 126 | \ nomodifiable nonumber foldmethod=indent filetype=gitgrep buftype=nofile hlsearch ignorecase 127 | 128 | if g:gitgrep_use_cursor_line == 1 129 | silent! setlocal cursorline 130 | endif 131 | 132 | " Finally, let it be picked up later 133 | let g:gitgrep_last_query = query 134 | endfunction " }}} 135 | 136 | " Opens a window and focuses on it 137 | function gitgrep#open_window(win_mode) " {{{ 138 | " `winmode` can be [vertical], [split], [tab] or [max]imised 139 | let mode = a:win_mode == '' ? 'max' : a:win_mode 140 | 141 | " save the referer window for splits 142 | let referer = winnr() 143 | 144 | if mode =~? '^v' " vertical 145 | let pos = g:gitgrep_vertical_position == 'right' ? 'botright' : 'topleft' 146 | exe 'vert ' . pos . ' new' 147 | let b:referer = referer 148 | elseif mode =~? '^t' " tab 149 | tabnew 150 | elseif mode =~? '^s' " split 151 | let pos = g:gitgrep_horizontal_position == 'bottom' ? 'bot' : 'top' 152 | exe '' . pos . ' new' 153 | exe 'resize ' . g:gitgrep_height 154 | let b:referer = referer 155 | elseif mode =~? 'm' " maximized 156 | " always opens on top 157 | exe 'top new' 158 | resize 159 | endif 160 | endfunction " }}} 161 | 162 | " Returns the buffer and window. Can return -1's 163 | " 164 | " let [buf, win] = gitgrep#get_current_window() 165 | " 166 | function! gitgrep#get_current_window() " {{{ 167 | let buf = bufnr(g:gitgrep_window) 168 | let win = bufwinnr(buf) 169 | return [buf, win] 170 | endfunction "}}} 171 | 172 | " Opens a window, or focuses if it's already there 173 | function! gitgrep#prepare_window(win_mode, options) " {{{ 174 | " Returns... 175 | " - `-1` if it did nothing 176 | " - `0` if it's a new window 177 | " - `1` if it's a new window & existing buffer 178 | " - `2` if it's an existing window & existing buffer 179 | let [buf, win] = gitgrep#get_current_window() 180 | let focusonly = get(a:options, 'focusonly', 0) 181 | 182 | if buf == -1 183 | " New buffer/window 184 | if focusonly == 1 | return -1 | endif 185 | call gitgrep#open_window(a:win_mode) 186 | " set buffer name 187 | exe 'file ' . g:gitgrep_window 188 | let b:gitgrep_buffer = 1 189 | return 0 190 | elseif win == -1 191 | " Old buffer, new window (reuse the old buffer) 192 | if focusonly == 1 | return -1 | endif 193 | call gitgrep#open_window(a:win_mode) 194 | exe 'b ' . buf 195 | return 1 196 | else 197 | " Old buffer, old window (focus on open window) 198 | exec '' . win . 'wincmd w' 199 | return 2 200 | endif 201 | endfunction " }}} 202 | 203 | " Binds keys 204 | function! gitgrep#bind_buffer_keys() " {{{ 205 | nnoremap :call gitgrep#navigate('open') 206 | nnoremap o :call gitgrep#navigate('open') 207 | nnoremap f :call gitgrep#toggle_follow_cursor() 208 | nnoremap R :call gitgrep#refresh() 209 | autocmd CursorMoved call gitgrep#on_cursor_move() 210 | endfunction " }}} 211 | 212 | function! gitgrep#on_cursor_move() " {{{ 213 | if exists('b:follow_cursor') && b:follow_cursor == 1 214 | call gitgrep#navigate('hover') 215 | endif 216 | endfunction " }}} 217 | 218 | " Navigates to a selected line in the search buffer 219 | function! gitgrep#navigate(mode) " {{{ 220 | " TODO: this is being double-called, lets optimise that 221 | " only operate on the gitgrep buffer 222 | if get(b:, 'gitgrep_buffer', 0) != 1 | return | endif 223 | 224 | let old_g_value = getreg('g') 225 | let old_g_type = getregtype('g') 226 | let old_default_register = @" 227 | let follow_cursor = exists('b:follow_cursor') && b:follow_cursor == 1 228 | 229 | " keep track of original cursor location 230 | normal mg 231 | 232 | " copy the first word. it might be the line number 233 | normal ^"gyw 234 | 235 | " use it as the line number if it's numeric 236 | if @g =~? '^\d\+$' 237 | let linenr = @g 238 | else 239 | let linenr = '1' 240 | endif 241 | 242 | " move to the filename and copy 243 | normal { 244 | if line('.') != '1' 245 | normal j 246 | endif 247 | " normal 0v$l"gy 248 | normal "gyy 249 | 250 | " snap back to old location 251 | normal 'g 252 | 253 | " keep a reference to the search results window 254 | let src = winnr() 255 | 256 | let filepath = @g 257 | " use the referer window if possible. winwidth() will check 258 | " if the window is still open 259 | if exists('b:referer') && winwidth(b:referer) != -1 260 | let win = b:referer 261 | else 262 | vert bot new 263 | let win = winnr() 264 | silent! exe '' . src . 'windo w' 265 | let b:referer = win 266 | endif 267 | silent exe '' . win . 'windo edit +' . linenr . ' ' . filepath 268 | 269 | " refocus back to the search results window 270 | if follow_cursor 271 | setlocal cursorline 272 | silent! exe '' . src . 'windo w' 273 | endif 274 | 275 | " restore old register 276 | call setreg('g', old_g_value, old_g_type) 277 | 278 | " to make `p` work as usual, we'll set @" as the last register that was used 279 | let @" = old_default_register 280 | 281 | " Turn off follow cursor mode 282 | if a:mode == 'open' 283 | let b:follow_cursor = 0 284 | endif 285 | endfunction " }}} 286 | 287 | function gitgrep#toggle_follow_cursor() " {{{ 288 | " only operate on the gitgrep buffer 289 | if get(b:, 'gitgrep_buffer', 0) != 1 | return | endif 290 | 291 | if exists('b:follow_cursor') && b:follow_cursor == 1 292 | echo "Follow cursor [off]" 293 | let b:follow_cursor = 0 294 | else 295 | echo "Follow cursor [on] - experimental!" 296 | let b:follow_cursor = 1 297 | end 298 | endfunction " }}} 299 | 300 | function gitgrep#refresh() " {{{ 301 | call gitgrep#run('', '', { 'force': 1, 'focusonly': 1 }) 302 | endfunction " }}} 303 | -------------------------------------------------------------------------------- /doc/gitgrep.txt: -------------------------------------------------------------------------------- 1 | *gitgrep* Git grep tool 2 | 3 | 4 | GIT GREP TOOL 5 | 6 | 7 | ======================================================================================= 8 | CONTENTS *gitgrep-contents* 9 | 10 | Commands ......................................... |gitgrep-commands| 11 | Configuration .................................... |gitgrep-configuration| 12 | Example config ................................... |gitgrep-example| 13 | 14 | ======================================================================================= 15 | COMMANDS *gitgrep-commands* 16 | *:GG* 17 | > 18 | :GG [] 19 | :GG! [] 20 | < 21 | Perform a search for ``. If the query isn't given, it will re-perform 22 | the last query. 23 | 24 | If a `!` is given, then the search will be case-insensitive. 25 | 26 | ======================================================================================= 27 | CONFIGURATION *gitgrep-configuration* 28 | *g:gitgrep_use_cursorline* 29 | > 30 | let g:gitgrep_use_cursorline = 1 31 | < 32 | If set to `1`, the current line in the search results will be highlighted. 33 | 34 | *g:gitgrep_window_mode* 35 | > 36 | let g:gitgrep_window_mode = 'vertical' 37 | < 38 | Changes the way the Gitgrep window appears. Valid values are: 39 | 40 | 'vertical' Vertical split (default) 41 | 'split' Horizontal split 42 | 'max' Maximized split 43 | 'tab' New tab 44 | 45 | *g:gitgrep_horizontal_position* 46 | > 47 | let g:gitgrep_horizontal_position = 'bottom' 48 | < 49 | Sets the position of when `g:gitgrep_window_mode` is set to 'split'. 50 | Can be set to either 'top' or 'bottom'. 51 | 52 | *g:gitgrep_vertical_position* 53 | > 54 | let g:gitgrep_vertical_position = 'right' 55 | < 56 | Sets the position of when `g:gitgrep_window_mode` is set to 'vertical'. 57 | Can be set to either 'left' or 'right'. 58 | 59 | *g:gitgrep_height* 60 | > 61 | let g:gitgrep_height = 30 62 | < 63 | Sets the height of the window when `g:gitgrep_window_mode` is set to 'split'. 64 | 65 | ======================================================================================= 66 | KEY BINDINGS *gitgrep-keys* 67 | 68 | These keys are available in the gitgrep window: 69 | 70 | `` navigate to line 71 | `R` refresh 72 | 73 | ======================================================================================= 74 | EXAMPLE CONFIG *gitgrep-example* 75 | > 76 | nnoremap / :GG! 77 | nnoremap * :GG! 78 | vnoremap * y:GG! " 79 | < 80 | ======================================================================================= 81 | vim:tw=78:ts=8:noet:ft=help:norl: 82 | -------------------------------------------------------------------------------- /images/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstacruz/vim-gitgrep/f8cd8e687413d7b3be6bb0c1b10b96e27a345023/images/preview.gif -------------------------------------------------------------------------------- /plugin/gitgrep.vim: -------------------------------------------------------------------------------- 1 | " Git grep 2 | command! -bang -nargs=* GG call gitgrep#run('', , { 'ignorecase': 0 }) 3 | 4 | augroup gitgrep 5 | autocmd FileType gitgrep call gitgrep#bind_buffer_keys() 6 | augroup END 7 | 8 | " junegunn/vim-slash 9 | if globpath(&rtp, "plugin/slash.vim") != "" 10 | augroup gitgrep_slash 11 | autocmd FileType gitgrep autocmd! slash CursorMoved,CursorMovedI * 12 | augroup END 13 | endif 14 | -------------------------------------------------------------------------------- /syntax/gitgrep.vim: -------------------------------------------------------------------------------- 1 | if exists('b:current_syntax') | finish | endif 2 | 3 | syntax match GitgrepFile /^[^ ].*/ 4 | syntax match GitGrepLineNr /^ \+\d\+:/ 5 | syntax match GitGrepError /^\(!\|fatal:\) .*/ 6 | 7 | highlight default link GitgrepFile Title 8 | highlight default link GitgrepLineNr LineNr 9 | highlight default link GitgrepError Error 10 | 11 | let b:current_syntax = 'gitgrep' 12 | --------------------------------------------------------------------------------