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.
This commit is contained in:
luukvbaal
2026-02-16 15:35:53 +01:00
committed by GitHub
parent d131d67e67
commit a393347673
2 changed files with 12 additions and 12 deletions

View File

@@ -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;

View File

@@ -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,