patch 9.2.0374: c_CTRL-{G,T} does not handle offset
Commit:
https://github.com/vim/vim/commit/c62342e5cfc339a87c1eb40ef34b2b31070d72a6
Author: Barrett Ruth <
br.barr...@gmail.com>
Date: Mon Apr 20 16:05:43 2026 +0000
patch 9.2.0374: c_CTRL-{G,T} does not handle offset
Problem: c_CTRL-{G,T} does not handle offset, when cycling between
matches
Solution: Refactor parsing logic into parse_search_pattern_offset() and
handle offsets, note: highlighting does not handle offsets
yet (Barrett Ruth).
fixes: #19991
closes: #19998
Signed-off-by: Barrett Ruth <
br.barr...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt
index 4a0729d0e..bf47c5199 100644
--- a/runtime/doc/cmdline.txt
+++ b/runtime/doc/cmdline.txt
@@ -1,4 +1,4 @@
-*cmdline.txt* For Vim version 9.2. Last change: 2026 Mar 17
+*cmdline.txt* For Vim version 9.2. Last change: 2026 Apr 20
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -466,14 +466,15 @@ CTRL-L A match is done on the pattern in front of the cursor. If
*c_CTRL-G* */_CTRL-G*
CTRL-G When 'incsearch' is set, entering a search pattern for "/" or
"?" and the current match is displayed then CTRL-G will move
- to the next match (does not take |search-offset| into account)
+ to the next match. The |search-offset| is applied when <CR>
+ is pressed, but does not affect the match highlighting.
Use CTRL-T to move to the previous match. Hint: on a regular
keyboard G is below T.
*c_CTRL-T* */_CTRL-T*
CTRL-T When 'incsearch' is set, entering a search pattern for "/" or
"?" and the current match is displayed then CTRL-T will move
- to the previous match (does not take |search-offset| into
- account).
+ to the previous match. The |search-offset| is applied when
+ <CR> is pressed, but does not affect the match highlighting.
Use CTRL-G to move to the next match. Hint: on a regular
keyboard T is above G.
diff --git a/src/ex_getln.c b/src/ex_getln.c
index f89bcecc5..0f69d0db2 100644
--- a/src/ex_getln.c
+++ b/src/ex_getln.c
@@ -614,14 +614,18 @@ may_adjust_incsearch_highlighting(
incsearch_state_T *is_state,
int c)
{
- int skiplen, patlen;
- pos_T t;
- char_u *pat;
- int search_flags = SEARCH_NOOF;
- int i;
- int save;
- int bslsh = FALSE;
- int search_delim;
+ int skiplen, patlen;
+ pos_T t;
+ char_u *pat;
+ char_u *dircp = NULL;
+ char_u *searchstr;
+ char_u *strcopy = NULL;
+ size_t searchstrlen;
+ size_t patlen_s;
+ soffset_T offset;
+ int search_flags = SEARCH_NOOF;
+ int i;
+ int search_delim;
// Parsing range may already set the last search pattern.
// NOTE: must call restore_last_search_pattern() before returning!
@@ -639,31 +643,16 @@ may_adjust_incsearch_highlighting(
return FAIL;
}
- if (search_delim == ccline.cmdbuff[skiplen])
- {
- pat = last_search_pattern();
- if (pat == NULL)
- {
- restore_last_search_pattern();
- return FAIL;
- }
- skiplen = 0;
- patlen = (int)last_search_pattern_len();
- }
- else
- pat = ccline.cmdbuff + skiplen;
+ pat = ccline.cmdbuff + skiplen;
+ searchstr = pat;
+ searchstrlen = (size_t)patlen;
+ patlen_s = (size_t)(ccline.cmdlen - skiplen);
// do not search for the search end delimiter,
// unless it is part of the pattern
- if (patlen > 2 && firstc == pat[patlen - 1])
- {
- patlen--;
- if (pat[patlen - 1] == '\')
- {
- pat[patlen - 1] = firstc;
- bslsh = TRUE;
- }
- }
+ (void)parse_search_pattern_offset(&pat, &patlen_s, search_delim,
+ SEARCH_OPT, &strcopy, &searchstr,
+ &searchstrlen, &dircp, &offset);
cursor_off();
out_flush();
@@ -681,18 +670,39 @@ may_adjust_incsearch_highlighting(
if (!p_hls)
search_flags += SEARCH_KEEP;
++emsg_off;
- save = pat[patlen];
- pat[patlen] = NUL;
i = searchit(curwin, curbuf, &t, NULL,
c == Ctrl_G ? FORWARD : BACKWARD,
- pat, patlen, count, search_flags, RE_SEARCH, NULL);
+ searchstr, searchstrlen, count, search_flags, RE_SEARCH, NULL);
--emsg_off;
- pat[patlen] = save;
- if (bslsh)
- pat[patlen - 1] = '\';
+ if (dircp != NULL)
+ *dircp = search_delim;
if (i)
{
- is_state->search_start = is_state->match_start;
+ pos_T match_start = is_state->match_start;
+ pos_T match_end = is_state->match_end;
+ long off = offset.off;
+
+ is_state->search_start = match_start;
+ if (!offset.line && (offset.end || off != 0))
+ {
+ if (offset.end)
+ {
+ is_state->search_start = match_end;
+ (void)decl(&is_state->search_start);
+ }
+ while (off > 0)
+ {
+ if (incl(&is_state->search_start) == -1)
+ break;
+ --off;
+ }
+ while (off < 0)
+ {
+ if (decl(&is_state->search_start) == -1)
+ break;
+ ++off;
+ }
+ }
is_state->match_end = t;
is_state->match_start = t;
if (c == Ctrl_T && firstc != '?')
@@ -733,6 +743,7 @@ may_adjust_incsearch_highlighting(
}
else
vim_beep(BO_ERROR);
+ vim_free(strcopy);
restore_last_search_pattern();
return FAIL;
}
diff --git a/src/proto/
search.pro b/src/proto/
search.pro
index 445817762..3e700a6d8 100644
--- a/src/proto/
search.pro
+++ b/src/proto/
search.pro
@@ -24,6 +24,7 @@ void set_last_search_pat(char_u *s, int idx, int magic, int setlast);
void last_pat_prog(regmmatch_T *regmatch);
int searchit(win_T *win, buf_T *buf, pos_T *pos, pos_T *end_pos, int dir, char_u *pat, size_t patlen, long count, int options, int pat_use, searchit_arg_T *extra_arg);
void set_search_direction(int cdir);
+int parse_search_pattern_offset(char_u **pat, size_t *patlen, int search_delim, int options, char_u **strcopy, char_u **searchstr, size_t *searchstrlen, char_u **dircp, soffset_T *offset);
int do_search(oparg_T *oap, int dirc, int search_delim, char_u *pat, size_t patlen, long count, int options, searchit_arg_T *sia);
int search_for_exact_line(buf_T *buf, pos_T *pos, int dir, char_u *pat);
int searchc(cmdarg_T *cap, int t_cmd);
diff --git a/src/search.c b/src/search.c
index 9b51ef94a..03e66aaec 100644
--- a/src/search.c
+++ b/src/search.c
@@ -1215,6 +1215,109 @@ first_submatch(regmmatch_T *rp)
}
#endif
+/*
+ * Parse a search pattern followed by an optional offset (e.g. "pat/e+1").
+ * On entry "*pat" points at the start of the pattern and "*patlen" is its
+ * length. Updates the in/out parameters:
+ * *pat / *patlen - moved past the pattern and offset
+ * *strcopy - allocated copy if "\?" or "\/" was unescaped
+ * (caller must vim_free() it)
+ * *searchstr and *searchstrlen - pointer/length of the search pattern only
+ * *dircp - location of the trailing delimiter that was
+ * replaced with NUL (or NULL); caller may restore
+ * it
+ * *offset - parsed offset (line/end/off)
+ *
+ * Returns the length of the parsed pattern + offset (used by get_address()
+ * to know how much of the command line was consumed).
+ */
+ int
+parse_search_pattern_offset(
+ char_u **pat,
+ size_t *patlen,
+ int search_delim,
+ int options,
+ char_u **strcopy,
+ char_u **searchstr,
+ size_t *searchstrlen,
+ char_u **dircp,
+ soffset_T *offset)
+{
+ int cmdlen = 0;
+ char_u *p;
+ char_u *ps;
+
+ if (*pat == NULL || **pat == NUL)
+ return 0;
+
+ ps = *strcopy;
+ *searchstr = *pat;
+ *searchstrlen = *patlen;
+ *dircp = NULL;
+
+ /*
+ * Find end of regular expression.
+ * If there is a matching '/' or '?', toss it.
+ */
+ p = skip_regexp_ex(*pat, search_delim, magic_isset(),
+ strcopy, NULL, NULL);
+ if (*strcopy != ps)
+ {
+ size_t len = STRLEN(*strcopy);
+ // made a copy of "pat" to change "\?" to "?"
+ cmdlen += (int)(*patlen - len);
+ *pat = *strcopy;
+ *patlen = len;
+ *searchstr = *strcopy;
+ *searchstrlen = len;
+ }
+ if (*p == search_delim)
+ {
+ *searchstrlen = p - *pat;
+ *dircp = p; // remember where we put the NUL
+ *p++ = NUL;
+ }
+
+ offset->line = FALSE;
+ offset->end = FALSE;
+ offset->off = 0;
+ /*
+ * Check for a line offset or a character offset.
+ * For get_address (echo off) we don't check for a character
+ * offset, because it is meaningless and the 's' could be a
+ * substitute command.
+ */
+ if (*p == '+' || *p == '-' || VIM_ISDIGIT(*p))
+ offset->line = TRUE;
+ else if ((options & SEARCH_OPT)
+ && (*p == 'e' || *p == 's' || *p == 'b'))
+ {
+ if (*p == 'e') // end
+ offset->end = SEARCH_END;
+ ++p;
+ }
+ if (VIM_ISDIGIT(*p) || *p == '+' || *p == '-') // got an offset
+ {
+ // 'nr' or '+nr' or '-nr'
+ if (VIM_ISDIGIT(*p) || VIM_ISDIGIT(*(p + 1)))
+ offset->off = atol((char *)p);
+ else if (*p == '-') // single '-'
+ offset->off = -1;
+ else // single '+'
+ offset->off = 1;
+ ++p;
+ while (VIM_ISDIGIT(*p)) // skip number
+ ++p;
+ }
+
+ // compute length of search command for get_address()
+ cmdlen += (int)(p - *pat);
+ *patlen -= p - *pat;
+ *pat = p; // put pat after search command
+
+ return cmdlen;
+}
+
/*
* Highest level string search function.
* Search for the 'count'th occurrence of pattern 'pat' in direction 'dirc'
@@ -1257,7 +1360,6 @@ do_search(
long c;
char_u *dircp;
char_u *strcopy = NULL;
- char_u *ps;
int show_search_stats;
char_u *msgbuf = NULL;
size_t msgbuflen = 0;
@@ -1369,66 +1471,9 @@ do_search(
if (pat != NULL && *pat != NUL) // look for (new) offset
{
- /*
- * Find end of regular expression.
- * If there is a matching '/' or '?', toss it.
- */
- ps = strcopy;
- p = skip_regexp_ex(pat, search_delim, magic_isset(),
- &strcopy, NULL, NULL);
- if (strcopy != ps)
- {
- size_t len = STRLEN(strcopy);
- // made a copy of "pat" to change "\?" to "?"
- searchcmdlen += (int)(patlen - len);
- pat = strcopy;
- patlen = len;
- searchstr = strcopy;
- searchstrlen = len;
- }
- if (*p == search_delim)
- {
- searchstrlen = p - pat;
- dircp = p; // remember where we put the NUL
- *p++ = NUL;
- }
- spats[0].off.line = FALSE;
- spats[0].off.end = FALSE;
- spats[0].off.off = 0;
- /*
- * Check for a line offset or a character offset.
- * For get_address (echo off) we don't check for a character
- * offset, because it is meaningless and the 's' could be a
- * substitute command.
- */
- if (*p == '+' || *p == '-' || VIM_ISDIGIT(*p))
- spats[0].off.line = TRUE;
- else if ((options & SEARCH_OPT)
- && (*p == 'e' || *p == 's' || *p == 'b'))
- {
- if (*p == 'e') // end
- spats[0].off.end = SEARCH_END;
- ++p;
- }
- if (VIM_ISDIGIT(*p) || *p == '+' || *p == '-') // got an offset
- {
- // 'nr' or '+nr' or '-nr'
- if (VIM_ISDIGIT(*p) || VIM_ISDIGIT(*(p + 1)))
- spats[0].off.off = atol((char *)p);
- else if (*p == '-') // single '-'
- spats[0].off.off = -1;
- else // single '+'
- spats[0].off.off = 1;
- ++p;
- while (VIM_ISDIGIT(*p)) // skip number
- ++p;
- }
-
- // compute length of search command for get_address()
- searchcmdlen += (int)(p - pat);
-
- patlen -= p - pat;
- pat = p; // put pat after search command
+ searchcmdlen += parse_search_pattern_offset(&pat, &patlen,
+ search_delim, options, &strcopy, &searchstr,
+ &searchstrlen, &dircp, &spats[0].off);
}
show_search_stats = FALSE;
diff --git a/src/testdir/test_search.vim b/src/testdir/test_search.vim
index 725d08b59..0a5068175 100644
--- a/src/testdir/test_search.vim
+++ b/src/testdir/test_search.vim
@@ -700,6 +700,27 @@ func Test_search_cmdline7()
call feedkeys("//e\<c-g>\<cr>", 'tx')
call assert_equal('1 bbvimb', getline('.'))
call assert_equal(4, col('.'))
+ call cursor(1, 1)
+ call feedkeys("//+1\<c-g>\<cr>", 'tx')
+ call assert_equal(' 2 bbvimb', getline('.'))
+ call assert_equal([0, 2, 1, 0], getpos('.'))
+ call setline(1, 'blah blah blah')
+ call feedkeys("gg0/blah/e\<C-G>\<CR>", 'tx')
+ call assert_equal([0, 1, 9, 0], getpos('.'))
+ call feedkeys("gg0/blah/e\<C-G>\<C-G>\<CR>", 'tx')
+ call assert_equal([0, 1, 14, 0], getpos('.'))
+ call feedkeys("gg0/blah/e\<C-G>\<C-G>\<C-T>\<CR>", 'tx')
+ call assert_equal([0, 1, 9, 0], getpos('.'))
+ call cursor(1, col('$'))
+ call feedkeys("?blah?e\<C-G>\<CR>", 'tx')
+ call assert_equal([0, 1, 9, 0], getpos('.'))
+ call feedkeys("gg0/blah/e+1\<C-G>\<CR>", 'tx')
+ call assert_equal([0, 1, 10, 0], getpos('.'))
+ call feedkeys("gg0/blah/e-2\<C-G>\<CR>", 'tx')
+ call assert_equal([0, 1, 7, 0], getpos('.'))
+ call setline(1, 'a/b a/b a/b')
+ call feedkeys("gg0/a\/b/e\<C-G>\<CR>", 'tx')
+ call assert_equal([0, 1, 7, 0], getpos('.'))
set noincsearch
call test_override("char_avail", 0)
diff --git a/src/version.c b/src/version.c
index 1328b4db3..f36742a33 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 374,
/**/
373,
/**/