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

d167c50de4

Co-authored-by: Barrett Ruth <br@barrettruth.com>
This commit is contained in:
zeertzjq
2026-06-23 16:51:03 +08:00
committed by GitHub
parent e750f7c357
commit eb0bd381e5
6 changed files with 235 additions and 4 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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('<C-X><C-N>')
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('<C-X><C-N>')
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('<C-X><C-N>')
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('<C-X><C-N>')
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(':ped<CR><c-w>4+')
feed('iaa bb cc dd ee ff gg hh ii jj<cr>')

View File

@@ -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()\<CR>")
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, "\<C-X>\<C-N>")
call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_text', {})
call term_sendkeys(buf, "\<Esc>")
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, "\<C-X>\<C-N>")
call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_match', {})
call term_sendkeys(buf, "\<Esc>")
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, "\<C-X>\<C-N>")
call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_rl', {})
call term_sendkeys(buf, "\<Esc>")
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, "\<C-X>\<C-N>")
call VerifyScreenDump(buf, 'Test_pum_position_with_concealed_wrap', {})
call term_sendkeys(buf, "\<Esc>")
call StopVimInTerminal(buf)
endfunc
func Test_complete_stopinsert_startinsert()