From 46c83ce321b2612e20f5df3f568c1c0884a6d50f Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 28 Apr 2026 07:00:23 +0800 Subject: [PATCH] fix(marks): don't use spell decorations from other lines (#39441) Spell decorations from other lines aren't relevant to the current line. Also, decor_redraw_col() can only go forward, while spell navigation needs to go both forward and backward. --- src/nvim/spell.c | 9 +-- test/functional/ui/spell_spec.lua | 121 ++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/src/nvim/spell.c b/src/nvim/spell.c index 91727c8c5c..4d5a14ef67 100644 --- a/src/nvim/spell.c +++ b/src/nvim/spell.c @@ -1255,15 +1255,10 @@ bool no_spell_checking(win_T *wp) return false; } -static void decor_spell_nav_start(win_T *wp) -{ - decor_state = (DecorState){ 0 }; - decor_redraw_reset(wp, &decor_state); -} - static TriState decor_spell_nav_col(win_T *wp, linenr_T lnum, linenr_T *decor_lnum, int col) { if (*decor_lnum != lnum) { + decor_redraw_reset(wp, &decor_state); decor_providers_invoke_spell(wp, lnum - 1, col, lnum - 1, -1); decor_redraw_line(wp, lnum - 1, &decor_state); *decor_lnum = lnum; @@ -1331,7 +1326,7 @@ size_t spell_move_to(win_T *wp, int dir, smt_T behaviour, bool curline, hlf_T *a // temporary DecorState. DecorState saved_decor_start = decor_state; linenr_T decor_lnum = -1; - decor_spell_nav_start(wp); + decor_state = (DecorState){ 0 }; while (!got_int) { char *line = ml_get_buf(wp->w_buffer, lnum); diff --git a/test/functional/ui/spell_spec.lua b/test/functional/ui/spell_spec.lua index 86d5a362e5..d16973dc20 100644 --- a/test/functional/ui/spell_spec.lua +++ b/test/functional/ui/spell_spec.lua @@ -455,4 +455,125 @@ describe("'spell'", function() | ]]) end) + + local function test_spell_false_nav(ephemeral) + screen:try_resize(50, 8) + insert('Splel\nSplle\nSepll\nSpele\nSpeel') + feed('gg0') + exec('set shortmess+=s spell spelloptions=noplainbuffer') + + n.exec_lua(function() + local ns = vim.api.nvim_create_namespace('spell') + if ephemeral then + local decors = {} --- @type [integer,integer,vim.api.keyset.set_extmark][] + local function on_do() + for _, decor in ipairs(decors) do + vim.api.nvim_buf_set_extmark( + 0, + ns, + decor[1], + decor[2], + vim.tbl_deep_extend('error', decor[3], { ephemeral = true }) + ) + end + end + vim.api.nvim_set_decoration_provider(ns, { + on_win = on_do, + _on_spell_nav = on_do, + }) + function _G.Update(new_decors) + decors = new_decors + vim.cmd('redraw!') + end + else + --- @param decors [integer,integer,vim.api.keyset.set_extmark][] + function _G.Update(decors) + vim.api.nvim_buf_clear_namespace(0, ns, 0, -1) + for _, decor in ipairs(decors) do + vim.api.nvim_buf_set_extmark(0, ns, decor[1], decor[2], decor[3]) + end + vim.cmd('redraw!') + end + end + end) + + n.exec_lua(function() + _G.Update({ + { 0, 0, { end_row = 5, spell = true, priority = 0 } }, + { 2, 0, { end_row = 3, spell = false, priority = 1 } }, + }) + end) + screen:expect([[ + {1:^Splel} | + {1:Splle} | + Sepll | + {1:Spele} | + {1:Speel} | + {0:~ }|*2 + | + ]]) + + t.eq({ 1, 0 }, api.nvim_win_get_cursor(0)) + feed(']s') + t.eq({ 2, 0 }, api.nvim_win_get_cursor(0)) + feed(']s') + t.eq({ 4, 0 }, api.nvim_win_get_cursor(0)) + feed(']s') + t.eq({ 5, 0 }, api.nvim_win_get_cursor(0)) + feed(']s') + t.eq({ 1, 0 }, api.nvim_win_get_cursor(0)) + feed('[s') + t.eq({ 5, 0 }, api.nvim_win_get_cursor(0)) + feed('[s') + t.eq({ 4, 0 }, api.nvim_win_get_cursor(0)) + feed('[s') + t.eq({ 2, 0 }, api.nvim_win_get_cursor(0)) + feed('[s') + t.eq({ 1, 0 }, api.nvim_win_get_cursor(0)) + + n.exec_lua(function() + _G.Update({ + { 0, 0, { end_row = 5, spell = true, priority = 0 } }, + { 3, 0, { end_row = 5, spell = false, priority = 1 } }, + { 3, 0, { end_row = 4, spell = true, priority = 2 } }, + }) + end) + screen:expect([[ + {1:^Splel} | + {1:Splle} | + {1:Sepll} | + {1:Spele} | + Speel | + {0:~ }|*2 + | + ]]) + + t.eq({ 1, 0 }, api.nvim_win_get_cursor(0)) + feed(']s') + t.eq({ 2, 0 }, api.nvim_win_get_cursor(0)) + feed(']s') + t.eq({ 3, 0 }, api.nvim_win_get_cursor(0)) + feed(']s') + t.eq({ 4, 0 }, api.nvim_win_get_cursor(0)) + feed(']s') + t.eq({ 1, 0 }, api.nvim_win_get_cursor(0)) + feed('[s') + t.eq({ 4, 0 }, api.nvim_win_get_cursor(0)) + feed('[s') + t.eq({ 3, 0 }, api.nvim_win_get_cursor(0)) + feed('[s') + t.eq({ 2, 0 }, api.nvim_win_get_cursor(0)) + feed('[s') + t.eq({ 1, 0 }, api.nvim_win_get_cursor(0)) + end + + describe('spell=false decoration on line with spelling mistake #39441', function() + it('using ephemeral decorations', function() + test_spell_false_nav(true) + end) + + it('using non-ephemeral extmarks', function() + test_spell_false_nav(false) + end) + end) end)