From eb0bd381e5aeaf96464ad82903794bfb8be1345b Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 23 Jun 2026 16:51:03 +0800 Subject: [PATCH] vim-patch:9.2.0707: completion: popup misplaced when text before it is concealed (#40374) Problem: When the cursor line has concealed text before the start of the completion, the insert-mode completion popup is drawn at the wrong screen column and the cursor no longer lines up with the completed text. Solution: Record the concealed width before the cursor on its screen line in a new `win_T` field while `win_line()` draws it, subtract it in `pum_display()` to place the menu over the visible text, and redraw the cursor line so `win_line()` corrects the cursor too. closes: vim/vim#20539 https://github.com/vim/vim/commit/d167c50de4b954e5ddd97208187c5b3b77dd89cc Co-authored-by: Barrett Ruth --- src/nvim/buffer_defs.h | 2 + src/nvim/drawline.c | 4 + src/nvim/insexpand.c | 8 ++ src/nvim/popupmenu.c | 14 +++- test/functional/ui/popupmenu_spec.lua | 110 +++++++++++++++++++++++++ test/old/testdir/test_ins_complete.vim | 101 ++++++++++++++++++++++- 6 files changed, 235 insertions(+), 4 deletions(-) diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 20bda65428..33e9f5a7e9 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -1254,6 +1254,8 @@ struct window_S { // This is related to positions in the window, not in the display or // buffer, thus w_wrow is relative to w_winrow. int w_wrow, w_wcol; // cursor position in window + int w_wcol_conceal_off; // screen cells concealed before w_wcol on + // the cursor's screen line, set by win_line() linenr_T w_botline; // number of the line below the bottom of // the window diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index 5ed65d02fb..c774a47bef 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -2756,6 +2756,10 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b && in_curline && conceal_cursor_line(wp) && (wlv.vcol + wlv.skip_cells >= wp->w_virtcol || mb_schar == NUL)) { wp->w_wcol = wlv.col - wlv.boguscols; + // Screen cells concealed before the cursor on this screen line, so + // pum_display() can line the menu up with the visible text; + // "skip_cells" is the concealed cell at the cursor not yet counted. + wp->w_wcol_conceal_off = wlv.vcol_off_co + wlv.skip_cells; if (wlv.vcol + wlv.skip_cells < wp->w_virtcol) { // Cursor beyond end of the line with 'virtualedit'. wp->w_wcol += wp->w_virtcol - wlv.vcol - wlv.skip_cells; diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index a6cdc913f7..44b5ada7ab 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -1777,6 +1777,14 @@ void ins_compl_show_pum(void) pum_display(compl_match_array, compl_match_arraysize, cur, array_changed, 0); curwin->w_cursor.col = col; + // The cursor was temporarily moved to "compl_col" above to position the + // menu, so the screen update left w_wcol conceal-corrected for that column + // rather than for the real cursor. Redraw the cursor line so the caret is + // positioned correctly when the cursor line has concealed text. + if (curwin->w_p_cole > 0 && conceal_cursor_line(curwin)) { + redrawWinline(curwin, curwin->w_cursor.lnum); + } + // After adding leader, set the current match to shown match. if (compl_started && compl_curr_match != compl_shown_match) { compl_curr_match = compl_shown_match; diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c index 846335738d..024ac57d5a 100644 --- a/src/nvim/popupmenu.c +++ b/src/nvim/popupmenu.c @@ -331,10 +331,20 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i } else { // anchor position: the start of the completed word pum_win_row = curwin->w_wrow; + int wcol = curwin->w_wcol; + // w_wcol does not account for text concealed before the cursor; + // shift by the offset win_line() recorded for the cursor line so the + // menu lines up with the visible text. + if (curwin->w_p_cole > 0 && conceal_cursor_line(curwin)) { + wcol -= curwin->w_wcol_conceal_off; + if (wcol < 0) { + wcol = 0; + } + } if (pum_rl) { - cursor_col = curwin->w_view_width - curwin->w_wcol - 1; + cursor_col = curwin->w_view_width - wcol - 1; } else { - cursor_col = curwin->w_wcol; + cursor_col = wcol; } } diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index 485ea9dc1b..2d29f85a15 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -1102,6 +1102,116 @@ describe('builtin popupmenu', function() }) end) + -- oldtest: Test_pum_position_with_concealed_text() + it('position with fully concealed text', function() + screen:try_resize(55, 10) + exec([[ + call setline(1, ['CONCEALED foobar', 'CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + ]]) + feed('2GA') + poke_eventloop() + feed('') + if multigrid then + screen:expect({ + float_pos = { [4] = { -1, 'NW', 2, 2, 0, false, 100, 1, 2, 0 } }, + }) + else + screen:expect([[ + foobar | + foobar^ | + {12:foobar }{1: }| + {1:~ }|*6 + {5:-- Keyword Local completion (^N^P) The only match} | + ]]) + end + end) + + -- oldtest: Test_pum_position_with_concealed_match() + it('position with concealed text replacement char', function() + screen:try_resize(55, 10) + exec([[ + call setline(1, ['XXX foobar', 'XXX foo']) + call matchadd('Conceal', 'XXX ', 10, -1, {'conceal': '+'}) + setlocal conceallevel=2 concealcursor=nvic + set completeopt=menu,menuone + ]]) + feed('2GA') + poke_eventloop() + feed('') + if multigrid then + screen:expect({ + float_pos = { [4] = { -1, 'NW', 2, 2, 0, false, 100, 1, 2, 0 } }, + }) + else + screen:expect([[ + {14:+}foobar | + {14:+}foobar^ | + {12: foobar }{1: }| + {1:~ }|*6 + {5:-- Keyword Local completion (^N^P) The only match} | + ]]) + end + end) + + -- oldtest: Test_pum_position_with_concealed_rl() + it('position with concealed text and rightleft', function() + screen:try_resize(55, 10) + exec([[ + set rightleft + call setline(1, ['CONCEALED foobar', 'CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + ]]) + feed('2GA') + poke_eventloop() + feed('') + if multigrid then + screen:expect({ + float_pos = { [4] = { -1, 'NW', 2, 2, 40, false, 100, 1, 2, 40 } }, + }) + else + screen:expect([[ + raboof| + ^ raboof| + {1: }{12: raboof}| + {1: ~}|*6 + {5:-- Keyword Local completion (^N^P) The only match} | + ]]) + end + end) + + -- oldtest: Test_pum_position_with_concealed_wrap() + it('position with wrapped concealed text', function() + screen:try_resize(20, 10) + exec([[ + call setline(1, ['foobar', 'aaaaaaaaaaaaaaaaaaaa CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + ]]) + feed('2GA') + poke_eventloop() + feed('') + if multigrid then + screen:expect({ + float_pos = { [4] = { -1, 'NW', 2, 3, 0, false, 100, 1, 3, 0 } }, + }) + else + screen:expect([[ + foobar | + aaaaaaaaaaaaaaaaaaaa| + foobar^ | + {12: foobar }{1: }| + {1:~ }|*5 + {5:-- The only match} | + ]]) + end + end) + it('with preview-window above', function() feed(':ped4+') feed('iaa bb cc dd ee ff gg hh ii jj') diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index fd9df78c59..af11f2763f 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -960,7 +960,7 @@ func Test_pum_stopped_by_timer() endfunc END - call writefile(lines, 'Xpumscript') + call writefile(lines, 'Xpumscript', 'D') let buf = RunVimInTerminal('-S Xpumscript', #{rows: 12}) call term_sendkeys(buf, ":call StartCompl()\") call TermWait(buf, 200) @@ -968,7 +968,104 @@ func Test_pum_stopped_by_timer() call VerifyScreenDump(buf, 'Test_pum_stopped_by_timer', {}) call StopVimInTerminal(buf) - call delete('Xpumscript') +endfunc + +" The completion popup menu must line up with the start of the completed text +" on screen, also when there is concealed text before it on the line. +func Test_pum_position_with_concealed_text() + CheckScreendump + + let lines =<< trim END + call setline(1, ['CONCEALED foobar', 'CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + END + + call writefile(lines, 'Xpumconceal', 'D') + let buf = RunVimInTerminal('-S Xpumconceal', #{rows: 10}) + call TermWait(buf, 50) + call term_sendkeys(buf, "2GA") + call TermWait(buf, 50) + call term_sendkeys(buf, "\\") + call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_text', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + +" Same alignment when the concealed text comes from a match and is shown as a +" replacement character with 'conceallevel' 2. +func Test_pum_position_with_concealed_match() + CheckScreendump + + let lines =<< trim END + call setline(1, ['XXX foobar', 'XXX foo']) + call matchadd('Conceal', 'XXX ', 10, -1, {'conceal': '+'}) + setlocal conceallevel=2 concealcursor=nvic + set completeopt=menu,menuone + END + + call writefile(lines, 'Xpumconcealmatch', 'D') + let buf = RunVimInTerminal('-S Xpumconcealmatch', #{rows: 10}) + call TermWait(buf, 50) + call term_sendkeys(buf, "2GA") + call TermWait(buf, 50) + call term_sendkeys(buf, "\\") + call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_match', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + +" The menu lines up with the visible text in a 'rightleft' window too, where +" the cursor screen column is mirrored. +func Test_pum_position_with_concealed_rl() + CheckScreendump + CheckFeature rightleft + + let lines =<< trim END + set rightleft + call setline(1, ['CONCEALED foobar', 'CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + END + + call writefile(lines, 'Xpumconcealrl', 'D') + let buf = RunVimInTerminal('-S Xpumconcealrl', #{rows: 10}) + call TermWait(buf, 50) + call term_sendkeys(buf, "2GA") + call TermWait(buf, 50) + call term_sendkeys(buf, "\\") + call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_rl', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + +" The recorded offset is per screen line, so the menu also lines up when the +" concealed text and the completion are on a wrapped continuation line. +func Test_pum_position_with_concealed_wrap() + CheckScreendump + + let lines =<< trim END + call setline(1, ['foobar', 'aaaaaaaaaaaaaaaaaaaa CONCEALED foo']) + syntax match Hidden /CONCEALED / conceal + setlocal conceallevel=3 concealcursor=nvic + set completeopt=menu,menuone + END + + call writefile(lines, 'Xpumconcealwrap', 'D') + let buf = RunVimInTerminal('-S Xpumconcealwrap', #{rows: 10, cols: 20}) + call TermWait(buf, 50) + call term_sendkeys(buf, "2GA") + call TermWait(buf, 50) + call term_sendkeys(buf, "\\") + call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_wrap', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) endfunc func Test_complete_stopinsert_startinsert()