Full Code of rickhowe/diffchar.vim for AI

master 14fc3fc79356 cached
5 files
89.4 KB
30.9k tokens
1 requests
Download .txt
Repository: rickhowe/diffchar.vim
Branch: master
Commit: 14fc3fc79356
Files: 5
Total size: 89.4 KB

Directory structure:
gitextract_c6cdx0o3/

├── LICENSE
├── README.md
├── autoload/
│   └── diffchar.vim
├── doc/
│   └── diffchar.txt
└── plugin/
    └── diffchar.vim

================================================
FILE CONTENTS
================================================

================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2014-2025 Rick Howe (Takumi Ohtani)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# diffchar.vim
*Highlight the exact differences, based on characters and words*
```
 ____   _  ____  ____  _____  _   _  _____  ____   
|    | | ||    ||    ||     || | | ||  _  ||  _ |  
|  _  || ||  __||  __||     || | | || | | || | ||  
| | | || || |__ | |__ |   __|| |_| || |_| || |_||_ 
| |_| || ||  __||  __||  |   |     ||     ||  __  |
|     || || |   | |   |  |__ |  _  ||  _  || |  | |
|____| |_||_|   |_|   |_____||_| |_||_| |_||_|  |_|
```

This plugin has been developed in order to make diff mode more useful. Vim
highlights all the text in between the first and last different characters on
a changed line. But this plugin will find the exact differences between them,
character by character - so called *DiffChar*.

For example, in diff mode:
![example1](example1.png)

This plugin will exactly show the changed/added/deleted units:
![example2](example2.png)

#### Sync with diff mode
This plugin will synchronously show/reset the highlights of the exact
differences as soon as the diff mode begins/ends. And the exact differences
will be kept updated while editing. Note that this plugin does nothing if an
`inline` item is set in the `doffopt` option and its value is other than
`simple`. On plugin loading, the item is removed only if set by default so
that this plugin can work.

#### Diff unit
This plugin shows the diffs based on a `g:DiffUnit`. Its default is 'Word1'
and it handles a `\w\+` word and a `\W` character as a diff unit. There are other
types of word provided and you can also set 'Char' to compare character by
character. In addition, you can specify one or more diff unit delimiters, such
as comma (','), colon (':'), tab ("\t"), and HTML tag symbols ('<' and '>'),
and also specify a custom pattern in the `g:DiffUnit`.

#### Diff matching colors
In diff mode, the corresponding `hl-DiffChange` lines are compared between two
windows. As a default, all the changed units are highlighted with
`hl-DiffText`. You can set `g:DiffColors` to use more than one matching color
to make it easy to find the corresponding units between two windows. The
number of colors depends on the color scheme. In addition, an added unit is
always highlighted with `hl-DiffAdd` and the position of the corresponding
deleted unit is shown with bold/underline or a virtual blank column,
depending on a `g:DiffDelPosVisible`.

#### Diff pair visible
While showing the exact differences, when the cursor is moved on a diff unit,
you can see its corresponding unit highlighted with `hl-Cursor`,
`hl-TermCursor`, or similar one in another window, based on a
`g:DiffPairVisible`. If you change its default, the corresponding unit is
echoed in the command line or displayed in a popup/floating window just below
the cursor position or at the mouse position. Those options take effect on
`:diffupdate` command as well.

#### Jump to next/prev diff unit
You can use `]b` or `]e` to jump cursor to start or end position of the next
diff unit, and `[b` or `[e` to the start or end position of the previous unit.

#### Get/put a diff unit
Like line-based `:diffget`/`:diffput` and `do`/`dp` vim commands, you can use
`<Leader>g` and `<Leader>p` commands in normal mode to get and put each diff
unit, where the cursor is on, between 2 buffers and undo its difference. Those
keymaps are configurable in your vimrc and so on.

#### Check diff lines locally
When the diff mode begins, this plugin locally checks the `hl-DiffChange`
lines in the limited range of the current visible and its upper/lower lines of
a window. And each time a cursor is moved on to another range upon scrolling
or searching, those diff lines will be checked in that range. Which means,
independently of the file size, the number of lines to be checked and then the
time consumed are always constant.

#### Tab page individual
This plugin works on each tab page individually. You can use a tab page
variable (t:), instead of a global one (g:), to specify different options on
each tab page. Note that this plugin can not handle more than two diff mode
windows in a tab page. If it would happen, to prevent any trouble, all the
highlighted units are to be reset in the tab page.

#### Follow 'diffopt' option
This plugin supports `icase`, `iwhite`, `iwhiteall`, and `iwhiteeol` in the
`diffopt` option. In addition, when `indent-heuristic` is specified,
positioning of the added/deleted diff units is adjusted to reduce the number
of diff hunks and make them easier to read.

#### Comparison algorithm
To find the exact differences, this plugin uses "An O(NP) Sequence Comparison
Algorithm" developed by S.Wu, et al., which always finds an optimum sequence.
But it takes time to check a long and dissimilar line. To improve the
performance, the algorithm is also implemented in Vim9 script. In addition,
if available, this plugin uses a builtin diff function (`diff()` in vim
patch-9.1.0071 and Lua `vim.diff()` in nvim 0.6.0) and makes it much faster.

#### See also
There are other diff related plugins available:
* [spotdiff.vim](https://github.com/rickhowe/spotdiff.vim): A range and area selectable `:diffthis` to compare partially
* [wrapfiller](https://github.com/rickhowe/wrapfiller): Align each wrapped line virtually between windows
* [difffilter](https://github.com/rickhowe/difffilter): Selectively compare lines as you want in diff mode
* [diffunitsyntax](https://github.com/rickhowe/diffunitsyntax): Highlight word or character based diff units in diff format

### Options

* `g:DiffUnit`, `t:DiffUnit`: A type of difference unit

  | Value | Description |
  | --- | --- |
  | 'Char' | any single character |
  | 'Word1' | `\w\+` word and any `\W` single character (default) |
  | 'Word2' | non-space and space words |
  | 'Word3' | `\<` or `\>` character class boundaries (set by `iskeyword`) |
  | 'word' | see `word` |
  | 'WORD' | see `WORD` |
  | '[{del}]' | one or more diff unit delimiters (e.g. "[,:\t<>]") |
  | '/{pat}/' | a pattern to split into diff units (e.g. '/.\{4}\zs/') |

* `g:DiffColors`, `t:DiffColors`: Matching colors for changed units

  | Value | Description |
  | --- | --- |
  | 0 | `hl-DiffText` (default) |
  | 1 | `hl-DiffText` + a few (3, 4, ...) highlight groups |
  | 2 | `hl-DiffText` + several (7, 8, ...) highlight groups |
  | 3 | `hl-DiffText` + many (11, 12, ...) highlight groups |
  | 100 | all available highlight groups in random order |
  | [{hlg}] | a list of your favorite highlight groups |

* `g:DiffPairVisible`, `t:DiffPairVisible`: Visibility of corresponding diff units

  | Value | Description |
  | --- | --- |
  | 0 | disable |
  | 1 | highlight with `hl-Cursor` (default) |
  | 2 | highlight with `hl-Cursor` + echo in the command line |
  | 3 | highlight with `hl-Cursor` + popup/floating window at cursor position |
  | 4 | highlight with `hl-Cursor` + popup/floating window at mouse position |

* `g:DiffDelPosVisible`, `t:DiffDelPosVisible`: Visibility of the position of deleted units

  | Value | Description |
  | --- | --- |
  | 0 | disable |
  | 1 | highlight previous/next chars of a deleted unit in bold/underline (default if inline "virtual-text" is not available) |
  | 2 | virtually show a blank column (set by `space` item in `listchars`) wih `hl-DiffDelete` (default if inline "virtual-text" is available) |

### Keymaps

| Mapping | Default Key | Description |
| --- | --- | --- |
| `<Plug>JumpDiffCharPrevStart` | `[b` | Jump cursor to the start position of the previous diff unit |
| `<Plug>JumpDiffCharNextStart` | `]b` | Jump cursor to the start position of the next diff unit |
| `<Plug>JumpDiffCharPrevEnd` | `[e` | Jump cursor to the end position of the previous diff unit |
| `<Plug>JumpDiffCharNextEnd` | `]e` | Jump cursor to the end position of the next diff unit |
| `<Plug>GetDiffCharPair` | `<Leader>g` | Get a corresponding diff unit from another buffer to undo difference |
| `<Plug>PutDiffCharPair` | `<Leader>p` | Put a corresponding diff unit to another buffer to undo difference |


================================================
FILE: autoload/diffchar.vim
================================================
" diffchar.vim: Highlight the exact differences, based on characters and words
"
"  ____   _  ____  ____  _____  _   _  _____  ____   
" |    | | ||    ||    ||     || | | ||  _  ||  _ |  
" |  _  || ||  __||  __||     || | | || | | || | ||  
" | | | || || |__ | |__ |   __|| |_| || |_| || |_||_ 
" | |_| || ||  __||  __||  |   |     ||     ||  __  |
" |     || || |   | |   |  |__ |  _  ||  _  || |  | |
" |____| |_||_|   |_|   |_____||_| |_||_| |_||_|  |_|
"
" Last Change: 2025/10/01
" Version:     10.0 (on or after vim 9.0 and nvim 0.7.0)
" Author:      Rick Howe (Takumi Ohtani) <rdcxy754@ybb.ne.jp>
" Copyright:   (c) 2014-2025 Rick Howe
" License:     MIT

let s:save_cpo = &cpoptions
set cpo&vim

" Vim feature, function, event and patch number which this plugin depends on
" patch-8.2.4204: screenpos() fixed to return zero row for invisible line
" patch-9.0.1067: virtual text fixed to correctly highlight in diff mode
" patch-9.1.0099: diff() fixed to correctly use in &diffexpr
let s:VF = #{
  \ScreenPos: exists('*screenpos') &&
                                \(has('patch-8.2.4204') || has('nvim-0.8.2')),
  \InlineVirtText: has('textprop') && has('patch-9.0.1067') ||
                                                          \has('nvim-0.10.0'),
  \BuiltinDiffFunc: exists('*diff') && has('patch-9.1.0099') ||
                                                          \has('nvim-0.6.0')}

function! s:ShowDiffChar(...) abort
  " !a:0 - all dfl from scratch, a:1 - specified dfl only
  if !a:0 && !exists('t:DChar') && s:InitializeDiffChar() == -1
    return
  endif
  let ak = 2 | while t:DChar.wid[ak] != win_getid() | let ak -= 1
    if ak == 0 | return | endif
  endwhile
  let lc = #{1: {}, 2: {}}
  for n in (!a:0) ? range(len(t:DChar.dfl[ak])) :
      \filter(map(copy(a:1), 'index(t:DChar.dfl[ak], v:val)'), 'v:val != -1')
    let tu = #{1: {}, 2: {}}
    for k in [1, 2]
      let tu[k].l = t:DChar.dfl[k][n]
      if has_key(t:DChar.hlc[k], tu[k].l) | let tu[k].l = 0 | endif
    endfor
    if 0 < tu[1].l && 0 < tu[2].l
      for k in [1, 2]
        let tu[k].t = getbufline(t:DChar.bnr[k], tu[k].l)[0]
        let t = t:DChar.opt.ic ? tolower(tu[k].t) : tu[k].t
        let tu[k].u = split(t:DChar.opt.iw == 0 ? t :
            \substitute(t, (t:DChar.opt.iw == 1) ? '\s\+' : '\s\+$', '', 'g'),
                                                                \t:DChar.upa)
        let tu[k].w = (tu[k].t =~ '\s\+' &&
          \(t:DChar.opt.iw == 1 || t:DChar.opt.iw == 2)) ? t:DChar.opt.iw : 0
        if tu[k].w == 2
          let u = [] | let s = ''
          for z in tu[k].u + ['']
            if z =~ '^\s\+$'
              let s .= z
            else
              if !empty(s) | let u += [s] | let s = '' | endif
              let u += [z]
            endif
          endfor
          let tu[k].u = u[: -2]
        endif
      endfor
      let uu = [copy(tu[1].u), copy(tu[2].u)]
      for k in [1, 2]
        if tu[k].w == 2
          call map(tu[k].u, 'substitute(v:val, "\\s\\+", " ", "g")')
        endif
      endfor
      if tu[1].u !=# tu[2].u
        let [tu[1].c, tu[2].c] = s:GetDiffUnitPos(uu,
                              \t:DChar.dfn(tu[1].u, tu[2].u, t:DChar.opt.ih))
        for k in [1, 2]
          if tu[k].w == 1
            let pc = [0] + filter(range(1, len(tu[k].t)),
                              \'tu[k].t[v:key] =~ "\\S"') + [len(tu[k].t) + 1]
            for n in range(len(tu[k].c))
              call map(tu[k].c[n][1], 'pc[v:val]')
            endfor
          endif
          let lc[k][tu[k].l] = tu[k].c
          let t:DChar.cks[k][tu[k].l] = s:ChecksumStr(tu[k].t)
        endfor
      endif
    endif
  endfor
  if !a:0
    call s:ToggleDiffCharEvent(1)
    call s:ToggleDiffHL(1)
    call s:ToggleDiffCharPair(1)
  endif
  if !empty(lc[ak])
    call s:HighlightDiffChar(lc)
    if 0 < t:DChar.dpv.pv | call s:ShowDiffCharPair(ak) | endif
  endif
endfunction

function! s:ResetDiffChar(...) abort
  " !a:0 - all dfl to scratch, a:1 - specified dfl only
  if !exists('t:DChar') | return | endif
  let ak = 2 | while t:DChar.wid[ak] != win_getid() | let ak -= 1
    if ak == 0 | return | endif
  endwhile
  let dl = #{1: [], 2: []}
  for n in (!a:0) ? range(len(t:DChar.dfl[ak])) :
      \filter(map(copy(a:1), 'index(t:DChar.dfl[ak], v:val)'), 'v:val != -1')
    for k in [1, 2]
      let l = t:DChar.dfl[k][n]
      if has_key(t:DChar.hlc[k], l)
        let dl[k] += [l]
        unlet t:DChar.cks[k][l]
      endif
    endfor
  endfor
  if !empty(dl[ak])
    if 0 < t:DChar.dpv.pv | call s:ClearDiffCharPair(ak) | endif
    call s:ClearDiffChar(dl)
  endif
  if !a:0
    call s:ToggleDiffCharPair(0)
    call s:ToggleDiffHL(0)
    unlet t:DChar
    call s:ToggleDiffCharEvent(0)
  endif
endfunction

function! s:InitializeDiffChar() abort
  if matchstr(&diffopt, '^.*inline:\zs.*') !~ '^simple\|^$'
    call s:EchoWarning('Disabled because "inline:" value set in &diffopt is
                                                            \ not "simple"!')
    return -1
  endif
  let cw = win_getid() | let cb = winbufnr(cw)
  let nw = filter(map(range(winnr() + 1, winnr('$')) +
                                  \range(1, winnr() - 1), 'win_getid(v:val)'),
                        \'getwinvar(v:val, "&diff") && winbufnr(v:val) != cb')
  let nb = map(copy(nw), 'winbufnr(v:val)')
  if !getwinvar(cw, '&diff') || empty(nw) || min(nb) != max(nb)
    return -1
  endif
  for tn in filter(range(1, tabpagenr('$')), 'v:val != tabpagenr()')
    let dc = gettabvar(tn, 'DChar')
    if !empty(dc)
      for bn in values(dc.bnr)
        if index([cb, nb[0]], bn) != -1
          call s:EchoWarning('Both or either selected buffer already
                                      \ highlighted in tab page ' . tn . '!')
          return -1
        endif
      endfor
    endif
  endfor
  call s:SetDiffCharHL()
  let t:DChar = {}
  let t:DChar.wid = #{1: cw, 2: nw[0]}
  let t:DChar.bnr = #{1: cb, 2: nb[0]}
  let t:DChar.opt = s:GetDiffCharOptions()
  let t:DChar.lcc = s:GetLineColCnr()
  let t:DChar.dfl = s:FocusDiffLines(0)
  let t:DChar.upa = s:GetDiffSplitRegExp(t:DChar.opt.ut)
  let t:DChar.dpv = s:GetDiffPairVisible(t:DChar.opt.pv)
  let t:DChar.hgp = s:GetDiffUnitHL(t:DChar.opt.cl)
  let t:DChar.csn = s:GetColorsName()
  let t:DChar.mid = #{1: {}, 2: {}}
  let t:DChar.hlc = #{1: {}, 2: {}}
  let t:DChar.cks = #{1: {}, 2: {}}
  let t:DChar.dfn = function(t:DChar.opt.df ?
                                      \'s:ApplyDiffFunc' : 's:TraceDiffChar')
endfunction

function! s:GetDiffSplitRegExp(du) abort
  if a:du == 'Char'
    let upa = '\zs'
  elseif a:du == 'Word2' || a:du ==# 'WORD'
    let upa = '\%(\s\+\|\S\+\)\zs'
  elseif a:du == 'Word3' || a:du ==# 'word'
    let upa = '\<\|\>'
  elseif a:du =~ '^\[.\+\]$'
    let s = escape(a:du[1 : -2], ']^-\')
    let upa = '\%([^' . s . ']\+\|[' . s . ']\)\zs'
  elseif a:du =~ '^\([/?]\).\+\1$'
    let upa = a:du[1 : -2]
  else
    let upa = '\%(\w\+\|\W\)\zs'
    if a:du != 'Word1'
      call s:EchoWarning('Not a valid difference unit type.
                                                      \ Use "Word1" instead.')
    endif
  endif
  return upa
endfunction

function! s:GetDiffPairVisible(pv) abort
  let dpv = #{pv: a:pv}
  if 0 < dpv.pv
    let dpv.ch = {}
    if dpv.pv == 3 || dpv.pv == 4 | let dpv.pw = has('nvim') ? {} : 0 | endif
  endif
  return dpv
endfunction

function! s:GetDiffUnitHL(dc) abort
  let hgp = [s:DCharHL.T]
  if type(a:dc) == type([])
    let hgp += filter(copy(a:dc),
                  \'0 < hlID(v:val) && !empty(synIDattr(hlID(v:val), "bg#"))')
    if 1 < len(hgp) | unlet hgp[0] | endif
  elseif 1 <= a:dc && a:dc <= 3
    let lv = a:dc - 1
    let bx = []
    for nm in values(s:DCharHL)
      let [fc, bc] = map(['fg#', 'bg#'],
                              \'s:ColorClass(synIDattr(hlID(nm), v:val), lv)')
      if !empty(bc) | let bx += [bc] | endif
      if nm == s:DCharHL.n | let fn = fc | endif
    endfor
    let hl = {} | let id = 1
    while 1
      let nm = synIDattr(id, 'name')
      if empty(nm) | break | endif
      if id == synIDtrans(id) && empty(filter(['underline', 'undercurl',
                          \'strikethrough', 'reverse', 'inverse', 'standout'],
                                            \'!empty(synIDattr(id, v:val))'))
        let [fc, bc] = map(['fg#', 'bg#'],
                                    \'s:ColorClass(synIDattr(id, v:val), lv)')
        if !empty(bc) && index(bx + [!empty(fc) ? fc : fn], bc) == -1
          let wt = !empty(fc) + (!empty(filter(['bold', 'italic'],
                                        \'!empty(synIDattr(id, v:val))'))) * 2
          if !has_key(hl, bc) || hl[bc][0] < wt
            let hl[bc] = [wt, nm]
          endif
        endif
      endif
      let id += 1
    endwhile
    let hgp += map(values(hl), 'v:val[1]')
  elseif a:dc == 100
    let bx = map(values(s:DCharHL), 'synIDattr(hlID(v:val), "bg#")')
    let hl = {} | let id = 1
    while 1
      let nm = synIDattr(id, 'name')
      if empty(nm) | break | endif
      if id == synIDtrans(id)
        let bg = synIDattr(id, 'bg#')
        if !empty(bg) && index(bx, bg) == -1
          let hl[reltimestr(reltime())[-2 :] . id] = nm
          let bx += [bg]
        endif
      endif
      let id += 1
    endwhile
    let hgp += values(hl)
  endif
  return hgp
endfunction

function! s:GetColorsName() abort
  return get(g:, 'colors_name', 'default')
endfunction

function! s:ColorClass(cn, lv) abort
  if empty(a:cn) | return a:cn | endif
  if a:cn[0] != '#'
    let cn = a:cn % 256
    if cn < 16
      let cv = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
                  \[0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],
                  \[128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0],
                  \[0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]]
      if &t_Co < 256
        let [cv[9], cv[12], cv[11], cv[14]] = [cv[12], cv[9], cv[14], cv[11]]
      endif
      let rgb = cv[cn]
    elseif cn < 232
      let cv = [0, 95, 135, 175, 215, 255]
      let cn -= 16
      let rgb = [cv[(cn / 36) % 6], cv[(cn / 6) % 6], cv[cn % 6]]
    else
      let cn = 10 * (cn - 232) + 8
      let rgb = [cn, cn, cn]
    endif
  else
    let rgb = map(split(a:cn[1 :], '..\zs'), 'str2nr(v:val, 16)')
  endif
  let cl = [[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 0, 0, 1, 1, 2, 2],
                                              \[0, 1, 2, 3, 4, 5, 6, 7]][a:lv]
  call map(rgb, 'v:val / 32')
  if max(rgb) == min(rgb)
    return '99' . cl[(rgb[0] + rgb[1] + rgb[2]) / 3]
  else
    return join(map(rgb, 'cl[v:val]'), '')
  endif
endfunction

function! s:SetDiffCharHL() abort
  " set diff hl in original and DChar modes
  let s:DiffHL = #{A: 'DiffAdd', C: 'DiffChange', D: 'DiffDelete',
                                                              \T: 'DiffText'}
  for [hs, hl] in items(s:DiffHL)
    let dh = {}
    let dh.id = hlID(hl)
    let dh.it = synIDtrans(dh.id)       " in case of linked
    let dh.nm = synIDattr(dh.it, 'name')
    " 0 : for original, 1 : for DChar
    let dh[0] = {}
    for hm in ['cterm', 'gui']
      for hc in ['fg', 'bg']
        let dh[0][hm . hc] = synIDattr(dh.it, hc, hm)
      endfor
      let dh[0][hm] = join(filter(['bold', 'underline', 'undercurl',
                \'strikethrough', 'reverse', 'inverse', 'italic', 'standout'],
                                \'!empty(synIDattr(dh.it, v:val, hm))'), ',')
    endfor
    call map(dh[0], '!empty(v:val) ? v:val : "NONE"')
    let dh[1] = (hs == 'C' || hs == 'T') ?
                  \map(copy(dh[0]), 'v:key =~ "bg$" ? v:val : "NONE"') : dh[0]
    let s:DiffHL[hs] = dh
  endfor
  for at in ['ctermbg', 'guibg']              " in case no bg in C, also in T
    if s:DiffHL.C[1][at] == 'NONE' | let s:DiffHL.T[1][at] = 'NONE' | endif
  endfor
  " set DChar hl
  let s:DCharHL = {}
  let s:DCharHL.n = 'Normal'
  let s:DCharHL.c = has('nvim') ? 'TermCursor' : 'Cursor'
  if !empty(filter(['fg', 'bg'],
                              \'empty(synIDattr(hlID(s:DCharHL.c), v:val))'))
    let s:DCharHL.c = 'dcCursor'
    for at in ['NONE', 'ctermfg=fg', 'ctermbg=bg', 'cterm=reverse',
                                      \'guifg=fg', 'guibg=bg', 'gui=reverse']
      call execute(join(['highlight', s:DCharHL.c, at]), 'silent!')
    endfor
  endif
  for [fs, ts, th, ta] in [['C', 'C', 'dcDiffChange', ''],
                          \['T', 'T', 'dcDiffText', ''],
                          \['A', 'A', 'dcDiffAdd', ''],
                          \['D', 'D', 'dcDiffDelete', ''],
                          \['C', 'E', 'dcDiffDelPos', 'bold,underline']]
    let fa = copy(s:DiffHL[fs][0])
    if !empty(ta)
      for hm in ['cterm', 'gui']
        let fa[hm] = ((fa[hm] != 'NONE') ? fa[hm] . ',' : '') . ta
      endfor
    endif
    for at in ['NONE'] + map(items(fa), 'join(v:val, "=")')
      call execute(join(['highlight', th, at]), 'silent!')
    endfor
    let s:DCharHL[ts] = th
  endfor
  if s:VF.InlineVirtText | call s:SetVirtColumn() | endif
endfunction

function! s:GetDiffCharOptions() abort
  let Dip = {op -> &diffopt =~ op}
  let do = #{ic: Dip('icase'),
            \iw: Dip('iwhiteall') ? 1 : Dip('\<iwhite\>') ? 2 :
                                                    \Dip('iwhiteeol') ? 3 : 0,
            \ih: Dip('indent-heuristic'), lm: Dip('linematch'), vc: ' '}
  if &listchars =~ '\<space\>'
    let do.vc = matchstr(&listchars, '^.*\<space:\zs[^,]\+')
    if do.vc =~ '^\\[ux]'
      let do.vc = nr2char(str2nr('0x' . do.vc[2 :], 16))
    endif
  endif
  for [ok, pv] in items(#{ut: ['DiffUnit', 'Word1'], cl: ['DiffColors', 0],
                    \pv: ['DiffPairVisible', 1], dv: ['DiffDelPosVisible', 2],
                    \fp: ['DiffFocusPages', 3], df: ['BuiltinDiffFunc', 1]})
    let [op, ov] = pv
    let ov = s:GetOptionVar(op, ov)
    if op == 'DiffPairVisible'
      if ov < 0 || 4 < ov | let ov = 0 | endif
    elseif op == 'DiffDelPosVisible'
      let ov = (ov < 0) ? 0 : min([ov, (s:VF.InlineVirtText ? 2 : 1)])
    elseif op == 'DiffFocusPages'
      " avoid to check off-screen diff lines linematch is not yet run
      if do.lm | let ov = (ov < 0) ? -1 : (0 < ov) ? 1 : 0 | endif
    elseif op == 'BuiltinDiffFunc'
      if !s:VF.BuiltinDiffFunc | let ov = 0 | endif
    endif
    let do[ok] = ov
  endfor
  return do
endfunction

function! s:GetOptionVar(op, ov) abort
  return get(t:, a:op, get(g:, a:op, a:ov))
endfunction

function! s:ToggleDiffHL(on) abort
  for dh in [s:DiffHL.C, s:DiffHL.T]
    call execute(join(['highlight', dh.nm] +
                                  \map(items(dh[a:on]), 'join(v:val, "=")')))
  endfor
endfunction

function! s:RefreshDiffCharHL(event) abort
  " a:event : 0 = TabEnter, 1 = ColorScheme
  if a:event == 1 | call s:SetDiffCharHL() | endif
  let on = exists('t:DChar')
  call s:ToggleDiffHL(on)
  if on
    " redraw DChar units with the latest colorscheme
    let csn = s:GetColorsName()
    if t:DChar.csn != csn
      let t:DChar.csn = csn
      if 1 < len(t:DChar.hgp)
        let hlc = deepcopy(t:DChar.hlc)
        call s:ClearDiffChar(map(copy(hlc), 'keys(v:val)'))
        let t:DChar.hgp = s:GetDiffUnitHL(t:DChar.opt.cl)
        call s:HighlightDiffChar(hlc)
      endif
    endif
  endif
endfunction

function! s:ToggleDiffCharEvent(on) abort
  call execute(g:DiffCharInitEvent)
  let tv = filter(map(range(1, tabpagenr('$')),
                              \'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 = '<buffer=' . td.bnr[k] . '>'
      let ac += [['WinClosed', bl, 's:WinClosedDiffChar()']]
      if td.opt.fp != 0
        let ac += [['WinScrolled', bl, 's:ScrollDiffLines(0)']]
      endif
      if 0 < td.dpv.pv
        let ac += [['CursorMoved', bl, 's:ShowDiffCharPair(' . k . ')']]
      endif
    endfor
  endfor
  let ac += [['TabEnter', '*', 's:RefreshDiffCharHL(0)']]
  let ac += [['ColorScheme', '*', 's:RefreshDiffCharHL(1)']]
  let ac += [['BufWinEnter', '*', 's:RepairDiffChar()']]
  let ac += [['DiffUpdated', '*', 's:UpdateDiffChar()']]
  call execute(map(ac, 'join(["autocmd", "diffchar", v:val[0], v:val[1],
                                                        \"call", v:val[2]])'))
endfunction

function! s:ShiftDiffChar(key, lines, shift) abort
  let im = {}
  for gm in getmatches(t:DChar.wid[a:key])
    let im[gm.id] = gm
  endfor
  let [lid, hlc, cks] = [{}, {}, {}]
  for ln in a:lines
    if has_key(t:DChar.mid[a:key], ln)
      let lid[ln + a:shift] = []
      for id in t:DChar.mid[a:key][ln]
        if 0 < id
          if has_key(im, id)
            call matchdelete(id, t:DChar.wid[a:key])
            let lid[ln + a:shift] += [matchaddpos(im[id].group,
                  \map(values(filter(copy(im[id]), 'v:key =~ "^pos\\d\\+$"')),
                                        \'[v:val[0] + a:shift] + v:val[1 :]'),
                        \im[id].priority, -1, #{window: t:DChar.wid[a:key]})]
          endif
        elseif id < 0
          " virtual column is kept on the line, no need to remake it
          let lid[ln + a:shift] += [id]
        endif
      endfor
      unlet t:DChar.mid[a:key][ln]
    endif
    if has_key(t:DChar.hlc[a:key], ln)
      let hlc[ln + a:shift] = t:DChar.hlc[a:key][ln]
      unlet t:DChar.hlc[a:key][ln]
    endif
    if has_key(t:DChar.cks[a:key], ln)
      let cks[ln + a:shift] = t:DChar.cks[a:key][ln]
      unlet t:DChar.cks[a:key][ln]
    endif
  endfor
  call extend(t:DChar.mid[a:key], lid)
  call extend(t:DChar.hlc[a:key], hlc)
  call extend(t:DChar.cks[a:key], cks)
endfunction

function! s:GetLineColCnr() abort
  let lcc = {}
  for k in [1, 2]
    call win_execute(t:DChar.wid[k], 'let lcc[k] =
                            \#{tl: line("w0"), bl: line("w$"), ll: line("$"),
                              \cl: line("."), cc: col("."), cn: changenr()}')
    if lcc[k].bl < lcc[k].ll && (s:VF.ScreenPos ?
                      \screenpos(t:DChar.wid[k], lcc[k].bl + 1, 1).row != 0 :
      \&display =~ 'lastline\|truncate' && getwinvar(t:DChar.wid[k], '&wrap'))
      let lcc[k].bl += 1
    endif
  endfor
  return lcc
endfunction

function! s:UpdateDiffChar() abort
  if mode(1) != 'n' || !exists('t:DChar') ||
                              \len(filter(gettabinfo(tabpagenr())[0].windows,
                                                \'getwinvar(v:val, "&diff") &&
                              \index(values(t:DChar.wid), v:val) != -1')) != 2
    return
  endif
  let lcc = t:DChar.lcc | let t:DChar.lcc = s:GetLineColCnr()
  let ak = 2
  while 0 < ak && lcc[ak].cn == t:DChar.lcc[ak].cn | let ak -= 1 | endwhile
  if 0 < ak
    " when text changed, find DChar lines to delete/add/shift, and
    " to leave undeleted/unadded
    let NoUpdate = {pl, cl -> get(t:DChar.cks[ak], pl, '') ==
                        \s:ChecksumStr(getbufline(t:DChar.bnr[ak], cl)[0]) &&
                      \(has('nvim') || 0 < min(get(t:DChar.mid[ak], pl, [])))}
    let bk = (ak == 1) ? 2 : 1
    let lnd = t:DChar.lcc[ak].ll - lcc[ak].ll
    let pfl = t:DChar.dfl
    let cfl = s:FocusDiffLines(0)
    let m = min([len(pfl[ak]), len(cfl[ak])])
    let s = 0
    while s < m && [pfl[ak][s], pfl[bk][s]] == [cfl[ak][s], cfl[bk][s]] &&
                                            \NoUpdate(pfl[ak][s], cfl[ak][s])
      let s += 1
    endwhile
    let m -= s
    let e = -1
    while e >= -m && [pfl[ak][e] + lnd, pfl[bk][e]] ==
                \[cfl[ak][e], cfl[bk][e]] && NoUpdate(pfl[ak][e], cfl[ak][e])
      let e -= 1
    endwhile
    let ddl = pfl[ak][s : e]
    let adl = cfl[ak][s : e]
    if lnd != 0 && e < -1
      let sdl = pfl[ak][e + 1 :]
    else
      let sdl = []
      for d in range(len(ddl) - 1, 0, -1)
        let a = index(adl, ddl[d])
        if a != -1 && pfl[bk][s + d] == cfl[bk][s + a] &&
                                                    \NoUpdate(ddl[d], adl[a])
          unlet ddl[d]
          unlet adl[a]
        endif
      endfor
    endif
    if 0 < t:DChar.dpv.pv | call s:ClearDiffCharPair(ak) | endif
    if !empty(ddl)
      call win_execute(t:DChar.wid[ak], 'call s:ResetDiffChar(ddl)')
    endif
    let t:DChar.dfl = cfl
    if !empty(sdl) | call s:ShiftDiffChar(ak, sdl, lnd) | endif
    if !empty(adl)
      call win_execute(t:DChar.wid[ak], 'call s:ShowDiffChar(adl)')
    endif
  else
    " when diffupdate invoked or diffopt changed, delete all previous and
    " then add all current DChar lines if some of DChar option is changed
    let opt = s:GetDiffCharOptions()
    if opt != t:DChar.opt
      let k = (t:DChar.wid[1] == win_getid()) ? 1 : 2
      call s:ResetDiffChar(t:DChar.dfl[k])
      if opt.ut != t:DChar.opt.ut
        let t:DChar.upa = s:GetDiffSplitRegExp(opt.ut)
      endif
      if opt.pv != t:DChar.opt.pv
        let t:DChar.dpv = s:GetDiffPairVisible(opt.pv)
      endif
      if opt.cl != t:DChar.opt.cl
        let t:DChar.hgp = s:GetDiffUnitHL(opt.cl)
      endif
      let t:DChar.opt = opt
      let t:DChar.dfl = s:FocusDiffLines(0)
      call s:ShowDiffChar(t:DChar.dfl[k])
    endif
  endif
endfunction

function! diffchar#JumpDiffChar(dp) abort
  " a:dp : 0=backward/start, 1=forward/start, 2=backward/end, 3=forward/end
  if !exists('t:DChar') | return | endif
  let k = 2 | while t:DChar.wid[k] != win_getid() | let k -= 1
    if k == 0 | return | endif
  endwhile
  let [dir, pos] = (a:dp == 0) ? [0, 0] : (a:dp == 1) ? [1, 0] :
                                              \(a:dp == 2) ? [0, -1] : [1, -1]
  let [ln, co] = [line('.'), col('.')]
  if co == col('$')   " empty line
    if !dir | let co = 0 | endif
  else
    if pos != 0
      let co += len(strcharpart(
                      \getbufline(t:DChar.bnr[k], ln)[0][co - 1 :], 0, 1)) - 1
    endif
  endif
  if has_key(t:DChar.hlc[k], ln) &&
                                \(dir ? co < t:DChar.hlc[k][ln][-1][1][pos] :
                                          \co > t:DChar.hlc[k][ln][0][1][pos])
    let co = filter(map(copy(t:DChar.hlc[k][ln]), 'v:val[1][pos]'),
                            \dir ? 'co < v:val' : 'co > v:val')[dir ? 0 : -1]
  else
    let dl = s:SearchDiffLines(dir, dir ? ln + 1 : ln - 1, 1)
    if empty(dl) | return | endif
    if t:DChar.opt.lm | let lp = ln | endif
    let ln = dl[0] | let lx = ln
    while 1
      " go up/down and check on-screen diff lines, linematch can realign
      if dir ? t:DChar.lcc[k].ll < lx : lx < 1 | return | endif
      noautocmd call cursor(lx, 0) | call s:ScrollDiffLines(k)
      if has_key(t:DChar.hlc[k], ln) | break | endif
      if t:DChar.opt.lm
        let dl = filter(sort(map(keys(t:DChar.hlc[k]), 'eval(v:val)'), 'n'),
                                          \dir ? 'lp < v:val' : 'lp > v:val')
        if !empty(dl) | let ln = dir ? dl[0] : dl[-1] | break | endif
      endif
      let lx = dir ? max([lx, t:DChar.lcc[k].bl]) + 1 :
                                            \min([lx, t:DChar.lcc[k].tl]) - 1
    endwhile
    let co = t:DChar.hlc[k][ln][dir ? 0 : -1][1][pos]
  endif
  " set a dummy cursor position to adjust the start/end
  if 0 < t:DChar.dpv.pv
    call s:ClearDiffCharPair(k)
    if a:dp == 1         " forward/start : rightmost
      let [t:DChar.lcc[k].cl, t:DChar.lcc[k].cc] = [ln, col('$')]
    elseif a:dp == 2     " backward/end : leftmost
      let [t:DChar.lcc[k].cl, t:DChar.lcc[k].cc] = [ln, 0]
    endif
  endif
  call cursor(ln, co)
endfunction

function! diffchar#CopyDiffCharPair(dir) abort
  " a:dir : 0 = get, 1 = put
  if !exists('t:DChar') | return | endif
  let ak = 2 | while t:DChar.wid[ak] != win_getid() | let ak -= 1
    if ak == 0 | return | endif
  endwhile
  let bk = (ak == 1) ? 2 : 1
  let un = -1
  if 0 < t:DChar.dpv.pv
    if !empty(t:DChar.dpv.ch) | let [al, un] = t:DChar.dpv.ch.lc | endif
  else
    let [al, co] = [line('.'), col('.')]
    if co == col('$') | let co = 0 | endif
    if has_key(t:DChar.hlc[ak], al)
      let hc = filter(map(copy(t:DChar.hlc[ak][al]), '[v:key, v:val[1]]'),
                                  \'v:val[1][0] <= co && co <= v:val[1][-1]')
      if !empty(hc) | let un = hc[0][0] | endif
    endif
  endif
  if un == -1
    call s:EchoWarning('Cursor is not on a difference unit!')
    return
  endif
  let bl = t:DChar.dfl[bk][index(t:DChar.dfl[ak], al)]
  let et = #{a: {}, b: {}}
  let [et.a.e, et.a.c] = t:DChar.hlc[ak][al][un]
  let [et.b.e, et.b.c] = t:DChar.hlc[bk][bl][un]
  let [et.a.t, et.b.t] = [getbufline(t:DChar.bnr[ak], al)[0],
                                          \getbufline(t:DChar.bnr[bk], bl)[0]]
  let [x, y] = a:dir ? ['b', 'a'] : ['a', 'b']
  let s1 = (1 < et[x].c[0]) ? et[x].t[: et[x].c[0] - 2] : ''
  let s2 = (et[x].e != 'a') ? et[y].t[et[y].c[0] - 1 : et[y].c[-1] - 1] : ''
  if et[x].e == 'd' && [et[x].c[0], et[x].c[-1]] != [0, 0]
    let ds = split(et[x].t[et[x].c[0] - 1 : et[x].c[-1] - 1], '\zs')
    let s2 = ((1 < et[y].c[0]) ? ds[0] : '') . s2 .
                                \((et[y].c[-1] < len(et[y].t)) ? ds[-1] : '')
  endif
  let s3 = (et[x].c[-1] < len(et[x].t)) ? et[x].t[et[x].c[-1] :] : ''
  let ss = s1 . s2 . s3
  if a:dir
    call setbufline(t:DChar.bnr[bk], bl, ss)
    call win_execute(t:DChar.wid[bk], 'let &undolevels = &undolevels')
  else
    call setbufline(t:DChar.bnr[ak], al, ss)
  endif
endfunction

function! s:ToggleDiffCharPair(on) abort
  if t:DChar.dpv.pv == 3 || t:DChar.dpv.pv == 4
    let on = (a:on && empty(t:DChar.dpv.pw)) ? 1 :
                                  \(!a:on && !empty(t:DChar.dpv.pw)) ? 0 : -1
    if on != -1 | call s:SetPopupWindow(on) | endif
  endif
endfunction

function! s:ShowDiffCharPair(key) abort
  if mode(1) != 'n' || !exists('t:DChar') | return | endif
  let [pl, pc] = [t:DChar.lcc[a:key].cl, t:DChar.lcc[a:key].cc]
  let [cl, cc] = [line('.'), col('.')]
  if cc == col('$') | let cc = 0 | endif
  let [t:DChar.lcc[a:key].cl, t:DChar.lcc[a:key].cc] = [cl, cc]
  if t:DChar.lcc[a:key].cn == changenr()
    if !empty(t:DChar.dpv.ch)
      if t:DChar.dpv.ch.bk == a:key
        " clear if a pair accidentally remains on diffsplit
        call s:ClearDiffCharPair((a:key == 1) ? 2 : 1)
      else
        let [hl, hi] = t:DChar.dpv.ch.lc
        let hc = t:DChar.hlc[a:key][hl][hi][1]
        if cl == hl && hc[0] <= cc && cc <= hc[-1] | return | endif
        call s:ClearDiffCharPair(a:key) " outside, clear it
      endif
    endif
    if has_key(t:DChar.hlc[a:key], cl)
      let hu = filter(map(copy(t:DChar.hlc[a:key][cl]), '[v:key, v:val[1]]'),
                                  \'v:val[1][0] <= cc && cc <= v:val[1][-1]')
      if !empty(hu)
        " for 2 continuous 'd', check if cursor moved forward/backward
        let ix = (len(hu) == 1) ? 0 : (cl == pl) ? cc < pc : cl < pl
        call s:HighlightDiffCharPair(a:key, cl, hu[ix][0])
      endif
    endif
  endif
endfunction

function! s:HighlightDiffCharPair(key, line, col) abort
  let [ak, bk] = (a:key == 1) ? [1, 2] : [2, 1]
  let [al, bl] = [a:line, t:DChar.dfl[bk][index(t:DChar.dfl[ak], a:line)]]
  let t:DChar.dpv.ch = #{lc: [al, a:col], bk: bk, id: 0}
  let ae = t:DChar.hlc[ak][al][a:col][0]
  let bc = t:DChar.hlc[bk][bl][a:col][1]
  if [bc[0], bc[-1]] != [0, 0]
    if t:DChar.opt.dv == 2 && ae == 'a'
      let id = t:DChar.mid[bk][bl][a:col]
      let t:DChar.mid[bk][bl][a:col] =
                        \s:ChangeVirtColumn(t:DChar.bnr[bk], id, s:DCharHL.c)
      let t:DChar.dpv.ch.id = t:DChar.mid[bk][bl][a:col]
    else
      let [pos, len] = [bc[0], bc[-1] - bc[0] + 1]
      let t:DChar.dpv.ch.id = matchaddpos(s:DCharHL.c, [[bl, pos, len]],
                                          \-1, -1, #{window: t:DChar.wid[bk]})
    endif
  endif
  call execute('autocmd! diffchar WinLeave <buffer=' . t:DChar.bnr[ak] .
                                    \'> call s:ClearDiffCharPair(' . ak . ')')
  if t:DChar.dpv.pv < 2 | return | endif
  let at = getbufline(t:DChar.bnr[ak], al)[0]
  let bt = getbufline(t:DChar.bnr[bk], bl)[0]
  if ae == 'c'
    let hl = t:DChar.hgp[(count(map(t:DChar.hlc[ak][al][: a:col], 'v:val[0]'),
                                                \'c') - 1) % len(t:DChar.hgp)]
    let [tb, tx, te] = ['', bt[bc[0] - 1 : bc[-1] - 1], '']
  elseif ae == 'd'
    let hl = s:DCharHL.A
    let [tb, tx, te] = [(1 < bc[0]) ? '<' : '', bt[bc[0] - 1 : bc[-1] - 1],
                                              \(bc[-1] < len(bt)) ? '>' : '']
  elseif ae == 'a'
    let hl = s:DCharHL.D
    let [tb, tx, te] = ['>', '', '<']
  endif
  if t:DChar.dpv.pv == 2
    call execute(['echon tb', 'echohl ' . hl, 'echon tx', 'echohl None',
                                                            \'echon te'], '')
  elseif t:DChar.dpv.pv == 3 || t:DChar.dpv.pv == 4
    call s:HighlightPopupWindow(tb . tx . te)
  endif
endfunction

function! s:ClearDiffCharPair(key) abort
  if !exists('t:DChar') | return | endif
  if !empty(t:DChar.dpv.ch)
    let [bk, id] = [t:DChar.dpv.ch.bk, t:DChar.dpv.ch.id]
    if win_id2win(t:DChar.wid[bk]) != 0
      if 0 < id
        silent! call matchdelete(id, t:DChar.wid[bk])
      elseif id < 0
        let [al, bi] = t:DChar.dpv.ch.lc
        let bl = t:DChar.dfl[bk][index(t:DChar.dfl[(bk == 1) ? 2 : 1], al)]
        let t:DChar.mid[bk][bl][bi] =
                        \s:ChangeVirtColumn(t:DChar.bnr[bk], id, s:DCharHL.D)
      endif
    endif
    call execute('autocmd! diffchar WinLeave <buffer=' .
                                        \t:DChar.bnr[(bk == 1) ? 2 : 1] . '>')
    let t:DChar.dpv.ch = {}
  endif
  if t:DChar.dpv.pv == 2 | call execute('echo', '')
  elseif t:DChar.dpv.pv == 3 || t:DChar.dpv.pv == 4
    call s:ClearPopupWindow()
  endif
endfunction

if has('nvim')
function! s:SetPopupWindow(on) abort
  if a:on
    let t:DChar.dpv.pw.fb = nvim_create_buf(0, 1)
    let t:DChar.dpv.pw.fw = nvim_open_win(t:DChar.dpv.pw.fb, 0,
                  \#{relative: 'editor', row: 0, col: 0, height: 1, width: 1,
                                            \focusable: 0, style: 'minimal'})
    call setbufline(t:DChar.dpv.pw.fb, 1, '')
    call setwinvar(t:DChar.dpv.pw.fw, '&winblend', 100)
    call setwinvar(t:DChar.dpv.pw.fw, '&winhighlight',
                                                    \'Normal:' . s:DCharHL.c)
  else
    call nvim_win_close(t:DChar.dpv.pw.fw, 1)
    call nvim_buf_delete(t:DChar.dpv.pw.fb, #{force: 1})
    let t:DChar.dpv.pw = {}
  endif
endfunction

function! s:HighlightPopupWindow(tx) abort
  if t:DChar.dpv.pv == 4 | let mp = getmousepos() | endif
  call nvim_win_set_config(t:DChar.dpv.pw.fw,
    \extend((t:DChar.dpv.pv == 3) ? #{relative: 'cursor', row: 1, col: 0} :
                \#{relative: 'editor', row: mp.screenrow, col: mp.screencol},
                                            \#{width: strdisplaywidth(a:tx)}))
  call setbufline(t:DChar.dpv.pw.fb, 1, a:tx)
  call setwinvar(t:DChar.dpv.pw.fw, '&winblend', 0)
endfunction

function! s:ClearPopupWindow() abort
  call nvim_win_set_config(t:DChar.dpv.pw.fw,
                            \#{relative: 'editor', row: 0, col: 0, width: 1})
  call setbufline(t:DChar.dpv.pw.fb, 1, '')
  call setwinvar(t:DChar.dpv.pw.fw, '&winblend', 100)
endfunction

function! s:SetVirtColumn() abort
  let s:DCharNS = nvim_create_namespace('diffchar')
endfunction

function! s:HighlightVirtColumn(bn, ln, co, hl) abort
  return -nvim_buf_set_extmark(a:bn, s:DCharNS, a:ln - 1, a:co - 1,
                                \#{virt_text: [[t:DChar.opt.vc, a:hl]],
                                \virt_text_pos: 'inline', invalidate: v:true})
endfunction

function! s:ClearVirtColumn(bn, id) abort
  call nvim_buf_del_extmark(a:bn, s:DCharNS, -a:id)
endfunction

function! s:PurgeVirtColumn(bn, ln) abort
endfunction

function! s:ChangeVirtColumn(bn, id, hl) abort
  let lc = nvim_buf_get_extmark_by_id(a:bn, s:DCharNS, -a:id, {})
  if !empty(lc)
    call s:ClearVirtColumn(a:bn, a:id)
    return s:HighlightVirtColumn(a:bn, lc[0] + 1, lc[1] + 1, a:hl)
  endif
  return a:id
endfunction
else
function! s:SetPopupWindow(on) abort
  if a:on
    let t:DChar.dpv.pw = popup_create('', #{hidden: 1, scrollbar: 0, wrap: 0,
                                                    \highlight: s:DCharHL.c})
  else
    let t:DChar.dpv.pw = popup_close(t:DChar.dpv.pw)
  endif
endfunction

function! s:HighlightPopupWindow(tx) abort
  if t:DChar.dpv.pv == 4 | let mp = getmousepos() | endif
  call popup_move(t:DChar.dpv.pw, (t:DChar.dpv.pv == 3) ?
                                        \#{line: 'cursor+1', col: 'cursor'} :
                                    \#{line: mp.screenrow, col: mp.screencol})
  call popup_settext(t:DChar.dpv.pw, a:tx)
  call popup_show(t:DChar.dpv.pw)
endfunction

function! s:ClearPopupWindow() abort
  call popup_hide(t:DChar.dpv.pw)
endfunction

function! s:SetVirtColumn() abort
  for hl in [s:DCharHL.c, s:DCharHL.D]
    call call(empty(prop_type_get(hl)) ?
                \'prop_type_add' : 'prop_type_change', [hl, #{highlight: hl}])
  endfor
endfunction

function! s:HighlightVirtColumn(bn, ln, co, hl) abort
  return prop_add(a:ln, a:co, #{bufnr: a:bn, type: a:hl,
                                                      \text: t:DChar.opt.vc})
endfunction

function! s:ClearVirtColumn(bn, id) abort
  call prop_remove(#{bufnr: a:bn, id: a:id})
endfunction

function! s:PurgeVirtColumn(bn, ln) abort
  for hl in [s:DCharHL.c, s:DCharHL.D]
    call prop_remove(#{bufnr: a:bn, type: hl, all: 1}, a:ln)
  endfor
endfunction

function! s:ChangeVirtColumn(bn, id, hl) abort
  let pr = prop_find(#{bufnr: a:bn, id: a:id, lnum: 1, col: 1})
  if !empty(pr)
    call s:ClearVirtColumn(a:bn, a:id)
    return s:HighlightVirtColumn(a:bn, pr.lnum, pr.col, a:hl)
  endif
  return a:id
endfunction
endif

function! diffchar#ToggleDiffModeSync(...) abort
  " a:0 : 0 = OptionSet diff, 1 = VimEnter
  if !exists('t:DChar') && !s:GetOptionVar('DiffChar', 1) | return | endif
  if a:0 || v:option_old != v:option_new
    let cw = win_getid()
    if exists('t:DChar') && ((a:0 || v:option_new) ?
                            \index(values(t:DChar.bnr), winbufnr(cw)) == -1 :
                                        \index(values(t:DChar.wid), cw) != -1)
      " diff mode ON on non-DChar buf || OFF on DChar win, try reset
      let dk = filter([1, 2], 'getwinvar(t:DChar.wid[v:val], "&diff")')
      if !empty(dk)
        if empty(filter(copy(dk), 't:DChar.wid[v:val] == cw'))
          let cw = t:DChar.wid[dk[0]]
        endif
        call win_execute(cw, 'call s:ResetDiffChar()')
      endif
    endif
    if !exists('t:DChar')
      let aw = win_id2win(cw)
      let dw = filter(map(range(aw, winnr('$')) + range(1, aw - 1),
                            \'win_getid(v:val)'), 'getwinvar(v:val, "&diff")')
      if 1 < len(dw)
        " 2 or more diff mode wins exists, try show
        call win_execute(dw[0], 'call s:ShowDiffChar()')
      endif
    endif
  endif
endfunction

function! s:WinClosedDiffChar() abort
  " reset and show (if possible) DChar on WinClosed
  for ti in filter(gettabinfo(), 'has_key(v:val.variables, "DChar")')
    let dc = ti.variables.DChar
    for k in [1, 2]
      if dc.wid[k] == eval(expand('<amatch>'))
        call win_execute(dc.wid[k], 'call s:ResetDiffChar()')
        let dw = filter(ti.windows, 'v:val != dc.wid[k] &&
                  \winbufnr(v:val) == dc.bnr[k] && getwinvar(v:val, "&diff")')
        if !empty(dw)
          call win_execute(dw[0], 'call s:ShowDiffChar()')
        endif
        return
      endif
    endfor
  endfor
endfunction

function! s:RepairDiffChar() abort
  " repair DChar whose win was accidentally closed on BufWinEnter/WinEnter
  if exists('t:DChar')
    let dc = t:DChar
    let dw = filter(copy(dc.wid), 'win_id2win(v:val) != 0 &&
              \winbufnr(v:val) == dc.bnr[v:key] && getwinvar(v:val, "&diff")')
    if len(dw) == 1
      call win_execute(values(dw)[0], ['call s:ResetDiffChar()',
                                                    \'call s:ShowDiffChar()'])
    endif
  endif
endfunction

function! s:EchoWarning(msg) abort
  call timer_start(0, {-> execute(['echohl WarningMsg',
                  \'echo "[diffchar]" ' . string(a:msg), 'echohl None'], '')})
endfunction

if !has('vim9script') || !get(g:, 'vim9script', 1)
function! s:TraceDiffChar(u1, u2, ih) abort
  " An O(NP) Sequence Comparison Algorithm
  let [u1, u2, eq, e1, e2] = [a:u1, a:u2, '=', '-', '+']
  let [n1, n2] = [len(u1), len(u2)]
  if u1 ==# u2 | return repeat(eq, n1)
  elseif n1 == 0 | return repeat(e2, n2)
  elseif n2 == 0 | return repeat(e1, n1)
  endif
  let [N, M, u1, u2] = (n1 >= n2) ? [n1, n2, u1, u2] : [n2, n1, u2, u1]
  if n1 < n2 | let [e1, e2] = [e2, e1] | endif
  let D = N - M
  let fp = repeat([-1], M + N + 1)
  let etree = []    " [next edit, previous p, previous k]
  let p = -1
  while fp[D] != N
    let p += 1
    let epk = repeat([[]], p * 2 + D + 1)
    for k in range(-p, D - 1, 1) + range(D + p, D, -1)
      let [y, epk[k]] = (fp[k - 1] + 1 > fp[k + 1]) ?
                        \[fp[k - 1] + 1, [e1, [(k > D) ? p - 1 : p, k - 1]]] :
                        \[fp[k + 1], [e2, [(k < D) ? p - 1 : p, k + 1]]]
      let x = y - k
      while x < M && y < N && u2[x] ==# u1[y]
        let epk[k][0] .= eq | let [x, y] += [1, 1]
      endwhile
      let fp[k] = y
    endfor
    let etree += [epk]
  endwhile
  let ses = ''
  while 1
    let ses = etree[p][k][0] . ses
    if [p, k] == [0, 0] | break | endif
    let [p, k] = etree[p][k][1]
  endwhile
  let ses = ses[1 :]
  return a:ih ? s:ReduceDiffHunk(a:u1, a:u2, ses) : ses
endfunction

function! s:ApplyDiffFunc(u1, u2, ih) abort
  let [eq, e1, e2] = ['=', '-', '+']
  let [n1, n2] = [len(a:u1), len(a:u2)]
  if a:u1 ==# a:u2 | return repeat(eq, n1)
  elseif n1 == 0 | return repeat(e2, n2)
  elseif n2 == 0 | return repeat(e1, n1)
  endif
  let ses = ''
  let vd = s:DiffFunc(a:u1, a:u2)
  if !empty(vd)
    let p1 = 0
    for [i1, c1, i2, c2] in vd + [[n1, 0, 0, 0]]
      let ses .= repeat(eq, i1 - p1) . repeat(e1, c1) . repeat(e2, c2)
      let p1 = i1 + c1
    endfor
  endif
  return a:ih ? s:ReduceDiffHunk(a:u1, a:u2, ses) : ses
endfunction

function! s:ReduceDiffHunk(u1, u2, ses) abort
  " in ==++++/==----, if == units equal to last ++/-- units, swap their SESs
  " (AB vs AxByAB : =+=+++ -> =++++= -> ++++==)
  let [eq, e1, e2] = ['=', '-', '+']
  let [p1, p2] = [-1, -1] | let ses = '' | let ez = ''
  for ed in reverse(split(a:ses, '[+-]\+\zs'))
    let es = ed . ez | let ez = '' | let qe = count(es, eq)
    if 0 < qe
      let [q1, q2] = [count(es, e1), count(es, e2)]
      let [uu, pp, qq] = (qe <= q1 && q2 == 0) ? [a:u1, p1, q1] :
                        \(q1 == 0 && qe <= q2) ? [a:u2, p2, q2] : [[], 0, 0]
      if !empty(uu) && uu[pp - qq - qe + 1 : pp - qq] ==# uu[pp - qe + 1 : pp]
        let ez = es[-qe :] . es[qe : -qe - 1] | let es = es[: qe - 1]
      else
        let [p1, p2] -= [q1, q2]
      endif
    endif
    let [p1, p2] -= [qe, qe]
    let ses = es . ses
  endfor
  let ses = ez . ses
  return ses
endfunction

if has('nvim')
function! s:DiffFunc(u1, u2) abort
  return map(v:lua.vim.diff(join(a:u1, "\n") . "\n", join(a:u2, "\n") . "\n",
                                                  \#{result_type: 'indices'}),
                            \'[v:val[0] - ((0 < v:val[1]) ? 1 : 0), v:val[1],
                            \v:val[2] - ((0 < v:val[3]) ? 1 : 0), v:val[3]]')
endfunction
else
function! s:DiffFunc(u1, u2) abort
  return map(diff(a:u1, a:u2, #{output: 'indices'}),
          \'[v:val.from_idx, v:val.from_count, v:val.to_idx, v:val.to_count]')
endfunction
endif

function! s:GetDiffUnitPos(uu, es) abort
  if empty(a:uu[0])
    return [[['d', [0, 0]]], [['a', [1, len(join(a:uu[1], ''))]]]]
  elseif empty(a:uu[1])
    return [[['a', [1, len(join(a:uu[0], ''))]]], [['d', [0, 0]]]]
  endif
  let cc = [[], []] | let ll = [1, 1] | let pp = [0, 0]
  for ed in split(a:es, '[+-]\+\zs', 1)[: -2]
    let qe = count(ed, '=') | let qq = [count(ed, '-'), count(ed, '+')]
    let ee = (qq[0] == 0) ? ['d', 'a'] : (qq[1] == 0) ? ['a', 'd'] :
                                                                  \['c', 'c']
    for k in [0, 1]
      if 0 < qe
        let [ll[k], pp[k]] +=
                        \[len(join(a:uu[k][pp[k] : pp[k] + qe - 1], '')), qe]
      endif
      if 0 < qq[k]
        let hh = [ll[k]]
        let [ll[k], pp[k]] +=
                  \[len(join(a:uu[k][pp[k] : pp[k] + qq[k] - 1], '')), qq[k]]
        let hh += [ll[k] - 1]
      else
        let hh = [ll[k] - ((0 < pp[k]) ?
                                \len(matchstr(a:uu[k][pp[k] - 1], '.$')) : 0),
                  \ll[k] + ((pp[k] < len(a:uu[k])) ?
                                \len(matchstr(a:uu[k][pp[k]], '^.')) : 0) - 1]
      endif
      if t:DChar.opt.dv == 2
        call extend(hh, [(0 < qq[k]) ? 0 : ll[k]], 1)
      endif
      let cc[k] += [[ee[k], hh]]
    endfor
  endfor
  return cc
endfunction

function! s:HighlightDiffChar(lec) abort
  let hn = len(t:DChar.hgp)
  for k in [1, 2]
    for [ln, ec] in items(a:lec[k])
      if has_key(t:DChar.mid[k], ln) | continue | endif
      if t:DChar.opt.dv == 2
        call s:PurgeVirtColumn(t:DChar.bnr[k], ln)
      endif
      let t:DChar.mid[k][ln] = []
      let t:DChar.hlc[k][ln] = ec
      let cn = 0
      for [ed, co] in ec
        if ed == 'c'
          let hl = t:DChar.hgp[cn % hn] | let cn += 1
        elseif ed == 'a'
          let hl = s:DCharHL.A
        elseif ed == 'd'
          if t:DChar.opt.dv == 0 || [co[0], co[-1]] == [0, 0]
            continue
          endif
          let hl = s:DCharHL.E
        endif
        let t:DChar.mid[k][ln] += [(t:DChar.opt.dv == 2 && ed == 'd') ?
              \s:HighlightVirtColumn(t:DChar.bnr[k], ln, co[1], s:DCharHL.D) :
              \matchaddpos(hl, [[ln, co[0], co[-1] - co[0] + 1]], -3, -1,
                                                  \#{window: t:DChar.wid[k]})]
      endfor
      let t:DChar.mid[k][ln] += [matchaddpos(s:DCharHL.C, [[ln]], -5, -1,
                                                  \#{window: t:DChar.wid[k]})]
    endfor
  endfor
endfunction

function! s:ClearDiffChar(lines) abort
  for k in [1, 2]
    let wd = win_id2win(t:DChar.wid[k])
    for ln in a:lines[k]
      if wd != 0
        for id in t:DChar.mid[k][ln]
          if 0 < id
            silent! call matchdelete(id, t:DChar.wid[k])
          elseif id < 0
            call s:ClearVirtColumn(t:DChar.bnr[k], id)
          endif
        endfor
        if t:DChar.opt.dv == 2
          call s:PurgeVirtColumn(t:DChar.bnr[k], ln)
        endif
      endif
      unlet t:DChar.mid[k][ln]
      unlet t:DChar.hlc[k][ln]
    endfor
  endfor
endfunction

function! s:ScrollDiffLines(key) abort
  " called on WinScrolled or other
  if !exists('t:DChar') | return | endif
  if 0 < a:key | let ak = a:key
  else
    let wid = eval(expand('<amatch>'))
    let ak = 2 | while t:DChar.wid[ak] != wid | let ak -= 1
      if ak == 0 | return | endif
    endwhile
  endif
  let lcc = s:GetLineColCnr()
  let sk = 0
  for k in [1, 2]
    " check if a scroll happens in either window with no change on both
    let sk += (t:DChar.lcc[k].cn != lcc[k].cn) ? -k * 10 :
          \(lcc[k].tl < t:DChar.lcc[k].tl || t:DChar.lcc[k].bl < lcc[k].bl) &&
                  \(empty(t:DChar.dfl[k]) || lcc[k].tl < t:DChar.dfl[k][0] ||
                                      \t:DChar.dfl[k][-1] < lcc[k].bl) ? k : 0
    let [t:DChar.lcc[k].tl, t:DChar.lcc[k].bl, t:DChar.lcc[k].cl] =
                                            \[lcc[k].tl, lcc[k].bl, lcc[k].cl]
  endfor
  if 0 < sk
    let dfl = s:FocusDiffLines(sk)
    if t:DChar.dfl != dfl
      " reset/show DChar lines on dfl changes
      let [dl, al] = [t:DChar.dfl[ak], dfl[ak]]
      let ix = map([0, -1], 'index(t:DChar.dfl[ak], dfl[ak][v:val])')
      if ix != [-1, -1]
        let [dl, al] = [[], []]
        if ix[0] != -1
          let dl += (0 < ix[0]) ? t:DChar.dfl[ak][: ix[0] - 1] : []
        else
          let al = dfl[ak][: index(dfl[ak], t:DChar.dfl[ak][0]) - 1]
        endif
        if ix[1] != -1
          let dl += (ix[1] < len(t:DChar.dfl[ak])) ?
                                            \t:DChar.dfl[ak][ix[1] + 1 :] : []
        else
          let al = dfl[ak][index(dfl[ak], t:DChar.dfl[ak][-1]) + 1 :]
        endif
      endif
      if !empty(dl)
        call win_execute(t:DChar.wid[ak], 'call s:ResetDiffChar(dl)')
      endif
      let t:DChar.dfl = dfl
      if !empty(al)
        call win_execute(t:DChar.wid[ak], 'call s:ShowDiffChar(al)')
      endif
    endif
  endif
endfunction

function! s:FocusDiffLines(key) abort
  " a:key : initiate dfl (0), either (1/2) or both (3) key using existing dfl
  let dfl = {}
  if t:DChar.opt.fp == 0
    if a:key == 0
      for k in [1, 2]
        call win_execute(t:DChar.wid[k], 'let dfl[k] =
                                      \s:GetDiffLines(1, t:DChar.lcc[k].ll)')
      endfor
      return dfl
    else
      return t:DChar.dfl
    endif
  endif
  " select specified win or more line displaying win as main
  let tb = (a:key == 1) ? [1, 0] : (a:key == 2) ? [0, 1] :
                \map([1, 2], 't:DChar.lcc[v:val].bl - t:DChar.lcc[v:val].tl')
  let [ak, bk] = (tb[0] >= tb[1]) ? [1, 2] : [2, 1]
  " get visible and upper/lower dfl in main win
  let [tl, bl] = [t:DChar.lcc[ak].tl, t:DChar.lcc[ak].bl]
  call win_execute(t:DChar.wid[ak], 'let dfl[ak] = s:GetDiffLines(tl, bl)')
  let [tx, bx] = [1, t:DChar.lcc[ak].ll]
  if 0 < t:DChar.opt.fp && 0 < a:key && !empty(t:DChar.dfl[ak])
    if tl > t:DChar.dfl[ak][0] | let tx = t:DChar.dfl[ak][-1] | endif
    if bl < t:DChar.dfl[ak][-1] | let bx = t:DChar.dfl[ak][0] | endif
  endif
  let [tl, bl] += [-1, 1]
  if 1 < abs(t:DChar.opt.fp)
    let [tz, bz] = [[], []]
    let rc = winheight(t:DChar.wid[ak]) * (abs(t:DChar.opt.fp) - 1)
    while 0 < rc
      let fc = 0
      if tx <= tl
        call win_execute(t:DChar.wid[ak], 'let fc = foldclosed(tl)')
        if fc == -1 | let tz = [tl] + tz | else | let tl = fc | endif
        let tl -= 1 | let rc -= 1
      endif
      if bl <= bx
        call win_execute(t:DChar.wid[ak], 'let fc = foldclosedend(bl)')
        if fc == -1 | let bz += [bl] | else | let bl = fc | endif
        let bl += 1 | let rc -= 1
      endif
      if fc == 0 | break | endif
    endwhile
    call win_execute(t:DChar.wid[ak], 'let dfl[ak] =
                      \s:CheckDiffLines(tz) + dfl[ak] + s:CheckDiffLines(bz)')
  endif
  " if no dfl found in dfp, try to find one toward top/bottom
  if empty(dfl[ak])
    if 0 < a:key | return t:DChar.dfl | endif
    call win_execute(t:DChar.wid[ak], 'let dfl[ak] =
                  \s:SearchDiffLines(0, tl, 1) + s:SearchDiffLines(1, bl, 1)')
    if empty(dfl[ak]) | let dfl[bk] = [] | return dfl | endif
  endif
  " get dfl in sub win based on the corresponding line between main/sub
  let ds = #{t: [1, 1, 1, dfl[ak][0] - 1],
            \b: [0, t:DChar.lcc[bk].ll, dfl[ak][-1] + 1, t:DChar.lcc[ak].ll]}
  let ix = -1
  if 0 < a:key && !empty(t:DChar.dfl[ak])
    let ix = index(t:DChar.dfl[ak], dfl[ak][0])
    if ix != -1
      let [sd, sl, dc] = [1, t:DChar.dfl[bk][ix], 0]
    else
      let ix = index(t:DChar.dfl[ak], dfl[ak][-1])
      if ix != -1
        let [sd, sl, dc] = [0, t:DChar.dfl[bk][ix], 0]
      else
        if t:DChar.dfl[ak][-1] < dfl[ak][0]
          let ds.t = [1, t:DChar.dfl[bk][-1], t:DChar.dfl[ak][-1],
                                                              \dfl[ak][0] - 1]
        elseif dfl[ak][-1] < t:DChar.dfl[ak][0]
          let ds.b = [0, t:DChar.dfl[bk][0], dfl[ak][-1] + 1,
                                                          \t:DChar.dfl[ak][0]]
        endif
      endif
    endif
  endif
  if ix == -1
    let [sd, sl, df, dl] = (ds.t[3] - ds.t[2] <= ds.b[3] - ds.b[2]) ?
                                                                  \ds.t : ds.b
    call win_execute(t:DChar.wid[ak], 'let dc = len(s:GetDiffLines(df, dl))')
  endif
  let ac = len(dfl[ak])
  call win_execute(t:DChar.wid[bk], 'let dfl[bk] =
                                        \s:SearchDiffLines(sd, sl, dc + ac)')
  let bc = len(dfl[bk])
  if ac != bc
    let [xk, xc] = (ac < bc) ? [bk, ac] : [ak, bc]
    let dfl[xk] = (xc == 0) ? [] : sd ? dfl[xk][-xc :] : dfl[xk][: xc - 1]
  endif
  " repair current dfl and merge with new dfl
  if 0 < a:key && !empty(t:DChar.dfl[ak]) && t:DChar.dfl != dfl
    " repair and redraw current on-screen dfl realigned by linematch
    if t:DChar.opt.lm
      let dxl = deepcopy(t:DChar.dfl)
      let dfi = {}
      for k in [1, 2]
        let dfi[k] = map(copy(dfl[k]), 'index(dxl[k], v:val)')
      endfor
      let [dl, al] = [[], []]
      if dfi[1] != dfi[2]
        for fi in range(len(dfi[ak]))
          let [i1, i2] = [dfi[1][fi], dfi[2][fi]]
          let [ki, xi] = (i1 == -1 && i2 != -1) ? [1, i2] :
                                  \(i1 != -1 && i2 == -1) ? [2, i1] : [0, -1]
          if ki != 0
            let [dl, al] += [[dxl[ak][xi]], [dfl[ak][fi]]]
            let dxl[ki][xi] = dfl[ki][fi]
          endif
        endfor
      endif
      if !empty(dl)
        call win_execute(t:DChar.wid[ak], 'call s:ResetDiffChar(dl)')
      endif
      let t:DChar.dfl = dxl
      if !empty(al)
        call win_execute(t:DChar.wid[ak], 'call s:ShowDiffChar(al)')
      endif
    endif
    " merge current and new dfls if overlapped or continued
    if 0 < t:DChar.opt.fp
      let mx = 0
      if t:DChar.dfl[ak][0] <= dfl[ak][0]
        if t:DChar.dfl[ak][-1] >= dfl[ak][0] | let mx = 1
        else
          call win_execute(t:DChar.wid[ak],
                \'let nl = s:SearchDiffLines(1, t:DChar.dfl[ak][-1] + 1, 1)')
          if !empty(nl) && dfl[ak][0] <= nl[0] | let mx = 2 | endif
        endif
      elseif dfl[ak][-1] <= t:DChar.dfl[ak][-1]
        if dfl[ak][-1] >= t:DChar.dfl[ak][0] | let mx = -1
        else
          call win_execute(t:DChar.wid[ak],
                  \'let nl = s:SearchDiffLines(0, t:DChar.dfl[ak][0] - 1, 1)')
          if !empty(nl) && nl[0] <= dfl[ak][-1] | let mx = -2 | endif
        endif
      endif
      if mx != 0
        for k in [1, 2]
          let dfl[k] = (0 < mx) ?
              \t:DChar.dfl[k] + filter(dfl[k], 'v:val > t:DChar.dfl[k][-1]') :
              \filter(dfl[k], 'v:val < t:DChar.dfl[k][0]') + t:DChar.dfl[k]
        endfor
      endif
    endif
  endif
  return dfl
endfunction

function! s:SearchDiffLines(sd, sl, sc) abort
  " a:sd = direction (1:down, 0:up), a:sl = start line, a:sc = count
  let dl = [] | let [ln, sc] = [a:sl, a:sc]
  if a:sd
    while 0 < sc && ln <= line('$')
      let zl = []
      while len(zl) < sc
        let fc = foldclosedend(ln)
        if fc == -1 | let zl += [ln] | else | let ln = fc | endif
        let ln += 1
      endwhile
      let zl = s:CheckDiffLines(zl) | let sc -= len(zl) | let dl += zl
    endwhile
  else
    while 0 < sc && 1 <= ln
      let zl = []
      while len(zl) < sc
        let fc = foldclosed(ln)
        if fc == -1 | let zl = [ln] + zl | else | let ln = fc | endif
        let ln -= 1
      endwhile
      let zl = s:CheckDiffLines(zl) | let sc -= len(zl) | let dl = zl + dl
    endwhile
  endif
  return dl
endfunction

function! s:GetDiffLines(sl, el) abort
  let dl = [] | let ln = a:sl
  while ln <= a:el
    let fc = foldclosedend(ln)
    if fc == -1 | let dl += [ln] | else | let ln = fc | endif
    let ln += 1
  endwhile
  return s:CheckDiffLines(dl)
endfunction

function! s:CheckDiffLines(ll) abort
  " check [0] first, diff_hlID() sometimes fails for the 1st entry of the list
  return filter([0] + a:ll,
          \'index([s:DiffHL.C.id, s:DiffHL.T.id], diff_hlID(v:val, 1)) != -1')
endfunction

function! s:ChecksumStr(str) abort
  return sha256(a:str)[: 5]
endfunction
else
function! s:DiffCharVim9Functions() abort
def! s:TraceDiffChar(u1: list<string>, u2: list<string>, ih: bool): string
  # An O(NP) Sequence Comparison Algorithm
  const [eq, n1, n2] = ['=', len(u1), len(u2)]
  var [e1, e2] = ['-', '+']
  if u1 ==# u2 | return repeat(eq, n1)
  elseif n1 == 0 | return repeat(e2, n2)
  elseif n2 == 0 | return repeat(e1, n1)
  endif
  const [N, M, v1, v2] = (n1 >= n2) ? [n1, n2, u1, u2] : [n2, n1, u2, u1]
  if n1 < n2 | [e1, e2] = [e2, e1] | endif
  const D = N - M
  var fp = repeat([-1], M + N + 1)
  var etree = []    # [next edit, previous p, previous k]
  var p = -1
  while fp[D] != N
    p += 1
    var epk = repeat([[]], p * 2 + D + 1)
    for k in range(-p, D - 1, 1) + range(D + p, D, -1)
      var x: number | var y: number
      [y, epk[k]] = (fp[k - 1] + 1 > fp[k + 1]) ?
                        [fp[k - 1] + 1, [e1, [(k > D) ? p - 1 : p, k - 1]]] :
                        [fp[k + 1], [e2, [(k < D) ? p - 1 : p, k + 1]]]
      x = y - k
      while x < M && y < N && v2[x] ==# v1[y]
        epk[k][0] ..= eq | [x, y] += [1, 1]
      endwhile
      fp[k] = y
    endfor
    etree += [epk]
  endwhile
  var k = D
  var ses = ''
  while 1
    ses = etree[p][k][0] .. ses
    if [p, k] == [0, 0] | break | endif
    [p, k] = etree[p][k][1]
  endwhile
  ses = ses[1 :]
  return ih ? s:ReduceDiffHunk(u1, u2, ses) : ses
enddef

def! s:ApplyDiffFunc(u1: list<string>, u2: list<string>, ih: bool): string
  const [eq, e1, e2] = ['=', '-', '+']
  const [n1, n2] = [len(u1), len(u2)]
  if u1 ==# u2 | return repeat(eq, n1)
  elseif n1 == 0 | return repeat(e2, n2)
  elseif n2 == 0 | return repeat(e1, n1)
  endif
  var ses = ''
  var vd = map(diff(u1, u2, {'output': 'indices'}),
                  (_, v) => [v.from_idx, v.from_count, v.to_idx, v.to_count])
  if !empty(vd)
    var p1 = 0
    for [i1, c1, i2, c2] in vd + [[n1, 0, 0, 0]]
      ses ..= repeat(eq, i1 - p1) .. repeat(e1, c1) .. repeat(e2, c2)
      p1 = i1 + c1
    endfor
  endif
  return ih ? s:ReduceDiffHunk(u1, u2, ses) : ses
enddef

def! s:ReduceDiffHunk(u1: list<string>, u2: list<string>, ses: string): string
  # in ==++++/==----, if == units equal to last ++/-- units, swap their SESs
  # (AB vs AxByAB : =+=+++ -> =++++= -> ++++==)
  const [eq, e1, e2] = ['=', '-', '+']
  var [p1, p2] = [-1, -1] | var xes = '' | var ez = ''
  for ed in reverse(split(ses, '[+-]\+\zs'))
    var es = ed .. ez | ez = '' | const qe = count(es, eq)
    if 0 < qe
      const [q1, q2] = [count(es, e1), count(es, e2)]
      const [uu, pp, qq] = (qe <= q1 && q2 == 0) ? [u1, p1, q1] :
                            (q1 == 0 && qe <= q2) ? [u2, p2, q2] : [[], 0, 0]
      if !empty(uu) && uu[pp - qq - qe + 1 : pp - qq] ==# uu[pp - qe + 1 : pp]
        ez = es[-qe :] .. es[qe : -qe - 1] | es = es[: qe - 1]
      else
        [p1, p2] -= [q1, q2]
      endif
    endif
    [p1, p2] -= [qe, qe]
    xes = es .. xes
  endfor
  xes = ez .. xes
  return xes
enddef

def! s:GetDiffUnitPos(uu: list<any>, es: string): list<any>
  if empty(uu[0])
    return [[['d', [0, 0]]], [['a', [1, len(join(uu[1], ''))]]]]
  elseif empty(uu[1])
    return [[['a', [1, len(join(uu[0], ''))]]], [['d', [0, 0]]]]
  endif
  var cc = [[], []] | var ll = [1, 1] | var pp = [0, 0]
  for ed in split(es, '[+-]\+\zs', 1)[: -2]
    var qe = count(ed, '=') | var qq = [count(ed, '-'), count(ed, '+')]
    var ee = (qq[0] == 0) ? ['d', 'a'] : (qq[1] == 0) ? ['a', 'd'] :
                                                                    ['c', 'c']
    for k in [0, 1]
      if 0 < qe
        [ll[k], pp[k]] += [len(join(uu[k][pp[k] : pp[k] + qe - 1], '')), qe]
      endif
      var hh: list<number>
      if 0 < qq[k]
        hh = [ll[k]]
        [ll[k], pp[k]] +=
                      [len(join(uu[k][pp[k] : pp[k] + qq[k] - 1], '')), qq[k]]
        hh += [ll[k] - 1]
      else
        hh = [ll[k] - ((0 < pp[k]) ?
                                  len(matchstr(uu[k][pp[k] - 1], '.$')) : 0),
              ll[k] + ((pp[k] < len(uu[k])) ?
                                  len(matchstr(uu[k][pp[k]], '^.')) : 0) - 1]
      endif
      if t:DChar.opt.dv == 2
        extend(hh, [(0 < qq[k]) ? 0 : ll[k]], 1)
      endif
      cc[k] += [[ee[k], hh]]
    endfor
  endfor
  return cc
enddef

def! s:HighlightDiffChar(lec: dict<any>)
  const hn = len(t:DChar.hgp)
  for k in [1, 2]
    for [l, ec] in items(lec[k])
      var ln = eval(l)
      if has_key(t:DChar.mid[k], ln) | continue | endif
      if t:DChar.opt.dv == 2
        s:PurgeVirtColumn(t:DChar.bnr[k], ln)
      endif
      t:DChar.mid[k][ln] = []
      t:DChar.hlc[k][ln] = ec
      var hl: string | var cn = 0
      for [ed, co] in ec
        if ed == 'c'
          hl = t:DChar.hgp[cn % hn] | cn += 1
        elseif ed == 'a'
          hl = s:DCharHL.A
        elseif ed == 'd'
          if t:DChar.opt.dv == 0 || [co[0], co[-1]] == [0, 0]
            continue
          endif
          hl = s:DCharHL.E
        endif
        t:DChar.mid[k][ln] += [(t:DChar.opt.dv == 2 && ed == 'd') ?
              s:HighlightVirtColumn(t:DChar.bnr[k], ln, co[1], s:DCharHL.D) :
              matchaddpos(hl, [[ln, co[0], co[-1] - co[0] + 1]], -3, -1,
                                                  {'window': t:DChar.wid[k]})]
      endfor
      t:DChar.mid[k][ln] += [matchaddpos(s:DCharHL.C, [ln], -5, -1,
                                                  {'window': t:DChar.wid[k]})]
    endfor
  endfor
enddef

def! s:ClearDiffChar(lines: dict<any>)
  for k in [1, 2]
    const wd = win_id2win(t:DChar.wid[k])
    for ln in lines[k]
      if wd != 0
        for id in t:DChar.mid[k][ln]
          if 0 < id
            silent! matchdelete(id, t:DChar.wid[k])
          elseif id < 0
            s:ClearVirtColumn(t:DChar.bnr[k], id)
          endif
        endfor
        if t:DChar.opt.dv == 2
          s:PurgeVirtColumn(t:DChar.bnr[k], ln)
        endif
      endif
      unlet t:DChar.mid[k][ln]
      unlet t:DChar.hlc[k][ln]
    endfor
  endfor
enddef

def! s:ScrollDiffLines(key: number)
  # called on WinScrolled or other
  if !exists('t:DChar') | return | endif
  var ak: number
  if 0 < key | ak = key
  else
    const wid = eval(expand('<amatch>'))
    ak = 2 | while t:DChar.wid[ak] != wid | ak -= 1
      if ak == 0 | return | endif
    endwhile
  endif
  const lcc = s:GetLineColCnr()
  var sk = 0
  for k in [1, 2]
    # check if a scroll happens in either window with no change on both
    sk += (t:DChar.lcc[k].cn != lcc[k].cn) ? -k * 10 :
          (lcc[k].tl < t:DChar.lcc[k].tl || t:DChar.lcc[k].bl < lcc[k].bl) &&
                    (empty(t:DChar.dfl[k]) || lcc[k].tl < t:DChar.dfl[k][0] ||
                                      t:DChar.dfl[k][-1] < lcc[k].bl) ? k : 0
    [t:DChar.lcc[k].tl, t:DChar.lcc[k].bl, t:DChar.lcc[k].cl] =
                                            [lcc[k].tl, lcc[k].bl, lcc[k].cl]
  endfor
  if 0 < sk
    const dfl = s:FocusDiffLines(sk)
    if t:DChar.dfl != dfl
      # reset/show DChar lines on dfl changes
      var [dl, al] = [t:DChar.dfl[ak], dfl[ak]]
      const ix = map([0, -1], (_, v) => index(t:DChar.dfl[ak], dfl[ak][v]))
      if ix != [-1, -1]
        [dl, al] = [[], []]
        if ix[0] != -1
          dl += (0 < ix[0]) ? t:DChar.dfl[ak][: ix[0] - 1] : []
        else
          al = dfl[ak][: index(dfl[ak], t:DChar.dfl[ak][0]) - 1]
        endif
        if ix[1] != -1
          dl += (ix[1] < len(t:DChar.dfl[ak])) ?
                                            t:DChar.dfl[ak][ix[1] + 1 :] : []
        else
          al = dfl[ak][index(dfl[ak], t:DChar.dfl[ak][-1]) + 1 :]
        endif
      endif
      if !empty(dl)
        s:WinExec(t:DChar.wid[ak], 's:ResetDiffChar', [dl])
      endif
      t:DChar.dfl = dfl
      if !empty(al)
        s:WinExec(t:DChar.wid[ak], 's:ShowDiffChar', [al])
      endif
    endif
  endif
enddef

def! s:FocusDiffLines(key: number): dict<any>
  # a:key : initiate dfl (0), either (1/2) or both (3) key using existing dfl
  var dfl = {}
  if t:DChar.opt.fp == 0
    if key == 0
      for k in [1, 2]
        dfl[k] = s:WinExec(t:DChar.wid[k], 's:GetDiffLines',
                                                      [1, t:DChar.lcc[k].ll])
      endfor
      return dfl
    else
      return t:DChar.dfl
    endif
  endif
  # select specified win or more line displaying win as main
  const tb = (key == 1) ? [1, 0] : (key == 2) ? [0, 1] :
                  map([1, 2], (_, v) => t:DChar.lcc[v].bl - t:DChar.lcc[v].tl)
  const [ak, bk] = (tb[0] >= tb[1]) ? [1, 2] : [2, 1]
  # get visible and upper/lower dfl in main win
  var [tl, bl] = [t:DChar.lcc[ak].tl, t:DChar.lcc[ak].bl]
  dfl[ak] = s:WinExec(t:DChar.wid[ak], 's:GetDiffLines', [tl, bl])
  var [tx, bx] = [1, t:DChar.lcc[ak].ll]
  if 0 < t:DChar.opt.fp && 0 < key && !empty(t:DChar.dfl[ak])
    if tl > t:DChar.dfl[ak][0] | tx = t:DChar.dfl[ak][-1] | endif
    if bl < t:DChar.dfl[ak][-1] | bx = t:DChar.dfl[ak][0] | endif
  endif
  [tl, bl] += [-1, 1]
  if 1 < abs(t:DChar.opt.fp)
    var [tz, bz] = [[], []]
    var rc = winheight(t:DChar.wid[ak]) * (abs(t:DChar.opt.fp) - 1)
    while 0 < rc
      var fc = 0
      if tx <= tl
        fc = s:WinExec(t:DChar.wid[ak], 'foldclosed', [tl])
        if fc == -1 | tz = [tl] + tz | else | tl = fc | endif
        tl -= 1 | rc -= 1
      endif
      if bl <= bx
        fc = s:WinExec(t:DChar.wid[ak], 'foldclosedend', [bl])
        if fc == -1 | bz += [bl] | else | bl = fc | endif
        bl += 1 | rc -= 1
      endif
      if fc == 0 | break | endif
    endwhile
    dfl[ak] = s:WinExec(t:DChar.wid[ak], 's:CheckDiffLines', [tz]) +
                dfl[ak] + s:WinExec(t:DChar.wid[ak], 's:CheckDiffLines', [bz])
  endif
  # if no dfl found in dfp, try to find one toward top/bottom
  if empty(dfl[ak])
    if 0 < key | return t:DChar.dfl | endif
    dfl[ak] = s:WinExec(t:DChar.wid[ak], 's:SearchDiffLines', [0, tl, 1]) +
                  s:WinExec(t:DChar.wid[ak], 's:SearchDiffLines', [1, bl, 1])
    if empty(dfl[ak]) | dfl[bk] = [] | return dfl | endif
  endif
  # get dfl in sub win based on the corresponding line between main/sub
  var [sd, sl, dc, fl, ll] = [0, 0, 0, 0, 0]
  var ds = {'t': [1, 1, 1, dfl[ak][0] - 1],
            'b': [0, t:DChar.lcc[bk].ll, dfl[ak][-1] + 1, t:DChar.lcc[ak].ll]}
  var ix = -1
  if 0 < key && !empty(t:DChar.dfl[ak])
    ix = index(t:DChar.dfl[ak], dfl[ak][0])
    if ix != -1
      [sd, sl, dc] = [1, t:DChar.dfl[bk][ix], 0]
    else
      ix = index(t:DChar.dfl[ak], dfl[ak][-1])
      if ix != -1
        [sd, sl, dc] = [0, t:DChar.dfl[bk][ix], 0]
      else
        if t:DChar.dfl[ak][-1] < dfl[ak][0]
          ds.t = [1, t:DChar.dfl[bk][-1], t:DChar.dfl[ak][-1], dfl[ak][0] - 1]
        elseif dfl[ak][-1] < t:DChar.dfl[ak][0]
          ds.b = [0, t:DChar.dfl[bk][0], dfl[ak][-1] + 1, t:DChar.dfl[ak][0]]
        endif
      endif
    endif
  endif
  if ix == -1
    [sd, sl, fl, ll] = (ds.t[3] - ds.t[2] <= ds.b[3] - ds.b[2]) ? ds.t : ds.b
    dc = len(s:WinExec(t:DChar.wid[ak], 's:GetDiffLines', [fl, ll]))
  endif
  const ac = len(dfl[ak])
  dfl[bk] = s:WinExec(t:DChar.wid[bk], 's:SearchDiffLines', [sd, sl, dc + ac])
  const bc = len(dfl[bk])
  if ac != bc
    var [xk, xc] = (ac < bc) ? [bk, ac] : [ak, bc]
    dfl[xk] = (xc == 0) ? [] : sd ? dfl[xk][-xc :] : dfl[xk][: xc - 1]
  endif
  # repair current dfl and merge with new dfl
  if 0 < key && !empty(t:DChar.dfl[ak]) && t:DChar.dfl != dfl
    # repair and redraw current on-screen dfl realigned by linematch
    if t:DChar.opt.lm
      var dxl = deepcopy(t:DChar.dfl)
      var dfi = {}
      for k in [1, 2]
        dfi[k] = map(copy(dfl[k]), (_, v) => index(dxl[k], v))
      endfor
      var [dl, al] = [[], []]
      if dfi[1] != dfi[2]
        for fi in range(len(dfi[ak]))
          const [i1, i2] = [dfi[1][fi], dfi[2][fi]]
          const [ki, xi] = (i1 == -1 && i2 != -1) ? [1, i2] :
                                  (i1 != -1 && i2 == -1) ? [2, i1] : [0, -1]
          if ki != 0
            [dl, al] += [[dxl[ak][xi]], [dfl[ak][fi]]]
            dxl[ki][xi] = dfl[ki][fi]
          endif
        endfor
      endif
      if !empty(dl)
        s:WinExec(t:DChar.wid[ak], 's:ResetDiffChar', [dl])
      endif
      t:DChar.dfl = dxl
      if !empty(al)
        s:WinExec(t:DChar.wid[ak], 's:ShowDiffChar', [al])
      endif
    endif
    # merge current and new dfls if overlapped or continued
    if 0 < t:DChar.opt.fp
      var nl: list<number>
      var mx = 0
      if t:DChar.dfl[ak][0] <= dfl[ak][0]
        if t:DChar.dfl[ak][-1] >= dfl[ak][0] | mx = 1
        else
          nl = s:WinExec(t:DChar.wid[ak], 's:SearchDiffLines',
                                              [1, t:DChar.dfl[ak][-1] + 1, 1])
          if !empty(nl) && dfl[ak][0] <= nl[0] | mx = 2 | endif
        endif
      elseif dfl[ak][-1] <= t:DChar.dfl[ak][-1]
        if dfl[ak][-1] >= t:DChar.dfl[ak][0] | mx = -1
        else
          nl = s:WinExec(t:DChar.wid[ak], 's:SearchDiffLines',
                                              [0, t:DChar.dfl[ak][0] - 1, 1])
          if !empty(nl) && nl[0] <= dfl[ak][-1] | mx = -2 | endif
        endif
      endif
      if mx != 0
        for k in [1, 2]
          dfl[k] = (0 < mx) ?
            t:DChar.dfl[k] + filter(dfl[k], (_, v) => v > t:DChar.dfl[k][-1]) :
            filter(dfl[k], (_, v) => v < t:DChar.dfl[k][0]) + t:DChar.dfl[k]
        endfor
      endif
    endif
  endif
  return dfl
enddef

def! s:SearchDiffLines(sd: number, sl: number, sc: number): list<number>
  # a:sd = direction (1:down, 0:up), a:sl = start line, a:sc = count
  var dl = [] | var ln = sl | var sn = sc
  if sd
    while 0 < sn && ln <= line('$')
      var zl = []
      while len(zl) < sn
        var fc = foldclosedend(ln)
        if fc == -1 | zl += [ln] | else | ln = fc | endif
        ln += 1
      endwhile
      zl = s:CheckDiffLines(zl) | sn -= len(zl) | dl += zl
    endwhile
  else
    while 0 < sn && 1 <= ln
      var zl = []
      while len(zl) < sn
        var fc = foldclosed(ln)
        if fc == -1 | zl = [ln] + zl | else | ln = fc | endif
        ln -= 1
      endwhile
      zl = s:CheckDiffLines(zl) | sn -= len(zl) | dl = zl + dl
    endwhile
  endif
  return dl
enddef

def! s:GetDiffLines(sl: number, el: number): list<number>
  var dl = [] | var ln = sl
  while ln <= el
    var fc = foldclosedend(ln)
    if fc == -1 | dl += [ln] | else | ln = fc | endif
    ln += 1
  endwhile
  return s:CheckDiffLines(dl)
enddef

def! s:CheckDiffLines(ll: list<number>): list<number>
  # check [0] first, diff_hlID() sometimes fails for the 1st entry of the list
  return filter([0] + ll,
      (_, v) => index([s:DiffHL.C.id, s:DiffHL.T.id], diff_hlID(v, 1)) != -1)
enddef

def! s:ChecksumStr(str: string): string
  return sha256(str)[: 5]
enddef

def! s:WinExec(wn: number, fn: string, ag: list<any>): any
  win_execute(wn,
          $'vim9cmd t:DChar.tmp = call({string(function(fn))}, {string(ag)})')
  return t:DChar.tmp
enddef
endfunction
call s:DiffCharVim9Functions()
endif

let &cpoptions = s:save_cpo
unlet s:save_cpo

" vim: ts=2 sw=0 sts=-1 et


================================================
FILE: doc/diffchar.txt
================================================
*diffchar.txt*  Highlight the exact differences, based on characters and words
>
  ____   _  ____  ____  _____  _   _  _____  ____   
 |    | | ||    ||    ||     || | | ||  _  ||  _ |  
 |  _  || ||  __||  __||     || | | || | | || | ||  
 | | | || || |__ | |__ |   __|| |_| || |_| || |_||_ 
 | |_| || ||  __||  __||  |   |     ||     ||  __  |
 |     || || |   | |   |  |__ |  _  ||  _  || |  | |
 |____| |_||_|   |_|   |_____||_| |_||_| |_||_|  |_|
<
Last Change: 2025/10/01
Version:     10.0 (on or after vim 9.0 and nvim 0.7.0)
Author:      Rick Howe (Takumi Ohtani) <rdcxy754@ybb.ne.jp>
Copyright:   (c) 2014-2025 Rick Howe
License:     MIT

-----------------------------------------------------------------------------
INTRODUCTION                                           *diffchar*

This plugin has been developed in order to make diff mode more useful. Vim
highlights all the text in between the first and last different characters on
a changed line. But this plugin will find the exact differences between them,
character by character - so called DiffChar.

For example, in diff mode: (`^`:`DiffText`, |#|:|DiffAdd|, |@|:|DiffDelete|)
    +-------------------------------------------------+
    |The `quick brown fox jumps over the lazy` dog.     |
    |    `^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^`          |
    |~                                                |
    +-------------------------------------------------+
    |The `lazy fox jumps over the quick brown` dog.     |
    |    `^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^`          |
    |~                                                |
    +-------------------------------------------------+

This plugin will exactly show the changed/added/deleted units:
    +-------------------------------------------------+
    |The `quick` |brown| fox jumps over the `lazy`  dog.    |
    |    `^^^^^` |######|                   `^^^^` |@|        |
    |~                                                |
    +-------------------------------------------------+
    |The `lazy`  fox jumps over the `quick` |brown| dog.    |
    |    `^^^^` |@|                   `^^^^^` |######|        |
    |~                                                |
    +-------------------------------------------------+

Sync with diff mode ~
This plugin will synchronously show/reset the highlights of the exact
differences as soon as the diff mode begins/ends. And the exact differences
will be kept updated while editing. Note that this plugin does nothing if an
"inline" item is set in the 'doffopt' option and its value is other than
"simple". On plugin loading, the item is removed only if set by default so
that this plugin can work.

Diff unit ~
This plugin shows the diffs based on a |g:DiffUnit|. Its default is 'Word1'
and it handles a \w\+ word and a \W character as a diff unit. There are other
types of word provided and you can also set 'Char' to compare character by
character. In addition, you can specify one or more diff unit delimiters, such
as comma (','), colon (':'), tab ("\t"), and HTML tag symbols ('<' and '>'),
and also specify a custom pattern in the |g:DiffUnit|.

Diff matching colors ~
In diff mode, the corresponding |hl-DiffChange| lines are compared between two
windows. As a default, all the changed units are highlighted with
|hl-DiffText|. You can set |g:DiffColors| to use more than one matching color
to make it easy to find the corresponding units between two windows. The
number of colors depends on the color scheme. In addition, an added unit is
always highlighted with |hl-DiffAdd| and the position of the corresponding
deleted unit is shown with bold/underline or a virtual blank column,
depending on a |g:DiffDelPosVisible|.

Diff pair visible ~
While showing the exact differences, when the cursor is moved on a diff unit,
you can see its corresponding unit highlighted with |hl-Cursor|,
|hl-TermCursor|, or similar one in another window, based on a
|g:DiffPairVisible|. If you change its default, the corresponding unit is
echoed in the command line or displayed in a popup/floating window just below
the cursor position or at the mouse position. Those options take effect on
`:diffupdate` command as well.

Jump to next/prev diff unit ~
You can use `]b` or `]e` to jump cursor to start or end position of the next
diff unit, and `[b` or `[e` to the start or end position of the previous unit.

Get/put a diff unit ~
Like line-based `:diffget`/`:diffput` and `do`/`dp` vim commands, you can use
`<Leader>g` and `<Leader>p` commands in normal mode to get and put each diff
unit, where the cursor is on, between 2 buffers and undo its difference. Those
keymaps are configurable in your vimrc and so on.

Check diff lines locally ~
When the diff mode begins, this plugin locally checks the |hl-DiffChange|
lines in the limited range of the current visible and its upper/lower lines of
a window. And each time a cursor is moved on to another range upon scrolling
or searching, those diff lines will be checked in that range. Which means,
independently of the file size, the number of lines to be checked and then the
time consumed are always constant.

Tab page individual ~
This plugin works on each tab page individually. You can use a tab page
variable (t:), instead of a global one (g:), to specify different options on
each tab page. Note that this plugin can not handle more than two diff mode
windows in a tab page. If it would happen, to prevent any trouble, all the
highlighted units are to be reset in the tab page.

Follow 'diffopt' option ~
This plugin supports "icase", "iwhite", "iwhiteall", and "iwhiteeol" in the
'diffopt' option. In addition, when "indent-heuristic" is specified,
positioning of the added/deleted diff units is adjusted to reduce the number
of diff hunks and make them easier to read.

Comparison algorithm ~
To find the exact differences, this plugin uses "An O(NP) Sequence Comparison
Algorithm" developed by S.Wu, et al., which always finds an optimum sequence.
But it takes time to check a long and dissimilar line. To improve the
performance, the algorithm is also implemented in Vim9 script. In addition,
if available, this plugin uses a builtin diff function (|diff()| in vim
patch-9.1.0071 and Lua |vim.diff()| in nvim 0.6.0) and makes it much faster.

See also ~
There are other diff related plugins available:
- |spotdiff.vim|: A range and area selectable `:diffthis` to compare partially
  (https://github.com/rickhowe/spotdiff.vim)
- |wrapfiller|: Align each wrapped line virtually between windows
  (https://github.com/rickhowe/wrapfiller)
- |difffilter|: Selectively compare lines as you want in diff mode
  (https://github.com/rickhowe/difffilter)
- |diffunitsyntax|: Highlight word or character based diff units in diff format
  (https://github.com/rickhowe/diffunitsyntax)

-----------------------------------------------------------------------------
OPTIONS                                                *diffchar-options*

|g:DiffUnit|, |t:DiffUnit|
    A type of diff unit
    'Char'    : any single character
    'Word1'   : \w\+ word and any \W single character (default)
    'Word2'   : non-space and space words
    'Word3'   : \< or \> character class boundaries (set by 'iskeyword')
    'word'    : see `word`
    'WORD'    : see `WORD`
    '[{del}]' : one or more diff unit delimiters (e.g. "[,:\t<>]")
    '/{pat}/' : a pattern to split into diff units (e.g. '/.\{4}\zs/')

|g:DiffColors|, |t:DiffColors|
    Matching colors for changed units
    0       : |hl-DiffText| (default)
    1       : |hl-DiffText| + a few (3, 4, ...) highlight groups
    2       : |hl-DiffText| + several (7, 8, ...) highlight groups
    3       : |hl-DiffText| + many (11, 12, ...) highlight groups
    100     : all available highlight groups in random order
    [{hlg}] : a list of your favorite highlight groups

|g:DiffPairVisible|, |t:DiffPairVisible|
    Visibility of corresponding diff units
    0 : disable
    1 : highlight with |hl-Cursor| (default)
    2 : highlight with |hl-Cursor| + echo in the command line
    3 : highlight with |hl-Cursor| + popup/floating window at cursor position
    4 : highlight with |hl-Cursor| + popup/floating window at mouse position

|g:DiffDelPosVisible|, |t:DiffDelPosVisible|
    Visibility of the position of deleted units
    0 : disable
    1 : highlight previous/next chars of a deleted unit in bold/underline
        (default if inline |virtual-text| is not available)
    2 : virtually show a blank column (set by "space" item in 'listchars')
        with |hl-DiffDelete| (default if inline |virtual-text| is available)

-----------------------------------------------------------------------------
KEYMAPS                                                *diffchar-keymaps*

<Plug>JumpDiffCharPrevStart (default: `[b`)
    Jump cursor to the start position of the previous diff unit

<Plug>JumpDiffCharNextStart (default: `]b`)
    Jump cursor to the start position of the next diff unit

<Plug>JumpDiffCharPrevEnd (default: `[e`)
    Jump cursor to the end position of the previous diff unit

<Plug>JumpDiffCharNextEnd (default: `]e`)
    Jump cursor to the end position of the next diff unit

<Plug>GetDiffCharPair (default: `<Leader>g`)
    Get a corresponding diff unit from another buffer to undo difference

<Plug>PutDiffCharPair (default: `<Leader>p`)
    Put a corresponding diff unit to another buffer to undo difference

-----------------------------------------------------------------------------
CHANGE HISTORY                                         *diffchar-history*

10.0
* Added |g:DiffDelPosVisible| option to show the position of deleted units.
* Implemented to remove an "inline" item from the 'diffopt' option on initial
  loading so that this plugin can work.
* Made this plugin available on or after vim 9.0 and nvim 0.7.0.

9.9
* Improved to remain previous diff lines highlighted upon scrolling if
  possible and not to check them later again.
* Implemented a few more functions in Vim9 script to make them faster.

9.8
* Improved to follow "linematch", which is run to realign diff lines when
  displayed on screen, if specified in the 'diffopt' option (default on nvim
  0.11). Accordingly, fixed errors such as E684 and E716 on nvim.
* Changed to disable this plugin to avoid a conflict with character/word-wise
  "inline" diff (patch-9.1.1243), if specified in the 'diffopt' option.
* Changed to overwrite nvim default mappings to set plugin specific ones on
  plugin loading. As of nvim 0.11, `[b` and `]b` keys are duplicated.
* Fixed to handle tab and space as a same whitespace character if "iwhite" or
  "iwhiteall" are specified in the 'diffopt' options.

9.7
* Implemented to use a new builtin |diff()| function (available on
  patch-9.1.0071) to compare diff units and make it faster in vim.

9.6, 9.61
* Changed to locally but not incrementally check the limited number of the
  |hl-DiffChange| lines upon scrolling or searching.
* Fixed not to use |hl-DiffText| as the first highlight group if [{hlg}] is
  specified in the |g:DiffColors| option.

9.5
* Improved not to update diff unit highlighting on unchanged diff lines on the
  DiffUpdated event.
* Fixed the E421 error when 'termguicolors' is off on nvim.

9.4
* Implemented to use a builtin Lua |vim.diff()| function to compare diff units
  and make it faster in nvim.

9.3
* Improved to follow the 'wincolor' option and show the colors accordingly.
* Fixed to work on some color scheme in which the "g:colors_name" variable is
  different from its file name.

9.2
* Fixed the plugin error when 3 or more windows turn to be diff mode.
* Fixed the issue which the new diff lines can not be incrementally found upon
  scrolling on patch-9.0.0913 or later.
* Made it faster to find diff lines in a file with many lines folded.

9.1
* Added vim original 'word' and 'WORD' units, one or more unit delimiters, and
  a custom pattern to split into diff units in |g:DiffUnit| option.
* Improved to redraw diff units whenever the ColorScheme event happens when
  multiple matching colors are being used (0 < |g:DiffColors|).
* Changed the category of |g:DiffColors| option as a few, several, and many
  numbers of matching colors, depending on the loaded color scheme.
* Added a list of your favorite highlight groups in |g:DiffColors| option.

9.01
* Fixed to work on a |hl-Diff| highlighting group even if it is linked.

9.0
* Enhanced to make diff units easier to read when "indent-heuristic" is
  specified in the 'diffopt' option.
* Improved to update diff units using `:diffupdate` command when |g:DiffUnit|,
  |g:DiffColors|, and |g:DiffPairVisible| options are modified.
* Updated to check a new WinScrolled event (patch-8.2.4713) to incrementally
  find scrolled diff lines.
* Implemented the comparison algorithm and diff unit highlighting in Vim9
  script and made them 10 times faster on patch-8.2.3965 or later.
* Made this plugin available on or after patch-8.1.1418 and nvim-0.5.0.

 vim:tw=78:ts=8:ft=help:norl:


================================================
FILE: plugin/diffchar.vim
================================================
" diffchar.vim: Highlight the exact differences, based on characters and words
"
"  ____   _  ____  ____  _____  _   _  _____  ____   
" |    | | ||    ||    ||     || | | ||  _  ||  _ |  
" |  _  || ||  __||  __||     || | | || | | || | ||  
" | | | || || |__ | |__ |   __|| |_| || |_| || |_||_ 
" | |_| || ||  __||  __||  |   |     ||     ||  __  |
" |     || || |   | |   |  |__ |  _  ||  _  || |  | |
" |____| |_||_|   |_|   |_____||_| |_||_| |_||_|  |_|
"
" Last Change: 2025/10/01
" Version:     10.0 (on or after vim 9.0 and nvim 0.7.0)
" Author:      Rick Howe (Takumi Ohtani) <rdcxy754@ybb.ne.jp>
" Copyright:   (c) 2014-2025 Rick Howe
" License:     MIT

if exists('g:loaded_diffchar') || !has('diff') ||
                                      \(v:version < 900 && !has('nvim-0.7.0'))
let g:loaded_diffchar = 0
  finish
endif
let g:loaded_diffchar = 10.0

let s:save_cpo = &cpoptions
set cpo&vim

" Keymaps
for [key, plg, cmd] in [
  \['[b', '<Plug>JumpDiffCharPrevStart',
                                      \':<C-U>call diffchar#JumpDiffChar(0)'],
  \[']b', '<Plug>JumpDiffCharNextStart',
                                      \':<C-U>call diffchar#JumpDiffChar(1)'],
  \['[e', '<Plug>JumpDiffCharPrevEnd',
                                      \':<C-U>call diffchar#JumpDiffChar(2)'],
  \[']e', '<Plug>JumpDiffCharNextEnd',
                                      \':<C-U>call diffchar#JumpDiffChar(3)'],
  \['<Leader>g', '<Plug>GetDiffCharPair',
                                  \':<C-U>call diffchar#CopyDiffCharPair(0)'],
  \['<Leader>p', '<Plug>PutDiffCharPair',
                                  \':<C-U>call diffchar#CopyDiffCharPair(1)']]
  if !hasmapto(plg, 'n') && maparg(key, 'n') =~ '^$\|_defaults.lua'
    if get(g:, 'DiffCharDoMapping', 1)
      call execute('nmap <silent> ' . key . ' ' . plg)
    endif
  endif
  call execute('nnoremap <silent> ' . plg . ' ' . cmd . '<CR>')
endfor

" Event groups
let g:DiffCharInitEvent = ['augroup diffchar', 'autocmd!',
                \'autocmd OptionSet diff call diffchar#ToggleDiffModeSync()',
                                                              \'augroup END']
call execute(g:DiffCharInitEvent)
call execute('autocmd diffchar VimEnter * ++once
                    \ if &diff | call diffchar#ToggleDiffModeSync(1) | endif')

" remove 'inline' from &diffopt if set only by default not to disable plugin
let inl = 'inline'
if &diffopt =~ inl
  let dip = split(&diffopt, ',')
  set diffopt&
  let def = split(&diffopt, ',')
  if match(filter(copy(dip), 'index(def, v:val) == -1'), inl) == -1
    call filter(dip, 'v:val !~ inl')
  endif
  let &diffopt = join(dip, ',')
endif

let &cpoptions = s:save_cpo
unlet s:save_cpo

" vim: ts=2 sw=0 sts=-1 et
Download .txt
gitextract_c6cdx0o3/

├── LICENSE
├── README.md
├── autoload/
│   └── diffchar.vim
├── doc/
│   └── diffchar.txt
└── plugin/
    └── diffchar.vim
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (95K chars).
[
  {
    "path": "LICENSE",
    "chars": 1097,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014-2025 Rick Howe (Takumi Ohtani)\n\nPermission is hereby granted, free of charge, "
  },
  {
    "path": "README.md",
    "chars": 7963,
    "preview": "# diffchar.vim\n*Highlight the exact differences, based on characters and words*\n```\n ____   _  ____  ____  _____  _   _ "
  },
  {
    "path": "autoload/diffchar.vim",
    "chars": 66795,
    "preview": "\" diffchar.vim: Highlight the exact differences, based on characters and words\n\"\n\"  ____   _  ____  ____  _____  _   _  "
  },
  {
    "path": "doc/diffchar.txt",
    "chars": 12993,
    "preview": "*diffchar.txt*  Highlight the exact differences, based on characters and words\n>\n  ____   _  ____  ____  _____  _   _  _"
  },
  {
    "path": "plugin/diffchar.vim",
    "chars": 2725,
    "preview": "\" diffchar.vim: Highlight the exact differences, based on characters and words\n\"\n\"  ____   _  ____  ____  _____  _   _  "
  }
]

About this extraction

This page contains the full source code of the rickhowe/diffchar.vim GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (89.4 KB), approximately 30.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!