From: Hirohito Higashi Date: Sun, 21 Jun 2026 14:53:03 +0000 (+0000) Subject: patch 9.2.0689: the "%" command is slow on a long line with many slashes X-Git-Tag: v9.2.0689^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9f9af034adcdb5773e19c4bb52e3c368bbce826e;p=thirdparty%2Fvim.git patch 9.2.0689: the "%" command is slow on a long line with many slashes Problem: The "%" command can be very slow on a long line that contains many slashes, for example a line of base64 data. Solution: When looking for a line comment, scan the line only once while skipping over strings, instead of rescanning from the start for every slash. Move check_linecomment() to cindent.c so it can reuse the file-local skip_string(). related: #20491 fixes: #20557 closes: #20575 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- diff --git a/src/cindent.c b/src/cindent.c index 6bea836e2e..b3a55c108c 100644 --- a/src/cindent.c +++ b/src/cindent.c @@ -146,6 +146,76 @@ is_pos_in_string(char_u *line, colnr_T col) return !((colnr_T)(p - line) <= col); } +/* + * Check if line[] contains a "//" comment, ignoring matches inside strings. + * Return MAXCOL if not, otherwise return the column. + * The line is scanned once (skipping strings), so this stays linear even on + * lines with many slashes (e.g. base64 data). + */ + int +check_linecomment(char_u *line) +{ + char_u *p; + + p = line; + // skip Lispish one-line comments + if (curbuf->b_p_lisp) + { + if (vim_strchr(p, ';') != NULL) // there may be comments + { + int in_str = FALSE; // inside of string + + p = line; // scan from start + while ((p = vim_strpbrk(p, (char_u *)"\";")) != NULL) + { + if (*p == '"') + { + if (in_str) + { + if (*(p - 1) != '\\') // skip escaped quote + in_str = FALSE; + } + else if (p == line || ((p - line) >= 2 + // skip #\" form + && *(p - 1) != '\\' && *(p - 2) != '#')) + in_str = TRUE; + } + else if (!in_str && ((p - line) < 2 + || (*(p - 1) != '\\' && *(p - 2) != '#')) + && !is_pos_in_string(line, (colnr_T)(p - line))) + break; // found! + ++p; + } + } + else + p = NULL; + } + else + { + // Scan the line once, skipping over strings, char constants and raw + // strings, instead of testing each '/' with is_pos_in_string() (which + // rescans from the start, making this quadratic on lines with many + // slashes). + for ( ; *p != NUL; ++p) + { + p = skip_string(p); + if (*p == NUL) + break; + // Accept a double /, unless it's preceded with * and followed by + // *, because * / / * is an end and start of a C comment. + if (p[0] == '/' && p[1] == '/' + && (p == line || p[-1] != '*' || p[2] != '*')) + break; + } + if (*p == NUL) + p = NULL; + } + + if (p == NULL) + return MAXCOL; + return (int)(p - line); +} + /* * Find the start of a comment, not knowing if we are in a comment right now. * Search starts at w_cursor.lnum and goes backwards. diff --git a/src/proto/cindent.pro b/src/proto/cindent.pro index 80a6add550..98721b1f2d 100644 --- a/src/proto/cindent.pro +++ b/src/proto/cindent.pro @@ -1,6 +1,7 @@ /* cindent.c */ int cin_is_cinword(char_u *line); int is_pos_in_string(char_u *line, colnr_T col); +int check_linecomment(char_u *line); pos_T *find_start_comment(int ind_maxcomment); int cindent_on(void); void parse_cino(buf_T *buf); diff --git a/src/proto/search.pro b/src/proto/search.pro index 3e700a6d8e..8830c86466 100644 --- a/src/proto/search.pro +++ b/src/proto/search.pro @@ -30,7 +30,6 @@ int search_for_exact_line(buf_T *buf, pos_T *pos, int dir, char_u *pat); int searchc(cmdarg_T *cap, int t_cmd); pos_T *findmatch(oparg_T *oap, int initc); pos_T *findmatchlimit(oparg_T *oap, int initc, int flags, int maxtravel); -int check_linecomment(char_u *line); void showmatch(int c); int current_search(long count, int forward); int linewhite(linenr_T lnum); diff --git a/src/search.c b/src/search.c index e60f2d7b43..77f945ddf8 100644 --- a/src/search.c +++ b/src/search.c @@ -2517,10 +2517,11 @@ findmatchlimit( } } - // Track block comment state when FM_SKIPCOMM is set. + // Track block comment state when FM_SKIPCOMM is set. Markers inside a + // string are not comments, so skip them while "inquote" is set. // Backward: '/' of end-marker enters comment; '*' of start-marker exits. // Forward: '/' of start-marker enters comment; '/' of end-marker exits. - if (skip_comments && !comment_dir) + if (skip_comments && !comment_dir && !inquote) { if (backwards) { @@ -2833,65 +2834,6 @@ findmatchlimit( return (pos_T *)NULL; // never found it } -/* - * Check if line[] contains a / / comment. - * Return MAXCOL if not, otherwise return the column. - */ - int -check_linecomment(char_u *line) -{ - char_u *p; - - p = line; - // skip Lispish one-line comments - if (curbuf->b_p_lisp) - { - if (vim_strchr(p, ';') != NULL) // there may be comments - { - int in_str = FALSE; // inside of string - - p = line; // scan from start - while ((p = vim_strpbrk(p, (char_u *)"\";")) != NULL) - { - if (*p == '"') - { - if (in_str) - { - if (*(p - 1) != '\\') // skip escaped quote - in_str = FALSE; - } - else if (p == line || ((p - line) >= 2 - // skip #\" form - && *(p - 1) != '\\' && *(p - 2) != '#')) - in_str = TRUE; - } - else if (!in_str && ((p - line) < 2 - || (*(p - 1) != '\\' && *(p - 2) != '#')) - && !is_pos_in_string(line, (colnr_T)(p - line))) - break; // found! - ++p; - } - } - else - p = NULL; - } - else - while ((p = vim_strchr(p, '/')) != NULL) - { - // Accept a double /, unless it's preceded with * and followed by - // *, because * / / * is an end and start of a C comment. Only - // accept the position if it is not inside a string. - if (p[1] == '/' && (p == line || p[-1] != '*' || p[2] != '*') - && !is_pos_in_string(line, (colnr_T)(p - line))) - break; - ++p; - } - - if (p == NULL) - return MAXCOL; - return (int)(p - line); -} - /* * Move cursor briefly to character matching the one under the cursor. * Used for Insert mode and "r" command. diff --git a/src/testdir/test_normal.vim b/src/testdir/test_normal.vim index b4298e3046..9b9eca8cfd 100644 --- a/src/testdir/test_normal.vim +++ b/src/testdir/test_normal.vim @@ -3960,6 +3960,44 @@ func Test_normal_percent_skip_comment() bwipe! endfunc +" A "//" inside a string must not be treated as a line comment by "%". The +" line is scanned in a single pass, so this stays fast even on lines with many +" slashes (e.g. base64 data). +func Test_normal_percent_skip_comment_string() + new + setlocal comments=s1:/*,mb:*,ex:*/,:// + + " The "//" inside the string is not a comment, so "(" matches the real ")". + call setline(1, ['("a // b")']) + call cursor(1, 1) + normal % + call assert_equal([1, 10], [line('.'), col('.')]) + + " JSON-like: "{" matches the closing "}" although the string has slashes. + silent! %delete _ + call setline(1, ['{', ' "k": "x//y",', '}']) + call cursor(1, 1) + normal % + call assert_equal([3, 1], [line('.'), col('.')]) + + " A "/*" inside a string must not start a block comment, so "(" still + " matches the real ")" after the string. + silent! %delete _ + call setline(1, ['( "a /* b" )']) + call cursor(1, 1) + normal % + call assert_equal([1, 12], [line('.'), col('.')]) + + " A real /* */ block comment is still skipped: "(" matches the last ")". + silent! %delete _ + call setline(1, ['( /* ) */ x )']) + call cursor(1, 1) + normal % + call assert_equal([1, 13], [line('.'), col('.')]) + + bwipe! +endfunc + " Test for << and >> commands to shift text by 'shiftwidth' func Test_normal_shift_rightleft() new diff --git a/src/version.c b/src/version.c index 917e637732..eb88baacac 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 689, /**/ 688, /**/