backport: feat(events): trigger MarkSet autocmd in :delmarks (#39218)

Problem:
`api.nvim_buf_del_mark` already emits a `MarkSet` event with `col` and `line` set to 0. However, `:delmarks` currently emits no events.

Solution:
Change `:delmarks` to emit the same `col==line==0` event.
This commit is contained in:
Ashley Hauck
2026-04-19 14:20:56 +02:00
committed by GitHub
parent 5907307662
commit 15991abaa7
4 changed files with 134 additions and 7 deletions
+4 -2
View File
@@ -785,11 +785,13 @@ LspRequest See |LspRequest|
LspTokenUpdate See |LspTokenUpdate|
*MarkSet*
MarkSet After a |mark| is set by |m|, |:mark|, and
|nvim_buf_set_mark()|. Supports `[a-zA-Z]`
|nvim_buf_set_mark()|, or deleted by |:delmarks|
and |nvim_buf_del_mark()|. Supports `[a-zA-Z]`
marks (may support more in the future).
The |autocmd-pattern| is matched against the
mark name (e.g. `[ab]` matches `a` or `b`, `*`
matches all).
matches all). If line is 0, the mark has been
deleted.
The |event-data| has these keys (type: `vim.event.markset.data`):
- name: Mark name (e.g. "a")
+2
View File
@@ -199,6 +199,8 @@ EVENTS
• |SessionLoadPre| is triggered before loading a |Session| file.
• |TabClosedPre| is triggered before closing a |tabpage|.
• |TermRequest| event gained a `terminator` parameter.
• |:delmarks| now triggers the |MarkSet| autocommand with line==col==0, same
as |nvim_buf_del_mark()|
HIGHLIGHTS
+65 -5
View File
@@ -81,7 +81,9 @@ void free_xfmark(xfmark_T fm)
free_fmark(fm.fmark);
}
/// Free and clear fmark_T item
/// Free and clear fmark_T item.
///
/// Does not trigger "MarkSet" event.
void clear_fmark(fmark_T *const fm, const Timestamp timestamp)
FUNC_ATTR_NONNULL_ALL
{
@@ -849,6 +851,8 @@ bool mark_check_line_bounds(buf_T *buf, fmark_T *fm, const char **errormsg)
///
/// Used mainly when trashing the entire buffer during ":e" type commands.
///
/// Does not trigger "MarkSet" event.
///
/// @param[out] buf Buffer to clear marks in.
void clrallmarks(buf_T *const buf, const Timestamp timestamp)
FUNC_ATTR_NONNULL_ALL
@@ -1010,9 +1014,30 @@ void ex_delmarks(exarg_T *eap)
{
int from, to;
int n;
pos_T pos = { 0, 0, 0 };
if (*eap->arg == NUL && eap->forceit) {
// clear all marks
for (size_t i = 0; i < NMARKS; i++) {
if (curbuf->b_namedm[i].mark.lnum != 0) {
do_markset_autocmd((char)(i + 'a'), &pos, curbuf);
}
}
if (curbuf->b_last_cursor.mark.lnum != 0) {
do_markset_autocmd('"', &pos, curbuf);
}
if (curbuf->b_last_insert.mark.lnum != 0) {
do_markset_autocmd('^', &pos, curbuf);
}
if (curbuf->b_last_change.mark.lnum != 0) {
do_markset_autocmd('.', &pos, curbuf);
}
if (curbuf->b_op_start.lnum != 0) {
do_markset_autocmd('[', &pos, curbuf);
}
if (curbuf->b_op_end.lnum != 0) {
do_markset_autocmd(']', &pos, curbuf);
}
clrallmarks(curbuf, os_time());
} else if (eap->forceit) {
emsg(_(e_invarg));
@@ -1044,6 +1069,9 @@ void ex_delmarks(exarg_T *eap)
for (int i = from; i <= to; i++) {
if (lower) {
if (curbuf->b_namedm[i - 'a'].mark.lnum != 0) {
do_markset_autocmd((char)i, &pos, curbuf);
}
curbuf->b_namedm[i - 'a'].mark.lnum = 0;
curbuf->b_namedm[i - 'a'].timestamp = timestamp;
} else {
@@ -1052,6 +1080,13 @@ void ex_delmarks(exarg_T *eap)
} else {
n = i - 'A';
}
if (namedfm[n].fmark.mark.lnum != 0) {
buf_T *buf = buflist_findnr(namedfm[n].fmark.fnum);
if (buf == NULL) {
buf = curbuf;
}
do_markset_autocmd((char)i, &pos, buf);
}
namedfm[n].fmark.mark.lnum = 0;
namedfm[n].fmark.fnum = 0;
namedfm[n].fmark.timestamp = timestamp;
@@ -1061,25 +1096,50 @@ void ex_delmarks(exarg_T *eap)
} else {
switch (*p) {
case '"':
if (curbuf->b_last_cursor.mark.lnum != 0) {
do_markset_autocmd(*p, &pos, curbuf);
}
clear_fmark(&curbuf->b_last_cursor, timestamp);
break;
case '^':
if (curbuf->b_last_insert.mark.lnum != 0) {
do_markset_autocmd(*p, &pos, curbuf);
}
clear_fmark(&curbuf->b_last_insert, timestamp);
break;
case ':':
// Readonly mark. No deletion allowed.
break;
case '.':
if (curbuf->b_last_change.mark.lnum != 0) {
do_markset_autocmd(*p, &pos, curbuf);
}
clear_fmark(&curbuf->b_last_change, timestamp);
break;
case '[':
curbuf->b_op_start.lnum = 0; break;
if (curbuf->b_op_start.lnum != 0) {
do_markset_autocmd(*p, &pos, curbuf);
}
curbuf->b_op_start.lnum = 0;
break;
case ']':
curbuf->b_op_end.lnum = 0; break;
if (curbuf->b_op_end.lnum != 0) {
do_markset_autocmd(*p, &pos, curbuf);
}
curbuf->b_op_end.lnum = 0;
break;
case '<':
curbuf->b_visual.vi_start.lnum = 0; break;
if (curbuf->b_visual.vi_start.lnum != 0) {
do_markset_autocmd(*p, &pos, curbuf);
}
curbuf->b_visual.vi_start.lnum = 0;
break;
case '>':
curbuf->b_visual.vi_end.lnum = 0; break;
if (curbuf->b_visual.vi_end.lnum != 0) {
do_markset_autocmd(*p, &pos, curbuf);
}
curbuf->b_visual.vi_end.lnum = 0;
break;
case ' ':
break;
default:
+63
View File
@@ -151,6 +151,8 @@ describe('MarkSet', function()
command('tabclose')
feed('mD')
-- delmarks should signal on the buf the mark is set on, not the current buf
command('delmarks A')
poke_eventloop()
eq({
@@ -186,6 +188,14 @@ describe('MarkSet', function()
name = 'D',
},
},
{
buf = 1,
event = {
col = 0,
line = 0,
name = 'A',
},
},
}, eval('g:markset_events'))
end)
@@ -283,4 +293,57 @@ describe('MarkSet', function()
eq({ 1, 1 }, api.nvim_buf_get_mark(third_bufnr, 'B'))
eq({ 3, 0 }, api.nvim_buf_get_mark(first_bufnr, 'C'))
end)
it('emits when marks are deleted', function()
command([[
let g:mark_events = []
autocmd MarkSet * call add(g:mark_events, {'event': deepcopy(v:event)})
]])
api.nvim_buf_set_lines(0, 0, -1, true, {
'line 1',
'line 2',
})
feed('ma')
feed('mb')
command('delmarks a')
-- deleting a second time should not trigger an event
command('delmarks a')
api.nvim_buf_del_mark(0, 'b')
api.nvim_buf_del_mark(0, 'b')
poke_eventloop()
eq({
{
event = {
col = 0,
line = 1,
name = 'a',
},
},
{
event = {
col = 0,
line = 1,
name = 'b',
},
},
{
event = {
col = 0,
line = 0,
name = 'a',
},
},
{
event = {
col = 0,
line = 0,
name = 'b',
},
},
}, eval('g:mark_events'))
end)
end)