vimlrs 0.2.1

Faithful Rust port of the Vimscript (VimL) interpreter, from the Neovim C eval engine
Documentation
" substitute_edge.vim — Vim's quirky edges of substitute()/vim_regsub, verified
" against nvim 0.12.3 and vim 9.2. Two behaviours that are easy to get wrong:
"
"   1. Empty-match handling in a global substitute. Vim's do_string_sub skips an
"      empty match that lands on the same position as the previous empty match
"      (copy one char, advance) instead of emitting a duplicate replacement — so
"      `substitute('aaa','a*','X','g')` is `X`, not `XX`.
"   2. Replacement special chars in vim_regsub: `\n` inserts a NUL (0x00), `\r` a
"      carriage return (0x0d), `\t` a tab, `\\` a backslash. Note `\n` is a NUL
"      here, the OPPOSITE of the pattern side where `\n` means newline.
"
" Self-test into v:errors.

" --- empty match after a non-empty match is suppressed (a* eats 'aaa', then the
"     trailing empty match at end-of-string is skipped)
call assert_equal('X', substitute('aaa', 'a*', 'X', 'g'))
call assert_equal('X', substitute('a', 'a*', 'X', 'g'))
call assert_equal('X', substitute('', 'a*', 'X', 'g'))

" --- an empty match that is NOT at the previous empty-match position is kept, so
"     runs of 'a' collapse to one X but the gaps still get their own empty-match X
call assert_equal('XXbX', substitute('aab', 'a*', 'X', 'g'))
call assert_equal('XXbX', substitute('aabaa', 'a*', 'X', 'g'))
call assert_equal('XxXXxX', substitute('xaax', 'a*', 'X', 'g'))
call assert_equal('XbX', substitute('b', 'a*', 'X', 'g'))
call assert_equal('-a--b--c-', substitute('aXbXc', 'X*', '-', 'g'))

" --- all-empty pattern still fires between every char (no regression)
call assert_equal('XaXaXaX', substitute('aaa', '', 'X', 'g'))
call assert_equal('-a-b-c-', substitute('abc', 'x*', '-', 'g'))
call assert_equal('X', substitute('', '', 'X', 'g'))

" --- plain matches, non-global, and empty replacement (no regression)
call assert_equal('bXnXnX', substitute('banana', 'a', 'X', 'g'))
call assert_equal('abc', substitute('aXbXc', 'X', '', 'g'))
call assert_equal('f0obar', substitute('foobar', 'o', '0', ''))
call assert_equal('f00bar', substitute('foobar', 'o', '0', 'g'))
call assert_equal('Xabc', substitute('abc', '', 'X', ''))

" --- backreferences in the replacement still work
call assert_equal('2420', substitute('2024', '\(..\)\(..\)', '\2\1', ''))
call assert_equal('06/2024', substitute('2024-06', '\(\d\+\)-\(\d\+\)', '\2/\1', ''))

" --- replacement special chars (strtrans renders NUL as ^@, CR as ^M, tab as ^I)
call assert_equal('a^@b', strtrans(substitute('x', 'x', 'a\nb', '')))
call assert_equal('a^Mb', strtrans(substitute('x', 'x', 'a\rb', '')))
call assert_equal('a^Ib', strtrans(substitute('x', 'x', 'a\tb', '')))
call assert_equal('a\b', substitute('x', 'x', 'a\\b', ''))

" --- `\n` inserts a real NUL byte, so the length is 3 (a, NUL, b)
call assert_equal(3, len(substitute('x', 'x', 'a\nb', '')))

" --- `\r` inserts a carriage return (0x0d), not a newline
call assert_equal(13, char2nr(substitute('x', 'x', 'a\rb', '')[1]))

if len(v:errors) > 0
  for err in v:errors
    echo err
  endfor
  throw 'substitute_edge.vim: ' . len(v:errors) . ' assertion(s) failed'
endif
echo 'substitute_edge.vim: all assertions passed'