├── .github └── issue_template.md ├── .gitignore ├── LICENCE ├── README.mkd ├── autoload ├── gitgutter.vim └── gitgutter │ ├── async.vim │ ├── debug.vim │ ├── diff.vim │ ├── diff_highlight.vim │ ├── fold.vim │ ├── highlight.vim │ ├── hunk.vim │ ├── sign.vim │ └── utility.vim ├── doc └── gitgutter.txt ├── plugin └── gitgutter.vim ├── screenshot.png └── test ├── .gitattributes ├── .gitconfig ├── cp932.txt ├── fixture.foo ├── fixture.txt ├── fixture_dos.txt ├── fixture_dos_noeol.txt ├── runner.vim ├── test └── test_gitgutter.vim /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | > What is the latest commit SHA in your installed vim-gitgutter? 2 | 3 | > What vim/nvim version are you on? 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | /misc 3 | /test/*.actual 4 | *.log 5 | 6 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Andrew Stewart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | ## vim-gitgutter 2 | 3 | A Vim plugin which shows a git diff in the sign column. It shows which lines have been added, modified, or removed. You can also preview, stage, and undo individual hunks; and stage partial hunks. The plugin also provides a hunk text object. 4 | 5 | The signs are always up to date and the plugin never saves your buffer. 6 | 7 | The name "gitgutter" comes from the Sublime Text 3 plugin which inspired this in 2013. 8 | 9 | Features: 10 | 11 | * Shows signs for added, modified, and removed lines. 12 | * Runs the diffs asynchronously where possible. 13 | * Ensures signs are always up to date. 14 | * Never saves the buffer. 15 | * Quick jumping between blocks of changed lines ("hunks"). 16 | * Stage/undo/preview individual hunks. 17 | * Previews highlight intra-line changes. 18 | * Stage partial hunks. 19 | * Provides a hunk text object. 20 | * Diffs against index (default) or any commit. 21 | * Handles file moves / renames. 22 | * Heeds git's "assume unchanged" bit. 23 | * Allows folding all unchanged text. 24 | * Provides fold text showing whether folded lines have been changed. 25 | * Can load all hunk locations into quickfix list or the current window's location list. 26 | * Handles line endings correctly, even with repos that do CRLF conversion. 27 | * Handles clean/smudge filters. 28 | * Optional line highlighting. 29 | * Optional line number highlighting. (Only available in Neovim 0.3.2 or higher) 30 | * Fully customisable (signs, sign column, line (number) highlights, mappings, extra git-diff arguments, etc). 31 | * Can be toggled on/off, globally or per buffer. 32 | * Preserves signs from other plugins. 33 | * Does the right thing when viewing revisions with [fugitive](https://github.com/tpope/vim-fugitive)'s `:0Gclog`. 34 | * Easy to integrate diff stats into status line; built-in integration with [vim-airline](https://github.com/bling/vim-airline/). 35 | * Works with fish shell (in addition to the usual shells). 36 | 37 | Constraints: 38 | 39 | * Supports git only. If you work with other version control systems, I recommend [vim-signify](https://github.com/mhinz/vim-signify). 40 | * Relies on the `FocusGained` event. If your terminal doesn't report focus events, either use something like [Terminus][] or set `let g:gitgutter_terminal_reports_focus=0`. For tmux, `set -g focus-events on` in your tmux.conf. 41 | 42 | Compatibility: 43 | 44 | Compatible back to Vim 7.4, and probably 7.3. 45 | 46 | 47 | ### Screenshot 48 | 49 | ![screenshot](./screenshot.png?raw=true) 50 | 51 | In the screenshot above you can see: 52 | 53 | * Lines 183-184 are new. 54 | * Lines 186-187 have been modified. 55 | * The preview for the modified lines highlights changed regions within the line. 56 | 57 | 58 | ### Installation 59 | 60 | First, install using your favourite package manager, or use Vim's built-in package support. 61 | 62 | Vim: 63 | 64 | ``` 65 | mkdir -p ~/.vim/pack/airblade/start 66 | cd ~/.vim/pack/airblade/start 67 | git clone https://github.com/airblade/vim-gitgutter.git 68 | vim -u NONE -c "helptags vim-gitgutter/doc" -c q 69 | ``` 70 | 71 | Neovim: 72 | 73 | ``` 74 | mkdir -p ~/.config/nvim/pack/airblade/start 75 | cd ~/.config/nvim/pack/airblade/start 76 | git clone https://github.com/airblade/vim-gitgutter.git 77 | nvim -u NONE -c "helptags vim-gitgutter/doc" -c q 78 | ``` 79 | 80 | Second, ensure your `updatetime` and `signcolumn` options are set appropriately. 81 | 82 | When you make a change to a file tracked by git, the diff markers should appear automatically after a short delay. The delay is governed by vim's `updatetime` option; the default value is `4000`, i.e. 4 seconds, but I suggest reducing it to around 100ms (add `set updatetime=100` to your vimrc). Note `updatetime` also controls the delay before vim writes its swap file (see `:help updatetime`). 83 | 84 | The `signcolumn` option can have any value except `'off'`. 85 | 86 | 87 | ### Windows 88 | 89 | There is a potential risk on Windows due to `cmd.exe` prioritising the current folder over folders in `PATH`. If you have a file named `git.*` (i.e. with any extension in `PATHEXT`) in your current folder, it will be executed instead of git whenever the plugin calls git. 90 | 91 | You can avoid this risk by configuring the full path to your git executable. For example: 92 | 93 | ```viml 94 | " This path probably won't work 95 | let g:gitgutter_git_executable = 'C:\Program Files\Git\bin\git.exe' 96 | ``` 97 | 98 | Unfortunately I don't know the correct escaping for the path - if you do, please let me know! 99 | 100 | 101 | ### Getting started 102 | 103 | When you make a change to a file tracked by git, the diff markers should appear automatically after a short delay. 104 | 105 | You can jump between hunks with `[c` and `]c`. You can preview, stage, and undo hunks with `hp`, `hs`, and `hu` respectively. 106 | 107 | You cannot unstage a staged hunk. 108 | 109 | After updating the signs, the plugin fires the `GitGutter` User autocommand. 110 | 111 | After staging a hunk or part of a hunk, the plugin fires the `GitGutterStage` User autocommand. 112 | 113 | 114 | #### Activation 115 | 116 | You can explicitly turn vim-gitgutter off and on (defaults to on): 117 | 118 | * turn off with `:GitGutterDisable` 119 | * turn on with `:GitGutterEnable` 120 | * toggle with `:GitGutterToggle`. 121 | 122 | To toggle vim-gitgutter per buffer: 123 | 124 | * turn off with `:GitGutterBufferDisable` 125 | * turn on with `:GitGutterBufferEnable` 126 | * toggle with `:GitGutterBufferToggle` 127 | 128 | You can turn the signs on and off (defaults to on): 129 | 130 | * turn on with `:GitGutterSignsEnable` 131 | * turn off with `:GitGutterSignsDisable` 132 | * toggle with `:GitGutterSignsToggle`. 133 | 134 | And you can turn line highlighting on and off (defaults to off): 135 | 136 | * turn on with `:GitGutterLineHighlightsEnable` 137 | * turn off with `:GitGutterLineHighlightsDisable` 138 | * toggle with `:GitGutterLineHighlightsToggle`. 139 | 140 | Note that if you have line highlighting on and signs off, you will have an empty sign column – more accurately, a sign column with invisible signs. This is because line highlighting requires signs and Vim/NeoVim always shows the sign column when there are signs even if the signs are invisible. 141 | 142 | With Neovim 0.3.2 or higher, you can turn line number highlighting on and off (defaults to off): 143 | 144 | * turn on with `:GitGutterLineNrHighlightsEnable` 145 | * turn off with `:GitGutterLineNrHighlightsDisable` 146 | * toggle with `:GitGutterLineNrHighlightsToggle`. 147 | 148 | The same caveat applies to line number highlighting as to line highlighting just above. 149 | 150 | If you switch off both line highlighting and signs, you won't see the sign column. 151 | 152 | In older Vims (pre 8.1.0614 / Neovim 0.4.0) vim-gitgutter will suppress the signs when a file has more than 500 changes, to avoid slowing down the UI. As soon as the number of changes falls below the limit vim-gitgutter will show the signs again. You can configure the threshold with: 153 | 154 | ```viml 155 | let g:gitgutter_max_signs = 500 " default value (Vim < 8.1.0614, Neovim < 0.4.0) 156 | let g:gitgutter_max_signs = -1 " default value (otherwise) 157 | ``` 158 | 159 | You can also remove the limit by setting `g:gitgutter_max_signs = -1`. 160 | 161 | #### Hunks 162 | 163 | You can jump between hunks: 164 | 165 | * jump to next hunk (change): `]c` 166 | * jump to previous hunk (change): `[c`. 167 | 168 | Both of those take a preceding count. 169 | 170 | To set your own mappings for these, for example `]h` and `[h`: 171 | 172 | ```viml 173 | nmap ]h (GitGutterNextHunk) 174 | nmap [h (GitGutterPrevHunk) 175 | ``` 176 | 177 | When you jump between hunks, a message like `Hunk 4 of 11` is shown on the command line. If you want to turn the message off, you can use: 178 | 179 | ```viml 180 | let g:gitgutter_show_msg_on_hunk_jumping = 0 181 | ``` 182 | 183 | You can load all your hunks into the quickfix list with `:GitGutterQuickFix`. Note this ignores any unsaved changes in your buffers. If the option `g:gitgutter_use_location_list` is set, this command will load hunks into the current window's location list instead. Use `:copen` (or `:lopen`) to open the quickfix / location list or add a custom command like this: 184 | 185 | ```viml 186 | command! Gqf GitGutterQuickFix | copen 187 | ``` 188 | 189 | You can stage or undo an individual hunk when your cursor is in it: 190 | 191 | * stage the hunk with `hs` or 192 | * undo it with `hu`. 193 | 194 | To stage part of an additions-only hunk by: 195 | 196 | * either visually selecting the part you want and staging with your mapping, e.g. `hs`; 197 | * or using a range with the `GitGutterStageHunk` command, e.g. `:42,45GitGutterStageHunk`. 198 | 199 | To stage part of any hunk: 200 | 201 | * preview the hunk, e.g. `hp`; 202 | * move to the preview window, e.g. `:wincmd P`; 203 | * delete the lines you do not want to stage; 204 | * stage the remaining lines: either write (`:w`) the window or stage via `hs` or `:GitGutterStageHunk`. 205 | 206 | Note the above workflow is not possible if you have opted in to preview hunks with Vim's popup windows. 207 | 208 | See the FAQ if you want to unstage staged changes. 209 | 210 | The `.` command will work with both these if you install [repeat.vim](https://github.com/tpope/vim-repeat). 211 | 212 | To set your own mappings for these, for example if you prefer `g`-based maps: 213 | 214 | ```viml 215 | nmap ghs (GitGutterStageHunk) 216 | nmap ghu (GitGutterUndoHunk) 217 | ``` 218 | 219 | And you can preview a hunk's changes with `hp`. The location of the preview window is configured with `g:gitgutter_preview_win_location` (default `'bo'`). You can of course change this mapping, e.g: 220 | 221 | ```viml 222 | nmap ghp (GitGutterPreviewHunk) 223 | ``` 224 | 225 | A hunk text object is provided which works in visual and operator-pending modes. 226 | 227 | - `ic` operates on all lines in the current hunk. 228 | - `ac` operates on all lines in the current hunk and any trailing empty lines. 229 | 230 | To re-map these, for example to `ih` and `ah`: 231 | 232 | ```viml 233 | omap ih (GitGutterTextObjectInnerPending) 234 | omap ah (GitGutterTextObjectOuterPending) 235 | xmap ih (GitGutterTextObjectInnerVisual) 236 | xmap ah (GitGutterTextObjectOuterVisual) 237 | ``` 238 | 239 | If you don't want vim-gitgutter to set up any mappings at all, use this: 240 | 241 | ```viml 242 | let g:gitgutter_map_keys = 0 243 | ``` 244 | 245 | Finally, you can force vim-gitgutter to update its signs across all visible buffers with `:GitGutterAll`. 246 | 247 | See the customisation section below for how to change the defaults. 248 | 249 | 250 | ### Vimdiff 251 | 252 | Use the `GitGutterDiffOrig` command to open a vimdiff view of the current buffer, respecting `g:gitgutter_diff_relative_to` and `:gitgutter_diff_base`. 253 | 254 | 255 | ### Folding 256 | 257 | Use the `GitGutterFold` command to fold all unchanged lines, leaving just the hunks visible. Use `zr` to unfold 3 lines of context above and below a hunk. 258 | 259 | Execute `GitGutterFold` a second time to restore the previous view. 260 | 261 | Use `gitgutter#fold#foldtext()` to augment the default `foldtext()` with an indicator of whether the folded lines have been changed. 262 | 263 | ```viml 264 | set foldtext=gitgutter#fold#foldtext() 265 | ``` 266 | 267 | For a closed fold with changed lines: 268 | 269 | ``` 270 | Default foldtext(): +-- 45 lines: abcdef 271 | gitgutter#fold#foldtext(): +-- 45 lines (*): abcdef 272 | ``` 273 | 274 | You can use `gitgutter#fold#is_changed()` in your own `foldtext` expression to find out whether the folded lines have been changed. 275 | 276 | 277 | ### Status line 278 | 279 | Call the `GitGutterGetHunkSummary()` function from your status line to get a list of counts of added, modified, and removed lines in the current buffer. For example: 280 | 281 | ```viml 282 | " Your vimrc 283 | function! GitStatus() 284 | let [a,m,r] = GitGutterGetHunkSummary() 285 | return printf('+%d ~%d -%d', a, m, r) 286 | endfunction 287 | set statusline+=%{GitStatus()} 288 | ``` 289 | 290 | 291 | ### Customisation 292 | 293 | You can customise: 294 | 295 | * The sign column's colours 296 | * Whether or not the sign column is shown when there aren't any signs (defaults to no) 297 | * How to handle non-gitgutter signs 298 | * The signs' colours and symbols 299 | * Line highlights 300 | * Line number highlights (only in Neovim 0.3.2 or higher) 301 | * The diff syntax colours used in the preview window 302 | * The intra-line diff highlights used in the preview window 303 | * Whether the diff is relative to the index (default) or working tree. 304 | * The base of the diff 305 | * Extra arguments for `git` when running `git diff` 306 | * Extra arguments for `git diff` 307 | * Key mappings 308 | * Whether vim-gitgutter is on initially (defaults to on) 309 | * Whether signs are shown (defaults to yes) 310 | * Whether line highlighting is on initially (defaults to off) 311 | * Whether line number highlighting is on initially (defaults to off) 312 | * Whether vim-gitgutter runs asynchronously (defaults to yes) 313 | * Whether to clobber or preserve non-gitgutter signs 314 | * The priority of gitgutter's signs. 315 | * Whether to use a floating/popup window for hunk previews 316 | * The appearance of a floating/popup window for hunk previews 317 | * Whether to populate the quickfix list or a location list with all hunks 318 | 319 | Please note that vim-gitgutter won't override any colours or highlights you've set in your colorscheme. 320 | 321 | 322 | #### Sign column 323 | 324 | Set the `SignColumn` highlight group to change the sign column's colour. For example: 325 | 326 | ```viml 327 | " vim-gitgutter used to do this by default: 328 | highlight! link SignColumn LineNr 329 | 330 | " or you could do this: 331 | highlight SignColumn guibg=whatever ctermbg=whatever 332 | ``` 333 | 334 | By default the sign column will appear when there are signs to show and disappear when there aren't. To always have the sign column, add to your vimrc: 335 | 336 | ```viml 337 | " Vim 7.4.2201 338 | set signcolumn=yes 339 | ``` 340 | 341 | GitGutter can preserve or ignore non-gitgutter signs. For Vim v8.1.0614 and later you can set gitgutter's signs' priorities with `g:gitgutter_sign_priority`, so gitgutter defaults to clobbering other signs. For Neovim v0.4.0 and later you can set an expanding sign column so gitgutter again defaults to clobbering other signs. Otherwise, gitgutter defaults to preserving other signs. You can configure this with: 342 | 343 | ```viml 344 | let g:gitgutter_sign_allow_clobber = 1 345 | ``` 346 | 347 | 348 | #### Signs' colours and symbols 349 | 350 | If you or your colourscheme has defined `GitGutter*` highlight groups, the plugin will use them for the signs' colours. 351 | 352 | If you want the background colours to match the sign column, but don't want to update the `GitGutter*` groups yourself, you can get the plugin to do it: 353 | 354 | ```viml 355 | let g:gitgutter_set_sign_backgrounds = 1 356 | ``` 357 | 358 | If no `GitGutter*` highlight groups exist, the plugin will check the `Diff*` highlight groups. If their foreground colours differ the plugin will use them; if not, these colours will be used: 359 | 360 | ```viml 361 | highlight GitGutterAdd guifg=#009900 ctermfg=2 362 | highlight GitGutterChange guifg=#bbbb00 ctermfg=3 363 | highlight GitGutterDelete guifg=#ff2222 ctermfg=1 364 | ``` 365 | 366 | To customise the symbols, add the following to your `~/.vimrc`: 367 | 368 | ```viml 369 | let g:gitgutter_sign_added = 'xx' 370 | let g:gitgutter_sign_modified = 'yy' 371 | let g:gitgutter_sign_removed = 'zz' 372 | let g:gitgutter_sign_removed_first_line = '^^' 373 | let g:gitgutter_sign_removed_above_and_below = '{' 374 | let g:gitgutter_sign_modified_removed = 'ww' 375 | ``` 376 | 377 | 378 | #### Line highlights 379 | 380 | Similarly to the signs' colours, set up the following highlight groups in your colorscheme or `~/.vimrc`: 381 | 382 | ```viml 383 | GitGutterAddLine " default: links to DiffAdd 384 | GitGutterChangeLine " default: links to DiffChange 385 | GitGutterDeleteLine " default: links to DiffDelete 386 | GitGutterChangeDeleteLine " default: links to GitGutterChangeLine, i.e. DiffChange 387 | ``` 388 | 389 | For example, in some colorschemes the `DiffText` highlight group is easier to read than `DiffChange`. You could use it like this: 390 | 391 | ```viml 392 | highlight link GitGutterChangeLine DiffText 393 | ``` 394 | 395 | 396 | #### Line number highlights 397 | 398 | NOTE: This feature requires Neovim 0.3.2 or higher. 399 | 400 | Similarly to the signs' colours, set up the following highlight groups in your colorscheme or `~/.vimrc`: 401 | 402 | ```viml 403 | GitGutterAddLineNr " default: links to CursorLineNr 404 | GitGutterChangeLineNr " default: links to CursorLineNr 405 | GitGutterDeleteLineNr " default: links to CursorLineNr 406 | GitGutterChangeDeleteLineNr " default: links to GitGutterChangeLineNr 407 | ``` 408 | 409 | Maybe you think `CursorLineNr` is a bit annoying. For example, you could use `Underlined` for this: 410 | 411 | ```viml 412 | highlight link GitGutterChangeLineNr Underlined 413 | ``` 414 | 415 | 416 | #### The diff syntax colours used in the preview window 417 | 418 | To change the diff syntax colours used in the preview window, set up the `diff*` highlight groups in your colorscheme or `~/.vimrc`: 419 | 420 | ```viml 421 | diffAdded " if not set: use GitGutterAdd's foreground colour 422 | diffChanged " if not set: use GitGutterChange's foreground colour 423 | diffRemoved " if not set: use GitGutterDelete's foreground colour 424 | ``` 425 | 426 | Note the `diff*` highlight groups are used in any buffer whose `'syntax'` is `diff`. 427 | 428 | 429 | #### The intra-line diff highlights used in the preview window 430 | 431 | To change the intra-line diff highlights used in the preview window, set up the following highlight groups in your colorscheme or `~/.vimrc`: 432 | 433 | ```viml 434 | GitGutterAddIntraLine " default: gui=reverse cterm=reverse 435 | GitGutterDeleteIntraLine " default: gui=reverse cterm=reverse 436 | ``` 437 | 438 | For example, to use `DiffAdd` for intra-line added regions: 439 | 440 | ```viml 441 | highlight link GitGutterAddIntraLine DiffAdd 442 | ``` 443 | 444 | 445 | #### Whether the diff is relative to the index or working tree 446 | 447 | By default diffs are relative to the index. How you can make them relative to the working tree: 448 | 449 | ```viml 450 | let g:gitgutter_diff_relative_to = 'working_tree' 451 | ``` 452 | 453 | 454 | #### The base of the diff 455 | 456 | By default buffers are diffed against the index. However you can diff against any commit by setting: 457 | 458 | ```viml 459 | let g:gitgutter_diff_base = '' 460 | ``` 461 | 462 | If you are looking at a previous version of a file with Fugitive (e.g. via `:0Gclog`), gitgutter sets the diff base to the parent of the current revision. 463 | 464 | This setting is ignored when the diffs are relative to the working tree. 465 | 466 | 467 | #### Extra arguments for `git` when running `git diff` 468 | 469 | If you want to pass extra arguments to `git` when running `git diff`, do so like this: 470 | 471 | ```viml 472 | let g:gitgutter_git_args = '--git-dir-""' 473 | ``` 474 | 475 | #### Extra arguments for `git diff` 476 | 477 | If you want to pass extra arguments to `git diff`, for example to ignore whitespace, do so like this: 478 | 479 | ```viml 480 | let g:gitgutter_diff_args = '-w' 481 | ``` 482 | 483 | #### Key mappings 484 | 485 | To disable all key mappings: 486 | 487 | ```viml 488 | let g:gitgutter_map_keys = 0 489 | ``` 490 | 491 | See above for configuring maps for hunk-jumping and staging/undoing. 492 | 493 | 494 | #### Use a custom `grep` command 495 | 496 | If you use an alternative to grep, you can tell vim-gitgutter to use it here. 497 | 498 | ```viml 499 | " Default: 500 | let g:gitgutter_grep = 'grep' 501 | ``` 502 | 503 | #### To turn off vim-gitgutter by default 504 | 505 | Add `let g:gitgutter_enabled = 0` to your `~/.vimrc`. 506 | 507 | 508 | #### To turn off signs by default 509 | 510 | Add `let g:gitgutter_signs = 0` to your `~/.vimrc`. 511 | 512 | 513 | #### To turn on line highlighting by default 514 | 515 | Add `let g:gitgutter_highlight_lines = 1` to your `~/.vimrc`. 516 | 517 | 518 | #### To turn on line number highlighting by default 519 | 520 | Add `let g:gitgutter_highlight_linenrs = 1` to your `~/.vimrc`. 521 | 522 | 523 | #### To turn off asynchronous updates 524 | 525 | By default diffs are run asynchronously. To run diffs synchronously instead: 526 | 527 | ```viml 528 | let g:gitgutter_async = 0 529 | ``` 530 | 531 | 532 | #### To use floating/popup windows for hunk previews 533 | 534 | Add `let g:gitgutter_preview_win_floating = 1` to your `~/.vimrc`. Note that on Vim this prevents you staging (partial) hunks via the preview window. 535 | 536 | On Neovim, the preview hunk command will move the cursor into the floating window if it is already open. 537 | 538 | 539 | #### The appearance of a floating/popup window for hunk previews 540 | 541 | Either set `g:gitgutter_floating_window_options` to a dictionary of the options you want. This dictionary is passed directly to `popup_create()` (Vim) / `nvim_open_win()` (Neovim). 542 | 543 | Or if you just want to override one or two of the defaults, you can do that with a file in an `after/` directory. For example: 544 | 545 | ```viml 546 | " ~/.vim/after/vim-gitgutter/overrides.vim 547 | let g:gitgutter_floating_window_options['border'] = 'single' 548 | ``` 549 | 550 | 551 | #### To load all hunks into the current window's location list instead of the quickfix list 552 | 553 | Add `let g:gitgutter_use_location_list = 1` to your `~/.vimrc`. 554 | 555 | 556 | ### Extensions 557 | 558 | #### Operate on every line in a hunk 559 | 560 | You can map an operator to do whatever you want to every line in a hunk. 561 | 562 | Let's say, for example, you want to remove trailing whitespace. 563 | 564 | ```viml 565 | function! CleanUp(...) 566 | if a:0 " opfunc 567 | let [first, last] = [line("'["), line("']")] 568 | else 569 | let [first, last] = [line("'<"), line("'>")] 570 | endif 571 | for lnum in range(first, last) 572 | let line = getline(lnum) 573 | 574 | " clean up the text, e.g.: 575 | let line = substitute(line, '\s\+$', '', '') 576 | 577 | call setline(lnum, line) 578 | endfor 579 | endfunction 580 | 581 | nmap x :set opfunc=CleanUpg@ 582 | ``` 583 | 584 | Then place your cursor in a hunk and type `\xic` (assuming a leader of `\`). 585 | 586 | Alternatively you could place your cursor in a hunk, type `vic` to select it, then `:call CleanUp()`. 587 | 588 | 589 | #### Operate on every changed line in a file 590 | 591 | You can write a command to do whatever you want to every changed line in a file. 592 | 593 | ```viml 594 | function! GlobalChangedLines(ex_cmd) 595 | for hunk in GitGutterGetHunks() 596 | for lnum in range(hunk[2], hunk[2]+hunk[3]-1) 597 | let cursor = getcurpos() 598 | silent! execute lnum.a:ex_cmd 599 | call setpos('.', cursor) 600 | endfor 601 | endfor 602 | endfunction 603 | 604 | command -nargs=1 Glines call GlobalChangedLines() 605 | ``` 606 | 607 | Let's say, for example, you want to remove trailing whitespace from all changed lines: 608 | 609 | ```viml 610 | :Glines s/\s\+$// 611 | ``` 612 | 613 | 614 | #### Cycle through hunks in current buffer 615 | 616 | This is like `:GitGutterNextHunk` but when it gets to the last hunk in the buffer it cycles around to the first. 617 | 618 | ```viml 619 | function! GitGutterNextHunkCycle() 620 | let line = line('.') 621 | silent! GitGutterNextHunk 622 | if line('.') == line 623 | 1 624 | GitGutterNextHunk 625 | endif 626 | endfunction 627 | ``` 628 | 629 | 630 | #### Cycle through hunks in all buffers 631 | 632 | You can use `:GitGutterQuickFix` to load all hunks into the quickfix list or the current window's location list. 633 | 634 | Alternatively, given that`]c` and `[c` jump from one hunk to the next in the current buffer, you can use this code to jump to the next hunk no matter which buffer it's in. 635 | 636 | ```viml 637 | function! NextHunkAllBuffers() 638 | let line = line('.') 639 | GitGutterNextHunk 640 | if line('.') != line 641 | return 642 | endif 643 | 644 | let bufnr = bufnr('') 645 | while 1 646 | bnext 647 | if bufnr('') == bufnr 648 | return 649 | endif 650 | if !empty(GitGutterGetHunks()) 651 | 1 652 | GitGutterNextHunk 653 | return 654 | endif 655 | endwhile 656 | endfunction 657 | 658 | function! PrevHunkAllBuffers() 659 | let line = line('.') 660 | GitGutterPrevHunk 661 | if line('.') != line 662 | return 663 | endif 664 | 665 | let bufnr = bufnr('') 666 | while 1 667 | bprevious 668 | if bufnr('') == bufnr 669 | return 670 | endif 671 | if !empty(GitGutterGetHunks()) 672 | normal! G 673 | GitGutterPrevHunk 674 | return 675 | endif 676 | endwhile 677 | endfunction 678 | 679 | nmap ]c :call NextHunkAllBuffers() 680 | nmap [c :call PrevHunkAllBuffers() 681 | ``` 682 | 683 | 684 | ### FAQ 685 | 686 | > How can I turn off realtime updates? 687 | 688 | Add this to your vim configuration (in an `/after/plugin` directory): 689 | 690 | ```viml 691 | " .vim/after/plugin/gitgutter.vim 692 | autocmd! gitgutter CursorHold,CursorHoldI 693 | ``` 694 | 695 | > I turned off realtime updates, how can I have signs updated when I save a file? 696 | 697 | If you really want to update the signs when you save a file, add this to your vimrc: 698 | 699 | ```viml 700 | autocmd BufWritePost * GitGutter 701 | ``` 702 | 703 | > Why can't I unstage staged changes? 704 | 705 | This plugin is for showing changes between the buffer and the index (and staging/undoing those changes). Unstaging a staged hunk would require showing changes between the index and HEAD, which is out of scope. 706 | 707 | > Why are the colours in the sign column weird? 708 | 709 | Your colorscheme is configuring the `SignColumn` highlight group weirdly. Please see the section above on customising the sign column. 710 | 711 | > What happens if I also use another plugin which uses signs (e.g. Syntastic)? 712 | 713 | You can configure whether GitGutter preserves or clobbers other signs using `g:gitgutter_sign_allow_clobber`. Set to `1` to clobber other signs (default on Vim >= 8.1.0614 and NeoVim >= 0.4.0) or `0` to preserve them. 714 | 715 | 716 | ### Troubleshooting 717 | 718 | #### When no signs are showing at all 719 | 720 | Here are some things you can check: 721 | 722 | * Try adding `let g:gitgutter_grep=''` to your vimrc. If it works, the problem is grep producing non-plain output; e.g. ANSI escape codes or colours. 723 | * Verify `:echo system("git --version")` succeeds. 724 | * Verify your git config is compatible with the version of git returned by the command above. 725 | * Verify your Vim supports signs (`:echo has('signs')` should give `1`). 726 | * Verify your file is being tracked by git and has unstaged changes. Check whether the plugin thinks git knows about your file: `:echo b:gitgutter.path` should show the path to the file in the repo. 727 | * Execute `:sign place group=gitgutter`; you should see a list of signs. 728 | - If the signs are listed: this is a colorscheme / highlight problem. Compare `:highlight GitGutterAdd` with `:highlight SignColumn`. 729 | - If no signs are listed: the call to git-diff is probably failing. Add `let g:gitgutter_log=1` to your vimrc, restart, reproduce the problem, and look at the `gitgutter.log` file in the plugin's directory. 730 | 731 | #### When the whole file is marked as added 732 | 733 | * If you use zsh, and you set `CDPATH`, make sure `CDPATH` doesn't include the current directory. 734 | 735 | #### When signs take a few seconds to appear 736 | 737 | * Try reducing `updatetime`, e.g. `set updatetime=100`. Note this also controls the delay before vim writes its swap file. 738 | 739 | #### When signs don't update after focusing Vim 740 | 741 | * Your terminal probably isn't reporting focus events. Either try installing [Terminus][] or set `let g:gitgutter_terminal_reports_focus=0`. For tmux, try `set -g focus-events on` in your tmux.conf. 742 | 743 | 744 | ### Shameless Plug 745 | 746 | If this plugin has helped you, or you'd like to learn more about Vim, why not check out this screencast I wrote for PeepCode: 747 | 748 | * [Smash Into Vim][siv] 749 | 750 | This was one of PeepCode's all-time top three bestsellers and is now available at Pluralsight. 751 | 752 | 753 | ### Intellectual Property 754 | 755 | Copyright Andrew Stewart, AirBlade Software Ltd. Released under the MIT licence. 756 | 757 | 758 | [pathogen]: https://github.com/tpope/vim-pathogen 759 | [siv]: http://pluralsight.com/training/Courses/TableOfContents/smash-into-vim 760 | [terminus]: https://github.com/wincent/terminus 761 | -------------------------------------------------------------------------------- /autoload/gitgutter.vim: -------------------------------------------------------------------------------- 1 | " Primary functions {{{ 2 | 3 | function! gitgutter#all(force) abort 4 | let visible = tabpagebuflist() 5 | 6 | for bufnr in range(1, bufnr('$') + 1) 7 | if buflisted(bufnr) 8 | let file = expand('#'.bufnr.':p') 9 | if !empty(file) 10 | if index(visible, bufnr) != -1 11 | call gitgutter#process_buffer(bufnr, a:force) 12 | elseif a:force 13 | call s:reset_tick(bufnr) 14 | endif 15 | endif 16 | endif 17 | endfor 18 | endfunction 19 | 20 | 21 | function! gitgutter#process_buffer(bufnr, force) abort 22 | " NOTE a:bufnr is not necessarily the current buffer. 23 | 24 | if gitgutter#utility#getbufvar(a:bufnr, 'enabled', -1) == -1 25 | call gitgutter#utility#setbufvar(a:bufnr, 'enabled', g:gitgutter_enabled) 26 | endif 27 | 28 | if gitgutter#utility#is_active(a:bufnr) 29 | 30 | if has('patch-7.4.1559') 31 | let l:Callback = function('gitgutter#process_buffer', [a:bufnr, a:force]) 32 | else 33 | let l:Callback = {'function': 'gitgutter#process_buffer', 'arguments': [a:bufnr, a:force]} 34 | endif 35 | let how = s:setup_path(a:bufnr, l:Callback) 36 | if [how] == ['async'] " avoid string-to-number conversion if how is a number 37 | return 38 | endif 39 | 40 | if a:force || s:has_fresh_changes(a:bufnr) 41 | 42 | let diff = 'NOT SET' 43 | try 44 | let diff = gitgutter#diff#run_diff(a:bufnr, g:gitgutter_diff_relative_to, 0) 45 | catch /gitgutter not tracked/ 46 | call gitgutter#debug#log('Not tracked: '.gitgutter#utility#file(a:bufnr)) 47 | catch /gitgutter assume unchanged/ 48 | call gitgutter#debug#log('Assume unchanged: '.gitgutter#utility#file(a:bufnr)) 49 | catch /gitgutter diff failed/ 50 | call gitgutter#debug#log('Diff failed: '.gitgutter#utility#file(a:bufnr)) 51 | call gitgutter#hunk#reset(a:bufnr) 52 | endtry 53 | 54 | if diff != 'async' && diff != 'NOT SET' 55 | call gitgutter#diff#handler(a:bufnr, diff) 56 | endif 57 | 58 | endif 59 | endif 60 | endfunction 61 | 62 | 63 | function! gitgutter#disable() abort 64 | call s:toggle_each_buffer(0) 65 | let g:gitgutter_enabled = 0 66 | endfunction 67 | 68 | function! gitgutter#enable() abort 69 | call s:toggle_each_buffer(1) 70 | let g:gitgutter_enabled = 1 71 | endfunction 72 | 73 | function s:toggle_each_buffer(enable) 74 | for bufnr in range(1, bufnr('$') + 1) 75 | if buflisted(bufnr) 76 | let file = expand('#'.bufnr.':p') 77 | if !empty(file) 78 | if a:enable 79 | call gitgutter#buffer_enable(bufnr) 80 | else 81 | call gitgutter#buffer_disable(bufnr) 82 | end 83 | endif 84 | endif 85 | endfor 86 | endfunction 87 | 88 | function! gitgutter#toggle() abort 89 | if g:gitgutter_enabled 90 | call gitgutter#disable() 91 | else 92 | call gitgutter#enable() 93 | endif 94 | endfunction 95 | 96 | 97 | function! gitgutter#buffer_disable(...) abort 98 | let bufnr = a:0 ? a:1 : bufnr('') 99 | call gitgutter#utility#setbufvar(bufnr, 'enabled', 0) 100 | call s:clear(bufnr) 101 | endfunction 102 | 103 | function! gitgutter#buffer_enable(...) abort 104 | let bufnr = a:0 ? a:1 : bufnr('') 105 | call gitgutter#utility#setbufvar(bufnr, 'enabled', 1) 106 | call gitgutter#process_buffer(bufnr, 1) 107 | endfunction 108 | 109 | function! gitgutter#buffer_toggle(...) abort 110 | let bufnr = a:0 ? a:1 : bufnr('') 111 | if gitgutter#utility#getbufvar(bufnr, 'enabled', 1) 112 | call gitgutter#buffer_disable(bufnr) 113 | else 114 | call gitgutter#buffer_enable(bufnr) 115 | endif 116 | endfunction 117 | 118 | " }}} 119 | 120 | 121 | " Optional argument is buffer number 122 | function! gitgutter#git(...) 123 | let git = g:gitgutter_git_executable 124 | if a:0 125 | let git .= ' -C '.gitgutter#utility#dir(a:1) 126 | endif 127 | if empty(g:gitgutter_git_args) 128 | return git 129 | else 130 | return git.' '.g:gitgutter_git_args 131 | endif 132 | endfunction 133 | 134 | 135 | function! gitgutter#setup_maps() 136 | if !g:gitgutter_map_keys 137 | return 138 | endif 139 | 140 | " Note hasmapto() and maparg() operate on the current buffer. 141 | 142 | let bufnr = bufnr('') 143 | 144 | if gitgutter#utility#getbufvar(bufnr, 'mapped', 0) 145 | return 146 | endif 147 | 148 | if !hasmapto('(GitGutterPrevHunk)') && maparg('[c', 'n') ==# '' 149 | nmap [c (GitGutterPrevHunk) 150 | endif 151 | if !hasmapto('(GitGutterNextHunk)') && maparg(']c', 'n') ==# '' 152 | nmap ]c (GitGutterNextHunk) 153 | endif 154 | 155 | if !hasmapto('(GitGutterStageHunk)', 'v') && maparg('hs', 'x') ==# '' 156 | xmap hs (GitGutterStageHunk) 157 | endif 158 | if !hasmapto('(GitGutterStageHunk)', 'n') && maparg('hs', 'n') ==# '' 159 | nmap hs (GitGutterStageHunk) 160 | endif 161 | if !hasmapto('(GitGutterUndoHunk)') && maparg('hu', 'n') ==# '' 162 | nmap hu (GitGutterUndoHunk) 163 | endif 164 | if !hasmapto('(GitGutterPreviewHunk)') && maparg('hp', 'n') ==# '' 165 | nmap hp (GitGutterPreviewHunk) 166 | endif 167 | 168 | if !hasmapto('(GitGutterTextObjectInnerPending)') && maparg('ic', 'o') ==# '' 169 | omap ic (GitGutterTextObjectInnerPending) 170 | endif 171 | if !hasmapto('(GitGutterTextObjectOuterPending)') && maparg('ac', 'o') ==# '' 172 | omap ac (GitGutterTextObjectOuterPending) 173 | endif 174 | if !hasmapto('(GitGutterTextObjectInnerVisual)') && maparg('ic', 'x') ==# '' 175 | xmap ic (GitGutterTextObjectInnerVisual) 176 | endif 177 | if !hasmapto('(GitGutterTextObjectOuterVisual)') && maparg('ac', 'x') ==# '' 178 | xmap ac (GitGutterTextObjectOuterVisual) 179 | endif 180 | 181 | call gitgutter#utility#setbufvar(bufnr, 'mapped', 1) 182 | endfunction 183 | 184 | function! s:setup_path(bufnr, continuation) 185 | if gitgutter#utility#has_repo_path(a:bufnr) | return | endif 186 | 187 | return gitgutter#utility#set_repo_path(a:bufnr, a:continuation) 188 | endfunction 189 | 190 | function! s:has_fresh_changes(bufnr) abort 191 | return getbufvar(a:bufnr, 'changedtick') != gitgutter#utility#getbufvar(a:bufnr, 'tick') 192 | endfunction 193 | 194 | function! s:reset_tick(bufnr) abort 195 | call gitgutter#utility#setbufvar(a:bufnr, 'tick', 0) 196 | endfunction 197 | 198 | function! s:clear(bufnr) 199 | call gitgutter#sign#clear_signs(a:bufnr) 200 | call gitgutter#hunk#reset(a:bufnr) 201 | call s:reset_tick(a:bufnr) 202 | call gitgutter#utility#setbufvar(a:bufnr, 'path', '') 203 | call gitgutter#utility#setbufvar(a:bufnr, 'basepath', '') 204 | endfunction 205 | 206 | 207 | " Note: 208 | " - this runs synchronously 209 | " - it ignores unsaved changes in buffers 210 | " - it does not change to the repo root 211 | function! gitgutter#quickfix(current_file) 212 | let cmd = gitgutter#git().' rev-parse --show-cdup' 213 | let path_to_repo = get(systemlist(cmd), 0, '') 214 | if !empty(path_to_repo) && path_to_repo[-1:] != '/' 215 | let path_to_repo .= '/' 216 | endif 217 | 218 | let locations = [] 219 | let cmd = gitgutter#git().' --no-pager'. 220 | \ ' diff --no-ext-diff --no-color -U0'. 221 | \ ' --src-prefix=a/'.path_to_repo.' --dst-prefix=b/'.path_to_repo.' '. 222 | \ g:gitgutter_diff_args. ' '. g:gitgutter_diff_base 223 | if a:current_file 224 | let cmd = cmd.' -- '.expand('%:p') 225 | endif 226 | let diff = systemlist(cmd) 227 | let lnum = 0 228 | for line in diff 229 | if line =~ '^diff --git [^"]' 230 | " No quotation mark therefore no spaces in filenames 231 | let [fnamel, fnamer] = split(line)[2:3] 232 | let fname = fnamel ==# fnamer ? fnamer : fnamer[2:] 233 | elseif line =~ '^diff --git "' 234 | " Quotation mark therefore do not split on space 235 | let [_, fnamel, _, fnamer] = split(line, '"') 236 | let fname = fnamel ==# fnamer ? fnamer : fnamer[2:] 237 | elseif line =~ '^diff --cc [^"]' 238 | let fname = line[10:] 239 | elseif line =~ '^diff --cc "' 240 | let [_, fname] = split(line, '"') 241 | elseif line =~ '^@@' 242 | let lnum = matchlist(line, '+\(\d\+\)')[1] 243 | elseif lnum > 0 244 | call add(locations, {'filename': fname, 'lnum': lnum, 'text': line}) 245 | let lnum = 0 246 | endif 247 | endfor 248 | if !g:gitgutter_use_location_list 249 | call setqflist(locations) 250 | else 251 | call setloclist(0, locations) 252 | endif 253 | endfunction 254 | 255 | 256 | function! gitgutter#difforig() 257 | let bufnr = bufnr('') 258 | let filetype = &filetype 259 | 260 | vertical new 261 | set buftype=nofile 262 | let &filetype = filetype 263 | 264 | if g:gitgutter_diff_relative_to ==# 'index' 265 | let index_name = gitgutter#utility#get_diff_base(bufnr).':'.gitgutter#utility#base_path(bufnr) 266 | let cmd = gitgutter#git(bufnr).' --no-pager show '.index_name 267 | " NOTE: this uses &shell to execute cmd. Perhaps we should use instead 268 | " gitgutter#utility's use_known_shell() / restore_shell() functions. 269 | silent! execute "read ++edit !" cmd 270 | else 271 | silent! execute "read ++edit" gitgutter#utility#repo_path(bufnr, 1) 272 | endif 273 | 274 | 0d_ 275 | diffthis 276 | wincmd p 277 | diffthis 278 | endfunction 279 | -------------------------------------------------------------------------------- /autoload/gitgutter/async.vim: -------------------------------------------------------------------------------- 1 | let s:available = has('nvim') || ( 2 | \ has('job') && ( 3 | \ (has('patch-7.4.1826') && !has('gui_running')) || 4 | \ (has('patch-7.4.1850') && has('gui_running')) || 5 | \ (has('patch-7.4.1832') && has('gui_macvim')) 6 | \ ) 7 | \ ) 8 | 9 | let s:jobs = {} 10 | 11 | function! gitgutter#async#available() 12 | return s:available 13 | endfunction 14 | 15 | 16 | function! gitgutter#async#execute(cmd, bufnr, handler) abort 17 | call gitgutter#debug#log('[async] '.a:cmd) 18 | 19 | let options = { 20 | \ 'stdoutbuffer': [], 21 | \ 'buffer': a:bufnr, 22 | \ 'handler': a:handler 23 | \ } 24 | let command = s:build_command(a:cmd) 25 | 26 | if has('nvim') 27 | call jobstart(command, extend(options, { 28 | \ 'on_stdout': function('s:on_stdout_nvim'), 29 | \ 'on_stderr': function('s:on_stderr_nvim'), 30 | \ 'on_exit': function('s:on_exit_nvim') 31 | \ })) 32 | else 33 | let job = job_start(command, { 34 | \ 'out_cb': function('s:on_stdout_vim', options), 35 | \ 'err_cb': function('s:on_stderr_vim', options), 36 | \ 'close_cb': function('s:on_exit_vim', options) 37 | \ }) 38 | let s:jobs[s:job_id(job)] = 1 39 | endif 40 | endfunction 41 | 42 | 43 | function! s:build_command(cmd) 44 | if has('unix') 45 | return ['sh', '-c', a:cmd] 46 | endif 47 | 48 | if has('win32') 49 | return has('nvim') ? a:cmd : 'cmd.exe /c '.a:cmd 50 | endif 51 | 52 | throw 'unknown os' 53 | endfunction 54 | 55 | 56 | function! s:on_stdout_nvim(_job_id, data, _event) dict abort 57 | if empty(self.stdoutbuffer) 58 | let self.stdoutbuffer = a:data 59 | else 60 | let self.stdoutbuffer = self.stdoutbuffer[:-2] + 61 | \ [self.stdoutbuffer[-1] . a:data[0]] + 62 | \ a:data[1:] 63 | endif 64 | endfunction 65 | 66 | function! s:on_stderr_nvim(_job_id, data, _event) dict abort 67 | if a:data != [''] " With Neovim there is always [''] reported on stderr. 68 | call self.handler.err(self.buffer) 69 | endif 70 | endfunction 71 | 72 | function! s:on_exit_nvim(_job_id, exit_code, _event) dict abort 73 | if !a:exit_code 74 | call self.handler.out(self.buffer, join(self.stdoutbuffer, "\n")) 75 | endif 76 | endfunction 77 | 78 | 79 | function! s:on_stdout_vim(_channel, data) dict abort 80 | call add(self.stdoutbuffer, a:data) 81 | endfunction 82 | 83 | function! s:on_stderr_vim(channel, _data) dict abort 84 | call self.handler.err(self.buffer) 85 | endfunction 86 | 87 | function! s:on_exit_vim(channel) dict abort 88 | let job = ch_getjob(a:channel) 89 | let jobid = s:job_id(job) 90 | if has_key(s:jobs, jobid) | unlet s:jobs[jobid] | endif 91 | while 1 92 | if job_status(job) == 'dead' 93 | let exit_code = job_info(job).exitval 94 | break 95 | endif 96 | sleep 5m 97 | endwhile 98 | 99 | if !exit_code 100 | call self.handler.out(self.buffer, join(self.stdoutbuffer, "\n")) 101 | endif 102 | endfunction 103 | 104 | function! s:job_id(job) 105 | " Vim 106 | return job_info(a:job).process 107 | endfunction 108 | -------------------------------------------------------------------------------- /autoload/gitgutter/debug.vim: -------------------------------------------------------------------------------- 1 | let s:plugin_dir = expand(':p:h:h:h').'/' 2 | let s:log_file = s:plugin_dir.'gitgutter.log' 3 | let s:channel_log = s:plugin_dir.'channel.log' 4 | let s:new_log_session = 1 5 | 6 | 7 | function! gitgutter#debug#debug() 8 | " Open a scratch buffer 9 | vsplit __GitGutter_Debug__ 10 | normal! ggdG 11 | setlocal buftype=nofile 12 | setlocal bufhidden=delete 13 | setlocal noswapfile 14 | 15 | call s:vim_version() 16 | call s:separator() 17 | 18 | call s:git_version() 19 | call s:separator() 20 | 21 | call s:grep_version() 22 | call s:separator() 23 | 24 | call s:option('updatetime') 25 | endfunction 26 | 27 | 28 | function! s:separator() 29 | call s:output('') 30 | endfunction 31 | 32 | function! s:vim_version() 33 | redir => version_info 34 | silent execute 'version' 35 | redir END 36 | call s:output(split(version_info, '\n')[0:2]) 37 | endfunction 38 | 39 | function! s:git_version() 40 | let v = system(g:gitgutter_git_executable.' --version') 41 | call s:output( substitute(v, '\n$', '', '') ) 42 | endfunction 43 | 44 | function! s:grep_version() 45 | let v = system(g:gitgutter_grep.' --version') 46 | call s:output( substitute(v, '\n$', '', '') ) 47 | 48 | let v = system(g:gitgutter_grep.' --help') 49 | call s:output( substitute(v, '\%x00', '', 'g') ) 50 | endfunction 51 | 52 | function! s:option(name) 53 | if exists('+' . a:name) 54 | let v = eval('&' . a:name) 55 | call s:output(a:name . '=' . v) 56 | " redir => output 57 | " silent execute "verbose set " . a:name . "?" 58 | " redir END 59 | " call s:output(a:name . '=' . output) 60 | else 61 | call s:output(a:name . ' [n/a]') 62 | end 63 | endfunction 64 | 65 | function! s:output(text) 66 | call append(line('$'), a:text) 67 | endfunction 68 | 69 | " assumes optional args are calling function's optional args 70 | function! gitgutter#debug#log(message, ...) abort 71 | if g:gitgutter_log 72 | if s:new_log_session && gitgutter#async#available() 73 | if exists('*ch_logfile') 74 | call ch_logfile(s:channel_log, 'w') 75 | endif 76 | endif 77 | 78 | if s:new_log_session 79 | let s:start = reltime() 80 | call writefile(['', '========== start log session '.strftime('%d.%m.%Y %H:%M:%S').' =========='], s:log_file, 'a') 81 | endif 82 | 83 | let elapsed = reltimestr(reltime(s:start)).' ' 84 | call writefile([''], s:log_file, 'a') 85 | " callers excluding this function 86 | call writefile([elapsed.expand('')[:-22].':'], s:log_file, 'a') 87 | call writefile([elapsed.s:format_for_log(a:message)], s:log_file, 'a') 88 | if a:0 && !empty(a:1) 89 | for msg in a:000 90 | call writefile([elapsed.s:format_for_log(msg)], s:log_file, 'a') 91 | endfor 92 | endif 93 | 94 | let s:new_log_session = 0 95 | endif 96 | endfunction 97 | 98 | function! s:format_for_log(data) abort 99 | if type(a:data) == 1 100 | return join(split(a:data,'\n'),"\n") 101 | elseif type(a:data) == 3 102 | return '['.join(a:data,"\n").']' 103 | else 104 | return a:data 105 | endif 106 | endfunction 107 | 108 | -------------------------------------------------------------------------------- /autoload/gitgutter/diff.vim: -------------------------------------------------------------------------------- 1 | scriptencoding utf8 2 | 3 | let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '' : '' 4 | 5 | let s:hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@' 6 | 7 | let s:temp_from = tempname() 8 | let s:temp_buffer = tempname() 9 | let s:counter = 0 10 | 11 | " Returns a diff of the buffer against the index or the working tree. 12 | " 13 | " After running the diff we pass it through grep where available to reduce 14 | " subsequent processing by the plugin. If grep is not available the plugin 15 | " does the filtering instead. 16 | " 17 | " When diffing against the index: 18 | " 19 | " The buffer contents is not the same as the file on disk so we need to pass 20 | " two instances of the file to git-diff: 21 | " 22 | " git diff myfileA myfileB 23 | " 24 | " where myfileA comes from 25 | " 26 | " git show :myfile > myfileA 27 | " 28 | " and myfileB is the buffer contents. 29 | " 30 | " Regarding line endings: 31 | " 32 | " git-show does not convert line endings. 33 | " git-diff FILE FILE does convert line endings for the given files. 34 | " 35 | " If a file has CRLF line endings and git's core.autocrlf is true, 36 | " the file in git's object store will have LF line endings. Writing 37 | " it out via git-show will produce a file with LF line endings. 38 | " 39 | " If this last file is one of the files passed to git-diff, git-diff will 40 | " convert its line endings to CRLF before diffing -- which is what we want -- 41 | " but also by default output a warning on stderr. 42 | " 43 | " warning: LF will be replace by CRLF in . 44 | " The file will have its original line endings in your working directory. 45 | " 46 | " When running the diff asynchronously, the warning message triggers the stderr 47 | " callbacks which assume the overall command has failed and reset all the 48 | " signs. As this is not what we want, and we can safely ignore the warning, 49 | " we turn it off by passing the '-c "core.safecrlf=false"' argument to 50 | " git-diff. 51 | " 52 | " When writing the temporary files we preserve the original file's extension 53 | " so that repos using .gitattributes to control EOL conversion continue to 54 | " convert correctly. 55 | " 56 | " Arguments: 57 | " 58 | " bufnr - the number of the buffer to be diffed 59 | " from - 'index' or 'working_tree'; what the buffer is diffed against 60 | " preserve_full_diff - truthy to return the full diff or falsey to return only 61 | " the hunk headers (@@ -x,y +m,n @@); only possible if 62 | " grep is available. 63 | function! gitgutter#diff#run_diff(bufnr, from, preserve_full_diff) abort 64 | if gitgutter#utility#repo_path(a:bufnr, 0) == -1 65 | throw 'gitgutter path not set' 66 | endif 67 | 68 | if gitgutter#utility#repo_path(a:bufnr, 0) == -2 69 | throw 'gitgutter not tracked' 70 | endif 71 | 72 | if gitgutter#utility#repo_path(a:bufnr, 0) == -3 73 | throw 'gitgutter assume unchanged' 74 | endif 75 | 76 | " Wrap compound commands in parentheses to make Windows happy. 77 | " bash doesn't mind the parentheses. 78 | let cmd = '(' 79 | 80 | " Append buffer number to temp filenames to avoid race conditions between 81 | " writing and reading the files when asynchronously processing multiple 82 | " buffers. 83 | 84 | " Without the buffer number, buff_file would have a race between the 85 | " second gitgutter#process_buffer() writing the file (synchronously, below) 86 | " and the first gitgutter#process_buffer()'s async job reading it (with 87 | " git-diff). 88 | let buff_file = s:temp_buffer.'.'.a:bufnr 89 | 90 | " Add a counter to avoid a similar race with two quick writes of the same buffer. 91 | " Use a modulus greater than a maximum reasonable number of visible buffers. 92 | let s:counter = (s:counter + 1) % 20 93 | let buff_file .= '.'.s:counter 94 | 95 | let extension = gitgutter#utility#extension(a:bufnr) 96 | if !empty(extension) 97 | let buff_file .= '.'.extension 98 | endif 99 | 100 | " Write buffer to temporary file. 101 | " Note: this is synchronous. 102 | call s:write_buffer(a:bufnr, buff_file) 103 | 104 | if a:from ==# 'index' 105 | " Without the buffer number, from_file would have a race in the shell 106 | " between the second process writing it (with git-show) and the first 107 | " reading it (with git-diff). 108 | let from_file = s:temp_from.'.'.a:bufnr 109 | 110 | " Add a counter to avoid a similar race with two quick writes of the same buffer. 111 | let from_file .= '.'.s:counter 112 | 113 | if !empty(extension) 114 | let from_file .= '.'.extension 115 | endif 116 | 117 | " Write file from index to temporary file. 118 | let index_name = gitgutter#utility#get_diff_base(a:bufnr).':'.gitgutter#utility#base_path(a:bufnr) 119 | let cmd .= gitgutter#git(a:bufnr).' --no-pager show --textconv '.index_name 120 | let cmd .= ' > '.gitgutter#utility#shellescape(from_file).' || exit 0) && (' 121 | 122 | elseif a:from ==# 'working_tree' 123 | let from_file = gitgutter#utility#repo_path(a:bufnr, 1) 124 | endif 125 | 126 | " Call git-diff. 127 | let cmd .= gitgutter#git(a:bufnr).' --no-pager' 128 | if gitgutter#utility#git_supports_command_line_config_override() 129 | let cmd .= ' -c "diff.autorefreshindex=0"' 130 | let cmd .= ' -c "diff.noprefix=false"' 131 | let cmd .= ' -c "core.safecrlf=false"' 132 | endif 133 | let cmd .= ' diff --no-ext-diff --no-color -U0 '.g:gitgutter_diff_args 134 | let cmd .= ' -- '.gitgutter#utility#shellescape(from_file).' '.gitgutter#utility#shellescape(buff_file) 135 | 136 | " Pipe git-diff output into grep. 137 | if !a:preserve_full_diff && !empty(g:gitgutter_grep) 138 | let cmd .= ' | '.g:gitgutter_grep.' '.gitgutter#utility#shellescape('^@@ ') 139 | endif 140 | 141 | " grep exits with 1 when no matches are found; git-diff exits with 1 when 142 | " differences are found. However we want to treat non-matches and 143 | " differences as non-erroneous behaviour; so we OR the command with one 144 | " which always exits with success (0). 145 | let cmd .= ' || exit 0' 146 | 147 | let cmd .= ')' 148 | 149 | if g:gitgutter_async && gitgutter#async#available() 150 | call gitgutter#async#execute(cmd, a:bufnr, { 151 | \ 'out': function('gitgutter#diff#handler'), 152 | \ 'err': function('gitgutter#hunk#reset'), 153 | \ }) 154 | return 'async' 155 | 156 | else 157 | let [diff, error_code] = gitgutter#utility#system(cmd) 158 | 159 | if error_code 160 | call gitgutter#debug#log(diff) 161 | throw 'gitgutter diff failed' 162 | endif 163 | 164 | return diff 165 | endif 166 | endfunction 167 | 168 | 169 | function! gitgutter#diff#handler(bufnr, diff) abort 170 | call gitgutter#debug#log(a:diff) 171 | 172 | if !bufexists(a:bufnr) 173 | return 174 | endif 175 | 176 | call gitgutter#hunk#set_hunks(a:bufnr, gitgutter#diff#parse_diff(a:diff)) 177 | let modified_lines = gitgutter#diff#process_hunks(a:bufnr, gitgutter#hunk#hunks(a:bufnr)) 178 | 179 | let signs_count = len(modified_lines) 180 | if g:gitgutter_max_signs != -1 && signs_count > g:gitgutter_max_signs 181 | call gitgutter#utility#warn_once(a:bufnr, printf( 182 | \ 'exceeded maximum number of signs (%d > %d, configured by g:gitgutter_max_signs).', 183 | \ signs_count, g:gitgutter_max_signs), 'max_signs') 184 | call gitgutter#sign#clear_signs(a:bufnr) 185 | 186 | else 187 | if g:gitgutter_signs || g:gitgutter_highlight_lines || g:gitgutter_highlight_linenrs 188 | call gitgutter#sign#update_signs(a:bufnr, modified_lines) 189 | endif 190 | endif 191 | 192 | call s:save_last_seen_change(a:bufnr) 193 | if exists('#User#GitGutter') 194 | let g:gitgutter_hook_context = {'bufnr': a:bufnr} 195 | execute 'doautocmd' s:nomodeline 'User GitGutter' 196 | unlet g:gitgutter_hook_context 197 | endif 198 | endfunction 199 | 200 | 201 | function! gitgutter#diff#parse_diff(diff) abort 202 | let hunks = [] 203 | for line in split(a:diff, '\n') 204 | let hunk_info = gitgutter#diff#parse_hunk(line) 205 | if len(hunk_info) == 4 206 | call add(hunks, hunk_info) 207 | endif 208 | endfor 209 | return hunks 210 | endfunction 211 | 212 | function! gitgutter#diff#parse_hunk(line) abort 213 | let matches = matchlist(a:line, s:hunk_re) 214 | if len(matches) > 0 215 | let from_line = str2nr(matches[1]) 216 | let from_count = (matches[2] == '') ? 1 : str2nr(matches[2]) 217 | let to_line = str2nr(matches[3]) 218 | let to_count = (matches[4] == '') ? 1 : str2nr(matches[4]) 219 | return [from_line, from_count, to_line, to_count] 220 | else 221 | return [] 222 | end 223 | endfunction 224 | 225 | " This function is public so it may be used by other plugins 226 | " e.g. vim-signature. 227 | function! gitgutter#diff#process_hunks(bufnr, hunks) abort 228 | let modified_lines = [] 229 | for hunk in a:hunks 230 | call extend(modified_lines, s:process_hunk(a:bufnr, hunk)) 231 | endfor 232 | return modified_lines 233 | endfunction 234 | 235 | " Returns [ [, ], ...] 236 | function! s:process_hunk(bufnr, hunk) abort 237 | let modifications = [] 238 | let from_line = a:hunk[0] 239 | let from_count = a:hunk[1] 240 | let to_line = a:hunk[2] 241 | let to_count = a:hunk[3] 242 | 243 | if s:is_added(from_count, to_count) 244 | call s:process_added(modifications, from_count, to_count, to_line) 245 | call gitgutter#hunk#increment_lines_added(a:bufnr, to_count) 246 | 247 | elseif s:is_removed(from_count, to_count) 248 | call s:process_removed(modifications, from_count, to_count, to_line) 249 | call gitgutter#hunk#increment_lines_removed(a:bufnr, from_count) 250 | 251 | elseif s:is_modified(from_count, to_count) 252 | call s:process_modified(modifications, from_count, to_count, to_line) 253 | call gitgutter#hunk#increment_lines_modified(a:bufnr, to_count) 254 | 255 | elseif s:is_modified_and_added(from_count, to_count) 256 | call s:process_modified_and_added(modifications, from_count, to_count, to_line) 257 | call gitgutter#hunk#increment_lines_added(a:bufnr, to_count - from_count) 258 | call gitgutter#hunk#increment_lines_modified(a:bufnr, from_count) 259 | 260 | elseif s:is_modified_and_removed(from_count, to_count) 261 | call s:process_modified_and_removed(modifications, from_count, to_count, to_line) 262 | call gitgutter#hunk#increment_lines_modified(a:bufnr, to_count) 263 | call gitgutter#hunk#increment_lines_removed(a:bufnr, from_count - to_count) 264 | 265 | endif 266 | return modifications 267 | endfunction 268 | 269 | function! s:is_added(from_count, to_count) abort 270 | return a:from_count == 0 && a:to_count > 0 271 | endfunction 272 | 273 | function! s:is_removed(from_count, to_count) abort 274 | return a:from_count > 0 && a:to_count == 0 275 | endfunction 276 | 277 | function! s:is_modified(from_count, to_count) abort 278 | return a:from_count > 0 && a:to_count > 0 && a:from_count == a:to_count 279 | endfunction 280 | 281 | function! s:is_modified_and_added(from_count, to_count) abort 282 | return a:from_count > 0 && a:to_count > 0 && a:from_count < a:to_count 283 | endfunction 284 | 285 | function! s:is_modified_and_removed(from_count, to_count) abort 286 | return a:from_count > 0 && a:to_count > 0 && a:from_count > a:to_count 287 | endfunction 288 | 289 | function! s:process_added(modifications, from_count, to_count, to_line) abort 290 | let offset = 0 291 | while offset < a:to_count 292 | let line_number = a:to_line + offset 293 | call add(a:modifications, [line_number, 'added']) 294 | let offset += 1 295 | endwhile 296 | endfunction 297 | 298 | function! s:process_removed(modifications, from_count, to_count, to_line) abort 299 | if a:to_line == 0 300 | call add(a:modifications, [1, 'removed_first_line']) 301 | else 302 | call add(a:modifications, [a:to_line, 'removed']) 303 | endif 304 | endfunction 305 | 306 | function! s:process_modified(modifications, from_count, to_count, to_line) abort 307 | let offset = 0 308 | while offset < a:to_count 309 | let line_number = a:to_line + offset 310 | call add(a:modifications, [line_number, 'modified']) 311 | let offset += 1 312 | endwhile 313 | endfunction 314 | 315 | function! s:process_modified_and_added(modifications, from_count, to_count, to_line) abort 316 | let offset = 0 317 | while offset < a:from_count 318 | let line_number = a:to_line + offset 319 | call add(a:modifications, [line_number, 'modified']) 320 | let offset += 1 321 | endwhile 322 | while offset < a:to_count 323 | let line_number = a:to_line + offset 324 | call add(a:modifications, [line_number, 'added']) 325 | let offset += 1 326 | endwhile 327 | endfunction 328 | 329 | function! s:process_modified_and_removed(modifications, from_count, to_count, to_line) abort 330 | let offset = 0 331 | while offset < a:to_count 332 | let line_number = a:to_line + offset 333 | call add(a:modifications, [line_number, 'modified']) 334 | let offset += 1 335 | endwhile 336 | let a:modifications[-1] = [a:to_line + offset - 1, 'modified_removed'] 337 | endfunction 338 | 339 | 340 | " Returns a diff for the current hunk. 341 | " Assumes there is only 1 current hunk unless the optional argument is given, 342 | " in which case the cursor is in two hunks and the argument specifies the one 343 | " to choose. 344 | " 345 | " Optional argument: 0 (to use the first hunk) or 1 (to use the second). 346 | function! gitgutter#diff#hunk_diff(bufnr, full_diff, ...) 347 | let modified_diff = [] 348 | let hunk_index = 0 349 | let keep_line = 1 350 | " Don't keepempty when splitting because the diff we want may not be the 351 | " final one. Instead add trailing NL at end of function. 352 | for line in split(a:full_diff, '\n') 353 | let hunk_info = gitgutter#diff#parse_hunk(line) 354 | if len(hunk_info) == 4 " start of new hunk 355 | let keep_line = gitgutter#hunk#cursor_in_hunk(hunk_info) 356 | 357 | if a:0 && hunk_index != a:1 358 | let keep_line = 0 359 | endif 360 | 361 | let hunk_index += 1 362 | endif 363 | if keep_line 364 | call add(modified_diff, line) 365 | endif 366 | endfor 367 | return join(modified_diff, "\n")."\n" 368 | endfunction 369 | 370 | 371 | function! s:write_buffer(bufnr, file) 372 | let bufcontents = getbufline(a:bufnr, 1, '$') 373 | 374 | if bufcontents == [''] && line2byte(1) == -1 375 | " Special case: completely empty buffer. 376 | " A nearly empty buffer of only a newline has line2byte(1) == 1. 377 | call writefile([], a:file) 378 | return 379 | endif 380 | 381 | if getbufvar(a:bufnr, '&fileformat') ==# 'dos' 382 | if getbufvar(a:bufnr, '&endofline') 383 | call map(bufcontents, 'v:val."\r"') 384 | else 385 | for i in range(len(bufcontents) - 1) 386 | let bufcontents[i] = bufcontents[i] . "\r" 387 | endfor 388 | endif 389 | endif 390 | 391 | if getbufvar(a:bufnr, '&endofline') 392 | call add(bufcontents, '') 393 | endif 394 | 395 | let fenc = getbufvar(a:bufnr, '&fileencoding') 396 | if fenc !=# &encoding 397 | call map(bufcontents, 'iconv(v:val, &encoding, "'.fenc.'")') 398 | endif 399 | 400 | if getbufvar(a:bufnr, '&bomb') 401 | let bufcontents[0]=''.bufcontents[0] 402 | endif 403 | 404 | " The file we are writing to is a temporary file. Sometimes the parent 405 | " directory is deleted outside Vim but, because Vim caches the directory 406 | " name at startup and does not check for its existence subsequently, Vim 407 | " does not realise. This causes E482 errors. 408 | try 409 | call writefile(bufcontents, a:file, 'b') 410 | catch /E482/ 411 | call mkdir(fnamemodify(a:file, ':h'), '', '0700') 412 | call writefile(bufcontents, a:file, 'b') 413 | endtry 414 | endfunction 415 | 416 | 417 | function! s:save_last_seen_change(bufnr) abort 418 | call gitgutter#utility#setbufvar(a:bufnr, 'tick', getbufvar(a:bufnr, 'changedtick')) 419 | endfunction 420 | -------------------------------------------------------------------------------- /autoload/gitgutter/diff_highlight.vim: -------------------------------------------------------------------------------- 1 | " This is the minimum number of characters required between regions of change 2 | " in a line. It's somewhat arbitrary: higher values mean less visual busyness; 3 | " lower values mean more detail. 4 | let s:gap_between_regions = 5 5 | 6 | 7 | " Calculates the changed portions of lines. 8 | " 9 | " Based on: 10 | " 11 | " - diff-highlight (included with git) 12 | " https://github.com/git/git/blob/master/contrib/diff-highlight/DiffHighlight.pm 13 | " 14 | " - Diff Strategies, Neil Fraser 15 | " https://neil.fraser.name/writing/diff/ 16 | 17 | 18 | " Returns a list of intra-line changed regions. 19 | " Each element is a list: 20 | " 21 | " [ 22 | " line number (1-based), 23 | " type ('+' or '-'), 24 | " start column (1-based, inclusive), 25 | " stop column (1-based, inclusive), 26 | " ] 27 | " 28 | " Args: 29 | " hunk_body - list of lines 30 | function! gitgutter#diff_highlight#process(hunk_body) 31 | " Check whether we have the same number of lines added as removed. 32 | let [removed, added] = [0, 0] 33 | for line in a:hunk_body 34 | if line[0] == '-' 35 | let removed += 1 36 | elseif line[0] == '+' 37 | let added += 1 38 | endif 39 | endfor 40 | if removed != added 41 | return [] 42 | endif 43 | 44 | let regions = [] 45 | 46 | for i in range(removed) 47 | " pair lines by position 48 | let rline = a:hunk_body[i] 49 | let aline = a:hunk_body[i + removed] 50 | 51 | call s:diff(rline, aline, i, i+removed, 0, 0, regions, 1) 52 | endfor 53 | 54 | return regions 55 | endfunction 56 | 57 | 58 | function! s:diff(rline, aline, rlinenr, alinenr, rprefix, aprefix, regions, whole_line) 59 | " diff marker does not count as a difference in prefix 60 | let start = a:whole_line ? 1 : 0 61 | let prefix = s:common_prefix(a:rline[start:], a:aline[start:]) 62 | if a:whole_line 63 | let prefix += 1 64 | endif 65 | let [rsuffix, asuffix] = s:common_suffix(a:rline, a:aline, prefix+1) 66 | 67 | " region of change (common prefix and suffix removed) 68 | let rtext = a:rline[prefix+1:rsuffix-1] 69 | let atext = a:aline[prefix+1:asuffix-1] 70 | 71 | " singular insertion 72 | if empty(rtext) 73 | if !a:whole_line || len(atext) != len(a:aline) " not whole line 74 | call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+asuffix+1-1]) 75 | endif 76 | return 77 | endif 78 | 79 | " singular deletion 80 | if empty(atext) 81 | if !a:whole_line || len(rtext) != len(a:rline) " not whole line 82 | call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+rsuffix+1-1]) 83 | endif 84 | return 85 | endif 86 | 87 | " two insertions 88 | let j = stridx(atext, rtext) 89 | if j != -1 90 | call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+prefix+j+1]) 91 | call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1+j+len(rtext), a:aprefix+asuffix+1-1]) 92 | return 93 | endif 94 | 95 | " two deletions 96 | let j = stridx(rtext, atext) 97 | if j != -1 98 | call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+prefix+j+1]) 99 | call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1+j+len(atext), a:rprefix+rsuffix+1-1]) 100 | return 101 | endif 102 | 103 | " two edits 104 | let lcs = s:lcs(rtext, atext) 105 | " TODO do we need to ensure we don't get more than 2 elements when splitting? 106 | if len(lcs) > s:gap_between_regions 107 | let redits = s:split(rtext, lcs) 108 | let aedits = s:split(atext, lcs) 109 | call s:diff(redits[0], aedits[0], a:rlinenr, a:alinenr, a:rprefix+prefix+1, a:aprefix+prefix+1, a:regions, 0) 110 | call s:diff(redits[1], aedits[1], a:rlinenr, a:alinenr, a:rprefix+prefix+1+len(redits[0])+len(lcs), a:aprefix+prefix+1+len(aedits[0])+len(lcs), a:regions, 0) 111 | return 112 | endif 113 | 114 | " fall back to highlighting entire changed area 115 | 116 | " if a change (but not the whole line) 117 | if !a:whole_line || ((prefix != 0 || rsuffix != len(a:rline)) && prefix+1 < rsuffix) 118 | call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+rsuffix+1-1]) 119 | endif 120 | 121 | " if a change (but not the whole line) 122 | if !a:whole_line || ((prefix != 0 || asuffix != len(a:aline)) && prefix+1 < asuffix) 123 | call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+asuffix+1-1]) 124 | endif 125 | endfunction 126 | 127 | 128 | function! s:lcs(s1, s2) 129 | if empty(a:s1) || empty(a:s2) 130 | return '' 131 | endif 132 | 133 | let matrix = map(repeat([repeat([0], len(a:s2)+1)], len(a:s1)+1), 'copy(v:val)') 134 | 135 | let maxlength = 0 136 | let endindex = len(a:s1) 137 | 138 | for i in range(1, len(a:s1)) 139 | for j in range(1, len(a:s2)) 140 | if a:s1[i-1] ==# a:s2[j-1] 141 | let matrix[i][j] = 1 + matrix[i-1][j-1] 142 | if matrix[i][j] > maxlength 143 | let maxlength = matrix[i][j] 144 | let endindex = i - 1 145 | endif 146 | endif 147 | endfor 148 | endfor 149 | 150 | return a:s1[endindex - maxlength + 1 : endindex] 151 | endfunction 152 | 153 | 154 | " Returns 0-based index of last character of common prefix 155 | " If there is no common prefix, returns -1. 156 | " 157 | " a, b - strings 158 | " 159 | function! s:common_prefix(a, b) 160 | let len = min([len(a:a), len(a:b)]) 161 | if len == 0 162 | return -1 163 | endif 164 | for i in range(len) 165 | if a:a[i:i] !=# a:b[i:i] 166 | return i - 1 167 | endif 168 | endfor 169 | return i 170 | endfunction 171 | 172 | 173 | " Returns 0-based indices of start of common suffix 174 | " 175 | " a, b - strings 176 | " start - 0-based index to start from 177 | function! s:common_suffix(a, b, start) 178 | let [sa, sb] = [len(a:a), len(a:b)] 179 | while sa >= a:start && sb >= a:start 180 | if a:a[sa] ==# a:b[sb] 181 | let sa -= 1 182 | let sb -= 1 183 | else 184 | break 185 | endif 186 | endwhile 187 | return [sa+1, sb+1] 188 | endfunction 189 | 190 | 191 | " Split a string on another string. 192 | " Assumes 1 occurrence of the delimiter. 193 | function! s:split(str, delimiter) 194 | let i = stridx(a:str, a:delimiter) 195 | 196 | if i == 0 197 | return ['', a:str[len(a:delimiter):]] 198 | endif 199 | 200 | return [a:str[:i-1], a:str[i+len(a:delimiter):]] 201 | endfunction 202 | -------------------------------------------------------------------------------- /autoload/gitgutter/fold.vim: -------------------------------------------------------------------------------- 1 | function! gitgutter#fold#enable() 2 | call s:save_fold_state() 3 | 4 | call s:set_fold_levels() 5 | setlocal foldexpr=gitgutter#fold#level(v:lnum) 6 | setlocal foldmethod=expr 7 | setlocal foldlevel=0 8 | setlocal foldenable 9 | 10 | call gitgutter#utility#setbufvar(bufnr(''), 'folded', 1) 11 | endfunction 12 | 13 | 14 | function! gitgutter#fold#disable() 15 | call s:restore_fold_state() 16 | call gitgutter#utility#setbufvar(bufnr(''), 'folded', 0) 17 | endfunction 18 | 19 | 20 | function! gitgutter#fold#toggle() 21 | if s:folded() 22 | call gitgutter#fold#disable() 23 | else 24 | call gitgutter#fold#enable() 25 | endif 26 | endfunction 27 | 28 | 29 | function! gitgutter#fold#level(lnum) 30 | return gitgutter#utility#getbufvar(bufnr(''), 'fold_levels')[a:lnum] 31 | endfunction 32 | 33 | 34 | function! gitgutter#fold#foldtext() 35 | if !gitgutter#fold#is_changed() 36 | return foldtext() 37 | endif 38 | 39 | return substitute(foldtext(), ':', ' (*):', '') 40 | endfunction 41 | 42 | 43 | " Returns 1 if any of the folded lines have been changed 44 | " (added, removed, or modified), 0 otherwise. 45 | function! gitgutter#fold#is_changed() 46 | for hunk in gitgutter#hunk#hunks(bufnr('')) 47 | let hunk_begin = hunk[2] 48 | let hunk_end = hunk[2] + (hunk[3] == 0 ? 1 : hunk[3]) 49 | 50 | if hunk_end < v:foldstart 51 | continue 52 | endif 53 | 54 | if hunk_begin > v:foldend 55 | break 56 | endif 57 | 58 | return 1 59 | endfor 60 | 61 | return 0 62 | endfunction 63 | 64 | 65 | " A line in a hunk has a fold level of 0. 66 | " A line within 3 lines of a hunk has a fold level of 1. 67 | " All other lines have a fold level of 2. 68 | function! s:set_fold_levels() 69 | let fold_levels = [''] 70 | 71 | for lnum in range(1, line('$')) 72 | let in_hunk = gitgutter#hunk#in_hunk(lnum) 73 | call add(fold_levels, (in_hunk ? 0 : 2)) 74 | endfor 75 | 76 | let lines_of_context = 3 77 | 78 | for lnum in range(1, line('$')) 79 | if fold_levels[lnum] == 2 80 | let pre = lnum >= 3 ? lnum - lines_of_context : 0 81 | let post = lnum + lines_of_context 82 | if index(fold_levels[pre:post], 0) != -1 83 | let fold_levels[lnum] = 1 84 | endif 85 | endif 86 | endfor 87 | 88 | call gitgutter#utility#setbufvar(bufnr(''), 'fold_levels', fold_levels) 89 | endfunction 90 | 91 | 92 | function! s:save_fold_state() 93 | let bufnr = bufnr('') 94 | call gitgutter#utility#setbufvar(bufnr, 'foldlevel', &foldlevel) 95 | call gitgutter#utility#setbufvar(bufnr, 'foldmethod', &foldmethod) 96 | if &foldmethod ==# 'manual' 97 | mkview 98 | endif 99 | endfunction 100 | 101 | function! s:restore_fold_state() 102 | let bufnr = bufnr('') 103 | let &foldlevel = gitgutter#utility#getbufvar(bufnr, 'foldlevel') 104 | let &foldmethod = gitgutter#utility#getbufvar(bufnr, 'foldmethod') 105 | if &foldmethod ==# 'manual' 106 | loadview 107 | else 108 | normal! zx 109 | endif 110 | endfunction 111 | 112 | function! s:folded() 113 | return gitgutter#utility#getbufvar(bufnr(''), 'folded') 114 | endfunction 115 | 116 | -------------------------------------------------------------------------------- /autoload/gitgutter/highlight.vim: -------------------------------------------------------------------------------- 1 | function! gitgutter#highlight#line_disable() abort 2 | let g:gitgutter_highlight_lines = 0 3 | call s:define_sign_line_highlights() 4 | 5 | if !g:gitgutter_signs 6 | call gitgutter#sign#clear_signs(bufnr('')) 7 | endif 8 | 9 | redraw! 10 | endfunction 11 | 12 | function! gitgutter#highlight#line_enable() abort 13 | let old_highlight_lines = g:gitgutter_highlight_lines 14 | 15 | let g:gitgutter_highlight_lines = 1 16 | call s:define_sign_line_highlights() 17 | 18 | if !old_highlight_lines && !g:gitgutter_signs 19 | call gitgutter#all(1) 20 | endif 21 | 22 | redraw! 23 | endfunction 24 | 25 | function! gitgutter#highlight#line_toggle() abort 26 | if g:gitgutter_highlight_lines 27 | call gitgutter#highlight#line_disable() 28 | else 29 | call gitgutter#highlight#line_enable() 30 | endif 31 | endfunction 32 | 33 | 34 | function! gitgutter#highlight#linenr_disable() abort 35 | let g:gitgutter_highlight_linenrs = 0 36 | call s:define_sign_linenr_highlights() 37 | 38 | if !g:gitgutter_signs 39 | call gitgutter#sign#clear_signs(bufnr('')) 40 | endif 41 | 42 | redraw! 43 | endfunction 44 | 45 | function! gitgutter#highlight#linenr_enable() abort 46 | let old_highlight_linenrs = g:gitgutter_highlight_linenrs 47 | 48 | let g:gitgutter_highlight_linenrs = 1 49 | call s:define_sign_linenr_highlights() 50 | 51 | if !old_highlight_linenrs && !g:gitgutter_signs 52 | call gitgutter#all(1) 53 | endif 54 | 55 | redraw! 56 | endfunction 57 | 58 | function! gitgutter#highlight#linenr_toggle() abort 59 | if g:gitgutter_highlight_linenrs 60 | call gitgutter#highlight#linenr_disable() 61 | else 62 | call gitgutter#highlight#linenr_enable() 63 | endif 64 | endfunction 65 | 66 | 67 | function! gitgutter#highlight#define_highlights() abort 68 | let [guibg, ctermbg] = s:get_background_colors('SignColumn') 69 | 70 | " Highlights used by the signs. 71 | 72 | " When they are invisible. 73 | execute "highlight GitGutterAddInvisible guifg=bg guibg=" . guibg . " ctermfg=" . ctermbg . " ctermbg=" . ctermbg 74 | execute "highlight GitGutterChangeInvisible guifg=bg guibg=" . guibg . " ctermfg=" . ctermbg . " ctermbg=" . ctermbg 75 | execute "highlight GitGutterDeleteInvisible guifg=bg guibg=" . guibg . " ctermfg=" . ctermbg . " ctermbg=" . ctermbg 76 | highlight default link GitGutterChangeDeleteInvisible GitGutterChangeInvisible 77 | 78 | " When they are visible. 79 | for type in ["Add", "Change", "Delete"] 80 | if hlexists("GitGutter".type) && s:get_foreground_colors("GitGutter".type) != ['NONE', 'NONE'] 81 | if g:gitgutter_set_sign_backgrounds 82 | execute "highlight GitGutter".type." guibg=".guibg." ctermbg=".ctermbg 83 | endif 84 | continue 85 | elseif s:useful_diff_colours() 86 | let [guifg, ctermfg] = s:get_foreground_colors('Diff'.type) 87 | else 88 | let [guifg, ctermfg] = s:get_foreground_fallback_colors(type) 89 | endif 90 | execute "highlight GitGutter".type." guifg=".guifg." guibg=".guibg." ctermfg=".ctermfg." ctermbg=".ctermbg 91 | endfor 92 | 93 | if hlexists("GitGutterChangeDelete") && g:gitgutter_set_sign_backgrounds 94 | execute "highlight GitGutterChangeDelete guibg=".guibg." ctermbg=".ctermbg 95 | endif 96 | 97 | highlight default link GitGutterChangeDelete GitGutterChange 98 | 99 | " Highlights used for the whole line. 100 | 101 | highlight default link GitGutterAddLine DiffAdd 102 | highlight default link GitGutterChangeLine DiffChange 103 | highlight default link GitGutterDeleteLine DiffDelete 104 | highlight default link GitGutterChangeDeleteLine GitGutterChangeLine 105 | 106 | highlight default link GitGutterAddLineNr CursorLineNr 107 | highlight default link GitGutterChangeLineNr CursorLineNr 108 | highlight default link GitGutterDeleteLineNr CursorLineNr 109 | highlight default link GitGutterChangeDeleteLineNr GitGutterChangeLineNr 110 | 111 | " Highlights used intra line. 112 | highlight default GitGutterAddIntraLine gui=reverse cterm=reverse 113 | highlight default GitGutterDeleteIntraLine gui=reverse cterm=reverse 114 | " Set diff syntax colours (used in the preview window) - diffAdded,diffChanged,diffRemoved - 115 | " to match the signs, if not set aleady. 116 | for [dtype,type] in [['Added','Add'], ['Changed','Change'], ['Removed','Delete']] 117 | if !hlexists('diff'.dtype) 118 | let [guifg, ctermfg] = s:get_foreground_colors('GitGutter'.type) 119 | execute "highlight diff".dtype." guifg=".guifg." ctermfg=".ctermfg." guibg=NONE ctermbg=NONE" 120 | endif 121 | endfor 122 | endfunction 123 | 124 | function! gitgutter#highlight#define_signs() abort 125 | sign define GitGutterLineAdded 126 | sign define GitGutterLineModified 127 | sign define GitGutterLineRemoved 128 | sign define GitGutterLineRemovedFirstLine 129 | sign define GitGutterLineRemovedAboveAndBelow 130 | sign define GitGutterLineModifiedRemoved 131 | 132 | call s:define_sign_text() 133 | call gitgutter#highlight#define_sign_text_highlights() 134 | call s:define_sign_line_highlights() 135 | call s:define_sign_linenr_highlights() 136 | endfunction 137 | 138 | function! s:define_sign_text() abort 139 | execute "sign define GitGutterLineAdded text=" . g:gitgutter_sign_added 140 | execute "sign define GitGutterLineModified text=" . g:gitgutter_sign_modified 141 | execute "sign define GitGutterLineRemoved text=" . g:gitgutter_sign_removed 142 | execute "sign define GitGutterLineRemovedFirstLine text=" . g:gitgutter_sign_removed_first_line 143 | execute "sign define GitGutterLineRemovedAboveAndBelow text=" . g:gitgutter_sign_removed_above_and_below 144 | execute "sign define GitGutterLineModifiedRemoved text=" . g:gitgutter_sign_modified_removed 145 | endfunction 146 | 147 | function! gitgutter#highlight#define_sign_text_highlights() abort 148 | " Once a sign's text attribute has been defined, it cannot be undefined or 149 | " set to an empty value. So to make signs' text disappear (when toggling 150 | " off or disabling) we make them invisible by setting their foreground colours 151 | " to the background's. 152 | if g:gitgutter_signs 153 | sign define GitGutterLineAdded texthl=GitGutterAdd 154 | sign define GitGutterLineModified texthl=GitGutterChange 155 | sign define GitGutterLineRemoved texthl=GitGutterDelete 156 | sign define GitGutterLineRemovedFirstLine texthl=GitGutterDelete 157 | sign define GitGutterLineRemovedAboveAndBelow texthl=GitGutterDelete 158 | sign define GitGutterLineModifiedRemoved texthl=GitGutterChangeDelete 159 | else 160 | sign define GitGutterLineAdded texthl=GitGutterAddInvisible 161 | sign define GitGutterLineModified texthl=GitGutterChangeInvisible 162 | sign define GitGutterLineRemoved texthl=GitGutterDeleteInvisible 163 | sign define GitGutterLineRemovedFirstLine texthl=GitGutterDeleteInvisible 164 | sign define GitGutterLineRemovedAboveAndBelow texthl=GitGutterDeleteInvisible 165 | sign define GitGutterLineModifiedRemoved texthl=GitGutterChangeDeleteInvisible 166 | endif 167 | endfunction 168 | 169 | function! s:define_sign_line_highlights() abort 170 | if g:gitgutter_highlight_lines 171 | sign define GitGutterLineAdded linehl=GitGutterAddLine 172 | sign define GitGutterLineModified linehl=GitGutterChangeLine 173 | sign define GitGutterLineRemoved linehl=GitGutterDeleteLine 174 | sign define GitGutterLineRemovedFirstLine linehl=GitGutterDeleteLine 175 | sign define GitGutterLineRemovedAboveAndBelow linehl=GitGutterDeleteLine 176 | sign define GitGutterLineModifiedRemoved linehl=GitGutterChangeDeleteLine 177 | else 178 | sign define GitGutterLineAdded linehl=NONE 179 | sign define GitGutterLineModified linehl=NONE 180 | sign define GitGutterLineRemoved linehl=NONE 181 | sign define GitGutterLineRemovedFirstLine linehl=NONE 182 | sign define GitGutterLineRemovedAboveAndBelow linehl=NONE 183 | sign define GitGutterLineModifiedRemoved linehl=NONE 184 | endif 185 | endfunction 186 | 187 | function! s:define_sign_linenr_highlights() abort 188 | if has('nvim-0.3.2') 189 | try 190 | if g:gitgutter_highlight_linenrs 191 | sign define GitGutterLineAdded numhl=GitGutterAddLineNr 192 | sign define GitGutterLineModified numhl=GitGutterChangeLineNr 193 | sign define GitGutterLineRemoved numhl=GitGutterDeleteLineNr 194 | sign define GitGutterLineRemovedFirstLine numhl=GitGutterDeleteLineNr 195 | sign define GitGutterLineRemovedAboveAndBelow numhl=GitGutterDeleteLineNr 196 | sign define GitGutterLineModifiedRemoved numhl=GitGutterChangeDeleteLineNr 197 | else 198 | sign define GitGutterLineAdded numhl=NONE 199 | sign define GitGutterLineModified numhl=NONE 200 | sign define GitGutterLineRemoved numhl=NONE 201 | sign define GitGutterLineRemovedFirstLine numhl=NONE 202 | sign define GitGutterLineRemovedAboveAndBelow numhl=NONE 203 | sign define GitGutterLineModifiedRemoved numhl=NONE 204 | endif 205 | catch /E475/ 206 | endtry 207 | endif 208 | endfunction 209 | 210 | function! s:get_hl(group, what, mode) abort 211 | let r = synIDattr(synIDtrans(hlID(a:group)), a:what, a:mode) 212 | if empty(r) || r == -1 213 | return 'NONE' 214 | endif 215 | return r 216 | endfunction 217 | 218 | function! s:get_foreground_colors(group) abort 219 | let ctermfg = s:get_hl(a:group, 'fg', 'cterm') 220 | let guifg = s:get_hl(a:group, 'fg', 'gui') 221 | return [guifg, ctermfg] 222 | endfunction 223 | 224 | function! s:get_background_colors(group) abort 225 | let ctermbg = s:get_hl(a:group, 'bg', 'cterm') 226 | let guibg = s:get_hl(a:group, 'bg', 'gui') 227 | return [guibg, ctermbg] 228 | endfunction 229 | 230 | function! s:useful_diff_colours() 231 | let [guifg_add, ctermfg_add] = s:get_foreground_colors('DiffAdd') 232 | let [guifg_del, ctermfg_del] = s:get_foreground_colors('DiffDelete') 233 | 234 | return guifg_add != guifg_del && ctermfg_add != ctermfg_del 235 | endfunction 236 | 237 | function! s:get_foreground_fallback_colors(type) 238 | if a:type == 'Add' 239 | return ['#009900', '2'] 240 | elseif a:type == 'Change' 241 | return ['#bbbb00', '3'] 242 | elseif a:type == 'Delete' 243 | return ['#ff2222', '1'] 244 | endif 245 | endfunction 246 | -------------------------------------------------------------------------------- /autoload/gitgutter/hunk.vim: -------------------------------------------------------------------------------- 1 | let s:winid = 0 2 | let s:preview_bufnr = 0 3 | let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '' : '' 4 | 5 | function! gitgutter#hunk#set_hunks(bufnr, hunks) abort 6 | call gitgutter#utility#setbufvar(a:bufnr, 'hunks', a:hunks) 7 | call s:reset_summary(a:bufnr) 8 | endfunction 9 | 10 | function! gitgutter#hunk#hunks(bufnr) abort 11 | return gitgutter#utility#getbufvar(a:bufnr, 'hunks', []) 12 | endfunction 13 | 14 | function! gitgutter#hunk#reset(bufnr) abort 15 | call gitgutter#utility#setbufvar(a:bufnr, 'hunks', []) 16 | call s:reset_summary(a:bufnr) 17 | endfunction 18 | 19 | 20 | function! gitgutter#hunk#summary(bufnr) abort 21 | return gitgutter#utility#getbufvar(a:bufnr, 'summary', [0,0,0]) 22 | endfunction 23 | 24 | function! s:reset_summary(bufnr) abort 25 | call gitgutter#utility#setbufvar(a:bufnr, 'summary', [0,0,0]) 26 | endfunction 27 | 28 | function! gitgutter#hunk#increment_lines_added(bufnr, count) abort 29 | let summary = gitgutter#hunk#summary(a:bufnr) 30 | let summary[0] += a:count 31 | call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary) 32 | endfunction 33 | 34 | function! gitgutter#hunk#increment_lines_modified(bufnr, count) abort 35 | let summary = gitgutter#hunk#summary(a:bufnr) 36 | let summary[1] += a:count 37 | call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary) 38 | endfunction 39 | 40 | function! gitgutter#hunk#increment_lines_removed(bufnr, count) abort 41 | let summary = gitgutter#hunk#summary(a:bufnr) 42 | let summary[2] += a:count 43 | call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary) 44 | endfunction 45 | 46 | 47 | function! gitgutter#hunk#next_hunk(count) abort 48 | let bufnr = bufnr('') 49 | if !gitgutter#utility#is_active(bufnr) | return | endif 50 | 51 | let hunks = gitgutter#hunk#hunks(bufnr) 52 | if empty(hunks) 53 | call gitgutter#utility#warn('No hunks in file') 54 | return 55 | endif 56 | 57 | let current_line = line('.') 58 | let hunk_count = 0 59 | for hunk in hunks 60 | if hunk[2] > current_line 61 | let hunk_count += 1 62 | if hunk_count == a:count 63 | let keys = &foldopen =~# '\' ? 'zv' : '' 64 | execute 'normal!' hunk[2] . 'G' . keys 65 | if g:gitgutter_show_msg_on_hunk_jumping 66 | redraw | echo printf('Hunk %d of %d', index(hunks, hunk) + 1, len(hunks)) 67 | endif 68 | if gitgutter#hunk#is_preview_window_open() 69 | call gitgutter#hunk#preview() 70 | endif 71 | return 72 | endif 73 | endif 74 | endfor 75 | call gitgutter#utility#warn('No more hunks') 76 | endfunction 77 | 78 | function! gitgutter#hunk#prev_hunk(count) abort 79 | let bufnr = bufnr('') 80 | if !gitgutter#utility#is_active(bufnr) | return | endif 81 | 82 | let hunks = gitgutter#hunk#hunks(bufnr) 83 | if empty(hunks) 84 | call gitgutter#utility#warn('No hunks in file') 85 | return 86 | endif 87 | 88 | let current_line = line('.') 89 | let hunk_count = 0 90 | for hunk in reverse(copy(hunks)) 91 | if hunk[2] < current_line 92 | let hunk_count += 1 93 | if hunk_count == a:count 94 | let keys = &foldopen =~# '\' ? 'zv' : '' 95 | let target = hunk[2] == 0 ? 1 : hunk[2] 96 | execute 'normal!' target . 'G' . keys 97 | if g:gitgutter_show_msg_on_hunk_jumping 98 | redraw | echo printf('Hunk %d of %d', index(hunks, hunk) + 1, len(hunks)) 99 | endif 100 | if gitgutter#hunk#is_preview_window_open() 101 | call gitgutter#hunk#preview() 102 | endif 103 | return 104 | endif 105 | endif 106 | endfor 107 | call gitgutter#utility#warn('No previous hunks') 108 | endfunction 109 | 110 | " Returns the hunk the cursor is currently in or an empty list if the cursor 111 | " isn't in a hunk. 112 | function! s:current_hunk() abort 113 | let bufnr = bufnr('') 114 | let current_hunk = [] 115 | 116 | for hunk in gitgutter#hunk#hunks(bufnr) 117 | if gitgutter#hunk#cursor_in_hunk(hunk) 118 | let current_hunk = hunk 119 | break 120 | endif 121 | endfor 122 | 123 | return current_hunk 124 | endfunction 125 | 126 | " Returns truthy if the cursor is in two hunks (which can only happen if the 127 | " cursor is on the first line and lines above have been deleted and lines 128 | " immediately below have been deleted) or falsey otherwise. 129 | function! s:cursor_in_two_hunks() 130 | let hunks = gitgutter#hunk#hunks(bufnr('')) 131 | 132 | if line('.') == 1 && len(hunks) > 1 && hunks[0][2:3] == [0, 0] && hunks[1][2:3] == [1, 0] 133 | return 1 134 | endif 135 | 136 | return 0 137 | endfunction 138 | 139 | " A line can be in 0 or 1 hunks, with the following exception: when the first 140 | " line(s) of a file has been deleted, and the new second line (and 141 | " optionally below) has been deleted, the new first line is in two hunks. 142 | function! gitgutter#hunk#cursor_in_hunk(hunk) abort 143 | let current_line = line('.') 144 | 145 | if current_line == 1 && a:hunk[2] == 0 146 | return 1 147 | endif 148 | 149 | if current_line >= a:hunk[2] && current_line < a:hunk[2] + (a:hunk[3] == 0 ? 1 : a:hunk[3]) 150 | return 1 151 | endif 152 | 153 | return 0 154 | endfunction 155 | 156 | 157 | function! gitgutter#hunk#in_hunk(lnum) 158 | " Hunks are sorted in the order they appear in the buffer. 159 | for hunk in gitgutter#hunk#hunks(bufnr('')) 160 | " if in a hunk on first line of buffer 161 | if a:lnum == 1 && hunk[2] == 0 162 | return 1 163 | endif 164 | 165 | " if in a hunk generally 166 | if a:lnum >= hunk[2] && a:lnum < hunk[2] + (hunk[3] == 0 ? 1 : hunk[3]) 167 | return 1 168 | endif 169 | 170 | " if hunk starts after the given line 171 | if a:lnum < hunk[2] 172 | return 0 173 | endif 174 | endfor 175 | 176 | return 0 177 | endfunction 178 | 179 | 180 | function! gitgutter#hunk#text_object(inner) abort 181 | let hunk = s:current_hunk() 182 | 183 | if empty(hunk) 184 | return 185 | endif 186 | 187 | let [first_line, last_line] = [hunk[2], hunk[2] + hunk[3] - 1] 188 | 189 | if ! a:inner 190 | let lnum = last_line 191 | let eof = line('$') 192 | while lnum < eof && empty(getline(lnum + 1)) 193 | let lnum +=1 194 | endwhile 195 | let last_line = lnum 196 | endif 197 | 198 | execute 'normal! 'first_line.'GV'.last_line.'G' 199 | endfunction 200 | 201 | 202 | function! gitgutter#hunk#stage(...) abort 203 | if !s:in_hunk_preview_window() && !gitgutter#utility#has_repo_path(bufnr('')) | return | endif 204 | 205 | if a:0 && (a:1 != 1 || a:2 != line('$')) 206 | call s:hunk_op(function('s:stage'), a:1, a:2) 207 | else 208 | call s:hunk_op(function('s:stage')) 209 | endif 210 | silent! call repeat#set("\(GitGutterStageHunk)", -1) 211 | endfunction 212 | 213 | function! gitgutter#hunk#undo() abort 214 | if !gitgutter#utility#has_repo_path(bufnr('')) | return | endif 215 | 216 | call s:hunk_op(function('s:undo')) 217 | silent! call repeat#set("\(GitGutterUndoHunk)", -1) 218 | endfunction 219 | 220 | function! gitgutter#hunk#preview() abort 221 | if !gitgutter#utility#has_repo_path(bufnr('')) | return | endif 222 | 223 | call s:hunk_op(function('s:preview')) 224 | silent! call repeat#set("\(GitGutterPreviewHunk)", -1) 225 | endfunction 226 | 227 | 228 | function! s:hunk_op(op, ...) 229 | let bufnr = bufnr('') 230 | 231 | if s:in_hunk_preview_window() 232 | if string(a:op) =~ '_stage' 233 | " combine hunk-body in preview window with updated hunk-header 234 | let hunk_body = getline(1, '$') 235 | 236 | let [removed, added] = [0, 0] 237 | for line in hunk_body 238 | if line[0] == '-' 239 | let removed += 1 240 | elseif line[0] == '+' 241 | let added += 1 242 | endif 243 | endfor 244 | 245 | let hunk_header = b:hunk_header 246 | " from count 247 | let hunk_header[4] = substitute(hunk_header[4], '\(-\d\+\)\(,\d\+\)\?', '\=submatch(1).",".removed', '') 248 | " to count 249 | let hunk_header[4] = substitute(hunk_header[4], '\(+\d\+\)\(,\d\+\)\?', '\=submatch(1).",".added', '') 250 | 251 | let hunk_diff = join(hunk_header + hunk_body, "\n")."\n" 252 | 253 | if &previewwindow 254 | call s:goto_original_window() 255 | endif 256 | call gitgutter#hunk#close_hunk_preview_window() 257 | call s:stage(hunk_diff) 258 | endif 259 | 260 | return 261 | endif 262 | 263 | if gitgutter#utility#is_active(bufnr) 264 | " Get a (synchronous) diff. 265 | let [async, g:gitgutter_async] = [g:gitgutter_async, 0] 266 | let diff = gitgutter#diff#run_diff(bufnr, g:gitgutter_diff_relative_to, 1) 267 | let g:gitgutter_async = async 268 | 269 | call gitgutter#hunk#set_hunks(bufnr, gitgutter#diff#parse_diff(diff)) 270 | call gitgutter#diff#process_hunks(bufnr, gitgutter#hunk#hunks(bufnr)) " so the hunk summary is updated 271 | 272 | if empty(s:current_hunk()) 273 | call gitgutter#utility#warn('Cursor is not in a hunk') 274 | elseif s:cursor_in_two_hunks() 275 | let choice = input('Choose hunk: upper or lower (u/l)? ') 276 | " Clear input 277 | normal! : 278 | if choice =~ 'u' 279 | call a:op(gitgutter#diff#hunk_diff(bufnr, diff, 0)) 280 | elseif choice =~ 'l' 281 | call a:op(gitgutter#diff#hunk_diff(bufnr, diff, 1)) 282 | else 283 | call gitgutter#utility#warn('Did not recognise your choice') 284 | endif 285 | else 286 | let hunk_diff = gitgutter#diff#hunk_diff(bufnr, diff) 287 | 288 | if a:0 289 | let hunk_first_line = s:current_hunk()[2] 290 | let hunk_diff = s:part_of_diff(hunk_diff, a:1-hunk_first_line, a:2-hunk_first_line) 291 | endif 292 | 293 | call a:op(hunk_diff) 294 | endif 295 | endif 296 | endfunction 297 | 298 | 299 | function! s:stage(hunk_diff) 300 | let bufnr = bufnr('') 301 | 302 | if gitgutter#utility#clean_smudge_filter_applies(bufnr) 303 | let choice = input('File uses clean/smudge filter. Stage entire file (y/n)? ') 304 | normal! : 305 | if choice =~ 'y' 306 | " We are about to add the file to the index so write the buffer to 307 | " ensure the file on disk matches it (the buffer). 308 | write 309 | let path = gitgutter#utility#repo_path(bufnr, 1) 310 | " Add file to index. 311 | let cmd = gitgutter#git(bufnr).' add '. 312 | \ gitgutter#utility#shellescape(gitgutter#utility#filename(bufnr)) 313 | let [_, error_code] = gitgutter#utility#system(cmd) 314 | else 315 | return 316 | endif 317 | 318 | else 319 | let diff = s:adjust_header(bufnr, a:hunk_diff) 320 | " Apply patch to index. 321 | let [_, error_code] = gitgutter#utility#system( 322 | \ gitgutter#git(bufnr).' apply --cached --unidiff-zero - ', 323 | \ diff) 324 | endif 325 | 326 | if error_code 327 | call gitgutter#utility#warn('Patch does not apply') 328 | else 329 | if exists('#User#GitGutterStage') 330 | execute 'doautocmd' s:nomodeline 'User GitGutterStage' 331 | endif 332 | endif 333 | 334 | " Refresh gitgutter's view of buffer. 335 | call gitgutter#process_buffer(bufnr, 1) 336 | endfunction 337 | 338 | 339 | function! s:undo(hunk_diff) 340 | " Apply reverse patch to buffer. 341 | let hunk = gitgutter#diff#parse_hunk(split(a:hunk_diff, '\n')[4]) 342 | let lines = map(split(a:hunk_diff, '\r\?\n')[5:], 'v:val[1:]') 343 | let lnum = hunk[2] 344 | let added_only = hunk[1] == 0 && hunk[3] > 0 345 | let removed_only = hunk[1] > 0 && hunk[3] == 0 346 | 347 | if removed_only 348 | call append(lnum, lines) 349 | elseif added_only 350 | execute lnum .','. (lnum+len(lines)-1) .'d _' 351 | else 352 | call append(lnum-1, lines[0:hunk[1]]) 353 | execute (lnum+hunk[1]) .','. (lnum+hunk[1]+hunk[3]) .'d _' 354 | endif 355 | 356 | " Refresh gitgutter's view of buffer. 357 | call gitgutter#process_buffer(bufnr(''), 1) 358 | endfunction 359 | 360 | 361 | function! s:preview(hunk_diff) 362 | if g:gitgutter_preview_win_floating && exists('*nvim_set_current_win') && s:winid != 0 363 | call nvim_set_current_win(s:winid) 364 | return 365 | endif 366 | 367 | let lines = split(a:hunk_diff, '\r\?\n') 368 | let header = lines[0:4] 369 | let body = lines[5:] 370 | 371 | call s:open_hunk_preview_window() 372 | call s:populate_hunk_preview_window(header, body) 373 | call s:enable_staging_from_hunk_preview_window() 374 | if &previewwindow 375 | call s:goto_original_window() 376 | endif 377 | endfunction 378 | 379 | 380 | " Returns a new hunk diff using the specified lines from the given one. 381 | " Assumes all lines are additions. 382 | " a:first, a:last - 0-based indexes into the body of the hunk. 383 | function! s:part_of_diff(hunk_diff, first, last) 384 | let diff_lines = split(a:hunk_diff, '\n', 1) 385 | 386 | " adjust 'to' line count in header 387 | let diff_lines[4] = substitute(diff_lines[4], '\(+\d\+\)\(,\d\+\)\?', '\=submatch(1).",".(a:last-a:first+1)', '') 388 | 389 | return join(diff_lines[0:4] + diff_lines[5+a:first:5+a:last], "\n")."\n" 390 | endfunction 391 | 392 | 393 | function! s:adjust_header(bufnr, hunk_diff) 394 | let filepath = gitgutter#utility#repo_path(a:bufnr, 0) 395 | return s:adjust_hunk_summary(s:fix_file_references(filepath, a:hunk_diff)) 396 | endfunction 397 | 398 | 399 | " Replaces references to temp files with the actual file. 400 | function! s:fix_file_references(filepath, hunk_diff) 401 | let lines = split(a:hunk_diff, '\n') 402 | 403 | let left_prefix = matchstr(lines[2], '[abciow12]').'/' 404 | let right_prefix = matchstr(lines[3], '[abciow12]').'/' 405 | let quote = lines[0][11] == '"' ? '"' : '' 406 | 407 | let left_file = quote.left_prefix.a:filepath.quote 408 | let right_file = quote.right_prefix.a:filepath.quote 409 | 410 | let lines[0] = 'diff --git '.left_file.' '.right_file 411 | let lines[2] = '--- '.left_file 412 | let lines[3] = '+++ '.right_file 413 | 414 | return join(lines, "\n")."\n" 415 | endfunction 416 | 417 | 418 | function! s:adjust_hunk_summary(hunk_diff) abort 419 | let line_adjustment = s:line_adjustment_for_current_hunk() 420 | let diff = split(a:hunk_diff, '\n', 1) 421 | let diff[4] = substitute(diff[4], '+\zs\(\d\+\)', '\=submatch(1)+line_adjustment', '') 422 | return join(diff, "\n") 423 | endfunction 424 | 425 | 426 | " Returns the number of lines the current hunk is offset from where it would 427 | " be if any changes above it in the file didn't exist. 428 | function! s:line_adjustment_for_current_hunk() abort 429 | let bufnr = bufnr('') 430 | let adj = 0 431 | for hunk in gitgutter#hunk#hunks(bufnr) 432 | if gitgutter#hunk#cursor_in_hunk(hunk) 433 | break 434 | else 435 | let adj += hunk[1] - hunk[3] 436 | endif 437 | endfor 438 | return adj 439 | endfunction 440 | 441 | 442 | function! s:in_hunk_preview_window() 443 | if g:gitgutter_preview_win_floating 444 | return win_id2win(s:winid) == winnr() 445 | else 446 | return &previewwindow 447 | endif 448 | endfunction 449 | 450 | 451 | " Floating window: does not move cursor to floating window. 452 | " Preview window: moves cursor to preview window. 453 | function! s:open_hunk_preview_window() 454 | let source_wrap = &wrap 455 | let source_window = winnr() 456 | 457 | if g:gitgutter_preview_win_floating 458 | if exists('*nvim_open_win') 459 | call gitgutter#hunk#close_hunk_preview_window() 460 | 461 | let buf = nvim_create_buf(v:false, v:false) 462 | " Set default width and height for now. 463 | let s:winid = nvim_open_win(buf, v:false, g:gitgutter_floating_window_options) 464 | call nvim_win_set_option(s:winid, 'wrap', source_wrap ? v:true : v:false) 465 | call nvim_buf_set_option(buf, 'filetype', 'diff') 466 | call nvim_buf_set_option(buf, 'buftype', 'acwrite') 467 | call nvim_buf_set_option(buf, 'bufhidden', 'delete') 468 | call nvim_buf_set_option(buf, 'swapfile', v:false) 469 | call nvim_buf_set_name(buf, 'gitgutter://hunk-preview') 470 | 471 | if g:gitgutter_close_preview_on_escape 472 | let winnr = nvim_win_get_number(s:winid) 473 | execute winnr.'wincmd w' 474 | nnoremap :call gitgutter#hunk#close_hunk_preview_window() 475 | wincmd w 476 | endif 477 | 478 | " Assumes cursor is in original window. 479 | autocmd CursorMoved,TabLeave ++once call gitgutter#hunk#close_hunk_preview_window() 480 | 481 | return 482 | endif 483 | 484 | if exists('*popup_create') 485 | if g:gitgutter_close_preview_on_escape 486 | let g:gitgutter_floating_window_options.filter = function('s:close_popup_on_escape') 487 | endif 488 | 489 | let s:winid = popup_create('', g:gitgutter_floating_window_options) 490 | 491 | call setbufvar(winbufnr(s:winid), '&filetype', 'diff') 492 | call setwinvar(s:winid, '&wrap', source_wrap) 493 | 494 | return 495 | endif 496 | endif 497 | 498 | if exists('&previewpopup') 499 | let [previewpopup, &previewpopup] = [&previewpopup, ''] 500 | endif 501 | 502 | " Specifying where to open the preview window can lead to the cursor going 503 | " to an unexpected window when the preview window is closed (#769). 504 | silent! noautocmd execute g:gitgutter_preview_win_location 'pedit gitgutter://hunk-preview' 505 | silent! wincmd P 506 | setlocal statusline=%{''} 507 | doautocmd WinEnter 508 | if exists('*win_getid') 509 | let s:winid = win_getid() 510 | else 511 | let s:preview_bufnr = bufnr('') 512 | endif 513 | setlocal filetype=diff buftype=acwrite bufhidden=delete 514 | let &l:wrap = source_wrap 515 | let b:source_window = source_window 516 | " Reset some defaults in case someone else has changed them. 517 | setlocal noreadonly modifiable noswapfile 518 | if g:gitgutter_close_preview_on_escape 519 | " Ensure cursor goes to the expected window. 520 | nnoremap :execute b:source_window . "wincmd w"pclose 521 | endif 522 | 523 | if exists('&previewpopup') 524 | let &previewpopup=previewpopup 525 | endif 526 | endfunction 527 | 528 | 529 | function! s:close_popup_on_escape(winid, key) 530 | if a:key == "\" 531 | call popup_close(a:winid) 532 | return 1 533 | endif 534 | return 0 535 | endfunction 536 | 537 | 538 | " Floating window: does not care where cursor is. 539 | " Preview window: assumes cursor is in preview window. 540 | function! s:populate_hunk_preview_window(header, body) 541 | if g:gitgutter_preview_win_floating 542 | if exists('*nvim_open_win') 543 | " Assumes cursor is not in previewing window. 544 | call nvim_buf_set_var(winbufnr(s:winid), 'hunk_header', a:header) 545 | 546 | let [_scrolloff, &scrolloff] = [&scrolloff, 0] 547 | 548 | let [width, height] = s:screen_lines(a:body) 549 | let height = min([height, g:gitgutter_floating_window_options.height]) 550 | call nvim_win_set_width(s:winid, width) 551 | call nvim_win_set_height(s:winid, height) 552 | 553 | let &scrolloff=_scrolloff 554 | 555 | call nvim_buf_set_lines(winbufnr(s:winid), 0, -1, v:false, []) 556 | call nvim_buf_set_lines(winbufnr(s:winid), 0, -1, v:false, a:body) 557 | call nvim_buf_set_option(winbufnr(s:winid), 'modified', v:false) 558 | 559 | let ns_id = nvim_create_namespace('GitGutter') 560 | call nvim_buf_clear_namespace(winbufnr(s:winid), ns_id, 0, -1) 561 | for region in gitgutter#diff_highlight#process(a:body) 562 | let group = region[1] == '+' ? 'GitGutterAddIntraLine' : 'GitGutterDeleteIntraLine' 563 | call nvim_buf_add_highlight(winbufnr(s:winid), ns_id, group, region[0]-1, region[2]-1, region[3]) 564 | endfor 565 | 566 | call nvim_win_set_cursor(s:winid, [1,0]) 567 | endif 568 | 569 | if exists('*popup_create') 570 | call popup_settext(s:winid, a:body) 571 | 572 | for region in gitgutter#diff_highlight#process(a:body) 573 | let group = region[1] == '+' ? 'GitGutterAddIntraLine' : 'GitGutterDeleteIntraLine' 574 | call win_execute(s:winid, "call matchaddpos('".group."', [[".region[0].", ".region[2].", ".(region[3]-region[2]+1)."]])") 575 | endfor 576 | endif 577 | 578 | else 579 | let b:hunk_header = a:header 580 | 581 | %delete _ 582 | call setline(1, a:body) 583 | setlocal nomodified 584 | 585 | let [_, height] = s:screen_lines(a:body) 586 | execute 'resize' height 587 | 1 588 | 589 | call clearmatches() 590 | for region in gitgutter#diff_highlight#process(a:body) 591 | let group = region[1] == '+' ? 'GitGutterAddIntraLine' : 'GitGutterDeleteIntraLine' 592 | call matchaddpos(group, [[region[0], region[2], region[3]-region[2]+1]]) 593 | endfor 594 | 595 | 1 596 | endif 597 | endfunction 598 | 599 | 600 | " Calculates the number of columns and the number of screen lines the given 601 | " array of lines will take up, taking account of wrapping. 602 | function! s:screen_lines(lines) 603 | let [_virtualedit, &virtualedit]=[&virtualedit, 'all'] 604 | let cursor = getcurpos() 605 | normal! 0g$ 606 | let available_width = virtcol('.') 607 | call setpos('.', cursor) 608 | let &virtualedit=_virtualedit 609 | let width = min([max(map(copy(a:lines), 'strdisplaywidth(v:val)')), available_width]) 610 | 611 | if exists('*reduce') 612 | let height = reduce(a:lines, { acc, val -> acc + strdisplaywidth(val) / width + (strdisplaywidth(val) % width == 0 ? 0 : 1) }, 0) 613 | else 614 | let height = eval(join(map(copy(a:lines), 'strdisplaywidth(v:val) / width + (strdisplaywidth(v:val) % width == 0 ? 0 : 1)'), '+')) 615 | endif 616 | 617 | return [width, height] 618 | endfunction 619 | 620 | 621 | function! s:enable_staging_from_hunk_preview_window() 622 | augroup gitgutter_hunk_preview 623 | autocmd! 624 | let bufnr = s:winid != 0 ? winbufnr(s:winid) : s:preview_bufnr 625 | execute 'autocmd BufWriteCmd GitGutterStageHunk' 626 | augroup END 627 | endfunction 628 | 629 | 630 | function! s:goto_original_window() 631 | noautocmd execute b:source_window . "wincmd w" 632 | doautocmd WinEnter 633 | endfunction 634 | 635 | 636 | function! gitgutter#hunk#close_hunk_preview_window() 637 | let bufnr = s:winid != 0 ? winbufnr(s:winid) : s:preview_bufnr 638 | call setbufvar(bufnr, '&modified', 0) 639 | 640 | if g:gitgutter_preview_win_floating 641 | if win_id2win(s:winid) > 0 642 | execute win_id2win(s:winid).'wincmd c' 643 | endif 644 | else 645 | pclose 646 | endif 647 | 648 | let s:winid = 0 649 | let s:preview_bufnr = 0 650 | endfunction 651 | 652 | 653 | function gitgutter#hunk#is_preview_window_open() 654 | if g:gitgutter_preview_win_floating 655 | if win_id2win(s:winid) > 0 656 | execute win_id2win(s:winid).'wincmd c' 657 | endif 658 | else 659 | for i in range(1, winnr('$')) 660 | if getwinvar(i, '&previewwindow') 661 | return 1 662 | endif 663 | endfor 664 | endif 665 | return 0 666 | endfunction 667 | -------------------------------------------------------------------------------- /autoload/gitgutter/sign.vim: -------------------------------------------------------------------------------- 1 | " For older Vims without sign_place() the plugin has to manaage the sign ids. 2 | let s:first_sign_id = 3000 3 | let s:next_sign_id = s:first_sign_id 4 | " Remove-all-signs optimisation requires Vim 7.3.596+. 5 | let s:supports_star = v:version > 703 || (v:version == 703 && has("patch596")) 6 | 7 | 8 | function! gitgutter#sign#enable() abort 9 | let old_signs = g:gitgutter_signs 10 | 11 | let g:gitgutter_signs = 1 12 | call gitgutter#highlight#define_sign_text_highlights() 13 | 14 | if !old_signs && !g:gitgutter_highlight_lines && !g:gitgutter_highlight_linenrs 15 | call gitgutter#all(1) 16 | endif 17 | endfunction 18 | 19 | function! gitgutter#sign#disable() abort 20 | let g:gitgutter_signs = 0 21 | call gitgutter#highlight#define_sign_text_highlights() 22 | 23 | if !g:gitgutter_highlight_lines && !g:gitgutter_highlight_linenrs 24 | call gitgutter#sign#clear_signs(bufnr('')) 25 | endif 26 | endfunction 27 | 28 | function! gitgutter#sign#toggle() abort 29 | if g:gitgutter_signs 30 | call gitgutter#sign#disable() 31 | else 32 | call gitgutter#sign#enable() 33 | endif 34 | endfunction 35 | 36 | 37 | " Removes gitgutter's signs from the buffer being processed. 38 | function! gitgutter#sign#clear_signs(bufnr) abort 39 | if exists('*sign_unplace') 40 | call sign_unplace('gitgutter', {'buffer': a:bufnr}) 41 | return 42 | endif 43 | 44 | 45 | call s:find_current_signs(a:bufnr) 46 | 47 | let sign_ids = map(values(gitgutter#utility#getbufvar(a:bufnr, 'gitgutter_signs')), 'v:val.id') 48 | call s:remove_signs(a:bufnr, sign_ids, 1) 49 | call gitgutter#utility#setbufvar(a:bufnr, 'gitgutter_signs', {}) 50 | endfunction 51 | 52 | 53 | " Updates gitgutter's signs in the buffer being processed. 54 | " 55 | " modified_lines: list of [, ] 56 | " where name = 'added|removed|modified|modified_removed' 57 | function! gitgutter#sign#update_signs(bufnr, modified_lines) abort 58 | if exists('*sign_unplace') 59 | " Vim is (hopefully) now quick enough to remove all signs then place new ones. 60 | call sign_unplace('gitgutter', {'buffer': a:bufnr}) 61 | 62 | let modified_lines = s:handle_double_hunk(a:modified_lines) 63 | let signs = map(copy(modified_lines), '{'. 64 | \ '"buffer": a:bufnr,'. 65 | \ '"group": "gitgutter",'. 66 | \ '"name": s:highlight_name_for_change(v:val[1]),'. 67 | \ '"lnum": v:val[0],'. 68 | \ '"priority": g:gitgutter_sign_priority'. 69 | \ '}') 70 | 71 | if exists('*sign_placelist') 72 | call sign_placelist(signs) 73 | return 74 | endif 75 | 76 | for sign in signs 77 | call sign_place(0, sign.group, sign.name, sign.buffer, {'lnum': sign.lnum, 'priority': sign.priority}) 78 | endfor 79 | return 80 | endif 81 | 82 | 83 | " Derive a delta between the current signs and the ones we want. 84 | " Remove signs from lines that no longer need a sign. 85 | " Upsert the remaining signs. 86 | 87 | call s:find_current_signs(a:bufnr) 88 | 89 | let new_gitgutter_signs_line_numbers = map(copy(a:modified_lines), 'v:val[0]') 90 | let obsolete_signs = s:obsolete_gitgutter_signs_to_remove(a:bufnr, new_gitgutter_signs_line_numbers) 91 | 92 | call s:remove_signs(a:bufnr, obsolete_signs, s:remove_all_old_signs) 93 | call s:upsert_new_gitgutter_signs(a:bufnr, a:modified_lines) 94 | endfunction 95 | 96 | 97 | " 98 | " Internal functions 99 | " 100 | 101 | 102 | function! s:find_current_signs(bufnr) abort 103 | let gitgutter_signs = {} " : {'id': , 'name': } 104 | if !g:gitgutter_sign_allow_clobber 105 | let other_signs = [] " [ signlines 115 | silent execute "sign place buffer=" . a:bufnr 116 | redir END 117 | 118 | for signline in filter(split(signlines, '\n')[2:], 'v:val =~# "="') 119 | " Typical sign line before v8.1.0614: line=88 id=1234 name=GitGutterLineAdded 120 | " We assume splitting is faster than a regexp. 121 | let components = split(signline) 122 | call add(signs, { 123 | \ 'lnum': str2nr(split(components[0], '=')[1]), 124 | \ 'id': str2nr(split(components[1], '=')[1]), 125 | \ 'name': split(components[2], '=')[1] 126 | \ }) 127 | endfor 128 | endif 129 | 130 | for sign in signs 131 | if sign.name =~# 'GitGutter' 132 | " Remove orphaned signs (signs placed on lines which have been deleted). 133 | " (When a line is deleted its sign lingers. Subsequent lines' signs' 134 | " line numbers are decremented appropriately.) 135 | if has_key(gitgutter_signs, sign.lnum) 136 | execute "sign unplace" gitgutter_signs[sign.lnum].id 137 | endif 138 | let gitgutter_signs[sign.lnum] = {'id': sign.id, 'name': sign.name} 139 | else 140 | if !g:gitgutter_sign_allow_clobber 141 | call add(other_signs, sign.lnum) 142 | endif 143 | endif 144 | endfor 145 | 146 | call gitgutter#utility#setbufvar(a:bufnr, 'gitgutter_signs', gitgutter_signs) 147 | if !g:gitgutter_sign_allow_clobber 148 | call gitgutter#utility#setbufvar(a:bufnr, 'other_signs', other_signs) 149 | endif 150 | endfunction 151 | 152 | 153 | " Returns a list of [, ...] 154 | " Sets `s:remove_all_old_signs` as a side-effect. 155 | function! s:obsolete_gitgutter_signs_to_remove(bufnr, new_gitgutter_signs_line_numbers) abort 156 | let signs_to_remove = [] " list of [, ...] 157 | let remove_all_signs = 1 158 | let old_gitgutter_signs = gitgutter#utility#getbufvar(a:bufnr, 'gitgutter_signs') 159 | for line_number in keys(old_gitgutter_signs) 160 | if index(a:new_gitgutter_signs_line_numbers, str2nr(line_number)) == -1 161 | call add(signs_to_remove, old_gitgutter_signs[line_number].id) 162 | else 163 | let remove_all_signs = 0 164 | endif 165 | endfor 166 | let s:remove_all_old_signs = remove_all_signs 167 | return signs_to_remove 168 | endfunction 169 | 170 | 171 | function! s:remove_signs(bufnr, sign_ids, all_signs) abort 172 | if a:all_signs && s:supports_star && (g:gitgutter_sign_allow_clobber || empty(gitgutter#utility#getbufvar(a:bufnr, 'other_signs'))) 173 | execute "sign unplace * buffer=" . a:bufnr 174 | else 175 | for id in a:sign_ids 176 | execute "sign unplace" id 177 | endfor 178 | endif 179 | endfunction 180 | 181 | 182 | function! s:upsert_new_gitgutter_signs(bufnr, modified_lines) abort 183 | if !g:gitgutter_sign_allow_clobber 184 | let other_signs = gitgutter#utility#getbufvar(a:bufnr, 'other_signs') 185 | endif 186 | let old_gitgutter_signs = gitgutter#utility#getbufvar(a:bufnr, 'gitgutter_signs') 187 | 188 | let modified_lines = s:handle_double_hunk(a:modified_lines) 189 | 190 | for line in modified_lines 191 | let line_number = line[0] " 192 | if g:gitgutter_sign_allow_clobber || index(other_signs, line_number) == -1 " don't clobber others' signs 193 | let name = s:highlight_name_for_change(line[1]) 194 | if !has_key(old_gitgutter_signs, line_number) " insert 195 | let id = s:next_sign_id() 196 | execute "sign place" id "line=" . line_number "name=" . name "buffer=" . a:bufnr 197 | else " update if sign has changed 198 | let old_sign = old_gitgutter_signs[line_number] 199 | if old_sign.name !=# name 200 | execute "sign place" old_sign.id "name=" . name "buffer=" . a:bufnr 201 | end 202 | endif 203 | endif 204 | endfor 205 | " At this point b:gitgutter_gitgutter_signs is out of date. 206 | endfunction 207 | 208 | 209 | " Handle special case where the first line is the site of two hunks: 210 | " lines deleted above at the start of the file, and lines deleted 211 | " immediately below. 212 | function! s:handle_double_hunk(modified_lines) 213 | if a:modified_lines[0:1] == [[1, 'removed_first_line'], [1, 'removed']] 214 | return [[1, 'removed_above_and_below']] + a:modified_lines[2:] 215 | endif 216 | 217 | return a:modified_lines 218 | endfunction 219 | 220 | 221 | function! s:next_sign_id() abort 222 | let next_id = s:next_sign_id 223 | let s:next_sign_id += 1 224 | return next_id 225 | endfunction 226 | 227 | 228 | " Only for testing. 229 | function! gitgutter#sign#reset() 230 | let s:next_sign_id = s:first_sign_id 231 | endfunction 232 | 233 | 234 | function! s:highlight_name_for_change(text) abort 235 | if a:text ==# 'added' 236 | return 'GitGutterLineAdded' 237 | elseif a:text ==# 'removed' 238 | return 'GitGutterLineRemoved' 239 | elseif a:text ==# 'removed_first_line' 240 | return 'GitGutterLineRemovedFirstLine' 241 | elseif a:text ==# 'modified' 242 | return 'GitGutterLineModified' 243 | elseif a:text ==# 'modified_removed' 244 | return 'GitGutterLineModifiedRemoved' 245 | elseif a:text ==# 'removed_above_and_below' 246 | return 'GitGutterLineRemovedAboveAndBelow' 247 | endif 248 | endfunction 249 | 250 | 251 | -------------------------------------------------------------------------------- /autoload/gitgutter/utility.vim: -------------------------------------------------------------------------------- 1 | function! gitgutter#utility#supports_overscore_sign() 2 | if gitgutter#utility#windows() 3 | return &encoding ==? 'utf-8' 4 | else 5 | return &termencoding ==? &encoding || &termencoding == '' 6 | endif 7 | endfunction 8 | 9 | " True for git v1.7.2+. 10 | function! gitgutter#utility#git_supports_command_line_config_override() abort 11 | if !exists('s:c_flag') 12 | let [_, error_code] = gitgutter#utility#system(gitgutter#git().' -c foo.bar=baz --version') 13 | let s:c_flag = !error_code 14 | endif 15 | return s:c_flag 16 | endfunction 17 | 18 | function! gitgutter#utility#setbufvar(buffer, varname, val) 19 | let buffer = +a:buffer 20 | " Default value for getbufvar() was introduced in Vim 7.3.831. 21 | let ggvars = getbufvar(buffer, 'gitgutter') 22 | if type(ggvars) == type('') 23 | unlet ggvars 24 | let ggvars = {} 25 | call setbufvar(buffer, 'gitgutter', ggvars) 26 | endif 27 | let ggvars[a:varname] = a:val 28 | endfunction 29 | 30 | function! gitgutter#utility#getbufvar(buffer, varname, ...) 31 | let buffer = +a:buffer 32 | let ggvars = getbufvar(buffer, 'gitgutter') 33 | if type(ggvars) == type({}) && has_key(ggvars, a:varname) 34 | return ggvars[a:varname] 35 | endif 36 | if a:0 37 | return a:1 38 | endif 39 | endfunction 40 | 41 | function! gitgutter#utility#warn(message) abort 42 | echohl WarningMsg 43 | echo a:message 44 | echohl None 45 | let v:warningmsg = a:message 46 | endfunction 47 | 48 | function! gitgutter#utility#warn_once(bufnr, message, key) abort 49 | if empty(gitgutter#utility#getbufvar(a:bufnr, a:key)) 50 | call gitgutter#utility#setbufvar(a:bufnr, a:key, '1') 51 | echohl WarningMsg 52 | redraw | echom a:message 53 | echohl None 54 | let v:warningmsg = a:message 55 | endif 56 | endfunction 57 | 58 | " Returns truthy when the buffer's file should be processed; and falsey when it shouldn't. 59 | " This function does not and should not make any system calls. 60 | function! gitgutter#utility#is_active(bufnr) abort 61 | return gitgutter#utility#getbufvar(a:bufnr, 'enabled') && 62 | \ !pumvisible() && 63 | \ s:is_file_buffer(a:bufnr) && 64 | \ s:exists_file(a:bufnr) && 65 | \ s:not_git_dir(a:bufnr) 66 | endfunction 67 | 68 | function! s:not_git_dir(bufnr) abort 69 | return gitgutter#utility#dir(a:bufnr) !~ '[/\\]\.git\($\|[/\\]\)' 70 | endfunction 71 | 72 | function! s:is_file_buffer(bufnr) abort 73 | return empty(getbufvar(a:bufnr, '&buftype')) 74 | endfunction 75 | 76 | " From tpope/vim-fugitive 77 | function! s:winshell() 78 | return &shell =~? 'cmd' || exists('+shellslash') && !&shellslash 79 | endfunction 80 | 81 | " From tpope/vim-fugitive 82 | function! gitgutter#utility#shellescape(arg) abort 83 | if a:arg =~ '^[A-Za-z0-9_/.-]\+$' 84 | return a:arg 85 | elseif s:winshell() 86 | return '"' . substitute(substitute(a:arg, '"', '""', 'g'), '%', '"%"', 'g') . '"' 87 | else 88 | return shellescape(a:arg) 89 | endif 90 | endfunction 91 | 92 | function! gitgutter#utility#file(bufnr) 93 | return s:abs_path(a:bufnr, 1) 94 | endfunction 95 | 96 | " Not shellescaped 97 | function! gitgutter#utility#extension(bufnr) abort 98 | return fnamemodify(s:abs_path(a:bufnr, 0), ':e') 99 | endfunction 100 | 101 | function! gitgutter#utility#system(cmd, ...) abort 102 | call gitgutter#debug#log(a:cmd, a:000) 103 | 104 | call s:use_known_shell() 105 | let prev_error_code = v:shell_error 106 | silent let output = (a:0 == 0) ? system(a:cmd) : system(a:cmd, a:1) 107 | let error_code = v:shell_error 108 | silent call system('exit ' . prev_error_code) 109 | call s:restore_shell() 110 | 111 | return [output, error_code] 112 | endfunction 113 | 114 | function! gitgutter#utility#has_repo_path(bufnr) 115 | return index(['', -1, -2], gitgutter#utility#repo_path(a:bufnr, 0)) == -1 116 | endfunction 117 | 118 | " Path of file relative to repo root. 119 | " 120 | " * empty string - not set 121 | " * non-empty string - path 122 | " * -1 - pending 123 | " * -2 - not tracked by git 124 | " * -3 - assume unchanged 125 | function! gitgutter#utility#repo_path(bufnr, shellesc) abort 126 | let p = gitgutter#utility#getbufvar(a:bufnr, 'path', '') 127 | return a:shellesc ? gitgutter#utility#shellescape(p) : p 128 | endfunction 129 | 130 | 131 | let s:set_path_handler = {} 132 | 133 | function! s:set_path_handler.out(buffer, listing) abort 134 | let listing = s:strip_trailing_new_line(a:listing) 135 | let [status, path] = [listing[0], listing[2:]] 136 | if status =~# '[a-z]' 137 | call gitgutter#utility#setbufvar(a:buffer, 'path', -3) 138 | else 139 | call gitgutter#utility#setbufvar(a:buffer, 'path', path) 140 | endif 141 | 142 | if type(self.continuation) == type(function('tr')) 143 | call self.continuation() 144 | else 145 | call call(self.continuation.function, self.continuation.arguments) 146 | endif 147 | endfunction 148 | 149 | function! s:set_path_handler.err(buffer) abort 150 | call gitgutter#utility#setbufvar(a:buffer, 'path', -2) 151 | endfunction 152 | 153 | 154 | " continuation - a funcref or hash to call after setting the repo path asynchronously. 155 | " 156 | " Returns 'async' if the the path is set asynchronously, 0 otherwise. 157 | function! gitgutter#utility#set_repo_path(bufnr, continuation) abort 158 | " Values of path: 159 | " * non-empty string - path 160 | " * -1 - pending 161 | " * -2 - not tracked by git 162 | " * -3 - assume unchanged 163 | 164 | call gitgutter#utility#setbufvar(a:bufnr, 'path', -1) 165 | let cmd = gitgutter#git(a:bufnr).' ls-files -v --error-unmatch --full-name -z -- '. 166 | \ gitgutter#utility#shellescape(gitgutter#utility#filename(a:bufnr)) 167 | 168 | if g:gitgutter_async && gitgutter#async#available() && !has('vim_starting') 169 | let handler = copy(s:set_path_handler) 170 | let handler.continuation = a:continuation 171 | call gitgutter#async#execute(cmd, a:bufnr, handler) 172 | return 'async' 173 | endif 174 | 175 | let [listing, error_code] = gitgutter#utility#system(cmd) 176 | 177 | if error_code 178 | call gitgutter#utility#setbufvar(a:bufnr, 'path', -2) 179 | return 180 | endif 181 | 182 | let listing = s:strip_trailing_new_line(listing) 183 | let [status, path] = [listing[0], listing[2:]] 184 | if status =~# '[a-z]' 185 | call gitgutter#utility#setbufvar(a:bufnr, 'path', -3) 186 | else 187 | call gitgutter#utility#setbufvar(a:bufnr, 'path', path) 188 | endif 189 | endfunction 190 | 191 | 192 | function! gitgutter#utility#clean_smudge_filter_applies(bufnr) 193 | let filtered = gitgutter#utility#getbufvar(a:bufnr, 'filter', -1) 194 | if filtered == -1 195 | let cmd = gitgutter#git(a:bufnr).' check-attr filter -- '. 196 | \ gitgutter#utility#shellescape(gitgutter#utility#filename(a:bufnr)) 197 | let [out, _] = gitgutter#utility#system(cmd) 198 | let filtered = out !~ 'unspecified' 199 | call gitgutter#utility#setbufvar(a:bufnr, 'filter', filtered) 200 | endif 201 | return filtered 202 | endfunction 203 | 204 | 205 | function! s:use_known_shell() abort 206 | if has('unix') && &shell !=# 'sh' 207 | let [s:shell, s:shellcmdflag, s:shellredir, s:shellpipe, s:shellquote, s:shellxquote] = [&shell, &shellcmdflag, &shellredir, &shellpipe, &shellquote, &shellxquote] 208 | let &shell = 'sh' 209 | set shellcmdflag=-c shellredir=>%s\ 2>&1 210 | endif 211 | if has('win32') && (&shell =~# 'pwsh' || &shell =~# 'powershell') 212 | let [s:shell, s:shellcmdflag, s:shellredir, s:shellpipe, s:shellquote, s:shellxquote] = [&shell, &shellcmdflag, &shellredir, &shellpipe, &shellquote, &shellxquote] 213 | let &shell = 'cmd.exe' 214 | set shellcmdflag=/s\ /c shellredir=>%s\ 2>&1 shellpipe=>%s\ 2>&1 shellquote= shellxquote=" 215 | endif 216 | endfunction 217 | 218 | function! s:restore_shell() abort 219 | if (has('unix') || has('win32')) && exists('s:shell') 220 | let [&shell, &shellcmdflag, &shellredir, &shellpipe, &shellquote, &shellxquote] = [s:shell, s:shellcmdflag, s:shellredir, s:shellpipe, s:shellquote, s:shellxquote] 221 | endif 222 | endfunction 223 | 224 | function! gitgutter#utility#get_diff_base(bufnr) 225 | let p = resolve(expand('#'.a:bufnr.':p')) 226 | let ml = matchlist(p, '\v^fugitive:/.*/(\x{40,})/') 227 | if !empty(ml) && !empty(ml[1]) 228 | return ml[1].'^' 229 | endif 230 | return g:gitgutter_diff_base 231 | endfunction 232 | 233 | " Returns the original path (shellescaped) at the buffer's diff base. 234 | function! gitgutter#utility#base_path(bufnr) 235 | let diffbase = gitgutter#utility#get_diff_base(a:bufnr) 236 | 237 | " If we already know the original path at this diff base, return it. 238 | let basepath = gitgutter#utility#getbufvar(a:bufnr, 'basepath', '') 239 | if !empty(basepath) 240 | " basepath is diffbase:path 241 | " Note that path can also contain colons. 242 | " List destructuring / unpacking where the remaining items are assigned 243 | " to a single variable (:help let-unpack) is only available in v8.2.0540. 244 | let parts = split(basepath, ':', 1) 245 | let base = parts[0] 246 | let bpath = join(parts[1:], ':') 247 | 248 | if base == diffbase 249 | return gitgutter#utility#shellescape(bpath) 250 | endif 251 | endif 252 | 253 | " Obtain buffers' paths. 254 | let current_paths = {} 255 | for bufnr in range(1, bufnr('$') + 1) 256 | if gitgutter#utility#has_repo_path(bufnr) 257 | let current_paths[gitgutter#utility#repo_path(bufnr, 0)] = bufnr 258 | endif 259 | endfor 260 | 261 | " Get a list of file renames at the buffer's diff base. 262 | " Store the original paths on any corresponding buffers. 263 | " If the buffer's file was one of them, return its original path. 264 | let op = '' 265 | let renames = s:obtain_file_renames(a:bufnr, diffbase) 266 | for [current, original] in items(renames) 267 | if has_key(current_paths, current) 268 | let bufnr = current_paths[current] 269 | let basepath = diffbase.':'.original 270 | call gitgutter#utility#setbufvar(bufnr, 'basepath', basepath) 271 | 272 | if bufnr == a:bufnr 273 | let op = original 274 | endif 275 | endif 276 | endfor 277 | if !empty(op) 278 | return gitgutter#utility#shellescape(op) 279 | endif 280 | 281 | " Buffer's file was not renamed, so store current path and return it. 282 | let current_path = gitgutter#utility#repo_path(a:bufnr, 0) 283 | let basepath = diffbase.':'.current_path 284 | call gitgutter#utility#setbufvar(a:bufnr, 'basepath', basepath) 285 | return gitgutter#utility#shellescape(current_path) 286 | endfunction 287 | 288 | " Returns a dict of current path to original path at the given base. 289 | function! s:obtain_file_renames(bufnr, base) 290 | let renames = {} 291 | let cmd = gitgutter#git(a:bufnr) 292 | if gitgutter#utility#git_supports_command_line_config_override() 293 | let cmd .= ' -c "core.safecrlf=false"' 294 | endif 295 | let cmd .= ' diff --diff-filter=R --name-status '.a:base 296 | let [out, error_code] = gitgutter#utility#system(cmd) 297 | if error_code 298 | " Assume the problem is the diff base. 299 | call gitgutter#utility#warn('g:gitgutter_diff_base ('.a:base.') is invalid') 300 | return {} 301 | endif 302 | for line in split(out, '\n') 303 | let fields = split(line) 304 | if len(fields) != 3 305 | call gitgutter#utility#warn('gitgutter: unable to list renamed files: '.line) 306 | return {} 307 | endif 308 | let [original, current] = fields[1:] 309 | let renames[current] = original 310 | endfor 311 | return renames 312 | endfunction 313 | 314 | function! s:abs_path(bufnr, shellesc) 315 | let p = resolve(expand('#'.a:bufnr.':p')) 316 | 317 | " Remove extra parts from fugitive's filepaths 318 | let p = substitute(substitute(p, '^fugitive:', '', ''), '\v\.git/\x{40,}/', '', '') 319 | 320 | return a:shellesc ? gitgutter#utility#shellescape(p) : p 321 | endfunction 322 | 323 | " Shellescaped 324 | function! gitgutter#utility#dir(bufnr) abort 325 | return gitgutter#utility#shellescape(fnamemodify(s:abs_path(a:bufnr, 0), ':h')) 326 | endfunction 327 | 328 | " Not shellescaped. 329 | function! gitgutter#utility#filename(bufnr) abort 330 | return fnamemodify(s:abs_path(a:bufnr, 0), ':t') 331 | endfunction 332 | 333 | function! s:exists_file(bufnr) abort 334 | return filereadable(s:abs_path(a:bufnr, 0)) 335 | endfunction 336 | 337 | " Get rid of any trailing new line or SOH character. 338 | " 339 | " git ls-files -z produces output with null line termination. 340 | " Vim's system() replaces any null characters in the output 341 | " with SOH (start of header), i.e. ^A. 342 | function! s:strip_trailing_new_line(line) abort 343 | return substitute(a:line, '[[:cntrl:]]$', '', '') 344 | endfunction 345 | 346 | function! gitgutter#utility#windows() 347 | return has('win64') || has('win32') || has('win16') 348 | endfunction 349 | -------------------------------------------------------------------------------- /doc/gitgutter.txt: -------------------------------------------------------------------------------- 1 | *gitgutter.txt* A Vim plugin which shows a git diff in the gutter. 2 | 3 | 4 | Vim GitGutter 5 | 6 | 7 | Author: Andy Stewart 8 | Plugin Homepage: 9 | 10 | 11 | =============================================================================== 12 | CONTENTS *gitgutter* 13 | 14 | Introduction ................. |gitgutter-introduction| 15 | Installation ................. |gitgutter-installation| 16 | Windows ................. |gitgutter-windows| 17 | Commands ..................... |gitgutter-commands| 18 | Mappings ..................... |gitgutter-mappings| 19 | Autocommand .................. |gitgutter-autocommand| 20 | Status line .................. |gitgutter-statusline| 21 | Options ...................... |gitgutter-options| 22 | Highlights ................... |gitgutter-highlights| 23 | FAQ .......................... |gitgutter-faq| 24 | TROUBLESHOOTING .............. |gitgutter-troubleshooting| 25 | 26 | 27 | =============================================================================== 28 | INTRODUCTION *gitgutter-introduction* 29 | 30 | GitGutter is a Vim plugin which shows a git diff in the sign column. 31 | It shows which lines have been added, modified, or removed. You can also 32 | preview, stage, and undo individual hunks. The plugin also provides a hunk 33 | text object. 34 | 35 | The signs are always up to date and the plugin never saves your buffer. 36 | 37 | The name "gitgutter" comes from the Sublime Text 3 plugin which inspired this 38 | one in 2013. 39 | 40 | 41 | =============================================================================== 42 | INSTALLATION *gitgutter-installation* 43 | 44 | First, use your favourite package manager, or use Vim's built-in package 45 | support. 46 | 47 | Vim:~ 48 | > 49 | mkdir -p ~/.vim/pack/airblade/start 50 | cd ~/.vim/pack/airblade/start 51 | git clone https://github.com/airblade/vim-gitgutter.git 52 | vim -u NONE -c "helptags vim-gitgutter/doc" -c q 53 | < 54 | 55 | Neovim:~ 56 | > 57 | mkdir -p ~/.config/nvim/pack/airblade/start 58 | cd ~/.config/nvim/pack/airblade/start 59 | git clone https://github.com/airblade/vim-gitgutter.git 60 | nvim -u NONE -c "helptags vim-gitgutter/doc" -c q 61 | < 62 | 63 | Second, ensure your 'updatetime' and 'signcolumn' options are set appropriately. 64 | 65 | When you make a change to a file tracked by git, the diff markers should 66 | appear automatically after a short delay. The delay is governed by vim's 67 | 'updatetime' option; the default value is `4000`, i.e. 4 seconds, but I 68 | suggest reducing it to around 100ms (add `set updatetime=100` to your vimrc). 69 | Note 'updatetime' also controls the delay before vim writes its swap file. 70 | 71 | The 'signcolumn' option can have any value except "off". 72 | 73 | 74 | =============================================================================== 75 | WINDOWS *gitgutter-windows* 76 | 77 | There is a potential risk on Windows due to `cmd.exe` prioritising the current 78 | folder over folders in `PATH`. If you have a file named `git.*` (i.e. with 79 | any extension in `PATHEXT`) in your current folder, it will be executed 80 | instead of git whenever the plugin calls git. 81 | 82 | You can avoid this risk by configuring the full path to your git executable. 83 | For example: 84 | > 85 | " This path probably won't work 86 | let g:gitgutter_git_executable = 'C:\Program Files\Git\bin\git.exe' 87 | < 88 | 89 | Unfortunately I don't know the correct escaping for the path - if you do, 90 | please let me know! 91 | 92 | 93 | =============================================================================== 94 | COMMANDS *gitgutter-commands* 95 | 96 | Commands for turning vim-gitgutter on and off:~ 97 | 98 | *gitgutter-:GitGutterDisable* 99 | :GitGutterDisable Turn vim-gitgutter off for all buffers. 100 | 101 | *gitgutter-:GitGutterEnable* 102 | :GitGutterEnable Turn vim-gitgutter on for all buffers. 103 | 104 | *gitgutter-:GitGutterToggle* 105 | :GitGutterToggle Toggle vim-gitgutter on or off for all buffers. 106 | 107 | *gitgutter-:GitGutterBufferDisable* 108 | :GitGutterBufferDisable Turn vim-gitgutter off for current buffer. 109 | 110 | *gitgutter-:GitGutterBufferEnable* 111 | :GitGutterBufferEnable Turn vim-gitgutter on for current buffer. 112 | 113 | *gitgutter-:GitGutterBufferToggle* 114 | :GitGutterBufferToggle Toggle vim-gitgutter on or off for current buffer. 115 | 116 | *gitgutter-:GitGutter* 117 | :GitGutter Update signs for the current buffer. You shouldn't 118 | need to run this. 119 | 120 | *gitgutter-:GitGutterAll* 121 | :GitGutterAll Update signs for all buffers. You shouldn't need to 122 | run this. 123 | 124 | 125 | Commands for turning signs on and off (defaults to on):~ 126 | 127 | *gitgutter-:GitGutterSignsEnable* 128 | :GitGutterSignsEnable Show signs for the diff. 129 | 130 | *gitgutter-:GitGutterSignsDisable* 131 | :GitGutterSignsDisable Do not show signs for the diff. 132 | 133 | *gitgutter-:GitGutterSignsToggle* 134 | :GitGutterSignsToggle Toggle signs on or off. 135 | 136 | 137 | Commands for turning line highlighting on and off (defaults to off):~ 138 | 139 | *gitgutter-:GitGutterLineHighlightsEnable* 140 | :GitGutterLineHighlightsEnable Turn on line highlighting. 141 | 142 | *gitgutter-:GitGutterLineHighlightsDisable* 143 | :GitGutterLineHighlightsDisable Turn off line highlighting. 144 | 145 | *gitgutter-:GitGutterLineHighlightsToggle* 146 | :GitGutterLineHighlightsToggle Turn line highlighting on or off. 147 | 148 | 149 | Commands for turning line number highlighting on and off (defaults to off):~ 150 | NOTE: This feature requires Neovim 0.3.2 or higher. 151 | 152 | *gitgutter-:GitGutterLineNrHighlightsEnable* 153 | :GitGutterLineNrHighlightsEnable Turn on line highlighting. 154 | 155 | *gitgutter-:GitGutterLineNrHighlightsDisable* 156 | :GitGutterLineNrHighlightsDisable Turn off line highlighting. 157 | 158 | *gitgutter-:GitGutterLineNrHighlightsToggle* 159 | :GitGutterLineNrHighlightsToggle Turn line highlighting on or off. 160 | 161 | 162 | Commands for jumping between hunks:~ 163 | 164 | *gitgutter-:GitGutterNextHunk* 165 | :GitGutterNextHunk Jump to the next [count] hunk. 166 | 167 | *gitgutter-:GitGutterPrevHunk* 168 | :GitGutterPrevHunk Jump to the previous [count] hunk. 169 | 170 | *gitgutter-:GitGutterQuickFix* 171 | :GitGutterQuickFix Load all hunks into the |quickfix| list. Note this 172 | ignores any unsaved changes in your buffers. The 173 | |g:gitgutter_use_location_list| option can be set to 174 | populate the location list of the current window 175 | instead. Use |:copen| (or |:lopen|) to open a buffer 176 | containing the search results in linked form; or add a 177 | custom command like this: 178 | > 179 | command! Gqf GitGutterQuickFix | copen 180 | < 181 | *gitgutter-:GitGutterQuickFixCurrentFile* 182 | :GitGutterQuickFixCurrentFile Same as :GitGutterQuickFix, but only load hunks for 183 | the file in the focused buffer. This has the same 184 | functionality as :GitGutterQuickFix when the focused 185 | buffer is empty. 186 | 187 | 188 | Commands for operating on a hunk:~ 189 | 190 | *gitgutter-:GitGutterStageHunk* 191 | :GitGutterStageHunk Stage the hunk the cursor is in. Use a visual selection 192 | to stage part of an (additions-only) hunk; or use a 193 | range. 194 | 195 | To stage part of any hunk, first |GitGutterPreviewHunk| 196 | it, then move to the preview window, delete the lines 197 | you do not want to stage, and |write| or 198 | |GitGutterStageHunk|. 199 | 200 | *gitgutter-:GitGutterUndoHunk* 201 | :GitGutterUndoHunk Undo the hunk the cursor is in. 202 | 203 | *gitgutter-:GitGutterPreviewHunk* 204 | :GitGutterPreviewHunk Preview the hunk the cursor is in or, if you are using 205 | floating preview windows in Neovim and the window is 206 | already open, move the cursor into the window. 207 | 208 | To stage part of the hunk, move to the preview window, 209 | delete any lines you do not want to stage, and |write| 210 | or |GitGutterStageHunk|. 211 | 212 | To close a non-floating preview window use |:pclose| 213 | or |CTRL-W_z| or |CTRL-W_CTRL-Z|; or normal window- 214 | closing (|:quit| or |:close| or |CTRL-W_c|) if your cursor 215 | is in the preview window. 216 | 217 | To close a floating window when the cursor is in the 218 | original buffer, move the cursor. 219 | 220 | To close a floating window when the cursor is in the 221 | floating window use normal window-closing, or move to 222 | the original window with |CTRL-W_p|. Alternatively set 223 | |g:gitgutter_close_preview_on_escape| and use . 224 | 225 | Two functions are available for your own logic: 226 | > 227 | gitgutter#hunk#is_preview_window_open() 228 | gitgutter#hunk#close_hunk_preview_window() 229 | < 230 | 231 | Commands for folds:~ 232 | 233 | *gitgutter-:GitGutterFold* 234 | :GitGutterFold Fold all unchanged lines. Execute again to undo. 235 | 236 | 237 | Other commands:~ 238 | 239 | *gitgutter-:GitGutterDiffOrig* 240 | :GitGutterDiffOrig Similar to |:DiffOrig| but shows gitgutter's diff. 241 | 242 | 243 | =============================================================================== 244 | AUTOCOMMANDS *gitgutter-autocommands* 245 | 246 | User GitGutter~ 247 | 248 | After updating a buffer's signs vim-gitgutter fires a |User| |autocmd| with the 249 | event GitGutter. You can listen for this event, for example: 250 | > 251 | autocmd User GitGutter call updateMyStatusLine() 252 | < 253 | A dictionary `g:gitgutter_hook_context` is made available during its execution, 254 | which contains an entry `bufnr` that contains the buffer number being updated. 255 | 256 | User GitGutterStage~ 257 | 258 | After staging a hunk or part of a hunk vim-gitgutter fires a |User| |autocmd| 259 | with the event GitGutterStage. Staging always happens in the current buffer. 260 | 261 | =============================================================================== 262 | MAPPINGS *gitgutter-mappings* 263 | 264 | You can disable all these mappings with: 265 | > 266 | let g:gitgutter_map_keys = 0 267 | < 268 | 269 | Hunk operations:~ 270 | 271 | These can be repeated with `.` if you have vim-repeat installed. 272 | 273 | *gitgutter-hp* 274 | hp Preview the hunk under the cursor. 275 | 276 | *gitgutter-hs* 277 | hs Stage the hunk under the cursor. 278 | 279 | *gitgutter-hu* 280 | hu Undo the hunk under the cursor. 281 | 282 | You can change these mappings like this: 283 | > 284 | nmap ghp (GitGutterPreviewHunk) 285 | nmap ghs (GitGutterStageHunk) 286 | nmap ghu (GitGutterUndoHunk) 287 | < 288 | 289 | Hunk jumping:~ 290 | 291 | *gitgutter-]c* 292 | ]c Jump to the next [count] hunk. 293 | 294 | *gitgutter-[c* 295 | [c Jump to the previous [count] hunk. 296 | 297 | You can change these mappings like this: 298 | > 299 | nmap [c (GitGutterPrevHunk) 300 | nmap ]c (GitGutterNextHunk) 301 | < 302 | 303 | Hunk text object:~ 304 | 305 | *gitgutter-ic* *gitgutter-ac* *gitgutter-text-object* 306 | "ic" operates on the current hunk's lines. "ac" does the same but also includes 307 | trailing empty lines. 308 | > 309 | omap ic (GitGutterTextObjectInnerPending) 310 | omap ac (GitGutterTextObjectOuterPending) 311 | xmap ic (GitGutterTextObjectInnerVisual) 312 | xmap ac (GitGutterTextObjectOuterVisual) 313 | < 314 | 315 | 316 | =============================================================================== 317 | STATUS LINE *gitgutter-statusline* 318 | 319 | 320 | Call the `GitGutterGetHunkSummary()` function from your status line to get a 321 | list of counts of added, modified, and removed lines in the current buffer. 322 | For example: 323 | > 324 | " Your vimrc 325 | function! GitStatus() 326 | let [a,m,r] = GitGutterGetHunkSummary() 327 | return printf('+%d ~%d -%d', a, m, r) 328 | endfunction 329 | set statusline+=%{GitStatus()} 330 | < 331 | 332 | 333 | =============================================================================== 334 | OPTIONS *gitgutter-options* 335 | 336 | The most important option is 'updatetime' which determines how long (in 337 | milliseconds) the plugin will wait after you stop typing before it updates the 338 | signs. Vim's default is 4000. I recommend 100. Note this also controls how 339 | long vim waits before writing its swap file. 340 | 341 | Most important option:~ 342 | 343 | 'updatetime' 344 | 345 | Git:~ 346 | 347 | |g:gitgutter_git_executable| 348 | |g:gitgutter_git_args| 349 | |g:gitgutter_diff_args| 350 | |g:gitgutter_diff_relative_to| 351 | |g:gitgutter_diff_base| 352 | 353 | Grep:~ 354 | 355 | |g:gitgutter_grep| 356 | 357 | Signs:~ 358 | 359 | |g:gitgutter_signs| 360 | |g:gitgutter_highlight_lines| 361 | |g:gitgutter_highlight_linenrs| 362 | |g:gitgutter_max_signs| 363 | |g:gitgutter_sign_priority| 364 | |g:gitgutter_sign_allow_clobber| 365 | |g:gitgutter_sign_added| 366 | |g:gitgutter_sign_modified| 367 | |g:gitgutter_sign_removed| 368 | |g:gitgutter_sign_removed_first_line| 369 | |g:gitgutter_sign_modified_removed| 370 | |g:gitgutter_set_sign_backgrounds| 371 | 372 | Hunk jumping:~ 373 | 374 | |g:gitgutter_show_msg_on_hunk_jumping| 375 | 376 | Hunk previews:~ 377 | 378 | |g:gitgutter_preview_win_floating| 379 | |g:gitgutter_floating_window_options| 380 | |g:gitgutter_close_preview_on_escape| 381 | 382 | Terminal:~ 383 | 384 | |g:gitgutter_terminal_reports_focus| 385 | 386 | General:~ 387 | 388 | |g:gitgutter_enabled| 389 | |g:gitgutter_map_keys| 390 | |g:gitgutter_async| 391 | |g:gitgutter_log| 392 | |g:gitgutter_use_location_list| 393 | 394 | 395 | *g:gitgutter_preview_win_location* 396 | Default: 'bo' 397 | 398 | This option determines where the preview window pops up as a result of the 399 | :GitGutterPreviewHunk command. Other plausible values are 'to', 'bel', 'abo'. 400 | See the end of the |opening-window| docs. 401 | 402 | *g:gitgutter_git_executable* 403 | Default: 'git' 404 | 405 | This option determines what git binary to use. Set this if git is not on your 406 | path. 407 | 408 | *g:gitgutter_git_args* 409 | Default: empty 410 | 411 | Use this option to pass any extra arguments to git when running git-diff. 412 | For example: 413 | > 414 | let g:gitgutter_git_args = '--git-dir=""' 415 | < 416 | 417 | *g:gitgutter_diff_args* 418 | Default: empty 419 | 420 | Use this option to pass any extra arguments to git-diff. For example: 421 | > 422 | let g:gitgutter_diff_args = '-w' 423 | < 424 | 425 | *g:gitgutter_diff_relative_to* 426 | Default: empty 427 | 428 | By default buffers are diffed against the index. Use this option to diff against 429 | the working tree. For example: 430 | > 431 | let g:gitgutter_diff_relative_to = 'working_tree' 432 | < 433 | 434 | *g:gitgutter_diff_base* 435 | Default: empty 436 | 437 | By default buffers are diffed against the index. Use this option to diff against 438 | a revision instead. For example: 439 | > 440 | let g:gitgutter_diff_base = '' 441 | < 442 | 443 | If you are looking at a previous version of a file with Fugitive (e.g. 444 | via :0Gclog), gitgutter sets the diff base to the parent of the current revision. 445 | 446 | This setting is ignore when the diff is relative to the working tree 447 | (|g:gitgutter_diff_relative_to|). 448 | 449 | *g:gitgutter_grep* 450 | Default: 'grep' 451 | 452 | The plugin pipes the output of git-diff into grep to minimise the amount of data 453 | vim has to process. Set this option if grep is not on your path. 454 | 455 | grep must produce plain-text output without any ANSI escape codes or colours. 456 | Use this option to turn off colours if necessary. 457 | > 458 | let g:gitgutter_grep = 'grep --color=never' 459 | < 460 | If you do not want to use grep at all (perhaps to debug why signs are not 461 | showing), set this option to an empty string: 462 | > 463 | let g:gitgutter_grep = '' 464 | < 465 | 466 | *g:gitgutter_signs* 467 | Default: 1 468 | 469 | Determines whether or not to show signs. 470 | 471 | *g:gitgutter_highlight_lines* 472 | Default: 0 473 | 474 | Determines whether or not to show line highlights. 475 | 476 | *g:gitgutter_highlight_linenrs* 477 | Default: 0 478 | 479 | Determines whether or not to show line number highlights. 480 | 481 | *g:gitgutter_max_signs* 482 | Default: 500 (Vim < 8.1.0614, Neovim < 0.4.0) 483 | -1 (otherwise) 484 | 485 | Sets the maximum number of signs to show in a buffer. Vim is slow at updating 486 | signs, so to avoid slowing down the GUI the number of signs is capped. When 487 | the number of changed lines exceeds this value, the plugin removes all signs 488 | and displays a warning message. 489 | 490 | When set to -1 the limit is not applied. 491 | 492 | *g:gitgutter_sign_priority* 493 | Default: 10 494 | 495 | Sets the |sign-priority| gitgutter assigns to its signs. 496 | 497 | *g:gitgutter_sign_allow_clobber* 498 | Default: 0 (Vim < 8.1.0614, Neovim < 0.4.0) 499 | 1 (otherwise) 500 | 501 | Determines whether gitgutter preserves non-gitgutter signs. When 1, gitgutter 502 | will not preserve non-gitgutter signs. 503 | 504 | *g:gitgutter_sign_added* 505 | *g:gitgutter_sign_modified* 506 | *g:gitgutter_sign_removed* 507 | *g:gitgutter_sign_removed_first_line* 508 | *g:gitgutter_sign_removed_above_and_below* 509 | *g:gitgutter_sign_modified_removed* 510 | Defaults: 511 | > 512 | let g:gitgutter_sign_added = '+' 513 | let g:gitgutter_sign_modified = '~' 514 | let g:gitgutter_sign_removed = '_' 515 | let g:gitgutter_sign_removed_first_line = '‾' 516 | let g:gitgutter_sign_removed_above_and_below = '_¯' 517 | let g:gitgutter_sign_modified_removed = '~_' 518 | < 519 | You can use unicode characters but not images. Signs must not take up more than 520 | 2 columns. 521 | 522 | *g:gitgutter_set_sign_backgrounds* 523 | Default: 0 524 | 525 | Only applies to existing GitGutter* highlight groups. See 526 | |gitgutter-highlights|. 527 | 528 | Controls whether to override the signs' background colours to match the 529 | |hl-SignColumn|. 530 | 531 | *g:gitgutter_preview_win_floating* 532 | Default: 0 (Vim) 533 | 0 (NeoVim which does not support floating windows) 534 | 1 (NeoVim which does support floating windows) 535 | 536 | Whether to use floating/popup windows for hunk previews. Note that if you use 537 | popup windows on Vim you will not be able to stage partial hunks via the 538 | preview window. 539 | 540 | *g:gitgutter_floating_window_options* 541 | Default: 542 | > 543 | " Vim 544 | { 545 | \ 'line': 'cursor+1', 546 | \ 'col': 'cursor', 547 | \ 'moved': 'any' 548 | } 549 | 550 | " Neovim 551 | { 552 | \ 'relative': 'cursor', 553 | \ 'row': 1, 554 | \ 'col': 0, 555 | \ 'width': 42, 556 | \ 'height': &previewheight, 557 | \ 'style': 'minimal' 558 | } 559 | < 560 | This dictionary is passed directly to |popup_create()| (Vim) or 561 | |nvim_open_win()| (Neovim). 562 | 563 | If you simply want to override one or two of the default values, create a file 564 | in an after/ directory. For example: 565 | > 566 | " ~/.vim/after/vim-gitgutter/overrides.vim 567 | let g:gitgutter_floating_window_options['border'] = 'single' 568 | < 569 | 570 | *g:gitgutter_close_preview_on_escape* 571 | Default: 0 572 | 573 | Whether pressing in a preview window closes it. 574 | 575 | *g:gitgutter_terminal_reports_focus* 576 | Default: 1 577 | 578 | Normally the plugin uses |FocusGained| to force-update all buffers when Vim 579 | receives focus. However some terminals do not report focus events and so the 580 | |FocusGained| autocommand never fires. 581 | 582 | If this applies to you, either install something like Terminus 583 | (https://github.com/wincent/terminus) to make |FocusGained| work or set this 584 | option to 0. 585 | 586 | If you use tmux, try this in your tmux.conf: 587 | > 588 | set -g focus-events on 589 | < 590 | 591 | When this option is 0, the plugin force-updates the buffer on |BufEnter| 592 | (instead of only updating if the buffer's contents has changed since the last 593 | update). 594 | 595 | *g:gitgutter_enabled* 596 | Default: 1 597 | 598 | Controls whether or not the plugin is on at startup. 599 | 600 | *g:gitgutter_map_keys* 601 | Default: 1 602 | 603 | Controls whether or not the plugin provides mappings. See |gitgutter-mappings|. 604 | 605 | *g:gitgutter_async* 606 | Default: 1 607 | 608 | Controls whether or not diffs are run in the background. This has no effect if 609 | your Vim does not support background jobs. 610 | 611 | *g:gitgutter_log* 612 | Default: 0 613 | 614 | When switched on, the plugin logs to gitgutter.log in the directory where it is 615 | installed. Additionally it logs channel activity to channel.log. 616 | 617 | *g:gitgutter_use_location_list* 618 | Default: 0 619 | 620 | When switched on, the :GitGutterQuickFix command populates the location list 621 | of the current window instead of the global quickfix list. 622 | 623 | *g:gitgutter_show_msg_on_hunk_jumping* 624 | Default: 1 625 | 626 | When switched on, a message like "Hunk 4 of 11" is shown on hunk jumping. 627 | 628 | 629 | =============================================================================== 630 | HIGHLIGHTS *gitgutter-highlights* 631 | 632 | To change the signs' colours, specify these highlight groups in your |vimrc|: 633 | > 634 | highlight GitGutterAdd guifg=#009900 ctermfg=2 635 | highlight GitGutterChange guifg=#bbbb00 ctermfg=3 636 | highlight GitGutterDelete guifg=#ff2222 ctermfg=1 637 | < 638 | 639 | See |highlight-guifg| and |highlight-ctermfg| for the values you can use. 640 | 641 | If you do not like the signs' background colours and you do not want to update 642 | the GitGutter* highlight groups yourself, you can get the plugin to do it 643 | |g:gitgutter_set_sign_backgrounds|. 644 | 645 | To change the line highlights, set up the following highlight groups in your 646 | colorscheme or |vimrc|: 647 | > 648 | GitGutterAddLine " default: links to DiffAdd 649 | GitGutterChangeLine " default: links to DiffChange 650 | GitGutterDeleteLine " default: links to DiffDelete 651 | GitGutterChangeDeleteLine " default: links to GitGutterChangeLine 652 | < 653 | 654 | For example, to use |hl-DiffText| instead of |hl-DiffChange|: 655 | > 656 | highlight link GitGutterChangeLine DiffText 657 | < 658 | To change the line number highlights, set up the following highlight groups in 659 | your colorscheme or |vimrc|: 660 | > 661 | GitGutterAddLineNr " default: links to CursorLineNr 662 | GitGutterChangeLineNr " default: links to CursorLineNr 663 | GitGutterDeleteLineNr " default: links to CursorLineNr 664 | GitGutterChangeDeleteLineNr " default: links to GitGutterChangeLineNr 665 | < 666 | For example, to use |hl-Underlined| instead of |hl-CursorLineNr|: 667 | > 668 | highlight link GitGutterChangeLineNr Underlined 669 | < 670 | To change the diff syntax colours used in the preview window, set up the diff* 671 | highlight groups in your colorscheme or |vimrc|: 672 | > 673 | diffAdded " if not set: use GitGutterAdd's foreground colour 674 | diffChanged " if not set: use GitGutterChange's foreground colour 675 | diffRemoved " if not set: use GitGutterDelete's foreground colour 676 | < 677 | Note the diff* highlight groups are used in any buffer whose 'syntax' is 678 | "diff". 679 | 680 | To change the intra-line diff highlights used in the preview window, set up 681 | the following highlight groups in your colorscheme or |vimrc|: 682 | > 683 | GitGutterAddIntraLine " default: gui=reverse cterm=reverse 684 | GitGutterDeleteIntraLine " default: gui=reverse cterm=reverse 685 | < 686 | For example, to use |hl-DiffAdd| for intra-line added regions: 687 | > 688 | highlight link GitGutterAddIntraLine DiffAdd 689 | < 690 | 691 | 692 | =============================================================================== 693 | FAQ *gitgutter-faq* 694 | 695 | a. How do I turn off realtime updates? 696 | 697 | Add this to your vim configuration in an |after-directory|: 698 | > 699 | autocmd! gitgutter CursorHold,CursorHoldI 700 | < 701 | 702 | b. I turned off realtime updates, how can I have signs updated when I save a 703 | file? 704 | 705 | If you really want to update the signs when you save a file, add this to your 706 | |vimrc|: 707 | > 708 | autocmd BufWritePost * GitGutter 709 | < 710 | 711 | c. Why can't I unstage staged changes? 712 | 713 | This plugin is for showing changes between the working tree and the index 714 | (and staging/undoing those changes). Unstaging a staged hunk would require 715 | showing changes between the index and HEAD, which is out of scope. 716 | 717 | d. Why are the colours in the sign column weird? 718 | 719 | Your colorscheme is configuring the |hl-SignColumn| highlight group weirdly. 720 | Here are two ways you could change the colours: 721 | > 722 | highlight! link SignColumn LineNr 723 | highlight SignColumn guibg=whatever ctermbg=whatever 724 | < 725 | 726 | e. What happens if I also use another plugin which uses signs (e.g. Syntastic)? 727 | 728 | Vim only allows one sign per line. Vim-gitgutter will not interfere with 729 | signs it did not add. 730 | 731 | 732 | =============================================================================== 733 | TROUBLESHOOTING *gitgutter-troubleshooting* 734 | 735 | When no signs are showing at all:~ 736 | 737 | 1. Try bypassing grep with: 738 | > 739 | let g:gitgutter_grep = '' 740 | < 741 | If it works, the problem is grep outputting ANSI escape codes. Use this 742 | option to pass arguments to grep to turn off the escape codes. 743 | 744 | 2. Verify git is on your path: 745 | > 746 | :echo system('git --version') 747 | < 748 | 749 | 3. Verify your git config is compatible with the version of git return by the 750 | command above. 751 | 752 | 4. Verify your Vim supports signs. The following should give 1: 753 | > 754 | :echo has('signs') 755 | < 756 | 757 | 5. Check whether the plugin thinks git knows about your file: 758 | > 759 | :echo getbufvar('','gitgutter').path 760 | < 761 | If the result is -2, the plugin thinks your file is not tracked by git. 762 | 763 | 6. Check whether the signs have been placed: 764 | > 765 | :sign place group=gitgutter 766 | < 767 | If you see a list of signs, this is a colorscheme / highlight problem. 768 | Compare these two highlight values: 769 | > 770 | :highlight GitGutterAdd 771 | :highlight SignColumn 772 | < 773 | If no signs are listed, the call to git-diff is probably failing. Turn on 774 | logging by adding the following to your vimrc, restart, reproduce the problem, 775 | and examing the gitgutter.log file in the plugin's directory. 776 | > 777 | let g:gitgutter_log = 1 778 | < 779 | 780 | When the whole file is marked as added:~ 781 | 782 | If you use zsh, and you set "CDPATH", make sure "CDPATH" does not include the 783 | current directory. 784 | 785 | 786 | When signs take a few seconds to appear:~ 787 | 788 | Try reducing 'updatetime': 789 | > 790 | set updatetime=100 791 | < 792 | 793 | Note this also controls how long vim waits before writing its swap file. 794 | 795 | 796 | When signs don't update after focusing Vim:~ 797 | 798 | Your terminal probably isn't reporting focus events. Either try installing 799 | Terminus (https://github.com/wincent/terminus) or set: 800 | > 801 | let g:gitgutter_terminal_reports_focus = 0 802 | < 803 | 804 | vim:tw=78:et:ft=help:norl: 805 | -------------------------------------------------------------------------------- /plugin/gitgutter.vim: -------------------------------------------------------------------------------- 1 | scriptencoding utf-8 2 | 3 | if exists('g:loaded_gitgutter') || !has('signs') || &cp 4 | finish 5 | endif 6 | let g:loaded_gitgutter = 1 7 | 8 | " Initialisation {{{ 9 | 10 | if v:version < 703 || (v:version == 703 && !has("patch105")) 11 | call gitgutter#utility#warn('Requires Vim 7.3.105') 12 | finish 13 | endif 14 | 15 | let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '' : '' 16 | 17 | function! s:obsolete(var) 18 | if exists(a:var) 19 | call gitgutter#utility#warn(a:var.' is obsolete and has no effect.') 20 | endif 21 | endfunction 22 | 23 | 24 | let g:gitgutter_preview_win_location = get(g:, 'gitgutter_preview_win_location', 'bo') 25 | if exists('*nvim_open_win') 26 | let g:gitgutter_preview_win_floating = get(g:, 'gitgutter_preview_win_floating', 1) 27 | let g:gitgutter_floating_window_options = get(g:, 'gitgutter_floating_window_options', { 28 | \ 'relative': 'cursor', 29 | \ 'row': 1, 30 | \ 'col': 0, 31 | \ 'width': 42, 32 | \ 'height': &previewheight, 33 | \ 'style': 'minimal' 34 | \ }) 35 | else 36 | let default = exists('&previewpopup') ? !empty(&previewpopup) : 0 37 | let g:gitgutter_preview_win_floating = get(g:, 'gitgutter_preview_win_floating', default) 38 | let g:gitgutter_floating_window_options = get(g:, 'gitgutter_floating_window_options', { 39 | \ 'line': 'cursor+1', 40 | \ 'col': 'cursor', 41 | \ 'moved': 'any' 42 | \ }) 43 | endif 44 | let g:gitgutter_enabled = get(g:, 'gitgutter_enabled', 1) 45 | if exists('*sign_unplace') 46 | let g:gitgutter_max_signs = get(g:, 'gitgutter_max_signs', -1) 47 | else 48 | let g:gitgutter_max_signs = get(g:, 'gitgutter_max_signs', 500) 49 | endif 50 | let g:gitgutter_signs = get(g:, 'gitgutter_signs', 1) 51 | let g:gitgutter_highlight_lines = get(g:, 'gitgutter_highlight_lines', 0) 52 | let g:gitgutter_highlight_linenrs = get(g:, 'gitgutter_highlight_linenrs', 0) 53 | let g:gitgutter_sign_priority = get(g:, 'gitgutter_sign_priority', 10) 54 | " Nvim 0.4.0 has an expanding sign column 55 | " The sign_place() function supports sign priority. 56 | if (has('nvim-0.4.0') || exists('*sign_place')) && !exists('g:gitgutter_sign_allow_clobber') 57 | let g:gitgutter_sign_allow_clobber = 1 58 | endif 59 | let g:gitgutter_sign_allow_clobber = get(g:, 'gitgutter_sign_allow_clobber', 0) 60 | let g:gitgutter_set_sign_backgrounds = get(g:, 'gitgutter_set_sign_backgrounds', 0) 61 | let g:gitgutter_sign_added = get(g:, 'gitgutter_sign_added', '+') 62 | let g:gitgutter_sign_modified = get(g:, 'gitgutter_sign_modified', '~') 63 | let g:gitgutter_sign_removed = get(g:, 'gitgutter_sign_removed', '_') 64 | 65 | if gitgutter#utility#supports_overscore_sign() 66 | let g:gitgutter_sign_removed_first_line = get(g:, 'gitgutter_sign_removed_first_line', '‾') 67 | else 68 | let g:gitgutter_sign_removed_first_line = get(g:, 'gitgutter_sign_removed_first_line', '_^') 69 | endif 70 | 71 | let g:gitgutter_sign_removed_above_and_below = get(g:, 'gitgutter_sign_removed_above_and_below', '_¯') 72 | let g:gitgutter_sign_modified_removed = get(g:, 'gitgutter_sign_modified_removed', '~_') 73 | let g:gitgutter_git_args = get(g:, 'gitgutter_git_args', '') 74 | let g:gitgutter_diff_relative_to = get(g:, 'gitgutter_diff_relative_to', 'index') 75 | let g:gitgutter_diff_args = get(g:, 'gitgutter_diff_args', '') 76 | let g:gitgutter_diff_base = get(g:, 'gitgutter_diff_base', '') 77 | let g:gitgutter_map_keys = get(g:, 'gitgutter_map_keys', 1) 78 | let g:gitgutter_terminal_reports_focus = get(g:, 'gitgutter_terminal_reports_focus', 1) 79 | let g:gitgutter_async = get(g:, 'gitgutter_async', 1) 80 | let g:gitgutter_log = get(g:, 'gitgutter_log', 0) 81 | let g:gitgutter_use_location_list = get(g:, 'gitgutter_use_location_list', 0) 82 | let g:gitgutter_close_preview_on_escape = get(g:, 'gitgutter_close_preview_on_escape', 0) 83 | let g:gitgutter_show_msg_on_hunk_jumping = get(g:, 'gitgutter_show_msg_on_hunk_jumping', 1) 84 | 85 | let g:gitgutter_git_executable = get(g:, 'gitgutter_git_executable', 'git') 86 | if !executable(g:gitgutter_git_executable) 87 | if g:gitgutter_enabled 88 | call gitgutter#utility#warn('Cannot find git. Please set g:gitgutter_git_executable.') 89 | endif 90 | finish 91 | endif 92 | 93 | let default_grep = 'grep' 94 | let g:gitgutter_grep = get(g:, 'gitgutter_grep', default_grep) 95 | if !empty(g:gitgutter_grep) 96 | if executable(split(g:gitgutter_grep)[0]) 97 | if $GREP_OPTIONS =~# '--color=always' 98 | let g:gitgutter_grep .= ' --color=never' 99 | endif 100 | else 101 | if g:gitgutter_grep !=# default_grep 102 | call gitgutter#utility#warn('Cannot find '.g:gitgutter_grep.'. Please check g:gitgutter_grep.') 103 | endif 104 | let g:gitgutter_grep = '' 105 | endif 106 | endif 107 | 108 | call gitgutter#highlight#define_highlights() 109 | call gitgutter#highlight#define_signs() 110 | 111 | " Prevent infinite loop where: 112 | " - executing a job in the foreground launches a new window which takes the focus; 113 | " - when the job finishes, focus returns to gvim; 114 | " - the FocusGained event triggers a new job (see below). 115 | if gitgutter#utility#windows() && !(g:gitgutter_async && gitgutter#async#available()) 116 | set noshelltemp 117 | endif 118 | 119 | " }}} 120 | 121 | " Primary functions {{{ 122 | 123 | command! -bar GitGutterAll call gitgutter#all(1) 124 | command! -bar GitGutter call gitgutter#process_buffer(bufnr(''), 1) 125 | 126 | command! -bar GitGutterDisable call gitgutter#disable() 127 | command! -bar GitGutterEnable call gitgutter#enable() 128 | command! -bar GitGutterToggle call gitgutter#toggle() 129 | 130 | command! -bar GitGutterBufferDisable call gitgutter#buffer_disable() 131 | command! -bar GitGutterBufferEnable call gitgutter#buffer_enable() 132 | command! -bar GitGutterBufferToggle call gitgutter#buffer_toggle() 133 | 134 | command! -bar GitGutterQuickFix call gitgutter#quickfix(0) 135 | command! -bar GitGutterQuickFixCurrentFile call gitgutter#quickfix(1) 136 | 137 | command! -bar GitGutterDiffOrig call gitgutter#difforig() 138 | 139 | " }}} 140 | 141 | " Line highlights {{{ 142 | 143 | command! -bar GitGutterLineHighlightsDisable call gitgutter#highlight#line_disable() 144 | command! -bar GitGutterLineHighlightsEnable call gitgutter#highlight#line_enable() 145 | command! -bar GitGutterLineHighlightsToggle call gitgutter#highlight#line_toggle() 146 | 147 | " }}} 148 | 149 | " 'number' column highlights {{{ 150 | command! -bar GitGutterLineNrHighlightsDisable call gitgutter#highlight#linenr_disable() 151 | command! -bar GitGutterLineNrHighlightsEnable call gitgutter#highlight#linenr_enable() 152 | command! -bar GitGutterLineNrHighlightsToggle call gitgutter#highlight#linenr_toggle() 153 | " }}} 154 | 155 | " Signs {{{ 156 | 157 | command! -bar GitGutterSignsEnable call gitgutter#sign#enable() 158 | command! -bar GitGutterSignsDisable call gitgutter#sign#disable() 159 | command! -bar GitGutterSignsToggle call gitgutter#sign#toggle() 160 | 161 | " }}} 162 | 163 | " Hunks {{{ 164 | 165 | command! -bar -count=1 GitGutterNextHunk call gitgutter#hunk#next_hunk() 166 | command! -bar -count=1 GitGutterPrevHunk call gitgutter#hunk#prev_hunk() 167 | 168 | command! -bar -range=% GitGutterStageHunk call gitgutter#hunk#stage(,) 169 | command! -bar GitGutterUndoHunk call gitgutter#hunk#undo() 170 | command! -bar GitGutterPreviewHunk call gitgutter#hunk#preview() 171 | 172 | " Hunk text object 173 | onoremap (GitGutterTextObjectInnerPending) :call gitgutter#hunk#text_object(1) 174 | onoremap (GitGutterTextObjectOuterPending) :call gitgutter#hunk#text_object(0) 175 | xnoremap (GitGutterTextObjectInnerVisual) :call gitgutter#hunk#text_object(1) 176 | xnoremap (GitGutterTextObjectOuterVisual) :call gitgutter#hunk#text_object(0) 177 | 178 | 179 | " Returns the git-diff hunks for the file or an empty list if there 180 | " aren't any hunks. 181 | " 182 | " The return value is a list of lists. There is one inner list per hunk. 183 | " 184 | " [ 185 | " [from_line, from_count, to_line, to_count], 186 | " [from_line, from_count, to_line, to_count], 187 | " ... 188 | " ] 189 | " 190 | " where: 191 | " 192 | " `from` - refers to the staged file 193 | " `to` - refers to the working tree's file 194 | " `line` - refers to the line number where the change starts 195 | " `count` - refers to the number of lines the change covers 196 | function! GitGutterGetHunks() 197 | let bufnr = bufnr('') 198 | return gitgutter#utility#is_active(bufnr) ? gitgutter#hunk#hunks(bufnr) : [] 199 | endfunction 200 | 201 | " Returns an array that contains a summary of the hunk status for the current 202 | " window. The format is [ added, modified, removed ], where each value 203 | " represents the number of lines added/modified/removed respectively. 204 | function! GitGutterGetHunkSummary() 205 | return gitgutter#hunk#summary(winbufnr(0)) 206 | endfunction 207 | 208 | " }}} 209 | 210 | " Folds {{{ 211 | 212 | command! -bar GitGutterFold call gitgutter#fold#toggle() 213 | 214 | " }}} 215 | 216 | command! -bar GitGutterDebug call gitgutter#debug#debug() 217 | 218 | " Maps {{{ 219 | 220 | nnoremap (GitGutterNextHunk) &diff ? ']c' : ":\execute v:count1 . 'GitGutterNextHunk'\" 221 | nnoremap GitGutterNextHunk &diff ? ']c' : ":\call gitgutter#utility#warn('Please change your map \Plug>GitGutterNextHunk to \Plug>(GitGutterNextHunk)')\" 222 | nnoremap (GitGutterPrevHunk) &diff ? '[c' : ":\execute v:count1 . 'GitGutterPrevHunk'\" 223 | nnoremap GitGutterPrevHunk &diff ? '[c' : ":\call gitgutter#utility#warn('Please change your map \Plug>GitGutterPrevHunk to \Plug>(GitGutterPrevHunk)')\" 224 | 225 | xnoremap (GitGutterStageHunk) :GitGutterStageHunk 226 | xnoremap GitGutterStageHunk :call gitgutter#utility#warn('Please change your map Plug>GitGutterStageHunk to Plug>(GitGutterStageHunk)') 227 | nnoremap (GitGutterStageHunk) :GitGutterStageHunk 228 | nnoremap GitGutterStageHunk :call gitgutter#utility#warn('Please change your map Plug>GitGutterStageHunk to Plug>(GitGutterStageHunk)') 229 | nnoremap (GitGutterUndoHunk) :GitGutterUndoHunk 230 | nnoremap GitGutterUndoHunk :call gitgutter#utility#warn('Please change your map Plug>GitGutterUndoHunk to Plug>(GitGutterUndoHunk)') 231 | nnoremap (GitGutterPreviewHunk) :GitGutterPreviewHunk 232 | nnoremap GitGutterPreviewHunk :call gitgutter#utility#warn('Please change your map Plug>GitGutterPreviewHunk to Plug>(GitGutterPreviewHunk)') 233 | 234 | " }}} 235 | 236 | function! s:on_bufenter() 237 | call gitgutter#setup_maps() 238 | 239 | " To keep vim's start-up fast, do not process the buffer when vim is starting. 240 | " Instead process it a short time later. Normally we would rely on our 241 | " CursorHold autocommand to handle this but it turns out CursorHold is not 242 | " guaranteed to fire if the user has not typed anything yet; so set up a 243 | " timer instead. The disadvantage is that if CursorHold does fire, the 244 | " plugin will do a round of unnecessary work; but since there will not have 245 | " been any changes to the buffer since the first round, the second round 246 | " will be cheap. 247 | if has('vim_starting') && !$VIM_GITGUTTER_TEST 248 | if exists('*timer_start') && has('lambda') 249 | call s:next_tick("call gitgutter#process_buffer(+".bufnr('').", 0)") 250 | else 251 | call gitgutter#process_buffer(bufnr(''), 0) 252 | endif 253 | return 254 | endif 255 | 256 | if exists('t:gitgutter_didtabenter') && t:gitgutter_didtabenter 257 | let t:gitgutter_didtabenter = 0 258 | call gitgutter#all(!g:gitgutter_terminal_reports_focus) 259 | else 260 | call gitgutter#process_buffer(bufnr(''), !g:gitgutter_terminal_reports_focus) 261 | endif 262 | endfunction 263 | 264 | function! s:next_tick(cmd) 265 | call timer_start(1, {-> execute(a:cmd)}) 266 | endfunction 267 | 268 | function! s:on_buffilepre(bufnr) 269 | if !exists('s:renaming') 270 | let s:renaming = [] 271 | let s:gitgutter_was_enabled = gitgutter#utility#getbufvar(a:bufnr, 'enabled') 272 | endif 273 | 274 | let s:renaming += [a:bufnr] 275 | endfunction 276 | 277 | function! s:on_buffilepost(bufnr) 278 | if len(s:renaming) > 1 279 | if s:renaming[0] != a:bufnr 280 | throw 'gitgutter rename error' s:renaming[0] a:bufnr 281 | endif 282 | unlet s:renaming[0] 283 | return 284 | endif 285 | 286 | " reset cached values 287 | GitGutterBufferDisable 288 | 289 | if s:gitgutter_was_enabled 290 | GitGutterBufferEnable 291 | endif 292 | 293 | unlet s:renaming 294 | unlet s:gitgutter_was_enabled 295 | endfunction 296 | 297 | " Autocommands {{{ 298 | 299 | augroup gitgutter 300 | autocmd! 301 | 302 | autocmd TabEnter * let t:gitgutter_didtabenter = 1 303 | 304 | autocmd BufEnter * call s:on_bufenter() 305 | 306 | " Ensure Vim is always checking for CursorMoved to avoid CursorMoved 307 | " being fired at the wrong time in floating preview window on Neovim. 308 | " See vim/vim#2053. 309 | autocmd CursorMoved * execute '' 310 | 311 | autocmd CursorHold,CursorHoldI * call gitgutter#process_buffer(bufnr(''), 0) 312 | if exists('*timer_start') && has('lambda') 313 | autocmd FileChangedShellPost * call s:next_tick("call gitgutter#process_buffer(+".expand('').", 1)") 314 | else 315 | autocmd FileChangedShellPost * call gitgutter#process_buffer(+expand(''), 1) 316 | endif 317 | 318 | " Ensure that all buffers are processed when opening vim with multiple files, e.g.: 319 | " 320 | " vim -o file1 file2 321 | autocmd VimEnter * if winnr() != winnr('$') | call gitgutter#all(0) | endif 322 | 323 | autocmd ShellCmdPost * call gitgutter#all(1) 324 | autocmd BufLeave term://* call gitgutter#all(1) 325 | 326 | autocmd User FugitiveChanged call gitgutter#all(1) 327 | 328 | " Handle all buffers when focus is gained, but only after it was lost. 329 | " FocusGained gets triggered on startup with Neovim at least already. 330 | " Therefore this tracks also if it was lost before. 331 | let s:focus_was_lost = 0 332 | autocmd FocusGained * if s:focus_was_lost | let s:focus_was_lost = 0 | call gitgutter#all(1) | endif 333 | autocmd FocusLost * let s:focus_was_lost = 1 334 | 335 | if exists('##VimResume') 336 | autocmd VimResume * call gitgutter#all(1) 337 | endif 338 | 339 | autocmd ColorScheme * call gitgutter#highlight#define_highlights() 340 | 341 | autocmd BufFilePre * call s:on_buffilepre(expand('')) 342 | autocmd BufFilePost * call s:on_buffilepost(expand('')) 343 | 344 | autocmd QuickFixCmdPre *vimgrep* 345 | \ if gitgutter#utility#getbufvar(expand(''), 'enabled') | 346 | \ let s:gitgutter_was_enabled = expand('') | 347 | \ else | 348 | \ let s:gitgutter_was_enabled = 0 | 349 | \ endif | 350 | \ GitGutterBufferDisable 351 | autocmd QuickFixCmdPost *vimgrep* 352 | \ if s:gitgutter_was_enabled | 353 | \ call gitgutter#buffer_enable(s:gitgutter_was_enabled) | 354 | \ endif | 355 | \ unlet s:gitgutter_was_enabled 356 | augroup END 357 | 358 | " }}} 359 | 360 | " vim:set et sw=2 fdm=marker: 361 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airblade/vim-gitgutter/85ca3a087204e3a32cb2faa5d9d0451524e08720/screenshot.png -------------------------------------------------------------------------------- /test/.gitattributes: -------------------------------------------------------------------------------- 1 | *.foo filter=reverse diff=reverse 2 | -------------------------------------------------------------------------------- /test/.gitconfig: -------------------------------------------------------------------------------- 1 | [filter "reverse"] 2 | clean = "rev" 3 | smudge = "rev" 4 | 5 | [diff "reverse"] 6 | textconv = "cat" 7 | -------------------------------------------------------------------------------- /test/cp932.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airblade/vim-gitgutter/85ca3a087204e3a32cb2faa5d9d0451524e08720/test/cp932.txt -------------------------------------------------------------------------------- /test/fixture.foo: -------------------------------------------------------------------------------- 1 | one 2 | two 3 | three 4 | four 5 | -------------------------------------------------------------------------------- /test/fixture.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d 5 | e 6 | f 7 | g 8 | h 9 | i 10 | j 11 | 12 | -------------------------------------------------------------------------------- /test/fixture_dos.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d 5 | e 6 | f 7 | g 8 | h 9 | i 10 | j 11 | 12 | -------------------------------------------------------------------------------- /test/fixture_dos_noeol.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d 5 | e 6 | f 7 | g -------------------------------------------------------------------------------- /test/runner.vim: -------------------------------------------------------------------------------- 1 | " 2 | " Adapted from https://github.com/vim/vim/blob/master/src/testdir/runtest.vim 3 | " 4 | " When debugging tests it can help to write debug output: 5 | " call Log('oh noes') 6 | " 7 | 8 | function RunTest(test) 9 | if exists("*SetUp") 10 | call SetUp() 11 | endif 12 | 13 | try 14 | execute 'call '.a:test 15 | catch 16 | call Exception() 17 | let s:errored = 1 18 | endtry 19 | 20 | if exists("*TearDown") 21 | call TearDown() 22 | endif 23 | endfunction 24 | 25 | function Log(msg) 26 | if type(a:msg) == type('') 27 | call add(s:messages, a:msg) 28 | elseif type(a:msg) == type([]) 29 | call extend(s:messages, a:msg) 30 | else 31 | call add(v:errors, 'Exception: unsupported type: '.type(a:msg)) 32 | endif 33 | endfunction 34 | 35 | function Exception() 36 | call add(v:errors, v:throwpoint.'..'.'Exception: '.v:exception) 37 | endfunction 38 | 39 | " Shuffles list in place. 40 | function Shuffle(list) 41 | " Fisher-Yates-Durstenfeld-Knuth 42 | let n = len(a:list) 43 | if n < 2 44 | return a:list 45 | endif 46 | for i in range(0, n-2) 47 | let j = Random(0, n-i-1) 48 | let e = a:list[i] 49 | let a:list[i] = a:list[i+j] 50 | let a:list[i+j] = e 51 | endfor 52 | return a:list 53 | endfunction 54 | 55 | " Returns a pseudorandom integer i such that 0 <= i <= max 56 | function Random(min, max) 57 | if has('unix') 58 | let i = system('echo $RANDOM') " 0 <= i <= 32767 59 | else 60 | let i = system('echo %RANDOM%') " 0 <= i <= 32767 61 | endif 62 | return i * (a:max - a:min + 1) / 32768 + a:min 63 | endfunction 64 | 65 | function FriendlyName(test_name) 66 | return substitute(a:test_name[5:-3], '_', ' ', 'g') 67 | endfunction 68 | 69 | function Align(left, right) 70 | if type(a:right) == type([]) 71 | let result = [] 72 | for s in a:right 73 | if empty(result) 74 | call add(result, printf('%-'.s:indent.'S', a:left).s) 75 | else 76 | call add(result, printf('%-'.s:indent.'S', '').s) 77 | endif 78 | endfor 79 | return result 80 | endif 81 | 82 | return printf('%-'.s:indent.'S', a:left).a:right 83 | endfunction 84 | 85 | let g:testname = expand('%') 86 | let s:errored = 0 87 | let s:done = 0 88 | let s:fail = 0 89 | let s:errors = 0 90 | let s:messages = [] 91 | let s:indent = '' 92 | 93 | call Log(g:testname.':') 94 | 95 | " Source the test script. 96 | try 97 | source % 98 | catch 99 | let s:errors += 1 100 | call Exception() 101 | endtry 102 | 103 | " Locate the test functions. 104 | set nomore 105 | redir @q 106 | silent function /^Test_ 107 | redir END 108 | let s:tests = split(substitute(@q, 'function \(\k*()\)', '\1', 'g')) 109 | 110 | " If there is another argument, filter test-functions' names against it. 111 | if argc() > 1 112 | let s:tests = filter(s:tests, 'v:val =~ argv(1)') 113 | endif 114 | 115 | let s:indent = max(map(copy(s:tests), {_, val -> len(FriendlyName(val))})) 116 | 117 | " Run the tests in random order. 118 | for test in Shuffle(s:tests) 119 | call RunTest(test) 120 | let s:done += 1 121 | 122 | let friendly_name = FriendlyName(test) 123 | if len(v:errors) == 0 124 | call Log(Align(friendly_name, ' - ok')) 125 | else 126 | if s:errored 127 | let s:errors += 1 128 | let s:errored = 0 129 | else 130 | let s:fail += 1 131 | endif 132 | call Log(Align(friendly_name, ' - not ok')) 133 | 134 | let i = 0 135 | for error in v:errors 136 | if i != 0 137 | call Log(Align('',' ! ----')) 138 | endif 139 | for trace in reverse(split(error, '\.\.')) 140 | call Log(Align('', ' ! '.trace)) 141 | endfor 142 | let i += 1 143 | endfor 144 | 145 | let v:errors = [] 146 | endif 147 | endfor 148 | 149 | let summary = [ 150 | \ s:done.( s:done == 1 ? ' test' : ' tests'), 151 | \ s:errors.(s:errors == 1 ? ' error' : ' errors'), 152 | \ s:fail.( s:fail == 1 ? ' failure' : ' failures'), 153 | \ ] 154 | call Log('') 155 | call Log(join(summary, ', ')) 156 | 157 | split messages.log 158 | call append(line('$'), s:messages) 159 | write 160 | 161 | qall! 162 | 163 | -------------------------------------------------------------------------------- /test/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VIM="/Applications/MacVim.app/Contents/MacOS/Vim -v" 4 | 5 | export VIM_GITGUTTER_TEST=1 6 | 7 | $VIM -u NONE -U NONE -N \ 8 | --cmd 'set rtp+=../' \ 9 | --cmd 'let g:gitgutter_async=0' \ 10 | --cmd 'source ../plugin/gitgutter.vim' \ 11 | -S runner.vim \ 12 | test_*.vim \ 13 | "$@" 14 | 15 | cat messages.log 16 | 17 | grep -q "0 errors, 0 failures" messages.log 18 | status=$? 19 | rm messages.log 20 | exit $status 21 | 22 | -------------------------------------------------------------------------------- /test/test_gitgutter.vim: -------------------------------------------------------------------------------- 1 | let s:current_dir = expand('%:p:h') 2 | let s:test_repo = s:current_dir.'/test-repo' 3 | let s:bufnr = bufnr('') 4 | 5 | " 6 | " Helpers 7 | " 8 | 9 | " Ignores unexpected keys in actual. 10 | function s:assert_list_of_dicts(expected, actual) 11 | if empty(a:expected) 12 | call assert_equal([], a:actual) 13 | return 14 | endif 15 | 16 | let expected_keys = keys(a:expected[0]) 17 | 18 | for dict in a:actual 19 | for k in keys(dict) 20 | if index(expected_keys, k) == -1 21 | call remove(dict, k) 22 | endif 23 | endfor 24 | endfor 25 | 26 | call assert_equal(a:expected, a:actual) 27 | endfunction 28 | 29 | " Ignores unexpected keys. 30 | " 31 | " expected - list of signs 32 | function s:assert_signs(expected, filename) 33 | let actual = sign_getplaced(a:filename, {'group': 'gitgutter'})[0].signs 34 | call s:assert_list_of_dicts(a:expected, actual) 35 | endfunction 36 | 37 | function s:git_diff(...) 38 | return split(system('git diff -U0 '.(a:0 ? a:1 : 'fixture.txt')), '\n') 39 | endfunction 40 | 41 | function s:git_diff_staged(...) 42 | return split(system('git diff -U0 --staged '.(a:0 ? a:1 : 'fixture.txt')), '\n') 43 | endfunction 44 | 45 | function s:trigger_gitgutter() 46 | doautocmd CursorHold 47 | endfunction 48 | 49 | 50 | " 51 | " SetUp / TearDown 52 | " 53 | 54 | function SetUp() 55 | let g:gitgutter_diff_base = '' 56 | call system("git init ".s:test_repo. 57 | \ " && cd ".s:test_repo. 58 | \ " && cp ../.gitconfig .". 59 | \ " && cp ../.gitattributes .". 60 | \ " && cp ../fixture.foo .". 61 | \ " && cp ../fixture.txt .". 62 | \ " && cp ../fixture_dos.txt .". 63 | \ " && cp ../fixture_dos_noeol.txt .". 64 | \ " && git add . && git commit -m 'initial'". 65 | \ " && git config diff.mnemonicPrefix false") 66 | execute ':cd' s:test_repo 67 | edit! fixture.txt 68 | call gitgutter#sign#reset() 69 | 70 | " FIXME why won't vim autoload the file? 71 | execute 'source' '../../autoload/gitgutter/diff_highlight.vim' 72 | execute 'source' '../../autoload/gitgutter/fold.vim' 73 | endfunction 74 | 75 | function TearDown() 76 | " delete all buffers except this one 77 | " TODO: move to runner.vim, accounting for multiple test files 78 | if s:bufnr > 1 79 | silent! execute '1,'.s:bufnr-1.'bdelete!' 80 | endif 81 | silent! execute s:bufnr+1.',$bdelete!' 82 | 83 | execute ':cd' s:current_dir 84 | call system("rm -rf ".s:test_repo) 85 | endfunction 86 | 87 | " 88 | " The tests 89 | " 90 | 91 | function Test_add_lines() 92 | normal ggo* 93 | call s:trigger_gitgutter() 94 | 95 | let expected = [{'lnum': 2, 'name': 'GitGutterLineAdded', 'group': 'gitgutter', 'priority': 10}] 96 | call s:assert_signs(expected, 'fixture.txt') 97 | endfunction 98 | 99 | 100 | function Test_add_lines_fish() 101 | let _shell = &shell 102 | set shell=/usr/local/bin/fish 103 | 104 | normal ggo* 105 | call s:trigger_gitgutter() 106 | 107 | let expected = [{'lnum': 2, 'name': 'GitGutterLineAdded'}] 108 | call s:assert_signs(expected, 'fixture.txt') 109 | 110 | let &shell = _shell 111 | endfunction 112 | 113 | 114 | function Test_modify_lines() 115 | normal ggi* 116 | call s:trigger_gitgutter() 117 | 118 | let expected = [{'lnum': 1, 'name': 'GitGutterLineModified'}] 119 | call s:assert_signs(expected, 'fixture.txt') 120 | endfunction 121 | 122 | 123 | function Test_remove_lines() 124 | execute '5d' 125 | call s:trigger_gitgutter() 126 | 127 | let expected = [{'lnum': 4, 'name': 'GitGutterLineRemoved'}] 128 | call s:assert_signs(expected, 'fixture.txt') 129 | endfunction 130 | 131 | 132 | function Test_remove_first_lines() 133 | execute '1d' 134 | call s:trigger_gitgutter() 135 | 136 | let expected = [{'lnum': 1, 'name': 'GitGutterLineRemovedFirstLine'}] 137 | call s:assert_signs(expected, 'fixture.txt') 138 | endfunction 139 | 140 | 141 | function Test_priority() 142 | let g:gitgutter_sign_priority = 5 143 | 144 | execute '1d' 145 | call s:trigger_gitgutter() 146 | 147 | call s:assert_signs([{'priority': 5}], 'fixture.txt') 148 | 149 | let g:gitgutter_sign_priority = 10 150 | endfunction 151 | 152 | 153 | function Test_overlapping_hunks() 154 | execute '3d' 155 | execute '1d' 156 | call s:trigger_gitgutter() 157 | 158 | let expected = [{'lnum': 1, 'name': 'GitGutterLineRemovedAboveAndBelow'}] 159 | call s:assert_signs(expected, 'fixture.txt') 160 | endfunction 161 | 162 | 163 | function Test_edit_file_with_same_name_as_a_branch() 164 | normal 5Gi* 165 | call system('git checkout -b fixture.txt') 166 | call s:trigger_gitgutter() 167 | 168 | let expected = [{'lnum': 5, 'name': 'GitGutterLineModified'}] 169 | call s:assert_signs(expected, 'fixture.txt') 170 | endfunction 171 | 172 | 173 | function Test_file_added_to_git() 174 | let tmpfile = 'fileAddedToGit.tmp' 175 | call system('touch '.tmpfile.' && git add '.tmpfile) 176 | execute 'edit '.tmpfile 177 | normal ihello 178 | call s:trigger_gitgutter() 179 | 180 | let expected = [{'lnum': 1, 'name': 'GitGutterLineAdded'}] 181 | call s:assert_signs(expected, 'fileAddedToGit.tmp') 182 | endfunction 183 | 184 | 185 | function Test_filename_with_equals() 186 | call system('touch =fixture=.txt && git add =fixture=.txt') 187 | edit =fixture=.txt 188 | normal ggo* 189 | call s:trigger_gitgutter() 190 | 191 | let expected = [ 192 | \ {'lnum': 1, 'name': 'GitGutterLineAdded'}, 193 | \ {'lnum': 2, 'name': 'GitGutterLineAdded'} 194 | \ ] 195 | call s:assert_signs(expected, '=fixture=.txt') 196 | endfunction 197 | 198 | 199 | function Test_filename_with_colon() 200 | call system('touch fix:ture.txt && git add fix:ture.txt') 201 | edit fix:ture.txt 202 | normal ggo* 203 | call s:trigger_gitgutter() 204 | 205 | let expected = [ 206 | \ {'lnum': 1, 'name': 'GitGutterLineAdded'}, 207 | \ {'lnum': 2, 'name': 'GitGutterLineAdded'} 208 | \ ] 209 | call s:assert_signs(expected, 'fix:ture.txt') 210 | endfunction 211 | 212 | 213 | function Test_filename_with_square_brackets() 214 | call system('touch fix[tu]re.txt && git add fix[tu]re.txt') 215 | edit fix[tu]re.txt 216 | normal ggo* 217 | call s:trigger_gitgutter() 218 | 219 | let expected = [ 220 | \ {'lnum': 1, 'name': 'GitGutterLineAdded'}, 221 | \ {'lnum': 2, 'name': 'GitGutterLineAdded'} 222 | \ ] 223 | call s:assert_signs(expected, 'fix[tu]re.txt') 224 | endfunction 225 | 226 | 227 | function Test_filename_with_space() 228 | call system('touch fix\ ture.txt && git add fix\ ture.txt') 229 | edit fix\ ture.txt 230 | normal ggo* 231 | call s:trigger_gitgutter() 232 | 233 | let expected = [ 234 | \ {'lnum': 1, 'name': 'GitGutterLineAdded'}, 235 | \ {'lnum': 2, 'name': 'GitGutterLineAdded'} 236 | \ ] 237 | call s:assert_signs(expected, 'fix\ ture.txt') 238 | endfunction 239 | 240 | 241 | function Test_filename_leading_dash() 242 | call system('touch -- -fixture.txt && git add -- -fixture.txt') 243 | edit -fixture.txt 244 | normal ggo* 245 | call s:trigger_gitgutter() 246 | 247 | let expected = [ 248 | \ {'lnum': 1, 'name': 'GitGutterLineAdded'}, 249 | \ {'lnum': 2, 'name': 'GitGutterLineAdded'} 250 | \ ] 251 | call s:assert_signs(expected, '-fixture.txt') 252 | endfunction 253 | 254 | 255 | function Test_filename_umlaut() 256 | call system('touch -- fixtüre.txt && git add -- fixtüre.txt') 257 | edit fixtüre.txt 258 | normal ggo* 259 | call s:trigger_gitgutter() 260 | 261 | let expected = [ 262 | \ {'lnum': 1, 'name': 'GitGutterLineAdded'}, 263 | \ {'lnum': 2, 'name': 'GitGutterLineAdded'} 264 | \ ] 265 | call s:assert_signs(expected, 'fixtüre.txt') 266 | endfunction 267 | 268 | 269 | function Test_file_cmd() 270 | normal ggo* 271 | 272 | file other.txt 273 | 274 | call s:trigger_gitgutter() 275 | call assert_equal(1, b:gitgutter.enabled) 276 | call assert_equal('', b:gitgutter.path) 277 | call s:assert_signs([], 'other.txt') 278 | 279 | write 280 | 281 | call s:trigger_gitgutter() 282 | call assert_equal(-2, b:gitgutter.path) 283 | endfunction 284 | 285 | 286 | function Test_saveas() 287 | normal ggo* 288 | 289 | saveas other.txt 290 | 291 | call s:trigger_gitgutter() 292 | call assert_equal(1, b:gitgutter.enabled) 293 | call assert_equal(-2, b:gitgutter.path) 294 | call s:assert_signs([], 'other.txt') 295 | endfunction 296 | 297 | 298 | function Test_file_mv() 299 | call system('git mv fixture.txt fixture_moved.txt') 300 | edit fixture_moved.txt 301 | normal ggo* 302 | call s:trigger_gitgutter() 303 | let expected = [{'lnum': 2, 'name': 'GitGutterLineAdded'}] 304 | call s:assert_signs(expected, 'fixture_moved.txt') 305 | 306 | write 307 | call system('git add fixture_moved.txt && git commit -m "moved and edited"') 308 | GitGutterDisable 309 | GitGutterEnable 310 | let expected = [] 311 | call s:assert_signs(expected, 'fixture_moved.txt') 312 | 313 | GitGutterDisable 314 | let g:gitgutter_diff_base = 'HEAD^' 315 | GitGutterEnable 316 | let expected = [{'lnum': 2, 'name': 'GitGutterLineAdded'}] 317 | call s:assert_signs(expected, 'fixture_moved.txt') 318 | endfunction 319 | 320 | 321 | " FIXME: this test fails when it is the first (or only) test to be run 322 | function Test_follow_symlink() 323 | let tmp = 'symlink' 324 | call system('ln -nfs fixture.txt '.tmp) 325 | execute 'edit '.tmp 326 | 6d 327 | call s:trigger_gitgutter() 328 | 329 | let expected = [{'lnum': 5, 'name': 'GitGutterLineRemoved'}] 330 | call s:assert_signs(expected, 'symlink') 331 | endfunction 332 | 333 | 334 | function Test_keep_alt() 335 | enew 336 | execute "normal! \" 337 | 338 | call assert_equal('fixture.txt', bufname('')) 339 | call assert_equal('', bufname('#')) 340 | 341 | normal ggx 342 | call s:trigger_gitgutter() 343 | 344 | call assert_equal('', bufname('#')) 345 | endfunction 346 | 347 | 348 | function Test_keep_modified() 349 | normal 5Go* 350 | call assert_equal(1, getbufvar('', '&modified')) 351 | 352 | call s:trigger_gitgutter() 353 | 354 | call assert_equal(1, getbufvar('', '&modified')) 355 | endfunction 356 | 357 | 358 | function Test_keep_op_marks() 359 | normal 5Go* 360 | call assert_equal([0,6,1,0], getpos("'[")) 361 | call assert_equal([0,6,2,0], getpos("']")) 362 | 363 | call s:trigger_gitgutter() 364 | 365 | call assert_equal([0,6,1,0], getpos("'[")) 366 | call assert_equal([0,6,2,0], getpos("']")) 367 | endfunction 368 | 369 | 370 | function Test_no_modifications() 371 | call s:assert_signs([], 'fixture.txt') 372 | endfunction 373 | 374 | 375 | function Test_orphaned_signs() 376 | execute "normal 5GoX\Y" 377 | call s:trigger_gitgutter() 378 | 6d 379 | call s:trigger_gitgutter() 380 | 381 | let expected = [{'lnum': 6, 'name': 'GitGutterLineAdded'}] 382 | call s:assert_signs(expected, 'fixture.txt') 383 | endfunction 384 | 385 | 386 | function Test_untracked_file_outside_repo() 387 | let tmp = tempname() 388 | call system('touch '.tmp) 389 | execute 'edit '.tmp 390 | 391 | call s:assert_signs([], tmp) 392 | endfunction 393 | 394 | 395 | function Test_untracked_file_within_repo() 396 | let tmp = 'untrackedFileWithinRepo.tmp' 397 | call system('touch '.tmp) 398 | execute 'edit '.tmp 399 | normal ggo* 400 | call s:trigger_gitgutter() 401 | 402 | call s:assert_signs([], tmp) 403 | call assert_equal(-2, b:gitgutter.path) 404 | 405 | call system('rm '.tmp) 406 | endfunction 407 | 408 | 409 | function Test_untracked_file_square_brackets_within_repo() 410 | let tmp = '[un]trackedFileWithinRepo.tmp' 411 | call system('touch '.tmp) 412 | execute 'edit '.tmp 413 | normal ggo* 414 | call s:trigger_gitgutter() 415 | 416 | call s:assert_signs([], tmp) 417 | 418 | call system('rm '.tmp) 419 | endfunction 420 | 421 | 422 | function Test_file_unknown_in_base() 423 | let starting_branch = split(system('git branch --show-current'))[0] 424 | call system('git checkout -b some-feature') 425 | let tmp = 'file-on-this-branch-only.tmp' 426 | call system('echo "hi" > '.tmp.' && git add '.tmp) 427 | execute 'edit '.tmp 428 | let g:gitgutter_diff_base = starting_branch 429 | GitGutter 430 | let expected = [{'lnum': 1, 'name': 'GitGutterLineAdded', 'group': 'gitgutter', 'priority': 10}] 431 | call s:assert_signs(expected, tmp) 432 | let g:gitgutter_diff_base = '' 433 | endfunction 434 | 435 | 436 | function Test_v_shell_error_not_clobbered() 437 | " set gitgutter up to generate a shell error 438 | let starting_branch = split(system('git branch --show-current'))[0] 439 | call system('git checkout -b some-feature') 440 | let tmp = 'file-on-this-branch-only.tmp' 441 | call system('echo "hi" > '.tmp.' && git add '.tmp) 442 | execute 'edit '.tmp 443 | let g:gitgutter_diff_base = starting_branch 444 | 445 | " run a successful shell command 446 | silent !echo foobar >/dev/null 447 | 448 | " run gitgutter 449 | GitGutter 450 | 451 | call assert_equal(0, v:shell_error) 452 | 453 | let g:gitgutter_diff_base = '' 454 | endfunction 455 | 456 | 457 | function Test_hunk_outside_noop() 458 | 5 459 | GitGutterStageHunk 460 | 461 | call s:assert_signs([], 'fixture.txt') 462 | call assert_equal([], s:git_diff()) 463 | call assert_equal([], s:git_diff_staged()) 464 | 465 | GitGutterUndoHunk 466 | 467 | call s:assert_signs([], 'fixture.txt') 468 | call assert_equal([], s:git_diff()) 469 | call assert_equal([], s:git_diff_staged()) 470 | endfunction 471 | 472 | 473 | function Test_preview() 474 | normal 5Gi* 475 | GitGutterPreviewHunk 476 | 477 | wincmd P 478 | call assert_equal(2, line('$')) 479 | call assert_equal('-e', getline(1)) 480 | call assert_equal('+*e', getline(2)) 481 | wincmd p 482 | endfunction 483 | 484 | 485 | function Test_preview_dos() 486 | edit! fixture_dos.txt 487 | 488 | normal 5Gi* 489 | GitGutterPreviewHunk 490 | 491 | wincmd P 492 | call assert_equal(2, line('$')) 493 | call assert_equal('-e', getline(1)) 494 | call assert_equal('+*e', getline(2)) 495 | wincmd p 496 | endfunction 497 | 498 | 499 | function Test_dos_noeol() 500 | edit! fixture_dos_noeol.txt 501 | GitGutter 502 | 503 | call s:assert_signs([], 'fixture_dos_noeol.txt') 504 | endfunction 505 | 506 | 507 | function Test_hunk_stage() 508 | let _shell = &shell 509 | set shell=foo 510 | 511 | normal 5Gi* 512 | GitGutterStageHunk 513 | 514 | call assert_equal('foo', &shell) 515 | let &shell = _shell 516 | 517 | call s:assert_signs([], 'fixture.txt') 518 | 519 | " Buffer is unsaved 520 | let expected = [ 521 | \ 'diff --git a/fixture.txt b/fixture.txt', 522 | \ 'index ae8e546..f5c6aff 100644', 523 | \ '--- a/fixture.txt', 524 | \ '+++ b/fixture.txt', 525 | \ '@@ -5 +5 @@ d', 526 | \ '-*e', 527 | \ '+e' 528 | \ ] 529 | call assert_equal(expected, s:git_diff()) 530 | 531 | " Index has been updated 532 | let expected = [ 533 | \ 'diff --git a/fixture.txt b/fixture.txt', 534 | \ 'index f5c6aff..ae8e546 100644', 535 | \ '--- a/fixture.txt', 536 | \ '+++ b/fixture.txt', 537 | \ '@@ -5 +5 @@ d', 538 | \ '-e', 539 | \ '+*e' 540 | \ ] 541 | call assert_equal(expected, s:git_diff_staged()) 542 | 543 | " Save the buffer 544 | write 545 | 546 | call assert_equal([], s:git_diff()) 547 | endfunction 548 | 549 | 550 | function Test_hunk_stage_nearby_hunk() 551 | execute "normal! 2Gox\y\z" 552 | normal 2jdd 553 | normal k 554 | GitGutterStageHunk 555 | 556 | let expected = [ 557 | \ {'lnum': 3, 'name': 'GitGutterLineAdded'}, 558 | \ {'lnum': 4, 'name': 'GitGutterLineAdded'}, 559 | \ {'lnum': 5, 'name': 'GitGutterLineAdded'} 560 | \ ] 561 | call s:assert_signs(expected, 'fixture.txt') 562 | 563 | " Buffer is unsaved 564 | let expected = [ 565 | \ 'diff --git a/fixture.txt b/fixture.txt', 566 | \ 'index 53b13df..f5c6aff 100644', 567 | \ '--- a/fixture.txt', 568 | \ '+++ b/fixture.txt', 569 | \ '@@ -3,0 +4 @@ c', 570 | \ '+d', 571 | \ ] 572 | call assert_equal(expected, s:git_diff()) 573 | 574 | " Index has been updated 575 | let expected = [ 576 | \ 'diff --git a/fixture.txt b/fixture.txt', 577 | \ 'index f5c6aff..53b13df 100644', 578 | \ '--- a/fixture.txt', 579 | \ '+++ b/fixture.txt', 580 | \ '@@ -4 +3,0 @@ c', 581 | \ '-d', 582 | \ ] 583 | call assert_equal(expected, s:git_diff_staged()) 584 | 585 | " Save the buffer 586 | write 587 | 588 | let expected = [ 589 | \ 'diff --git a/fixture.txt b/fixture.txt', 590 | \ 'index 53b13df..8fdfda7 100644', 591 | \ '--- a/fixture.txt', 592 | \ '+++ b/fixture.txt', 593 | \ '@@ -2,0 +3,3 @@ b', 594 | \ '+x', 595 | \ '+y', 596 | \ '+z', 597 | \ ] 598 | call assert_equal(expected, s:git_diff()) 599 | endfunction 600 | 601 | 602 | function Test_hunk_stage_partial_visual_added() 603 | call append(5, ['A','B','C','D']) 604 | execute "normal 7GVj:GitGutterStageHunk\" 605 | 606 | let expected = [ 607 | \ {'lnum': 6, 'name': 'GitGutterLineAdded'}, 608 | \ {'lnum': 9, 'name': 'GitGutterLineAdded'}, 609 | \ ] 610 | call s:assert_signs(expected, 'fixture.txt') 611 | 612 | let expected = [ 613 | \ 'diff --git a/fixture.txt b/fixture.txt', 614 | \ 'index 8a7026e..f5c6aff 100644', 615 | \ '--- a/fixture.txt', 616 | \ '+++ b/fixture.txt', 617 | \ '@@ -6,2 +5,0 @@ e', 618 | \ '-B', 619 | \ '-C', 620 | \ ] 621 | call assert_equal(expected, s:git_diff()) 622 | 623 | let expected = [ 624 | \ 'diff --git a/fixture.txt b/fixture.txt', 625 | \ 'index f5c6aff..8a7026e 100644', 626 | \ '--- a/fixture.txt', 627 | \ '+++ b/fixture.txt', 628 | \ '@@ -5,0 +6,2 @@ e', 629 | \ '+B', 630 | \ '+C', 631 | \ ] 632 | call assert_equal(expected, s:git_diff_staged()) 633 | endfunction 634 | 635 | 636 | function Test_hunk_stage_partial_cmd_added() 637 | call append(5, ['A','B','C','D']) 638 | 6 639 | 7,8GitGutterStageHunk 640 | 641 | let expected = [ 642 | \ {'lnum': 6, 'name': 'GitGutterLineAdded'}, 643 | \ {'lnum': 9, 'name': 'GitGutterLineAdded'}, 644 | \ ] 645 | call s:assert_signs(expected, 'fixture.txt') 646 | 647 | let expected = [ 648 | \ 'diff --git a/fixture.txt b/fixture.txt', 649 | \ 'index 8a7026e..f5c6aff 100644', 650 | \ '--- a/fixture.txt', 651 | \ '+++ b/fixture.txt', 652 | \ '@@ -6,2 +5,0 @@ e', 653 | \ '-B', 654 | \ '-C', 655 | \ ] 656 | call assert_equal(expected, s:git_diff()) 657 | 658 | let expected = [ 659 | \ 'diff --git a/fixture.txt b/fixture.txt', 660 | \ 'index f5c6aff..8a7026e 100644', 661 | \ '--- a/fixture.txt', 662 | \ '+++ b/fixture.txt', 663 | \ '@@ -5,0 +6,2 @@ e', 664 | \ '+B', 665 | \ '+C', 666 | \ ] 667 | call assert_equal(expected, s:git_diff_staged()) 668 | endfunction 669 | 670 | 671 | function Test_hunk_stage_partial_preview_added() 672 | call append(5, ['A','B','C','D']) 673 | 6 674 | GitGutterPreviewHunk 675 | wincmd P 676 | 677 | " remove C and A so we stage B and D 678 | 3delete 679 | 1delete 680 | 681 | GitGutterStageHunk 682 | write 683 | 684 | let expected = [ 685 | \ {'lnum': 6, 'name': 'GitGutterLineAdded'}, 686 | \ {'lnum': 8, 'name': 'GitGutterLineAdded'}, 687 | \ ] 688 | call s:assert_signs(expected, 'fixture.txt') 689 | 690 | let expected = [ 691 | \ 'diff --git a/fixture.txt b/fixture.txt', 692 | \ 'index 975852f..3dd23a3 100644', 693 | \ '--- a/fixture.txt', 694 | \ '+++ b/fixture.txt', 695 | \ '@@ -5,0 +6 @@ e', 696 | \ '+A', 697 | \ '@@ -6,0 +8 @@ B', 698 | \ '+C', 699 | \ ] 700 | call assert_equal(expected, s:git_diff()) 701 | 702 | let expected = [ 703 | \ 'diff --git a/fixture.txt b/fixture.txt', 704 | \ 'index f5c6aff..975852f 100644', 705 | \ '--- a/fixture.txt', 706 | \ '+++ b/fixture.txt', 707 | \ '@@ -5,0 +6,2 @@ e', 708 | \ '+B', 709 | \ '+D', 710 | \ ] 711 | call assert_equal(expected, s:git_diff_staged()) 712 | endfunction 713 | 714 | 715 | function Test_hunk_stage_preview_write() 716 | call append(5, ['A','B','C','D']) 717 | 6 718 | GitGutterPreviewHunk 719 | wincmd P 720 | 721 | " preview window 722 | call feedkeys(":w\", 'tx') 723 | " original window 724 | write 725 | 726 | call s:assert_signs([], 'fixture.txt') 727 | 728 | call assert_equal([], s:git_diff()) 729 | 730 | let expected = [ 731 | \ 'diff --git a/fixture.txt b/fixture.txt', 732 | \ 'index f5c6aff..3dd23a3 100644', 733 | \ '--- a/fixture.txt', 734 | \ '+++ b/fixture.txt', 735 | \ '@@ -5,0 +6,4 @@ e', 736 | \ '+A', 737 | \ '+B', 738 | \ '+C', 739 | \ '+D', 740 | \ ] 741 | call assert_equal(expected, s:git_diff_staged()) 742 | endfunction 743 | 744 | 745 | function Test_hunk_stage_partial_preview_added_removed() 746 | 4,5delete 747 | call append(3, ['A','B','C','D']) 748 | 4 749 | GitGutterPreviewHunk 750 | wincmd P 751 | 752 | " -d 753 | " -e 754 | " +A 755 | " +B 756 | " +C 757 | " +D 758 | 759 | " remove D and d so they do not get staged 760 | 6delete 761 | 1delete 762 | 763 | GitGutterStageHunk 764 | write 765 | 766 | let expected = [ 767 | \ {'lnum': 3, 'name': 'GitGutterLineRemoved'}, 768 | \ {'lnum': 7, 'name': 'GitGutterLineAdded'}, 769 | \ ] 770 | call s:assert_signs(expected, 'fixture.txt') 771 | 772 | let expected = [ 773 | \ 'diff --git a/fixture.txt b/fixture.txt', 774 | \ 'index 9a19589..e63fb0a 100644', 775 | \ '--- a/fixture.txt', 776 | \ '+++ b/fixture.txt', 777 | \ '@@ -4 +3,0 @@ c', 778 | \ '-d', 779 | \ '@@ -7,0 +7 @@ C', 780 | \ '+D', 781 | \ ] 782 | call assert_equal(expected, s:git_diff()) 783 | 784 | let expected = [ 785 | \ 'diff --git a/fixture.txt b/fixture.txt', 786 | \ 'index f5c6aff..9a19589 100644', 787 | \ '--- a/fixture.txt', 788 | \ '+++ b/fixture.txt', 789 | \ '@@ -5 +5,3 @@ d', 790 | \ '-e', 791 | \ '+A', 792 | \ '+B', 793 | \ '+C', 794 | \ ] 795 | call assert_equal(expected, s:git_diff_staged()) 796 | endfunction 797 | 798 | 799 | function Test_hunk_undo() 800 | let _shell = &shell 801 | set shell=foo 802 | 803 | normal 5Gi* 804 | GitGutterUndoHunk 805 | 806 | call assert_equal('foo', &shell) 807 | let &shell = _shell 808 | 809 | call s:assert_signs([], 'fixture.txt') 810 | call assert_equal([], s:git_diff()) 811 | call assert_equal([], s:git_diff_staged()) 812 | call assert_equal('e', getline(5)) 813 | endfunction 814 | 815 | 816 | function Test_hunk_undo_dos() 817 | edit! fixture_dos.txt 818 | 819 | normal 5Gi* 820 | GitGutterUndoHunk 821 | 822 | call s:assert_signs([], 'fixture_dos.txt') 823 | call assert_equal([], s:git_diff('fixture_dos.txt')) 824 | call assert_equal([], s:git_diff_staged('fixture_dos.txt')) 825 | call assert_equal('e', getline(5)) 826 | endfunction 827 | 828 | 829 | function Test_undo_nearby_hunk() 830 | execute "normal! 2Gox\y\z" 831 | normal 2jdd 832 | normal k 833 | call s:trigger_gitgutter() 834 | GitGutterUndoHunk 835 | call s:trigger_gitgutter() 836 | 837 | let expected = [ 838 | \ {'lnum': 3, 'name': 'GitGutterLineAdded'}, 839 | \ {'lnum': 4, 'name': 'GitGutterLineAdded'}, 840 | \ {'lnum': 5, 'name': 'GitGutterLineAdded'} 841 | \ ] 842 | call s:assert_signs(expected, 'fixture.txt') 843 | 844 | call assert_equal([], s:git_diff()) 845 | 846 | call assert_equal([], s:git_diff_staged()) 847 | 848 | " Save the buffer 849 | write 850 | 851 | let expected = [ 852 | \ 'diff --git a/fixture.txt b/fixture.txt', 853 | \ 'index f5c6aff..3fbde56 100644', 854 | \ '--- a/fixture.txt', 855 | \ '+++ b/fixture.txt', 856 | \ '@@ -2,0 +3,3 @@ b', 857 | \ '+x', 858 | \ '+y', 859 | \ '+z', 860 | \ ] 861 | call assert_equal(expected, s:git_diff()) 862 | 863 | endfunction 864 | 865 | 866 | function Test_overlapping_hunk_op() 867 | func! Answer(char) 868 | call feedkeys(a:char."\") 869 | endfunc 870 | 871 | " Undo upper 872 | 873 | execute '3d' 874 | execute '1d' 875 | call s:trigger_gitgutter() 876 | normal gg 877 | call timer_start(100, {-> Answer('u')} ) 878 | GitGutterUndoHunk 879 | call s:trigger_gitgutter() 880 | 881 | let expected = [{'lnum': 2, 'name': 'GitGutterLineRemoved'}] 882 | call s:assert_signs(expected, 'fixture.txt') 883 | 884 | " Undo lower 885 | 886 | execute '1d' 887 | call s:trigger_gitgutter() 888 | normal gg 889 | call timer_start(100, {-> Answer('l')} ) 890 | GitGutterUndoHunk 891 | call s:trigger_gitgutter() 892 | 893 | let expected = [{'lnum': 1, 'name': 'GitGutterLineRemovedFirstLine'}] 894 | call s:assert_signs(expected, 'fixture.txt') 895 | endfunction 896 | 897 | 898 | function Test_write_option() 899 | set nowrite 900 | 901 | normal ggo* 902 | call s:trigger_gitgutter() 903 | 904 | let expected = [{'lnum': 2, 'name': 'GitGutterLineAdded'}] 905 | call s:assert_signs(expected, 'fixture.txt') 906 | 907 | set write 908 | endfunction 909 | 910 | 911 | function Test_inner_text_object() 912 | execute "normal! 2Gox\y\z\\" 913 | call s:trigger_gitgutter() 914 | normal dic 915 | call s:trigger_gitgutter() 916 | 917 | call s:assert_signs([], 'fixture.txt') 918 | call assert_equal(readfile('fixture.txt'), getline(1,'$')) 919 | 920 | " Excludes trailing lines 921 | normal 9Gi* 922 | normal 10Gi* 923 | call s:trigger_gitgutter() 924 | execute "normal vic\" 925 | call assert_equal([9, 10], [line("'<"), line("'>")]) 926 | endfunction 927 | 928 | 929 | function Test_around_text_object() 930 | execute "normal! 2Gox\y\z\\" 931 | call s:trigger_gitgutter() 932 | normal dac 933 | call s:trigger_gitgutter() 934 | 935 | call s:assert_signs([], 'fixture.txt') 936 | call assert_equal(readfile('fixture.txt'), getline(1,'$')) 937 | 938 | " Includes trailing lines 939 | normal 9Gi* 940 | normal 10Gi* 941 | call s:trigger_gitgutter() 942 | execute "normal vac\" 943 | call assert_equal([9, 11], [line("'<"), line("'>")]) 944 | endfunction 945 | 946 | 947 | function Test_user_autocmd() 948 | autocmd User GitGutter let s:autocmd_user = g:gitgutter_hook_context.bufnr 949 | 950 | " Verify not fired when nothing changed. 951 | let s:autocmd_user = 0 952 | call s:trigger_gitgutter() 953 | call assert_equal(0, s:autocmd_user) 954 | 955 | " Verify fired when there was a change. 956 | normal ggo* 957 | let bufnr = bufnr('') 958 | call s:trigger_gitgutter() 959 | call assert_equal(bufnr, s:autocmd_user) 960 | endfunction 961 | 962 | 963 | function Test_fix_file_references() 964 | let sid = matchstr(execute('filter autoload/gitgutter/hunk.vim scriptnames'), '\d\+') 965 | let FixFileReferences = function("".sid."_fix_file_references") 966 | 967 | " No special characters 968 | let hunk_diff = join([ 969 | \ 'diff --git a/fixture.txt b/fixture.txt', 970 | \ 'index f5c6aff..3fbde56 100644', 971 | \ '--- a/fixture.txt', 972 | \ '+++ b/fixture.txt', 973 | \ '@@ -2,0 +3,1 @@ b', 974 | \ '+x' 975 | \ ], "\n")."\n" 976 | let filepath = 'blah.txt' 977 | 978 | let expected = join([ 979 | \ 'diff --git a/blah.txt b/blah.txt', 980 | \ 'index f5c6aff..3fbde56 100644', 981 | \ '--- a/blah.txt', 982 | \ '+++ b/blah.txt', 983 | \ '@@ -2,0 +3,1 @@ b', 984 | \ '+x' 985 | \ ], "\n")."\n" 986 | 987 | call assert_equal(expected, FixFileReferences(filepath, hunk_diff)) 988 | 989 | " diff.mnemonicPrefix; spaces in filename 990 | let hunk_diff = join([ 991 | \ 'diff --git i/x/cat dog w/x/cat dog', 992 | \ 'index f5c6aff..3fbde56 100644', 993 | \ '--- i/x/cat dog', 994 | \ '+++ w/x/cat dog', 995 | \ '@@ -2,0 +3,1 @@ b', 996 | \ '+x' 997 | \ ], "\n")."\n" 998 | let filepath = 'blah.txt' 999 | 1000 | let expected = join([ 1001 | \ 'diff --git i/blah.txt w/blah.txt', 1002 | \ 'index f5c6aff..3fbde56 100644', 1003 | \ '--- i/blah.txt', 1004 | \ '+++ w/blah.txt', 1005 | \ '@@ -2,0 +3,1 @@ b', 1006 | \ '+x' 1007 | \ ], "\n")."\n" 1008 | 1009 | call assert_equal(expected, FixFileReferences(filepath, hunk_diff)) 1010 | 1011 | " Backslashes in filename; quotation marks 1012 | let hunk_diff = join([ 1013 | \ 'diff --git "a/C:\\Users\\FOO~1.PAR\\AppData\\Local\\Temp\\nvimJcmSv9\\11.1.vim" "b/C:\\Users\\FOO~1.PAR\\AppData\\Local\\Temp\\nvimJcmSv9\\12.1.vim"', 1014 | \ 'index f42aeb0..4930403 100644', 1015 | \ '--- "a/C:\\Users\\FOO~1.PAR\\AppData\\Local\\Temp\\nvimJcmSv9\\11.1.vim"', 1016 | \ '+++ "b/C:\\Users\\FOO~1.PAR\\AppData\\Local\\Temp\\nvimJcmSv9\\12.1.vim"', 1017 | \ '@@ -172,0 +173 @@ stuff', 1018 | \ '+x' 1019 | \ ], "\n")."\n" 1020 | let filepath = 'init.vim' 1021 | 1022 | let expected = join([ 1023 | \ 'diff --git "a/init.vim" "b/init.vim"', 1024 | \ 'index f42aeb0..4930403 100644', 1025 | \ '--- "a/init.vim"', 1026 | \ '+++ "b/init.vim"', 1027 | \ '@@ -172,0 +173 @@ stuff', 1028 | \ '+x' 1029 | \ ], "\n")."\n" 1030 | 1031 | call assert_equal(expected, FixFileReferences(filepath, hunk_diff)) 1032 | endfunction 1033 | 1034 | 1035 | function Test_encoding() 1036 | call system('cp ../cp932.txt . && git add cp932.txt') 1037 | edit ++enc=cp932 cp932.txt 1038 | 1039 | call s:trigger_gitgutter() 1040 | 1041 | call s:assert_signs([], 'cp932.txt') 1042 | endfunction 1043 | 1044 | 1045 | function Test_empty_file() 1046 | " 0-byte file 1047 | call system('touch empty.txt && git add empty.txt') 1048 | edit empty.txt 1049 | 1050 | call s:trigger_gitgutter() 1051 | call s:assert_signs([], 'empty.txt') 1052 | 1053 | 1054 | " File consisting only of a newline 1055 | call system('echo "" > newline.txt && git add newline.txt') 1056 | edit newline.txt 1057 | 1058 | call s:trigger_gitgutter() 1059 | call s:assert_signs([], 'newline.txt') 1060 | 1061 | 1062 | " 1 line file without newline 1063 | " Vim will force a newline unless we tell it not to. 1064 | call system('echo -n a > oneline.txt && git add oneline.txt') 1065 | set noeol nofixeol 1066 | edit! oneline.txt 1067 | 1068 | call s:trigger_gitgutter() 1069 | call s:assert_signs([], 'oneline.txt') 1070 | 1071 | set eol fixeol 1072 | endfunction 1073 | 1074 | 1075 | function Test_quickfix() 1076 | call setline(5, ['A', 'B']) 1077 | call setline(9, ['C', 'D']) 1078 | write 1079 | let bufnr1 = bufnr('') 1080 | 1081 | edit fixture_dos.txt 1082 | call setline(2, ['A', 'B']) 1083 | write 1084 | let bufnr2 = bufnr('') 1085 | 1086 | GitGutterQuickFix 1087 | 1088 | let expected = [ 1089 | \ {'lnum': 5, 'bufnr': bufnr1, 'text': '-e'}, 1090 | \ {'lnum': 9, 'bufnr': bufnr1, 'text': '-i'}, 1091 | \ {'lnum': 2, 'bufnr': bufnr2, 'text': "-b\r"} 1092 | \ ] 1093 | 1094 | call s:assert_list_of_dicts(expected, getqflist()) 1095 | 1096 | GitGutterQuickFixCurrentFile 1097 | 1098 | let expected = [ 1099 | \ {'lnum': 2, 'bufnr': bufnr(''), 'text': "-b\r"}, 1100 | \ ] 1101 | 1102 | call s:assert_list_of_dicts(expected, getqflist()) 1103 | endfunction 1104 | 1105 | 1106 | function Test_common_prefix() 1107 | let sid = matchstr(execute('filter autoload/gitgutter/diff_highlight.vim scriptnames'), '\d\+') 1108 | let CommonPrefix = function("".sid."_common_prefix") 1109 | 1110 | " zero length 1111 | call assert_equal(-1, CommonPrefix('', 'foo')) 1112 | call assert_equal(-1, CommonPrefix('foo', '')) 1113 | " nothing in common 1114 | call assert_equal(-1, CommonPrefix('-abcde', '+pqrst')) 1115 | call assert_equal(-1, CommonPrefix('abcde', 'pqrst')) 1116 | " something in common 1117 | call assert_equal(-1, CommonPrefix('-abcde', '+abcpq')) 1118 | call assert_equal(2, CommonPrefix('abcde', 'abcpq')) 1119 | call assert_equal(0, CommonPrefix('abc', 'apq')) 1120 | " everything in common 1121 | call assert_equal(-1, CommonPrefix('-abcde', '+abcde')) 1122 | call assert_equal(4, CommonPrefix('abcde', 'abcde')) 1123 | " different lengths 1124 | call assert_equal(-1, CommonPrefix('-abcde', '+abx')) 1125 | call assert_equal(1, CommonPrefix('abcde', 'abx')) 1126 | call assert_equal(-1, CommonPrefix('-abx', '+abcde')) 1127 | call assert_equal(1, CommonPrefix('abx', 'abcde')) 1128 | call assert_equal(-1, CommonPrefix('-abcde', '+abc')) 1129 | call assert_equal(2, CommonPrefix('abcde', 'abc')) 1130 | endfunction 1131 | 1132 | 1133 | function Test_common_suffix() 1134 | let sid = matchstr(execute('filter autoload/gitgutter/diff_highlight.vim scriptnames'), '\d\+') 1135 | let CommonSuffix = function("".sid."_common_suffix") 1136 | 1137 | " nothing in common 1138 | call assert_equal([6,6], CommonSuffix('-abcde', '+pqrst', 0)) 1139 | " something in common 1140 | call assert_equal([3,3], CommonSuffix('-abcde', '+pqcde', 0)) 1141 | " everything in common 1142 | call assert_equal([5,5], CommonSuffix('-abcde', '+abcde', 5)) 1143 | " different lengths 1144 | call assert_equal([4,2], CommonSuffix('-abcde', '+xde', 0)) 1145 | call assert_equal([2,4], CommonSuffix('-xde', '+abcde', 0)) 1146 | endfunction 1147 | 1148 | 1149 | " Note the order of lists within the overall returned list does not matter. 1150 | function Test_diff_highlight() 1151 | " Ignores mismatched number of added and removed lines. 1152 | call assert_equal([], gitgutter#diff_highlight#process(['-foo'])) 1153 | call assert_equal([], gitgutter#diff_highlight#process(['+foo'])) 1154 | call assert_equal([], gitgutter#diff_highlight#process(['-foo','-bar','+baz'])) 1155 | 1156 | " everything changed 1157 | let hunk = ['-foo', '+cat'] 1158 | let expected = [] 1159 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1160 | 1161 | " change in middle 1162 | let hunk = ['-foo bar baz', '+foo zip baz'] 1163 | let expected = [[1, '-', 6, 8], [2, '+', 6, 8]] 1164 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1165 | 1166 | " change at start 1167 | let hunk = ['-foo bar baz', '+zip bar baz'] 1168 | let expected = [[1, '-', 2, 4], [2, '+', 2, 4]] 1169 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1170 | 1171 | " change at end 1172 | let hunk = ['-foo bar baz', '+foo bar zip'] 1173 | let expected = [[1, '-', 10, 12], [2, '+', 10, 12]] 1174 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1175 | 1176 | " removed in middle 1177 | let hunk = ['-foo bar baz', '+foo baz'] 1178 | let expected = [[1, '-', 8, 11]] 1179 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1180 | 1181 | " added in middle 1182 | let hunk = ['-foo baz', '+foo bar baz'] 1183 | let expected = [[2, '+', 8, 11]] 1184 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1185 | 1186 | " two insertions at start 1187 | let hunk = ['-foo bar baz', '+(foo) bar baz'] 1188 | let expected = [[2, '+', 2, 2], [2, '+', 6, 6]] 1189 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1190 | 1191 | " two insertions in middle 1192 | let hunk = ['-foo bar baz', '+foo (bar) baz'] 1193 | let expected = [[2, '+', 6, 6], [2, '+', 10, 10]] 1194 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1195 | 1196 | " two insertions at end 1197 | let hunk = ['-foo bar baz', '+foo bar (baz)'] 1198 | let expected = [[2, '+', 10, 10], [2, '+', 14, 14]] 1199 | call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) 1200 | 1201 | " singular insertion 1202 | let hunk = ['-The cat in the hat.', '+The furry cat in the hat.'] 1203 | call assert_equal([[2, '+', 6, 11]], gitgutter#diff_highlight#process(hunk)) 1204 | 1205 | " singular deletion 1206 | let hunk = ['-The cat in the hat.', '+The cat.'] 1207 | call assert_equal([[1, '-', 9, 19]], gitgutter#diff_highlight#process(hunk)) 1208 | 1209 | " two insertions 1210 | let hunk = ['-The cat in the hat.', '+The furry cat in the teal hat.'] 1211 | call assert_equal([[2, '+', 6, 11], [2, '+', 22, 26]], gitgutter#diff_highlight#process(hunk)) 1212 | 1213 | " two deletions 1214 | let hunk = ['-The furry cat in the teal hat.', '+The cat in the hat.'] 1215 | call assert_equal([[1, '-', 6, 11], [1, '-', 22, 26]], gitgutter#diff_highlight#process(hunk)) 1216 | 1217 | " two edits 1218 | let hunk = ['-The cat in the hat.', '+The ox in the box.'] 1219 | call assert_equal([[1, '-', 6, 8], [2, '+', 6, 7], [1, '-', 17, 19], [2, '+', 16, 18]], gitgutter#diff_highlight#process(hunk)) 1220 | 1221 | " Requires s:gap_between_regions = 2 to pass. 1222 | " let hunk = ['-foo: bar.zap', '+foo: quux(bar)'] 1223 | " call assert_equal([[2, '+', 7, 11], [1, '-', 10, 13], [2, '+', 15, 15]], gitgutter#diff_highlight#process(hunk)) 1224 | 1225 | let hunk = ['-gross_value: transaction.unexplained_amount', '+gross_value: amount(transaction)'] 1226 | call assert_equal([[2, '+', 15, 21], [1, '-', 26, 44], [2, '+', 33, 33]], gitgutter#diff_highlight#process(hunk)) 1227 | 1228 | let hunk = ['-gem "contact_sport", "~> 1.0.2"', '+gem ("contact_sport"), "~> 1.2"'] 1229 | call assert_equal([[2, '+', 6, 6], [2, '+', 22, 22], [1, '-', 28, 29]], gitgutter#diff_highlight#process(hunk)) 1230 | endfunction 1231 | 1232 | 1233 | function Test_lcs() 1234 | let sid = matchstr(execute('filter autoload/gitgutter/diff_highlight.vim scriptnames'), '\d\+') 1235 | let Lcs = function("".sid."_lcs") 1236 | 1237 | call assert_equal('', Lcs('', 'foo')) 1238 | call assert_equal('', Lcs('foo', '')) 1239 | call assert_equal('bar', Lcs('foobarbaz', 'bbart')) 1240 | call assert_equal('transaction', Lcs('transaction.unexplained_amount', 'amount(transaction)')) 1241 | endfunction 1242 | 1243 | 1244 | function Test_split() 1245 | let sid = matchstr(execute('filter autoload/gitgutter/diff_highlight.vim scriptnames'), '\d\+') 1246 | let Split = function("".sid."_split") 1247 | 1248 | call assert_equal(['foo', 'baz'], Split('foobarbaz', 'bar')) 1249 | call assert_equal(['', 'barbaz'], Split('foobarbaz', 'foo')) 1250 | call assert_equal(['foobar', ''], Split('foobarbaz', 'baz')) 1251 | call assert_equal(['1', '2'], Split('1~2', '~')) 1252 | endfunction 1253 | 1254 | 1255 | function Test_foldtext() 1256 | 8d 1257 | call s:trigger_gitgutter() 1258 | call assert_equal(0, gitgutter#fold#is_changed()) 1259 | 1260 | let v:foldstart = 5 1261 | let v:foldend = 9 1262 | call assert_equal(1, gitgutter#fold#is_changed()) 1263 | call assert_equal('+- 5 lines (*): e', gitgutter#fold#foldtext()) 1264 | 1265 | let v:foldstart = 1 1266 | let v:foldend = 3 1267 | call assert_equal(0, gitgutter#fold#is_changed()) 1268 | call assert_equal('+- 3 lines: a', gitgutter#fold#foldtext()) 1269 | endfunction 1270 | 1271 | 1272 | function Test_assume_unchanged() 1273 | call system("git update-index --assume-unchanged fixture.txt") 1274 | unlet b:gitgutter.path " it was already set when fixture.txt was loaded in SetUp() 1275 | normal ggo* 1276 | call s:trigger_gitgutter() 1277 | call s:assert_signs([], 'fixture.txt') 1278 | endfunction 1279 | 1280 | 1281 | function Test_clean_smudge_filter() 1282 | call system("git config --local include.path ../.gitconfig") 1283 | call system("rm fixture.foo && git checkout fixture.foo") 1284 | 1285 | func! Answer(char) 1286 | call feedkeys(a:char."\") 1287 | endfunc 1288 | 1289 | edit fixture.foo 1290 | call setline(2, ['A']) 1291 | call setline(4, ['B']) 1292 | call s:trigger_gitgutter() 1293 | normal! 2G 1294 | call timer_start(100, {-> Answer('y')} ) 1295 | GitGutterStageHunk 1296 | call s:trigger_gitgutter() 1297 | 1298 | let expected = [ 1299 | \ {'lnum': 2, 'id': 23, 'name': 'GitGutterLineModified', 'priority': 10, 'group': 'gitgutter'}, 1300 | \ {'lnum': 4, 'id': 24, 'name': 'GitGutterLineModified', 'priority': 10, 'group': 'gitgutter'} 1301 | \ ] 1302 | " call s:assert_signs(expected, 'fixture.foo') 1303 | call s:assert_signs([], 'fixture.foo') 1304 | endfunction 1305 | --------------------------------------------------------------------------------