From c9326e6df84ce710c540d241a7bc3e39c19dc912 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 22 Jul 2026 17:32:24 +0800 Subject: [PATCH] vim-patch:9.2.0825: regexp: submatch in a look-behind is empty with the NFA engine (#40899) Problem: With the NFA engine a sub-expression inside a variable width look-behind, e.g. "\v(.)@<=", is empty for the first match on every line except the first one. The old engine is correct (Mukundan) Solution: The look-behind is retried from the previous line, because the width of "." is over-estimated. While scanning that line the start state is added at the end of the line, where it gets the position of the line break as its start position, even though the match actually starts on the next line. Use the position of the start of the next line in that case (Hirohito Higashi). fixes: vim/vim#20802 closes: vim/vim#20805 https://github.com/vim/vim/commit/ade577e4dc2a9f34d2d592cbe1c80b9fc89753bf Co-authored-by: Hirohito Higashi Co-authored-by: Claude Opus 4.8 (1M context) --- src/nvim/regexp.c | 25 ++++++++++++++++++++++++- test/old/testdir/test_regexp_utf8.vim | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index 8529bcb49a..4abde83bba 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -15525,7 +15525,30 @@ static int nfa_regmatch(nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *subm } } } else { - if (addstate(nextlist, start, m, NULL, clen) == NULL) { + uint8_t *save_line = rex.line; + uint8_t *save_input = rex.input; + linenr_T save_lnum = rex.lnum; + + // At the end of a line the match can only start on the next + // line, use that position instead of the line break. + if (REG_MULTI && clen == 0 && nfa_endp != NULL + && rex.lnum < nfa_endp->se_u.pos.lnum) { + char *next_line = reg_getline(rex.lnum + 1); + + if (next_line != NULL) { + rex.line = (uint8_t *)next_line; + rex.input = rex.line; + rex.lnum++; + } + } + + r = addstate(nextlist, start, m, NULL, clen); + + rex.line = save_line; + rex.input = save_input; + rex.lnum = save_lnum; + + if (r == NULL) { nfa_match = NFA_TOO_EXPENSIVE; goto theend; } diff --git a/test/old/testdir/test_regexp_utf8.vim b/test/old/testdir/test_regexp_utf8.vim index d6284a559f..f5ec0b77d2 100644 --- a/test/old/testdir/test_regexp_utf8.vim +++ b/test/old/testdir/test_regexp_utf8.vim @@ -656,4 +656,29 @@ func Test_regex_collection_range_with_composing_crash() bwipe! endfunc +" A submatch in a look-behind must not start on the previous line. See +" issue #20802. +func Test_lookbehind_submatch_on_second_line() + new + for re in range(0, 2) + exe "set re=" .. re + call setline(1, ['testing', 'testing']) + %s/\v(.)@<=/[\1]/g + call assert_equal(['t[t]e[e]s[s]t[t]i[i]n[n]g', + \ 't[t]e[e]s[s]t[t]i[i]n[n]g'], getline(1, '$'), 're=' .. re) + %d _ + + " With "\_." the look-behind can match the line break, then the submatch + " does start on the previous line. + call setline(1, ['abc', 'def']) + %s/\v(\_.)@<=/[\1]/g + call assert_equal(['a[a]b[b]c[c]', '[]d[d]e[e]f[f]'], + \ getline(1, '$'), 're=' .. re) + %d _ + endfor + + set re& + bwipe! +endfunc + " vim: shiftwidth=2 sts=2 expandtab