" diffchar.vim: Highlight the exact differences, based on characters and words " " ____ _ ____ ____ _____ _ _ _____ ____ " | | | || || || || | | || _ || _ | " | _ || || __|| __|| || | | || | | || | || " | | | || || |__ | |__ | __|| |_| || |_| || |_||_ " | |_| || || __|| __|| | | || || __ | " | || || | | | | |__ | _ || _ || | | | " |____| |_||_| |_| |_____||_| |_||_| |_||_| |_| " " Last Change: 2021/12/07 " Version: 8.91 " Author: Rick Howe (Takumi Ohtani) " Copyright: (c) 2014-2021 by Rick Howe let s:save_cpo = &cpoptions set cpo&vim " Vim feature, function, event and patch number which this plugin depends on " patch-8.0.736: OptionSet event triggered with diff option " patch-8.0.794: count() fixed to accept a string " patch-8.0.914: nocombine attribute available " patch-8.0.1038: strikethrough attribute available " patch-8.0.1160: gettabvar() fixed not to return empty " patch-8.0.1290: changenr() fixed to return correct value " patch-8.1.414: v:option fixed in OptionSet diff autocmd " patch-8.1.1084: window ID argument available in all match functions " patch-8.1.1832: win_execute() fixed to work in other tabpage let s:VF = { \'DiffUpdated': exists('##DiffUpdated'), \'WinScrolled': exists('##WinScrolled'), \'WinClosed': exists('##WinClosed'), \'GUIColors': has('gui_running') || \has('termguicolors') && &termguicolors, \'DiffExecutable': executable('diff'), \'PopupWindow': has('popupwin'), \'FloatingWindow': exists('*nvim_create_buf'), \'GetMousePos': exists('*getmousepos'), \'WinExecute': exists('*win_execute') && \(has('patch-8.1.1832') || has('nvim-0.5.0')), \'DiffOptionSet': has('patch-8.0.736'), \'CountString': has('patch-8.0.794'), \'StrikeAttr': has('patch-8.0.1038') && \(has('gui_running') || !empty(&t_Ts) && !empty(&t_Te)), \'GettabvarFixed': has('patch-8.0.1160'), \'ChangenrFixed': has('patch-8.0.1290'), \'VOptionFixed': has('patch-8.1.414') || has('nvim-0.3.2'), \'WinIDinMatch': has('patch-8.1.1084') || has('nvim-0.5.0'), \'NvimDiffHLID': has('nvim') && !has('nvim-0.4.0')} function! s:SetDiffCharHL() abort " check vim original Diff highlights and set attributes for changes let s:DiffHL = {} for [hs, hl] in [['A', 'DiffAdd'], ['C', 'DiffChange'], \['D', 'DiffDelete'], ['T', 'DiffText']] let dh = {} let hn = has('nvim') ? '' : \matchstr(split(&highlight, ','), '^' . hs . '\C:') let dh.id = hlID(empty(hn) ? hl : hn[2:]) let dh.it = synIDtrans(dh.id) " in case of linked let dh.nm = synIDattr(dh.it, 'name') " dh: 0 = original, 1 = for single color, 2 = for multi color let dh.0 = {} for hm in ['term', 'cterm', 'gui'] for hc in ['fg', 'bg', 'sp'] 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 filter(dh.0, '!empty(v:val)') let dh.1 = (hs == 'C' || hs == 'T') ? \filter(copy(dh.0), 'v:key =~ "bg$"') : dh.0 let dh.2 = (hs == 'T') ? {} : dh.1 " diff_hlID() incorrectly returns (hlID() - 1) until nvim 0.4.0 if s:VF.NvimDiffHLID | let dh.id -= 1 | endif let s:DiffHL[hs] = dh endfor " set DiffChar specific highlights let s:DCharHL = {'A': 'DiffAdd', 'D': 'DiffDelete', 'n': 'LineNr'} if has('nvim') let s:DCharHL.c = 'TermCursor' else let s:DCharHL.c = 'Cursor' if !s:VF.GUIColors let id = 1 while 1 let nm = synIDattr(id, 'name') if empty(nm) | break | endif if id == synIDtrans(id) && !empty(synIDattr(id, 'reverse')) && \empty(filter(['fg', 'bg', 'sp', 'bold', 'underline', \'undercurl', 'strikethrough', 'italic', 'standout'], \'!empty(synIDattr(id, v:val))')) let s:DCharHL.c = nm break endif let id += 1 endwhile endif endif for [fs, ts, th, ta] in [['C', 'C', 'dcDiffChange', ''], \['T', 'T', 'dcDiffText', ''], \['C', 'E', 'dcDiffErase', 'bold,underline']] + \(s:VF.StrikeAttr ? [['D', 'D', 'dcDiffDelete', 'strikethrough']] : []) let fa = copy(s:DiffHL[fs].0) if !empty(ta) for hm in ['term', 'cterm', 'gui'] let fa[hm] = has_key(fa, hm) ? fa[hm] . ',' . ta : ta endfor endif call execute(['highlight clear ' . th, \'highlight ' . th . ' ' . \join(map(items(fa), 'join(v:val, "=")'))]) let s:DCharHL[ts] = th endfor " change diff highlights according to current DChar call s:ToggleDiffHL(exists('t:DChar')) endfunction function! s:InitializeDiffChar() abort " select current and next diff mode windows whose buffer is different " do no initiate if more than 2 diff mode windows exist in a tab page and " if a selected buffer already DChar highlighted in other tab pages 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 = s: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 " set diffchar highlights call s:SetDiffCharHL() " define a DiffChar dictionary on this tab page let t:DChar = {} " windowID and bufnr let t:DChar.wid = {'1': cw, '2': nw[0]} let t:DChar.bnr = {'1': cb, '2': nb[0]} " diff mode synchronization flag let t:DChar.dsy = get(g:, 'DiffModeSync', 1) " a multiple of the current visible page to locally detect diff lines let t:DChar.dfp = get(g:, 'DiffFocalPages', 3) " top/bottom/last lines, cursor line/column, changenr on each window let t:DChar.lcc = s:GetLineColCnr(1) " a list of diff focus lines let t:DChar.dfl = s:FocusDiffLines(1, 1) " a type of diff pair visible let pv = get(t:, 'DiffPairVisible', g:DiffPairVisible) if (pv == 3 || pv == 4) && !(s:VF.PopupWindow || s:VF.FloatingWindow) || \pv == 4 && !s:VF.GetMousePos let pv = 1 endif let t:DChar.dpv = {'pv': pv} if 0 < pv let t:DChar.dpv.ch = {} if pv == 3 || pv == 4 let t:DChar.dpv.pw = s:VF.PopupWindow ? 0 : \s:VF.FloatingWindow ? {'fb': -1, 'fw': -1} : -1 endif endif " a list of highlight IDs per line let t:DChar.mid = {'1': {}, '2': {}} " a list of added/deleted/changed columns per line let t:DChar.hlc = {'1': {}, '2': {}} " checksum per line let t:DChar.cks = {'1': {}, '2': {}} " ignorecase and ignorespace flags let do = split(&diffopt, ',') let t:DChar.igc = (index(do, 'icase') != -1) let t:DChar.igs = (index(do, 'iwhiteall') != -1) ? 1 : \(index(do, 'iwhite') != -1) ? 2 : \(index(do, 'iwhiteeol') != -1) ? 3 : 0 " a pattern to split difference units let du = get(t:, 'DiffUnit', g:DiffUnit) if du == 'Char' " any single character let t:DChar.upa = '\zs' elseif du == 'Word2' " non-space and space words let t:DChar.upa = '\%(\s\+\|\S\+\)\zs' elseif du == 'Word3' " \< or \> boundaries let t:DChar.upa = '\<\|\>' elseif du =~ '^CSV(.\+)$' " split characters let s = escape(du[4 : -2], '^-]') let t:DChar.upa = '\%([^'. s . ']\+\|[' . s . ']\)\zs' elseif du =~ '^SRE(.\+)$' " split regular expression let t:DChar.upa = du[4 : -2] else " \w\+ word and any \W character let t:DChar.upa = '\%(\w\+\|\W\)\zs' if du != 'Word1' call s:EchoWarning('Not a valid difference unit type. \ Use "Word1" instead.') endif endif " a list of difference matching colors let t:DChar.hgp = [s:DCharHL.T] let dc = get(t:, 'DiffColors', g:DiffColors) if 1 <= dc && dc <= 4 " select all available hl which has bg and has not attribute let [fd, bd] = map(['fg#', 'bg#'], 'synIDattr(hlID("Normal"), v:val)') let id = 1 while empty(fd) || empty(bd) let nm = synIDattr(id, 'name') if empty(nm) | break | endif if id == synIDtrans(id) if empty(fd) && synIDattr(id, 'bg') == 'fg' let fd = synIDattr(id, 'bg#') endif if empty(bd) && synIDattr(id, 'fg') == 'bg' let bd = synIDattr(id, 'fg#') endif endif let id += 1 endwhile let xb = 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 [fg, bg, rv] = map(['fg#', 'bg#', 'reverse'], \'synIDattr(id, v:val)') if empty(fg) | let fg = fd | endif if !empty(rv) | let bg = !empty(fg) ? fg : fd | endif if !empty(bg) && bg != fg && bg != bd && \index(xb, bg) == -1 && empty(filter(map(['bold', \'underline', 'undercurl', 'strikethrough', 'italic', \'standout'], \'synIDattr(id, v:val)'), '!empty(v:val)')) let hl[bg] = nm endif endif let id += 1 endwhile let t:DChar.hgp += values(hl)[: ((dc == 1) ? 2 : (dc == 2) ? 6 : \(dc == 3) ? 14 : -1)] elseif dc == 100 let hl = {} let id = 1 while 1 let nm = synIDattr(id, 'name') if empty(nm) | break | endif if index(values(s:DCharHL), nm) == -1 && id == synIDtrans(id) && \!empty(filter(['fg', 'bg', 'sp', 'bold', 'underline', \'undercurl', 'strikethrough', 'reverse', 'inverse', \'italic', 'standout'], '!empty(synIDattr(id, v:val))')) let hl[reltimestr(reltime())[-2 :] . id] = nm endif let id += 1 endwhile let t:DChar.hgp += values(hl) elseif -3 <= dc && dc <= -1 let t:DChar.hgp += ['SpecialKey', 'Search', 'CursorLineNr', \'Visual', 'WarningMsg', 'StatusLineNC', 'MoreMsg', \'ErrorMsg', 'LineNr', 'Conceal', 'NonText', \'ColorColumn', 'ModeMsg', 'PmenuSel', 'Title'] \[: ((dc == -1) ? 2 : (dc == -2) ? 6 : -1)] endif endfunction function! diffchar#ToggleDiffChar(lines) abort if exists('t:DChar') for k in [1, 2, 0] if k == 0 | return | endif if t:DChar.wid[k] == win_getid() | break | endif endfor for hl in keys(t:DChar.hlc[k]) if index(a:lines, eval(hl)) != -1 call diffchar#ResetDiffChar(a:lines) return endif endfor endif call diffchar#ShowDiffChar(a:lines) endfunction function! diffchar#ShowDiffChar(...) abort let init = !exists('t:DChar') if init && s:InitializeDiffChar() == -1 | return | endif for ak in [1, 2, 0] if ak == 0 | return | endif if t:DChar.wid[ak] == win_getid() | break | endif endfor let dl = [] for n in filter(map(copy(t:DChar.dfl[ak]), \'(a:0 && index(a:1, v:val) == -1) ? -1 : v:key'), 'v:val != -1') let [l1, l2] = [t:DChar.dfl[1][n], t:DChar.dfl[2][n]] if !has_key(t:DChar.hlc[1], l1) && !has_key(t:DChar.hlc[2], l2) let dl += [[{l1: getbufline(t:DChar.bnr[1], l1)[0]}, \{l2: getbufline(t:DChar.bnr[2], l2)[0]}]] endif endfor if !init && empty(dl) | return | endif let save_igc = &ignorecase | let &ignorecase = t:DChar.igc let uu = [] for n in range(len(dl)) let [d1, d2] = dl[n] for k in [1, 2] let t = values(d{k})[0] if t:DChar.igs let u{k} = split(substitute(t, '\s\+$', '', ''), t:DChar.upa) if t:DChar.igs == 1 " iwhiteall call filter(u{k}, 'v:val !~ "^\\s\\+$"') elseif t:DChar.igs == 2 " iwhite let s = len(u{k}) - 1 while 0 < s if u{k}[s - 1] . u{k}[s] =~ '^\s\+$' let u{k}[s - 1] .= u{k}[s] unlet u{k}[s] endif let s -= 1 endwhile endif else let u{k} = split(t, t:DChar.upa) endif endfor if u1 == u2 | let dl[n] = [] else | let uu += [[u1, u2]] endif endfor call filter(dl, '!empty(v:val)') let es = [] if s:VF.DiffExecutable let [mu, mt, st] = [get(g:, 'DiffIntMaxUnits', 2000), \get(g:, 'DiffIntMaxTime', 1.0), reltime()] endif for n in range(len(uu)) let [u1, u2] = uu[n] if s:VF.DiffExecutable && \(mu < len(u1) + len(u2) || mt < reltimefloat(reltime(st))) " the next line includes 2000+ units OR already spent 1.0+s let es += s:ExecDiffCommand(uu[n :]) break endif if t:DChar.igs == 2 " iwhite for k in [1, 2] let u{k} = map(copy(u{k}), \'(v:val =~ "^\\s\\+$") ? " " : v:val') endfor endif let es += [s:TraceDiffChar(u1, u2)] endfor let lc = {'1': {}, '2': {}} for n in range(len(dl)) let [d1, d2] = dl[n] let [c1, c2] = s:GetDiffUnitPos(es[n], uu[n]) for k in [1, 2] let [l, t] = items(d{k})[0] if t:DChar.igs == 1 " iwhiteall if t =~ '\s\+' let ap = filter(range(1, len(t)), 't[v:val - 1] !~ "\\s"') call map(c{k}, '[v:val[0], \[ap[v:val[1][0] - 1], ap[v:val[1][1] - 1]]]') endif endif let lc[k][l] = c{k} let t:DChar.cks[k][l] = s:ChecksumStr(t) endfor endfor let &ignorecase = save_igc call s:HighlightDiffChar(ak, lc) if !t:DChar.dsy && index(values(t:DChar.hlc), {}) != -1 unlet t:DChar else if init " set event when DChar HL is newly defined call s:ToggleDiffCharEvent(1) call s:ToggleDiffHL(1) call s:ToggleDiffCharPair(1) endif if 0 < t:DChar.dpv.pv | call s:ShowDiffCharPair(ak) | endif endif endfunction function! s:ExecDiffCommand(uu) abort " prepare 2 input files for diff for [k, u] in [[1, 0], [2, 1]] " insert '|:' before each line and " add '=:' at the beginning of each unit let u{k} = [''] " a dummy to avoid 1st null unit error for n in range(len(a:uu)) let u{k} += ['|' . n . ':'] + \map(copy(a:uu[n][u]), '"=" . n . ":" . v:val') endfor let f{k} = tempname() call writefile(u{k}, f{k}) endfor " call diff in unified format and assign edit symbols [=+-] to each unit let dc = ['diff', '-a', '--binary', ((t:DChar.igc == 1) ? '-i' : ''), \((t:DChar.igs == 1) ? '-w' : (t:DChar.igs == 2) ? '-b' : \(t:DChar.igs == 3) ? '-Z' : ''), \'-d', '-U', len(u1) + len(u2), f1, f2] let save_stmp = &shelltemp let &shelltemp = 0 let dt = systemlist(join(dc)) let &shelltemp = save_stmp for k in [1, 2] | call delete(f{k}) | endfor return split(join(map(filter(dt, 'v:val =~ "^[ +-][|=]"'), \'(v:val[0] != " ") ? v:val[0] : v:val[1]'), ''), '|') endfunction function! s:GetDiffUnitPos(es, uu) abort let [u1, u2] = a:uu if empty(u1) return [[['d', [0, 0]]], [['a', [1, len(join(u2, ''))]]]] elseif empty(u2) return [[['a', [1, len(join(u1, ''))]]], [['d', [0, 0]]]] endif let [c1, c2] = [[], []] let [l1, l2, p1, p2] = [1, 1, 0, 0] for ed in split(a:es, '[+-]\+\zs', 1)[: -2] let [qe, q1, q2] = [s:CountChar(ed, '='), s:CountChar(ed, '-'), \s:CountChar(ed, '+')] for k in [1, 2] if 0 < qe let [l{k}, p{k}] += \[len(join(u{k}[p{k} : p{k} + qe - 1], '')), qe] endif if 0 < q{k} let l = l{k} let [l{k}, p{k}] += \[len(join(u{k}[p{k} : p{k} + q{k} - 1], '')), q{k}] let h{k} = [l, l{k} - 1] else let h{k} = [ \l{k} - ((0 < p{k}) ? len(strcharpart(u{k}[p{k} - 1], \strchars(u{k}[p{k} - 1]) - 1, 1)) : 0), \l{k} + ((p{k} < len(u{k})) ? \len(strcharpart(u{k}[p{k}], 0, 1)) - 1 : -1)] endif endfor let [r1, r2] = (q1 == 0) ? ['d', 'a'] : \(q2 == 0) ? ['a', 'd'] : ['c', 'c'] let [c1, c2] += [[[r1, h1]], [[r2, h2]]] endfor return [c1, c2] endfunction function! s:TraceDiffChar(u1, u2) abort " An O(NP) Sequence Comparison Algorithm let [n1, n2] = [len(a:u1), len(a:u2)] if a:u1 == a:u2 | return repeat('=', n1) elseif n1 == 0 | return repeat('+', n2) elseif n2 == 0 | return repeat('-', n1) endif " reverse to be N >= M let [N, M, u1, u2, e1, e2] = (n1 >= n2) ? \[n1, n2, a:u1, a:u2, '+', '-'] : [n2, n1, a:u2, a:u1, '-', '+'] 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] < fp[k + 1]) ? \[fp[k + 1], [e1, (k < D) ? p - 1 : p, k + 1]] : \[fp[k - 1] + 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] .= '=' let [x, y] += [1, 1] endwhile let fp[k] = y endfor let etree += [epk] endwhile " create a shortest edit script (SES) from last p and k let ses = '' while 1 let ses = etree[p][k][0] . ses if p == 0 && k == 0 | return ses[1 :] | endif let [p, k] = etree[p][k][1 : 2] endwhile endfunction function! diffchar#ResetDiffChar(...) abort if !exists('t:DChar') | return | endif let last = (a:0 && type(a:1) == type(0)) ? a:1 : 0 for k in [1, 2, 0] if k == 0 | return | endif if t:DChar.wid[k] == win_getid() | break | endif endfor let dl = {'1': [], '2': []} for n in filter(map(copy(t:DChar.dfl[k]), \'(a:0 && type(a:1) == type([]) && \index(a:1, v:val) == -1) ? -1 : v:key'), 'v:val != -1') let [l1, l2] = [t:DChar.dfl[1][n], t:DChar.dfl[2][n]] if has_key(t:DChar.hlc[1], l1) || has_key(t:DChar.hlc[2], l2) let [dl[1], dl[2]] += [[l1], [l2]] unlet t:DChar.cks[1][l1] | unlet t:DChar.cks[2][l2] endif endfor if !last && empty(dl[k])| return | endif call s:ClearDiffChar(k, dl) if 0 < t:DChar.dpv.pv | call s:ClearDiffCharPair(k) | endif if last || !t:DChar.dsy && index(values(t:DChar.hlc), {}) != -1 call s:ToggleDiffHL(0) call s:ToggleDiffCharPair(0) unlet t:DChar call s:ToggleDiffCharEvent(0) endif endfunction function! s:ToggleDiffCharEvent(on) abort call execute(g:DiffCharInitEvent) let tv = filter(map(range(1, tabpagenr('$')), \'s:Gettabvar(v:val, "DChar")'), '!empty(v:val)') if empty(tv) | return | endif let ac = [] for td in tv for k in [1, 2] let bl = '' if td.dsy let ac += [['TextChanged,InsertLeave', bl, \'s:UpdateDiffChar(' . k . ', 0)']] if s:VF.DiffUpdated let ac += [['DiffUpdated', bl, \'s:UpdateDiffChar(' . k . ', 1)']] endif let ac += [[s:VF.WinClosed ? 'WinClosed' : 'BufWinLeave', bl, \'s:WinClosedDiffChar()']] if td.dfp != 0 let ac += [[ \s:VF.WinScrolled ? 'WinScrolled' : 'CursorMoved', bl, \'s:ScrollDiffChar(' . k . ')']] endif endif if 0 < td.dpv.pv let ac += [['CursorMoved', bl, \'s:ShowDiffCharPair(' . k . ')']] endif endfor endfor let ac += [['TabEnter', '*', 's:AdjustGlobalOption()']] let ac += [['ColorScheme', '*', 's:SetDiffCharHL()']] let ac += [[s:VF.WinClosed ? 'BufWinEnter' : 'BufWinEnter,WinEnter', '*', \'s:RepairDiffChar()']] if !s:VF.DiffUpdated if s:VF.DiffOptionSet let ac += [['OptionSet', 'diffopt', 's:FollowDiffOption()']] elseif !empty(filter(tv, 'v:val.dsy')) let s:save_ch = 's:ResetDiffModeSync()' let ac += [['CursorHold', '*', s:save_ch]] if !a:on | unlet s:save_ch | endif call s:ChangeUTOpt(a:on) endif endif call execute(map(ac, '"autocmd diffchar " . v:val[0] . " " . v:val[1] . \" call " . v:val[2]')) endfunction function! s:FocusDiffLines(key, init) abort " a:init : initialize dfl (do not use previous dfl) let dfl = {} let ks = (a:key == 1) ? [2, 1] : [1, 2] " check all diff lines if t:DChar.dfp == 0 if a:init for k in ks call s:WinGotoID(t:DChar.wid[k]) call s:WinExecute('let dfl[k] = \s:GetDiffLines(1, t:DChar.lcc[k].ll)') endfor return dfl else return t:DChar.dfl endif endif " check visible diff lines and merge with previous dfl " 1. get dfl in current visible lines and return if no new in both wins for k in ks call s:WinGotoID(t:DChar.wid[k]) call s:WinExecute('let dfl[k] = \s:GetDiffLines(t:DChar.lcc[k].tl, t:DChar.lcc[k].bl)') endfor if !a:init let nd = 0 for k in ks if !empty(dfl[k]) let [ti, bi] = [index(t:DChar.dfl[k], dfl[k][0]), \index(t:DChar.dfl[k], dfl[k][-1])] let nd += (ti == -1) || (bi == -1) || \(t:DChar.dfl[k][ti : bi] != dfl[k]) endif endfor if nd == 0 | return t:DChar.dfl | endif let lh = {} for k in ks let lh[k] = empty(dfl[k]) ? {'l': 0, 'h': 0} : \{'l': len(filter(copy(t:DChar.dfl[k]), 'v:val < dfl[k][0]')), \'h': len(filter(copy(t:DChar.dfl[k]), 'dfl[k][-1] < v:val'))} endfor endif " 2. get dfl in upper/lower lines and return if not found in both wins for k in ks let [fl, tl, bl, ll] = \[1, t:DChar.lcc[k].tl, t:DChar.lcc[k].bl, t:DChar.lcc[k].ll] if !a:init && 0 < t:DChar.dfp if 0 < lh[k].l | let fl = t:DChar.dfl[k][lh[k].l - 1] + 1 | endif if 0 < lh[k].h | let ll = t:DChar.dfl[k][-lh[k].h] - 1 | endif endif let [tz, bz] = [tl, bl] let rc = min([(tl - fl) + (ll - bl), \(abs(t:DChar.dfp) - 1) * (bl - tl + 1)]) if 0 < rc let hc = (rc + 1) / 2 let [tr, br] = [tl - fl, ll - bl] let tb = [hc <= tr, rc - hc <= br] let [tc, bc] = (tb == [1, 1]) ? [hc, rc - hc] : (tb == [0, 1]) ? \[tr, rc - tr] : (tb == [1, 0]) ? [rc - br, br] : [tr, br] let [tz, bz] += [-tc, bc] endif call s:WinGotoID(t:DChar.wid[k]) call s:WinExecute('let dfl[k] = s:GetDiffLines(tz, tl - 1) + dfl[k] + \s:GetDiffLines(bl + 1, bz)') endfor if empty(dfl[1]) && empty(dfl[2]) | return dfl | endif if index(values(dfl), []) != -1 " 3. if no dfl found in either, set a reference line and search dfl let [ek, fk] = empty(dfl[1]) ? [1, 2] : [2, 1] let rl = {} let [rl[ek], rl[fk]] = [{'l': 1, 'h': t:DChar.lcc[ek].ll}, \{'l': 1, 'h': t:DChar.lcc[fk].ll}] if !a:init if 0 < lh[fk].l let [rl[ek].l, rl[fk].l] = [t:DChar.dfl[ek][lh[fk].l - 1], \t:DChar.dfl[fk][lh[fk].l - 1]] endif if 0 < lh[fk].h let [rl[ek].h, rl[fk].h] = [t:DChar.dfl[ek][-lh[fk].h], \t:DChar.dfl[fk][-lh[fk].h]] endif endif let sd = dfl[fk][0] - rl[fk].l < rl[fk].h - dfl[fk][-1] call s:WinGotoID(t:DChar.wid[fk]) call s:WinExecute('let dc = len(sd ? \s:GetDiffLines(rl[fk].l, dfl[fk][0] - 1) : \s:GetDiffLines(dfl[fk][-1] + 1, rl[fk].h))') let fc = len(dfl[fk]) call s:WinGotoID(t:DChar.wid[ek]) call s:WinExecute('let dfl[ek] = \s:SearchDiffLines(sd, sd ? rl[ek].l : rl[ek].h, dc + fc)') let dfl[ek] = sd ? dfl[ek][-fc :] : dfl[ek][: fc - 1] call s:WinGotoID(t:DChar.wid[ks[1]]) else " 4. set reference lines using the closest line of previous dfl let rl = [[1, 1], [t:DChar.lcc[1].ll, t:DChar.lcc[2].ll]] if !a:init && index(values(t:DChar.dfl), []) == -1 let rx = [] for k in ks let rx += ((0 < lh[k].l) ? [lh[k].l - 1] : [0]) + \((0 < lh[k].h) ? [-lh[k].h] : [-1]) endfor let rl += map(rx, \'[t:DChar.dfl[1][v:val], t:DChar.dfl[2][v:val]]') endif " 5. select a reference line which is closest to new dfl let ds = map(copy(rl), 'abs(v:val[0] - (dfl[1][0] + dfl[1][-1]) / 2) + \abs(v:val[1] - (dfl[2][0] + dfl[2][-1]) / 2)') let ci = index(ds, min(ds)) let cl = {'1': rl[ci][0], '2': rl[ci][1]} " 6. get # of dfl (+/-) between reference and top/bottom lines let tb = {} for k in ks let [tl, bl, xl] = [dfl[k][0], dfl[k][-1], cl[k]] let td = (tl <= xl) ? [tl, xl, 1] : [xl, tl, -1] let bd = (bl <= xl) ? [bl, xl, 1] : [xl, bl, -1] call s:WinGotoID(t:DChar.wid[k]) call s:WinExecute('let tb[k] = \[(len(s:GetDiffLines(td[0], td[1])) - 1) * td[2], \(len(s:GetDiffLines(bd[0], bd[1])) - 1) * bd[2]]') endfor " 7. search and adjust dfl above/below the top/bottom in each window let dx = {'1': [0, 0, len(dfl[2])], '2': [0, 0, len(dfl[1])]} let [tx, bx] = [tb[1][0] - tb[2][0], tb[1][1] - tb[2][1]] let k = (tx < 0) ? 1 : (tx > 0) ? 2 : 0 if k != 0 | let dx[k][0] = abs(tx) | endif let k = (bx < 0) ? 2 : (bx > 0) ? 1 : 0 if k != 0 | let dx[k][1] = abs(bx) | endif for k in ks call s:WinGotoID(t:DChar.wid[k]) call s:WinExecute('let [td, bd] = \[s:SearchDiffLines(0, dfl[k][0] - 1, dx[k][0]), \s:SearchDiffLines(1, dfl[k][-1] + 1, dx[k][1])]') let [tx, bx] = \[min([dx[k][2], len(td)]), min([dx[k][2], len(bd)])] let dfl[k] = td[: tx - 1] + dfl[k] + bd[-bx :] endfor endif " 8. merge with previous dfl when 0 < dfp if !a:init && index(values(t:DChar.dfl), []) == -1 && 0 < t:DChar.dfp for k in ks call filter(dfl[k], 'index(t:DChar.dfl[k], v:val) == -1') let dfl[k] = empty(dfl[k]) ? t:DChar.dfl[k] : \(t:DChar.dfl[k][-1] < dfl[k][0]) ? t:DChar.dfl[k] + dfl[k] : \(dfl[k][-1] < t:DChar.dfl[k][0]) ? dfl[k] + t:DChar.dfl[k] : \filter(copy(t:DChar.dfl[k]), 'v:val < dfl[k][0]') + dfl[k] + \filter(copy(t:DChar.dfl[k]), 'dfl[k][-1] < v:val') endfor 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 = [] if 0 < a:sc let sl = a:sl if a:sd while sl <= line('$') let fl = foldclosedend(sl) if fl != -1 | let sl = fl + 1 | endif let dl += s:GetDiffLines(sl, min([sl + a:sc - 1, line('$')])) if a:sc <= len(dl) | let dl = dl[: a:sc - 1] | break | endif let sl += a:sc endwhile else while 1 <= sl let fl = foldclosed(sl) if fl != -1 | let sl = fl - 1 | endif let dl = s:GetDiffLines(max([sl - a:sc + 1, 1]), sl) + dl if a:sc <= len(dl) | let dl = dl[-a:sc :] | break | endif let sl -= a:sc endwhile endif endif return dl endfunction function! s:GetDiffLines(sl, el) abort return (a:sl > a:el) ? [] : \filter(filter(range(a:sl, a:el), 'foldlevel(v:val) == 0'), \'index([s:DiffHL.C.id, s:DiffHL.T.id], diff_hlID(v:val, 1)) != -1') endfunction function! s:GetLineColCnr(key) abort let lcc = {} for k in (a:key == 1) ? [2, 1] : [1, 2] call s:WinGotoID(t:DChar.wid[k]) call s:WinExecute('let lcc[k] = \{"tl": line("w0"), "bl": line("w$"), "ll": line("$"), \"cl": line("."), "cc": col("."), "cn": s:Changenr(), "ig": 0}') call s:WinExecute('let [tl, bl] = \[foldclosedend(lcc[k].tl), foldclosed(lcc[k].bl)]') if tl != -1 | let lcc[k].tl = tl | endif if bl != -1 | let lcc[k].bl = bl | endif endfor return lcc endfunction function! s:ScrollDiffChar(key) abort if !exists('t:DChar') || t:DChar.wid[a:key] != win_getid() return endif let lcc = s:GetLineColCnr(a:key) let scl = 0 for k in [1, 2] " check if a scroll happens in either window with no change on both let scl += (t:DChar.lcc[k].cn != lcc[k].cn) ? -1 : \([t:DChar.lcc[k].tl, t:DChar.lcc[k].bl] != \[lcc[k].tl, lcc[k].bl]) ? 1 : 0 let [t:DChar.lcc[k].tl, t:DChar.lcc[k].bl] = [lcc[k].tl, lcc[k].bl] endfor if 0 < scl let dfl = s:FocusDiffLines(a:key, 0) if t:DChar.dfl != dfl " reset/show DChar lines on dfl changes if t:DChar.dfp < 0 let ddl = filter(copy(t:DChar.dfl[a:key]), \'index(dfl[a:key], v:val) == -1') if !empty(ddl) | call diffchar#ResetDiffChar(ddl) | endif endif let adl = filter(copy(dfl[a:key]), \'index(t:DChar.dfl[a:key], v:val) == -1') let t:DChar.dfl = dfl if !empty(adl) | call diffchar#ShowDiffChar(adl) | endif endif endif endfunction function! s:HighlightDiffChar(key, lec) abort let hn = len(t:DChar.hgp) for k in (a:key == 1) ? [2, 1] : [1, 2] if !s:VF.WinIDinMatch | call s:WinGotoID(t:DChar.wid[k]) | endif for [l, ec] in items(a:lec[k]) if has_key(t:DChar.mid[k], l) | continue | endif let t:DChar.hlc[k][l] = ec " collect all the column positions per highlight group let hc = {} let cn = 0 for [e, c] in ec if e == 'c' let h = t:DChar.hgp[cn % hn] let cn += 1 elseif e == 'a' let h = s:DCharHL.A elseif e == 'd' if c == [0, 0] | continue | endif let h = s:DCharHL.E endif if !has_key(hc, h) | let hc[h] = [] | endif let hc[h] += [[l, c[0], c[1] - c[0] + 1]] endfor let t:DChar.mid[k][l] = [s:Matchaddpos(s:DCharHL.C, [[l]], -5, -1, \{'window': t:DChar.wid[k]})] for [h, c] in items(hc) let t:DChar.mid[k][l] += map(range(0, len(c) - 1, 8), \'s:Matchaddpos(h, c[v:val : v:val + 7], -3, -1, \{"window": t:DChar.wid[k]})') endfor endfor endfor endfunction function! s:ClearDiffChar(key, lines) abort for k in (a:key == 1) ? [2, 1] : [1, 2] if win_id2win(t:DChar.wid[k]) != 0 if !s:VF.WinIDinMatch | call s:WinGotoID(t:DChar.wid[k]) | endif for l in a:lines[k] silent! call map(t:DChar.mid[k][l], \'s:Matchdelete(v:val, t:DChar.wid[k])') endfor endif for l in a:lines[k] unlet t:DChar.mid[k][l] unlet t:DChar.hlc[k][l] endfor endfor endfunction function! s:ShiftDiffChar(key, lines, shift) abort let [lid, hlc, cks] = [[], {}, {}] for l in filter(copy(a:lines), 'has_key(t:DChar.mid[a:key], v:val)') let lid += [[l, t:DChar.mid[a:key][l]]] let hlc[l + a:shift] = t:DChar.hlc[a:key][l] let cks[l + a:shift] = t:DChar.cks[a:key][l] unlet t:DChar.mid[a:key][l] unlet t:DChar.hlc[a:key][l] unlet t:DChar.cks[a:key][l] endfor call extend(t:DChar.mid[a:key], \s:ShiftMatchaddLines(t:DChar.wid[a:key], lid, a:shift)) call extend(t:DChar.hlc[a:key], hlc) call extend(t:DChar.cks[a:key], cks) endfunction function! s:ShiftMatchaddLines(wid, lid, shift) abort let lid = {} let gm = s:Getmatches(a:wid) for [l, id] in a:lid let mx = filter(copy(gm), 'index(id, v:val.id) != -1') call map(copy(mx), 's:Matchdelete(v:val.id, a:wid)') let lid[l + a:shift] = map(reverse(mx), 's:Matchaddpos(v:val.group, \map(filter(items(v:val), "v:val[0] =~ ''^pos\\d\\+$''"), \"[v:val[1][0] + a:shift] + v:val[1][1 :]"), v:val.priority, -1, \{"window": a:wid})') endfor return lid endfunction function! s:UpdateDiffChar(key, event) abort " a:event : 0 = TextChanged/InsertLeave, 1 = DiffUpdated if mode(1) != 'n' || !exists('t:DChar') || \!empty(filter(values(t:DChar.wid), '!getwinvar(v:val, "&diff")')) return endif if !s:VF.DiffUpdated | call s:RedrawDiffChar(a:key, 1) | return | endif " try to redraw updated DChar lines at the last DiffUpdated which comes " just after TextChanged or InsertLeave if text changed if a:event == 1 if t:DChar.lcc[a:key].ig == 0 if t:DChar.lcc[a:key].cn == s:Changenr() call s:RedrawDiffChar(a:key, 0) else " in case of e:, DiffUpdated happens 3 times, " first, all dfl not diff highlighed but no line diff folded, " then wait for the next two events if &foldmethod == 'diff' && !empty(t:DChar.dfl[a:key]) && \empty(filter(copy(t:DChar.dfl[a:key]), \'diff_hlID(v:val, 1) != 0')) && \empty(filter(range(t:DChar.dfl[a:key][0], \t:DChar.dfl[a:key][-1]), '0 < foldlevel(v:val)')) let t:DChar.lcc[a:key].ig += 1 endif endif elseif t:DChar.lcc[a:key].ig == 1 " wait for another one let t:DChar.lcc[a:key].ig += 1 elseif t:DChar.lcc[a:key].ig == 2 " the last one came then redraw let t:DChar.lcc[a:key].ig = 0 call s:RedrawDiffChar(a:key, 1) endif else if &diffopt =~ 'internal' && empty(&diffexpr) if t:DChar.lcc[a:key].cn != s:Changenr() let t:DChar.lcc[a:key].ig = 2 " wait for the next/last one endif else call s:RedrawDiffChar(a:key, 1) " DiffUpdated not happen next endif endif endfunction function! s:RedrawDiffChar(key, txtcg) abort " a:txtcg : 0 = for text unchanged, 1 = for text changed let ll = t:DChar.lcc[a:key].ll let t:DChar.lcc = s:GetLineColCnr(a:key) let cfl = s:FocusDiffLines(a:key, 1) if a:txtcg " compare between previous and current DChar and diff lines " using checksum and find ones to be deleted, added, and shifted let lnd = t:DChar.lcc[a:key].ll - ll let bk = (a:key == 1) ? 2 : 1 let [pfa, pfb] = [t:DChar.dfl[a:key], t:DChar.dfl[bk]] let [cfa, cfb] = [cfl[a:key], cfl[bk]] let m = min([len(pfa), len(cfa)]) if pfa == cfa let ddl = [] for s in range(m) if pfb[s] != cfb[s] || get(t:DChar.cks[a:key], pfa[s]) != \s:ChecksumStr(getline(cfa[s])) let ddl += [pfa[s]] endif endfor let adl = ddl let sdl = [] else let s = 0 while s < m && pfa[s] == cfa[s] && pfb[s] == cfb[s] && \get(t:DChar.cks[a:key], pfa[s]) == \s:ChecksumStr(getline(cfa[s])) let s += 1 endwhile let e = -1 let m -= s while e >= -m && pfa[e] + lnd == cfa[e] && pfb[e] == cfb[e] && \get(t:DChar.cks[a:key], pfa[e]) == \s:ChecksumStr(getline(cfa[e])) let e -= 1 endwhile let ddl = pfa[s : e] let adl = cfa[s : e] let sdl = (lnd != 0 && e < -1) ? pfa[e + 1 :] : [] endif " redraw updated DChar lines if 0 < t:DChar.dpv.pv | call s:ClearDiffCharPair(a:key) | endif if !empty(ddl) | call diffchar#ResetDiffChar(ddl) | endif let t:DChar.dfl = cfl if !empty(sdl) | call s:ShiftDiffChar(a:key, sdl, lnd) | endif if !empty(adl) | call diffchar#ShowDiffChar(adl) | endif else " reset dfl and redraw all DChar lines on text unchanged " (diffupdate and diffopt changes) let do = split(&diffopt, ',') let igc = (index(do, 'icase') != -1) let igs = (index(do, 'iwhiteall') != -1) ? 1 : \(index(do, 'iwhite') != -1) ? 2 : \(index(do, 'iwhiteeol') != -1) ? 3 : 0 if [t:DChar.dfl, t:DChar.igc, t:DChar.igs] != [cfl, igc, igs] call diffchar#ResetDiffChar() let [t:DChar.dfl, t:DChar.igc, t:DChar.igs] = [cfl, igc, igs] call diffchar#ShowDiffChar() endif endif endfunction function! diffchar#JumpDiffChar(dir, pos) abort " a:dir : 0 = backward, 1 = forward / a:pos : 0 = start, 1 = end if !exists('t:DChar') | return | endif for k in [1, 2, 0] if k == 0 | return | endif if t:DChar.wid[k] == win_getid() | break | endif endfor let [ln, co] = [line('.'), col('.')] if co == col('$') " empty line if !a:dir | let co = 0 | endif else if a:pos let co += len(strcharpart(getline(ln)[co - 1 :], 0, 1)) - 1 endif endif if has_key(t:DChar.hlc[k], ln) && \(a:dir ? co < t:DChar.hlc[k][ln][-1][1][a:pos] : \co > t:DChar.hlc[k][ln][0][1][a:pos]) " found in current line let hc = filter(map(copy(t:DChar.hlc[k][ln]), 'v:val[1][a:pos]'), \a:dir ? 'co < v:val' : 'co > v:val') let co = hc[a:dir ? 0 : -1] else if t:DChar.dfp == 0 " try to find the next highlighted line let hl = filter(map(keys(t:DChar.hlc[k]), 'eval(v:val)'), \a:dir ? 'ln < v:val' : 'ln > v:val') if empty(hl) | return | endif let ln = a:dir ? min(hl) : max(hl) else " try to find the next diff line and then apply hlc or scroll let cp = [line('.'), col('.')] while 1 let dl = s:SearchDiffLines(a:dir, a:dir ? ln + 1 : ln - 1, 1) if empty(dl) | noautocmd call cursor(cp) | return | endif let ln = dl[0] if has_key(t:DChar.hlc[k], ln) | break | endif noautocmd call cursor(ln, 0) call s:ScrollDiffChar(k) if has_key(t:DChar.hlc[k], ln) | break | endif endwhile endif let co = t:DChar.hlc[k][ln][a:dir ? 0 : -1][1][a:pos] endif " set a dummy cursor position to adjust the start/end if 0 < t:DChar.dpv.pv call s:ClearDiffCharPair(k) if [a:dir, a:pos] == [1, 0] " forward/start : rightmost let [t:DChar.lcc[k].cl, t:DChar.lcc[k].cc] = [ln, col('$')] elseif [a:dir, a:pos] == [0, 1] " backward/end : leftmost let [t:DChar.lcc[k].cl, t:DChar.lcc[k].cc] = [ln, 0] endif endif call cursor(ln, co) endfunction function! s:ShowDiffCharPair(key) abort if mode(1) != 'n' || !exists('t:DChar') || \t:DChar.wid[a:key] != win_getid() return endif let [pl, pc, pn] = [t:DChar.lcc[a:key].cl, t:DChar.lcc[a:key].cc, \t:DChar.lcc[a:key].cn] 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 pn != s:Changenr() | return | endif " do nothing on TextChanged if !empty(t:DChar.dpv.ch) " pair highlight exists let [hl, hi] = t:DChar.dpv.ch.lc let hc = t:DChar.hlc[a:key][hl][hi][1] " inside the highlight, do nothing if cl == hl && hc[0] <= cc && cc <= hc[1] | return | endif call s:ClearDiffCharPair(a:key) " outside, clear it 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 contineous 'd', check if cursor moved forward or backward let ix = (len(hu) == 1) ? 0 : (cl == pl) ? cc < pc : cl < pl call s:HighlightDiffCharPair(a:key, cl, hu[ix][0]) 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)]] " set a pair cursor position (line, colnum) and match id let t:DChar.dpv.ch.lc = [al, a:col] let t:DChar.dpv.ch.bk = bk " show a cursor-like highlight at the corresponding position let bc = t:DChar.hlc[bk][bl][a:col][1] if bc != [0, 0] let [pos, len] = [bc[0], bc[1] - bc[0] + 1] if !s:VF.WinIDinMatch | call s:WinGotoID(t:DChar.wid[bk]) | endif let t:DChar.dpv.ch.id = s:Matchaddpos(s:DCharHL.c, [[bl, pos, len]], \-1, -1, {'window': t:DChar.wid[bk]}) if !s:VF.WinIDinMatch | call s:WinGotoID(t:DChar.wid[ak]) | endif else let t:DChar.dpv.ch.id = -1 " no cursor hl on empty line endif call execute(['augroup diffchar2', 'autocmd!', \'autocmd WinLeave call s:ClearDiffCharPair(' . ak . ')', 'augroup END']) if t:DChar.dpv.pv < 2 | return | endif " show the corresponding unit in echo or popup-window let at = getbufline(t:DChar.bnr[ak], al)[0] let bt = getbufline(t:DChar.bnr[bk], bl)[0] let [ae, ac] = t:DChar.hlc[ak][al][a:col] 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] = [(1 < ac[0]) ? '>' : '', \repeat((t:DChar.dpv.pv == 2 && s:VF.StrikeAttr) ? ' ' : \(&fillchars =~ 'diff') ? matchstr(&fillchars, 'diff:\zs.') : '-', \strwidth(at[ac[0] - 1 : ac[1] - 1])), \(ac[1] < len(at)) ? '<' : ''] 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 if t:DChar.dpv.pv == 4 | let mp = getmousepos() | endif if s:VF.PopupWindow 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, tb . tx . te) call popup_show(t:DChar.dpv.pw) elseif s:VF.FloatingWindow 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(tb . tx . te)})) call setbufline(t:DChar.dpv.pw.fb, 1, tb . tx . te) call setwinvar(t:DChar.dpv.pw.fw, '&winblend', 0) endif endif endfunction function! s:ClearDiffCharPair(key) abort if !empty(t:DChar.dpv.ch) let [bk, id] = [t:DChar.dpv.ch.bk, t:DChar.dpv.ch.id] if id != -1 && win_id2win(t:DChar.wid[bk]) != 0 if !s:VF.WinIDinMatch | call s:WinGotoID(t:DChar.wid[bk]) | endif silent! call s:Matchdelete(id, t:DChar.wid[bk]) if !s:VF.WinIDinMatch call s:WinGotoID(t:DChar.wid[a:key]) endif endif call execute(['augroup diffchar2', 'autocmd!', 'augroup END', \'augroup! diffchar2']) 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 if s:VF.PopupWindow | call popup_hide(t:DChar.dpv.pw) elseif s:VF.FloatingWindow 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) endif endif endfunction function! s:ToggleDiffCharPair(on) abort if t:DChar.dpv.pv == 3 || t:DChar.dpv.pv == 4 if s:VF.PopupWindow let t:DChar.dpv.pw = a:on ? \popup_create('', {'hidden': 1, 'scrollbar': 0, 'wrap': 0, \'highlight': s:DCharHL.c}) : \popup_close(t:DChar.dpv.pw) elseif s:VF.FloatingWindow 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) let t:DChar.dpv.pw = {'fb': -1, 'fw': -1} endif endif endif endfunction function! diffchar#CopyDiffCharPair(dir) abort " a:dir : 0 = get, 1 = put if !exists('t:DChar') | return | endif for ak in [1, 2, 0] if ak == 0 | return | endif if t:DChar.wid[ak] == win_getid() | break | endif endfor 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 [ae, ac] = t:DChar.hlc[ak][al][un] let [be, bc] = t:DChar.hlc[bk][bl][un] let at = getbufline(t:DChar.bnr[ak], al)[0] let bt = getbufline(t:DChar.bnr[bk], bl)[0] let [x, y] = a:dir ? ['b', 'a'] : ['a', 'b'] " put : get let s1 = (1 < {x}c[0]) ? {x}t[: {x}c[0] - 2] : '' let s2 = ({x}e != 'a') ? {y}t[{y}c[0] - 1 : {y}c[1] - 1] : '' if {x}e == 'd' && {x}c != [0, 0] let ds = split({x}t[{x}c[0] - 1 : {x}c[1] - 1], '\zs') let s2 = ((1 < {y}c[0]) ? ds[0] : '') . s2 . \(({y}c[1] < len({y}t)) ? ds[-1] : '') endif let s3 = ({x}c[1] < len({x}t)) ? {x}t[{x}c[1] :] : '' let ss = s1 . s2 . s3 if a:dir " put call s:WinGotoID(t:DChar.wid[bk]) call s:WinExecute('noautocmd call setline(bl, ss)') call s:WinExecute('call s:RedrawDiffChar(bk, 1)') call s:WinGotoID(t:DChar.wid[ak]) else " get call setline(al, ss) endif endfunction function! diffchar#EchoDiffChar(lines, short) abort if !exists('t:DChar') | return | endif for ak in [1, 2, 0] if ak == 0 | return | endif if t:DChar.wid[ak] == win_getid() | break | endif endfor let bk = (ak == 1) ? 2 : 1 let nw = max([&numberwidth - 1, len(string(line('$')))]) let ec = [] for al in a:lines let gt = [] if &number || &relativenumber let gt += [[s:DCharHL.n, printf('%'. nw . 'd ', \(&relativenumber ? abs(al - line('.')) : al))]] endif let at = getbufline(t:DChar.bnr[ak], al)[0] if !has_key(t:DChar.hlc[ak], al) if a:short | continue | endif let gt += [['', empty(at) ? "\n" : at]] else let bl = t:DChar.dfl[bk][index(t:DChar.dfl[ak], al)] let bt = getbufline(t:DChar.bnr[bk], bl)[0] let hl = repeat('C', len(at)) let tx = at for an in range(len(t:DChar.hlc[ak][al]) - 1, 0, -1) let [ae, ac] = t:DChar.hlc[ak][al][an] " enclose highlight and text in '[+' and '+]' " if strike not available if ae == 'c' || ae == 'a' let it = at[ac[0] - 1 : ac[1] - 1] if !s:VF.StrikeAttr | let it = '[+' . it . '+]' | endif let ih = repeat((ae == 'a') ? 'A' : 'T', len(it)) let hl = ((1 < ac[0]) ? hl[: ac[0] - 2] : '') . ih . \hl[ac[1] :] let tx = ((1 < ac[0]) ? tx[: ac[0] - 2] : '') . it . \tx[ac[1] :] endif " enclose corresponding changed/deleted units in '[-' and '-]' " if strike not available, " and insert them to highlight and text if ae == 'c' || ae == 'd' let bc = t:DChar.hlc[bk][bl][an][1] let it = bt[bc[0] - 1 : bc[1] - 1] if !s:VF.StrikeAttr | let it = '[-' . it . '-]' | endif let ih = repeat('D', len(it)) if ae == 'c' let hl = ((1 < ac[0]) ? hl[: ac[0] - 2] : '') . ih . \hl[ac[0] - 1 :] let tx = ((1 < ac[0]) ? tx[: ac[0] - 2] : '') . it . \tx[ac[0] - 1 :] else if ac[0] == 1 && bc[0] == 1 let hl = ih . hl let tx = it . tx else let ix = ac[0] + \len(strcharpart(at[ac[0] - 1 :], 0, 1)) - 2 let hl = hl[: ix] . ih . hl[ix + 1 :] let tx = tx[: ix] . it . tx[ix + 1 :] endif endif endif endfor let sm = a:short && &columns <= strdisplaywidth(tx) let ix = 0 let tn = 0 for h in split(hl, '\%(\(.\)\1*\)\zs') if h[0] == 'T' let g = t:DChar.hgp[tn % len(t:DChar.hgp)] let tn += 1 else let g = s:DCharHL[h[0]] endif let t = tx[ix : ix + len(h) - 1] if sm && h[0] == 'C' let s = split(t, '\zs') if ix == 0 && 1 < len(s) && \3 < strdisplaywidth(join(s[: -2], '')) let t = '...' . s[-1] elseif ix + len(h) == len(tx) && 1 < len(s) && \3 < strdisplaywidth(join(s[1 :], '')) let t = s[0] . '...' elseif 2 < len(s) && \3 < strdisplaywidth(join(s[1 : -2], '')) let t = s[0] . '...' . s[-1] endif endif let gt += [[g, t]] let ix += len(h) endfor endif let ec += ['echo ""'] for [g, t] in gt let ec += ['echohl ' . g, 'echon "' . escape(t, '"') . '"'] endfor let ec += ['echohl None'] endfor call execute(ec, '') endfunction function! diffchar#DiffCharExpr() abort let [f1, f2] = [readfile(v:fname_in), readfile(v:fname_new)] call writefile(([f1, f2] == [['line1'], ['line2']]) ? ['1c1'] : \(s:VF.DiffExecutable && len(f1) + len(f2) > 100) ? \s:ExtDiffExpr(v:fname_in, v:fname_new) : \s:IntDiffExpr(f1, f2), v:fname_out) endfunction function! s:IntDiffExpr(f1, f2) abort let [f1, f2] = [a:f1, a:f2] let do = split(&diffopt, ',') let save_igc = &ignorecase let &ignorecase = (index(do, 'icase') != -1) if index(do, 'iwhiteall') != -1 for k in [1, 2] call map(f{k}, 'substitute(v:val, "\\s\\+", "", "g")') endfor elseif index(do, 'iwhite') != -1 for k in [1, 2] call map(f{k}, 'substitute(v:val, "\\s\\+", " ", "g")') call map(f{k}, 'substitute(v:val, "\\s\\+$", "", "")') endfor elseif index(do, 'iwhiteeol') != -1 for k in [1, 2] call map(f{k}, 'substitute(v:val, "\\s\\+$", "", "")') endfor endif let dfcmd = [] let [l1, l2] = [1, 1] for ed in split(s:TraceDiffChar(f1, f2), '[+-]\+\zs', 1)[: -2] let [qe, q1, q2] = [s:CountChar(ed, '='), s:CountChar(ed, '-'), \s:CountChar(ed, '+')] let [l1, l2] += [qe, qe] let dfcmd += [((1 < q1) ? l1 . ',' : '') . (l1 + q1 - 1) . \((q1 == 0) ? 'a' : (q2 == 0) ? 'd' : 'c') . \((1 < q2) ? l2 . ',' : '') . (l2 + q2 - 1)] let [l1, l2] += [q1, q2] endfor let &ignorecase = save_igc return dfcmd endfunction function! s:ExtDiffExpr(f1, f2) abort let do = split(&diffopt, ',') let dc = ['diff', '-a', '--binary', \((index(do, 'icase') != -1) ? '-i' : ''), \((index(do, 'iwhiteall') != -1) ? '-w' : \(index(do, 'iwhite') != -1) ? '-b' : \(index(do, 'iwhiteeol') != -1) ? '-Z' : ''), a:f1, a:f2] let save_stmp = &shelltemp let &shelltemp = 0 let dt = systemlist(join(dc)) let &shelltemp = save_stmp return filter(dt, 'v:val[0] =~ "\\d"') endfunction if s:VF.DiffOptionSet function! diffchar#ToggleDiffModeSync(event) abort " a:event : 0 = OptionSet diff, 1 = VimEnter if !get(g:, 'DiffModeSync', 1) | return | endif if s:VF.VOptionFixed if a:event || v:option_old != v:option_new call s:SwitchDiffChar(a:event || v:option_new) endif else call s:SwitchDiffChar(a:event || &diff) endif endfunction else function! diffchar#SetDiffModeSync() abort " DiffModeSync is triggered ON by FilterWritePost if !get(g:, 'DiffModeSync', 1) | return | endif if !exists('s:dmbuf') " as a diff session, when FilterWritePos comes, current buf and " other 1 or more buf should be diff mode let s:dmbuf = map(filter(gettabinfo(tabpagenr())[0].windows, \'getwinvar(v:val, "&diff")'), 'winbufnr(v:val)') if index(s:dmbuf, bufnr('%')) == -1 || \min(s:dmbuf) == max(s:dmbuf) " not a diff session, then clear unlet s:dmbuf return endif " wait for the contineous 1 or more FilterWitePost (diff) or " 1 ShellFilterPost (non diff) call execute('autocmd! diffchar ShellFilterPost * \ call s:ClearDiffModeSync()') " prepare to complete sync just in case for accidents let s:id = timer_start(0, function('s:CompleteDiffModeSync')) endif " check if all the FilterWritePost has come if empty(filter(s:dmbuf, 'v:val != bufnr("%")')) call s:CompleteDiffModeSync(0) endif endfunction function! s:CompleteDiffModeSync(id) abort if exists('s:id') if a:id == 0 | call timer_stop(s:id) | endif unlet s:id else if exists('s:save_ch') && !empty(s:save_ch) call execute('autocmd! diffchar CursorHold * call ' . \s:save_ch) call s:ChangeUTOpt(1) else call execute('autocmd! diffchar CursorHold *') call s:ChangeUTOpt(0) endif silent call feedkeys("g\", 'n') endif call s:ClearDiffModeSync() call timer_start(0, function('s:SwitchDiffChar')) endfunction function! s:ClearDiffModeSync() abort unlet s:dmbuf call execute('autocmd! diffchar ShellFilterPost *') endfunction function! s:ResetDiffModeSync() abort " DiffModeSync is triggered OFF by CursorHold if exists('t:DChar') && t:DChar.dsy && \!empty(filter(values(t:DChar.wid), '!getwinvar(v:val, "&diff")')) " if either or both of DChar win is now non-diff mode, " reset it and show with current diff mode wins call s:SwitchDiffChar(0) endif endfunction endif function! s:SwitchDiffChar(on) abort let cw = win_getid() let aw = cw if exists('t:DChar') && \(a:on ? 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 for k in [1, 2] if getwinvar(t:DChar.wid[k], '&diff') let aw = t:DChar.wid[k] call s:WinGotoID(aw) call s:WinExecute('call diffchar#ResetDiffChar(1)') call s:WinGotoID(cw) break endif endfor endif if !exists('t:DChar') && get(g:, 'DiffModeSync', 1) let aw = win_id2win(aw) 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 s:WinGotoID(dw[0]) call s:WinExecute('call diffchar#ShowDiffChar()') call s:WinGotoID(cw) endif endif endfunction function! s:WinClosedDiffChar() abort " reset and show (if possible) DChar on WinClosed or BufWinLeave for tn in range(1, tabpagenr('$')) let dc = s:Gettabvar(tn, 'DChar') if !empty(dc) for k in [1, 2] if s:VF.WinClosed ? dc.wid[k] == eval(expand('')) : \dc.bnr[k] == eval(expand('')) let cw = win_getid() call s:WinGotoID(dc.wid[k]) call s:WinExecute('call diffchar#ResetDiffChar(1)') if dc.dsy let dw = filter(gettabinfo(tn)[0].windows, \'v:val != dc.wid[k] && \winbufnr(v:val) == dc.bnr[k] && \getwinvar(v:val, "&diff")') if !empty(dw) call s:WinGotoID(dw[0]) call s:WinExecute('call diffchar#ShowDiffChar()') endif endif call s:WinGotoID(cw) return endif endfor endif 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 let cw = win_getid() call s:WinGotoID(values(dw)[0]) call s:WinExecute('call diffchar#ResetDiffChar(1)') if dc.dsy call s:WinExecute('call diffchar#ShowDiffChar()') endif call s:WinGotoID(cw) endif endif endfunction function! s:ChecksumStr(str) abort return eval('0x' . sha256(a:str)[-4 :]) endfunction function! s:EchoWarning(msg) abort call execute(['echohl WarningMsg', 'echo a:msg', 'echohl None'], '') endfunction if s:VF.CountString let s:CountChar = function('count') else function! s:CountChar(str, chr) abort return len(a:str) - len(substitute(a:str, a:chr, '', 'g')) endfunction endif if s:VF.GettabvarFixed let s:Gettabvar = function('gettabvar') else function! s:Gettabvar(tp, var) abort call gettabvar(a:tp, a:var) " call twice as a workaround return gettabvar(a:tp, a:var) endfunction endif if s:VF.ChangenrFixed let s:Changenr = function('changenr') else function! s:Changenr() abort let ute = undotree().entries for n in range(len(ute)) if has_key(ute[n], 'curhead') " if curhead exists, undotree().seq_cur should be this but not " then changenr() returns a wrong number return (0 < n) ? ute[n - 1].seq : 0 endif endfor return changenr() endfunction endif if s:VF.WinExecute function! s:WinGotoID(wid) abort let s:WinExecute = function('win_execute', [a:wid]) endfunction else function! s:WinGotoID(wid) abort noautocmd call win_gotoid(a:wid) endfunction let s:WinExecute = function('execute') endif if s:VF.WinIDinMatch let s:Matchaddpos = function('matchaddpos') let s:Matchdelete = function('matchdelete') let s:Getmatches = function('getmatches') else function! s:Matchaddpos(grp, pos, pri, ...) abort return matchaddpos(a:grp, a:pos, a:pri) endfunction function! s:Matchdelete(id, ...) abort return matchdelete(a:id) endfunction function! s:Getmatches(...) abort return getmatches() endfunction endif function! s:AdjustGlobalOption() abort if !s:VF.DiffUpdated && !s:VF.DiffOptionSet call s:ChangeUTOpt(exists('t:DChar') && t:DChar.dsy) endif call s:ToggleDiffHL(exists('t:DChar')) endfunction if !s:VF.DiffUpdated if s:VF.DiffOptionSet function! s:FollowDiffOption() abort if v:option_old != v:option_new let cw = win_getid() for dc in filter(map(range(1, tabpagenr('$')), \'s:Gettabvar(v:val, "DChar")'), '!empty(v:val)') call s:WinGotoID(dc.wid[1]) call s:WinExecute('call s:RedrawDiffChar(1, 0)') endfor call s:WinGotoID(cw) endif endfunction else function! s:ChangeUTOpt(on) abort if a:on && !exists('s:save_ut') let s:save_ut = &updatetime let &updatetime = 500 elseif !a:on && exists('s:save_ut') let &updatetime = s:save_ut unlet s:save_ut endif endfunction endif endif function! s:ToggleDiffHL(on) abort " dh: 0 = original, 1 = for single color, 2 = for multi color for dh in values(s:DiffHL) call execute(['highlight clear ' . dh.nm, 'highlight ' . dh.nm . ' ' . \join(map(items(dh[!a:on ? 0 : (len(t:DChar.hgp) == 1) ? 1 : 2]), \'join(v:val, "=")'))]) endfor endfunction let &cpoptions = s:save_cpo unlet s:save_cpo " vim: ts=4 sw=4