Merge #40621 fix(ui2)!: legacy 'ruler' inconsistencies

This commit is contained in:
Justin M. Keyes
2026-07-15 08:13:37 -04:00
committed by GitHub
16 changed files with 208 additions and 42 deletions

View File

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

View File

@@ -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, <Esc>: stop paging

View File

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

View File

@@ -5168,6 +5168,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,

View File

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

View File

@@ -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 '' })
@@ -389,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.
@@ -462,6 +470,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

View File

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

View File

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

View File

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

View File

@@ -511,7 +511,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

View File

@@ -7072,6 +7072,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,

View File

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

View File

@@ -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
@@ -549,8 +557,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);
@@ -1464,9 +1472,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 == '(') {

View File

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

View File

@@ -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)<CR>]])
@@ -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 <cmd>echo "foo\nbar" <bar> ls<CR>]])
@@ -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<lt>')
@@ -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('qix<Esc>dwi<C-r>')
screen:expect([[
{18:^"} |
{1:~ }|*12
^R 1,1 All|
^R 1,1 All|
]])
feed('-<Esc>')
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('<BS><Esc>')
-- 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.
@@ -1092,20 +1092,129 @@ 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<CR>')
screen:expect([[
local search_count = [[
{10:^foo} |
bar |
{1:~ }|*11
/foo W [1/1] 1,1 All|
]])
/foo W [1/1] 1,1 All|
]]
screen:expect(search_count)
feed('<C-L>j')
screen:expect([[
{10:foo} |
^bar |
{1:~ }|*11
2,1 All|
2,1 All|
]])
feed('n')
screen:expect(search_count)
command('echo "-"->repeat(&columns)')
screen:expect([[
{10:^foo} |
bar |
{1:~ }|*11
-----------------------------------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('<C-L>')
command('set ruler showcmd | echo "-"->repeat(&columns)')
screen:expect([[
^ |
{1:~ }|*12
-----------------------------------0,0-1 All|
]])
feed('<C-L>')
-- message with multibyte characters
command('echo "∙"->repeat(&columns)')
screen:expect([[
^ |
{1:~ }|*12
∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙0,0-1 All|
]])
feed('<C-L>')
-- message with multicell characters
command('echo "-".."🙂"->repeat(&columns/2-1)')
screen:expect([[
^ |
{1:~ }|*12
-🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂0,0-1 All|
]])
feed('<C-L>')
command('echo "🙂"->repeat(&columns/2)')
screen:expect([[
^ |
{1:~ }|*12
🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂 0,0-1 All|
]])
feed('<C-L>')
-- 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('<C-L>')
command('set cmdheight=2 showbreak=> | redraw | echo "-"->repeat(2*&columns-1)')
screen:expect([[
^ |
{1:~ }|*11
-----------------------------------------------------|
{1:>}----------------------------------0,0-1 All|
]])
command('set showbreak&')
feed('<C-L>')
-- 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('<C-L>')
-- 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('<Esc>')
screen:expect([[
^ |
{1:~ }|*12
-----------------------------------0,0-1 All|
]])
feed('<C-L>')
-- 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('<C-L>')
-- 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)

View File

@@ -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('abcde<cr>12345<esc>')
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('<esc>^')
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('<c-v>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('o<esc>d')
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({