From 41163b773136638e4f8212752fb87e3f9b1fee1f Mon Sep 17 00:00:00 2001 From: XiaowenHu Date: Wed, 22 Jul 2026 20:27:54 +0800 Subject: [PATCH] fix(inccommand): enforce timeout during matching, not just after a line #40892 Problem: do_sub() only checks the timeout limit after finishing a line. A pathological regex will run on a single line input unbounded until the compute is completed. Solution: Pass the timeout limit to `vim_regexec_multi()` so the computation on the regex engine is bounded per-line. Signed-off-by: XiaowenHu96 (cherry picked from commit bd73d8c01146695b727849e67fc1f1f209d5884f) --- src/nvim/ex_cmds.c | 17 ++++++++++------- test/functional/ui/inccommand_spec.lua | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 5bb39e828f..3694323239 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -3525,10 +3525,11 @@ static int check_regexp_delim(int c) /// /// The usual escapes are supported as described in the regexp docs. /// +/// @param tm Timeout limit /// @param cmdpreview_ns The namespace to show 'inccommand' preview highlights. /// If <= 0, preview shouldn't be shown. /// @return 0, 1 or 2. See cmdpreview_may_show() for more information on the meaning. -static int do_sub(exarg_T *eap, const proftime_T timeout, const int cmdpreview_ns, +static int do_sub(exarg_T *eap, proftime_T tm, const int cmdpreview_ns, const handle_T cmdpreview_bufnr) { #define ADJUST_SUB_FIRSTLNUM() \ @@ -3759,13 +3760,15 @@ static int do_sub(exarg_T *eap, const proftime_T timeout, const int cmdpreview_n // If preview: limit to max('cmdwinheight', viewport). linenr_T line2 = eap->line2; + int timed_out = false; + for (linenr_T lnum = eap->line1; - lnum <= line2 && !got_quit && !aborting() + lnum <= line2 && !got_quit && !timed_out && !aborting() && (cmdpreview_ns <= 0 || preview_lines.lines_needed <= (linenr_T)p_cwh || lnum <= curwin->w_botline); lnum++) { int nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum, - 0, NULL, NULL); + 0, &tm, &timed_out); if (nmatch) { colnr_T copycol; colnr_T matchcol; @@ -4342,7 +4345,7 @@ skip: || nmatch_tl > 0 || (nmatch = vim_regexec_multi(®match, curwin, curbuf, sub_firstlnum, - matchcol, NULL, NULL)) == 0 + matchcol, &tm, &timed_out)) == 0 || regmatch.startpos[0].lnum > 0) { if (new_start != NULL) { // Copy the rest of the line, that didn't match. @@ -4418,7 +4421,7 @@ skip: } if (nmatch == -1 && !lastone) { nmatch = vim_regexec_multi(®match, curwin, curbuf, - sub_firstlnum, matchcol, NULL, NULL); + sub_firstlnum, matchcol, &tm, &timed_out); } // 5. break if there isn't another match in this line @@ -4475,7 +4478,7 @@ skip: line_breakcheck(); - if (profile_passed_limit(timeout)) { + if (timed_out || profile_passed_limit(tm)) { got_quit = true; } } @@ -4558,7 +4561,7 @@ skip: // Show 'inccommand' preview if there are matched lines. if (cmdpreview_ns > 0 && !aborting()) { - if (got_quit || profile_passed_limit(timeout)) { // Too slow, disable. + if (got_quit || profile_passed_limit(tm)) { // Too slow, disable. set_option_direct(kOptInccommand, STATIC_CSTR_AS_OPTVAL(""), 0, SID_NONE); } else if (*p_icm != NUL && pat != NULL) { if (pre_hl_id == 0) { diff --git a/test/functional/ui/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua index f1553cec7e..62a2e47705 100644 --- a/test/functional/ui/inccommand_spec.lua +++ b/test/functional/ui/inccommand_spec.lua @@ -1140,6 +1140,24 @@ describe(':substitute, inccommand=split', function() eq('split', eval('&inccommand')) end) + it('time limit is enforced while matching a single line #40773', function() + -- prevent redraws from 'incsearch' + api.nvim_set_option_value('incsearch', false, {}) + -- Assert that 'inccommand' is ENABLED initially. + eq('split', eval('&inccommand')) + -- Set 'redrawtime' to minimal value, to ensure timeout is triggered. + command('set redrawtime=1 nowrap') + -- Prepare the text + api.nvim_buf_set_lines(0, 0, -1, true, { ('aaaaaaaa/'):rep(6) .. ('b'):rep(200) }) + feed([[:%s/.\+\/\(.\+\)\+ft\/\1]]) + screen:expect({ any = vim.pesc([[:%s/.\+\/\(.\+\)\+ft\/\1]]) }) + -- Assert that 'inccommand' is DISABLED in cmdline mode. + eq('', eval('&inccommand')) + -- Assert that 'inccommand' is again ENABLED after leaving cmdline mode. + feed([[]]) + eq('split', eval('&inccommand')) + end) + it("deactivates if 'foldexpr' is slow #9557", function() insert([[ a