mirror of
https://github.com/neovim/neovim.git
synced 2026-07-16 06:10:39 +00:00
feat(events)!: support Optionset modified, drop BufModifiedSet #35610
Problem: BufModifiedSet autocmd only triggered for current buffer during redraw, causing delayed events when :wa writes non-current buffers. Solution: - Use the aucmd_defer approach to implement `Optionset modified`. - Drop BufModifiedSet.
This commit is contained in:
@@ -292,10 +292,6 @@ BufLeave Before leaving to another buffer. Also when
|
||||
|
||||
Not used for ":qa" or ":q" when exiting Vim.
|
||||
|
||||
*BufModifiedSet*
|
||||
BufModifiedSet After the `'modified'` value of a buffer has
|
||||
been changed. Special-case of |OptionSet|.
|
||||
|
||||
*BufNew*
|
||||
BufNew After creating a new buffer (except during
|
||||
startup, see |VimEnter|) or renaming an
|
||||
@@ -926,9 +922,9 @@ OptionSet After setting an option (except during
|
||||
always use |:noautocmd| to prevent triggering
|
||||
OptionSet.
|
||||
|
||||
Note: Not triggered by the 'modified' option;
|
||||
the |BufModifiedSet| event may be used to
|
||||
handle that.
|
||||
Note: The 'modified' option triggers
|
||||
|OptionSet| deferred to the next event-loop
|
||||
tick, even when set via |:set|.
|
||||
|
||||
Non-recursive: |:set| in the autocommand does
|
||||
not trigger OptionSet again.
|
||||
|
||||
@@ -24,6 +24,9 @@ API
|
||||
• |nvim_exec_autocmds()|
|
||||
• |nvim_clear_autocmds()|
|
||||
|
||||
EVENTS
|
||||
• *BufModifiedSet* Use |OptionSet| with pattern "modified" instead.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
DEPRECATED IN 0.12 *deprecated-0.12*
|
||||
|
||||
|
||||
@@ -81,6 +81,8 @@ EDITOR
|
||||
|
||||
EVENTS
|
||||
|
||||
• |BufModifiedSet| has been removed. Use the |OptionSet| event with pattern
|
||||
"modified" instead.
|
||||
• todo
|
||||
|
||||
LSP
|
||||
|
||||
@@ -334,7 +334,8 @@ Editor:
|
||||
|
||||
Events (autocommands):
|
||||
- Fixed inconsistent behavior in execution of nested autocommands #23368
|
||||
- |BufModifiedSet|
|
||||
- |OptionSet| is now fired for 'modified' when writing a file,
|
||||
undoing changes, or using |:set| modified.
|
||||
- |Progress|
|
||||
- |RecordingEnter|
|
||||
- |RecordingLeave|
|
||||
|
||||
1
runtime/lua/vim/_meta/api_keysets.gen.lua
generated
1
runtime/lua/vim/_meta/api_keysets.gen.lua
generated
@@ -88,7 +88,6 @@ error('Cannot require a meta file')
|
||||
--- |'BufFilePre'
|
||||
--- |'BufHidden'
|
||||
--- |'BufLeave'
|
||||
--- |'BufModifiedSet'
|
||||
--- |'BufNew'
|
||||
--- |'BufNewFile'
|
||||
--- |'BufRead'
|
||||
|
||||
@@ -10,7 +10,6 @@ return {
|
||||
BufFilePre = true, -- before renaming a buffer
|
||||
BufHidden = true, -- just after buffer becomes hidden
|
||||
BufLeave = true, -- before leaving a buffer
|
||||
BufModifiedSet = true, -- after the 'modified' state of a buffer changes
|
||||
BufNew = true, -- after creating any buffer
|
||||
BufNewFile = true, -- when creating a buffer for a new file
|
||||
BufReadCmd = true, -- read buffer using command
|
||||
@@ -157,7 +156,6 @@ return {
|
||||
--- @type table<string,true>
|
||||
--- List of Nvim-specific events or aliases for generating syntax file.
|
||||
nvim_specific = {
|
||||
BufModifiedSet = true,
|
||||
DiagnosticChanged = true,
|
||||
LspAttach = true,
|
||||
LspDetach = true,
|
||||
|
||||
@@ -1551,6 +1551,35 @@ static void deferred_event(void **argv)
|
||||
xfree(e);
|
||||
}
|
||||
|
||||
static void deferred_optionset_modified(void **argv)
|
||||
{
|
||||
Error err = ERROR_INIT;
|
||||
buf_T *buf = find_buffer_by_handle((handle_T)(uintptr_t)argv[0], &err);
|
||||
api_clear_error(&err);
|
||||
if (buf) {
|
||||
bool new_val = (bool)(uintptr_t)argv[1];
|
||||
OptVal old = BOOLEAN_OPTVAL(!new_val);
|
||||
OptVal new = BOOLEAN_OPTVAL(new_val);
|
||||
aco_save_T aco;
|
||||
aucmd_prepbuf(&aco, buf);
|
||||
apply_optionset_autocmd_now(kOptModified, OPT_LOCAL, old, old, old, new, NULL);
|
||||
aucmd_restbuf(&aco);
|
||||
}
|
||||
}
|
||||
|
||||
/// Special case of aucmd_defer for `OptionSet modified`.
|
||||
void aucmd_defer_modified(buf_T *buf, bool new_val)
|
||||
{
|
||||
if (!has_event(EVENT_OPTIONSET)
|
||||
|| (bt_nofile(buf) && !buf->b_p_bl)
|
||||
|| *get_vim_var_str(VV_OPTION_TYPE) != NUL) {
|
||||
return;
|
||||
}
|
||||
multiqueue_put(deferred_events, deferred_optionset_modified,
|
||||
(void *)(uintptr_t)buf->handle,
|
||||
(void *)(uintptr_t)new_val);
|
||||
}
|
||||
|
||||
/// Execute autocommands for "event" and file name "fname".
|
||||
///
|
||||
/// @param event event that occurred
|
||||
|
||||
@@ -404,9 +404,6 @@ struct file_buffer {
|
||||
|
||||
int b_changed; // 'modified': Set to true if something in the
|
||||
// file has been changed and not written out.
|
||||
bool b_changed_invalid; // Set if BufModified autocmd has not been
|
||||
// triggered since the last time b_changed was
|
||||
// modified.
|
||||
|
||||
/// Change-identifier incremented for each change, including undo.
|
||||
///
|
||||
|
||||
@@ -150,12 +150,15 @@ void changed(buf_T *buf)
|
||||
/// Also used for recovery.
|
||||
void changed_internal(buf_T *buf)
|
||||
{
|
||||
bool was_changed = buf->b_changed;
|
||||
buf->b_changed = true;
|
||||
buf->b_changed_invalid = true;
|
||||
ml_setflags(buf);
|
||||
redraw_buf_status_later(buf);
|
||||
redraw_tabline = true;
|
||||
need_maketitle = true; // set window title later
|
||||
if (!was_changed) {
|
||||
aucmd_defer_modified(buf, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalidate a window's w_valid flags and w_lines[] entries after changing lines.
|
||||
@@ -606,8 +609,8 @@ void changed_lines(buf_T *buf, linenr_T lnum, colnr_T col, linenr_T lnume, linen
|
||||
void unchanged(buf_T *buf, bool ff, bool always_inc_changedtick)
|
||||
{
|
||||
if (buf->b_changed || (ff && file_ff_differs(buf, false))) {
|
||||
bool was_changed = buf->b_changed;
|
||||
buf->b_changed = false;
|
||||
buf->b_changed_invalid = true;
|
||||
ml_setflags(buf);
|
||||
if (ff) {
|
||||
save_file_ff(buf);
|
||||
@@ -616,6 +619,9 @@ void unchanged(buf_T *buf, bool ff, bool always_inc_changedtick)
|
||||
redraw_tabline = true;
|
||||
need_maketitle = true; // set window title later
|
||||
buf_inc_changedtick(buf);
|
||||
if (was_changed) {
|
||||
aucmd_defer_modified(buf, false);
|
||||
}
|
||||
} else if (always_inc_changedtick) {
|
||||
buf_inc_changedtick(buf);
|
||||
}
|
||||
|
||||
@@ -1456,18 +1456,8 @@ void ins_redraw(bool ready)
|
||||
may_trigger_win_scrolled_resized();
|
||||
}
|
||||
|
||||
// Trigger BufModified if b_changed_invalid is set.
|
||||
if (ready && has_event(EVENT_BUFMODIFIEDSET)
|
||||
&& curbuf->b_changed_invalid == true
|
||||
&& !pum_visible()) {
|
||||
apply_autocmds(EVENT_BUFMODIFIEDSET, NULL, NULL, false, curbuf);
|
||||
curbuf->b_changed_invalid = false;
|
||||
}
|
||||
|
||||
// Trigger SafeState if nothing is pending.
|
||||
may_trigger_safestate(ready
|
||||
&& !ins_compl_active()
|
||||
&& !pum_visible());
|
||||
may_trigger_safestate(ready && !ins_compl_active() && !pum_visible());
|
||||
|
||||
pum_check_clear();
|
||||
show_cursor_info_later(false);
|
||||
|
||||
@@ -1326,16 +1326,6 @@ static void normal_check_text_changed(NormalState *s)
|
||||
}
|
||||
}
|
||||
|
||||
static void normal_check_buffer_modified(NormalState *s)
|
||||
{
|
||||
// Trigger BufModified if 'modified' changed.
|
||||
if (!finish_op && has_event(EVENT_BUFMODIFIEDSET)
|
||||
&& curbuf->b_changed_invalid == true) {
|
||||
apply_autocmds(EVENT_BUFMODIFIEDSET, NULL, NULL, false, curbuf);
|
||||
curbuf->b_changed_invalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// If nothing is pending and we are going to wait for the user to
|
||||
/// type a character, trigger SafeState.
|
||||
static void normal_check_safe_state(NormalState *s)
|
||||
@@ -1451,7 +1441,6 @@ static int normal_check(VimState *state)
|
||||
normal_check_cursor_moved(s);
|
||||
normal_check_text_changed(s);
|
||||
normal_check_window_scrolled(s);
|
||||
normal_check_buffer_modified(s);
|
||||
normal_check_safe_state(s);
|
||||
|
||||
// Updating diffs from changed() does not always work properly,
|
||||
|
||||
@@ -2060,9 +2060,9 @@ void set_option_sctx(OptIndex opt_idx, int opt_flags, sctx_T script_ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the OptionSet autocommand.
|
||||
static void apply_optionset_autocmd(OptIndex opt_idx, int opt_flags, OptVal oldval, OptVal oldval_g,
|
||||
OptVal oldval_l, OptVal newval, const char *errmsg)
|
||||
/// Fire OptionSet autocmd directly (called from deferred context, bypasses defer logic).
|
||||
void apply_optionset_autocmd_now(OptIndex opt_idx, int opt_flags, OptVal oldval, OptVal oldval_g,
|
||||
OptVal oldval_l, OptVal newval, const char *errmsg)
|
||||
{
|
||||
// Don't do this while starting up, failure or recursively.
|
||||
if (starting || errmsg != NULL || *get_vim_var_str(VV_OPTION_TYPE) != NUL) {
|
||||
@@ -2101,6 +2101,20 @@ static void apply_optionset_autocmd(OptIndex opt_idx, int opt_flags, OptVal oldv
|
||||
reset_v_option_vars();
|
||||
}
|
||||
|
||||
/// For 'modified', the event is deferred.
|
||||
static void apply_optionset_autocmd(OptIndex opt_idx, int opt_flags, OptVal oldval, OptVal oldval_g,
|
||||
OptVal oldval_l, OptVal newval, const char *errmsg)
|
||||
{
|
||||
if (starting || errmsg != NULL) {
|
||||
return;
|
||||
}
|
||||
if (opt_idx == kOptModified) {
|
||||
aucmd_defer_modified(curbuf, newval.data.boolean);
|
||||
return;
|
||||
}
|
||||
apply_optionset_autocmd_now(opt_idx, opt_flags, oldval, oldval_g, oldval_l, newval, errmsg);
|
||||
}
|
||||
|
||||
/// Process the updated 'arabic' option value.
|
||||
static const char *did_set_arabic(optset_T *args)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ local command = n.command
|
||||
local exec_lua = n.exec_lua
|
||||
local retry = t.retry
|
||||
local source = n.source
|
||||
local request = n.request
|
||||
|
||||
describe('autocmd', function()
|
||||
before_each(clear)
|
||||
@@ -856,4 +857,52 @@ describe('autocmd', function()
|
||||
fn.execute('autocmd User ,,,there,is,,a,fly,,')
|
||||
)
|
||||
end)
|
||||
|
||||
describe('OptionSet modified', function()
|
||||
before_each(clear)
|
||||
|
||||
it('is triggered when modified and un-modified', function()
|
||||
source([[
|
||||
let g:modified = 0
|
||||
autocmd OptionSet modified let g:modified += 1
|
||||
]])
|
||||
request('nvim_command', [[normal! aa\<Esc>]])
|
||||
eq(1, eval('g:modified'))
|
||||
request('nvim_command', [[normal! u]])
|
||||
eq(2, eval('g:modified'))
|
||||
end)
|
||||
|
||||
it('triggers when writes non-current buffer #32817', function()
|
||||
source([[
|
||||
let g:modified = 0
|
||||
let g:second_trigger_buf = 0
|
||||
autocmd OptionSet modified let g:modified += 1 | if g:modified == 2 | let g:second_trigger_buf = bufnr() | endif
|
||||
]])
|
||||
request('nvim_command', [[edit test_a | badd test_b]])
|
||||
request('nvim_command', [[normal! aa\<Esc>]])
|
||||
request('nvim_command', [[let g:buf_a = bufnr()]])
|
||||
request('nvim_command', [[bn]])
|
||||
request('nvim_command', [[wa]])
|
||||
os.remove('test_a')
|
||||
eq({ 2, true }, { eval('g:modified'), eval('g:buf_a') == eval('g:second_trigger_buf') })
|
||||
end)
|
||||
|
||||
it('OptionSet triggers correctly when modified changes', function()
|
||||
command([[
|
||||
autocmd OptionSet modified call add(g:messages, string(!!v:option_old) . ' -> ' . string(!!v:option_new) . ' - actual: ' . &modified)
|
||||
]])
|
||||
command('let g:messages = []')
|
||||
local fname = t.tmpname()
|
||||
command('new ' .. fname)
|
||||
command("call setline(1, 'hi')")
|
||||
command('write')
|
||||
command('set modified')
|
||||
eq({
|
||||
'0 -> 1 - actual: 1',
|
||||
'1 -> 0 - actual: 0',
|
||||
'0 -> 1 - actual: 1',
|
||||
}, eval('g:messages'))
|
||||
os.remove(fname)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
local t = require('test.testutil')
|
||||
local n = require('test.functional.testnvim')()
|
||||
|
||||
local clear = n.clear
|
||||
local eq = t.eq
|
||||
local eval = n.eval
|
||||
local source = n.source
|
||||
local request = n.request
|
||||
|
||||
describe('BufModified', function()
|
||||
before_each(clear)
|
||||
|
||||
it('is triggered when modified and un-modified', function()
|
||||
source([[
|
||||
let g:modified = 0
|
||||
autocmd BufModifiedSet * let g:modified += 1
|
||||
]])
|
||||
request('nvim_command', [[normal! aa\<Esc>]])
|
||||
eq(1, eval('g:modified'))
|
||||
request('nvim_command', [[normal! u]])
|
||||
eq(2, eval('g:modified'))
|
||||
end)
|
||||
end)
|
||||
@@ -1423,12 +1423,14 @@ describe(':terminal buffer', function()
|
||||
end)
|
||||
|
||||
it('does not allow OptionSet or b:term_title watcher to delete buffer', function()
|
||||
local au = api.nvim_create_autocmd('OptionSet', { command = 'bwipeout!' })
|
||||
local chan = api.nvim_open_term(0, {})
|
||||
local chan = exec_lua([[
|
||||
local au = vim.api.nvim_create_autocmd('OptionSet', { command = 'bwipeout!' })
|
||||
local chan = vim.api.nvim_open_term(0, {})
|
||||
vim.api.nvim_del_autocmd(au)
|
||||
return chan
|
||||
]])
|
||||
matches('^E937: ', api.nvim_get_vvar('errmsg'))
|
||||
api.nvim_del_autocmd(au)
|
||||
api.nvim_set_vvar('errmsg', '')
|
||||
|
||||
api.nvim_chan_send(chan, '\027]2;SOME_TITLE\007')
|
||||
eq('SOME_TITLE', api.nvim_buf_get_var(0, 'term_title'))
|
||||
command([[call dictwatcheradd(b:, 'term_title', {-> execute('bwipe!')})]])
|
||||
|
||||
Reference in New Issue
Block a user