diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index 37c2f548e6..fc1e8a3fb7 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -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. diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt index b2f5de192f..b66ecd9abb 100644 --- a/runtime/doc/deprecated.txt +++ b/runtime/doc/deprecated.txt @@ -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* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 94e65736ef..d45c2bd765 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -81,6 +81,8 @@ EDITOR EVENTS +• |BufModifiedSet| has been removed. Use the |OptionSet| event with pattern + "modified" instead. • todo LSP diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 06351529f5..41ce83bd6a 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -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| diff --git a/runtime/lua/vim/_meta/api_keysets.gen.lua b/runtime/lua/vim/_meta/api_keysets.gen.lua index c27a23a0bd..f5067142b8 100644 --- a/runtime/lua/vim/_meta/api_keysets.gen.lua +++ b/runtime/lua/vim/_meta/api_keysets.gen.lua @@ -88,7 +88,6 @@ error('Cannot require a meta file') --- |'BufFilePre' --- |'BufHidden' --- |'BufLeave' ---- |'BufModifiedSet' --- |'BufNew' --- |'BufNewFile' --- |'BufRead' diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua index 4463f0c291..36731489b5 100644 --- a/src/nvim/auevents.lua +++ b/src/nvim/auevents.lua @@ -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 --- List of Nvim-specific events or aliases for generating syntax file. nvim_specific = { - BufModifiedSet = true, DiagnosticChanged = true, LspAttach = true, LspDetach = true, diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index a5c6be1d06..e5c5d3ffc2 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -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 diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 90845531f4..b4e31c77a0 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -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. /// diff --git a/src/nvim/change.c b/src/nvim/change.c index b5533f273f..8286af2e51 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -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); } diff --git a/src/nvim/edit.c b/src/nvim/edit.c index d51929583e..fbc0b156de 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -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); diff --git a/src/nvim/normal.c b/src/nvim/normal.c index ef971d3e8a..aadbebbd46 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -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, diff --git a/src/nvim/option.c b/src/nvim/option.c index c23a6855aa..76355c783b 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -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) { diff --git a/test/functional/autocmd/autocmd_spec.lua b/test/functional/autocmd/autocmd_spec.lua index f68695bc83..fde6440d50 100644 --- a/test/functional/autocmd/autocmd_spec.lua +++ b/test/functional/autocmd/autocmd_spec.lua @@ -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\]]) + 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\]]) + 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) diff --git a/test/functional/autocmd/bufmodifiedset_spec.lua b/test/functional/autocmd/bufmodifiedset_spec.lua deleted file mode 100644 index fb8f160b05..0000000000 --- a/test/functional/autocmd/bufmodifiedset_spec.lua +++ /dev/null @@ -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\]]) - eq(1, eval('g:modified')) - request('nvim_command', [[normal! u]]) - eq(2, eval('g:modified')) - end) -end) diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index 63e0272874..6bb66fbad5 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -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!')})]])