From e997599894e083e79864913a6c6711083a7af35e Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sun, 14 Jun 2026 10:52:50 +0800 Subject: [PATCH] vim-patch:9.2.0640: the "%" command jumps to parens and braces inside comments (#40229) Problem: The "%" command jumps to parens and braces inside comments, unlike the "=" operator (cindent), which ignores them. Solution: When 'comments' defines C-style comments and "%" is not in 'cpoptions', skip matching parens inside such comments, except when the cursor is inside a comment so a match there can still be found. fixes: vim/vim#20329 related: vim/vim#20111 closes: vim/vim#20491 https://github.com/vim/vim/commit/b8a109dcfb96f213328a966a7e38eb28da697229 Co-authored-by: Hirohito Higashi Co-authored-by: Claude Opus 4.8 (1M context) --- runtime/doc/motion.txt | 10 +++-- src/nvim/normal.c | 30 ++++++++++++++- test/old/testdir/test_normal.vim | 64 ++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/runtime/doc/motion.txt b/runtime/doc/motion.txt index 5d4921f240..147399bcbc 100644 --- a/runtime/doc/motion.txt +++ b/runtime/doc/motion.txt @@ -1338,9 +1338,13 @@ remembered. quotes). Note that this works fine for C, but not for Perl, where single quotes are used for strings. - Nothing special is done for matches in comments. You - can either use the |matchit| plugin or put quotes around - matches. + When in addition 'comments' defines C-style "//" or + "/*" comments, parens and braces inside such comments + are skipped, like the |=| operator does. This does + not happen when the cursor is inside a comment, so a + match inside that comment can still be found. + Otherwise you can use the |matchit| plugin or put + quotes around matches. No count is allowed, {count}% jumps to a line {count} percentage down the file |N%|. Using '%' on diff --git a/src/nvim/normal.c b/src/nvim/normal.c index f9434c1c1e..c4c62cfb63 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -4356,6 +4356,23 @@ static void nv_brackets(cmdarg_T *cap) } } +/// Return true when 'comments' defines a C-style line ("//") or block comment. +/// This is when "%" should skip matching parens in comments, like the "=" +/// operator does. +static bool buf_has_cstyle_comments(void) +{ + char part_buf[COM_MAX_LEN]; // buffer for one 'comments' part + + for (char *list = curbuf->b_p_com; *list;) { + (void)copy_option_part(&list, part_buf, COM_MAX_LEN, ","); + char *string = vim_strchr(part_buf, ':'); // flags and comment leader + if (string != NULL && string[1] == '/' && (string[2] == '/' || string[2] == '*')) { + return true; + } + } + return false; +} + /// Handle Normal mode "%" command. static void nv_percent(cmdarg_T *cap) { @@ -4384,10 +4401,21 @@ static void nv_percent(cmdarg_T *cap) beginline(BL_SOL | BL_FIX); } } else { // "%" : go to matching paren + int flags = 0; + // Skip matching parens inside C-style comments, like the "=" operator + // does, but not when "%" is in 'cpoptions' (Vi-compatible) or the + // cursor sits in a line comment (so a match there can still be found). + if (vim_strchr(p_cpo, CPO_MATCH) == NULL && buf_has_cstyle_comments()) { + int comment_col = check_linecomment(get_cursor_line_ptr()); + if (comment_col == MAXCOL || curwin->w_cursor.col < (colnr_T)comment_col) { + flags = FM_SKIPCOMM; + } + } + pos_T *pos; cap->oap->motion_type = kMTCharWise; cap->oap->use_reg_one = true; - if ((pos = findmatch(cap->oap, NUL)) == NULL) { + if ((pos = findmatchlimit(cap->oap, NUL, flags, 0)) == NULL) { clearopbeep(cap->oap); } else { setpcmark(); diff --git a/test/old/testdir/test_normal.vim b/test/old/testdir/test_normal.vim index 6a272c80f4..0296f503a3 100644 --- a/test/old/testdir/test_normal.vim +++ b/test/old/testdir/test_normal.vim @@ -3908,6 +3908,70 @@ func Test_normal_percent_jump() bwipe! endfunc +" Test that "%" skips parens inside comments when 'comments' defines C-style +" "//" or "/*" comments. +func Test_normal_percent_skip_comment() + new + setlocal comments=s1:/*,mb:*,ex:*/,:// + + " Forward: skip a ")" inside a // comment, match the real one. + silent! %delete _ + call setline(1, ['foo( // )', ');']) + call cursor(1, 4) + normal % + call assert_equal([2, 1], [line('.'), col('.')]) + + " Forward: skip a ")" inside a /* */ comment, match the real one. + silent! %delete _ + call setline(1, ['bar( /* ) */ x)']) + call cursor(1, 4) + normal % + call assert_equal([1, 15], [line('.'), col('.')]) + + " Backward: skip a "(" inside a // comment, match the real one. + silent! %delete _ + call setline(1, ['( // (', ')']) + call cursor(2, 1) + normal % + call assert_equal([1, 1], [line('.'), col('.')]) + + " Cursor inside a // comment: a match inside that comment is still found. + silent! %delete _ + call setline(1, ['x // ( y )']) + call cursor(1, 6) + normal % + call assert_equal([1, 10], [line('.'), col('.')]) + + " Cursor inside a /* */ comment: a match inside that comment is still found. + silent! %delete _ + call setline(1, ['/* a ( b ) c */']) + call cursor(1, 6) + normal % + call assert_equal([1, 10], [line('.'), col('.')]) + + " When 'comments' has no C-style comments the parens are not skipped. + setlocal comments=b:# + silent! %delete _ + call setline(1, ['foo( // )', ');']) + call cursor(1, 4) + normal % + call assert_equal([1, 10], [line('.'), col('.')]) + + " With "%" in 'cpoptions' Vi-compatible matching is used and the parens + " inside comments are not skipped. + let save_cpo = &cpoptions + setlocal comments=s1:/*,mb:*,ex:*/,:// + set cpoptions+=% + silent! %delete _ + call setline(1, ['foo( // )', ');']) + call cursor(1, 4) + normal % + call assert_equal([1, 10], [line('.'), col('.')]) + let &cpoptions = save_cpo + + bwipe! +endfunc + " Test for << and >> commands to shift text by 'shiftwidth' func Test_normal_shift_rightleft() new