fix(ruler)!: consistent width in last line and statusline

Problem: Traditionally, the ruler in the last line is one cell shorter
than in the statusline, leaving the last cell of the screen blank.
According to code comments, this is in order to prevent unwanted
scrolling on "some" (unspecified, but presumably ancient) terminals.
Berkeley vi is more specific in its `vs_modeline` function: dumb
terminals with hardware scroll, SunOS 4.1.1 and Ultrix 4.2 curses.
(n)curses still has a similar limitation in `(w)addstr`, but apparently
only for historical reasons.

Maintaining the different widths leads to awkward inconsistencies when
the ruler is configured with 'rulerformat', except for the special case
where it contains a top-level `%=`. Shifting the ruler in the last line
to the left would be a solution, but the empty cell at the end doesn't
seem to be relevant anymore.

Solution: extend the ruler in the last line all the way to the right
edge of the screen, just like in the statusline. The exact same amount
of place will be available to the rest of the UI as before.

BREAKING CHANGE:
- the default ruler width is now 18 cells
- the last cell of the screen is no longer empty

Closes #41076
This commit is contained in:
Sébastien Hoffmann
2026-08-01 18:01:00 +02:00
parent 81261804a8
commit 259e6fa9cc
21 changed files with 54 additions and 60 deletions

View File

@@ -163,6 +163,12 @@ TREESITTER
• todo
UI
• The bottom right cell of the screen is no longer left empty when the ruler
is shown. This was originally intended to prevent unwanted scrolling on
quirky terminals.
VIMSCRIPT
• Removed: ctxget(), ctxpop(), ctxpush(), ctxset(), ctxsize(). Use

View File

@@ -5212,7 +5212,7 @@ A jump table for the options with a short description can be found at |Q_op|.
The format of this option is like that of 'statusline'.
This option cannot be set in a modeline when 'modelineexpr' is off.
The default ruler width is 17 characters. To make the ruler 15
The default ruler width is 18 characters. To make the ruler 15
characters wide, put "%15(" at the start and "%)" at the end.
Example: >vim
set rulerformat=%15(%c%V\ %p%%%)

View File

@@ -5403,7 +5403,7 @@ vim.go.ru = vim.go.ruler
--- The format of this option is like that of 'statusline'.
--- This option cannot be set in a modeline when 'modelineexpr' is off.
---
--- The default ruler width is 17 characters. To make the ruler 15
--- The default ruler width is 18 characters. To make the ruler 15
--- characters wide, put "%15(" at the start and "%)" at the end.
--- Example:
---

View File

@@ -1125,7 +1125,7 @@ static void recording_mode(int hl_id)
msg_puts_hl(s, hl_id, false);
}
#define COL_RULER 17 // columns needed by standard ruler
#define COL_RULER 18 // columns needed by standard ruler
/// Compute columns for ruler and shown command. 'sc_col' is also used to
/// decide what the maximum length of a message on the status line can be.
@@ -1138,7 +1138,7 @@ void comp_col(void)
sc_col = 0;
ru_col = 0;
if (p_ru) {
ru_col = (ru_wid ? ru_wid : COL_RULER) + 1;
ru_col = (ru_wid ? ru_wid : COL_RULER);
// no last status line, adjust sc_col
if (!last_has_status) {
sc_col = ru_col;

View File

@@ -7312,7 +7312,7 @@ local options = {
The format of this option is like that of 'statusline'.
This option cannot be set in a modeline when 'modelineexpr' is off.
The default ruler width is 17 characters. To make the ruler 15
The default ruler width is 18 characters. To make the ruler 15
characters wide, put "%15(" at the start and "%)" at the end.
Example: >vim
set rulerformat=%15(%c%V\ %p%%%)

View File

@@ -338,7 +338,6 @@ static void win_redr_stl_expr(win_T *wp, bool draw_winbar, bool draw_ruler, bool
if (!in_status_line) {
row = Rows - 1;
grid = grid_adjust(&msg_grid_adj, &row, &col);
maxwidth--; // writing in last column may cause scrolling
fillchar = schar_from_ascii(' ');
group = HLF_MSG;
}
@@ -557,14 +556,9 @@ void redraw_ruler(void)
(int)virtcol + 1);
// Add a "50%" if there is room for it.
// On the last line, don't print in the last column (scrolls the
// screen up on some terminals).
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 && !ui_has(kUIMessages)) {
n1++; // can't use last char of screen
}
int this_ru_col = ru_col - (Columns - width);
// Never use more than half the window/screen width, leave the other half

View File

@@ -2337,7 +2337,7 @@ describe('API/win', function()
screen:expect([[
│^ |
~ │~ |*4
0,0-1 All |
0,0-1 All|
{5:-- TERMINAL --} |
]])
screen:detach()
@@ -2351,7 +2351,7 @@ describe('API/win', function()
screen:expect([[
^ │ |
~ │~ |*4
0,0-1 All |
0,0-1 All|
{5:-- TERMINAL --} |
]])
end)

View File

@@ -1507,7 +1507,7 @@ describe('completion', function()
{12:hello }{1: }|
{4:hullo }{1: }|
{4:heee }{1: }|
{5:-- INSERT --} 4,6 All |
{5:-- INSERT --} 4,6 All|
]])
end)

View File

@@ -429,7 +429,7 @@ describe('swapfile detection', function()
screen:expect([[
^ |
{1:~ }|*16
{19:W325: Ignoring swapfile from Nvim process }0,0-1 All |
{19:W325: Ignoring swapfile from Nvim process }0,0-1 All|
]])
eq(('\n' .. msg_expected):rep(3):sub(2), n.exec_capture('messages'))
command('bwipe!')

View File

@@ -386,7 +386,7 @@ describe('cmdline', function()
api.nvim_set_option_value('rulerformat', '%!TestRulerFn()', {})
screen:expect([[
^ |
10,20 30% |
10,20 30%|
]])
end)

View File

@@ -263,7 +263,7 @@ describe(':confirm command dialog', function()
fooba^r |
{1:~ }|*5
|
1,6 All |
1,6 All|
]])
eq('foo\n', read_file('Xconfirm_write_ro'))
@@ -377,7 +377,7 @@ describe(':confirm command dialog', function()
d |
{1:~ }|*2
|
1,1 All |
1,1 All|
]])
eq('a\nb\nc\nd\n', read_file('Xwrite_partial'))
os.remove('Xwrite_partial')

View File

@@ -249,7 +249,7 @@ describe('linebreak', function()
{17:foo}^ │{1:~ }|
xxxxxxxxxxxxxxxxxxxx│{1:~ }|
{1:~ }│{1:~ }|*2
{5:-- VISUAL BLOCK --} 2x4 2,4 All |
{5:-- VISUAL BLOCK --} 2x4 2,4 All|
]])
-- TAB as end char: 'linebreak' shouldn't break Visual block hl
@@ -264,7 +264,7 @@ describe('linebreak', function()
f{17:oo12345}bar |
f^o{17:o }bar |
{1:~ }|*2
{5:-- VISUAL BLOCK --} 3x7 3,2 All |
{5:-- VISUAL BLOCK --} 3x7 3,2 All|
]])
feed('<Esc>:setlocal linebreak<CR>gv')
screen:expect_unchanged(true)
@@ -281,7 +281,7 @@ describe('linebreak', function()
f{17:oo123456}bar |
f^o{17:o<ffff>}bar |
{1:~ }|*2
{5:-- VISUAL BLOCK --} 3x8 3,2 All |
{5:-- VISUAL BLOCK --} 3x8 3,2 All|
]])
feed('<Esc>:setlocal linebreak<CR>gv')
screen:expect_unchanged(true)
@@ -303,7 +303,7 @@ describe('linebreak', function()
xx{17:xx}foo: {17:x}xxxxx |
xx{17:xx}bar: ^xxxxxx |
{1:~ }|*2
{5:-- VISUAL BLOCK --} 3x8 3,5-10 All |
{5:-- VISUAL BLOCK --} 3x8 3,5-10 All|
]])
feed('<Esc>:setlocal linebreak<CR>gv')
screen:expect_unchanged(true)

View File

@@ -158,7 +158,7 @@ describe('normal', function()
^4 |
5 |
6 |
40 more lines 5,1 %8 |
40 more lines 5,1 %8|
]])
end)
end)

View File

@@ -31,7 +31,7 @@ it('scrolling with laststatus=0 and :botright split', function()
98 |
99 |
^100 |
100,1 Bot |
100,1 Bot|
]])
end)

View File

@@ -821,7 +821,7 @@ describe('TUI :restart', function()
screen:expect([[
^ |
~ |*4
0,0-1 All |
0,0-1 All|
{5:-- TERMINAL --} |
]])
@@ -833,7 +833,7 @@ describe('TUI :restart', function()
0002;<control>;Cc;0;BN;;;;;N;START OF TEXT;;;; |
0003;<control>;Cc;0;BN;;;;;N;END OF TEXT;;;; |
0004;<control>;Cc;0;BN;;;;;N;END OF TRANSMISSION;;|
TRIGGERED: 1 1,1 Top |
TRIGGERED: 1 1,1 Top|
{5:-- TERMINAL --} |
]])

View File

@@ -423,7 +423,7 @@ describe(':terminal window', function()
cool line 8 |
cool line 9 |
cool line 10 |
{5:-- TERMINAL --} 6,1 Bot |
{5:-- TERMINAL --} 6,1 Bot|
]])
command('call nvim_win_set_cursor(0, [1, 0])')
screen:expect_unchanged()
@@ -436,7 +436,7 @@ describe(':terminal window', function()
cool line 8 |
cool line 9 |
cool line 10 |
{5:-- TERMINAL --} 7,5 Bot |
{5:-- TERMINAL --} 7,5 Bot|
]])
-- Check topline correct after leaving terminal mode.
-- The new cursor position is one column left of the terminal's actual cursor position.
@@ -448,7 +448,7 @@ describe(':terminal window', function()
cool line 8 |
cool line 9 |
cool line 10 |
7,4 Bot |
7,4 Bot|
]])
end)

View File

@@ -3578,7 +3578,7 @@ describe('float window', function()
|
{0:~ }|*5
## grid 3
1,1 All |
1,1 All|
## grid 4
{1:^aaa aab }|
{1:abb acc }|
@@ -3597,7 +3597,7 @@ describe('float window', function()
{0:~ }{1:abb acc }{0: }|
{0:~ }{2:~ }{0: }|
{0:~ }|*3
1,1 All |
1,1 All|
]],
}
end
@@ -3613,7 +3613,7 @@ describe('float window', function()
|
{0:~ }|*5
## grid 3
1,5 All |
1,5 All|
## grid 4
{1:aaa ^aab }|
{1:abb acc }|
@@ -3632,7 +3632,7 @@ describe('float window', function()
{0:~ }{1:abb acc }{0: }|
{0:~ }{2:~ }{0: }|
{0:~ }|*3
1,5 All |
1,5 All|
]],
}
end
@@ -3692,7 +3692,7 @@ describe('float window', function()
|
{0:~ }|*7
## grid 3
0,0-1 All |
0,0-1 All|
## grid 4
{1: }|
{2:~ }|*2
@@ -3716,7 +3716,7 @@ describe('float window', function()
{1: } {1:^ } |
{2:~ }{0: }{2:~ }{0: }|*2
{0:~ }|*5
0,0-1 All |
0,0-1 All|
]],
}
end

View File

@@ -1894,7 +1894,7 @@ describe('ui/builtin messages', function()
screen:expect([[
^ |
{1:~ }|*5
0-1 100% |
0-1 100% |
]])
-- Ruler is cleared when it is no longer drawn.
@@ -1909,14 +1909,14 @@ describe('ui/builtin messages', function()
screen:expect([[
^ |
{1:~ }|*5
0,0-1 All |
0,0-1 All|
]])
command('hi MsgArea guibg=#333333')
screen:expect([[
^ |
{1:~ }|*5
{101: 0,0-1 All }|
{101: 0,0-1 All}|
]])
end)

View File

@@ -303,35 +303,35 @@ describe('multibyte rendering', function()
screen:expect([[
^🏳️‍⚧️ |
{1:~ }|*4
1,1 All |
1,1 All|
]])
feed('a word<esc>')
screen:expect([[
🏳️‍⚧️ wor^d |
{1:~ }|*4
1,21-7 All |
1,21-7 All|
]])
feed('0')
screen:expect([[
^🏳️‍⚧️ word |
{1:~ }|*4
1,1 All |
1,1 All|
]])
feed('l')
screen:expect([[
🏳️‍⚧️^ word |
{1:~ }|*4
1,17-3 All |
1,17-3 All|
]])
feed('h')
screen:expect([[
^🏳️‍⚧️ word |
{1:~ }|*4
1,1 All |
1,1 All|
]])
feed('o❤ variant selected<esc>')
@@ -339,7 +339,7 @@ describe('multibyte rendering', function()
🏳️‍⚧️ word |
❤️ variant selecte^d |
{1:~ }|*3
2,23-19 All |
2,23-19 All|
]])
feed('0')
@@ -347,7 +347,7 @@ describe('multibyte rendering', function()
🏳️‍⚧️ word |
^❤️ variant selected |
{1:~ }|*3
2,1 All |
2,1 All|
]])
feed('l')
@@ -355,7 +355,7 @@ describe('multibyte rendering', function()
🏳️‍⚧️ word |
❤️^ variant selected |
{1:~ }|*3
2,7-3 All |
2,7-3 All|
]])
feed('h')
@@ -363,7 +363,7 @@ describe('multibyte rendering', function()
🏳️‍⚧️ word |
^❤️ variant selected |
{1:~ }|*3
2,1 All |
2,1 All|
]])
-- without selector: single width (note column 18 and not 19)
@@ -373,7 +373,7 @@ describe('multibyte rendering', function()
❤️ variant selected |
❤ variant selecte^d |
{1:~ }|*2
3,20-18 All |
3,20-18 All|
]])
end)
end)

View File

@@ -366,7 +366,7 @@ local function screen_tests(linegrid)
0123^456 |
789 |
{1:~ }|*11
1,5 All |
1,5 All|
]])
end)
end)

View File

@@ -346,7 +346,7 @@ describe('global statusline', function()
screen:expect([[
^ |
{1:~ }|*14
0,0-1 All |
0,0-1 All|
]])
command('set laststatus=3')
@@ -402,7 +402,7 @@ describe('global statusline', function()
{2:< 0,0-1 All <-1 All <}│{1:~ }|
│{1:~ }|
{1:~ }│{1:~ }|*4
0,0-1 All |
0,0-1 All|
]])
command('set laststatus=3')
@@ -816,16 +816,10 @@ describe('statusline', function()
{1:~}{15:^ }{1: }|
{1:~ }|*4
{2:[No Name] 0,0-1 All}|
0,0-1 All |
]])
command('set rulerformat=%17(%l,%c%V%=%P%)')
screen:expect([[
|
{1:~}{15:^ }{1: }|
{1:~ }|*4
{2:[No Name] 0,0-1 All}|
0,0-1 All |
0,0-1 All|
]])
command('set rulerformat=%18(%l,%c%V%=%P%)')
screen:expect_unchanged()
command('set rulerformat&')
api.nvim_win_close(win, true)
screen:expect([[