diff --git a/src/nvim/indent_c.c b/src/nvim/indent_c.c index c89dde5bb3..81b165b5ef 100644 --- a/src/nvim/indent_c.c +++ b/src/nvim/indent_c.c @@ -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. diff --git a/src/nvim/normal.c b/src/nvim/normal.c index c4c62cfb63..b9ec196239 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -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" diff --git a/src/nvim/search.c b/src/nvim/search.c index 6723f7844d..66cfef02c3 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -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. diff --git a/test/old/testdir/test_normal.vim b/test/old/testdir/test_normal.vim index 0296f503a3..53fc655dc4 100644 --- a/test/old/testdir/test_normal.vim +++ b/test/old/testdir/test_normal.vim @@ -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