From: Barrett Ruth
Date: Mon, 22 Jun 2026 20:13:22 +0000 (+0000)
Subject: patch 9.2.0707: completion: popup misplaced when text before it is concealed
X-Git-Tag: v9.2.0707^0
X-Git-Url: http://git.ipfire.org/index.cgi?a=commitdiff_plain;ds=sidebyside;p=thirdparty%2Fvim.git
patch 9.2.0707: completion: popup misplaced when text before it is concealed
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: #20539
Signed-off-by: Barrett Ruth
Signed-off-by: Christian Brabandt
---
diff --git a/src/drawline.c b/src/drawline.c
index 86911cefc2..a2679e1467 100644
--- a/src/drawline.c
+++ b/src/drawline.c
@@ -3813,6 +3813,10 @@ win_line(
else
# endif
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 + skip_cells;
if (wlv.vcol + skip_cells < wp->w_virtcol)
// Cursor beyond end of the line with 'virtualedit'.
wp->w_wcol += wp->w_virtcol - wlv.vcol - skip_cells;
diff --git a/src/insexpand.c b/src/insexpand.c
index 5adad45309..c7186b9297 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -1896,6 +1896,15 @@ ins_compl_show_pum(void)
pum_display(compl_match_array, compl_match_arraysize, cur);
curwin->w_cursor.col = col;
+#ifdef FEAT_CONCEAL
+ // 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);
+#endif
+
// 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/popupmenu.c b/src/popupmenu.c
index b7929607d9..bed2495b87 100644
--- a/src/popupmenu.c
+++ b/src/popupmenu.c
@@ -361,6 +361,17 @@ pum_display(
{
// w_wcol includes virtual text "above"
int wcol = curwin->w_wcol % curwin->w_width;
+#ifdef FEAT_CONCEAL
+ // 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;
+ }
+#endif
#ifdef FEAT_RIGHTLEFT
if (pum_rl)
cursor_col = curwin->w_wincol + curwin->w_width - wcol - 1;
diff --git a/src/structs.h b/src/structs.h
index 5d6511a5a8..70010567d2 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -4367,6 +4367,10 @@ struct window_S
* buffer, thus w_wrow is relative to w_winrow.
*/
int w_wrow, w_wcol; // cursor position in window
+#ifdef FEAT_CONCEAL
+ int w_wcol_conceal_off; // screen cells concealed before w_wcol on
+ // the cursor's screen line, set by win_line()
+#endif
/*
* Info about the lines currently in the window is remembered to avoid
diff --git a/src/testdir/dumps/Test_pum_position_with_concealed_match.dump b/src/testdir/dumps/Test_pum_position_with_concealed_match.dump
new file mode 100644
index 0000000000..f8d39c8bff
--- /dev/null
+++ b/src/testdir/dumps/Test_pum_position_with_concealed_match.dump
@@ -0,0 +1,10 @@
+|++0#e0e0e08#6c6c6c255|f+0#0000000#ffffff0|o@1|b|a|r| @67
+|++0#e0e0e08#6c6c6c255|f+0#0000000#ffffff0|o@1|b|a|r> @67
+| +0#0000001#e0e0e08|f|o@1|b|a|r| @8| +0#4040ff13#ffffff0@58
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|-+2#0000000&@1| |K|e|y|w|o|r|d| |L|o|c|a|l| |c|o|m|p|l|e|t|i|o|n| |(|^|N|^|P|)| |T|h|e| |o|n|l|y| |m|a|t|c|h| +0&&@25
diff --git a/src/testdir/dumps/Test_pum_position_with_concealed_rl.dump b/src/testdir/dumps/Test_pum_position_with_concealed_rl.dump
new file mode 100644
index 0000000000..c2f497a871
--- /dev/null
+++ b/src/testdir/dumps/Test_pum_position_with_concealed_rl.dump
@@ -0,0 +1,10 @@
+| +0ffffff0@68|r|a|b|o@1|f
+| @67> |r|a|b|o@1|f
+| +0#4040ff13&@59| +0#0000001#e0e0e08@8|r|a|b|o@1|f
+| +0#4040ff13#ffffff0@73|~
+| @73|~
+| @73|~
+| @73|~
+| @73|~
+| @73|~
+|-+2#0000000&@1| |K|e|y|w|o|r|d| |L|o|c|a|l| |c|o|m|p|l|e|t|i|o|n| |(|^|N|^|P|)| |T|h|e| |o|n|l|y| |m|a|t|c|h| +0&&@25
diff --git a/src/testdir/dumps/Test_pum_position_with_concealed_text.dump b/src/testdir/dumps/Test_pum_position_with_concealed_text.dump
new file mode 100644
index 0000000000..a30e2f86bb
--- /dev/null
+++ b/src/testdir/dumps/Test_pum_position_with_concealed_text.dump
@@ -0,0 +1,10 @@
+|f+0ffffff0|o@1|b|a|r| @68
+|f|o@1|b|a|r> @68
+|f+0#0000001#e0e0e08|o@1|b|a|r| @8| +0#4040ff13#ffffff0@59
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|~| @73
+|-+2#0000000&@1| |K|e|y|w|o|r|d| |L|o|c|a|l| |c|o|m|p|l|e|t|i|o|n| |(|^|N|^|P|)| |T|h|e| |o|n|l|y| |m|a|t|c|h| +0&&@25
diff --git a/src/testdir/dumps/Test_pum_position_with_concealed_wrap.dump b/src/testdir/dumps/Test_pum_position_with_concealed_wrap.dump
new file mode 100644
index 0000000000..0f3a3cbe0f
--- /dev/null
+++ b/src/testdir/dumps/Test_pum_position_with_concealed_wrap.dump
@@ -0,0 +1,10 @@
+|f+0ffffff0|o@1|b|a|r| @13
+|a@19
+| |f|o@1|b|a|r> @12
+| +0#0000001#e0e0e08|f|o@1|b|a|r| @8| +0#4040ff13#ffffff0@3
+|~| @18
+|~| @18
+|~| @18
+|~| @18
+|~| @18
+|-+2#0000000&@1| |T|h|e| |o|n|l|y| |m|a|t|c|h| +0&&@2
diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index 93ca66f5a9..cb901d609f 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -870,6 +870,104 @@ func Test_pum_stopped_by_timer()
call StopVimInTerminal(buf)
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()
nnoremap startinsert
inoremap stopinsert
diff --git a/src/version.c b/src/version.c
index e663c2117f..ff0e68e1c7 100644
--- a/src/version.c
+++ b/src/version.c
@@ -759,6 +759,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 707,
/**/
706,
/**/