" 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 changed first and last characters
" on changed lines. But this plugin will find the exact differences between
" them, character by character - so called DiffChar.
"
" For example, in diff mode: ([DiffText], )
"
" (file A) The [quick brown fox jumps over the lazy] dog.
" (file B) The [lazy fox jumps over the quick brown] dog.
"
" this plugin will exactly highlight the changed and added units:
"
" (file A) The [quick] fox jumps over the [lazy] dog.
" (file B) The [lazy] fox jumps over the [quick] dog.
"
" This plugin can be triggered after diff mode starts by pressing /
" or using SDChar command. At update 5.5, a new global variable,
" g:DiffModeSync, is introduced. Its default is "enable" and synchronously
" show/reset the highlights of the exact differences as soon as the diff
" mode starts/ends. It also works on your custom diff tool (e.g. git-diff)
" when specified to the diffexpr option.
"
" In diff mode, this plugin compares the corresponding changed lines between
" two windows. In non-diff mode, it compares the same lines between them.
"
" This plugin has been using "An O(NP) Sequence Comparison Algorithm"
" developed by S.Wu, et al., which always finds an optimum sequence quickly.
" But for longer lines and less-similar files, it takes time to complete the
" diff tracing. At update 5.2, this plugin splits the tracing with the diff
" command. Firstly applies the internal O(NP) algorithm. If not completed
" within the time specified by a g:DiffSplitTime global (and tabpage)
" variable, continuously switches to the diff command at that point, and
" then joins both results. This approach provides a stable performance and
" reasonable accuracy, because the diff command effectively optimizes
" between them. The default of its variable is 500 ms, which would be
" useful for smaller files. If prefer to always apply the internal algorithm
" for accuracy (or the diff command for performance) only, set some large
" value (or 0) to the variable.
"
" Since update 4.7, this plugin has set the DiffCharExpr() to the diffexpr
" option, if it is empty. this function would be rather useful for smaller
" files than the diff command. If the total number of lines on two windows
" <= 200, by default, this plugin's internal algorithm is used to make the
" diff faster. If prefer to leave the diffexpr option as empty, set 0 to
" g:DiffExpr.
"
" This plugin has been always positively supporting mulltibyte characters.
"
" Commands
" :[range]SDChar - Highlight difference units for [range]
" :[range]RDChar - Reset the highlight of difference units for [range]
"
" Configurable Keymaps
" ToggleDiffCharAllLines (default: )
" toggle the highlight/reset of difference units for all lines
" ToggleDiffCharCurrentLine (default: )
" toggle the highlight/reset of difference units for current line
" JumpDiffCharPrevStart (default: [b)
" jump cursor to the start position of the previous difference unit
" JumpDiffCharNextStart (default: ]b)
" jump cursor to the start position of the next difference unit
" JumpDiffCharPrevEnd (default: [e)
" jump cursor to the end position of the previous difference unit
" JumpDiffCharNextEnd (default: ]e)
" jump cursor to the end position of the next difference unit
"
" Global Variables (and Tabpage variables when using t:)
" g:DiffUnit - type of difference unit
" "Char" : any single character (default)
" "Word1" : \w\+ word and any \W single character
" "Word2" : non-space and space words
" "Word3" : \< or \> character class boundaries
" "CSV(,)" : separated by characters such as ',', ';', and '\t'
" g:DiffColors - matching colors for changed unit pairs
" 0 : always DiffText (default)
" 1 : 4 colors in fixed order
" 2 : 8 colors in fixed order
" 3 : 16 colors in fixed order
" 100 : all available colors in dynamic random order
" (notes : always DiffAdd for added units)
" g:DiffUpdate - interactively updating of highlightings while editing
" 1 : enable (default)
" 0 : disable
" (notes : available on vim 7.4)
" g:DiffSplitTime - a time length (ms) to apply the internal algorithm first
" 0 ~ : (500 as default)
" g:DiffModeSync - synchronously show/reset with diff mode
" 1 : enable (default)
" 0 : disable
" g:DiffExpr - set DiffCharExpr() to the diffexpr potion
" 1 : enable (default)
" 0 : disable
"
" DiffCharExpr(mxi) function for the diffexpr option
" mxi: the maximum number of total lines of both windows to apply internal
" algorithm, apply diff command when more lines
"
" Update : 5.5
" * Introduced g:DiffModeSync to synchronously show/reset the highlights as
" the diff mode starts/ends, which also works on your custom diff tool.
" * Changed to enable g:DiffUpdate as a default and then interactively update
" the highlights while editing.
" * Enhanced to draw and delete the highlights faster by specifying as many
" position parameters as possible in one matchaddpos() and matchadd() call.
" * Changed to select current window and next diff mode window (if present)
" whose buffer is different at initialize.
" * Fixed:
" - caused an error on getwinvar() in vim 7.3.
" - in non-gVim, did not show a matching pair cursor when jumping the
" cursor by using [e or ]b, depending on a color scheme.
" - sometimes failed to toggle the highlights when using or in
" diff mode windows.
" - did not interactively update the highlight of all the lines when
" multiple lines were changed at once if g:DiffUpdate = 1.
"
" Update : 5.4
" * Enhanced to show a position of a deleted unit with underline on its
" previous and next characters. This position is where a unit is added
" between those characters in another diffchar window.
" * Improved to be able to change this plugin's global variables anytime.
" * Changed to select current window and then the next (wincmd w) window
" whose buffer is different.
"
" Update : 5.3
" * Performance improved for long lines and some defects fixed when the diff
" command is used for the diff tracing.
"
" Update : 5.2
" * Enhanced to provide a stable performance even for less-similar long files.
" The new approach applies this plugin's algorithm first, and if not
" completed within the specified time, continuously splits the tracing with
" the diff command, and join both results.
" * Fixed: when diff command does not choose minimal algorithm and it shows
" the equivalent lines as "changed", this plugin sometimes makes an error.
" * Fixed: if file encoding is not same as buffer encoding, a difference may
" not be correctly detected in DiffCharExpr().
"
" Update : 5.1
" * Since vim 7.4.682, it has become impossible to overwrite the vim's diff
" highlights with this plugin. Then, for example, DiffText bold typeface
" will be left in all the diff highlighted lines (for more info, see
" https://groups.google.com/forum/?hl=en_US#!topic/vim_use/1jQnbTva2fY).
" This update provides a workaround to reduce its effect and to show the
" differences mostly same as before.
"
" Update : 5.0
" * Significantly improved the way to trace and show the differences and
" make them 1.5 ~ 2.0 times faster.
" * Introduced g:DiffMaxRatio (and t:DiffMaxRatio), a maximum difference
" ratio to trace (100% as default). Once exceeds, the diff tracing is
" recursively split and helps to keep performance instead of diff accuracy.
" * Discontinued other difference algorithms (OND and Basic) than the ONP,
" then g:DiffAlgorithm no longer supported.
" * Improved to allow to specify one or more characters for "CSV(c)" in
" g:DiffUnit (and t:DiffUnit). For example, "CSV(,:\t)" will split the
" units by a comma, colon, and tab. Use '\\' for a backslash.
"
" Update : 4.9
" * Fixed DiffCharExpr() to check the number of total lines, not different
" lines only, of both windows and apply either internal algorithm or
" external diff command, in order to keep the appropriate performance
" for large files.
"
" Update : 4.81
" * Enhanced to make DiffCharExpr() a bit faster by using uniq() or so.
"
" Update : 4.8
" * Enhanced to set the threshold value on DiffCharExpr() to check how many
" differences and then apply either of internal algorithm or external diff
" command. The default for diffexpr option using DiffCharExpr() is changed
" to use this threshold, 200 - apply internal if less than 200 differences,
" apply external if more.
" * Changed the way to select windows when more than 2 windows in the page.
" - automatically select the diff mode's next (wincmd w) window, if any,
" in addition to the current window
" - can select any of split windows as vim can do for diff
"
" Update : 4.7
" * Enhanced to set DiffCharExpr() to the diffexpr option, if it is empty.
" When diff mode begins, vim calls this function which finds differences by
" this plugin's internal diff algorithm (default) and then initially shows
" the exact differences (default). You can also explicitly set this function
" to the option with different arguments.
" * Enhanced to make the key mappings configurable.
" For example, the default can be modified by:
" nmap "your favorite key" ToggleDiffCharAllLines
" * Fixed to correctly adjust the position of difference units when diffopt's
" iwhite option is enabled.
"
" Update : 4.6
" * Fixed to correctly show the colors of changed units in one-by-one defined
" order of g:DiffColors. Since an added unit was improperly counted as
" changed one, some colors were skipped and not shown. The first changed
" unit is now always highlighted with DiffText in any color mode.
"
" Update : 4.5
" * Fixed to trace the differences until the end of the units. Previously
" the last same units were skipped, so last added units were sometimes shown
" as changed ones (eg: the last "swift brown" on above were shown as changed
" units but now shows "brown" as added ones).
" * Enhanced to use your global variables if defined in vimrc.
"
" Update : 4.4
" * Enhanced to follow diffopt's icase and iwhite options for both diff and
" non-diff modes (ignorecase option is not used). Previously, it has been
" always case and space/tab sensitive.
" * Implemented to highlight the difference units using a new matchaddpos()
" function, introduced in 7.4.330, when available to draw faster.
"
" Update : 4.3
" * Enhanced to differently show added/deleted/changed difference units
" with original diff highlightings.
" - added units will be always highlighted with DiffAdd.
" - changed units will be highlighted based on the g:DiffColors (and
" t:DiffColors) variable, but DiffText is always used for the first
" changed unit.
" - when jumping cursor by "[b"/"]b" or "[e"/"]e" on the added unit, it
" highlights around the corresponding deleted units with a cursor-type color
" in another window, and echoes a diff-delete filler with DiffDelete,
" along with common characters on both sides (e.g. a-----b).
"
" Update : 4.2
" * Enhanced to update the highlighted DiffChar units while editing.
" A g:DiffUpdate (and t:DiffUpdate) variable enables and disables (default)
" this update behavior. If a text line was added/deleted, reset all the
" highlightings. This feature is available on vim 7.4.
"
" Update : 4.1
" * Implemented to echo a matching difference unit with its color when jumping
" cursor by "[b"/"]b" or "[e"/"]e".
" * Fixed defects: not using the new uniq() function introduced in vim 7.4.
"
" Update : 4.0
" * Enhanced to easily find a corresponding pair of each difference unit.
" - each unit pair will be shown in individual same color on both windows.
" A g:DiffColors (and t:DiffColors) variable is a type of matching colors,
" 0 (default) for always 1 color as before, 1/2/3 for 4/8/16 colors in
" fixed order, and 100 for all available colors in dynamic random order.
" - when jumping cursor by "[b"/"]b" or "[e"/"]e" in either window,
" the start or end position of a matching unit will be highlighted with
" a cursor-type color in another window.
"
" Update : 3.6
" * Added two g:DiffUnit (and t:DiffUnit) types. "Word3" will split at the
" \< or \> boundaries, which can separate based on the character class like
" CJK, Hiragana, Katakana, Hangul, full width symbols and so on. And "CSV(c)"
" will split the units by a specified character "c". For example, "CSV(,)"
" and "CSV(\t)" can be used for comma and tab separated text.
"
" Update : 3.5
" * Fixed defects: DiffChar highlighting units do not override/hide hlsearch.
"
" Update : 3.4
" * Enhanced to support individual DiffChar handling on each tab page.
" Difference unit and algorithm can also be set page by page using
" tab page local variables, t:DiffUnit and t:DiffAlgorithm.
"
" Update : 3.3
" * Enhanced to jump cursor to the DiffChar highlighting units. Sample keymaps
" "]b" and "[b" will move cursor forwards to the next and backwards to the
" previous start positions. And "]e" and "[e" will move to the end positions.
"
" Update : 3.2
" * Enhanced to follow diff mode without any limitations. Compare between
" the corresponding DiffChange lines on both windows and properly handle
" DiffAdd and DiffDelete lines.
"
" Update : 3.1
" * Enhanced to show/reset/toggle DiffChar highlightings on individual line
" by line.
" * Implemented the window layout handling.
" - the DiffChar'ed windows will remain the highlightings even if the
" window position is rotated/replaced/moved and another new window opens.
" - if either DiffChar'ed window is closed, reset all the DiffChar
" highlightings on another window.
" * Removed limitations:
" - when more than 2 windows exist, current and next (wincmd w) windows
" will be selected.
" - if the specified numbers of lines are different in both windows, ignore
" the redundant lines and continue to compare the text on the same lines.
" - RDChar sample command has a range attribute (e.g. %RDChar).
" * Fixed defects:
" - reset just DiffChar highlightings only and remain others.
"
" Update : 3.0
" * Implemented word by word differences. A g:DiffUnit variable is a type of
" a difference unit. Its default is "Char", which will trace character
" by character as before. "Word1" will split into \w\+ words and
" any \W single characters. And "Word2" will separate the units at the
" \s\+ space boundaries.
" * Improved the performance around 10%.
"
" Update : 2.1
" * Coding changes in the O(NP) function for readability.
"
" Update : 2.0
" * Implemented the O(NP) and O(ND) Difference algorithms to improve the
" performance. This update uses the O(NP) by default, and can be changed
" to the O(ND) if necessary, or to the basic algorithm implemented in
" the initial version.
"
" Author: Rick Howe
" Last Change: 2015/11/23
" Created:
" Requires:
" Version: 5.5
if exists("g:loaded_diffchar")
finish
endif
let g:loaded_diffchar = 5.5
let s:save_cpo = &cpo
set cpo&vim
" Commands
command! -range SDChar call s:ShowDiffChar(range(, ))
command! -range RDChar call s:ResetDiffChar(range(, ))
" Configurable Keymaps
nnoremap ToggleDiffCharAllLines
\ :call ToggleDiffChar(range(1, line('$')))
nnoremap ToggleDiffCharCurrentLine
\ :call ToggleDiffChar([line('.')])
nnoremap JumpDiffCharPrevStart
\ :call JumpDiffChar(0, 1)
nnoremap JumpDiffCharNextStart
\ :call JumpDiffChar(1, 1)
nnoremap JumpDiffCharPrevEnd
\ :call JumpDiffChar(0, 0)
nnoremap JumpDiffCharNextEnd
\ :call JumpDiffChar(1, 0)
if !hasmapto('ToggleDiffCharAllLines', 'n')
nmap ToggleDiffCharAllLines
endif
if !hasmapto('ToggleDiffCharCurrentLine', 'n')
nmap ToggleDiffCharCurrentLine
endif
if !hasmapto('JumpDiffCharPrevStart', 'n')
nmap [b JumpDiffCharPrevStart
endif
if !hasmapto('JumpDiffCharNextStart', 'n')
nmap ]b JumpDiffCharNextStart
endif
if !hasmapto('JumpDiffCharPrevEnd', 'n')
nmap [e JumpDiffCharPrevEnd
endif
if !hasmapto('JumpDiffCharNextEnd', 'n')
nmap ]e JumpDiffCharNextEnd
endif
" Set a difference unit type
if !exists("g:DiffUnit")
let g:DiffUnit = "Char" " any single character
" let g:DiffUnit = "Word1" " \w\+ word and any \W single character
" let g:DiffUnit = "Word2" " non-space and space words
" let g:DiffUnit = "Word3" " \< or \> character class boundaries
" let g:DiffUnit = "CSV(,)" " split characters
endif
" Set a difference unit matching colors
if !exists("g:DiffColors")
let g:DiffColors = 0 " always 1 color
" let g:DiffColors = 1 " 4 colors in fixed order
" let g:DiffColors = 2 " 8 colors in fixed order
" let g:DiffColors = 3 " 16 colors in fixed order
" let g:DiffColors = 100 " all available colors in dynamic random order
endif
" Set a difference unit updating while editing
if exists("##TextChanged") && exists("##TextChangedI")
if !exists("g:DiffUpdate")
let g:DiffUpdate = 1 " enable
" let g:DiffUpdate = 0 " disable
endif
endif
" Set a time length (ms) to apply this plugin's internal algorithm first
if !exists("g:DiffSplitTime")
let g:DiffSplitTime = 500 " when timeout, split to diff command
" let g:DiffSplitTime = 0 " always apply diff command only
endif
" Set a diff mode synchronization to show/reset exact differences
if !exists("g:DiffModeSync")
let g:DiffModeSync = 1 " enable
" let g:DiffModeSync = 0 " disable
endif
" Set this plugin's DiffCharExpr() to the diffexpr option if empty
if !exists("g:DiffExpr")
let g:DiffExpr = 1 " enable
" let g:DiffExpr = 0 " disable
endif
if g:DiffExpr && empty(&diffexpr)
let &diffexpr = "DiffCharExpr(200)" " use internal algo if <= 200 lines
" let &diffexpr = "DiffCharExpr(0)" " use diff command only
endif
" Set an event group of this plugin
augroup dchar
au!
au! FilterWritePre * call s:SetDiffModeSync()
augroup END
function! DiffCharExpr(mxi)
" read both files to be diff traced
let [f1, f2] = [readfile(v:fname_in), readfile(v:fname_new)]
" find the fist diff trial call and return here
if [f1, f2] == [["line1"], ["line2"]]
call writefile(["1c1"], v:fname_out)
return
endif
" get a list of diff commands and write to output file
call writefile(len(f1 + f2) > a:mxi ? s:ApplyDiffCommand() :
\s:ApplyInternalAlgorithm(f1, f2), v:fname_out)
endfunction
function! s:SetDiffModeSync()
if !g:DiffModeSync | return | endif
" set current bufnr, actual diff winnr, actual diff bufnr
let cbuf = bufnr('%')
let dwin = filter(range(1, winnr('$')), 'getwinvar(v:val, "&diff")')
let dbuf = map(copy(dwin), 'winbufnr(v:val)')
" reset bufnr record
if exists("s:save_dex")
" diffexpr was not invoked in last prepare, meaning it was
" the first event in one diff session, reset except last one
if len(s:diffbuf) > 1 | unlet s:diffbuf[:-2] | endif
endif
if !exists("s:diffbuf") || index(s:diffbuf, cbuf) != -1 ||
\!empty(filter(copy(s:diffbuf), 'index(dbuf, v:val) == -1'))
" this is the first event in one diff session, reset all
let s:diffbuf = []
endif
" append current bufnr where the event happens
let s:diffbuf += [cbuf]
" run each time when event happens on 2 buffers among one session
if len(s:diffbuf) > 1
" find winnr of v:fname_in(s:diffbuf[0]) and v:fname_new([-1])
let win = {}
let cwin = winnr()
for k in [1, 2]
let w = filter(copy(dwin), 'winbufnr(v:val) ==
\s:diffbuf[k == 1 ? 0 : -1]')
let win[k] = (len(w) > 1 &&
\index(w, cwin) != -1) ? cwin : w[0]
endfor
" prepare diffexpr to be called soon
if !exists("s:save_dex")
let s:save_dex = &diffexpr
endif
let &diffexpr = "DiffModeSyncExpr(" . string(win) . ")"
endif
endfunction
function! DiffModeSyncExpr(win)
" call saved diffexpr or diff command if empty
call eval(empty(s:save_dex) ? "DiffCharExpr(0)" : 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 s:ResetDiffChar(range(1, line('$')))
else
let save_ei = &eventignore | let &eventignore = "all"
exec t:DChar.win[1] . "wincmd w"
call s: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 s:ShowDiffChar(range(1, line('$')))
else
let save_ei = &eventignore | let &eventignore = "all"
exec t:DChar.win[1] . "wincmd w"
call s: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)
" handle icase and iwhite diff options
let save_igc = &ignorecase
let &ignorecase = (&diffopt =~ "icase")
if &diffopt =~ "iwhite"
for k in [1, 2]
let f{k} = copy(a:f{k})
call map(f{k}, 'substitute(v:val, "\\s\\+", " ", "g")')
call map(f{k}, 'substitute(v:val, "\\s\\+$", "", "")')
endfor
else
let [f1, f2] = [a:f1, a:f2]
endif
" trace the diff lines between f1/f2
let dfcmd = []
let [l1, l2] = [1, 1]
for ed in split(s:TraceDiffChar(f1, f2), '\%(=\+\|[+-]\+\)\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 == 0) ? (l1 - 1) : (q1 == 1) ?
\l1 : l1 . ',' . (l1 + q1 - 1)) .
\((q1 == 0) ? 'a' : (q2 == 0) ? 'd' : 'c') .
\((q2 == 0) ? (l2 - 1) : (q2 == 1) ?
\l2 : l2 . ',' . (l2 + q2 - 1))]
let [l1, l2] += [q1, q2]
endif
endfor
" restore ignorecase flag
let &ignorecase = save_igc
return dfcmd
endfunction
function! s:ApplyDiffCommand()
" execute a diff command
let opt = "-a --binary "
if &diffopt =~ "icase" | let opt .= "-i " | endif
if &diffopt =~ "iwhite" | let opt .= "-b " | endif
" return diff commands only
return filter(split(
\system("diff " . opt . v:fname_in . " " . v:fname_new),
\'\n'), 'v:val =~ "^\\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 = -1
for w in range(cwin + 1, winnr('$')) + range(1, cwin - 1)
if winbufnr(w) != winbufnr(cwin)
if nwin == -1 | let nwin = w | endif
if getwinvar(w, "&diff")
let nwin = w
break
endif
endif
endfor
let [t:DChar.win[1], t:DChar.win[2]] = [cwin, nwin]
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" : "CursorColumn"}
" find corresponding DiffChange/DiffText lines on diff mode windows
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]
if getwinvar(t:DChar.win[k], "&diff")
exec t:DChar.win[k] . "wincmd w"
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
else
unlet t:DChar.vdl
break
endif
endfor
exec cwin . "wincmd w"
let &eventignore = save_ei
" 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 == "Char" " any single character
let t:DChar.usp = t:DChar.igs ? '\%(\s\+\|.\)\zs' : '\zs'
elseif 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 =~ '^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\+\|.\)\zs' : '\zs'
echo 'Not a valid difference unit type. Use "Char" 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 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! s:ShowDiffChar(lines)
" initialize when t:DChar is not defined
if !exists("t:DChar") && s:InitializeDiffChar() == -1 | 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 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} = []
for d in d{k}
let u{k} += getbufline(winbufnr(t:DChar.win[k]), d)
endfor
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
" remove equivalent lines
for n in range(n1 - 1, 0, -1)
if u1[n] == u2[n]
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
" restore ignorecase flag
let &ignorecase = save_igc
let buf = {}
" 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
" set events in each buffer
for k in [1, 2]
exec "au! dchar BufWinLeave call s:ResetDiffChar(range(1, line('$')))"
endfor
if exists("t:DChar.lsv")
for k in [1, 2]
exec "au! dchar TextChanged,TextChangedI call s:UpdateDiffChar(" . k . ")"
endfor
endif
if g:DiffModeSync && exists("t:DChar.vdl")
for k in [1, 2]
exec "au! dchar CursorHold call s:ResetSwitchDiffModeSync(" . k . ")"
endfor
if !exists("s:save_ut") && len(filter(range(1, tabpagenr('$')),
\'!empty(gettabvar(v:val, "DChar"))')) == 1
let s:save_ut = &updatetime
let &updatetime = 1
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 = {}
" start timer
let st = reltime()
" compare each line and trace difference units
for ln in range(len(a:u1))
" if timeout, break here
if str2float(reltimestr(reltime(st))) > t:DChar.slt / 1000.0
break
endif
" set unit lists for tracing
let [u1, u2] = [a:u1[ln], a:u2[ln]]
let [u1t, u2t] = [u1, u2]
" handle ignorespace option
if t:DChar.igs
for k in [1, 2]
if !empty(u{k}t)
" convert \s\+ to a single space
call map(u{k}t, 'substitute
\(v:val, "\\s\\+", " ", "g")')
" remove/unlet the last \s\+$
let u{k}t[-1] = substitute
\(u{k}t[-1], '\s\+$', '', '')
if empty(u{k}t[-1])
unlet u{k}t[-1]
endif
endif
endfor
endif
" skip diff tracing if no diff exists
if u1t == u2t | continue | endif
" start diff tracing
let [c1, c2] = [[], []]
let [l1, l2, p1, p2] = [1, 1, 0, 0]
for ed in split(s:TraceDiffChar(u1t, u2t),
\'\%(=\+\|[+-]\+\)\zs')
let qn = len(ed)
if ed[0] == '=' " one or more '='
let [l1, l2, p1, p2] += [
\len(join(u1[p1 : p1 + qn - 1], '')),
\len(join(u2[p2 : p2 + qn - 1], '')),
\qn, qn]
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(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} - 1, l{k}]
endif
endfor
let [r1, r2] = (q1 == 0) ? ['d', 'a'] :
\(q2 == 0) ? ['a', 'd'] : ['c', 'c']
let [c1, c2] += [[[r1, h1]], [[r2, h2]]]
endif
endfor
if !empty(c1) || !empty(c2)
let cbx[ln] = [c1, c2]
endif
endfor
return cbx
endfunction
function! s:TraceWithDiffCommand(u1, u2)
" prepare 2 input files for diff
let [utd, lnb, lne, lns] = [':', '{', '}', '#####']
for k in [1, 2]
" insert its number + unit delimiter per unit, and
" enclose the line with its number + begin/end symbols + id,
" and append a line separator
let v{k} = []
for n in range(len(a:u{k}))
let l = n + 1
let v{k} += [l . lnb . k] +
\map(copy(a:u{k}[n]), 'l . utd . v:val') +
\[l . lne . k] + [lns]
endfor
let f{k} = tempname()
call writefile(v{k}[:-2], f{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
let dfo = split(system("diff " . opt . f1 . " " . f2), '\n')
call delete(f1) | call delete(f2)
" trace diff output and generate a list of diff's commands per line
let dlc = {}
let hda = [] " a hunk takes multiple lines and includes 'd' or 'a'
let [bkd, lnd] = [nr2char(0x1e), nr2char(0x1f)]
call map(dfo, '((v:val =~ "^\\d\\+") ? bkd . lnd : lnd) . v:val')
for db in split(join(dfo, ''), bkd)
let dl = split(db, lnd)
let dc = dl[0]
let z1 = filter(copy(dl), 'v:val[0] == "<"')
let z2 = filter(copy(dl), 'v:val[0] == ">"')
" get an operation and line numbers from diff's command line
let [se1, acd, se2] = map(split(substitute(
\dc, '[acd]', '|&|', ''), '|'), 'split(v:val, ",")')
let [s1, s2] = [eval(se1[0]), eval(se2[0])]
let [e1, e2] = [len(se1) == 1 ? s1 : eval(se1[1]),
\len(se2) == 1 ? s2 : eval(se2[1])]
let dx = acd + [[s1, e1]] + [[s2, e2]]
" check if one hunk takes signle line or not
let ll = map(filter(z1 + z2, 'v:val !~ "^. " . lns'),
\'substitute(v:val, "^. \\(\\d\\+\\).*$", "\\1", "")')
" if single line, just copy
if min(ll) == max(ll)
let dlc[ll[0]] = get(dlc, ll[0], []) + [dx]
continue
endif
" if multiple lines, separate by lines
let lh = []
for lx in sort(map(ll, 'printf("%8d", v:val)'))
let lx = eval(lx)
if index(lh, lx) == -1
for k in [1, 2]
let n{k} = len(filter(copy(z{k}),
\'v:val =~ "^. " . lx . "\\D"'))
let e{k} = s{k} + n{k} - 1
endfor
if n1 > 0 && n2 > 0
let dx = ['c', [s1, e1], [s2, e2]]
let [s1, s2] = [e1 + 2, e2 + 2]
else
if n1 > 0 && n2 == 0
let dx = ['d', [s1, e1], []]
let s1 = e1 + 2
elseif n1 == 0 && n2 > 0
let dx = ['a', [], [s2, e2]]
let s2 = e2 + 2
endif
if index(hda, lx) == -1
let hda += [lx]
endif
endif
let dlc[lx] = get(dlc, lx, []) + [dx]
let lh += [lx]
endif
endfor
endfor
" merge continuous 'a+d' and 'd+a' to one 'c'
for ln in hda
let ds = dlc[ln]
let dn = len(ds)
for n in range(dn - 1)
if empty(ds[n][2]) && empty(ds[n + 1][1])
let ds[n] = ['c', ds[n][1], ds[n + 1][2]]
unlet ds[n + 1]
elseif empty(ds[n][1]) && empty(ds[n + 1][2])
let ds[n] = ['c', ds[n + 1][1], ds[n][2]]
unlet ds[n + 1]
endif
endfor
if dn != len(ds) | let dlc[ln] = ds | endif
endfor
" generate a list of commands with byte index per line
let cbx = {}
for [ln, ds] in items(dlc)
let ln -= 1
let [c1, c2] = [[], []]
let [l1, l2, p1, p2] = [1, 1, 0, 0]
let dn = len(ds)
for n in range(dn)
let [cd, x1, x2] = ds[n]
let [s1, e1] = x1
let [s2, e2] = x2
" adjust the unit number and diff's command
" (remove a count of line begin and end symbols)
if n == 0
let [bs1, bs2] = [s1 - 1, s2 - 1]
endif
let [s1, s2] -= (n == 0) ?
\[bs1, bs2] : [bs1 + 1, bs2 + 1]
let [e1, e2] -= (n == dn - 1) ?
\[bs1 + 2, bs2 + 2] : [bs1 + 1, bs2 + 1]
if cd == 'c'
let [w1, w2] = [s1 > e1, s2 > e2]
if [w1, w2] == [1, 1]
continue
elseif [w1, w2] == [1, 0]
let cd = 'a'
let s1 = e1
elseif [w1, w2] == [0, 1]
let cd = 'd'
let s2 = e2
endif
endif
let [r1, r2] = (cd == 'a') ? ['d', 'a'] :
\(cd == 'd') ? ['a', 'd'] : ['c', 'c']
" caluculate byte index from unit number
for k in [1, 2]
let [s{k}, e{k}] -= [1, 1]
if r{k} == 'd'
if s{k} >= 0
let l{k} += len(join(a:u{k}[ln]
\[p{k} : s{k}], ''))
endif
let h{k} = [l{k} - 1, l{k}]
let p{k} = e{k} + 1
else
if s{k} > 0
let l{k} += len(join(a:u{k}[ln]
\[p{k} : s{k} - 1], ''))
endif
let r = len(join(a:u{k}[ln]
\[s{k} : e{k}], ''))
let h{k} = [l{k}, l{k} + r - 1]
let l{k} += r
let p{k} = e{k} + 1
endif
endfor
let [c1, c2] += [[[r1, h1]], [[r2, h2]]]
endfor
if !empty(c1) || !empty(c2)
let cbx[ln] = [c1, c2]
endif
endfor
return cbx
endfunction
function! s: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)]
let buf = {}
" 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 "
endfor
if exists("t:DChar.lsv")
for k in [1, 2]
exec "au! dchar TextChanged,TextChangedI "
endfor
endif
if exists("t:DChar.vdl")
for k in [1, 2]
exec "au! dchar CursorHold "
endfor
if exists("s:save_ut") && len(filter(range(1, tabpagenr('$')),
\'!empty(gettabvar(v:val, "DChar"))')) == 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! s: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 s:ResetDiffChar(a:lines)
return
endif
endfor
endif
call s:ShowDiffChar(a:lines)
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
let bl = getbufline(
\winbufnr(t:DChar.win[a:key]), l)[0]
let c = [c[0] - (0 < c[0] ? len(split(
\bl[:c[0] - 1], '\zs')[-1]) : 0) + 1,
\c[1] + (c[1] <= len(bl) ? len(split(
\bl[c[1] - 1:], '\zs')[0]) : 0) - 1]
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)
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 s:ResetDiffChar(
\range(1, max([t:DChar.lsv[a:key][0], line('$')])))
return
endif
" save the current t:DChar settings except highlightings
let sdc = deepcopy(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
" update only highlighted and current changed lines
let hl = map(keys(t:DChar.hlc[a:key]), 'eval(v:val)')
let lsv = s:LinesValues(a:key, hl)
let chl = filter(hl, 'lsv[v:val] != t:DChar.lsv[a:key][v:val]')
call s:ResetDiffChar(chl)
if !exists("t:DChar") | let t:DChar = sdc | endif
call s:MarkDiffCharWID(1)
call s:ShowDiffChar(chl)
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 &diff | return | endif
call s:RefreshDiffCharWID()
let cwin = winnr()
if cwin != t:DChar.win[a:key] | return | endif
let [win, vdl] = [t:DChar.win, t:DChar.vdl]
call s:ResetDiffChar(range(1, line('$')))
" switch to another diff mode window of the same buffer if present
let bwin = filter(range(1, winnr('$')),
\'winbufnr(v:val) == bufnr("%") && getwinvar(v:val, "&diff")')
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 s:ShowDiffChar(range(1, line('$')))
exec cwin . "wincmd w"
let &eventignore = save_ei
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 lsv = {}
for l in a:lines
exec "let lsv[l] = " . substitute(sha256(getbufline(winbufnr(
\t:DChar.win[a:key]), l)[0]), '.\{4}', '+0x&', 'g')
endfor
return lsv
endfunction
function! s: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)
let m = (a:key == 1) ? 2 : 1
if exists("t:DChar.vdl") " diff mode
let line = t:DChar.vdl[m][index(t:DChar.vdl[a:key], a:line)]
else " non-diff mode
let line = a:line
endif
let bl = getbufline(winbufnr(t:DChar.win[m]), line)[0]
let [e, c] = t:DChar.hlc[m][line][a:col]
if e == 'd'
" deleted unit
let pc = 0 < c[0] ? split(bl[:c[0] - 1], '\zs')[-1] : ""
let nc = c[1] <= len(bl) ? split(bl[c[1] - 1:], '\zs')[0] : ""
" echo a-----b with DiffChange/DiffDelete
exec "echohl " . t:DChar.dhl.C
echon pc
exec "echohl " . t:DChar.dhl.D
let col = t:DChar.hlc[a:key][a:line][a:col][1]
echon repeat('-', strwidth(
\getbufline(winbufnr(t:DChar.win[a:key]), a:line)[0]
\[col[0] - 1 : col[1] - 1]))
exec "echohl " . t:DChar.dhl.C
echon nc
echohl None
" set position/length for both side of deleted unit
let clen = len(pc . nc)
let cpos = c[0] - len(pc) + 1
else
" changed unit
let dc = bl[c[0] - 1 : c[1] - 1]
" echo the matching unit with its color
exec "echohl " . t:DChar.dmc
\[(count(map(t:DChar.hlc[m][line][: a:col],
\'v:val[0]'), 'c') - 1) % len(t:DChar.dmc)]
echon dc
echohl None
" set position/length for matching unit
let clen = len(split(dc, '\zs')[a:pos ? 0 : -1])
let cpos = a:pos ? c[0] : c[1] - clen + 1
endif
" show cursor on deleted unit or matching unit on another window
let save_ei = &eventignore | let &eventignore = "all"
exec t:DChar.win[m] . "wincmd w"
call s:ResetDiffCharPair(m)
if exists("*matchaddpos")
let t:DChar.pci[m] = matchaddpos(t:DChar.dhl.U,
\[[line, cpos, clen]], 0)
else
let t:DChar.pci[m] = matchadd(t:DChar.dhl.U, '\%' . line .
\'l\%>' . (cpos - 1) . 'c\%<' . (cpos + clen) . 'c', 0)
endif
exec "au! dchar WinEnter call s:ResetDiffCharPair(" . m . ")"
exec t:DChar.win[a:key] . "wincmd w"
let &eventignore = save_ei
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 "
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 + 3)
let etree = [] " [next edit, previous p, previous k]
let p = -1
while fp[D] != M
let p += 1
let etree += [repeat([['', 0, 0]], p * 2 + D + 1)]
for k in range(-p, D - 1, 1) + range(D + p, D + 1, -1) + [D]
let [x, etree[p][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 etree[p][k][0] .= '='
let [x, y] += [1, 1]
endwhile
let fp[k] = x
endfor
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
" number of tabpages where DiffChange/DiffText have been overwritten
let tn = len(filter(map(range(1, tabpagenr('$')),
\'gettabvar(v:val, "DChar")'),
\'!empty(v:val) && exists("v:val.dtm")'))
if a:on
" globally disable DiffChange/DiffText at the first ON
if tn == 0
" if either of fg and attr is set, disable its HL
let ct = ''
for dh in ['C', 'T']
for at in ["fg", "bold", "italic",
\"underline", "undercurl", "reverse",
\"inverse", "standout"]
let vl = synIDattr(
\hlID(t:DChar.dhl[dh]), at)
if !empty(vl) && vl != -1
let ct .= dh
break
endif
endfor
if ct == 'C' | let ct = 'CT' | break | endif
endfor
if !empty(ct)
let s:save_hl = &highlight
let &highlight = join(map(split(s:save_hl,
\','), 'v:val[0] =~# "[" . ct . "]" ?
\v:val[0] . "n" : v:val'), ',')
let s:overwrite_ct = split(ct, '\zs')
endif
endif
if exists("s:overwrite_ct")
call s:OverwriteDiffHL(s:overwrite_ct)
endif
else
" globally restore DiffChange/DiffText at the last OFF
if exists("s:overwrite_ct")
if tn == 1
let &highlight = s:save_hl
unlet s:save_hl
unlet s:overwrite_ct
endif
call s:RestoreDiffHL()
endif
endif
endfunction
function! s:OverwriteDiffHL(ct)
" overwrite disabled DiffChange/DiffText 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"
if index(a:ct, 'C') != -1 | let cl = t:DChar.vdl[k] | endif
let tl = []
if !exists("s:save_dex")
" normal case
let dt = hlID(t:DChar.dhl.T)
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 a:ct
let ll = (hl == 'C') ? cl : 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