From ab82c2c6b087a7740e3e9bc1c02732d2ce3d5511 Mon Sep 17 00:00:00 2001 From: Goldpigg <110080943+GoldPigg@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:35:22 +0800 Subject: [PATCH] feat(extmark): virt_lines can highlight until EOL #41289 --- runtime/doc/api.txt | 4 +- runtime/doc/news.txt | 2 + runtime/lua/vim/_meta/api.gen.lua | 3 +- runtime/lua/vim/lsp/inline_completion.lua | 6 ++ src/nvim/api/extmark.c | 3 +- src/nvim/drawline.c | 14 +++-- test/functional/ui/decorations_spec.lua | 76 +++++++++++++++++++++++ 7 files changed, 101 insertions(+), 7 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index cc497fa5e4..3a1057836b 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3356,7 +3356,9 @@ nvim_buf_set_extmark({buf}, {ns_id}, {line}, {col}, {opts}) 'wrap' and 'linebreak' options do not take effect, so the number of extra screen lines will match the list-size (but see `virt_lines_overflow`). By default lines are placed - below the marked line. + below the marked line. Special case: If the last + `text_chunk` is empty, the corresponding `hl` will extend + to the end of the line. • virt_lines_above: Place virtual lines above instead. • virt_lines_leftcol: Place virtual lines in the leftmost column of the window, bypassing sign and number columns. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index d0154d3e15..30464fd3f7 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -228,6 +228,8 @@ API • |nvim_buf_set_extmark()| `virt_lines_overflow` accepts "wrap" to enable wrapping onto extra rows and "auto" which enables horizontal scrolling when 'nowrap' is set and wrapping when 'wrap' is set. +• |nvim_buf_set_extmark()| `virt_lines` can extend the last `hl` in each `virt_line` + till the end of the line. • |nvim_set_option_value()| accepts a new `operation` field to modify the existing option value. • |nvim_set_option_value()| returns the new option value. diff --git a/runtime/lua/vim/_meta/api.gen.lua b/runtime/lua/vim/_meta/api.gen.lua index 99485c9a92..cd23a3a97c 100644 --- a/runtime/lua/vim/_meta/api.gen.lua +++ b/runtime/lua/vim/_meta/api.gen.lua @@ -671,7 +671,8 @@ function vim.api.nvim_buf_line_count(buf) end --- display of the text, except 'tabstop' is used for hard tabs. The 'wrap' and --- 'linebreak' options do not take effect, so the number of extra screen lines will --- match the list-size (but see `virt_lines_overflow`). By default lines are placed ---- below the marked line. +--- below the marked line. Special case: If the last `text_chunk` is empty, +--- the corresponding `hl` will extend to the end of the line. --- - virt_lines_above: Place virtual lines above instead. --- - virt_lines_leftcol: Place virtual lines in the leftmost column of the window, --- bypassing sign and number columns. diff --git a/runtime/lua/vim/lsp/inline_completion.lua b/runtime/lua/vim/lsp/inline_completion.lua index 88c0817bc1..d016f678ea 100644 --- a/runtime/lua/vim/lsp/inline_completion.lua +++ b/runtime/lua/vim/lsp/inline_completion.lua @@ -247,6 +247,12 @@ function Completor:show(hint) col = col + skip - 1 local virt_lines = { unpack(lines, 2) } + for i, s in ipairs(virt_lines) do + if #s == 1 and s[1][1] == '' then + virt_lines[i] = {} + end + end + api.nvim_buf_set_extmark(self.bufnr, namespace, row, col, { virt_text = virt_text, virt_lines = virt_lines, diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 0f3c77a5c4..b1ade3a0b3 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -457,7 +457,8 @@ ArrayOf(DictAs(get_extmark_item)) nvim_buf_get_extmarks(Buffer buf, Integer ns_i /// display of the text, except 'tabstop' is used for hard tabs. The 'wrap' and /// 'linebreak' options do not take effect, so the number of extra screen lines will /// match the list-size (but see `virt_lines_overflow`). By default lines are placed -/// below the marked line. +/// below the marked line. Special case: If the last `text_chunk` is empty, +/// the corresponding `hl` will extend to the end of the line. /// - virt_lines_above: Place virtual lines above instead. /// - virt_lines_leftcol: Place virtual lines in the leftmost column of the window, /// bypassing sign and number columns. diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index fe8fa0e032..b8399bb647 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -361,7 +361,7 @@ static void draw_virt_text(win_T *wp, buf_T *buf, int col_off, int *end_col, int if (vt) { int vcol = item->draw_col - col_off; int col = draw_virt_text_item(buf, item->draw_col, vt->data.virt_text, - vt->hl_mode, max_col, vcol, 0); + vt->hl_mode, max_col, vcol, 0, false); if (do_eol && ((vt->pos == kVPosEndOfLine) || (vt->pos == kVPosEndOfLineRightAlign))) { state->eol_col = col + 1; } @@ -373,14 +373,20 @@ static void draw_virt_text(win_T *wp, buf_T *buf, int col_off, int *end_col, int } } +/// @param eol_hl Extend highlight to EOL. static int draw_virt_text_item(buf_T *buf, int col, VirtText vt, HlMode hl_mode, int max_col, - int vcol, int skip_cells) + int vcol, int skip_cells, bool eol_hl) { const char *virt_str = ""; int virt_attr = 0; size_t virt_pos = 0; + eol_hl &= kv_size(vt) && *kv_A(vt, kv_size(vt) - 1).text == NUL; while (col < max_col) { + // extending last highlight till the end of line + if (eol_hl && *virt_str == NUL && virt_pos == kv_size(vt)) { + virt_str = " "; + } if (skip_cells >= 0 && *virt_str == NUL) { if (virt_pos >= kv_size(vt)) { break; @@ -2983,7 +2989,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b } if (kv_size(fold_vt) > 0) { - draw_virt_text_item(buf, win_col_offset, fold_vt, kHlModeCombine, view_width, 0, 0); + draw_virt_text_item(buf, win_col_offset, fold_vt, kHlModeCombine, view_width, 0, 0, false); } draw_virt_text(wp, buf, win_col_offset, &wlv.col, wlv.row); // Set increasing virtual columns in grid->vcols[] to set correct curswant @@ -3261,7 +3267,7 @@ end_check: kHlModeReplace, view_width, 0, - virt_line_skip_cells); + virt_line_skip_cells, true); } else if (wlv.filler_todo <= 0) { draw_virt_text(wp, buf, win_col_offset, &draw_col, wlv.row); } diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index b3cda38a9f..38ded1235d 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -7276,6 +7276,82 @@ if (h->n_buckets < new_n_buckets) { // expand ]]) end) + it('highlight till eol with virt_lines_overflow=trunc', function() + insert('line1') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { { { 'VIRT LINE1', 'String' }, { '', 'Visual' } }, { { 'VIRT LINE2', 'String' } } }, + }) + screen:expect([[ + line^1 | + {26:VIRT LINE1}{17: }| + {26:VIRT LINE2} | + {1:~ }|*8 + | + ]]) + end) + + it('highlight till eol with virt_lines_overflow=wrap', function() + insert('line1') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { { { 'VIRT LINE', 'String' }, { string.rep('-', 50), 'Visual' }, { '', 'Visual' } } }, + virt_lines_overflow = 'wrap', + }) + screen:expect([[ + line^1 | + {26:VIRT LINE}{17:-----------------------------------------}| + {17:--------- }| + {1:~ }|*8 + | + ]]) + end) + + it('highlight till eol with virt_lines_overflow=scroll', function() + command('set nowrap') + insert('abcdefghijklmnopqrstuvwxyz') + api.nvim_buf_set_extmark(0, ns, 0, 0, { + virt_lines = { { { 'VIRT LINE', 'String' }, { '', 'Visual' } } }, + virt_lines_overflow = 'scroll', + }) + screen:expect([[ + abcdefghijklmnopqrstuvwxy^z | + {26:VIRT LINE}{17: }| + {1:~ }|*9 + | + ]]) + + feed('zl') + screen:expect([[ + bcdefghijklmnopqrstuvwxy^z | + {26:IRT LINE}{17: }| + {1:~ }|*9 + | + ]]) + + feed('zl') + screen:expect([[ + cdefghijklmnopqrstuvwxy^z | + {26:RT LINE}{17: }| + {1:~ }|*9 + | + ]]) + + feed('6zl') + screen:expect([[ + ijklmnopqrstuvwxy^z | + {26:E}{17: }| + {1:~ }|*9 + | + ]]) + + feed('zl') + screen:expect([[ + jklmnopqrstuvwxy^z | + {17: }| + {1:~ }|*9 + | + ]]) + end) + it('does not show twice if end_row or end_col is specified #18622', function() screen:try_resize(50, 8) insert([[