From 5773f0e99463ecc1dce2d5c88313e4ea22a6ff7e Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 17 Mar 2026 21:52:25 +0800 Subject: [PATCH] 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. --- src/nvim/buffer.c | 2 +- src/nvim/change.c | 2 +- src/nvim/edit.c | 2 +- src/nvim/mark.c | 13 +- src/nvim/mark_defs.h | 3 +- src/nvim/tag.c | 2 +- test/functional/editor/jump_spec.lua | 212 +++++++++++++++++---------- test/functional/editor/mark_spec.lua | 36 ++--- 8 files changed, 171 insertions(+), 101 deletions(-) diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 94b670b6dc..94f327726d 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -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.col = col; 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) { diff --git a/src/nvim/change.c b/src/nvim/change.c index 4a11078679..8f4fd4b864 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -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 (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); diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 55ba68f3d8..bc166e39e8 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -3087,7 +3087,7 @@ static bool ins_esc(int *count, int cmdchar, bool nomove) // Remember the last Insert position in the '^ mark. 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); } diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 31eac728f9..200a59b243 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -41,6 +41,7 @@ #include "nvim/os/time.h" #include "nvim/os/time_defs.h" #include "nvim/path.h" +#include "nvim/plines.h" #include "nvim/pos_defs.h" #include "nvim/quickfix.h" #include "nvim/strings.h" @@ -63,7 +64,7 @@ // Returns OK on success, FAIL if bad name given. 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); } @@ -283,7 +284,7 @@ void setpcmark(void) curwin->w_jumplistidx = curwin->w_jumplistlen; 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); } @@ -691,13 +692,17 @@ void mark_view_restore(fmark_T *fm) // and this check can prevent restoring mark view in that case. if (topline >= 1) { 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. diff --git a/src/nvim/mark_defs.h b/src/nvim/mark_defs.h index 70a7b42a77..6769d891a4 100644 --- a/src/nvim/mark_defs.h +++ b/src/nvim/mark_defs.h @@ -74,9 +74,10 @@ typedef enum { typedef struct { 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. + colnr_T skipcol; } fmarkv_T; -#define INIT_FMARKV { MAXLNUM } +#define INIT_FMARKV { MAXLNUM, 0 } /// Structure defining single local mark typedef struct { diff --git a/src/nvim/tag.c b/src/nvim/tag.c index d7c015c586..a4ed2f9c48 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -534,7 +534,7 @@ void do_tag(char *tag, int type, int count, int forceit, bool verbose) if (save_pos) { tagstack[tagstackidx].fmark.mark = curwin->w_cursor; 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 diff --git a/test/functional/editor/jump_spec.lua b/test/functional/editor/jump_spec.lua index cfb24e826d..406786594e 100644 --- a/test/functional/editor/jump_spec.lua +++ b/test/functional/editor/jump_spec.lua @@ -55,44 +55,39 @@ describe('jumplist', function() write_file(fname1, ('foobar\n'):rep(100)) write_file(fname2, 'baz') - local screen = Screen.new(5, 25) + local screen = Screen.new(12, 25) command('set number') command('edit ' .. fname1) feed('35gg') command('edit ' .. fname2) feed('') - screen:expect { - grid = [[ - {1: 24 }foobar | - {1: 25 }foobar | - {1: 26 }foobar | - {1: 27 }foobar | - {1: 28 }foobar | - {1: 29 }foobar | - {1: 30 }foobar | - {1: 31 }foobar | - {1: 32 }foobar | - {1: 33 }foobar | - {1: 34 }foobar | - {1: 35 }^foobar | - {1: 36 }foobar | - {1: 37 }foobar | - {1: 38 }foobar | - {1: 39 }foobar | - {1: 40 }foobar | - {1: 41 }foobar | - {1: 42 }foobar | - {1: 43 }foobar | - {1: 44 }foobar | - {1: 45 }foobar | - {1: 46 }foobar | - {1: 47 }foobar | + screen:expect([[ + {8: 24 }foobar | + {8: 25 }foobar | + {8: 26 }foobar | + {8: 27 }foobar | + {8: 28 }foobar | + {8: 29 }foobar | + {8: 30 }foobar | + {8: 31 }foobar | + {8: 32 }foobar | + {8: 33 }foobar | + {8: 34 }foobar | + {8: 35 }^foobar | + {8: 36 }foobar | + {8: 37 }foobar | + {8: 38 }foobar | + {8: 39 }foobar | + {8: 40 }foobar | + {8: 41 }foobar | + {8: 42 }foobar | + {8: 43 }foobar | + {8: 44 }foobar | + {8: 45 }foobar | + {8: 46 }foobar | + {8: 47 }foobar | | - ]], - attr_ids = { - [1] = { foreground = Screen.colors.Brown }, - }, - } + ]]) end) end) @@ -384,65 +379,134 @@ describe('jumpoptions=view', function() end) it('restores the view', function() - local screen = Screen.new(5, 8) + local screen = Screen.new(12, 8) command('edit ' .. file1) feed('12Gztj') feed('gg') screen:expect([[ - 12 line | - ^13 line | - 14 line | - 15 line | - 16 line | - 17 line | - 18 line | - | + 12 line | + ^13 line | + 14 line | + 15 line | + 16 line | + 17 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('') + screen:expect(s1) + feed('') + screen:expect(s2) + feed('') + 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('') + screen:expect(s3) + feed('') + screen:expect(s2) + command([[call setline(10, '10 line')]]) + feed('') + -- 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) it('restores the view across files', function() - local screen = Screen.new(5, 5) + local screen = Screen.new(12, 5) command('args ' .. file1 .. ' ' .. file2) feed('12Gzt') command('next') feed('G') screen:expect([[ - 27 line | - 28 line | - 29 line | - ^30 line | - | + 27 line | + 28 line | + 29 line | + ^30 line | + | ]]) feed('') screen:expect([[ - ^12 line | - 13 line | - 14 line | - 15 line | - | + ^12 line | + 13 line | + 14 line | + 15 line | + | ]]) end) it('restores the view across files with /:bprevious/:bnext', function() - local screen = Screen.new(5, 5) + local screen = Screen.new(12, 5) command('args ' .. file1 .. ' ' .. file2) feed('12Gzt') local s1 = [[ - ^12 line | - 13 line | - 14 line | - 15 line | - | + ^12 line | + 13 line | + 14 line | + 15 line | + | ]] screen:expect(s1) command('next') feed('G') local s2 = [[ - 27 line | - 28 line | - 29 line | - ^30 line | - | + 27 line | + 28 line | + 29 line | + ^30 line | + | ]] screen:expect(s2) feed('') @@ -456,7 +520,7 @@ describe('jumpoptions=view', function() end) 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) feed('7GzbG') 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. feed('') screen:expect([[ - 4 line | - 5 line | - 6 line | - ^7 line | - 8 line | - 9 line | - 10 line | - | + 4 line | + 5 line | + 6 line | + ^7 line | + 8 line | + 9 line | + 10 line | + | ]]) end) 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) feed('10ggzzvwy') screen:expect([[ @@ -524,7 +588,7 @@ describe('jumpoptions=view', function() end) 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) feed('10Gzb30Gzt') screen:expect([[ diff --git a/test/functional/editor/mark_spec.lua b/test/functional/editor/mark_spec.lua index 91bd2f4bbd..495c603a3f 100644 --- a/test/functional/editor/mark_spec.lua +++ b/test/functional/editor/mark_spec.lua @@ -350,7 +350,7 @@ describe('named marks view', function() end) 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) feed('jWma') feed("G'a") @@ -363,7 +363,7 @@ describe('named marks view', function() 7 line | 8 line | | - ]] + ]] screen:expect({ grid = expected }) feed('G`a') screen:expect([[ @@ -375,7 +375,7 @@ describe('named marks view', function() 7 line | 8 line | | - ]]) + ]]) -- not in op-pending mode #20886 feed('ggj=`a') screen:expect([[ @@ -387,35 +387,35 @@ describe('named marks view', function() 6 line | 7 line | | - ]]) + ]]) end) it('is restored across files', function() - local screen = Screen.new(5, 5) + local screen = Screen.new(12, 5) command('args ' .. file1 .. ' ' .. file2) feed('mA') local mark_view = [[ - ^2 line | - 3 line | - 4 line | - 5 line | - | + ^2 line | + 3 line | + 4 line | + 5 line | + | ]] screen:expect(mark_view) command('next') screen:expect([[ - ^1 line | - 2 line | - 3 line | - 4 line | - | + ^1 line | + 2 line | + 3 line | + 4 line | + | ]]) feed("'A") screen:expect(mark_view) end) 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) feed('7GzbmaG') -- Seven lines from the top 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 | {3: