vimsuite/vimfiles/autoload/diffchar.vim
Stefan Liebl 82ed35903e GLVS
Change-Id: I73fb1df3adaba7a1248c20664f1654372ad7223e
2016-05-24 15:24:48 +02:00

1263 lines
36 KiB
VimL

" diffchar.vim : Highlight the exact differences, based on characters and words
"
" ____ _ ____ ____ _____ _ _ _____ ____
" | | | || || || || | | || _ || _ |
" | _ || || __|| __|| || | | || | | || | ||
" | | | || || |__ | |__ | __|| |_| || |_| || |_||_
" | |_| || || __|| __|| | | || || __ |
" | || || | | | | |__ | _ || _ || | | |
" |____| |_||_| |_| |_____||_| |_||_| |_||_| |_|
"
" Last Change: 2016/03/09
" Version: 6.1
" Author: Rick Howe <rdcxy754@ybb.ne.jp>
let s:save_cpo = &cpo
set cpo&vim
function! diffchar#DiffCharExpr()
" find the fist diff trial call and return here
if readfile(v:fname_in, '', 1) == ['line1'] &&
\readfile(v:fname_new, '', 1) == ['line2']
call writefile(['1c1'], v:fname_out)
return
endif
" get a list of diff commands and write to output file
for fn in ['ApplyInternalAlgorithm', 'ApplyDiffCommand']
let dfcmd = s:{fn}(v:fname_in, v:fname_new)
" if empty, try next
if !empty(dfcmd) | break | endif
endfor
call writefile(dfcmd, v:fname_out)
endfunction
function! diffchar#SetDiffModeSync()
if exists('t:DiffModeSync') ? !t:DiffModeSync : !g:DiffModeSync
return
endif
if !exists('s:dmsync')
" a filter command (not diff) also triggers FilterWritePre but
" continueously triggers ShellFilterPost,
" prepare here to clear s:dmsync
au! dchar ShellFilterPost * call s:ClearDiffModeSync()
let s:dmsync = {}
let s:dmsync.ebuf = []
" find all the diff mode winnr and bufnr at the first event of
" a diff session.
let s:dmsync.dwin = s:ValidDiffModeWins(range(1, winnr('$')))
let s:dmsync.dbuf = map(copy(s:dmsync.dwin), 'winbufnr(v:val)')
if min(s:dmsync.dbuf) == max(s:dmsync.dbuf)
call s:ClearDiffModeSync()
return
endif
endif
" append current bufnr where the event happens
if index(s:dmsync.dbuf, bufnr('%')) != -1
let s:dmsync.ebuf += [bufnr('%')]
endif
if empty(filter(copy(s:dmsync.dbuf),
\'index(s:dmsync.ebuf, v:val) == -1'))
" when all the events of one diff session come, get winnr of
" the first/last buffers for v:fname_in/v:fname_new
let win = {}
let cwin = winnr()
for k in [1, 2]
let w = filter(copy(s:dmsync.dwin), 'winbufnr(v:val) ==
\s:dmsync.ebuf[k == 1 ? 0 : -1]')
let win[k] = (len(w) > 1 &&
\index(w, cwin) != -1) ? cwin : w[0]
endfor
" then set diffexpr to be called soon
if !exists('s:save_dex')
let s:save_dex = &diffexpr
endif
let &diffexpr = 'diffchar#DiffModeSyncExpr(' . string(win) . ')'
" intialize here to be prepared for the next diff session
call s:ClearDiffModeSync()
endif
endfunction
function! s:ClearDiffModeSync()
unlet! s:dmsync
au! dchar ShellFilterPost *
endfunction
function! diffchar#DiffModeSyncExpr(win)
" call saved diffexpr or DiffCharExpr() if empty
call eval(empty(s:save_dex) ? 'diffchar#DiffCharExpr()' : s:save_dex)
" clear current diffchar highlights if present
if exists('t:DChar')
call s:RefreshDiffCharWID()
let cwin = winnr()
if index(values(t:DChar.win), cwin) != -1
call diffchar#ResetDiffChar(range(1, line('$')))
else
let save_ei = &eventignore | let &eventignore = 'all'
exec t:DChar.win[1] . 'wincmd w'
call diffchar#ResetDiffChar(range(1, line('$')))
exec cwin . 'wincmd w'
let &eventignore = save_ei
endif
endif
" find 'c' command and extract the line to be changed
let [c1, c2] = [[], []]
for ct in filter(readfile(v:fname_out), 'v:val =~ "^\\d.*c"')
let [p1, p2] = map(split(ct, 'c'), 'split(v:val, ",")')
let cn = min([len(p1) == 1 ? 0 : p1[1] - p1[0],
\len(p2) == 1 ? 0 : p2[1] - p2[0]])
let [c1, c2] += [range(p1[0], p1[0] + cn),
\range(p2[0], p2[0] + cn)]
endfor
" if there are changed lines and initialize successes
if !empty(c1) && !empty(c2) && s:InitializeDiffChar() != -1
" change window numbers and diff lines
let t:DChar.win = a:win
let t:DChar.vdl = {1: c1, 2: c2}
" highlight the diff changed lines
call s:MarkDiffCharWID(1)
let cwin = winnr()
if index(values(t:DChar.win), cwin) != -1
call diffchar#ShowDiffChar(range(1, line('$')))
else
let save_ei = &eventignore | let &eventignore = 'all'
exec t:DChar.win[1] . 'wincmd w'
call diffchar#ShowDiffChar(range(1, line('$')))
exec cwin . 'wincmd w'
let &eventignore = save_ei
endif
endif
" resume back to the original diffexpr
if exists('s:save_dex')
let &diffexpr = s:save_dex
unlet s:save_dex
endif
endfunction
function! s:ApplyInternalAlgorithm(f1, f2)
" read both files to be diff traced
let [f1, f2] = [readfile(a:f1), readfile(a:f2)]
" handle icase and iwhite diff options
let save_igc = &ignorecase
let &ignorecase = (&diffopt =~ 'icase')
if &diffopt =~ 'iwhite'
for k in [1, 2]
call map(f{k}, 'substitute(v:val, "\\s\\+", " ", "g")')
call map(f{k}, 'substitute(v:val, "\\s\\+$", "", "")')
endfor
endif
" trace the diff lines between f1/f2 until the end time
let ses = s:TraceDiffChar(f1, f2, str2float(reltimestr(reltime())) +
\(exists('t:DiffSplitTime') ?
\t:DiffSplitTime : g:DiffSplitTime) / 1000.0)
" restore ignorecase flag
let &ignorecase = save_igc
" if timeout, return here with empty result
if ses == '*' | return [] | endif
let dfcmd = []
let [l1, l2] = [1, 1]
for ed in split(ses, '\%(=\+\|[+-]\+\)\zs')
let qn = len(ed)
if ed[0] == '=' " one or more '='
let [l1, l2] += [qn, qn]
else " one or more '[+-]'
let q1 = len(escape(ed, '-')) - qn
let q2 = qn - q1
let dfcmd += [
\((q1 > 1) ? l1 . ',' : '') . (l1 + q1 - 1) .
\((q1 == 0) ? 'a' : (q2 == 0) ? 'd' : 'c') .
\((q2 > 1) ? l2 . ',' : '') . (l2 + q2 - 1)]
let [l1, l2] += [q1, q2]
endif
endfor
return dfcmd
endfunction
function! s:ApplyDiffCommand(f1, f2)
" execute a diff command
let opt = '-a --binary '
if &diffopt =~ 'icase' | let opt .= '-i ' | endif
if &diffopt =~ 'iwhite' | let opt .= '-b ' | endif
if exists('g:DiffOptions') | let opt .= g:DiffOptions . ' ' | endif
" return diff commands only
return filter(split(system('diff ' . opt . a:f1 . ' ' . a:f2), '\n'),
\'v:val[0] =~ "\\d"')
endfunction
function! s:InitializeDiffChar()
if min(tabpagebuflist()) == max(tabpagebuflist())
echo 'Need more buffers displayed on this tab page!'
return -1
endif
" define a DiffChar dictionary on this tab page
let t:DChar = {}
" select current window and next (diff mode if available) window
" whose buffer is different
let t:DChar.win = {}
let cwin = winnr()
let nwin = filter(range(cwin + 1, winnr('$')) + range(1, cwin - 1),
\'winbufnr(v:val) != winbufnr(cwin)')
let dwin = s:ValidDiffModeWins(copy(nwin))
let [t:DChar.win[1], t:DChar.win[2]] =
\[cwin, empty(dwin) ? nwin[0] : dwin[0]]
call s:MarkDiffCharWID(1)
" set highlight groups used for diffchar on this tab page
let t:DChar.dhl = {'A': 'DiffAdd', 'C': 'DiffChange',
\'D': 'DiffDelete', 'T': 'DiffText', 'Z': '_DiffDelPos',
\'U': has('gui_running') ? 'Cursor' : 'VertSplit'}
" find corresponding DiffChange/DiffText lines on diff mode windows
if len(s:ValidDiffModeWins(values(t:DChar.win))) == 2
let t:DChar.vdl = {}
let dh = [hlID(t:DChar.dhl.C), hlID(t:DChar.dhl.T)]
let save_ei = &eventignore | let &eventignore = 'all'
for k in [1, 2]
exec t:DChar.win[k] . 'wincmd w'
call diff_hlID(0, 0) " a workaround for vim defect
let t:DChar.vdl[k] = filter(range(1, line('$')),
\'index(dh, diff_hlID(v:val, 1)) != -1')
if empty(t:DChar.vdl[k])
unlet t:DChar.vdl
break
endif
endfor
exec cwin . 'wincmd w'
let &eventignore = save_ei
endif
" set ignorecase and ignorespace flags
let t:DChar.igc = (&diffopt =~ 'icase')
let t:DChar.igs = (&diffopt =~ 'iwhite')
" set line and its highlight id record
let t:DChar.mid = {}
let [t:DChar.mid[1], t:DChar.mid[2]] = [{}, {}]
" set highlighted lines and columns record
let t:DChar.hlc = {}
let [t:DChar.hlc[1], t:DChar.hlc[2]] = [{}, {}]
" set a difference unit type on this tab page and set a split pattern
let du = exists('t:DiffUnit') ? t:DiffUnit : g:DiffUnit
if du == 'Word1' " \w\+ word and any \W character
let t:DChar.usp = t:DChar.igs ? '\%(\s\+\|\w\+\|\W\)\zs' :
\'\%(\w\+\|\W\)\zs'
elseif du == 'Word2' " non-space and space words
let t:DChar.usp = '\%(\s\+\|\S\+\)\zs'
elseif du == 'Word3' " \< or \> boundaries
let t:DChar.usp = '\<\|\>'
elseif du == 'Char' " any single character
let t:DChar.usp = t:DChar.igs ? '\%(\s\+\|.\)\zs' : '\zs'
elseif du =~ '^CSV(.\+)$' " split characters
let s = escape(du[4 : -2], '^-]')
let t:DChar.usp = '\%([^'. s . ']\+\|[' . s . ']\)\zs'
elseif du =~ '^SRE(.\+)$' " split regular expression
let t:DChar.usp = du[4 : -2]
else
let t:DChar.usp = t:DChar.igs ? '\%(\s\+\|\w\+\|\W\)\zs' :
\'\%(\w\+\|\W\)\zs'
echo 'Not a valid difference unit type. Use "Word1" instead.'
endif
" set a difference unit updating on this tab page
" and a record of line values and number of total lines
if exists('##TextChanged') && exists('##TextChangedI')
if exists('t:DiffUpdate') ? t:DiffUpdate : g:DiffUpdate
let t:DChar.lsv = {}
let [t:DChar.lsv[1], t:DChar.lsv[2]] = [{}, {}]
endif
endif
" Set a time length (ms) to apply the internal algorithm first
let t:DChar.slt = exists('t:DiffSplitTime') ?
\t:DiffSplitTime : g:DiffSplitTime
" Set a diff mode synchronization flag
let t:DChar.dsy = exists('t:DiffModeSync') ?
\t:DiffModeSync : g:DiffModeSync
" set a matching pair cursor id on this tab page
let t:DChar.pci = {}
" set a difference matching colors on this tab page
let dc = exists('t:DiffColors') ? t:DiffColors : g:DiffColors
let t:DChar.dmc = [t:DChar.dhl.T]
if dc == 1
let t:DChar.dmc += ['NonText', 'Search', 'VisualNOS']
elseif dc == 2
let t:DChar.dmc += ['NonText', 'Search', 'VisualNOS',
\'ErrorMsg', 'MoreMsg', 'TabLine', 'Title']
elseif dc == 3
let t:DChar.dmc += ['NonText', 'Search', 'VisualNOS',
\'ErrorMsg', 'MoreMsg', 'TabLine', 'Title',
\'StatusLine', 'WarningMsg', 'Conceal', 'SpecialKey',
\'ColorColumn', 'ModeMsg', 'SignColumn', 'Question']
elseif dc == 100
redir => hl | silent highlight | redir END
let h = map(filter(split(hl, '\n'),
\'v:val =~ "^\\S" && v:val =~ "="'), 'split(v:val)[0]')
for c in values(t:DChar.dhl)
let i = index(h, c) | if i != -1 | unlet h[i] | endif
endfor
while !empty(h)
let r = localtime() % len(h)
let t:DChar.dmc += [h[r]] | unlet h[r]
endwhile
endif
" define a specific highlight group to show a position
" of a deleted unit, _DiffDelPos = DiffChange +/- underline
exec 'silent highlight clear ' . t:DChar.dhl.Z
" get current DiffChange
redir => hl | exec 'silent highlight ' . t:DChar.dhl.C | redir END
let ha = {}
for [ky, ag] in map(filter(split(hl, '\%(\n\|\s\)\+'),
\'v:val =~ "="'), 'split(v:val, "=")')
let ha[ky] = ag
endfor
" add or delete a specific attribute (underline)
let at = 'underline'
let hm = has('gui_running') ? 'gui' : &t_Co > 1 ? 'cterm' : 'term'
let ha[hm] = !exists('ha[hm]') ? at :
\match(ha[hm], at) == -1 ? ha[hm] . ',' . at :
\substitute(ha[hm], at . ',\=\|,\=' . at, '', '')
" set as a highlight
exec 'silent highlight ' . t:DChar.dhl.Z . ' ' .
\join(values(map(filter(ha, '!empty(v:val)'),
\'v:key . "=" . v:val')))
endfunction
function! diffchar#ShowDiffChar(lines)
" initialize when t:DChar is not defined
if !exists('t:DChar')
if s:InitializeDiffChar() == -1 | return | endif
let first = 1
else
let first = empty(t:DChar.hlc[1]) || empty(t:DChar.hlc[2])
endif
" refresh window number of diffchar windows
call s:RefreshDiffCharWID()
" return if current window is not either of diffchar windows
let cwin = winnr()
for k in [1, 2, 0]
if k == 0 | return | endif
if t:DChar.win[k] == cwin | break | endif
endfor
" set a possible DiffChar line list among a:lines
let [d1, d2] = exists('t:DChar.vdl') ?
\s:DiffModeLines(k, a:lines) : [copy(a:lines), copy(a:lines)]
" remove already highlighted lines and get those text
for k in [1, 2]
let hl = map(keys(t:DChar.hlc[k]), 'eval(v:val)')
call filter(d{k}, 'index(hl, v:val) == -1')
let u{k} = map(copy(d{k}),
\'getbufline(winbufnr(t:DChar.win[k]), v:val)[0]')
let n{k} = len(u{k})
endfor
" remove redundant lines in either window
if n1 > n2
unlet u1[n2 - n1 :] | unlet d1[n2 - n1 :] | let n1 = n2
elseif n1 < n2
unlet u2[n1 - n2 :] | unlet d2[n1 - n2 :] | let n2 = n1
endif
" set ignorecase flag
let save_igc = &ignorecase
let &ignorecase = t:DChar.igc
for n in range(n1 - 1, 0, -1)
if t:DChar.igs
" delete \s\+ at line end
let u1[n] = substitute(u1[n], '\s\+$', '', '')
let u2[n] = substitute(u2[n], '\s\+$', '', '')
endif
if u1[n] == u2[n]
" remove equivalent lines
unlet u1[n] | unlet d1[n]
unlet u2[n] | unlet d2[n]
let [n1, n2] -= [1, 1]
endif
endfor
if n1 == 0
if empty(t:DChar.hlc[1]) || empty(t:DChar.hlc[2])
call s:MarkDiffCharWID(0)
unlet t:DChar
endif
let &ignorecase = save_igc
return
endif
" a list of actual difference units for tracing
call map(u1, 'split(v:val, t:DChar.usp)')
call map(u2, 'split(v:val, t:DChar.usp)')
" a list of different lines and columns
let [lc1, lc2] = [{}, {}]
let cmp = 0
for fn in ['TraceWithInternalAlgorithm', 'TraceWithDiffCommand']
" trace with this plugin's algorithm first,
" if timeout, split to the diff command
for [ln, cx] in items(s:{fn}(u1[cmp :], u2[cmp :]))
let [lc1[d1[cmp + ln]], lc2[d2[cmp + ln]]] =
\[cx[0], cx[1]]
endfor
let cmp = len(lc1)
if cmp >= n1 | break | endif
endfor
call filter(lc1, '!empty(v:val)')
call filter(lc2, '!empty(v:val)')
" restore ignorecase flag
let &ignorecase = save_igc
" highlight lines and columns
let save_ei = &eventignore | let &eventignore = 'all'
for k in [1, 2]
let buf{k} = winbufnr(t:DChar.win[k])
exec t:DChar.win[k] . 'wincmd w'
call s:HighlightDiffChar(k, lc{k})
if exists('t:DChar.lsv')
call extend(t:DChar.lsv[k], s:LinesValues(k,
\map(keys(lc{k}), 'eval(v:val)')))
let t:DChar.lsv[k][0] = line('$')
endif
endfor
exec cwin . 'wincmd w'
let &eventignore = save_ei
if empty(t:DChar.hlc[1]) || empty(t:DChar.hlc[2])
call s:MarkDiffCharWID(0)
unlet t:DChar
return
endif
" if not the first call in this tab page, return here
if !first | return | endif
" set events in each buffer
for k in [1, 2]
exec 'au! dchar BufWinLeave <buffer=' . buf{k} .
\'> call diffchar#ResetDiffChar(range(1, line("$")))'
if exists('##QuitPre')
exec 'au! dchar QuitPre <buffer=' . buf{k} .
\'> call s:SwitchDiffChar()'
endif
endfor
if exists('t:DChar.lsv')
for k in [1, 2]
exec 'au! dchar TextChanged <buffer=' . buf{k} .
\'> call s:UpdateDiffChar(' . k . ', "n")'
exec 'au! dchar TextChangedI <buffer=' . buf{k} .
\'> call s:UpdateDiffChar(' . k . ', "i")'
endfor
endif
if t:DChar.dsy && exists('t:DChar.vdl')
for k in [1, 2]
exec 'au! dchar CursorHold <buffer=' . buf{k} .
\'> call s:ResetSwitchDiffModeSync(' . k . ')'
endfor
if !exists('s:save_ut') &&
\len(filter(map(range(1, tabpagenr('$')),
\'gettabvar(v:val, "DChar")'),
\'!empty(v:val) && v:val.dsy &&
\exists("v:val.vdl")')) == 1
let s:save_ut = &updatetime
let &updatetime = 500
endif
endif
if has('patch-7.4.682')
call s:ToggleDiffHL(1)
endif
endfunction
function! s:TraceWithInternalAlgorithm(u1, u2)
" a list of commands with byte index per line
let cbx = {}
" set an end time for diff tracing
let et = str2float(reltimestr(reltime())) + t:DChar.slt / 1000.0
" compare each line and trace difference units
for ln in range(len(a:u1))
if t:DChar.igs
" convert \s\+ to a single space
let [u1, u2] = [map(copy(a:u1[ln]), 'substitute(
\v:val, "\\s\\+", " ", "g")'),
\map(copy(a:u2[ln]), 'substitute(
\v:val, "\\s\\+", " ", "g")')]
else
let [u1, u2] = [a:u1[ln], a:u2[ln]]
endif
" get edit script
let es = s:TraceDiffChar(u1, u2, et)
" if timeout, break here
if es == '*' | break | endif
let cbx[ln] = s:GetComWithByteIdx(es, a:u1[ln], a:u2[ln])
endfor
return cbx
endfunction
function! s:TraceWithDiffCommand(u1, u2)
" prepare 2 input files for diff
let lns = '|'
for k in [1, 2]
" add '<line number>:' at the beginning of each unit,
" enclose each line with '<line number>{<id>' and
" '<line number>}<id>', and insert '|' between lines
let g{k} = []
let p = -1 | let p{k} = [] " line separator position
for n in range(len(a:u{k}))
let l = n + 1
let g = [l . '{' . k] +
\map(copy(a:u{k}[n]), 'l . ":" . v:val') +
\[l . '}' . k] + [lns]
let g{k} += g
let p += len(g) | let p{k} += [p]
endfor
unlet g{k}[-1]
unlet p{k}[-1]
" write to a temp file for diff command
let f{k} = tempname() | call writefile(g{k}, f{k})
" initialize a list of edit symbols [=+-#] for each unit
call map(g{k}, '"="')
endfor
" call diff and get output as a list
let opt = '-a --binary '
if t:DChar.igc | let opt .= '-i ' | endif
if t:DChar.igs | let opt .= '-b ' | endif
if exists('g:DiffOptions') | let opt .= g:DiffOptions . ' ' | endif
let dfo = split(system('diff ' . opt . f1 . ' ' . f2), '\n')
call delete(f1) | call delete(f2)
" assign edit symbols [=+-#] to each unit
for dc in filter(dfo, 'v:val[0] =~ "\\d"')
let [se1, op, se2] = split(substitute(dc, '\a', ' & ', ''))
let [s1, e1] = (se1 =~ ',') ? split(se1, ',') : [se1, se1]
let [s2, e2] = (se2 =~ ',') ? split(se2, ',') : [se2, se2]
let [s1, e1, s2, e2] -= [1, 1, 1, 1]
if op == 'c'
let g1[s1 : e1] = repeat(['-'], e1 - s1 + 1)
let g2[s2 : e2] = repeat(['+'], e2 - s2 + 1)
elseif op == 'd'
let g1[s1 : e1] = repeat(['-'], e1 - s1 + 1)
let g2[s2] .= '#' " append add/del position mark
else "if op == 'a'
let g1[s1] .= '#' " append add/del position mark
let g2[s2 : e2] = repeat(['+'], e2 - s2 + 1)
endif
endfor
" separate lines and divide units
for k in [1, 2]
for p in p{k} | let g{k}[p] = lns | endfor
let g{k} = map(split(join(g{k}, ''), lns),
\'split(v:val, "\\%(=\\+\\|[+-]\\+\\|#\\)\\zs")')
endfor
" a list of commands with byte index per line
let cbx = {}
for ln in range(len(g1))
call map(g1[ln], 'v:val[0] == "#" ? "" : v:val')
call map(g2[ln], 'v:val[0] == "+" ? v:val : ""')
let es = join(map(g1[ln], 'v:val . g2[ln][v:key]'), '')
" delete the first and last [+-] of line begin/end symbols
let es = substitute(es, '^[^+]*\zs+\|+\ze[^+]*$', '', 'g')
let es = substitute(es, '^[^-]*\zs-\|-\ze[^-]*$', '', 'g')
let cbx[ln] = s:GetComWithByteIdx(es, a:u1[ln], a:u2[ln])
endfor
return cbx
endfunction
function! s:GetComWithByteIdx(es, u1, u2)
let [c1, c2] = [[], []]
let [l1, l2, p1, p2] = [1, 1, 0, 0]
for ed in split(a:es, '\%(=\+\|[+-]\+\)\zs')
let qn = len(ed)
if ed[0] == '=' " one or more '='
for k in [1, 2]
let [l{k}, p{k}] += [len(join(a:u{k}[p{k} :
\p{k} + qn - 1], '')), qn]
endfor
else " one or more '[+-]'
let q1 = len(escape(ed, '-')) - qn
let q2 = qn - q1
for k in [1, 2]
if q{k} > 0
let r = len(join(a:u{k}[p{k} :
\p{k} + q{k} - 1], ''))
let h{k} = [l{k}, l{k} + r - 1]
let [l{k}, p{k}] += [r, q{k}]
else
let h{k} = [l{k} - (0 < p{k} ?
\len(split(a:u{k}[p{k} - 1],
\'\zs')[-1]) : 0),
\l{k} + (p{k} < len(a:u{k}) ?
\len(split(a:u{k}[p{k}],
\'\zs')[0]) : 0) - 1]
endif
endfor
let [r1, r2] = (q1 == 0) ? ['d', 'a'] :
\(q2 == 0) ? ['a', 'd'] : ['c', 'c']
let [c1, c2] += [[[r1, h1]], [[r2, h2]]]
endif
endfor
return [c1, c2]
endfunction
function! diffchar#ResetDiffChar(lines)
if !exists('t:DChar') | return | endif
" refresh window number of diffchar windows
call s:RefreshDiffCharWID()
" return if current window is not either of diffchar windows
let cwin = winnr()
for k in [1, 2, 0]
if k == 0 | return | endif
if t:DChar.win[k] == cwin | break | endif
endfor
" set a possible DiffChar line list among a:lines
let [d1, d2] = exists('t:DChar.vdl') ?
\s:DiffModeLines(k, a:lines) : [copy(a:lines), copy(a:lines)]
" remove not highlighted lines
let save_ei = &eventignore | let &eventignore = 'all'
for k in [1, 2]
let hl = map(keys(t:DChar.hlc[k]), 'eval(v:val)')
call filter(d{k}, 'index(hl, v:val) != -1')
let buf{k} = winbufnr(t:DChar.win[k])
exec t:DChar.win[k] . 'wincmd w'
call s:ClearDiffChar(k, d{k})
call s:ResetDiffCharPair(k)
if exists('t:DChar.lsv')
call map(d{k}, 'remove(t:DChar.lsv[k], v:val)')
endif
endfor
exec cwin . 'wincmd w'
let &eventignore = save_ei
if !empty(t:DChar.hlc[1]) && !empty(t:DChar.hlc[2])
return
endif
" reset events and all when no highlight exists
for k in [1, 2]
exec 'au! dchar BufWinLeave <buffer=' . buf{k} . '>'
if exists('##QuitPre')
exec 'au! dchar QuitPre <buffer=' . buf{k} . '>'
endif
endfor
if exists('t:DChar.lsv')
for k in [1, 2]
exec 'au! dchar TextChanged,TextChangedI <buffer=' .
\buf{k} . '>'
endfor
endif
if t:DChar.dsy && exists('t:DChar.vdl')
for k in [1, 2]
exec 'au! dchar CursorHold <buffer=' . buf{k} . '>'
endfor
if exists('s:save_ut') &&
\len(filter(map(range(1, tabpagenr('$')),
\'gettabvar(v:val, "DChar")'),
\'!empty(v:val) && v:val.dsy &&
\exists("v:val.vdl")')) == 1
let &updatetime = s:save_ut
unlet s:save_ut
endif
endif
if has('patch-7.4.682')
call s:ToggleDiffHL(0)
endif
call s:MarkDiffCharWID(0)
unlet t:DChar
endfunction
function! diffchar#ToggleDiffChar(lines)
if exists('t:DChar')
call s:RefreshDiffCharWID()
for k in [1, 2, 0]
if k == 0 | return | endif
if t:DChar.win[k] == winnr() | 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! s:SwitchDiffChar()
" if diffchar is on one of split windows and when that window quits,
" catch QuitPre and switch to the rest (diff mode first) of the windows
call s:RefreshDiffCharWID()
let cwin = winnr()
let swin = filter(range(cwin + 1, winnr('$')) + range(1, cwin - 1),
\'winbufnr(v:val) == bufnr("%")')
if !empty(swin) && index(values(t:DChar.win), cwin) != -1
let win = t:DChar.win
call diffchar#ResetDiffChar(range(1, line('$')))
if s:InitializeDiffChar() != -1
let dwin = s:ValidDiffModeWins(swin)
let nwin = empty(dwin) ? swin[0] : dwin[0]
let t:DChar.win = map(win,
\'v:val == cwin ? nwin : v:val')
call s:MarkDiffCharWID(1)
let save_ei = &eventignore | let &eventignore = 'all'
exec nwin . 'wincmd w'
call diffchar#ShowDiffChar(range(1, line('$')))
let &eventignore = save_ei
endif
endif
endfunction
function! s:HighlightDiffChar(key, lec)
for [l, ec] in items(a:lec)
if has_key(t:DChar.mid[a:key], l) | continue | endif
let t:DChar.hlc[a:key][l] = ec
" collect all the column positions per highlight group
let ap = {}
let cn = 0
for [e, c] in ec
if e == 'c'
let hl = t:DChar.dmc[cn % len(t:DChar.dmc)]
let cn += 1
elseif e == 'a'
let hl = t:DChar.dhl.A
elseif e == 'd'
let hl = t:DChar.dhl.Z
endif
let ap[hl] = get(ap, hl, []) + [c]
endfor
" do highlightings on all the lines and columns
" with minimum matchaddpos() or one matchadd() call
if exists('*matchaddpos')
let t:DChar.mid[a:key][l] =
\[matchaddpos(t:DChar.dhl.C, [[l]], 0)]
for [hl, cp] in items(ap)
call map(cp, '[l, v:val[0],
\v:val[1] - v:val[0] + 1]')
while !empty(cp)
let t:DChar.mid[a:key][l] +=
\[matchaddpos(hl, cp[:7], 0)]
unlet cp[:7]
endwhile
endfor
else
let dl = '\%' . l . 'l'
let t:DChar.mid[a:key][l] =
\[matchadd(t:DChar.dhl.C, dl . '.', 0)]
for [hl, cp] in items(ap)
call map(cp, '"\\%>" . (v:val[0] - 1) .
\"c\\%<" . (v:val[1] + 1) . "c"')
let dc = len(cp) > 1 ?
\'\%(' . join(cp, '\|') . '\)' : cp[0]
let t:DChar.mid[a:key][l] +=
\[matchadd(hl, dl . dc, 0)]
endfor
endif
endfor
endfunction
function! s:ClearDiffChar(key, lines)
for l in a:lines
if has_key(t:DChar.mid[a:key], l)
call map(t:DChar.mid[a:key][l], 'matchdelete(v:val)')
unlet t:DChar.mid[a:key][l]
unlet t:DChar.hlc[a:key][l]
endif
endfor
endfunction
function! s:UpdateDiffChar(key, mode)
call s:RefreshDiffCharWID()
let cwin = winnr()
if cwin != t:DChar.win[a:key]
let save_ei = &eventignore | let &eventignore = 'all'
exec t:DChar.win[a:key] . 'wincmd w'
endif
" if number of lines was changed, reset all
if t:DChar.lsv[a:key][0] != line('$')
call diffchar#ResetDiffChar(
\range(1, max([t:DChar.lsv[a:key][0], line('$')])))
else
" find changed lines which were highlighted
let chl = map(keys(t:DChar.hlc[a:key]), 'eval(v:val)')
if a:mode == 'i'
call filter(chl, 'v:val == line(".")')
endif
let lsv = s:LinesValues(a:key, chl)
call filter(chl, 'lsv[v:val] != t:DChar.lsv[a:key][v:val]')
if !empty(chl)
" save the current t:DChar in case all hl can be reset
let sdc = deepcopy(t:DChar)
" reset hl of changed lines
call diffchar#ResetDiffChar(chl)
" if all hl was reset, restore saved t:DChar except hl
if !exists('t:DChar')
let [sdc.mid[1], sdc.mid[2]] = [{}, {}]
let [sdc.hlc[1], sdc.hlc[2]] = [{}, {}]
if exists('sdc.dtm') | unlet sdc.dtm | endif
if exists('sdc.lsv')
let [sdc.lsv[1], sdc.lsv[2]] = [{}, {}]
endif
let t:DChar = sdc
call s:MarkDiffCharWID(1)
endif
" show hl of changed lines
call diffchar#ShowDiffChar(chl)
" if hl lines are changed in diff mode, refresh diff HL
if has('patch-7.4.682') && exists('t:DChar.vdl') &&
\chl != map(keys(t:DChar.hlc[a:key]),
\'eval(v:val)')
call s:RestoreDiffHL()
call s:OverwriteDiffHL()
endif
endif
endif
if exists('save_ei')
exec cwin . 'wincmd w'
let &eventignore = save_ei
endif
endfunction
function! s:ResetSwitchDiffModeSync(key)
" when diff mode turns off on the current window, reset it
if !empty(s:ValidDiffModeWins([winnr()])) | return | endif
call s:RefreshDiffCharWID()
let cwin = winnr()
if cwin != t:DChar.win[a:key] | return | endif
let [win, vdl, dsy] = [t:DChar.win, t:DChar.vdl, t:DChar.dsy]
call diffchar#ResetDiffChar(range(1, line('$')))
" if there is another diff mode window of the same buffer and
" need to contine diff mode sync, switch to that window
if dsy
let bwin = s:ValidDiffModeWins(filter(range(1, winnr('$')),
\'winbufnr(v:val) == bufnr("%")'))
if !empty(bwin) && s:InitializeDiffChar() != -1
let t:DChar.win =
\map(win, 'v:key == a:key ? bwin[0] : v:val')
let t:DChar.vdl = vdl
call s:MarkDiffCharWID(1)
let save_ei = &eventignore | let &eventignore = 'all'
exec t:DChar.win[1] . 'wincmd w'
call diffchar#ShowDiffChar(range(1, line('$')))
exec cwin . 'wincmd w'
let &eventignore = save_ei
endif
endif
endfunction
function! s:DiffModeLines(key, lines)
" in diff mode, need to compare the different line between windows
" if current window is t:DChar.win[1], narrow a:lines within vdl[1]
" and get the corresponding lines from vdl[2]
let [d1, d2] = [copy(t:DChar.vdl[1]), copy(t:DChar.vdl[2])]
let [i, j] = (a:key == 1) ? [1, 2] : [2, 1]
call map(d{i}, 'index(a:lines, v:val) == -1 ? -1 : v:val')
call filter(d{j}, 'd{i}[v:key] != -1')
call filter(d{i}, 'v:val != -1')
return [d1, d2]
endfunction
function! s:LinesValues(key, lines)
let bnr = winbufnr(t:DChar.win[a:key])
return eval('{' . join(map(copy(a:lines), 'v:val . ":" .
\str2nr(sha256(getbufline(bnr, v:val)[0]), 16)'),
\',') . '}')
endfunction
function! s:ValidDiffModeWins(wlist)
" Try to use diffput to check if the diff mode is really valid or not.
let cwin = winnr()
let save_ei = &eventignore | let &eventignore = 'all'
let vdmw = []
for w in a:wlist
if getwinvar(w, '&diff')
exec w . 'wincmd w'
try
exec 'silent diffput 99999'
catch /^Vim(diffput):E99:/
" &diff == 1 but invalid diff mode
catch /^Vim(diffput):/
let vdmw += [w]
endtry
endif
endfor
exec cwin . 'wincmd w'
let &eventignore = save_ei
return vdmw
endfunction
function! diffchar#JumpDiffChar(dir, pos)
" dir : 1 = forward, 0 = backward
" pos : 1 = start, 0 = end
if !exists('t:DChar') | return | endif
" refresh window number of diffchar windows
call s:RefreshDiffCharWID()
" return if current window is not either of diffchar windows
let cwin = winnr()
for k in [1, 2, 0]
if k == 0 | return | endif
if t:DChar.win[k] == cwin | break | endif
endfor
let found = 0
let l = line('.')
while !found && 1 <= l && l <= line('$')
if has_key(t:DChar.hlc[k], l)
if l == line('.')
let c = col('.')
if !a:pos
" end pos workaround for multibyte char
let c += len(matchstr(getbufline(
\winbufnr(cwin), l)[0],
\'.', c - 1)) - 1
endif
else
let c = a:dir ? 0 : 99999
endif
let hc = map(copy(t:DChar.hlc[k][l]),
\'(v:val[0] == "d") ? "" :
\v:val[1][a:pos ? 0 : 1]')
if !a:dir
let c = - c
call map(reverse(hc),
\'empty(v:val) ? "" : - v:val')
endif
for n in range(len(hc))
if !empty(hc[n]) && c < hc[n]
let c = hc[n]
if !a:dir
let c = - c
let n = len(hc) - n - 1
endif
call cursor(l, c)
call s:ShowDiffCharPair(k, l, n, a:pos)
let found = 1
break
endif
endfor
endif
let l = a:dir ? l + 1 : l - 1
endwhile
endfunction
function! s:ShowDiffCharPair(key, line, col, pos)
" show cursor on deleted or matching unit on another window
let bkey = (a:key == 1) ? 2 : 1
if exists('t:DChar.vdl') " diff mode
let bline = t:DChar.vdl[bkey][index(t:DChar.vdl[a:key], a:line)]
else " non-diff mode
let bline = a:line
endif
let bl = getbufline(winbufnr(t:DChar.win[bkey]), bline)[0]
let co = t:DChar.hlc[bkey][bline][a:col][1]
let dc = bl[co[0] - 1 : co[1] - 1]
let save_ei = &eventignore | let &eventignore = 'all'
exec t:DChar.win[bkey] . 'wincmd w'
call s:ResetDiffCharPair(bkey)
let clen = len(split(dc, '\zs')[a:pos ? 0 : -1])
let cpos = a:pos ? co[0] : co[1] - clen + 1
if exists('*matchaddpos')
let t:DChar.pci[bkey] = matchaddpos(t:DChar.dhl.U,
\[[bline, cpos, clen]], 0)
else
let t:DChar.pci[bkey] = matchadd(t:DChar.dhl.U, '\%' . bline .
\'l\%>' . (cpos - 1) . 'c\%<' . (cpos + clen) . 'c', 0)
endif
exec 'au! dchar WinEnter <buffer=' . winbufnr(t:DChar.win[bkey]) .
\'> call s:ResetDiffCharPair(' . bkey . ')'
exec t:DChar.win[a:key] . 'wincmd w'
let &eventignore = save_ei
" echo the deleted and matching unit with its color
let [ed, co] = t:DChar.hlc[a:key][a:line][a:col]
if ed == 'a' " added unit
let bl = getbufline(winbufnr(t:DChar.win[a:key]), a:line)[0]
exec 'echohl ' . t:DChar.dhl.C
echon (1 < co[0]) ? split(bl[: co[0] - 2], '\zs')[-1] : ''
exec 'echohl ' . t:DChar.dhl.D
echon repeat('-', strwidth(bl[co[0] - 1 : co[1] - 1]))
exec 'echohl ' . t:DChar.dhl.C
echon (co[1] < len(bl)) ? split(bl[co[1] :], '\zs')[0] : ''
echohl None
elseif ed == 'c' " changed unit
exec 'echohl ' . t:DChar.dmc[(count(
\map(t:DChar.hlc[a:key][a:line][: a:col], 'v:val[0]'),
\'c') - 1) % len(t:DChar.dmc)]
echon dc
echohl None
endif
endfunction
function! s:ResetDiffCharPair(key)
if exists('t:DChar.pci[a:key]')
call matchdelete(t:DChar.pci[a:key])
unlet t:DChar.pci[a:key]
exec 'au! dchar WinEnter <buffer=' .
\winbufnr(t:DChar.win[a:key]) . '>'
echon ''
endif
endfunction
function! s:MarkDiffCharWID(on)
" mark w:DCharWID (1/2) on diffchar windows or delete them
for wvr in map(range(1, winnr('$')), 'getwinvar(v:val, "")')
if has_key(wvr, 'DCharWID') | unlet wvr.DCharWID | endif
endfor
if a:on
call map([1, 2],
\'setwinvar(t:DChar.win[v:val], "DCharWID", v:val)')
endif
endfunction
function! s:RefreshDiffCharWID()
" find diffchar windows and set their winnr to t:DChar.win again
let t:DChar.win = {}
for win in range(1, winnr('$'))
let id = get(getwinvar(win, ''), 'DCharWID', 0)
if id | let t:DChar.win[id] = win | endif
endfor
endfunction
" "An O(NP) Sequence Comparison Algorithm"
" by S.Wu, U.Manber, G.Myers and W.Miller
function! s:TraceDiffChar(u1, u2, ...)
let [l1, l2] = [len(a:u1), len(a:u2)]
if l1 == 0 && l2 == 0 | return ''
elseif l1 == 0 | return repeat('+', l2)
elseif l2 == 0 | return repeat('-', l1)
endif
" reverse to be M >= N
let [M, N, u1, u2, e1, e2] = (l1 >= l2) ?
\[l1, l2, a:u1, a:u2, '+', '-'] :
\[l2, l1, a:u2, a:u1, '-', '+']
let D = M - N
let fp = repeat([-1], M + N + 1)
let etree = [] " [next edit, previous p, previous k]
" check time limit when specified the end time
let ckt = (a:0 > 0) ? 'str2float(reltimestr(reltime())) > a:1' : 0
let p = -1
while fp[D] != M
" if timeout, return here with '*'
if eval(ckt) | return '*' | endif
let p += 1
let epk = repeat([[]], p * 2 + D + 1)
for k in range(-p, D - 1, 1) + range(D + p, D, -1)
let [x, 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 y = x - k
while x < M && y < N && u1[x] == u2[y]
let epk[k][0] .= '='
let [x, y] += [1, 1]
endwhile
let fp[k] = x
endfor
let etree += [epk]
endwhile
" create a shortest edit script (SES) from last p and k
let ses = ''
while p != 0 || k != 0
let [e, p, k] = etree[p][k]
let ses = e . ses
endwhile
let ses = etree[p][k][0] . ses
return ses[1:] " remove the first entry
endfunction
if has('patch-7.4.682')
function! s:ToggleDiffHL(on)
" no need in no-diff mode
if !exists('t:DChar.vdl') | return | endif
let tn = len(filter(map(range(1, tabpagenr('$')),
\'gettabvar(v:val, "DChar")'),
\'!empty(v:val) && exists("v:val.dtm")'))
if a:on
if tn == 0 " set event at first ON
au! dchar TabEnter * call s:AdjustHLOption()
endif
" disable hl option and overwrite DiffChange/DiffText area
call s:DisableHLOption()
call s:OverwriteDiffHL()
else
if tn == 1 " clear event at last OFF
au! dchar TabEnter *
endif
" restore hl option and DiffChange/DiffText area
call s:RestoreHLOption()
call s:RestoreDiffHL()
endif
endfunction
function! s:AdjustHLOption()
call eval(exists('t:DChar.vdl') ?
\'s:DisableHLOption()' : 's:RestoreHLOption()')
endfunction
function! s:DisableHLOption()
if !exists('s:save_hl')
let s:save_hl = &highlight
let &highlight = join(map(split(s:save_hl, ','),
\'v:val[0] =~# "[CT]" ? v:val[0] . "-" : v:val'), ',')
endif
endfunction
function! s:RestoreHLOption()
if exists('s:save_hl')
let &highlight = s:save_hl
unlet s:save_hl
endif
endfunction
function! s:OverwriteDiffHL()
" overwrite DiffChange/DiffText area with its match
if exists('t:DChar.dtm') | return | endif
let t:DChar.dtm = {}
let cwin = winnr()
let save_ei = &eventignore | let &eventignore = 'all'
for k in [1, 2]
exec t:DChar.win[k] . 'wincmd w'
let tl = []
if !exists('s:save_dex')
" normal case
let dt = hlID(t:DChar.dhl.T)
call diff_hlID(0, 0) " a workaround for vim defect
for l in t:DChar.vdl[k]
let t = filter(range(1, col([l, '$']) - 1),
\'diff_hlID(l, v:val) == dt')
if empty(t) | continue | endif
let [cs, ce] = [t[0], t[-1]]
let tl += [[l, cs, ce - cs + 1]]
endfor
else
" diffexpr exceptional case
for l in t:DChar.vdl[k]
let h = get(t:DChar.hlc[k], l, [])
if empty(h) | continue | endif
let cs = h[0][1][h[0][0] == 'd' ? 1 : 0]
let ce = h[-1][1][h[-1][0] == 'd' ? 0 : 1]
if cs > ce | continue | endif
let tl += [[l, cs, ce - cs + 1]]
endfor
endif
let t:DChar.dtm[k] = []
for hl in ['C', 'T']
let ll = (hl == 'C') ? t:DChar.vdl[k] : tl
let p = 0
while p < len(ll)
let t:DChar.dtm[k] += [matchaddpos(
\t:DChar.dhl[hl], ll[p : p + 7], -1)]
let p += 8
endwhile
endfor
endfor
exec cwin . 'wincmd w'
let &eventignore = save_ei
endfunction
function! s:RestoreDiffHL()
" delete all the overwritten DiffChange/DiffText matches
if !exists('t:DChar.dtm') | return | endif
let cwin = winnr()
let save_ei = &eventignore | let &eventignore = 'all'
for k in [1, 2]
exec t:DChar.win[k] . 'wincmd w'
call map(t:DChar.dtm[k], 'matchdelete(v:val)')
endfor
exec cwin . 'wincmd w'
let &eventignore = save_ei
unlet t:DChar.dtm
endfunction
endif
let &cpo = s:save_cpo
unlet s:save_cpo