neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit a39334767321bf56dc6d0baedb048066fc56e11c
parent d131d67e67a8406fff2c3dc2f9a4aceb8ef36716
Author: luukvbaal <luukvbaal@gmail.com>
Date:   Mon, 16 Feb 2026 15:35:53 +0100

fix(extmark): adjust invalidate range by one for deleted lines #37897

Problem:  A multi-line extmark that ends exactly at a deleted range end
          is not invalidated.
          Revalidated sign mark is added with incorrect range.
Solution: Remove questionable invalidation range condition which was
          originally added to avoid deleting a mark that ends below a
          deleted line.

          Since splicing for a deleted line, and a replaced range that
          explicitly ends at column 0 beyond a deleted line is identical,
          we can't try to distinguish these two cases. I.e. :1delete 1
          and nvim_buf_set_text(0, 0, 0, 1, 0, {}) yield the same splice
          operation.

          This means that a multi-line sign_text mark should now span at
          least one column beyond its end_row, as seen in the adjusted
          test. This is still somewhat unexpected/inconvenient to me
          which is what prompted me to try to avoid it with the original
          condition.

          Add revalidated sign mark back to decor with correct range;
          third sign mark added to test exposed a crash.
Diffstat:
Msrc/nvim/extmark.c | 7+++----
Mtest/functional/api/extmark_spec.lua | 17+++++++++--------
2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c @@ -141,7 +141,7 @@ static void extmark_setraw(buf_T *buf, uint64_t mark, int row, colnr_T col, bool } if (invalid) { - buf_put_decor(buf, mt_decor(key), MIN(row, key.pos.row), MAX(row, key.pos.row)); + buf_put_decor(buf, mt_decor(key), MIN(row, alt.pos.row), MAX(row, alt.pos.row)); } else if (!mt_invalid(key) && key.flags & MT_FLAG_DECOR_SIGNTEXT && buf->b_signcols.autom) { buf_signcols_count_range(buf, row1, MIN(curbuf->b_ml.ml_line_count - 1, row2), 0, kNone); } @@ -398,9 +398,8 @@ void extmark_splice_delete(buf_T *buf, int l_row, colnr_T l_col, int u_row, coln // range has been deleted. if ((!mt_paired(mark) && mark.pos.row < u_row) || (mt_paired(mark) - && (endpos.col <= u_col || (!u_col && endpos.row == mark.pos.row)) - && mark.pos.col >= l_col - && mark.pos.row >= l_row && endpos.row <= u_row - (u_col ? 0 : 1))) { + && (mark.pos.row > l_row || (mark.pos.row == l_row && mark.pos.col >= l_col)) + && (endpos.row < u_row || (endpos.row == u_row && endpos.col <= u_col)))) { if (mt_no_undo(mark)) { extmark_del(buf, itr, mark, true); continue; diff --git a/test/functional/api/extmark_spec.lua b/test/functional/api/extmark_spec.lua @@ -1774,13 +1774,14 @@ describe('API/extmarks', function() it('invalidated marks are deleted', function() screen = Screen.new(40, 6) feed('dd6iaaa bbb ccc<CR><ESC>gg') - api.nvim_set_option_value('signcolumn', 'auto:2', {}) + api.nvim_set_option_value('signcolumn', 'auto:3', {}) set_extmark(ns, 1, 0, 0, { invalidate = true, sign_text = 'S1', end_row = 1 }) - set_extmark(ns, 2, 1, 0, { invalidate = true, sign_text = 'S2', end_row = 2 }) + set_extmark(ns, 2, 1, 0, { invalidate = true, sign_text = 'S2', end_row = 2, end_col = 0 }) + set_extmark(ns, 3, 1, 0, { invalidate = true, sign_text = 'S3', end_row = 2, end_col = 1 }) -- mark with invalidate is removed command('d2') screen:expect([[ - {7:S2}^aaa bbb ccc | + {7:S3}^aaa bbb ccc | {7: }aaa bbb ccc |*3 {7: } | | @@ -1788,15 +1789,16 @@ describe('API/extmarks', function() -- mark is restored with undo_restore == true command('silent undo') screen:expect([[ - {7:S1 }^aaa bbb ccc | - {7:S2S1}aaa bbb ccc | - {7:S2 }aaa bbb ccc | - {7: }aaa bbb ccc |*2 + {7:S1 }^aaa bbb ccc | + {7:S3S2S1}aaa bbb ccc | + {7:S3S2 }aaa bbb ccc | + {7: }aaa bbb ccc |*2 | ]]) -- decor is not removed twice command('d3') api.nvim_buf_del_extmark(0, ns, 1) + api.nvim_buf_del_extmark(0, ns, 3) command('silent undo') -- mark is deleted with undo_restore == false set_extmark(ns, 1, 0, 0, { invalidate = true, undo_restore = false, sign_text = 'S1' }) @@ -1806,7 +1808,6 @@ describe('API/extmarks', function() -- mark is not removed when deleting bytes before the range set_extmark(ns, 3, 0, 4, { invalidate = true, - undo_restore = true, hl_group = 'Error', end_col = 7, right_gravity = false,