fix(nvim__set_complete): pum preview info truncated during completion #32555

Problem:
1. The original info text is truncated in pum_set_preview_text.
2. There is an extra newline character at the end of the info text.
3. Incorrect usage of plines_win leads to errors in calculating the window height.

Solution:
1. Improved string handling in pum_preview_set_text to safely process line content without modifying the original text. Now, the preview info is correctly preserved while splitting lines.
2. Do not append a trailing newline to the preview buffer when one is present at the end.
3. Set w_widher_inner in advance; otherwise, plines_win cannot correctly calculate the wrapped line height.
This commit is contained in:
glepnir
2025-03-10 04:19:01 +08:00
committed by GitHub
parent 34a2bfdcc5
commit d9585bdcfb
2 changed files with 309 additions and 160 deletions

View File

@@ -803,14 +803,25 @@ static void pum_preview_set_text(buf_T *buf, char *info, linenr_T *lnum, int *ma
Error err = ERROR_INIT; Error err = ERROR_INIT;
Arena arena = ARENA_EMPTY; Arena arena = ARENA_EMPTY;
Array replacement = ARRAY_DICT_INIT; Array replacement = ARRAY_DICT_INIT;
char *token = NULL;
char *line = os_strtok(info, "\n", &token);
buf->b_p_ma = true; buf->b_p_ma = true;
while (line != NULL) {
ADD(replacement, STRING_OBJ(cstr_to_string(line))); // Iterate through the string line by line by temporarily replacing newlines with NUL
for (char *curr = info, *next; curr; curr = next ? next + 1 : NULL) {
if ((next = strchr(curr, '\n'))) {
*next = NUL; // Temporarily replace the newline with a string terminator
}
// Only skip if this is an empty line AND it's the last line
if (*curr == '\0' && !next) {
break;
}
*max_width = MAX(*max_width, (int)mb_string2cells(curr));
ADD(replacement, STRING_OBJ(cstr_to_string(curr)));
(*lnum)++; (*lnum)++;
(*max_width) = MAX(*max_width, (int)mb_string2cells(line));
line = os_strtok(NULL, "\n", &token); if (next) {
*next = '\n';
}
} }
int original_textlock = textlock; int original_textlock = textlock;
@@ -828,8 +839,23 @@ static void pum_preview_set_text(buf_T *buf, char *info, linenr_T *lnum, int *ma
buf->b_p_ma = false; buf->b_p_ma = false;
} }
/// Calculate the total height (in screen lines) of the first 'count' buffer lines in window 'wp'.
/// Takes line wrapping and other display factors into account.
///
/// @param wp Window pointer
/// @param count Number of buffer lines to measure (1-based)
/// @return Total height in screen lines
static inline int pum_preview_win_height(win_T *wp, linenr_T count)
{
int height = 0;
for (int i = 1; i <= count; i++) {
height += plines_win(wp, i, false);
}
return height;
}
/// adjust floating info preview window position /// adjust floating info preview window position
static void pum_adjust_info_position(win_T *wp, int height, int width) static void pum_adjust_info_position(win_T *wp, int width)
{ {
int col = pum_col + pum_width + pum_scrollbar + 1; int col = pum_col + pum_width + pum_scrollbar + 1;
// TODO(glepnir): support config align border by using completepopup // TODO(glepnir): support config align border by using completepopup
@@ -850,8 +876,10 @@ static void pum_adjust_info_position(win_T *wp, int height, int width)
} }
// when pum_above is SW otherwise is NW // when pum_above is SW otherwise is NW
wp->w_config.anchor = pum_above ? kFloatAnchorSouth : 0; wp->w_config.anchor = pum_above ? kFloatAnchorSouth : 0;
wp->w_config.row = pum_above ? pum_row + height : pum_row; linenr_T count = wp->w_buffer->b_ml.ml_line_count;
wp->w_config.height = MIN(Rows, height); wp->w_width_inner = wp->w_config.width;
wp->w_config.height = MIN(Rows, pum_preview_win_height(wp, count));
wp->w_config.row = pum_above ? pum_row + wp->w_config.height : pum_row;
wp->w_config.hide = false; wp->w_config.hide = false;
win_config_float(wp, wp->w_config); win_config_float(wp, wp->w_config);
} }
@@ -884,10 +912,7 @@ win_T *pum_set_info(int selected, char *info)
RedrawingDisabled--; RedrawingDisabled--;
redraw_later(wp, UPD_NOT_VALID); redraw_later(wp, UPD_NOT_VALID);
if (wp->w_p_wrap) { pum_adjust_info_position(wp, max_info_width);
lnum += plines_win(wp, lnum, true);
}
pum_adjust_info_position(wp, lnum, max_info_width);
unblock_autocmds(); unblock_autocmds();
return wp; return wp;
} }
@@ -1063,11 +1088,8 @@ static bool pum_set_selected(int n, int repeat)
curwin->w_cursor.col = 0; curwin->w_cursor.col = 0;
if (use_float) { if (use_float) {
if (curwin->w_p_wrap) {
lnum += plines_win(curwin, lnum, true);
}
// adjust floating window by actually height and max info text width // adjust floating window by actually height and max info text width
pum_adjust_info_position(curwin, lnum, max_info_width); pum_adjust_info_position(curwin, max_info_width);
} }
if ((curwin != curwin_save && win_valid(curwin_save)) if ((curwin != curwin_save && win_valid(curwin_save))

View File

@@ -1851,15 +1851,16 @@ describe('builtin popupmenu', function()
end) end)
describe('floating window preview popup', function() describe('floating window preview popup', function()
it('pum popup preview', function() before_each(function()
--row must > 10 --row must > 10
screen:try_resize(40, 11) screen:try_resize(40, 11)
exec([[ exec([[
let g:list = [#{word: "one", info: "1info"}, #{word: "two", info: "2info"}, #{word: "looooooooooooooong"}]
funct Omni_test(findstart, base) funct Omni_test(findstart, base)
if a:findstart if a:findstart
return col(".") - 1 return col(".") - 1
endif endif
return [#{word: "one", info: "1info"}, #{word: "two", info: "2info"}, #{word: "looooooooooooooong"}] return g:list
endfunc endfunc
set omnifunc=Omni_test set omnifunc=Omni_test
set completeopt=menu,popup set completeopt=menu,popup
@@ -1886,28 +1887,42 @@ describe('builtin popupmenu', function()
autocmd! Group autocmd! Group
autocmd CompleteChanged * call TsHl() autocmd CompleteChanged * call TsHl()
endfunc endfunc
funct Append_multipe()
call extend(g:list, [#{word: "for .. ipairs", info: "```lua\nfor index, value in ipairs(t) do\n\t\nend\n```"}])
endfunc
]]) ]])
end)
it('pum popup preview', function()
feed('Gi<C-x><C-o>') feed('Gi<C-x><C-o>')
--floating preview in right --floating preview in right
if multigrid then if multigrid then
screen:expect { screen:expect({
grid = [[ grid = [[
## grid 1 ## grid 1
[2:----------------------------------------]|*10 [2:----------------------------------------]|*10
[3:----------------------------------------]| [3:----------------------------------------]|
## grid 2 ## grid 2
one^ | one^ |
{1:~ }|*9 {1:~ }|*9
## grid 3 ## grid 3
{2:-- }{5:match 1 of 3} | {2:-- }{5:match 1 of 3} |
## grid 4 ## grid 4
{n:1info}| {n:1info}|
{n: }| ## grid 5
## grid 5 {s:one }|
{s:one }| {n:two }|
{n:two }| {n:looooooooooooooong }|
{n:looooooooooooooong }| ]],
]], win_pos = {
[2] = {
height = 10,
startcol = 0,
startrow = 0,
width = 40,
win = 1000,
},
},
float_pos = { float_pos = {
[5] = { -1, 'NW', 2, 1, 0, false, 100 }, [5] = { -1, 'NW', 2, 1, 0, false, 100 },
[4] = { 1001, 'NW', 1, 1, 19, false, 50 }, [4] = { 1001, 'NW', 1, 1, 19, false, 50 },
@@ -1925,65 +1940,7 @@ describe('builtin popupmenu', function()
[4] = { [4] = {
win = 1001, win = 1001,
topline = 0, topline = 0,
botline = 2, botline = 1,
curline = 0,
curcol = 0,
linecount = 1,
sum_scroll_delta = 0,
},
},
}
else
screen:expect {
grid = [[
one^ |
{s:one }{n:1info}{1: }|
{n:two }{1: }|
{n:looooooooooooooong }{1: }|
{1:~ }|*6
{2:-- }{5:match 1 of 3} |
]],
}
end
-- delete one character make the pum width smaller than before
-- info window position should be adjusted when popupmenu width changed
feed('<BS>')
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*10
[3:----------------------------------------]|
## grid 2
on^ |
{1:~ }|*9
## grid 3
{2:-- }{5:match 1 of 3} |
## grid 4
{n:1info}|
{n: }|
## grid 5
{s:one }|
]],
float_pos = {
[5] = { -1, 'NW', 2, 1, 0, false, 100 },
[4] = { 1001, 'NW', 1, 1, 15, false, 50 },
},
win_viewport = {
[2] = {
win = 1000,
topline = 0,
botline = 2,
curline = 0,
curcol = 2,
linecount = 1,
sum_scroll_delta = 0,
},
[4] = {
win = 1001,
topline = 0,
botline = 2,
curline = 0, curline = 0,
curcol = 0, curcol = 0,
linecount = 1, linecount = 1,
@@ -2008,15 +1965,92 @@ describe('builtin popupmenu', function()
}, },
}) })
else else
screen:expect([[
one^ |
{s:one }{n:1info}{1: }|
{n:two }{1: }|
{n:looooooooooooooong }{1: }|
{1:~ }|*6
{2:-- }{5:match 1 of 3} |
]])
end
-- delete one character make the pum width smaller than before
-- info window position should be adjusted when popupmenu width changed
feed('<BS>')
if multigrid then
screen:expect({ screen:expect({
grid = [[ grid = [[
## grid 1
[2:----------------------------------------]|*10
[3:----------------------------------------]|
## grid 2
on^ | on^ |
{s:one }{n:1info}{1: }| {1:~ }|*9
{1:~ }{n: }{1: }| ## grid 3
{1:~ }|*7
{2:-- }{5:match 1 of 3} | {2:-- }{5:match 1 of 3} |
## grid 4
{n:1info}|
## grid 5
{s:one }|
]], ]],
win_pos = {
[2] = {
height = 10,
startcol = 0,
startrow = 0,
width = 40,
win = 1000,
},
},
float_pos = {
[5] = { -1, 'NW', 2, 1, 0, false, 100 },
[4] = { 1001, 'NW', 1, 1, 15, false, 50 },
},
win_viewport = {
[2] = {
win = 1000,
topline = 0,
botline = 2,
curline = 0,
curcol = 2,
linecount = 1,
sum_scroll_delta = 0,
},
[4] = {
win = 1001,
topline = 0,
botline = 1,
curline = 0,
curcol = 0,
linecount = 1,
sum_scroll_delta = 0,
},
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000,
},
[4] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1001,
},
},
}) })
else
screen:expect([[
on^ |
{s:one }{n:1info}{1: }|
{1:~ }|*8
{2:-- }{5:match 1 of 3} |
]])
end end
-- when back to original the preview float should be closed. -- when back to original the preview float should be closed.
@@ -2034,7 +2068,6 @@ describe('builtin popupmenu', function()
{2:-- }{8:Back at original} | {2:-- }{8:Back at original} |
## grid 4 (hidden) ## grid 4 (hidden)
{n:1info}| {n:1info}|
{n: }|
## grid 5 ## grid 5
{n:one }| {n:one }|
]], ]],
@@ -2063,7 +2096,7 @@ describe('builtin popupmenu', function()
[4] = { [4] = {
win = 1001, win = 1001,
topline = 0, topline = 0,
botline = 2, botline = 1,
curline = 0, curline = 0,
curcol = 0, curcol = 0,
linecount = 1, linecount = 1,
@@ -2097,9 +2130,11 @@ describe('builtin popupmenu', function()
]], ]],
}) })
end end
feed('<C-E><ESC>')
end)
-- test nvim__complete_set_info it('nvim__set_complete', function()
feed('<ESC>S<C-X><C-O><C-N><C-N>') feed('S<C-X><C-O><C-N><C-N>')
if multigrid then if multigrid then
screen:expect({ screen:expect({
grid = [[ grid = [[
@@ -2111,13 +2146,12 @@ describe('builtin popupmenu', function()
{1:~ }|*9 {1:~ }|*9
## grid 3 ## grid 3
{2:-- }{5:match 3 of 3} | {2:-- }{5:match 3 of 3} |
## grid 4
{n:3info}|
## grid 5 ## grid 5
{n:one }| {n:one }|
{n:two }| {n:two }|
{s:looooooooooooooong }| {s:looooooooooooooong }|
## grid 6
{n:3info}|
{n: }|
]], ]],
win_pos = { win_pos = {
[2] = { [2] = {
@@ -2130,7 +2164,7 @@ describe('builtin popupmenu', function()
}, },
float_pos = { float_pos = {
[5] = { -1, 'NW', 2, 1, 0, false, 100 }, [5] = { -1, 'NW', 2, 1, 0, false, 100 },
[6] = { 1002, 'NW', 1, 1, 19, false, 50 }, [4] = { 1001, 'NW', 1, 1, 19, false, 50 },
}, },
win_viewport = { win_viewport = {
[2] = { [2] = {
@@ -2142,10 +2176,10 @@ describe('builtin popupmenu', function()
linecount = 1, linecount = 1,
sum_scroll_delta = 0, sum_scroll_delta = 0,
}, },
[6] = { [4] = {
win = 1002, win = 1001,
topline = 0, topline = 0,
botline = 2, botline = 1,
curline = 0, curline = 0,
curcol = 0, curcol = 0,
linecount = 1, linecount = 1,
@@ -2160,30 +2194,29 @@ describe('builtin popupmenu', function()
top = 0, top = 0,
win = 1000, win = 1000,
}, },
[6] = { [4] = {
bottom = 0, bottom = 0,
left = 0, left = 0,
right = 0, right = 0,
top = 0, top = 0,
win = 1002, win = 1001,
}, },
}, },
}) })
else else
screen:expect { screen:expect([[
grid = [[
looooooooooooooong^ | looooooooooooooong^ |
{n:one 3info}{1: }| {n:one 3info}{1: }|
{n:two }{1: }| {n:two }{1: }|
{s:looooooooooooooong }{1: }| {s:looooooooooooooong }{1: }|
{1:~ }|*6 {1:~ }|*6
{2:-- }{5:match 3 of 3} | {2:-- }{5:match 3 of 3} |
]], ]])
}
end end
feed('<C-E><ESC>')
end)
-- preview in left it('popup preview place in left', function()
feed('<ESC>cc')
insert(('test'):rep(5)) insert(('test'):rep(5))
feed('i<C-x><C-o>') feed('i<C-x><C-o>')
if multigrid then if multigrid then
@@ -2193,17 +2226,16 @@ describe('builtin popupmenu', function()
[2:----------------------------------------]|*10 [2:----------------------------------------]|*10
[3:----------------------------------------]| [3:----------------------------------------]|
## grid 2 ## grid 2
itesttesttesttesttesone^t | testtesttesttesttesone^t |
{1:~ }|*9 {1:~ }|*9
## grid 3 ## grid 3
{2:-- }{5:match 1 of 3} | {2:-- }{5:match 1 of 3} |
## grid 4
{n:1info}|
## grid 5 ## grid 5
{s: one }| {s: one }|
{n: two }| {n: two }|
{n: looooooooooooooong }| {n: looooooooooooooong }|
## grid 7
{n:1info}|
{n: }|
]], ]],
win_pos = { win_pos = {
[2] = { [2] = {
@@ -2215,8 +2247,8 @@ describe('builtin popupmenu', function()
}, },
}, },
float_pos = { float_pos = {
[7] = { 1003, 'NW', 1, 1, 14, false, 50 }, [5] = { -1, 'NW', 2, 1, 18, false, 100 },
[5] = { -1, 'NW', 2, 1, 19, false, 100 }, [4] = { 1001, 'NW', 1, 1, 13, false, 50 },
}, },
win_viewport = { win_viewport = {
[2] = { [2] = {
@@ -2224,14 +2256,14 @@ describe('builtin popupmenu', function()
topline = 0, topline = 0,
botline = 2, botline = 2,
curline = 0, curline = 0,
curcol = 23, curcol = 22,
linecount = 1, linecount = 1,
sum_scroll_delta = 0, sum_scroll_delta = 0,
}, },
[7] = { [4] = {
win = 1003, win = 1001,
topline = 0, topline = 0,
botline = 2, botline = 1,
curline = 0, curline = 0,
curcol = 0, curcol = 0,
linecount = 1, linecount = 1,
@@ -2246,30 +2278,29 @@ describe('builtin popupmenu', function()
top = 0, top = 0,
win = 1000, win = 1000,
}, },
[7] = { [4] = {
bottom = 0, bottom = 0,
left = 0, left = 0,
right = 0, right = 0,
top = 0, top = 0,
win = 1003, win = 1001,
}, },
}, },
}) })
else else
screen:expect { screen:expect([[
grid = [[ testtesttesttesttesone^t |
itesttesttesttesttesone^t | {1:~ }{n:1info}{s: one }{1: }|
{1:~ }{n:1info}{s: one }{1: }| {1:~ }{n: two }{1: }|
{1:~ }{n: two }{1: }| {1:~ }{n: looooooooooooooong }{1: }|
{1:~ }{n: looooooooooooooong }{1: }|
{1:~ }|*6 {1:~ }|*6
{2:-- }{5:match 1 of 3} | {2:-- }{5:match 1 of 3} |
]], ]])
}
end end
feed('<C-E><Esc>') feed('<C-E><Esc>')
end)
-- works when scroll with treesitter highlight it('works when scroll with treesitter highlight', function()
command('call TestTs()') command('call TestTs()')
feed('S<C-x><C-o>') feed('S<C-x><C-o>')
if multigrid then if multigrid then
@@ -2283,17 +2314,16 @@ describe('builtin popupmenu', function()
{1:~ }|*9 {1:~ }|*9
## grid 3 ## grid 3
{2:-- }{5:match 1 of 3} | {2:-- }{5:match 1 of 3} |
## grid 5 ## grid 4
{s:one }|
{n:two }|
{n:looooooooooooooong }|
## grid 8
{n:```lua }| {n:```lua }|
{n:function test()}| {n:function test()}|
{n: print('foo') }| {n: print('foo') }|
{n:end }| {n:end }|
{n:``` }| {n:``` }|
{n: }| ## grid 5
{s:one }|
{n:two }|
{n:looooooooooooooong }|
]], ]],
win_pos = { win_pos = {
[2] = { [2] = {
@@ -2306,7 +2336,7 @@ describe('builtin popupmenu', function()
}, },
float_pos = { float_pos = {
[5] = { -1, 'NW', 2, 1, 0, false, 100 }, [5] = { -1, 'NW', 2, 1, 0, false, 100 },
[8] = { 1004, 'NW', 1, 1, 19, false, 50 }, [4] = { 1001, 'NW', 1, 1, 19, false, 50 },
}, },
win_viewport = { win_viewport = {
[2] = { [2] = {
@@ -2318,10 +2348,10 @@ describe('builtin popupmenu', function()
linecount = 1, linecount = 1,
sum_scroll_delta = 0, sum_scroll_delta = 0,
}, },
[8] = { [4] = {
win = 1004, win = 1001,
topline = 0, topline = 0,
botline = 6, botline = 5,
curline = 0, curline = 0,
curcol = 0, curcol = 0,
linecount = 5, linecount = 5,
@@ -2336,30 +2366,127 @@ describe('builtin popupmenu', function()
top = 0, top = 0,
win = 1000, win = 1000,
}, },
[8] = { [4] = {
bottom = 0, bottom = 0,
left = 0, left = 0,
right = 0, right = 0,
top = 0, top = 0,
win = 1004, win = 1001,
}, },
}, },
}) })
else else
screen:expect([[
one^ |
{s:one }{n:```lua }{1: }|
{n:two function test()}{1: }|
{n:looooooooooooooong print('foo') }{1: }|
{1:~ }{n:end }{1: }|
{1:~ }{n:``` }{1: }|
{1:~ }|*4
{2:-- }{5:match 1 of 3} |
]])
end
feed('<C-E><ESC>')
end)
it('#avoid modified original info text', function()
screen:try_resize(40, 11)
command('call Append_multipe()')
feed('S<C-x><C-o><C-P><C-P>')
if multigrid then
screen:expect({ screen:expect({
grid = [[ grid = [[
one^ | ## grid 1
{s:one }{n:```lua }{1: }| [2:----------------------------------------]|*10
{n:two function test()}{1: }| [3:----------------------------------------]|
{n:looooooooooooooong print('foo') }{1: }| ## grid 2
{1:~ }{n:end }{1: }| for .. ipairs^ |
{1:~ }{n:``` }{1: }| {1:~ }|*9
{1:~ }{n: }{1: }| ## grid 3
{1:~ }|*3 {2:-- }{5:match 1 of 4} |
{2:-- }{5:match 1 of 3} | ## grid 4
{n:one }|
{n:two }|
{n:looooooooooooooong }|
{s:for .. ipairs }|
## grid 5
{n:```lua }|
{n:for index, value in }|
{n:ipairs(t) do }|
{n: }|
{n:end }|
{n:``` }|
]], ]],
win_pos = {
[2] = {
height = 10,
startcol = 0,
startrow = 0,
width = 40,
win = 1000,
},
},
float_pos = {
[5] = { 1001, 'NW', 1, 1, 19, false, 50 },
[4] = { -1, 'NW', 2, 1, 0, false, 100 },
},
win_viewport = {
[2] = {
win = 1000,
topline = 0,
botline = 2,
curline = 0,
curcol = 13,
linecount = 1,
sum_scroll_delta = 0,
},
[5] = {
win = 1001,
topline = 0,
botline = 5,
curline = 0,
curcol = 0,
linecount = 5,
sum_scroll_delta = 0,
},
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000,
},
[5] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1001,
},
},
}) })
else
screen:expect([[
for .. ipairs^ |
{n:one ```lua }{1: }|
{n:two for index, value in }{1: }|
{n:looooooooooooooong ipairs(t) do }{1: }|
{s:for .. ipairs }{n: }{1: }|
{1:~ }{n:end }{1: }|
{1:~ }{n:``` }{1: }|
{1:~ }|*3
{2:-- }{5:match 1 of 4} |
]])
end end
feed('<C-N><C-N><C-N><C-N><C-N>')
if not multigrid then
screen:expect_unchanged()
end
feed('<C-E><ESC>')
end) end)
end) end)