mirror of
https://github.com/neovim/neovim.git
synced 2026-07-10 03:19:44 +00:00
vim-patch:9.2.0689: the "%" command is slow on a long line with many slashes (#40361)
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: vim/vim#20491
fixes: vim/vim#20557
closes: vim/vim#20575
9f9af034ad
Co-authored-by: Hirohito Higashi <h.east.727@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,68 @@ typedef struct {
|
||||
} cpp_baseclass_cache_T;
|
||||
|
||||
#include "indent_c.c.generated.h"
|
||||
|
||||
/// 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(const char *line)
|
||||
{
|
||||
const char *p = line; // scan from start
|
||||
// skip Lispish one-line comments
|
||||
if (curbuf->b_p_lisp) {
|
||||
if (vim_strchr(p, ';') != NULL) { // there may be comments
|
||||
bool in_str = false; // inside of string
|
||||
|
||||
while ((p = strpbrk(p, "\";")) != 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.
|
||||
// Return NULL when not inside a comment.
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "nvim/help.h"
|
||||
#include "nvim/highlight.h"
|
||||
#include "nvim/highlight_defs.h"
|
||||
#include "nvim/indent_c.h"
|
||||
#include "nvim/keycodes.h"
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/mapping.h"
|
||||
|
||||
@@ -2068,10 +2068,11 @@ pos_T *findmatchlimit(oparg_T *oap, int initc, int flags, int64_t maxtravel)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Guard pos.col < comment_col: don't misread '* /' at the '//'
|
||||
// position as a block-comment end-marker.
|
||||
@@ -2339,56 +2340,6 @@ pos_T *findmatchlimit(oparg_T *oap, int initc, int flags, int64_t maxtravel)
|
||||
return (pos_T *)NULL; // never found it
|
||||
}
|
||||
|
||||
/// Check if line[] contains a / / comment.
|
||||
/// @returns MAXCOL if not, otherwise return the column.
|
||||
int check_linecomment(const char *line)
|
||||
{
|
||||
const char *p = line; // scan from start
|
||||
// skip Lispish one-line comments
|
||||
if (curbuf->b_p_lisp) {
|
||||
if (vim_strchr(p, ';') != NULL) { // there may be comments
|
||||
bool in_str = false; // inside of string
|
||||
|
||||
while ((p = strpbrk(p, "\";")) != 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.
|
||||
/// Show the match only if it is visible on the screen.
|
||||
|
||||
@@ -3972,6 +3972,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
|
||||
|
||||
Reference in New Issue
Block a user