From c5be3315027de331bc201c3a4bb69180e33f6360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Sun, 28 Jun 2026 14:40:33 +0200 Subject: [PATCH 1/8] fix(ui2)!: don't constrain ruler to ru_wid, fix leading item group Problem: ui2's ruler is no longer of fixed width, and the internal width variable `ru_wid` now acts as an arbitrary maximum, which is unnecessary because the same can now be achieved transparently with an item group. Furthermore, the old way of setting `ru_wid` with an overloaded subset of the item group syntax leads to a number of inconsistencies. In particular, when the configured ruler expression is reused for including the ruler into the statusline, where the item group syntax is interpreted in the usual way. The fact that this width is set in the `minwid` position is more confusing now that it is a de-facto maximum. Solution: for ui2, constrain the ruler only by half the screen width, which previously was an additional limit relevant on small screens. Skip the special-case treatment of the ruler's leading item group spec, process it like a normal item group. Example: the current fallback ruler expression in the default statusline now actually works for `rulerformat` itself: set rulerformat=%-14.(%l,%c%V%)\ %P because the result can naturally expand to 18 cells instead of being truncated to the default 17 cells, and removing the period after `minwid` doesn't change the result, whereas before, it would unhelpfully lead to setting the fixed ruler width to 14 cells. BREAKING CHANGE: if the user used to set the ruler's fixed width, it will now be treated as minimum instead of a maximum width, which in practice increases backwards-compatibility with ui1 and vim. The main remaining difference is that item groups are right-aligned by default, but backwards-compatibility can be achieved by setting a negative `minwid`, which ui1 and vim ignore. To restore the previous fixed width, specify `maxwid` as well, but this does not work in ui1 and vim. If `rulerformat` contains a `%=` without enclosing item group, it will now expand to fill half the screen width. But in that case, it will already spread out similarly in the new expression-based statusline, so consistency of the ruler in both positions is increased. To simulate the previous default behaviour, wrap it in `%-17.17(...%)`. For best compatibility and reuse with vim, leave out `maxwid` and make `minwid` negative, e.g. `%-20(...%)`. fix https://github.com/neovim/neovim/issues/40247 --- runtime/doc/news.txt | 6 +++++- runtime/doc/options.txt | 7 +++++++ runtime/lua/vim/_meta/options.gen.lua | 7 +++++++ src/nvim/options.lua | 7 +++++++ src/nvim/optionstr.c | 7 ++++--- src/nvim/statusline.c | 10 +++++++++- 6 files changed, 39 insertions(+), 5 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 9a2cdbbba5..96c20ea290 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -44,7 +44,11 @@ DIAGNOSTICS OPTIONS -• todo +• When |ui2| is enabled, since the ruler is no longer a left-aligned box of + fixed width, there is no longer an inconsistent special handling of item + groups in 'rulerformat' to configure that fixed width. An old-style left- + aligned fixed-width ruler can now be achieved by using item groups exactly + as they are documented, e.g. `%-20.20(...%)`. TREESITTER diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index d3a1923d6f..6612af2fbb 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -5169,6 +5169,13 @@ A jump table for the options with a short description can be found at |Q_op|. Example: >vim set rulerformat=%15(%c%V\ %p%%%) < + This looks like an item group, but there are some differences in this + particular case. Most notably, the width is fixed and not a minimum, + and the ruler is left-aligned, whereas the alignment of item groups is + configurable and right-aligned by default. + + When |ui2| is enabled, the ruler no longer has a fixed width and the + item group syntax has no special meaning for 'rulerformat'. *'runtimepath'* *'rtp'* *vimfiles* 'runtimepath' 'rtp' string (default "$XDG_CONFIG_HOME/nvim, diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua index e7296cea5d..7b76ddbdc4 100644 --- a/runtime/lua/vim/_meta/options.gen.lua +++ b/runtime/lua/vim/_meta/options.gen.lua @@ -5380,6 +5380,13 @@ vim.go.ru = vim.go.ruler --- set rulerformat=%15(%c%V\ %p%%%) --- ``` --- +--- This looks like an item group, but there are some differences in this +--- particular case. Most notably, the width is fixed and not a minimum, +--- and the ruler is left-aligned, whereas the alignment of item groups is +--- configurable and right-aligned by default. +--- +--- When `ui2` is enabled, the ruler no longer has a fixed width and the +--- item group syntax has no special meaning for 'rulerformat'. --- --- @type string vim.o.rulerformat = "" diff --git a/src/nvim/options.lua b/src/nvim/options.lua index ebd1e3048d..cd40fa02cc 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -7073,6 +7073,13 @@ local options = { Example: >vim set rulerformat=%15(%c%V\ %p%%%) < + This looks like an item group, but there are some differences in this + particular case. Most notably, the width is fixed and not a minimum, + and the ruler is left-aligned, whereas the alignment of item groups is + configurable and right-aligned by default. + + When |ui2| is enabled, the ruler no longer has a fixed width and the + item group syntax has no special meaning for 'rulerformat'. ]=], full_name = 'rulerformat', modelineexpr = true, diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index 3826f3a5b0..aef2f16eef 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -52,6 +52,7 @@ #include "nvim/tag.h" #include "nvim/terminal.h" #include "nvim/types_defs.h" +#include "nvim/ui.h" #include "nvim/vim_defs.h" #include "nvim/window.h" #include "nvim/winfloat.h" @@ -1917,7 +1918,7 @@ static const char *did_set_statustabline_rulerformat(optset_T *args, bool rulerf win_config_float(win, win->w_config); } - if (rulerformat && *s == '%') { + if (rulerformat && !ui_has(kUIMessages) && *s == '%') { // set ru_wid if 'ruf' starts with "%99(" if (*++s == '-') { // ignore a '-' s++; @@ -1932,8 +1933,8 @@ static const char *did_set_statustabline_rulerformat(optset_T *args, bool rulerf errmsg = check_stl_option(p_ruf); } } - } else if (rulerformat || s[0] != '%' || s[1] != '!') { - // check 'statusline', 'winbar', 'tabline' or 'statuscolumn' + } else if (s[0] != '%' || s[1] != '!') { + // check 'statusline', 'rulerformat', 'winbar', 'tabline' or 'statuscolumn' // only if it doesn't start with "%!" errmsg = check_stl_option(s); } diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index 4913dc9a02..cc2a66cdca 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -308,7 +308,15 @@ static void win_redr_stl_expr(win_T *wp, bool draw_winbar, bool draw_ruler, bool wp->w_status_click_defs = stl_alloc_click_defs(wp->w_status_click_defs, maxwidth, &wp->w_status_click_defs_size); - if (draw_ruler) { + if (draw_ruler && ui_event) { + stl = p_ruf; + opt_idx = kOptRulerformat; + maxwidth = Columns / 2; + if (!in_status_line) { + fillchar = schar_from_ascii(' '); + group = HLF_MSG; + } + } else if (draw_ruler) { stl = p_ruf; opt_idx = kOptRulerformat; // advance past any leading group spec - implicit in ru_col From 60687ccb6af6a93a7cbcca4535a3d65d1cc21eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Tue, 16 Jun 2026 20:12:25 +0200 Subject: [PATCH 2/8] fix(ui2)!: consistent ruler width inside/outside statusline Problem: before ui2, the ruler in the last line had a one cell safety margin on the right because writing in the last column of the last line "scrolls the screen up on some terminals," according to code comments. When the ruler is included in the statusline, it aligns on the left with the normal ruler, but goes all the way to the screen edge on the right, and is therefore one cell longer. Since ui2 doesn't have that padding cell, they don't align anymore. Also, in the future, when the C implementation of the ruler will be replaced with a default expression that will be included in the default statusline (currently `%-14.(%l,%c%V%) %P`), the width will naturally be the same in both places, and the unit tests will need adapting anyway. Solution: when ui2 is active, increase the ruler width by 1. BREAKING CHANGE: the default ruler in ui2 is now 1 cell wider. ref https://github.com/neovim/neovim/issues/40247 --- src/nvim/statusline.c | 4 ++-- test/functional/ui/messages2_spec.lua | 26 +++++++++++++------------- test/functional/ui/messages_spec.lua | 18 +++++++++--------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index cc2a66cdca..93baf16afd 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -553,8 +553,8 @@ void redraw_ruler(void) char rel_pos[RULER_BUF_LEN]; int rel_poslen = get_rel_pos(wp, rel_pos, RULER_BUF_LEN); int n1 = bufferlen + vim_strsize(rel_pos); - if (wp->w_status_height == 0 && !is_stl_global) { // can't use last char of screen - n1++; + if (wp->w_status_height == 0 && !is_stl_global && !ui_has(kUIMessages)) { + n1++; // can't use last char of screen } int this_ru_col = ru_col - (Columns - width); diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index 7a611ebadb..d3018f6727 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -62,7 +62,7 @@ describe('messages2', function() {3: }| ^foo | bar | - 1,1 All| + 1,1 All| ]]) -- Multiple messages in same event loop iteration are appended and shown in full. feed([[q:echo "foo" | echo "bar\nbaz\n"->repeat(&lines)]]) @@ -86,21 +86,21 @@ describe('messages2', function() screen:expect([[ ^ | {1:~ }|*12 - foo 0,0-1 All| + foo 0,0-1 All| ]]) command('echo "foo"') -- Ruler still positioned correctly after dupe message. screen:expect([[ ^ | {1:~ }|*12 - foo(1) 0,0-1 All| + foo(1) 0,0-1 All| ]]) command('echo "foo"') -- Dupe counter increases beyond 1 screen:expect([[ ^ | {1:~ }|*12 - foo(2) 0,0-1 All| + foo(2) 0,0-1 All| ]]) -- No error for ruler virt_text msg_row exceeding buffer length. command([[map Q echo "foo\nbar" ls]]) @@ -117,7 +117,7 @@ describe('messages2', function() screen:expect([[ ^ | {1:~ }|*12 - 0,0-1 All| + 0,0-1 All| ]]) -- g< shows messages from last command feed('g') @@ -128,20 +128,20 @@ describe('messages2', function() ^foo | bar | 1 %a "[No Name]" line 1 | - 1,1 All| + 1,1 All| ]]) -- edit_unputchar() does not clear already updated screen #34515. feed('qixdwi') screen:expect([[ {18:^"} | {1:~ }|*12 - ^R 1,1 All| + ^R 1,1 All| ]]) feed('-') screen:expect([[ ^x | {1:~ }|*12 - 1,1 All| + 1,1 All| ]]) -- Switching tabpage closes expanded cmdline #37659. command('tabnew | echo "foo\nbar"') @@ -158,7 +158,7 @@ describe('messages2', function() {5: + [No Name] }{24: [No Name] }{2: }{24:X}| ^x | {1:~ }|*11 - foo [+1] 1,1 All| + foo [+1] 1,1 All| ]]) -- Don't enter the pager in insert mode. command('tabonly | call nvim_echo([["foo\n"]]->repeat(&lines), 1, {}) | startinsert') @@ -174,7 +174,7 @@ describe('messages2', function() | ^x | {1:~ }|*11 - foo [+14] 2,1 All| + foo [+14] 2,1 All| ]]) feed('') -- First multiline message expands cmdline, additional message updates spill indicator. @@ -192,7 +192,7 @@ describe('messages2', function() screen:expect([[ ^foo | foo |*12 - 1,1 Top| + 1,1 Top| ]]) -- Changing 'laststatus' reveals the global statusline with a pager height -- exceeding the available lines: #38008. @@ -1098,14 +1098,14 @@ describe('messages2', function() {10:^foo} | bar | {1:~ }|*11 - /foo W [1/1] 1,1 All| + /foo W [1/1] 1,1 All| ]]) feed('j') screen:expect([[ {10:foo} | ^bar | {1:~ }|*11 - 2,1 All| + 2,1 All| ]]) end) end) diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua index 89062b9291..7e3928fc35 100644 --- a/test/functional/ui/messages_spec.lua +++ b/test/functional/ui/messages_spec.lua @@ -941,7 +941,7 @@ describe('ui/ext_messages', function() ^ | {1:~ }|*4 ]], - ruler = { { '0,0-1 All', 9, 'MsgArea' } }, + ruler = { { '0,0-1 All', 9, 'MsgArea' } }, }) command('hi clear MsgArea') feed('i') @@ -951,7 +951,7 @@ describe('ui/ext_messages', function() {1:~ }|*4 ]], showmode = { { '-- INSERT --', 5, 'ModeMsg' } }, - ruler = { { '0,1 All', 'MsgArea' } }, + ruler = { { '0,1 All', 'MsgArea' } }, } feed('abcde12345') screen:expect { @@ -960,7 +960,7 @@ describe('ui/ext_messages', function() 1234^5 | {1:~ }|*3 ]], - ruler = { { '2,5 All', 'MsgArea' } }, + ruler = { { '2,5 All', 'MsgArea' } }, } feed('d') screen:expect { @@ -970,7 +970,7 @@ describe('ui/ext_messages', function() {1:~ }|*3 ]], showcmd = { { 'd' } }, - ruler = { { '2,5 All', 'MsgArea' } }, + ruler = { { '2,5 All', 'MsgArea' } }, } feed('^') screen:expect { @@ -979,7 +979,7 @@ describe('ui/ext_messages', function() ^12345 | {1:~ }|*3 ]], - ruler = { { '2,1 All', 'MsgArea' } }, + ruler = { { '2,1 All', 'MsgArea' } }, } feed('k2l') screen:expect({ @@ -990,7 +990,7 @@ describe('ui/ext_messages', function() ]], showmode = { { '-- VISUAL BLOCK --', 5, 'ModeMsg' } }, showcmd = { { '2x3' } }, - ruler = { { '1,3 All', 'MsgArea' } }, + ruler = { { '1,3 All', 'MsgArea' } }, }) feed('od') screen:expect { @@ -1000,7 +1000,7 @@ describe('ui/ext_messages', function() {1:~ }|*3 ]], showcmd = { { 'd' } }, - ruler = { { '2,1 All', 'MsgArea' } }, + ruler = { { '2,1 All', 'MsgArea' } }, } feed('i') screen:expect { @@ -1010,7 +1010,7 @@ describe('ui/ext_messages', function() {1:~ }|*3 ]], showcmd = { { 'di' } }, - ruler = { { '2,1 All', 'MsgArea' } }, + ruler = { { '2,1 All', 'MsgArea' } }, } feed('w') screen:expect { @@ -1019,7 +1019,7 @@ describe('ui/ext_messages', function() ^ | {1:~ }|*3 ]], - ruler = { { '2,0-1 All', 'MsgArea' } }, + ruler = { { '2,0-1 All', 'MsgArea' } }, } command('set rulerformat=Foo%#ErrorMsg#Bar') screen:expect({ From 5b455a8aa03e2091ebfb2b9d762c930f3e00f17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Tue, 16 Jun 2026 21:07:37 +0200 Subject: [PATCH 3/8] feat(statusline): remove arbitrary item group minwid limit - with today's screens, the arbitrary 50 cells limit is no longer as ample as a few decades ago - minwid is effectively constrained by the output buffer size anyway, so accidentally setting huge numbers introduces no noticeable lag - restores some backwards compatibility to ui2 for the edge-case where the ruler width was set to a number > 50 with the item group syntax --- src/nvim/statusline.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index 93baf16afd..21cd747181 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -1467,9 +1467,8 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op } } - // Bound the minimum width at 50. // Make the number negative to denote left alignment of the item - minwid = (minwid > 50 ? 50 : minwid) * (left_align ? -1 : 1); + minwid *= left_align ? -1 : 1; // Denotes the start of a new group if (*fmt_p == '(') { From fcc391d89a2a30b02c3ba25a6351d37cdcd79e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Sun, 5 Jul 2026 11:06:46 +0200 Subject: [PATCH 4/8] fix(ui2): clear showcmd virt_text together with search count Problem: when the search count is displayed, the showcmd virt_text is set to 11 empty spaces to ensure a consistent distance between search count and ruler, or screen edge in case of noruler. But when the search count is removed to make place for a message, the empty dummy showcmd remains and unnecessarily erases part of the message. Solution: properly remove showcmd together with search count. --- runtime/lua/vim/_core/ui2/messages.lua | 1 + test/functional/ui/messages2_spec.lua | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua index 07dc9c205a..1961084c38 100644 --- a/runtime/lua/vim/_core/ui2/messages.lua +++ b/runtime/lua/vim/_core/ui2/messages.lua @@ -462,6 +462,7 @@ function M.msg_show(kind, content, replace_last, _, append, id, trigger) M.cmd.last_emsg = (kind == 'emsg' or kind == 'wmsg') and os.time() or M.cmd.last_emsg -- Should clear the search count now, mark itself is cleared by invalidate. M.virt.last[M.virt.idx.search][1] = nil + M.virt.last[M.virt.idx.cmd] = {} end -- When message was emitted below an already expanded cmdline, move and route to pager. tgt = ui.cmd.expand > 0 and 'pager' or tgt diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index d3018f6727..7fbee747fa 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -1092,14 +1092,15 @@ describe('messages2', function() end) it('search count is cleared', function() - command('set ruler shortmess-=S | call setline(1, ["foo", "bar"])') + command('set ruler showcmd shortmess-=S | call setline(1, ["foo", "bar"])') feed('/foo') - screen:expect([[ + local search_count = [[ {10:^foo} | bar | {1:~ }|*11 /foo W [1/1] 1,1 All| - ]]) + ]] + screen:expect(search_count) feed('j') screen:expect([[ {10:foo} | @@ -1107,5 +1108,14 @@ describe('messages2', function() {1:~ }|*11 2,1 All| ]]) + feed('n') + screen:expect(search_count) + command('echo "-"->repeat(&columns)') + screen:expect([[ + {10:^foo} | + bar | + {1:~ }|*11 + -----------------------------------1,1 All| + ]]) end) end) From 2473cb62b7f4d77e2691d5040e06a2a61ab34462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Sun, 5 Jul 2026 13:55:22 +0200 Subject: [PATCH 5/8] fix(ui2): correctly crop message at multibyte/multicell character --- runtime/lua/vim/_core/ui2/messages.lua | 5 +- test/functional/ui/messages2_spec.lua | 90 ++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua index 1961084c38..4182fd573b 100644 --- a/runtime/lua/vim/_core/ui2/messages.lua +++ b/runtime/lua/vim/_core/ui2/messages.lua @@ -126,7 +126,7 @@ local function set_virttext(type, tgt) }) local row = texth.end_row local col = fn.virtcol2col(win, row + 1, texth.end_vcol) - local scol = fn.screenpos(win, row + 1, col).col ---@type integer + local scol = fn.screenpos(win, row + 1, col).endcol ---@type integer if type ~= 'last' then -- Calculate at which column to place the virt_text such that it is at the end @@ -179,7 +179,8 @@ local function set_virttext(type, tgt) -- Crop text on last screen row and find byte offset to place mark at. local vcol = texth.end_vcol - (scol - M.cmd.last_col) - col = vcol <= 0 and 0 or fn.virtcol2col(win, row + 1, vcol) + col = vcol <= 0 and 0 or fn.virtcol2col(win, row + 1, vcol + 1) - 1 + scol = fn.screenpos(win, row + 1, col).endcol M.cmd.prev_msg = mode > 0 and '' or M.cmd.prev_msg M.virt.cmd = mode > 0 and { {}, {} } or M.virt.cmd api.nvim_buf_set_text(ui.bufs.cmd, row, col, row, -1, { mode > 0 and ' ' or '' }) diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index 7fbee747fa..334b42365d 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -1118,4 +1118,94 @@ describe('messages2', function() -----------------------------------1,1 All| ]]) end) + + it('crops long messages to make place for ruler', function() + command('set noruler | echo "-"->repeat(&columns)') + screen:expect([[ + ^ | + {1:~ }|*12 + -----------------------------------------------------| + ]]) + feed('') + command('set ruler showcmd | echo "-"->repeat(&columns)') + screen:expect([[ + ^ | + {1:~ }|*12 + -----------------------------------0,0-1 All| + ]]) + feed('') + -- message with multibyte characters + command('echo "∙"->repeat(&columns)') + screen:expect([[ + ^ | + {1:~ }|*12 + ∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙0,0-1 All| + ]]) + feed('') + -- message with multicell characters + command('echo "-".."🙂"->repeat(&columns/2-1)') + screen:expect([[ + ^ | + {1:~ }|*12 + -🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂0,0-1 All| + ]]) + feed('') + command('echo "🙂"->repeat(&columns/2)') + screen:expect([[ + ^ | + {1:~ }|*12 + 🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂 0,0-1 All| + ]]) + feed('') + -- when cmdheight>1, only the last line is cropped + command('set cmdheight=2 | redraw | echo "-"->repeat(2*&columns)') + screen:expect([[ + ^ | + {1:~ }|*11 + -----------------------------------------------------| + -----------------------------------0,0-1 All| + ]]) + feed('') + command('set cmdheight=2 showbreak=> | redraw | echo "-"->repeat(2*&columns-1)') + screen:expect([[ + ^ | + {1:~ }|*11 + -----------------------------------------------------| + {1:>}----------------------------------0,0-1 All| + ]]) + command('set showbreak&') + feed('') + -- ruler is not shown after dismissing expanded cmdline with key press -> no need to crop + command('set cmdheight=1 | redraw | echo "-"->repeat(2*&columns)') + feed('k') + screen:expect([[ + ^ | + {1:~ }|*12 + -----------------------------------------------------| + ]]) + feed('') + -- messages are deferred until showcmd is cleared -> no need to crop for showcmd + command('lua vim.defer_fn(function() vim.cmd[[echo "-"->repeat(&columns)]] end, 50)') + feed('g') + screen:expect([[ + ^ | + {1:~ }|*12 + g 0,0-1 All| + ]]) + screen:sleep(100) + feed('') + screen:expect([[ + ^ | + {1:~ }|*12 + -----------------------------------0,0-1 All| + ]]) + feed('') + -- when the ruler is configured smaller, there is more space for messages + command('set rulerformat=%l | redraw | echo "-"->repeat(&columns)') + screen:expect([[ + ^ | + {1:~ }|*12 + ----------------------------------------------------0| + ]]) + end) end) From 0ffd74ae85bf9c6c41c904775ec6ec6d8a3eebb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Sat, 11 Jul 2026 16:38:12 +0200 Subject: [PATCH 6/8] fix(ui2): fix ruler on repeated messages Problem: a long message is cropped to keep the ruler visible. But when the message is repeated, it can be written over the ruler, while the repetition indicator "(1)" is still placed just before the ruler, which ends up somewhere inside the message. Solution: currently, long messages are only cropped when setting the 'last' virttext. Make sure this happens after a repeated long message. --- runtime/lua/vim/_core/ui2/messages.lua | 7 +++++++ test/functional/ui/messages2_spec.lua | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua index 4182fd573b..7a6bd6e4c0 100644 --- a/runtime/lua/vim/_core/ui2/messages.lua +++ b/runtime/lua/vim/_core/ui2/messages.lua @@ -390,6 +390,13 @@ function M.show_msg(tgt, kind, content, replace_last, append, id) M.virt[tgt][M.virt.idx.dupe][1] = dupe > 0 and { 0, ('(%d)'):format(dupe) } or nil M.virt[tgt][M.virt.idx.spill][1] = tgt == 'cmd' and M.virt.cmd[M.virt.idx.spill][1] or nil set_virttext(tgt --[[@as 'cmd'|'msg']], tgt) + if tgt == 'cmd' then + -- make sure repeated long messages are cropped to keep the ruler intact + -- (if this is not scheduled, it breaks messages during textlock) + vim.schedule(function() + set_virttext('last', 'cmd') + end) + end end -- Reset message state the next event loop iteration. diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index 334b42365d..30acb62925 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -1200,6 +1200,15 @@ describe('messages2', function() -----------------------------------0,0-1 All| ]]) feed('') + -- message with repetition indicator keeps ruler intact + command('echo "-"->repeat(&columns)') + command('echo "-"->repeat(&columns)') + screen:expect([[ + ^ | + {1:~ }|*12 + --------------------------------(1)0,0-1 All| + ]]) + feed('') -- when the ruler is configured smaller, there is more space for messages command('set rulerformat=%l | redraw | echo "-"->repeat(&columns)') screen:expect([[ From 935a14bb72f7b3b2da6c653314f6060901ed1f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Wed, 8 Jul 2026 20:23:01 +0200 Subject: [PATCH 7/8] docs(ui2): deprecate v:echospace The purpose of `v:echospace` was to avoid disruptive "Press ENTER" prompts. Since those are no longer a problem with ui2, the complexity of maintaining it up-to-date is no longer justified. --- runtime/doc/deprecated.txt | 5 +++++ runtime/doc/message.txt | 1 - runtime/doc/vvars.txt | 2 +- runtime/lua/vim/_meta/vvars.gen.lua | 2 +- src/nvim/vvars.lua | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt index 158491d8a9..0f82077f46 100644 --- a/runtime/doc/deprecated.txt +++ b/runtime/doc/deprecated.txt @@ -40,6 +40,11 @@ LUA • vim.F.npcall() Renamed to |vim.npcall()| • vim.F.nil_wrap() Use |vim.npcall()| instead +UI + +• |v:echospace| Its purpose to avoid a "Press ENTER" prompt + no longer applies with |ui2|. + LSP • vim.lsp.util.character_offset() Use to |vim.str_utfindex()| instead diff --git a/runtime/doc/message.txt b/runtime/doc/message.txt index 926b5275a0..d2740624de 100644 --- a/runtime/doc/message.txt +++ b/runtime/doc/message.txt @@ -843,7 +843,6 @@ To avoid overflow ("spill") of the |ui2| messages area: - Set 'cmdheight' to 2 or higher. - Add flags to 'shortmess'. - Reset 'showcmd' and/or 'ruler'. -- Make sure `:echo` text is within |v:echospace| screen cells. *more-prompt* *pager* > f/d/j: screen/page/line down, b/u/k: up, : stop paging diff --git a/runtime/doc/vvars.txt b/runtime/doc/vvars.txt index da5323523b..58d5c5d4dd 100644 --- a/runtime/doc/vvars.txt +++ b/runtime/doc/vvars.txt @@ -128,7 +128,7 @@ v:dying v:echospace Number of screen cells that can be used for an `:echo` message in the last screen line before causing the |hit-enter| prompt - (or "overflow" with |ui2|). + (no longer applicable when |ui2| is enabled). Depends on 'showcmd', 'ruler' and 'columns'. You need to check 'cmdheight' for whether there are full-width lines diff --git a/runtime/lua/vim/_meta/vvars.gen.lua b/runtime/lua/vim/_meta/vvars.gen.lua index 3bc819b5a1..0154045f20 100644 --- a/runtime/lua/vim/_meta/vvars.gen.lua +++ b/runtime/lua/vim/_meta/vvars.gen.lua @@ -129,7 +129,7 @@ vim.v.dying = ... --- Number of screen cells that can be used for an `:echo` message --- in the last screen line before causing the `hit-enter` prompt ---- (or "overflow" with `ui2`). +--- (no longer applicable when `ui2` is enabled). --- --- Depends on 'showcmd', 'ruler' and 'columns'. You need to --- check 'cmdheight' for whether there are full-width lines diff --git a/src/nvim/vvars.lua b/src/nvim/vvars.lua index 0cca5124e3..d57c4b4596 100644 --- a/src/nvim/vvars.lua +++ b/src/nvim/vvars.lua @@ -145,7 +145,7 @@ M.vars = { desc = [=[ Number of screen cells that can be used for an `:echo` message in the last screen line before causing the |hit-enter| prompt - (or "overflow" with |ui2|). + (no longer applicable when |ui2| is enabled). Depends on 'showcmd', 'ruler' and 'columns'. You need to check 'cmdheight' for whether there are full-width lines From 5e5cd87aa6a63d2cc38426c2f44e43662717abe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Sat, 11 Jul 2026 15:45:34 +0200 Subject: [PATCH 8/8] fix(ui2): remove obsolete usages of sc_col Like `v:echospace`, `sc_col` was used in many places to avoid disruptive "Press ENTER" prompts. Since those are no longer a problem with ui2, the complexity of keeping these variables up-to-date and maintaining bespoke truncation code at the message producers is no longer justified. Unfortunately, some messages still need to be kept under 1 line to avoid glitches. `ru_wid`, `ru_col`, and `sc_col` are now no longer used when ui2 is enabled, except `ru_col` in the C implementation of the default ruler, which will be replaced by a default expression. --- src/nvim/insexpand.c | 8 +++++++- src/nvim/message.c | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 53c903d8d7..332e1c2261 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -5241,7 +5241,13 @@ void ins_compl_insert(bool move_cursor, bool insert_prefix) static void ins_compl_show_filename(void) { char *const lead = _("match in file"); - int space = sc_col - vim_strsize(lead) - 2; + + // In the case of ext_messages, `sc_col` is obsolete. Fortunately, long messages are no longer + // disruptive, so truncation could be skipped. But in this particular case, with default + // configuration `showmode cmdheight=1`, a multi-line message is shown partially, and a message + // that fits into one line is not shown at all. It's better to be consistent: it should not depend + // on the exact length of the message whether it is displayed at all. + int space = (ui_has(kUIMessages) ? Columns : sc_col) - vim_strsize(lead) - 2; if (space <= 0) { return; } diff --git a/src/nvim/message.c b/src/nvim/message.c index 684ccaeb25..d9f5111633 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -519,7 +519,12 @@ char *msg_strtrunc(const char *s, int force) room = (Rows - msg_row) * Columns - 1; } else { // Use up to 'showcmd' column. - room = (Rows - msg_row - 1) * Columns + sc_col - 1; + // In the case of ui2, we no longer need to avoid the "Press ENTER" prompt, but the message is + // still kept under 1 line to avoid glitches. For example, when a long search term is + // displayed that expands the cmdline, it will be immediately collapsed again when the search + // count is displayed, which leads to flickering on each hit. + int last_row = ui_has(kUIMessages) ? Columns : sc_col - 1; + room = (Rows - msg_row - 1) * Columns + last_row; } if (len > room && room > 0) { // may have up to 18 bytes per cell (6 per char, up to two