feat(events): add TabMoved autocommand event #24137

Problem:
No way to handle a "tab moved" event.
Use-case: tabline plugins may cache tab labels, and need to know when to
invalidate their cache.

Solution:
Add a `TabMoved` event that triggers whenever tabs are reordered via `:tabmove`
or via mouse click-and-drag.
This commit is contained in:
yashlala
2026-05-16 13:55:42 -04:00
committed by GitHub
parent 0b96b3cd52
commit 7d99104058
11 changed files with 144 additions and 0 deletions

View File

@@ -1165,6 +1165,14 @@ TabEnter Just after entering a |tabpage|.
TabLeave Just before leaving a |tabpage|.
After WinLeave.
*TabMoved*
TabMoved After a |tabpage| is moved.
See |:tabmove|. <amatch> expands to the
affected |tabpage-number| before the move.
The |event-data| has these keys (type: `vim.event.tabmoved.data`)
- tabnr_old: |tabpage-number| before the move.
- tabnr_new: |tabpage-number| after the move.
*TabNew*
TabNew When creating a new |tabpage|.
After WinEnter.

View File

@@ -148,6 +148,7 @@ EVENTS
• |:delmarks| now triggers the |MarkSet| autocommand with line==col==0, same
as |nvim_buf_del_mark()|
• |TextPutPre| and |TextPutPost| are triggered before/after putting text.
• |TabMoved| is triggered when tabs are reordered.
HIGHLIGHTS

View File

@@ -2593,6 +2593,7 @@ A jump table for the options with a short description can be found at |Q_op|.
|TabClosedPre|,
|TabEnter|,
|TabLeave|,
|TabMoved|,
|TabNew|,
|TabNewEntered|,
|TermClose|,

View File

@@ -343,6 +343,7 @@ Events (autocommands):
- |RecordingLeave|
- |SearchWrapped|
- |Signal|
- |TabMoved|
- |TabNewEntered|
- |TermClose|
- |TermOpen|

View File

@@ -194,6 +194,7 @@ error('Cannot require a meta file')
--- |'TabClosedPre'
--- |'TabEnter'
--- |'TabLeave'
--- |'TabMoved'
--- |'TabNew'
--- |'TabNewEntered'
--- |'TermChanged'

View File

@@ -50,6 +50,10 @@ error('Cannot require a meta file')
--- @field status? string
--- @field title? string
--- @class vim.event.tabmoved.data
--- @field tabnr_old integer
--- @field tabnr_new integer
--- @class vim.event.termrequest.data
--- @field sequence string
--- @field terminator string

View File

@@ -2240,6 +2240,7 @@ vim.go.ei = vim.go.eventignore
--- `TabClosedPre`,
--- `TabEnter`,
--- `TabLeave`,
--- `TabMoved`,
--- `TabNew`,
--- `TabNewEntered`,
--- `TermClose`,

View File

@@ -113,6 +113,7 @@ return {
TabClosedPre = false, -- before closing a tab page
TabEnter = false, -- after entering a tab page
TabLeave = false, -- before leaving a tab page
TabMoved = false, -- after a tab was moved
TabNew = false, -- when creating a new tab
TabNewEntered = false, -- after entering a new tab
TermChanged = false, -- after changing 'term'
@@ -171,6 +172,7 @@ return {
RecordingEnter = true,
RecordingLeave = true,
Signal = true,
TabMoved = true,
TabNewEntered = true,
TermClose = true,
TermOpen = true,

View File

@@ -1843,6 +1843,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
|| event == EVENT_SPELLFILEMISSING
|| event == EVENT_SYNTAX
|| event == EVENT_TABCLOSED
|| event == EVENT_TABMOVED
|| event == EVENT_USER
|| event == EVENT_WINCLOSED
|| event == EVENT_WINRESIZED

View File

@@ -5005,6 +5005,7 @@ void tabpage_move(int nr)
return;
}
int old_nr = tabpage_index(curtab);
tabpage_T *tp_dst = tp;
// Remove the current tab page from the list of tab pages.
@@ -5035,6 +5036,15 @@ void tabpage_move(int nr)
// Need to redraw the tabline. Tab page contents doesn't change.
redraw_tabline = true;
if (has_event(EVENT_TABMOVED)) {
char prev_idx[NUMBUFLEN];
vim_snprintf(prev_idx, NUMBUFLEN, "%i", old_nr);
MAXSIZE_TEMP_DICT(data, 2);
PUT_C(data, "tabnr_old", INTEGER_OBJ(old_nr));
PUT_C(data, "tabnr_new", INTEGER_OBJ(tabpage_index(curtab)));
aucmd_defer(EVENT_TABMOVED, prev_idx, NULL, AUGROUP_ALL, curbuf, NULL, &DICT_OBJ(data));
}
}
/// Go to another window.

View File

@@ -0,0 +1,114 @@
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local clear = n.clear
local command = n.command
local exec_lua = n.exec_lua
local insert = n.insert
local poke_eventloop = n.poke_eventloop
local api = n.api
local eval = n.eval
local eq = t.eq
describe('TabMoved', function()
before_each(function()
clear()
end)
it('sets <amatch> and event-data fields', function()
-- start on tabpage 1, create 2 more so tabpage 3 is "current tab"
command('tabnew')
command('tabnew')
exec_lua([[
_G.tm_events = {}
vim.api.nvim_create_autocmd('TabMoved', {
callback = function(ev)
table.insert(_G.tm_events, ev)
end,
})
]])
command('tabmove 0')
poke_eventloop()
local events = exec_lua('return _G.tm_events')
eq('3', events[1].match)
eq(3, events[1].data.tabnr_old)
eq(1, events[1].data.tabnr_new)
command('tabnext 2')
command('tabmove $')
poke_eventloop()
events = exec_lua('return _G.tm_events')
eq('2', events[2].match)
eq(2, events[2].data.tabnr_old)
eq(3, events[2].data.tabnr_new)
command('tabmove -1')
poke_eventloop()
events = exec_lua('return _G.tm_events')
eq('3', events[3].match)
eq(3, events[3].data.tabnr_old)
eq(2, events[3].data.tabnr_new)
end)
it('does not trigger when a tab is moved to same position', function()
command('tabnew')
command('tabnew')
command('let g:test = 0')
command('au! TabMoved * let g:test += 1')
command('tabmove $') -- already at last position, no-op
poke_eventloop()
eq(0, eval('g:test'))
end)
it('is not triggered when creating or closing tabs', function()
command('let g:test = 0')
command('au! TabMoved * let g:test += 1')
command('file Xtestfile1')
command('0tabedit Xtestfile2')
command('tabclose')
poke_eventloop()
eq(0, eval('g:test'))
end)
it('responds correctly to mouse interactions on the tabline', function()
local function setup()
Screen.new(25, 5)
command('set mouse=a')
command('tabnew')
command('tabnew')
exec_lua([[
_G.tm_events = {}
vim.api.nvim_create_autocmd('TabMoved', {
callback = function(ev)
table.insert(_G.tm_events, ev)
end,
})
]])
end
-- clicking tab 2 shouldn't trigger the event.
setup()
api.nvim_input_mouse('left', 'press', '', 0, 0, 5)
api.nvim_input_mouse('left', 'release', '', 0, 0, 5)
poke_eventloop()
eq(0, #exec_lua('return _G.tm_events'))
clear()
-- dragging tab 1 to the right should trigger the event.
setup()
api.nvim_input_mouse('left', 'press', '', 0, 0, 4)
poke_eventloop()
eq(0, #exec_lua('return _G.tm_events'))
api.nvim_input_mouse('left', 'drag', '', 0, 0, 14)
poke_eventloop()
local events = exec_lua('return _G.tm_events')
eq(1, #events)
eq(1, events[1].data.tabnr_old)
eq(2, events[1].data.tabnr_new)
end)
end)