fix(marks): make jumpoptions=view work with 'smoothscroll' (#38339)

Problem:  'jumpoptions' "view" doesn't remember skipcol and may lead to
          glitched display with 'smoothscroll'.
Solution: Save skipcol in the mark view. Also make sure skipcol doesn't
          exceed line size.
This commit is contained in:
zeertzjq
2026-03-17 21:52:25 +08:00
committed by GitHub
parent a5b8cf145d
commit 5773f0e994
8 changed files with 171 additions and 101 deletions

View File

@@ -2764,7 +2764,7 @@ void buflist_setfpos(buf_T *const buf, win_T *const win, linenr_T lnum, colnr_T
wip->wi_mark.mark.lnum = lnum; wip->wi_mark.mark.lnum = lnum;
wip->wi_mark.mark.col = col; wip->wi_mark.mark.col = col;
if (win != NULL) { if (win != NULL) {
wip->wi_mark.view = mark_view_make(win->w_topline, wip->wi_mark.mark); wip->wi_mark.view = mark_view_make(win, wip->wi_mark.mark);
} }
} }
if (win != NULL) { if (win != NULL) {

View File

@@ -254,7 +254,7 @@ static void changed_common(buf_T *buf, linenr_T lnum, colnr_T col, linenr_T lnum
if (curwin->w_buffer == buf) { if (curwin->w_buffer == buf) {
if (lnum >= curwin->w_topline && lnum <= curwin->w_botline) { if (lnum >= curwin->w_topline && lnum <= curwin->w_botline) {
view = mark_view_make(curwin->w_topline, curwin->w_cursor); view = mark_view_make(curwin, curwin->w_cursor);
} }
} }
RESET_FMARK(&buf->b_last_change, ((pos_T) { lnum, col, 0 }), buf->handle, view); RESET_FMARK(&buf->b_last_change, ((pos_T) { lnum, col, 0 }), buf->handle, view);

View File

@@ -3087,7 +3087,7 @@ static bool ins_esc(int *count, int cmdchar, bool nomove)
// Remember the last Insert position in the '^ mark. // Remember the last Insert position in the '^ mark.
if ((cmdmod.cmod_flags & CMOD_KEEPJUMPS) == 0) { if ((cmdmod.cmod_flags & CMOD_KEEPJUMPS) == 0) {
fmarkv_T view = mark_view_make(curwin->w_topline, curwin->w_cursor); fmarkv_T view = mark_view_make(curwin, curwin->w_cursor);
RESET_FMARK(&curbuf->b_last_insert, curwin->w_cursor, curbuf->b_fnum, view); RESET_FMARK(&curbuf->b_last_insert, curwin->w_cursor, curbuf->b_fnum, view);
} }

View File

@@ -41,6 +41,7 @@
#include "nvim/os/time.h" #include "nvim/os/time.h"
#include "nvim/os/time_defs.h" #include "nvim/os/time_defs.h"
#include "nvim/path.h" #include "nvim/path.h"
#include "nvim/plines.h"
#include "nvim/pos_defs.h" #include "nvim/pos_defs.h"
#include "nvim/quickfix.h" #include "nvim/quickfix.h"
#include "nvim/strings.h" #include "nvim/strings.h"
@@ -63,7 +64,7 @@
// Returns OK on success, FAIL if bad name given. // Returns OK on success, FAIL if bad name given.
int setmark(int c) int setmark(int c)
{ {
fmarkv_T view = mark_view_make(curwin->w_topline, curwin->w_cursor); fmarkv_T view = mark_view_make(curwin, curwin->w_cursor);
return setmark_pos(c, &curwin->w_cursor, curbuf->b_fnum, &view); return setmark_pos(c, &curwin->w_cursor, curbuf->b_fnum, &view);
} }
@@ -283,7 +284,7 @@ void setpcmark(void)
curwin->w_jumplistidx = curwin->w_jumplistlen; curwin->w_jumplistidx = curwin->w_jumplistlen;
fm = &curwin->w_jumplist[curwin->w_jumplistlen - 1]; fm = &curwin->w_jumplist[curwin->w_jumplistlen - 1];
fmarkv_T view = mark_view_make(curwin->w_topline, curwin->w_pcmark); fmarkv_T view = mark_view_make(curwin, curwin->w_pcmark);
SET_XFMARK(fm, curwin->w_pcmark, curbuf->b_fnum, view, NULL); SET_XFMARK(fm, curwin->w_pcmark, curbuf->b_fnum, view, NULL);
} }
@@ -691,13 +692,17 @@ void mark_view_restore(fmark_T *fm)
// and this check can prevent restoring mark view in that case. // and this check can prevent restoring mark view in that case.
if (topline >= 1) { if (topline >= 1) {
set_topline(curwin, topline); set_topline(curwin, topline);
curwin->w_skipcol = (fm->view.skipcol > 0
&& !hasFolding(curwin, topline, NULL, NULL)
&& fm->view.skipcol < linetabsize_eol(curwin, topline))
? fm->view.skipcol : 0;
} }
} }
} }
fmarkv_T mark_view_make(linenr_T topline, pos_T pos) fmarkv_T mark_view_make(const win_T *wp, pos_T pos)
{ {
return (fmarkv_T){ pos.lnum - topline }; return (fmarkv_T){ pos.lnum - wp->w_topline, wp->w_skipcol };
} }
/// Search for the next named mark in the current file from a start position. /// Search for the next named mark in the current file from a start position.

View File

@@ -74,9 +74,10 @@ typedef enum {
typedef struct { typedef struct {
linenr_T topline_offset; ///< Amount of lines from the mark lnum to the top of the window. linenr_T topline_offset; ///< Amount of lines from the mark lnum to the top of the window.
///< Use MAXLNUM to indicate that the mark does not have a view. ///< Use MAXLNUM to indicate that the mark does not have a view.
colnr_T skipcol;
} fmarkv_T; } fmarkv_T;
#define INIT_FMARKV { MAXLNUM } #define INIT_FMARKV { MAXLNUM, 0 }
/// Structure defining single local mark /// Structure defining single local mark
typedef struct { typedef struct {

View File

@@ -534,7 +534,7 @@ void do_tag(char *tag, int type, int count, int forceit, bool verbose)
if (save_pos) { if (save_pos) {
tagstack[tagstackidx].fmark.mark = curwin->w_cursor; tagstack[tagstackidx].fmark.mark = curwin->w_cursor;
tagstack[tagstackidx].fmark.fnum = curbuf->b_fnum; tagstack[tagstackidx].fmark.fnum = curbuf->b_fnum;
tagstack[tagstackidx].fmark.view = mark_view_make(curwin->w_topline, curwin->w_cursor); tagstack[tagstackidx].fmark.view = mark_view_make(curwin, curwin->w_cursor);
} }
// Curwin will change in the call to jumpto_tag() if ":stag" was // Curwin will change in the call to jumpto_tag() if ":stag" was

View File

@@ -55,44 +55,39 @@ describe('jumplist', function()
write_file(fname1, ('foobar\n'):rep(100)) write_file(fname1, ('foobar\n'):rep(100))
write_file(fname2, 'baz') write_file(fname2, 'baz')
local screen = Screen.new(5, 25) local screen = Screen.new(12, 25)
command('set number') command('set number')
command('edit ' .. fname1) command('edit ' .. fname1)
feed('35gg') feed('35gg')
command('edit ' .. fname2) command('edit ' .. fname2)
feed('<C-O>') feed('<C-O>')
screen:expect { screen:expect([[
grid = [[ {8: 24 }foobar |
{1: 24 }foobar | {8: 25 }foobar |
{1: 25 }foobar | {8: 26 }foobar |
{1: 26 }foobar | {8: 27 }foobar |
{1: 27 }foobar | {8: 28 }foobar |
{1: 28 }foobar | {8: 29 }foobar |
{1: 29 }foobar | {8: 30 }foobar |
{1: 30 }foobar | {8: 31 }foobar |
{1: 31 }foobar | {8: 32 }foobar |
{1: 32 }foobar | {8: 33 }foobar |
{1: 33 }foobar | {8: 34 }foobar |
{1: 34 }foobar | {8: 35 }^foobar |
{1: 35 }^foobar | {8: 36 }foobar |
{1: 36 }foobar | {8: 37 }foobar |
{1: 37 }foobar | {8: 38 }foobar |
{1: 38 }foobar | {8: 39 }foobar |
{1: 39 }foobar | {8: 40 }foobar |
{1: 40 }foobar | {8: 41 }foobar |
{1: 41 }foobar | {8: 42 }foobar |
{1: 42 }foobar | {8: 43 }foobar |
{1: 43 }foobar | {8: 44 }foobar |
{1: 44 }foobar | {8: 45 }foobar |
{1: 45 }foobar | {8: 46 }foobar |
{1: 46 }foobar | {8: 47 }foobar |
{1: 47 }foobar |
| |
]], ]])
attr_ids = {
[1] = { foreground = Screen.colors.Brown },
},
}
end) end)
end) end)
@@ -384,65 +379,134 @@ describe('jumpoptions=view', function()
end) end)
it('restores the view', function() it('restores the view', function()
local screen = Screen.new(5, 8) local screen = Screen.new(12, 8)
command('edit ' .. file1) command('edit ' .. file1)
feed('12Gztj') feed('12Gztj')
feed('gg<C-o>') feed('gg<C-o>')
screen:expect([[ screen:expect([[
12 line | 12 line |
^13 line | ^13 line |
14 line | 14 line |
15 line | 15 line |
16 line | 16 line |
17 line | 17 line |
18 line | 18 line |
| |
]])
end)
it("restores the view with 'smoothscroll'", function()
local screen = Screen.new(12, 8)
command('edit ' .. file1)
command('setlocal smoothscroll')
command([[call setline(24, repeat('a', 55))]])
feed('12Gzz')
local s1 = [[
9 line |
10 line |
11 line |
^12 line |
13 line |
14 line |
15 line |
|
]]
screen:expect(s1)
feed('G')
local s2 = [[
{1:<<<}aaaa |
25 line |
26 line |
27 line |
28 line |
29 line |
^30 line |
|
]]
screen:expect(s2)
feed('<C-o>')
screen:expect(s1)
feed('<C-i>')
screen:expect(s2)
feed('<C-o>')
screen:expect(s1)
command([[call setline(10, repeat('a', 55))]])
feed('zz')
local s3 = [[
{1:<<<}aaaaaaaaa|
aaaaaaa |
11 line |
^12 line |
13 line |
14 line |
15 line |
|
]]
screen:expect(s3)
feed('G')
screen:expect(s2)
feed('<C-o>')
screen:expect(s3)
feed('<C-i>')
screen:expect(s2)
command([[call setline(10, '10 line')]])
feed('<C-o>')
-- Topline should still be line 10, but skipcol should be reset.
screen:expect([[
10 line |
11 line |
^12 line |
13 line |
14 line |
15 line |
16 line |
|
]]) ]])
end) end)
it('restores the view across files', function() it('restores the view across files', function()
local screen = Screen.new(5, 5) local screen = Screen.new(12, 5)
command('args ' .. file1 .. ' ' .. file2) command('args ' .. file1 .. ' ' .. file2)
feed('12Gzt') feed('12Gzt')
command('next') command('next')
feed('G') feed('G')
screen:expect([[ screen:expect([[
27 line | 27 line |
28 line | 28 line |
29 line | 29 line |
^30 line | ^30 line |
| |
]]) ]])
feed('<C-o><C-o>') feed('<C-o><C-o>')
screen:expect([[ screen:expect([[
^12 line | ^12 line |
13 line | 13 line |
14 line | 14 line |
15 line | 15 line |
| |
]]) ]])
end) end)
it('restores the view across files with <C-^>/:bprevious/:bnext', function() it('restores the view across files with <C-^>/:bprevious/:bnext', function()
local screen = Screen.new(5, 5) local screen = Screen.new(12, 5)
command('args ' .. file1 .. ' ' .. file2) command('args ' .. file1 .. ' ' .. file2)
feed('12Gzt') feed('12Gzt')
local s1 = [[ local s1 = [[
^12 line | ^12 line |
13 line | 13 line |
14 line | 14 line |
15 line | 15 line |
| |
]] ]]
screen:expect(s1) screen:expect(s1)
command('next') command('next')
feed('G') feed('G')
local s2 = [[ local s2 = [[
27 line | 27 line |
28 line | 28 line |
29 line | 29 line |
^30 line | ^30 line |
| |
]] ]]
screen:expect(s2) screen:expect(s2)
feed('<C-^>') feed('<C-^>')
@@ -456,7 +520,7 @@ describe('jumpoptions=view', function()
end) end)
it("falls back to standard behavior when view can't be recovered", function() it("falls back to standard behavior when view can't be recovered", function()
local screen = Screen.new(5, 8) local screen = Screen.new(12, 8)
command('edit ' .. file1) command('edit ' .. file1)
feed('7GzbG') feed('7GzbG')
api.nvim_buf_set_lines(0, 0, 2, true, {}) api.nvim_buf_set_lines(0, 0, 2, true, {})
@@ -468,19 +532,19 @@ describe('jumpoptions=view', function()
-- Therefore falls back to standard behavior which is centering the view/line. -- Therefore falls back to standard behavior which is centering the view/line.
feed('<C-o>') feed('<C-o>')
screen:expect([[ screen:expect([[
4 line | 4 line |
5 line | 5 line |
6 line | 6 line |
^7 line | ^7 line |
8 line | 8 line |
9 line | 9 line |
10 line | 10 line |
| |
]]) ]])
end) end)
it('falls back to standard behavior for a mark without a view', function() it('falls back to standard behavior for a mark without a view', function()
local screen = Screen.new(5, 8) local screen = Screen.new(12, 8)
command('edit ' .. file1) command('edit ' .. file1)
feed('10ggzzvwy') feed('10ggzzvwy')
screen:expect([[ screen:expect([[
@@ -524,7 +588,7 @@ describe('jumpoptions=view', function()
end) end)
it('restores the view', function() it('restores the view', function()
local screen = Screen.new(5, 6) local screen = Screen.new(12, 6)
command('set laststatus=2 | set statusline=%f | edit ' .. file1) command('set laststatus=2 | set statusline=%f | edit ' .. file1)
feed('10Gzb<C-]>30Gzt<C-]>') feed('10Gzb<C-]>30Gzt<C-]>')
screen:expect([[ screen:expect([[

View File

@@ -350,7 +350,7 @@ describe('named marks view', function()
end) end)
it('is restored in normal mode but not op-pending mode', function() it('is restored in normal mode but not op-pending mode', function()
local screen = Screen.new(5, 8) local screen = Screen.new(12, 8)
command('edit ' .. file1) command('edit ' .. file1)
feed('<C-e>jWma') feed('<C-e>jWma')
feed("G'a") feed("G'a")
@@ -363,7 +363,7 @@ describe('named marks view', function()
7 line | 7 line |
8 line | 8 line |
| |
]] ]]
screen:expect({ grid = expected }) screen:expect({ grid = expected })
feed('G`a') feed('G`a')
screen:expect([[ screen:expect([[
@@ -375,7 +375,7 @@ describe('named marks view', function()
7 line | 7 line |
8 line | 8 line |
| |
]]) ]])
-- not in op-pending mode #20886 -- not in op-pending mode #20886
feed('ggj=`a') feed('ggj=`a')
screen:expect([[ screen:expect([[
@@ -387,35 +387,35 @@ describe('named marks view', function()
6 line | 6 line |
7 line | 7 line |
| |
]]) ]])
end) end)
it('is restored across files', function() it('is restored across files', function()
local screen = Screen.new(5, 5) local screen = Screen.new(12, 5)
command('args ' .. file1 .. ' ' .. file2) command('args ' .. file1 .. ' ' .. file2)
feed('<C-e>mA') feed('<C-e>mA')
local mark_view = [[ local mark_view = [[
^2 line | ^2 line |
3 line | 3 line |
4 line | 4 line |
5 line | 5 line |
| |
]] ]]
screen:expect(mark_view) screen:expect(mark_view)
command('next') command('next')
screen:expect([[ screen:expect([[
^1 line | ^1 line |
2 line | 2 line |
3 line | 3 line |
4 line | 4 line |
| |
]]) ]])
feed("'A") feed("'A")
screen:expect(mark_view) screen:expect(mark_view)
end) end)
it("fallback to standard behavior when view can't be recovered", function() it("fallback to standard behavior when view can't be recovered", function()
local screen = Screen.new(10, 10) local screen = Screen.new(12, 10)
command('edit ' .. file1) command('edit ' .. file1)
feed('7GzbmaG') -- Seven lines from the top feed('7GzbmaG') -- Seven lines from the top
command('new') -- Screen size for window is now half the height can't be restored command('new') -- Screen size for window is now half the height can't be restored
@@ -429,11 +429,11 @@ describe('named marks view', function()
8 line | 8 line |
{3:<itor-marks }| {3:<itor-marks }|
| |
]]) ]])
end) end)
it('fallback to standard behavior when mark is loaded from shada', function() it('fallback to standard behavior when mark is loaded from shada', function()
local screen = Screen.new(10, 6) local screen = Screen.new(12, 6)
command('edit ' .. file1) command('edit ' .. file1)
feed('G') feed('G')
feed('mA') feed('mA')