]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0689: the "%" command is slow on a long line with many slashes v9.2.0689
authorHirohito Higashi <h.east.727@gmail.com>
Sun, 21 Jun 2026 14:53:03 +0000 (14:53 +0000)
committerChristian Brabandt <cb@256bit.org>
Sun, 21 Jun 2026 14:53:03 +0000 (14:53 +0000)
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) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
src/cindent.c
src/proto/cindent.pro
src/proto/search.pro
src/search.c
src/testdir/test_normal.vim
src/version.c

index 6bea836e2eec9e4a6a57f0cd2ef6abdf1a52cedf..b3a55c108ca13442877e12aaa4e874c91b32ee87 100644 (file)
@@ -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.
index 80a6add55041278e4434061770c7ec1c20603b5d..98721b1f2dd63a0e17c94be24b882abe15c62e2b 100644 (file)
@@ -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);
index 3e700a6d8e4df53bd6b78f1bba627a317da1a12a..8830c8646671b26cf7dba0a9c584c99d5c3e7198 100644 (file)
@@ -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);
index e60f2d7b4329d1a646ae4a29bccecdea065c576b..77f945ddf823c9f99ededc063e42fdfdc5f626a8 100644 (file)
@@ -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.
index b4298e3046a41f5255e95ca01234c45d3cb075f8..9b9eca8cfd8a42d5df91981774ca8d28d3092dc6 100644 (file)
@@ -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
index 917e6377323d2922e9890a80e7bd82d21a6ccca7..eb88baacac98f950898f44334ad8f0eacf5a8692 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    689,
 /**/
     688,
 /**/