Repository: rickhowe/diffchar.vim Branch: master Commit: 14fc3fc79356 Files: 5 Total size: 89.4 KB Directory structure: gitextract_c6cdx0o3/ ├── LICENSE ├── README.md ├── autoload/ │ └── diffchar.vim ├── doc/ │ └── diffchar.txt └── plugin/ └── diffchar.vim ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014-2025 Rick Howe (Takumi Ohtani) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # diffchar.vim *Highlight the exact differences, based on characters and words* ``` ____ _ ____ ____ _____ _ _ _____ ____ | | | || || || || | | || _ || _ | | _ || || __|| __|| || | | || | | || | || | | | || || |__ | |__ | __|| |_| || |_| || |_||_ | |_| || || __|| __|| | | || || __ | | || || | | | | |__ | _ || _ || | | | |____| |_||_| |_| |_____||_| |_||_| |_||_| |_| ``` This plugin has been developed in order to make diff mode more useful. Vim highlights all the text in between the first and last different characters on a changed line. But this plugin will find the exact differences between them, character by character - so called *DiffChar*. For example, in diff mode: ![example1](example1.png) This plugin will exactly show the changed/added/deleted units: ![example2](example2.png) #### Sync with diff mode This plugin will synchronously show/reset the highlights of the exact differences as soon as the diff mode begins/ends. And the exact differences will be kept updated while editing. Note that this plugin does nothing if an `inline` item is set in the `doffopt` option and its value is other than `simple`. On plugin loading, the item is removed only if set by default so that this plugin can work. #### Diff unit This plugin shows the diffs based on a `g:DiffUnit`. Its default is 'Word1' and it handles a `\w\+` word and a `\W` character as a diff unit. There are other types of word provided and you can also set 'Char' to compare character by character. In addition, you can specify one or more diff unit delimiters, such as comma (','), colon (':'), tab ("\t"), and HTML tag symbols ('<' and '>'), and also specify a custom pattern in the `g:DiffUnit`. #### Diff matching colors In diff mode, the corresponding `hl-DiffChange` lines are compared between two windows. As a default, all the changed units are highlighted with `hl-DiffText`. You can set `g:DiffColors` to use more than one matching color to make it easy to find the corresponding units between two windows. The number of colors depends on the color scheme. In addition, an added unit is always highlighted with `hl-DiffAdd` and the position of the corresponding deleted unit is shown with bold/underline or a virtual blank column, depending on a `g:DiffDelPosVisible`. #### Diff pair visible While showing the exact differences, when the cursor is moved on a diff unit, you can see its corresponding unit highlighted with `hl-Cursor`, `hl-TermCursor`, or similar one in another window, based on a `g:DiffPairVisible`. If you change its default, the corresponding unit is echoed in the command line or displayed in a popup/floating window just below the cursor position or at the mouse position. Those options take effect on `:diffupdate` command as well. #### Jump to next/prev diff unit You can use `]b` or `]e` to jump cursor to start or end position of the next diff unit, and `[b` or `[e` to the start or end position of the previous unit. #### Get/put a diff unit Like line-based `:diffget`/`:diffput` and `do`/`dp` vim commands, you can use `g` and `p` commands in normal mode to get and put each diff unit, where the cursor is on, between 2 buffers and undo its difference. Those keymaps are configurable in your vimrc and so on. #### Check diff lines locally When the diff mode begins, this plugin locally checks the `hl-DiffChange` lines in the limited range of the current visible and its upper/lower lines of a window. And each time a cursor is moved on to another range upon scrolling or searching, those diff lines will be checked in that range. Which means, independently of the file size, the number of lines to be checked and then the time consumed are always constant. #### Tab page individual This plugin works on each tab page individually. You can use a tab page variable (t:), instead of a global one (g:), to specify different options on each tab page. Note that this plugin can not handle more than two diff mode windows in a tab page. If it would happen, to prevent any trouble, all the highlighted units are to be reset in the tab page. #### Follow 'diffopt' option This plugin supports `icase`, `iwhite`, `iwhiteall`, and `iwhiteeol` in the `diffopt` option. In addition, when `indent-heuristic` is specified, positioning of the added/deleted diff units is adjusted to reduce the number of diff hunks and make them easier to read. #### Comparison algorithm To find the exact differences, this plugin uses "An O(NP) Sequence Comparison Algorithm" developed by S.Wu, et al., which always finds an optimum sequence. But it takes time to check a long and dissimilar line. To improve the performance, the algorithm is also implemented in Vim9 script. In addition, if available, this plugin uses a builtin diff function (`diff()` in vim patch-9.1.0071 and Lua `vim.diff()` in nvim 0.6.0) and makes it much faster. #### See also There are other diff related plugins available: * [spotdiff.vim](https://github.com/rickhowe/spotdiff.vim): A range and area selectable `:diffthis` to compare partially * [wrapfiller](https://github.com/rickhowe/wrapfiller): Align each wrapped line virtually between windows * [difffilter](https://github.com/rickhowe/difffilter): Selectively compare lines as you want in diff mode * [diffunitsyntax](https://github.com/rickhowe/diffunitsyntax): Highlight word or character based diff units in diff format ### Options * `g:DiffUnit`, `t:DiffUnit`: A type of difference unit | Value | Description | | --- | --- | | 'Char' | any single character | | 'Word1' | `\w\+` word and any `\W` single character (default) | | 'Word2' | non-space and space words | | 'Word3' | `\<` or `\>` character class boundaries (set by `iskeyword`) | | 'word' | see `word` | | 'WORD' | see `WORD` | | '[{del}]' | one or more diff unit delimiters (e.g. "[,:\t<>]") | | '/{pat}/' | a pattern to split into diff units (e.g. '/.\{4}\zs/') | * `g:DiffColors`, `t:DiffColors`: Matching colors for changed units | Value | Description | | --- | --- | | 0 | `hl-DiffText` (default) | | 1 | `hl-DiffText` + a few (3, 4, ...) highlight groups | | 2 | `hl-DiffText` + several (7, 8, ...) highlight groups | | 3 | `hl-DiffText` + many (11, 12, ...) highlight groups | | 100 | all available highlight groups in random order | | [{hlg}] | a list of your favorite highlight groups | * `g:DiffPairVisible`, `t:DiffPairVisible`: Visibility of corresponding diff units | Value | Description | | --- | --- | | 0 | disable | | 1 | highlight with `hl-Cursor` (default) | | 2 | highlight with `hl-Cursor` + echo in the command line | | 3 | highlight with `hl-Cursor` + popup/floating window at cursor position | | 4 | highlight with `hl-Cursor` + popup/floating window at mouse position | * `g:DiffDelPosVisible`, `t:DiffDelPosVisible`: Visibility of the position of deleted units | Value | Description | | --- | --- | | 0 | disable | | 1 | highlight previous/next chars of a deleted unit in bold/underline (default if inline "virtual-text" is not available) | | 2 | virtually show a blank column (set by `space` item in `listchars`) wih `hl-DiffDelete` (default if inline "virtual-text" is available) | ### Keymaps | Mapping | Default Key | Description | | --- | --- | --- | | `JumpDiffCharPrevStart` | `[b` | Jump cursor to the start position of the previous diff unit | | `JumpDiffCharNextStart` | `]b` | Jump cursor to the start position of the next diff unit | | `JumpDiffCharPrevEnd` | `[e` | Jump cursor to the end position of the previous diff unit | | `JumpDiffCharNextEnd` | `]e` | Jump cursor to the end position of the next diff unit | | `GetDiffCharPair` | `g` | Get a corresponding diff unit from another buffer to undo difference | | `PutDiffCharPair` | `p` | Put a corresponding diff unit to another buffer to undo difference | ================================================ FILE: autoload/diffchar.vim ================================================ " diffchar.vim: Highlight the exact differences, based on characters and words " " ____ _ ____ ____ _____ _ _ _____ ____ " | | | || || || || | | || _ || _ | " | _ || || __|| __|| || | | || | | || | || " | | | || || |__ | |__ | __|| |_| || |_| || |_||_ " | |_| || || __|| __|| | | || || __ | " | || || | | | | |__ | _ || _ || | | | " |____| |_||_| |_| |_____||_| |_||_| |_||_| |_| " " Last Change: 2025/10/01 " Version: 10.0 (on or after vim 9.0 and nvim 0.7.0) " Author: Rick Howe (Takumi Ohtani) " Copyright: (c) 2014-2025 Rick Howe " License: MIT let s:save_cpo = &cpoptions set cpo&vim " Vim feature, function, event and patch number which this plugin depends on " patch-8.2.4204: screenpos() fixed to return zero row for invisible line " patch-9.0.1067: virtual text fixed to correctly highlight in diff mode " patch-9.1.0099: diff() fixed to correctly use in &diffexpr let s:VF = #{ \ScreenPos: exists('*screenpos') && \(has('patch-8.2.4204') || has('nvim-0.8.2')), \InlineVirtText: has('textprop') && has('patch-9.0.1067') || \has('nvim-0.10.0'), \BuiltinDiffFunc: exists('*diff') && has('patch-9.1.0099') || \has('nvim-0.6.0')} function! s:ShowDiffChar(...) abort " !a:0 - all dfl from scratch, a:1 - specified dfl only if !a:0 && !exists('t:DChar') && s:InitializeDiffChar() == -1 return endif let ak = 2 | while t:DChar.wid[ak] != win_getid() | let ak -= 1 if ak == 0 | return | endif endwhile let lc = #{1: {}, 2: {}} for n in (!a:0) ? range(len(t:DChar.dfl[ak])) : \filter(map(copy(a:1), 'index(t:DChar.dfl[ak], v:val)'), 'v:val != -1') let tu = #{1: {}, 2: {}} for k in [1, 2] let tu[k].l = t:DChar.dfl[k][n] if has_key(t:DChar.hlc[k], tu[k].l) | let tu[k].l = 0 | endif endfor if 0 < tu[1].l && 0 < tu[2].l for k in [1, 2] let tu[k].t = getbufline(t:DChar.bnr[k], tu[k].l)[0] let t = t:DChar.opt.ic ? tolower(tu[k].t) : tu[k].t let tu[k].u = split(t:DChar.opt.iw == 0 ? t : \substitute(t, (t:DChar.opt.iw == 1) ? '\s\+' : '\s\+$', '', 'g'), \t:DChar.upa) let tu[k].w = (tu[k].t =~ '\s\+' && \(t:DChar.opt.iw == 1 || t:DChar.opt.iw == 2)) ? t:DChar.opt.iw : 0 if tu[k].w == 2 let u = [] | let s = '' for z in tu[k].u + [''] if z =~ '^\s\+$' let s .= z else if !empty(s) | let u += [s] | let s = '' | endif let u += [z] endif endfor let tu[k].u = u[: -2] endif endfor let uu = [copy(tu[1].u), copy(tu[2].u)] for k in [1, 2] if tu[k].w == 2 call map(tu[k].u, 'substitute(v:val, "\\s\\+", " ", "g")') endif endfor if tu[1].u !=# tu[2].u let [tu[1].c, tu[2].c] = s:GetDiffUnitPos(uu, \t:DChar.dfn(tu[1].u, tu[2].u, t:DChar.opt.ih)) for k in [1, 2] if tu[k].w == 1 let pc = [0] + filter(range(1, len(tu[k].t)), \'tu[k].t[v:key] =~ "\\S"') + [len(tu[k].t) + 1] for n in range(len(tu[k].c)) call map(tu[k].c[n][1], 'pc[v:val]') endfor endif let lc[k][tu[k].l] = tu[k].c let t:DChar.cks[k][tu[k].l] = s:ChecksumStr(tu[k].t) endfor endif endif endfor if !a:0 call s:ToggleDiffCharEvent(1) call s:ToggleDiffHL(1) call s:ToggleDiffCharPair(1) endif if !empty(lc[ak]) call s:HighlightDiffChar(lc) if 0 < t:DChar.dpv.pv | call s:ShowDiffCharPair(ak) | endif endif endfunction function! s:ResetDiffChar(...) abort " !a:0 - all dfl to scratch, a:1 - specified dfl only if !exists('t:DChar') | return | endif let ak = 2 | while t:DChar.wid[ak] != win_getid() | let ak -= 1 if ak == 0 | return | endif endwhile let dl = #{1: [], 2: []} for n in (!a:0) ? range(len(t:DChar.dfl[ak])) : \filter(map(copy(a:1), 'index(t:DChar.dfl[ak], v:val)'), 'v:val != -1') for k in [1, 2] let l = t:DChar.dfl[k][n] if has_key(t:DChar.hlc[k], l) let dl[k] += [l] unlet t:DChar.cks[k][l] endif endfor endfor if !empty(dl[ak]) if 0 < t:DChar.dpv.pv | call s:ClearDiffCharPair(ak) | endif call s:ClearDiffChar(dl) endif if !a:0 call s:ToggleDiffCharPair(0) call s:ToggleDiffHL(0) unlet t:DChar call s:ToggleDiffCharEvent(0) endif endfunction function! s:InitializeDiffChar() abort if matchstr(&diffopt, '^.*inline:\zs.*') !~ '^simple\|^$' call s:EchoWarning('Disabled because "inline:" value set in &diffopt is \ not "simple"!') return -1 endif let cw = win_getid() | let cb = winbufnr(cw) let nw = filter(map(range(winnr() + 1, winnr('$')) + \range(1, winnr() - 1), 'win_getid(v:val)'), \'getwinvar(v:val, "&diff") && winbufnr(v:val) != cb') let nb = map(copy(nw), 'winbufnr(v:val)') if !getwinvar(cw, '&diff') || empty(nw) || min(nb) != max(nb) return -1 endif for tn in filter(range(1, tabpagenr('$')), 'v:val != tabpagenr()') let dc = gettabvar(tn, 'DChar') if !empty(dc) for bn in values(dc.bnr) if index([cb, nb[0]], bn) != -1 call s:EchoWarning('Both or either selected buffer already \ highlighted in tab page ' . tn . '!') return -1 endif endfor endif endfor call s:SetDiffCharHL() let t:DChar = {} let t:DChar.wid = #{1: cw, 2: nw[0]} let t:DChar.bnr = #{1: cb, 2: nb[0]} let t:DChar.opt = s:GetDiffCharOptions() let t:DChar.lcc = s:GetLineColCnr() let t:DChar.dfl = s:FocusDiffLines(0) let t:DChar.upa = s:GetDiffSplitRegExp(t:DChar.opt.ut) let t:DChar.dpv = s:GetDiffPairVisible(t:DChar.opt.pv) let t:DChar.hgp = s:GetDiffUnitHL(t:DChar.opt.cl) let t:DChar.csn = s:GetColorsName() let t:DChar.mid = #{1: {}, 2: {}} let t:DChar.hlc = #{1: {}, 2: {}} let t:DChar.cks = #{1: {}, 2: {}} let t:DChar.dfn = function(t:DChar.opt.df ? \'s:ApplyDiffFunc' : 's:TraceDiffChar') endfunction function! s:GetDiffSplitRegExp(du) abort if a:du == 'Char' let upa = '\zs' elseif a:du == 'Word2' || a:du ==# 'WORD' let upa = '\%(\s\+\|\S\+\)\zs' elseif a:du == 'Word3' || a:du ==# 'word' let upa = '\<\|\>' elseif a:du =~ '^\[.\+\]$' let s = escape(a:du[1 : -2], ']^-\') let upa = '\%([^' . s . ']\+\|[' . s . ']\)\zs' elseif a:du =~ '^\([/?]\).\+\1$' let upa = a:du[1 : -2] else let upa = '\%(\w\+\|\W\)\zs' if a:du != 'Word1' call s:EchoWarning('Not a valid difference unit type. \ Use "Word1" instead.') endif endif return upa endfunction function! s:GetDiffPairVisible(pv) abort let dpv = #{pv: a:pv} if 0 < dpv.pv let dpv.ch = {} if dpv.pv == 3 || dpv.pv == 4 | let dpv.pw = has('nvim') ? {} : 0 | endif endif return dpv endfunction function! s:GetDiffUnitHL(dc) abort let hgp = [s:DCharHL.T] if type(a:dc) == type([]) let hgp += filter(copy(a:dc), \'0 < hlID(v:val) && !empty(synIDattr(hlID(v:val), "bg#"))') if 1 < len(hgp) | unlet hgp[0] | endif elseif 1 <= a:dc && a:dc <= 3 let lv = a:dc - 1 let bx = [] for nm in values(s:DCharHL) let [fc, bc] = map(['fg#', 'bg#'], \'s:ColorClass(synIDattr(hlID(nm), v:val), lv)') if !empty(bc) | let bx += [bc] | endif if nm == s:DCharHL.n | let fn = fc | endif endfor let hl = {} | let id = 1 while 1 let nm = synIDattr(id, 'name') if empty(nm) | break | endif if id == synIDtrans(id) && empty(filter(['underline', 'undercurl', \'strikethrough', 'reverse', 'inverse', 'standout'], \'!empty(synIDattr(id, v:val))')) let [fc, bc] = map(['fg#', 'bg#'], \'s:ColorClass(synIDattr(id, v:val), lv)') if !empty(bc) && index(bx + [!empty(fc) ? fc : fn], bc) == -1 let wt = !empty(fc) + (!empty(filter(['bold', 'italic'], \'!empty(synIDattr(id, v:val))'))) * 2 if !has_key(hl, bc) || hl[bc][0] < wt let hl[bc] = [wt, nm] endif endif endif let id += 1 endwhile let hgp += map(values(hl), 'v:val[1]') elseif a:dc == 100 let bx = map(values(s:DCharHL), 'synIDattr(hlID(v:val), "bg#")') let hl = {} | let id = 1 while 1 let nm = synIDattr(id, 'name') if empty(nm) | break | endif if id == synIDtrans(id) let bg = synIDattr(id, 'bg#') if !empty(bg) && index(bx, bg) == -1 let hl[reltimestr(reltime())[-2 :] . id] = nm let bx += [bg] endif endif let id += 1 endwhile let hgp += values(hl) endif return hgp endfunction function! s:GetColorsName() abort return get(g:, 'colors_name', 'default') endfunction function! s:ColorClass(cn, lv) abort if empty(a:cn) | return a:cn | endif if a:cn[0] != '#' let cn = a:cn % 256 if cn < 16 let cv = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0], \[0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192], \[128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0], \[0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]] if &t_Co < 256 let [cv[9], cv[12], cv[11], cv[14]] = [cv[12], cv[9], cv[14], cv[11]] endif let rgb = cv[cn] elseif cn < 232 let cv = [0, 95, 135, 175, 215, 255] let cn -= 16 let rgb = [cv[(cn / 36) % 6], cv[(cn / 6) % 6], cv[cn % 6]] else let cn = 10 * (cn - 232) + 8 let rgb = [cn, cn, cn] endif else let rgb = map(split(a:cn[1 :], '..\zs'), 'str2nr(v:val, 16)') endif let cl = [[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 0, 0, 1, 1, 2, 2], \[0, 1, 2, 3, 4, 5, 6, 7]][a:lv] call map(rgb, 'v:val / 32') if max(rgb) == min(rgb) return '99' . cl[(rgb[0] + rgb[1] + rgb[2]) / 3] else return join(map(rgb, 'cl[v:val]'), '') endif endfunction function! s:SetDiffCharHL() abort " set diff hl in original and DChar modes let s:DiffHL = #{A: 'DiffAdd', C: 'DiffChange', D: 'DiffDelete', \T: 'DiffText'} for [hs, hl] in items(s:DiffHL) let dh = {} let dh.id = hlID(hl) let dh.it = synIDtrans(dh.id) " in case of linked let dh.nm = synIDattr(dh.it, 'name') " 0 : for original, 1 : for DChar let dh[0] = {} for hm in ['cterm', 'gui'] for hc in ['fg', 'bg'] let dh[0][hm . hc] = synIDattr(dh.it, hc, hm) endfor let dh[0][hm] = join(filter(['bold', 'underline', 'undercurl', \'strikethrough', 'reverse', 'inverse', 'italic', 'standout'], \'!empty(synIDattr(dh.it, v:val, hm))'), ',') endfor call map(dh[0], '!empty(v:val) ? v:val : "NONE"') let dh[1] = (hs == 'C' || hs == 'T') ? \map(copy(dh[0]), 'v:key =~ "bg$" ? v:val : "NONE"') : dh[0] let s:DiffHL[hs] = dh endfor for at in ['ctermbg', 'guibg'] " in case no bg in C, also in T if s:DiffHL.C[1][at] == 'NONE' | let s:DiffHL.T[1][at] = 'NONE' | endif endfor " set DChar hl let s:DCharHL = {} let s:DCharHL.n = 'Normal' let s:DCharHL.c = has('nvim') ? 'TermCursor' : 'Cursor' if !empty(filter(['fg', 'bg'], \'empty(synIDattr(hlID(s:DCharHL.c), v:val))')) let s:DCharHL.c = 'dcCursor' for at in ['NONE', 'ctermfg=fg', 'ctermbg=bg', 'cterm=reverse', \'guifg=fg', 'guibg=bg', 'gui=reverse'] call execute(join(['highlight', s:DCharHL.c, at]), 'silent!') endfor endif for [fs, ts, th, ta] in [['C', 'C', 'dcDiffChange', ''], \['T', 'T', 'dcDiffText', ''], \['A', 'A', 'dcDiffAdd', ''], \['D', 'D', 'dcDiffDelete', ''], \['C', 'E', 'dcDiffDelPos', 'bold,underline']] let fa = copy(s:DiffHL[fs][0]) if !empty(ta) for hm in ['cterm', 'gui'] let fa[hm] = ((fa[hm] != 'NONE') ? fa[hm] . ',' : '') . ta endfor endif for at in ['NONE'] + map(items(fa), 'join(v:val, "=")') call execute(join(['highlight', th, at]), 'silent!') endfor let s:DCharHL[ts] = th endfor if s:VF.InlineVirtText | call s:SetVirtColumn() | endif endfunction function! s:GetDiffCharOptions() abort let Dip = {op -> &diffopt =~ op} let do = #{ic: Dip('icase'), \iw: Dip('iwhiteall') ? 1 : Dip('\') ? 2 : \Dip('iwhiteeol') ? 3 : 0, \ih: Dip('indent-heuristic'), lm: Dip('linematch'), vc: ' '} if &listchars =~ '\' let do.vc = matchstr(&listchars, '^.*\' let ac += [['WinClosed', bl, 's:WinClosedDiffChar()']] if td.opt.fp != 0 let ac += [['WinScrolled', bl, 's:ScrollDiffLines(0)']] endif if 0 < td.dpv.pv let ac += [['CursorMoved', bl, 's:ShowDiffCharPair(' . k . ')']] endif endfor endfor let ac += [['TabEnter', '*', 's:RefreshDiffCharHL(0)']] let ac += [['ColorScheme', '*', 's:RefreshDiffCharHL(1)']] let ac += [['BufWinEnter', '*', 's:RepairDiffChar()']] let ac += [['DiffUpdated', '*', 's:UpdateDiffChar()']] call execute(map(ac, 'join(["autocmd", "diffchar", v:val[0], v:val[1], \"call", v:val[2]])')) endfunction function! s:ShiftDiffChar(key, lines, shift) abort let im = {} for gm in getmatches(t:DChar.wid[a:key]) let im[gm.id] = gm endfor let [lid, hlc, cks] = [{}, {}, {}] for ln in a:lines if has_key(t:DChar.mid[a:key], ln) let lid[ln + a:shift] = [] for id in t:DChar.mid[a:key][ln] if 0 < id if has_key(im, id) call matchdelete(id, t:DChar.wid[a:key]) let lid[ln + a:shift] += [matchaddpos(im[id].group, \map(values(filter(copy(im[id]), 'v:key =~ "^pos\\d\\+$"')), \'[v:val[0] + a:shift] + v:val[1 :]'), \im[id].priority, -1, #{window: t:DChar.wid[a:key]})] endif elseif id < 0 " virtual column is kept on the line, no need to remake it let lid[ln + a:shift] += [id] endif endfor unlet t:DChar.mid[a:key][ln] endif if has_key(t:DChar.hlc[a:key], ln) let hlc[ln + a:shift] = t:DChar.hlc[a:key][ln] unlet t:DChar.hlc[a:key][ln] endif if has_key(t:DChar.cks[a:key], ln) let cks[ln + a:shift] = t:DChar.cks[a:key][ln] unlet t:DChar.cks[a:key][ln] endif endfor call extend(t:DChar.mid[a:key], lid) call extend(t:DChar.hlc[a:key], hlc) call extend(t:DChar.cks[a:key], cks) endfunction function! s:GetLineColCnr() abort let lcc = {} for k in [1, 2] call win_execute(t:DChar.wid[k], 'let lcc[k] = \#{tl: line("w0"), bl: line("w$"), ll: line("$"), \cl: line("."), cc: col("."), cn: changenr()}') if lcc[k].bl < lcc[k].ll && (s:VF.ScreenPos ? \screenpos(t:DChar.wid[k], lcc[k].bl + 1, 1).row != 0 : \&display =~ 'lastline\|truncate' && getwinvar(t:DChar.wid[k], '&wrap')) let lcc[k].bl += 1 endif endfor return lcc endfunction function! s:UpdateDiffChar() abort if mode(1) != 'n' || !exists('t:DChar') || \len(filter(gettabinfo(tabpagenr())[0].windows, \'getwinvar(v:val, "&diff") && \index(values(t:DChar.wid), v:val) != -1')) != 2 return endif let lcc = t:DChar.lcc | let t:DChar.lcc = s:GetLineColCnr() let ak = 2 while 0 < ak && lcc[ak].cn == t:DChar.lcc[ak].cn | let ak -= 1 | endwhile if 0 < ak " when text changed, find DChar lines to delete/add/shift, and " to leave undeleted/unadded let NoUpdate = {pl, cl -> get(t:DChar.cks[ak], pl, '') == \s:ChecksumStr(getbufline(t:DChar.bnr[ak], cl)[0]) && \(has('nvim') || 0 < min(get(t:DChar.mid[ak], pl, [])))} let bk = (ak == 1) ? 2 : 1 let lnd = t:DChar.lcc[ak].ll - lcc[ak].ll let pfl = t:DChar.dfl let cfl = s:FocusDiffLines(0) let m = min([len(pfl[ak]), len(cfl[ak])]) let s = 0 while s < m && [pfl[ak][s], pfl[bk][s]] == [cfl[ak][s], cfl[bk][s]] && \NoUpdate(pfl[ak][s], cfl[ak][s]) let s += 1 endwhile let m -= s let e = -1 while e >= -m && [pfl[ak][e] + lnd, pfl[bk][e]] == \[cfl[ak][e], cfl[bk][e]] && NoUpdate(pfl[ak][e], cfl[ak][e]) let e -= 1 endwhile let ddl = pfl[ak][s : e] let adl = cfl[ak][s : e] if lnd != 0 && e < -1 let sdl = pfl[ak][e + 1 :] else let sdl = [] for d in range(len(ddl) - 1, 0, -1) let a = index(adl, ddl[d]) if a != -1 && pfl[bk][s + d] == cfl[bk][s + a] && \NoUpdate(ddl[d], adl[a]) unlet ddl[d] unlet adl[a] endif endfor endif if 0 < t:DChar.dpv.pv | call s:ClearDiffCharPair(ak) | endif if !empty(ddl) call win_execute(t:DChar.wid[ak], 'call s:ResetDiffChar(ddl)') endif let t:DChar.dfl = cfl if !empty(sdl) | call s:ShiftDiffChar(ak, sdl, lnd) | endif if !empty(adl) call win_execute(t:DChar.wid[ak], 'call s:ShowDiffChar(adl)') endif else " when diffupdate invoked or diffopt changed, delete all previous and " then add all current DChar lines if some of DChar option is changed let opt = s:GetDiffCharOptions() if opt != t:DChar.opt let k = (t:DChar.wid[1] == win_getid()) ? 1 : 2 call s:ResetDiffChar(t:DChar.dfl[k]) if opt.ut != t:DChar.opt.ut let t:DChar.upa = s:GetDiffSplitRegExp(opt.ut) endif if opt.pv != t:DChar.opt.pv let t:DChar.dpv = s:GetDiffPairVisible(opt.pv) endif if opt.cl != t:DChar.opt.cl let t:DChar.hgp = s:GetDiffUnitHL(opt.cl) endif let t:DChar.opt = opt let t:DChar.dfl = s:FocusDiffLines(0) call s:ShowDiffChar(t:DChar.dfl[k]) endif endif endfunction function! diffchar#JumpDiffChar(dp) abort " a:dp : 0=backward/start, 1=forward/start, 2=backward/end, 3=forward/end if !exists('t:DChar') | return | endif let k = 2 | while t:DChar.wid[k] != win_getid() | let k -= 1 if k == 0 | return | endif endwhile let [dir, pos] = (a:dp == 0) ? [0, 0] : (a:dp == 1) ? [1, 0] : \(a:dp == 2) ? [0, -1] : [1, -1] let [ln, co] = [line('.'), col('.')] if co == col('$') " empty line if !dir | let co = 0 | endif else if pos != 0 let co += len(strcharpart( \getbufline(t:DChar.bnr[k], ln)[0][co - 1 :], 0, 1)) - 1 endif endif if has_key(t:DChar.hlc[k], ln) && \(dir ? co < t:DChar.hlc[k][ln][-1][1][pos] : \co > t:DChar.hlc[k][ln][0][1][pos]) let co = filter(map(copy(t:DChar.hlc[k][ln]), 'v:val[1][pos]'), \dir ? 'co < v:val' : 'co > v:val')[dir ? 0 : -1] else let dl = s:SearchDiffLines(dir, dir ? ln + 1 : ln - 1, 1) if empty(dl) | return | endif if t:DChar.opt.lm | let lp = ln | endif let ln = dl[0] | let lx = ln while 1 " go up/down and check on-screen diff lines, linematch can realign if dir ? t:DChar.lcc[k].ll < lx : lx < 1 | return | endif noautocmd call cursor(lx, 0) | call s:ScrollDiffLines(k) if has_key(t:DChar.hlc[k], ln) | break | endif if t:DChar.opt.lm let dl = filter(sort(map(keys(t:DChar.hlc[k]), 'eval(v:val)'), 'n'), \dir ? 'lp < v:val' : 'lp > v:val') if !empty(dl) | let ln = dir ? dl[0] : dl[-1] | break | endif endif let lx = dir ? max([lx, t:DChar.lcc[k].bl]) + 1 : \min([lx, t:DChar.lcc[k].tl]) - 1 endwhile let co = t:DChar.hlc[k][ln][dir ? 0 : -1][1][pos] endif " set a dummy cursor position to adjust the start/end if 0 < t:DChar.dpv.pv call s:ClearDiffCharPair(k) if a:dp == 1 " forward/start : rightmost let [t:DChar.lcc[k].cl, t:DChar.lcc[k].cc] = [ln, col('$')] elseif a:dp == 2 " backward/end : leftmost let [t:DChar.lcc[k].cl, t:DChar.lcc[k].cc] = [ln, 0] endif endif call cursor(ln, co) endfunction function! diffchar#CopyDiffCharPair(dir) abort " a:dir : 0 = get, 1 = put if !exists('t:DChar') | return | endif let ak = 2 | while t:DChar.wid[ak] != win_getid() | let ak -= 1 if ak == 0 | return | endif endwhile let bk = (ak == 1) ? 2 : 1 let un = -1 if 0 < t:DChar.dpv.pv if !empty(t:DChar.dpv.ch) | let [al, un] = t:DChar.dpv.ch.lc | endif else let [al, co] = [line('.'), col('.')] if co == col('$') | let co = 0 | endif if has_key(t:DChar.hlc[ak], al) let hc = filter(map(copy(t:DChar.hlc[ak][al]), '[v:key, v:val[1]]'), \'v:val[1][0] <= co && co <= v:val[1][-1]') if !empty(hc) | let un = hc[0][0] | endif endif endif if un == -1 call s:EchoWarning('Cursor is not on a difference unit!') return endif let bl = t:DChar.dfl[bk][index(t:DChar.dfl[ak], al)] let et = #{a: {}, b: {}} let [et.a.e, et.a.c] = t:DChar.hlc[ak][al][un] let [et.b.e, et.b.c] = t:DChar.hlc[bk][bl][un] let [et.a.t, et.b.t] = [getbufline(t:DChar.bnr[ak], al)[0], \getbufline(t:DChar.bnr[bk], bl)[0]] let [x, y] = a:dir ? ['b', 'a'] : ['a', 'b'] let s1 = (1 < et[x].c[0]) ? et[x].t[: et[x].c[0] - 2] : '' let s2 = (et[x].e != 'a') ? et[y].t[et[y].c[0] - 1 : et[y].c[-1] - 1] : '' if et[x].e == 'd' && [et[x].c[0], et[x].c[-1]] != [0, 0] let ds = split(et[x].t[et[x].c[0] - 1 : et[x].c[-1] - 1], '\zs') let s2 = ((1 < et[y].c[0]) ? ds[0] : '') . s2 . \((et[y].c[-1] < len(et[y].t)) ? ds[-1] : '') endif let s3 = (et[x].c[-1] < len(et[x].t)) ? et[x].t[et[x].c[-1] :] : '' let ss = s1 . s2 . s3 if a:dir call setbufline(t:DChar.bnr[bk], bl, ss) call win_execute(t:DChar.wid[bk], 'let &undolevels = &undolevels') else call setbufline(t:DChar.bnr[ak], al, ss) endif endfunction function! s:ToggleDiffCharPair(on) abort if t:DChar.dpv.pv == 3 || t:DChar.dpv.pv == 4 let on = (a:on && empty(t:DChar.dpv.pw)) ? 1 : \(!a:on && !empty(t:DChar.dpv.pw)) ? 0 : -1 if on != -1 | call s:SetPopupWindow(on) | endif endif endfunction function! s:ShowDiffCharPair(key) abort if mode(1) != 'n' || !exists('t:DChar') | return | endif let [pl, pc] = [t:DChar.lcc[a:key].cl, t:DChar.lcc[a:key].cc] let [cl, cc] = [line('.'), col('.')] if cc == col('$') | let cc = 0 | endif let [t:DChar.lcc[a:key].cl, t:DChar.lcc[a:key].cc] = [cl, cc] if t:DChar.lcc[a:key].cn == changenr() if !empty(t:DChar.dpv.ch) if t:DChar.dpv.ch.bk == a:key " clear if a pair accidentally remains on diffsplit call s:ClearDiffCharPair((a:key == 1) ? 2 : 1) else let [hl, hi] = t:DChar.dpv.ch.lc let hc = t:DChar.hlc[a:key][hl][hi][1] if cl == hl && hc[0] <= cc && cc <= hc[-1] | return | endif call s:ClearDiffCharPair(a:key) " outside, clear it endif endif if has_key(t:DChar.hlc[a:key], cl) let hu = filter(map(copy(t:DChar.hlc[a:key][cl]), '[v:key, v:val[1]]'), \'v:val[1][0] <= cc && cc <= v:val[1][-1]') if !empty(hu) " for 2 continuous 'd', check if cursor moved forward/backward let ix = (len(hu) == 1) ? 0 : (cl == pl) ? cc < pc : cl < pl call s:HighlightDiffCharPair(a:key, cl, hu[ix][0]) endif endif endif endfunction function! s:HighlightDiffCharPair(key, line, col) abort let [ak, bk] = (a:key == 1) ? [1, 2] : [2, 1] let [al, bl] = [a:line, t:DChar.dfl[bk][index(t:DChar.dfl[ak], a:line)]] let t:DChar.dpv.ch = #{lc: [al, a:col], bk: bk, id: 0} let ae = t:DChar.hlc[ak][al][a:col][0] let bc = t:DChar.hlc[bk][bl][a:col][1] if [bc[0], bc[-1]] != [0, 0] if t:DChar.opt.dv == 2 && ae == 'a' let id = t:DChar.mid[bk][bl][a:col] let t:DChar.mid[bk][bl][a:col] = \s:ChangeVirtColumn(t:DChar.bnr[bk], id, s:DCharHL.c) let t:DChar.dpv.ch.id = t:DChar.mid[bk][bl][a:col] else let [pos, len] = [bc[0], bc[-1] - bc[0] + 1] let t:DChar.dpv.ch.id = matchaddpos(s:DCharHL.c, [[bl, pos, len]], \-1, -1, #{window: t:DChar.wid[bk]}) endif endif call execute('autocmd! diffchar WinLeave call s:ClearDiffCharPair(' . ak . ')') if t:DChar.dpv.pv < 2 | return | endif let at = getbufline(t:DChar.bnr[ak], al)[0] let bt = getbufline(t:DChar.bnr[bk], bl)[0] if ae == 'c' let hl = t:DChar.hgp[(count(map(t:DChar.hlc[ak][al][: a:col], 'v:val[0]'), \'c') - 1) % len(t:DChar.hgp)] let [tb, tx, te] = ['', bt[bc[0] - 1 : bc[-1] - 1], ''] elseif ae == 'd' let hl = s:DCharHL.A let [tb, tx, te] = [(1 < bc[0]) ? '<' : '', bt[bc[0] - 1 : bc[-1] - 1], \(bc[-1] < len(bt)) ? '>' : ''] elseif ae == 'a' let hl = s:DCharHL.D let [tb, tx, te] = ['>', '', '<'] endif if t:DChar.dpv.pv == 2 call execute(['echon tb', 'echohl ' . hl, 'echon tx', 'echohl None', \'echon te'], '') elseif t:DChar.dpv.pv == 3 || t:DChar.dpv.pv == 4 call s:HighlightPopupWindow(tb . tx . te) endif endfunction function! s:ClearDiffCharPair(key) abort if !exists('t:DChar') | return | endif if !empty(t:DChar.dpv.ch) let [bk, id] = [t:DChar.dpv.ch.bk, t:DChar.dpv.ch.id] if win_id2win(t:DChar.wid[bk]) != 0 if 0 < id silent! call matchdelete(id, t:DChar.wid[bk]) elseif id < 0 let [al, bi] = t:DChar.dpv.ch.lc let bl = t:DChar.dfl[bk][index(t:DChar.dfl[(bk == 1) ? 2 : 1], al)] let t:DChar.mid[bk][bl][bi] = \s:ChangeVirtColumn(t:DChar.bnr[bk], id, s:DCharHL.D) endif endif call execute('autocmd! diffchar WinLeave ') let t:DChar.dpv.ch = {} endif if t:DChar.dpv.pv == 2 | call execute('echo', '') elseif t:DChar.dpv.pv == 3 || t:DChar.dpv.pv == 4 call s:ClearPopupWindow() endif endfunction if has('nvim') function! s:SetPopupWindow(on) abort if a:on let t:DChar.dpv.pw.fb = nvim_create_buf(0, 1) let t:DChar.dpv.pw.fw = nvim_open_win(t:DChar.dpv.pw.fb, 0, \#{relative: 'editor', row: 0, col: 0, height: 1, width: 1, \focusable: 0, style: 'minimal'}) call setbufline(t:DChar.dpv.pw.fb, 1, '') call setwinvar(t:DChar.dpv.pw.fw, '&winblend', 100) call setwinvar(t:DChar.dpv.pw.fw, '&winhighlight', \'Normal:' . s:DCharHL.c) else call nvim_win_close(t:DChar.dpv.pw.fw, 1) call nvim_buf_delete(t:DChar.dpv.pw.fb, #{force: 1}) let t:DChar.dpv.pw = {} endif endfunction function! s:HighlightPopupWindow(tx) abort if t:DChar.dpv.pv == 4 | let mp = getmousepos() | endif call nvim_win_set_config(t:DChar.dpv.pw.fw, \extend((t:DChar.dpv.pv == 3) ? #{relative: 'cursor', row: 1, col: 0} : \#{relative: 'editor', row: mp.screenrow, col: mp.screencol}, \#{width: strdisplaywidth(a:tx)})) call setbufline(t:DChar.dpv.pw.fb, 1, a:tx) call setwinvar(t:DChar.dpv.pw.fw, '&winblend', 0) endfunction function! s:ClearPopupWindow() abort call nvim_win_set_config(t:DChar.dpv.pw.fw, \#{relative: 'editor', row: 0, col: 0, width: 1}) call setbufline(t:DChar.dpv.pw.fb, 1, '') call setwinvar(t:DChar.dpv.pw.fw, '&winblend', 100) endfunction function! s:SetVirtColumn() abort let s:DCharNS = nvim_create_namespace('diffchar') endfunction function! s:HighlightVirtColumn(bn, ln, co, hl) abort return -nvim_buf_set_extmark(a:bn, s:DCharNS, a:ln - 1, a:co - 1, \#{virt_text: [[t:DChar.opt.vc, a:hl]], \virt_text_pos: 'inline', invalidate: v:true}) endfunction function! s:ClearVirtColumn(bn, id) abort call nvim_buf_del_extmark(a:bn, s:DCharNS, -a:id) endfunction function! s:PurgeVirtColumn(bn, ln) abort endfunction function! s:ChangeVirtColumn(bn, id, hl) abort let lc = nvim_buf_get_extmark_by_id(a:bn, s:DCharNS, -a:id, {}) if !empty(lc) call s:ClearVirtColumn(a:bn, a:id) return s:HighlightVirtColumn(a:bn, lc[0] + 1, lc[1] + 1, a:hl) endif return a:id endfunction else function! s:SetPopupWindow(on) abort if a:on let t:DChar.dpv.pw = popup_create('', #{hidden: 1, scrollbar: 0, wrap: 0, \highlight: s:DCharHL.c}) else let t:DChar.dpv.pw = popup_close(t:DChar.dpv.pw) endif endfunction function! s:HighlightPopupWindow(tx) abort if t:DChar.dpv.pv == 4 | let mp = getmousepos() | endif call popup_move(t:DChar.dpv.pw, (t:DChar.dpv.pv == 3) ? \#{line: 'cursor+1', col: 'cursor'} : \#{line: mp.screenrow, col: mp.screencol}) call popup_settext(t:DChar.dpv.pw, a:tx) call popup_show(t:DChar.dpv.pw) endfunction function! s:ClearPopupWindow() abort call popup_hide(t:DChar.dpv.pw) endfunction function! s:SetVirtColumn() abort for hl in [s:DCharHL.c, s:DCharHL.D] call call(empty(prop_type_get(hl)) ? \'prop_type_add' : 'prop_type_change', [hl, #{highlight: hl}]) endfor endfunction function! s:HighlightVirtColumn(bn, ln, co, hl) abort return prop_add(a:ln, a:co, #{bufnr: a:bn, type: a:hl, \text: t:DChar.opt.vc}) endfunction function! s:ClearVirtColumn(bn, id) abort call prop_remove(#{bufnr: a:bn, id: a:id}) endfunction function! s:PurgeVirtColumn(bn, ln) abort for hl in [s:DCharHL.c, s:DCharHL.D] call prop_remove(#{bufnr: a:bn, type: hl, all: 1}, a:ln) endfor endfunction function! s:ChangeVirtColumn(bn, id, hl) abort let pr = prop_find(#{bufnr: a:bn, id: a:id, lnum: 1, col: 1}) if !empty(pr) call s:ClearVirtColumn(a:bn, a:id) return s:HighlightVirtColumn(a:bn, pr.lnum, pr.col, a:hl) endif return a:id endfunction endif function! diffchar#ToggleDiffModeSync(...) abort " a:0 : 0 = OptionSet diff, 1 = VimEnter if !exists('t:DChar') && !s:GetOptionVar('DiffChar', 1) | return | endif if a:0 || v:option_old != v:option_new let cw = win_getid() if exists('t:DChar') && ((a:0 || v:option_new) ? \index(values(t:DChar.bnr), winbufnr(cw)) == -1 : \index(values(t:DChar.wid), cw) != -1) " diff mode ON on non-DChar buf || OFF on DChar win, try reset let dk = filter([1, 2], 'getwinvar(t:DChar.wid[v:val], "&diff")') if !empty(dk) if empty(filter(copy(dk), 't:DChar.wid[v:val] == cw')) let cw = t:DChar.wid[dk[0]] endif call win_execute(cw, 'call s:ResetDiffChar()') endif endif if !exists('t:DChar') let aw = win_id2win(cw) let dw = filter(map(range(aw, winnr('$')) + range(1, aw - 1), \'win_getid(v:val)'), 'getwinvar(v:val, "&diff")') if 1 < len(dw) " 2 or more diff mode wins exists, try show call win_execute(dw[0], 'call s:ShowDiffChar()') endif endif endif endfunction function! s:WinClosedDiffChar() abort " reset and show (if possible) DChar on WinClosed for ti in filter(gettabinfo(), 'has_key(v:val.variables, "DChar")') let dc = ti.variables.DChar for k in [1, 2] if dc.wid[k] == eval(expand('')) call win_execute(dc.wid[k], 'call s:ResetDiffChar()') let dw = filter(ti.windows, 'v:val != dc.wid[k] && \winbufnr(v:val) == dc.bnr[k] && getwinvar(v:val, "&diff")') if !empty(dw) call win_execute(dw[0], 'call s:ShowDiffChar()') endif return endif endfor endfor endfunction function! s:RepairDiffChar() abort " repair DChar whose win was accidentally closed on BufWinEnter/WinEnter if exists('t:DChar') let dc = t:DChar let dw = filter(copy(dc.wid), 'win_id2win(v:val) != 0 && \winbufnr(v:val) == dc.bnr[v:key] && getwinvar(v:val, "&diff")') if len(dw) == 1 call win_execute(values(dw)[0], ['call s:ResetDiffChar()', \'call s:ShowDiffChar()']) endif endif endfunction function! s:EchoWarning(msg) abort call timer_start(0, {-> execute(['echohl WarningMsg', \'echo "[diffchar]" ' . string(a:msg), 'echohl None'], '')}) endfunction if !has('vim9script') || !get(g:, 'vim9script', 1) function! s:TraceDiffChar(u1, u2, ih) abort " An O(NP) Sequence Comparison Algorithm let [u1, u2, eq, e1, e2] = [a:u1, a:u2, '=', '-', '+'] let [n1, n2] = [len(u1), len(u2)] if u1 ==# u2 | return repeat(eq, n1) elseif n1 == 0 | return repeat(e2, n2) elseif n2 == 0 | return repeat(e1, n1) endif let [N, M, u1, u2] = (n1 >= n2) ? [n1, n2, u1, u2] : [n2, n1, u2, u1] if n1 < n2 | let [e1, e2] = [e2, e1] | endif let D = N - M let fp = repeat([-1], M + N + 1) let etree = [] " [next edit, previous p, previous k] let p = -1 while fp[D] != N let p += 1 let epk = repeat([[]], p * 2 + D + 1) for k in range(-p, D - 1, 1) + range(D + p, D, -1) let [y, epk[k]] = (fp[k - 1] + 1 > fp[k + 1]) ? \[fp[k - 1] + 1, [e1, [(k > D) ? p - 1 : p, k - 1]]] : \[fp[k + 1], [e2, [(k < D) ? p - 1 : p, k + 1]]] let x = y - k while x < M && y < N && u2[x] ==# u1[y] let epk[k][0] .= eq | let [x, y] += [1, 1] endwhile let fp[k] = y endfor let etree += [epk] endwhile let ses = '' while 1 let ses = etree[p][k][0] . ses if [p, k] == [0, 0] | break | endif let [p, k] = etree[p][k][1] endwhile let ses = ses[1 :] return a:ih ? s:ReduceDiffHunk(a:u1, a:u2, ses) : ses endfunction function! s:ApplyDiffFunc(u1, u2, ih) abort let [eq, e1, e2] = ['=', '-', '+'] let [n1, n2] = [len(a:u1), len(a:u2)] if a:u1 ==# a:u2 | return repeat(eq, n1) elseif n1 == 0 | return repeat(e2, n2) elseif n2 == 0 | return repeat(e1, n1) endif let ses = '' let vd = s:DiffFunc(a:u1, a:u2) if !empty(vd) let p1 = 0 for [i1, c1, i2, c2] in vd + [[n1, 0, 0, 0]] let ses .= repeat(eq, i1 - p1) . repeat(e1, c1) . repeat(e2, c2) let p1 = i1 + c1 endfor endif return a:ih ? s:ReduceDiffHunk(a:u1, a:u2, ses) : ses endfunction function! s:ReduceDiffHunk(u1, u2, ses) abort " in ==++++/==----, if == units equal to last ++/-- units, swap their SESs " (AB vs AxByAB : =+=+++ -> =++++= -> ++++==) let [eq, e1, e2] = ['=', '-', '+'] let [p1, p2] = [-1, -1] | let ses = '' | let ez = '' for ed in reverse(split(a:ses, '[+-]\+\zs')) let es = ed . ez | let ez = '' | let qe = count(es, eq) if 0 < qe let [q1, q2] = [count(es, e1), count(es, e2)] let [uu, pp, qq] = (qe <= q1 && q2 == 0) ? [a:u1, p1, q1] : \(q1 == 0 && qe <= q2) ? [a:u2, p2, q2] : [[], 0, 0] if !empty(uu) && uu[pp - qq - qe + 1 : pp - qq] ==# uu[pp - qe + 1 : pp] let ez = es[-qe :] . es[qe : -qe - 1] | let es = es[: qe - 1] else let [p1, p2] -= [q1, q2] endif endif let [p1, p2] -= [qe, qe] let ses = es . ses endfor let ses = ez . ses return ses endfunction if has('nvim') function! s:DiffFunc(u1, u2) abort return map(v:lua.vim.diff(join(a:u1, "\n") . "\n", join(a:u2, "\n") . "\n", \#{result_type: 'indices'}), \'[v:val[0] - ((0 < v:val[1]) ? 1 : 0), v:val[1], \v:val[2] - ((0 < v:val[3]) ? 1 : 0), v:val[3]]') endfunction else function! s:DiffFunc(u1, u2) abort return map(diff(a:u1, a:u2, #{output: 'indices'}), \'[v:val.from_idx, v:val.from_count, v:val.to_idx, v:val.to_count]') endfunction endif function! s:GetDiffUnitPos(uu, es) abort if empty(a:uu[0]) return [[['d', [0, 0]]], [['a', [1, len(join(a:uu[1], ''))]]]] elseif empty(a:uu[1]) return [[['a', [1, len(join(a:uu[0], ''))]]], [['d', [0, 0]]]] endif let cc = [[], []] | let ll = [1, 1] | let pp = [0, 0] for ed in split(a:es, '[+-]\+\zs', 1)[: -2] let qe = count(ed, '=') | let qq = [count(ed, '-'), count(ed, '+')] let ee = (qq[0] == 0) ? ['d', 'a'] : (qq[1] == 0) ? ['a', 'd'] : \['c', 'c'] for k in [0, 1] if 0 < qe let [ll[k], pp[k]] += \[len(join(a:uu[k][pp[k] : pp[k] + qe - 1], '')), qe] endif if 0 < qq[k] let hh = [ll[k]] let [ll[k], pp[k]] += \[len(join(a:uu[k][pp[k] : pp[k] + qq[k] - 1], '')), qq[k]] let hh += [ll[k] - 1] else let hh = [ll[k] - ((0 < pp[k]) ? \len(matchstr(a:uu[k][pp[k] - 1], '.$')) : 0), \ll[k] + ((pp[k] < len(a:uu[k])) ? \len(matchstr(a:uu[k][pp[k]], '^.')) : 0) - 1] endif if t:DChar.opt.dv == 2 call extend(hh, [(0 < qq[k]) ? 0 : ll[k]], 1) endif let cc[k] += [[ee[k], hh]] endfor endfor return cc endfunction function! s:HighlightDiffChar(lec) abort let hn = len(t:DChar.hgp) for k in [1, 2] for [ln, ec] in items(a:lec[k]) if has_key(t:DChar.mid[k], ln) | continue | endif if t:DChar.opt.dv == 2 call s:PurgeVirtColumn(t:DChar.bnr[k], ln) endif let t:DChar.mid[k][ln] = [] let t:DChar.hlc[k][ln] = ec let cn = 0 for [ed, co] in ec if ed == 'c' let hl = t:DChar.hgp[cn % hn] | let cn += 1 elseif ed == 'a' let hl = s:DCharHL.A elseif ed == 'd' if t:DChar.opt.dv == 0 || [co[0], co[-1]] == [0, 0] continue endif let hl = s:DCharHL.E endif let t:DChar.mid[k][ln] += [(t:DChar.opt.dv == 2 && ed == 'd') ? \s:HighlightVirtColumn(t:DChar.bnr[k], ln, co[1], s:DCharHL.D) : \matchaddpos(hl, [[ln, co[0], co[-1] - co[0] + 1]], -3, -1, \#{window: t:DChar.wid[k]})] endfor let t:DChar.mid[k][ln] += [matchaddpos(s:DCharHL.C, [[ln]], -5, -1, \#{window: t:DChar.wid[k]})] endfor endfor endfunction function! s:ClearDiffChar(lines) abort for k in [1, 2] let wd = win_id2win(t:DChar.wid[k]) for ln in a:lines[k] if wd != 0 for id in t:DChar.mid[k][ln] if 0 < id silent! call matchdelete(id, t:DChar.wid[k]) elseif id < 0 call s:ClearVirtColumn(t:DChar.bnr[k], id) endif endfor if t:DChar.opt.dv == 2 call s:PurgeVirtColumn(t:DChar.bnr[k], ln) endif endif unlet t:DChar.mid[k][ln] unlet t:DChar.hlc[k][ln] endfor endfor endfunction function! s:ScrollDiffLines(key) abort " called on WinScrolled or other if !exists('t:DChar') | return | endif if 0 < a:key | let ak = a:key else let wid = eval(expand('')) let ak = 2 | while t:DChar.wid[ak] != wid | let ak -= 1 if ak == 0 | return | endif endwhile endif let lcc = s:GetLineColCnr() let sk = 0 for k in [1, 2] " check if a scroll happens in either window with no change on both let sk += (t:DChar.lcc[k].cn != lcc[k].cn) ? -k * 10 : \(lcc[k].tl < t:DChar.lcc[k].tl || t:DChar.lcc[k].bl < lcc[k].bl) && \(empty(t:DChar.dfl[k]) || lcc[k].tl < t:DChar.dfl[k][0] || \t:DChar.dfl[k][-1] < lcc[k].bl) ? k : 0 let [t:DChar.lcc[k].tl, t:DChar.lcc[k].bl, t:DChar.lcc[k].cl] = \[lcc[k].tl, lcc[k].bl, lcc[k].cl] endfor if 0 < sk let dfl = s:FocusDiffLines(sk) if t:DChar.dfl != dfl " reset/show DChar lines on dfl changes let [dl, al] = [t:DChar.dfl[ak], dfl[ak]] let ix = map([0, -1], 'index(t:DChar.dfl[ak], dfl[ak][v:val])') if ix != [-1, -1] let [dl, al] = [[], []] if ix[0] != -1 let dl += (0 < ix[0]) ? t:DChar.dfl[ak][: ix[0] - 1] : [] else let al = dfl[ak][: index(dfl[ak], t:DChar.dfl[ak][0]) - 1] endif if ix[1] != -1 let dl += (ix[1] < len(t:DChar.dfl[ak])) ? \t:DChar.dfl[ak][ix[1] + 1 :] : [] else let al = dfl[ak][index(dfl[ak], t:DChar.dfl[ak][-1]) + 1 :] endif endif if !empty(dl) call win_execute(t:DChar.wid[ak], 'call s:ResetDiffChar(dl)') endif let t:DChar.dfl = dfl if !empty(al) call win_execute(t:DChar.wid[ak], 'call s:ShowDiffChar(al)') endif endif endif endfunction function! s:FocusDiffLines(key) abort " a:key : initiate dfl (0), either (1/2) or both (3) key using existing dfl let dfl = {} if t:DChar.opt.fp == 0 if a:key == 0 for k in [1, 2] call win_execute(t:DChar.wid[k], 'let dfl[k] = \s:GetDiffLines(1, t:DChar.lcc[k].ll)') endfor return dfl else return t:DChar.dfl endif endif " select specified win or more line displaying win as main let tb = (a:key == 1) ? [1, 0] : (a:key == 2) ? [0, 1] : \map([1, 2], 't:DChar.lcc[v:val].bl - t:DChar.lcc[v:val].tl') let [ak, bk] = (tb[0] >= tb[1]) ? [1, 2] : [2, 1] " get visible and upper/lower dfl in main win let [tl, bl] = [t:DChar.lcc[ak].tl, t:DChar.lcc[ak].bl] call win_execute(t:DChar.wid[ak], 'let dfl[ak] = s:GetDiffLines(tl, bl)') let [tx, bx] = [1, t:DChar.lcc[ak].ll] if 0 < t:DChar.opt.fp && 0 < a:key && !empty(t:DChar.dfl[ak]) if tl > t:DChar.dfl[ak][0] | let tx = t:DChar.dfl[ak][-1] | endif if bl < t:DChar.dfl[ak][-1] | let bx = t:DChar.dfl[ak][0] | endif endif let [tl, bl] += [-1, 1] if 1 < abs(t:DChar.opt.fp) let [tz, bz] = [[], []] let rc = winheight(t:DChar.wid[ak]) * (abs(t:DChar.opt.fp) - 1) while 0 < rc let fc = 0 if tx <= tl call win_execute(t:DChar.wid[ak], 'let fc = foldclosed(tl)') if fc == -1 | let tz = [tl] + tz | else | let tl = fc | endif let tl -= 1 | let rc -= 1 endif if bl <= bx call win_execute(t:DChar.wid[ak], 'let fc = foldclosedend(bl)') if fc == -1 | let bz += [bl] | else | let bl = fc | endif let bl += 1 | let rc -= 1 endif if fc == 0 | break | endif endwhile call win_execute(t:DChar.wid[ak], 'let dfl[ak] = \s:CheckDiffLines(tz) + dfl[ak] + s:CheckDiffLines(bz)') endif " if no dfl found in dfp, try to find one toward top/bottom if empty(dfl[ak]) if 0 < a:key | return t:DChar.dfl | endif call win_execute(t:DChar.wid[ak], 'let dfl[ak] = \s:SearchDiffLines(0, tl, 1) + s:SearchDiffLines(1, bl, 1)') if empty(dfl[ak]) | let dfl[bk] = [] | return dfl | endif endif " get dfl in sub win based on the corresponding line between main/sub let ds = #{t: [1, 1, 1, dfl[ak][0] - 1], \b: [0, t:DChar.lcc[bk].ll, dfl[ak][-1] + 1, t:DChar.lcc[ak].ll]} let ix = -1 if 0 < a:key && !empty(t:DChar.dfl[ak]) let ix = index(t:DChar.dfl[ak], dfl[ak][0]) if ix != -1 let [sd, sl, dc] = [1, t:DChar.dfl[bk][ix], 0] else let ix = index(t:DChar.dfl[ak], dfl[ak][-1]) if ix != -1 let [sd, sl, dc] = [0, t:DChar.dfl[bk][ix], 0] else if t:DChar.dfl[ak][-1] < dfl[ak][0] let ds.t = [1, t:DChar.dfl[bk][-1], t:DChar.dfl[ak][-1], \dfl[ak][0] - 1] elseif dfl[ak][-1] < t:DChar.dfl[ak][0] let ds.b = [0, t:DChar.dfl[bk][0], dfl[ak][-1] + 1, \t:DChar.dfl[ak][0]] endif endif endif endif if ix == -1 let [sd, sl, df, dl] = (ds.t[3] - ds.t[2] <= ds.b[3] - ds.b[2]) ? \ds.t : ds.b call win_execute(t:DChar.wid[ak], 'let dc = len(s:GetDiffLines(df, dl))') endif let ac = len(dfl[ak]) call win_execute(t:DChar.wid[bk], 'let dfl[bk] = \s:SearchDiffLines(sd, sl, dc + ac)') let bc = len(dfl[bk]) if ac != bc let [xk, xc] = (ac < bc) ? [bk, ac] : [ak, bc] let dfl[xk] = (xc == 0) ? [] : sd ? dfl[xk][-xc :] : dfl[xk][: xc - 1] endif " repair current dfl and merge with new dfl if 0 < a:key && !empty(t:DChar.dfl[ak]) && t:DChar.dfl != dfl " repair and redraw current on-screen dfl realigned by linematch if t:DChar.opt.lm let dxl = deepcopy(t:DChar.dfl) let dfi = {} for k in [1, 2] let dfi[k] = map(copy(dfl[k]), 'index(dxl[k], v:val)') endfor let [dl, al] = [[], []] if dfi[1] != dfi[2] for fi in range(len(dfi[ak])) let [i1, i2] = [dfi[1][fi], dfi[2][fi]] let [ki, xi] = (i1 == -1 && i2 != -1) ? [1, i2] : \(i1 != -1 && i2 == -1) ? [2, i1] : [0, -1] if ki != 0 let [dl, al] += [[dxl[ak][xi]], [dfl[ak][fi]]] let dxl[ki][xi] = dfl[ki][fi] endif endfor endif if !empty(dl) call win_execute(t:DChar.wid[ak], 'call s:ResetDiffChar(dl)') endif let t:DChar.dfl = dxl if !empty(al) call win_execute(t:DChar.wid[ak], 'call s:ShowDiffChar(al)') endif endif " merge current and new dfls if overlapped or continued if 0 < t:DChar.opt.fp let mx = 0 if t:DChar.dfl[ak][0] <= dfl[ak][0] if t:DChar.dfl[ak][-1] >= dfl[ak][0] | let mx = 1 else call win_execute(t:DChar.wid[ak], \'let nl = s:SearchDiffLines(1, t:DChar.dfl[ak][-1] + 1, 1)') if !empty(nl) && dfl[ak][0] <= nl[0] | let mx = 2 | endif endif elseif dfl[ak][-1] <= t:DChar.dfl[ak][-1] if dfl[ak][-1] >= t:DChar.dfl[ak][0] | let mx = -1 else call win_execute(t:DChar.wid[ak], \'let nl = s:SearchDiffLines(0, t:DChar.dfl[ak][0] - 1, 1)') if !empty(nl) && nl[0] <= dfl[ak][-1] | let mx = -2 | endif endif endif if mx != 0 for k in [1, 2] let dfl[k] = (0 < mx) ? \t:DChar.dfl[k] + filter(dfl[k], 'v:val > t:DChar.dfl[k][-1]') : \filter(dfl[k], 'v:val < t:DChar.dfl[k][0]') + t:DChar.dfl[k] endfor endif endif endif return dfl endfunction function! s:SearchDiffLines(sd, sl, sc) abort " a:sd = direction (1:down, 0:up), a:sl = start line, a:sc = count let dl = [] | let [ln, sc] = [a:sl, a:sc] if a:sd while 0 < sc && ln <= line('$') let zl = [] while len(zl) < sc let fc = foldclosedend(ln) if fc == -1 | let zl += [ln] | else | let ln = fc | endif let ln += 1 endwhile let zl = s:CheckDiffLines(zl) | let sc -= len(zl) | let dl += zl endwhile else while 0 < sc && 1 <= ln let zl = [] while len(zl) < sc let fc = foldclosed(ln) if fc == -1 | let zl = [ln] + zl | else | let ln = fc | endif let ln -= 1 endwhile let zl = s:CheckDiffLines(zl) | let sc -= len(zl) | let dl = zl + dl endwhile endif return dl endfunction function! s:GetDiffLines(sl, el) abort let dl = [] | let ln = a:sl while ln <= a:el let fc = foldclosedend(ln) if fc == -1 | let dl += [ln] | else | let ln = fc | endif let ln += 1 endwhile return s:CheckDiffLines(dl) endfunction function! s:CheckDiffLines(ll) abort " check [0] first, diff_hlID() sometimes fails for the 1st entry of the list return filter([0] + a:ll, \'index([s:DiffHL.C.id, s:DiffHL.T.id], diff_hlID(v:val, 1)) != -1') endfunction function! s:ChecksumStr(str) abort return sha256(a:str)[: 5] endfunction else function! s:DiffCharVim9Functions() abort def! s:TraceDiffChar(u1: list, u2: list, ih: bool): string # An O(NP) Sequence Comparison Algorithm const [eq, n1, n2] = ['=', len(u1), len(u2)] var [e1, e2] = ['-', '+'] if u1 ==# u2 | return repeat(eq, n1) elseif n1 == 0 | return repeat(e2, n2) elseif n2 == 0 | return repeat(e1, n1) endif const [N, M, v1, v2] = (n1 >= n2) ? [n1, n2, u1, u2] : [n2, n1, u2, u1] if n1 < n2 | [e1, e2] = [e2, e1] | endif const D = N - M var fp = repeat([-1], M + N + 1) var etree = [] # [next edit, previous p, previous k] var p = -1 while fp[D] != N p += 1 var epk = repeat([[]], p * 2 + D + 1) for k in range(-p, D - 1, 1) + range(D + p, D, -1) var x: number | var y: number [y, epk[k]] = (fp[k - 1] + 1 > fp[k + 1]) ? [fp[k - 1] + 1, [e1, [(k > D) ? p - 1 : p, k - 1]]] : [fp[k + 1], [e2, [(k < D) ? p - 1 : p, k + 1]]] x = y - k while x < M && y < N && v2[x] ==# v1[y] epk[k][0] ..= eq | [x, y] += [1, 1] endwhile fp[k] = y endfor etree += [epk] endwhile var k = D var ses = '' while 1 ses = etree[p][k][0] .. ses if [p, k] == [0, 0] | break | endif [p, k] = etree[p][k][1] endwhile ses = ses[1 :] return ih ? s:ReduceDiffHunk(u1, u2, ses) : ses enddef def! s:ApplyDiffFunc(u1: list, u2: list, ih: bool): string const [eq, e1, e2] = ['=', '-', '+'] const [n1, n2] = [len(u1), len(u2)] if u1 ==# u2 | return repeat(eq, n1) elseif n1 == 0 | return repeat(e2, n2) elseif n2 == 0 | return repeat(e1, n1) endif var ses = '' var vd = map(diff(u1, u2, {'output': 'indices'}), (_, v) => [v.from_idx, v.from_count, v.to_idx, v.to_count]) if !empty(vd) var p1 = 0 for [i1, c1, i2, c2] in vd + [[n1, 0, 0, 0]] ses ..= repeat(eq, i1 - p1) .. repeat(e1, c1) .. repeat(e2, c2) p1 = i1 + c1 endfor endif return ih ? s:ReduceDiffHunk(u1, u2, ses) : ses enddef def! s:ReduceDiffHunk(u1: list, u2: list, ses: string): string # in ==++++/==----, if == units equal to last ++/-- units, swap their SESs # (AB vs AxByAB : =+=+++ -> =++++= -> ++++==) const [eq, e1, e2] = ['=', '-', '+'] var [p1, p2] = [-1, -1] | var xes = '' | var ez = '' for ed in reverse(split(ses, '[+-]\+\zs')) var es = ed .. ez | ez = '' | const qe = count(es, eq) if 0 < qe const [q1, q2] = [count(es, e1), count(es, e2)] const [uu, pp, qq] = (qe <= q1 && q2 == 0) ? [u1, p1, q1] : (q1 == 0 && qe <= q2) ? [u2, p2, q2] : [[], 0, 0] if !empty(uu) && uu[pp - qq - qe + 1 : pp - qq] ==# uu[pp - qe + 1 : pp] ez = es[-qe :] .. es[qe : -qe - 1] | es = es[: qe - 1] else [p1, p2] -= [q1, q2] endif endif [p1, p2] -= [qe, qe] xes = es .. xes endfor xes = ez .. xes return xes enddef def! s:GetDiffUnitPos(uu: list, es: string): list if empty(uu[0]) return [[['d', [0, 0]]], [['a', [1, len(join(uu[1], ''))]]]] elseif empty(uu[1]) return [[['a', [1, len(join(uu[0], ''))]]], [['d', [0, 0]]]] endif var cc = [[], []] | var ll = [1, 1] | var pp = [0, 0] for ed in split(es, '[+-]\+\zs', 1)[: -2] var qe = count(ed, '=') | var qq = [count(ed, '-'), count(ed, '+')] var ee = (qq[0] == 0) ? ['d', 'a'] : (qq[1] == 0) ? ['a', 'd'] : ['c', 'c'] for k in [0, 1] if 0 < qe [ll[k], pp[k]] += [len(join(uu[k][pp[k] : pp[k] + qe - 1], '')), qe] endif var hh: list if 0 < qq[k] hh = [ll[k]] [ll[k], pp[k]] += [len(join(uu[k][pp[k] : pp[k] + qq[k] - 1], '')), qq[k]] hh += [ll[k] - 1] else hh = [ll[k] - ((0 < pp[k]) ? len(matchstr(uu[k][pp[k] - 1], '.$')) : 0), ll[k] + ((pp[k] < len(uu[k])) ? len(matchstr(uu[k][pp[k]], '^.')) : 0) - 1] endif if t:DChar.opt.dv == 2 extend(hh, [(0 < qq[k]) ? 0 : ll[k]], 1) endif cc[k] += [[ee[k], hh]] endfor endfor return cc enddef def! s:HighlightDiffChar(lec: dict) const hn = len(t:DChar.hgp) for k in [1, 2] for [l, ec] in items(lec[k]) var ln = eval(l) if has_key(t:DChar.mid[k], ln) | continue | endif if t:DChar.opt.dv == 2 s:PurgeVirtColumn(t:DChar.bnr[k], ln) endif t:DChar.mid[k][ln] = [] t:DChar.hlc[k][ln] = ec var hl: string | var cn = 0 for [ed, co] in ec if ed == 'c' hl = t:DChar.hgp[cn % hn] | cn += 1 elseif ed == 'a' hl = s:DCharHL.A elseif ed == 'd' if t:DChar.opt.dv == 0 || [co[0], co[-1]] == [0, 0] continue endif hl = s:DCharHL.E endif t:DChar.mid[k][ln] += [(t:DChar.opt.dv == 2 && ed == 'd') ? s:HighlightVirtColumn(t:DChar.bnr[k], ln, co[1], s:DCharHL.D) : matchaddpos(hl, [[ln, co[0], co[-1] - co[0] + 1]], -3, -1, {'window': t:DChar.wid[k]})] endfor t:DChar.mid[k][ln] += [matchaddpos(s:DCharHL.C, [ln], -5, -1, {'window': t:DChar.wid[k]})] endfor endfor enddef def! s:ClearDiffChar(lines: dict) for k in [1, 2] const wd = win_id2win(t:DChar.wid[k]) for ln in lines[k] if wd != 0 for id in t:DChar.mid[k][ln] if 0 < id silent! matchdelete(id, t:DChar.wid[k]) elseif id < 0 s:ClearVirtColumn(t:DChar.bnr[k], id) endif endfor if t:DChar.opt.dv == 2 s:PurgeVirtColumn(t:DChar.bnr[k], ln) endif endif unlet t:DChar.mid[k][ln] unlet t:DChar.hlc[k][ln] endfor endfor enddef def! s:ScrollDiffLines(key: number) # called on WinScrolled or other if !exists('t:DChar') | return | endif var ak: number if 0 < key | ak = key else const wid = eval(expand('')) ak = 2 | while t:DChar.wid[ak] != wid | ak -= 1 if ak == 0 | return | endif endwhile endif const lcc = s:GetLineColCnr() var sk = 0 for k in [1, 2] # check if a scroll happens in either window with no change on both sk += (t:DChar.lcc[k].cn != lcc[k].cn) ? -k * 10 : (lcc[k].tl < t:DChar.lcc[k].tl || t:DChar.lcc[k].bl < lcc[k].bl) && (empty(t:DChar.dfl[k]) || lcc[k].tl < t:DChar.dfl[k][0] || t:DChar.dfl[k][-1] < lcc[k].bl) ? k : 0 [t:DChar.lcc[k].tl, t:DChar.lcc[k].bl, t:DChar.lcc[k].cl] = [lcc[k].tl, lcc[k].bl, lcc[k].cl] endfor if 0 < sk const dfl = s:FocusDiffLines(sk) if t:DChar.dfl != dfl # reset/show DChar lines on dfl changes var [dl, al] = [t:DChar.dfl[ak], dfl[ak]] const ix = map([0, -1], (_, v) => index(t:DChar.dfl[ak], dfl[ak][v])) if ix != [-1, -1] [dl, al] = [[], []] if ix[0] != -1 dl += (0 < ix[0]) ? t:DChar.dfl[ak][: ix[0] - 1] : [] else al = dfl[ak][: index(dfl[ak], t:DChar.dfl[ak][0]) - 1] endif if ix[1] != -1 dl += (ix[1] < len(t:DChar.dfl[ak])) ? t:DChar.dfl[ak][ix[1] + 1 :] : [] else al = dfl[ak][index(dfl[ak], t:DChar.dfl[ak][-1]) + 1 :] endif endif if !empty(dl) s:WinExec(t:DChar.wid[ak], 's:ResetDiffChar', [dl]) endif t:DChar.dfl = dfl if !empty(al) s:WinExec(t:DChar.wid[ak], 's:ShowDiffChar', [al]) endif endif endif enddef def! s:FocusDiffLines(key: number): dict # a:key : initiate dfl (0), either (1/2) or both (3) key using existing dfl var dfl = {} if t:DChar.opt.fp == 0 if key == 0 for k in [1, 2] dfl[k] = s:WinExec(t:DChar.wid[k], 's:GetDiffLines', [1, t:DChar.lcc[k].ll]) endfor return dfl else return t:DChar.dfl endif endif # select specified win or more line displaying win as main const tb = (key == 1) ? [1, 0] : (key == 2) ? [0, 1] : map([1, 2], (_, v) => t:DChar.lcc[v].bl - t:DChar.lcc[v].tl) const [ak, bk] = (tb[0] >= tb[1]) ? [1, 2] : [2, 1] # get visible and upper/lower dfl in main win var [tl, bl] = [t:DChar.lcc[ak].tl, t:DChar.lcc[ak].bl] dfl[ak] = s:WinExec(t:DChar.wid[ak], 's:GetDiffLines', [tl, bl]) var [tx, bx] = [1, t:DChar.lcc[ak].ll] if 0 < t:DChar.opt.fp && 0 < key && !empty(t:DChar.dfl[ak]) if tl > t:DChar.dfl[ak][0] | tx = t:DChar.dfl[ak][-1] | endif if bl < t:DChar.dfl[ak][-1] | bx = t:DChar.dfl[ak][0] | endif endif [tl, bl] += [-1, 1] if 1 < abs(t:DChar.opt.fp) var [tz, bz] = [[], []] var rc = winheight(t:DChar.wid[ak]) * (abs(t:DChar.opt.fp) - 1) while 0 < rc var fc = 0 if tx <= tl fc = s:WinExec(t:DChar.wid[ak], 'foldclosed', [tl]) if fc == -1 | tz = [tl] + tz | else | tl = fc | endif tl -= 1 | rc -= 1 endif if bl <= bx fc = s:WinExec(t:DChar.wid[ak], 'foldclosedend', [bl]) if fc == -1 | bz += [bl] | else | bl = fc | endif bl += 1 | rc -= 1 endif if fc == 0 | break | endif endwhile dfl[ak] = s:WinExec(t:DChar.wid[ak], 's:CheckDiffLines', [tz]) + dfl[ak] + s:WinExec(t:DChar.wid[ak], 's:CheckDiffLines', [bz]) endif # if no dfl found in dfp, try to find one toward top/bottom if empty(dfl[ak]) if 0 < key | return t:DChar.dfl | endif dfl[ak] = s:WinExec(t:DChar.wid[ak], 's:SearchDiffLines', [0, tl, 1]) + s:WinExec(t:DChar.wid[ak], 's:SearchDiffLines', [1, bl, 1]) if empty(dfl[ak]) | dfl[bk] = [] | return dfl | endif endif # get dfl in sub win based on the corresponding line between main/sub var [sd, sl, dc, fl, ll] = [0, 0, 0, 0, 0] var ds = {'t': [1, 1, 1, dfl[ak][0] - 1], 'b': [0, t:DChar.lcc[bk].ll, dfl[ak][-1] + 1, t:DChar.lcc[ak].ll]} var ix = -1 if 0 < key && !empty(t:DChar.dfl[ak]) ix = index(t:DChar.dfl[ak], dfl[ak][0]) if ix != -1 [sd, sl, dc] = [1, t:DChar.dfl[bk][ix], 0] else ix = index(t:DChar.dfl[ak], dfl[ak][-1]) if ix != -1 [sd, sl, dc] = [0, t:DChar.dfl[bk][ix], 0] else if t:DChar.dfl[ak][-1] < dfl[ak][0] ds.t = [1, t:DChar.dfl[bk][-1], t:DChar.dfl[ak][-1], dfl[ak][0] - 1] elseif dfl[ak][-1] < t:DChar.dfl[ak][0] ds.b = [0, t:DChar.dfl[bk][0], dfl[ak][-1] + 1, t:DChar.dfl[ak][0]] endif endif endif endif if ix == -1 [sd, sl, fl, ll] = (ds.t[3] - ds.t[2] <= ds.b[3] - ds.b[2]) ? ds.t : ds.b dc = len(s:WinExec(t:DChar.wid[ak], 's:GetDiffLines', [fl, ll])) endif const ac = len(dfl[ak]) dfl[bk] = s:WinExec(t:DChar.wid[bk], 's:SearchDiffLines', [sd, sl, dc + ac]) const bc = len(dfl[bk]) if ac != bc var [xk, xc] = (ac < bc) ? [bk, ac] : [ak, bc] dfl[xk] = (xc == 0) ? [] : sd ? dfl[xk][-xc :] : dfl[xk][: xc - 1] endif # repair current dfl and merge with new dfl if 0 < key && !empty(t:DChar.dfl[ak]) && t:DChar.dfl != dfl # repair and redraw current on-screen dfl realigned by linematch if t:DChar.opt.lm var dxl = deepcopy(t:DChar.dfl) var dfi = {} for k in [1, 2] dfi[k] = map(copy(dfl[k]), (_, v) => index(dxl[k], v)) endfor var [dl, al] = [[], []] if dfi[1] != dfi[2] for fi in range(len(dfi[ak])) const [i1, i2] = [dfi[1][fi], dfi[2][fi]] const [ki, xi] = (i1 == -1 && i2 != -1) ? [1, i2] : (i1 != -1 && i2 == -1) ? [2, i1] : [0, -1] if ki != 0 [dl, al] += [[dxl[ak][xi]], [dfl[ak][fi]]] dxl[ki][xi] = dfl[ki][fi] endif endfor endif if !empty(dl) s:WinExec(t:DChar.wid[ak], 's:ResetDiffChar', [dl]) endif t:DChar.dfl = dxl if !empty(al) s:WinExec(t:DChar.wid[ak], 's:ShowDiffChar', [al]) endif endif # merge current and new dfls if overlapped or continued if 0 < t:DChar.opt.fp var nl: list var mx = 0 if t:DChar.dfl[ak][0] <= dfl[ak][0] if t:DChar.dfl[ak][-1] >= dfl[ak][0] | mx = 1 else nl = s:WinExec(t:DChar.wid[ak], 's:SearchDiffLines', [1, t:DChar.dfl[ak][-1] + 1, 1]) if !empty(nl) && dfl[ak][0] <= nl[0] | mx = 2 | endif endif elseif dfl[ak][-1] <= t:DChar.dfl[ak][-1] if dfl[ak][-1] >= t:DChar.dfl[ak][0] | mx = -1 else nl = s:WinExec(t:DChar.wid[ak], 's:SearchDiffLines', [0, t:DChar.dfl[ak][0] - 1, 1]) if !empty(nl) && nl[0] <= dfl[ak][-1] | mx = -2 | endif endif endif if mx != 0 for k in [1, 2] dfl[k] = (0 < mx) ? t:DChar.dfl[k] + filter(dfl[k], (_, v) => v > t:DChar.dfl[k][-1]) : filter(dfl[k], (_, v) => v < t:DChar.dfl[k][0]) + t:DChar.dfl[k] endfor endif endif endif return dfl enddef def! s:SearchDiffLines(sd: number, sl: number, sc: number): list # a:sd = direction (1:down, 0:up), a:sl = start line, a:sc = count var dl = [] | var ln = sl | var sn = sc if sd while 0 < sn && ln <= line('$') var zl = [] while len(zl) < sn var fc = foldclosedend(ln) if fc == -1 | zl += [ln] | else | ln = fc | endif ln += 1 endwhile zl = s:CheckDiffLines(zl) | sn -= len(zl) | dl += zl endwhile else while 0 < sn && 1 <= ln var zl = [] while len(zl) < sn var fc = foldclosed(ln) if fc == -1 | zl = [ln] + zl | else | ln = fc | endif ln -= 1 endwhile zl = s:CheckDiffLines(zl) | sn -= len(zl) | dl = zl + dl endwhile endif return dl enddef def! s:GetDiffLines(sl: number, el: number): list var dl = [] | var ln = sl while ln <= el var fc = foldclosedend(ln) if fc == -1 | dl += [ln] | else | ln = fc | endif ln += 1 endwhile return s:CheckDiffLines(dl) enddef def! s:CheckDiffLines(ll: list): list # check [0] first, diff_hlID() sometimes fails for the 1st entry of the list return filter([0] + ll, (_, v) => index([s:DiffHL.C.id, s:DiffHL.T.id], diff_hlID(v, 1)) != -1) enddef def! s:ChecksumStr(str: string): string return sha256(str)[: 5] enddef def! s:WinExec(wn: number, fn: string, ag: list): any win_execute(wn, $'vim9cmd t:DChar.tmp = call({string(function(fn))}, {string(ag)})') return t:DChar.tmp enddef endfunction call s:DiffCharVim9Functions() endif let &cpoptions = s:save_cpo unlet s:save_cpo " vim: ts=2 sw=0 sts=-1 et ================================================ FILE: doc/diffchar.txt ================================================ *diffchar.txt* Highlight the exact differences, based on characters and words > ____ _ ____ ____ _____ _ _ _____ ____ | | | || || || || | | || _ || _ | | _ || || __|| __|| || | | || | | || | || | | | || || |__ | |__ | __|| |_| || |_| || |_||_ | |_| || || __|| __|| | | || || __ | | || || | | | | |__ | _ || _ || | | | |____| |_||_| |_| |_____||_| |_||_| |_||_| |_| < Last Change: 2025/10/01 Version: 10.0 (on or after vim 9.0 and nvim 0.7.0) Author: Rick Howe (Takumi Ohtani) Copyright: (c) 2014-2025 Rick Howe License: MIT ----------------------------------------------------------------------------- INTRODUCTION *diffchar* This plugin has been developed in order to make diff mode more useful. Vim highlights all the text in between the first and last different characters on a changed line. But this plugin will find the exact differences between them, character by character - so called DiffChar. For example, in diff mode: (`^`:`DiffText`, |#|:|DiffAdd|, |@|:|DiffDelete|) +-------------------------------------------------+ |The `quick brown fox jumps over the lazy` dog. | | `^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^` | |~ | +-------------------------------------------------+ |The `lazy fox jumps over the quick brown` dog. | | `^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^` | |~ | +-------------------------------------------------+ This plugin will exactly show the changed/added/deleted units: +-------------------------------------------------+ |The `quick` |brown| fox jumps over the `lazy` dog. | | `^^^^^` |######| `^^^^` |@| | |~ | +-------------------------------------------------+ |The `lazy` fox jumps over the `quick` |brown| dog. | | `^^^^` |@| `^^^^^` |######| | |~ | +-------------------------------------------------+ Sync with diff mode ~ This plugin will synchronously show/reset the highlights of the exact differences as soon as the diff mode begins/ends. And the exact differences will be kept updated while editing. Note that this plugin does nothing if an "inline" item is set in the 'doffopt' option and its value is other than "simple". On plugin loading, the item is removed only if set by default so that this plugin can work. Diff unit ~ This plugin shows the diffs based on a |g:DiffUnit|. Its default is 'Word1' and it handles a \w\+ word and a \W character as a diff unit. There are other types of word provided and you can also set 'Char' to compare character by character. In addition, you can specify one or more diff unit delimiters, such as comma (','), colon (':'), tab ("\t"), and HTML tag symbols ('<' and '>'), and also specify a custom pattern in the |g:DiffUnit|. Diff matching colors ~ In diff mode, the corresponding |hl-DiffChange| lines are compared between two windows. As a default, all the changed units are highlighted with |hl-DiffText|. You can set |g:DiffColors| to use more than one matching color to make it easy to find the corresponding units between two windows. The number of colors depends on the color scheme. In addition, an added unit is always highlighted with |hl-DiffAdd| and the position of the corresponding deleted unit is shown with bold/underline or a virtual blank column, depending on a |g:DiffDelPosVisible|. Diff pair visible ~ While showing the exact differences, when the cursor is moved on a diff unit, you can see its corresponding unit highlighted with |hl-Cursor|, |hl-TermCursor|, or similar one in another window, based on a |g:DiffPairVisible|. If you change its default, the corresponding unit is echoed in the command line or displayed in a popup/floating window just below the cursor position or at the mouse position. Those options take effect on `:diffupdate` command as well. Jump to next/prev diff unit ~ You can use `]b` or `]e` to jump cursor to start or end position of the next diff unit, and `[b` or `[e` to the start or end position of the previous unit. Get/put a diff unit ~ Like line-based `:diffget`/`:diffput` and `do`/`dp` vim commands, you can use `g` and `p` commands in normal mode to get and put each diff unit, where the cursor is on, between 2 buffers and undo its difference. Those keymaps are configurable in your vimrc and so on. Check diff lines locally ~ When the diff mode begins, this plugin locally checks the |hl-DiffChange| lines in the limited range of the current visible and its upper/lower lines of a window. And each time a cursor is moved on to another range upon scrolling or searching, those diff lines will be checked in that range. Which means, independently of the file size, the number of lines to be checked and then the time consumed are always constant. Tab page individual ~ This plugin works on each tab page individually. You can use a tab page variable (t:), instead of a global one (g:), to specify different options on each tab page. Note that this plugin can not handle more than two diff mode windows in a tab page. If it would happen, to prevent any trouble, all the highlighted units are to be reset in the tab page. Follow 'diffopt' option ~ This plugin supports "icase", "iwhite", "iwhiteall", and "iwhiteeol" in the 'diffopt' option. In addition, when "indent-heuristic" is specified, positioning of the added/deleted diff units is adjusted to reduce the number of diff hunks and make them easier to read. Comparison algorithm ~ To find the exact differences, this plugin uses "An O(NP) Sequence Comparison Algorithm" developed by S.Wu, et al., which always finds an optimum sequence. But it takes time to check a long and dissimilar line. To improve the performance, the algorithm is also implemented in Vim9 script. In addition, if available, this plugin uses a builtin diff function (|diff()| in vim patch-9.1.0071 and Lua |vim.diff()| in nvim 0.6.0) and makes it much faster. See also ~ There are other diff related plugins available: - |spotdiff.vim|: A range and area selectable `:diffthis` to compare partially (https://github.com/rickhowe/spotdiff.vim) - |wrapfiller|: Align each wrapped line virtually between windows (https://github.com/rickhowe/wrapfiller) - |difffilter|: Selectively compare lines as you want in diff mode (https://github.com/rickhowe/difffilter) - |diffunitsyntax|: Highlight word or character based diff units in diff format (https://github.com/rickhowe/diffunitsyntax) ----------------------------------------------------------------------------- OPTIONS *diffchar-options* |g:DiffUnit|, |t:DiffUnit| A type of diff unit 'Char' : any single character 'Word1' : \w\+ word and any \W single character (default) 'Word2' : non-space and space words 'Word3' : \< or \> character class boundaries (set by 'iskeyword') 'word' : see `word` 'WORD' : see `WORD` '[{del}]' : one or more diff unit delimiters (e.g. "[,:\t<>]") '/{pat}/' : a pattern to split into diff units (e.g. '/.\{4}\zs/') |g:DiffColors|, |t:DiffColors| Matching colors for changed units 0 : |hl-DiffText| (default) 1 : |hl-DiffText| + a few (3, 4, ...) highlight groups 2 : |hl-DiffText| + several (7, 8, ...) highlight groups 3 : |hl-DiffText| + many (11, 12, ...) highlight groups 100 : all available highlight groups in random order [{hlg}] : a list of your favorite highlight groups |g:DiffPairVisible|, |t:DiffPairVisible| Visibility of corresponding diff units 0 : disable 1 : highlight with |hl-Cursor| (default) 2 : highlight with |hl-Cursor| + echo in the command line 3 : highlight with |hl-Cursor| + popup/floating window at cursor position 4 : highlight with |hl-Cursor| + popup/floating window at mouse position |g:DiffDelPosVisible|, |t:DiffDelPosVisible| Visibility of the position of deleted units 0 : disable 1 : highlight previous/next chars of a deleted unit in bold/underline (default if inline |virtual-text| is not available) 2 : virtually show a blank column (set by "space" item in 'listchars') with |hl-DiffDelete| (default if inline |virtual-text| is available) ----------------------------------------------------------------------------- KEYMAPS *diffchar-keymaps* JumpDiffCharPrevStart (default: `[b`) Jump cursor to the start position of the previous diff unit JumpDiffCharNextStart (default: `]b`) Jump cursor to the start position of the next diff unit JumpDiffCharPrevEnd (default: `[e`) Jump cursor to the end position of the previous diff unit JumpDiffCharNextEnd (default: `]e`) Jump cursor to the end position of the next diff unit GetDiffCharPair (default: `g`) Get a corresponding diff unit from another buffer to undo difference PutDiffCharPair (default: `p`) Put a corresponding diff unit to another buffer to undo difference ----------------------------------------------------------------------------- CHANGE HISTORY *diffchar-history* 10.0 * Added |g:DiffDelPosVisible| option to show the position of deleted units. * Implemented to remove an "inline" item from the 'diffopt' option on initial loading so that this plugin can work. * Made this plugin available on or after vim 9.0 and nvim 0.7.0. 9.9 * Improved to remain previous diff lines highlighted upon scrolling if possible and not to check them later again. * Implemented a few more functions in Vim9 script to make them faster. 9.8 * Improved to follow "linematch", which is run to realign diff lines when displayed on screen, if specified in the 'diffopt' option (default on nvim 0.11). Accordingly, fixed errors such as E684 and E716 on nvim. * Changed to disable this plugin to avoid a conflict with character/word-wise "inline" diff (patch-9.1.1243), if specified in the 'diffopt' option. * Changed to overwrite nvim default mappings to set plugin specific ones on plugin loading. As of nvim 0.11, `[b` and `]b` keys are duplicated. * Fixed to handle tab and space as a same whitespace character if "iwhite" or "iwhiteall" are specified in the 'diffopt' options. 9.7 * Implemented to use a new builtin |diff()| function (available on patch-9.1.0071) to compare diff units and make it faster in vim. 9.6, 9.61 * Changed to locally but not incrementally check the limited number of the |hl-DiffChange| lines upon scrolling or searching. * Fixed not to use |hl-DiffText| as the first highlight group if [{hlg}] is specified in the |g:DiffColors| option. 9.5 * Improved not to update diff unit highlighting on unchanged diff lines on the DiffUpdated event. * Fixed the E421 error when 'termguicolors' is off on nvim. 9.4 * Implemented to use a builtin Lua |vim.diff()| function to compare diff units and make it faster in nvim. 9.3 * Improved to follow the 'wincolor' option and show the colors accordingly. * Fixed to work on some color scheme in which the "g:colors_name" variable is different from its file name. 9.2 * Fixed the plugin error when 3 or more windows turn to be diff mode. * Fixed the issue which the new diff lines can not be incrementally found upon scrolling on patch-9.0.0913 or later. * Made it faster to find diff lines in a file with many lines folded. 9.1 * Added vim original 'word' and 'WORD' units, one or more unit delimiters, and a custom pattern to split into diff units in |g:DiffUnit| option. * Improved to redraw diff units whenever the ColorScheme event happens when multiple matching colors are being used (0 < |g:DiffColors|). * Changed the category of |g:DiffColors| option as a few, several, and many numbers of matching colors, depending on the loaded color scheme. * Added a list of your favorite highlight groups in |g:DiffColors| option. 9.01 * Fixed to work on a |hl-Diff| highlighting group even if it is linked. 9.0 * Enhanced to make diff units easier to read when "indent-heuristic" is specified in the 'diffopt' option. * Improved to update diff units using `:diffupdate` command when |g:DiffUnit|, |g:DiffColors|, and |g:DiffPairVisible| options are modified. * Updated to check a new WinScrolled event (patch-8.2.4713) to incrementally find scrolled diff lines. * Implemented the comparison algorithm and diff unit highlighting in Vim9 script and made them 10 times faster on patch-8.2.3965 or later. * Made this plugin available on or after patch-8.1.1418 and nvim-0.5.0. vim:tw=78:ts=8:ft=help:norl: ================================================ FILE: plugin/diffchar.vim ================================================ " diffchar.vim: Highlight the exact differences, based on characters and words " " ____ _ ____ ____ _____ _ _ _____ ____ " | | | || || || || | | || _ || _ | " | _ || || __|| __|| || | | || | | || | || " | | | || || |__ | |__ | __|| |_| || |_| || |_||_ " | |_| || || __|| __|| | | || || __ | " | || || | | | | |__ | _ || _ || | | | " |____| |_||_| |_| |_____||_| |_||_| |_||_| |_| " " Last Change: 2025/10/01 " Version: 10.0 (on or after vim 9.0 and nvim 0.7.0) " Author: Rick Howe (Takumi Ohtani) " Copyright: (c) 2014-2025 Rick Howe " License: MIT if exists('g:loaded_diffchar') || !has('diff') || \(v:version < 900 && !has('nvim-0.7.0')) let g:loaded_diffchar = 0 finish endif let g:loaded_diffchar = 10.0 let s:save_cpo = &cpoptions set cpo&vim " Keymaps for [key, plg, cmd] in [ \['[b', 'JumpDiffCharPrevStart', \':call diffchar#JumpDiffChar(0)'], \[']b', 'JumpDiffCharNextStart', \':call diffchar#JumpDiffChar(1)'], \['[e', 'JumpDiffCharPrevEnd', \':call diffchar#JumpDiffChar(2)'], \[']e', 'JumpDiffCharNextEnd', \':call diffchar#JumpDiffChar(3)'], \['g', 'GetDiffCharPair', \':call diffchar#CopyDiffCharPair(0)'], \['p', 'PutDiffCharPair', \':call diffchar#CopyDiffCharPair(1)']] if !hasmapto(plg, 'n') && maparg(key, 'n') =~ '^$\|_defaults.lua' if get(g:, 'DiffCharDoMapping', 1) call execute('nmap ' . key . ' ' . plg) endif endif call execute('nnoremap ' . plg . ' ' . cmd . '') endfor " Event groups let g:DiffCharInitEvent = ['augroup diffchar', 'autocmd!', \'autocmd OptionSet diff call diffchar#ToggleDiffModeSync()', \'augroup END'] call execute(g:DiffCharInitEvent) call execute('autocmd diffchar VimEnter * ++once \ if &diff | call diffchar#ToggleDiffModeSync(1) | endif') " remove 'inline' from &diffopt if set only by default not to disable plugin let inl = 'inline' if &diffopt =~ inl let dip = split(&diffopt, ',') set diffopt& let def = split(&diffopt, ',') if match(filter(copy(dip), 'index(def, v:val) == -1'), inl) == -1 call filter(dip, 'v:val !~ inl') endif let &diffopt = join(dip, ',') endif let &cpoptions = s:save_cpo unlet s:save_cpo " vim: ts=2 sw=0 sts=-1 et