diff options
-rw-r--r-- | runtime/doc/autocmd.txt | 19 | ||||
-rw-r--r-- | runtime/doc/eval.txt | 9 | ||||
-rw-r--r-- | src/nvim/auevents.lua | 1 | ||||
-rw-r--r-- | src/nvim/eval.c | 13 | ||||
-rw-r--r-- | src/nvim/eval.h | 3 | ||||
-rw-r--r-- | src/nvim/fileio.c | 3 | ||||
-rw-r--r-- | src/nvim/option.c | 93 | ||||
-rw-r--r-- | src/nvim/version.c | 8 | ||||
-rw-r--r-- | test/functional/legacy/autocmd_option_spec.lua | 277 |
9 files changed, 413 insertions, 13 deletions
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index a0ed91c95d..38d53249d1 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -258,6 +258,7 @@ Name triggered by ~ |Syntax| when the 'syntax' option has been set |EncodingChanged| after the 'encoding' option has been changed |TermChanged| after the value of 'term' has changed +|OptionSet| after setting any option Startup and exit |VimEnter| after doing all the startup stuff @@ -745,6 +746,24 @@ MenuPopup Just before showing the popup menu (under the o Operator-pending i Insert c Command line + *OptionSet* +OptionSet After setting an option. The pattern is + matched against the long option name. + The |v:option_old| variable indicates the + old option value, |v:option_new| variable + indicates the newly set value, the + |v:option_type| variable indicates whether + it's global or local scoped and |<amatch>| + indicates what option has been set. + + Note: It's a bad idea, to reset an option + during this autocommand, since this will + probably break plugins. You can always use + |noa| to prevent triggering this autocommand. + Could be used, to check for existence of the + 'backupdir' and 'undodir' options and create + directories, if they don't exist yet. + *QuickFixCmdPre* QuickFixCmdPre Before a quickfix command is run (|:make|, |:lmake|, |:grep|, |:lgrep|, |:grepadd|, diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 7b12d2082f..4ff0636b61 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1535,6 +1535,15 @@ v:oldfiles List of file names that is loaded from the |shada| file on than String this will cause trouble. {only when compiled with the |+shada| feature} + *v:option_new* +v:option_new New value of the option. Valid while executing an |OptionSet| + autocommand. + *v:option_old* +v:option_old Old value of the option. Valid while executing an |OptionSet| + autocommand. + *v:option_type* +v:option_type Scope of the set command. Valid while executing an + |OptionSet| autocommand. Can be either "global" or "local" *v:operator* *operator-variable* v:operator The last operator given in Normal mode. This is a single character except for commands starting with <g> or <z>, diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua index 7624dd2303..aa4a8d8332 100644 --- a/src/nvim/auevents.lua +++ b/src/nvim/auevents.lua @@ -57,6 +57,7 @@ return { 'InsertLeave', -- when leaving Insert mode 'JobActivity', -- when job sent some data 'MenuPopup', -- just before popup menu is displayed + 'OptionSet', -- after setting any option 'QuickFixCmdPost', -- after :make, :grep etc. 'QuickFixCmdPre', -- before :make, :grep etc. 'QuitPre', -- before :quit diff --git a/src/nvim/eval.c b/src/nvim/eval.c index f104098dbf..4a2bf2ac7a 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -373,6 +373,9 @@ static struct vimvar { {VV_NAME("progpath", VAR_STRING), VV_RO}, {VV_NAME("command_output", VAR_STRING), 0}, {VV_NAME("completed_item", VAR_DICT), VV_RO}, + {VV_NAME("option_new", VAR_STRING), VV_RO}, + {VV_NAME("option_old", VAR_STRING), VV_RO}, + {VV_NAME("option_type", VAR_STRING), VV_RO}, {VV_NAME("msgpack_types", VAR_DICT), VV_RO}, }; @@ -21238,9 +21241,13 @@ void ex_oldfiles(exarg_T *eap) } } - - - +// reset v:option_new, v:option_old and v:option_type +void reset_v_option_vars(void) +{ + set_vim_var_string(VV_OPTION_NEW, NULL, -1); + set_vim_var_string(VV_OPTION_OLD, NULL, -1); + set_vim_var_string(VV_OPTION_TYPE, NULL, -1); +} /* * Adjust a filename, according to a string of modifiers. diff --git a/src/nvim/eval.h b/src/nvim/eval.h index 8ccf71068c..19a1bbb083 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -108,6 +108,9 @@ enum { VV_PROGPATH, VV_COMMAND_OUTPUT, VV_COMPLETED_ITEM, + VV_OPTION_NEW, + VV_OPTION_OLD, + VV_OPTION_TYPE, VV_MSGPACK_TYPES, VV_LEN, /* number of v: vars */ }; diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 11673785fd..bc5b08ef24 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -6407,7 +6407,7 @@ apply_autocmds_group ( * invalid. */ if (fname_io == NULL) { - if (event == EVENT_COLORSCHEME) + if (event == EVENT_COLORSCHEME || event == EVENT_OPTIONSET) autocmd_fname = NULL; else if (fname != NULL && *fname != NUL) autocmd_fname = fname; @@ -6457,6 +6457,7 @@ apply_autocmds_group ( if (event == EVENT_COLORSCHEME || event == EVENT_FILETYPE || event == EVENT_FUNCUNDEFINED + || event == EVENT_OPTIONSET || event == EVENT_QUICKFIXCMDPOST || event == EVENT_QUICKFIXCMDPRE || event == EVENT_REMOTEREPLY diff --git a/src/nvim/option.c b/src/nvim/option.c index f5080c7a91..dbcd230186 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -1503,9 +1503,10 @@ do_set ( } else if (opt_idx >= 0) { /* string */ char_u *save_arg = NULL; char_u *s = NULL; - char_u *oldval; /* previous value if *varp */ + char_u *oldval = NULL; // previous value if *varp char_u *newval; - char_u *origval; + char_u *origval = NULL; + char_u *saved_origval = NULL; unsigned newlen; int comma; int bs; @@ -1772,14 +1773,37 @@ do_set ( /* Set the new value. */ *(char_u **)(varp) = newval; + if (!starting && origval != NULL) { + // origval may be freed by + // did_set_string_option(), make a copy. + saved_origval = vim_strsave(origval); + } + /* Handle side effects, and set the global value for * ":set" on local options. */ errmsg = did_set_string_option(opt_idx, (char_u **)varp, new_value_alloced, oldval, errbuf, opt_flags); - /* If error detected, print the error message. */ - if (errmsg != NULL) + // If error detected, print the error message. + if (errmsg != NULL) { + xfree(saved_origval); goto skip; + } + + if (saved_origval != NULL) { + char_u buf_type[7]; + vim_snprintf((char *)buf_type, ARRAY_SIZE(buf_type), "%s", + (opt_flags & OPT_LOCAL) ? "local" : "global"); + set_vim_var_string(VV_OPTION_NEW, + *(char_u **)varp, -1); + set_vim_var_string(VV_OPTION_OLD, saved_origval, -1); + set_vim_var_string(VV_OPTION_TYPE, buf_type, -1); + apply_autocmds(EVENT_OPTIONSET, + (char_u *)options[opt_idx].fullname, + NULL, false, NULL); + reset_v_option_vars(); + xfree(saved_origval); + } } else { // key code option(FIXME(tarruda): Show a warning or something // similar) @@ -2329,6 +2353,7 @@ set_string_option ( char_u *s; char_u **varp; char_u *oldval; + char_u *saved_oldval = NULL; char_u *r = NULL; if (options[opt_idx].var == NULL) /* don't set hidden option */ @@ -2342,10 +2367,30 @@ set_string_option ( : opt_flags); oldval = *varp; *varp = s; - if ((r = did_set_string_option(opt_idx, varp, TRUE, oldval, NULL, + + if (!starting) { + saved_oldval = vim_strsave(oldval); + } + + if ((r = did_set_string_option(opt_idx, varp, (int)true, oldval, NULL, opt_flags)) == NULL) did_set_option(opt_idx, opt_flags, TRUE); + // call autocommand after handling side effects + if (saved_oldval != NULL) { + char_u buf_type[7]; + vim_snprintf((char *)buf_type, ARRAY_SIZE(buf_type), "%s", + (opt_flags & OPT_LOCAL) ? "local" : "global"); + set_vim_var_string(VV_OPTION_NEW, *varp, -1); + set_vim_var_string(VV_OPTION_OLD, saved_oldval, -1); + set_vim_var_string(VV_OPTION_TYPE, buf_type, -1); + apply_autocmds(EVENT_OPTIONSET, + (char_u *)options[opt_idx].fullname, + NULL, false, NULL); + reset_v_option_vars(); + xfree(saved_oldval); + } + return r; } @@ -3814,8 +3859,29 @@ set_bool_option ( * End of handling side effects for bool options. */ + // after handling side effects, call autocommand + options[opt_idx].flags |= P_WAS_SET; + if (!starting) { + char_u buf_old[2]; + char_u buf_new[2]; + char_u buf_type[7]; + vim_snprintf((char *)buf_old, ARRAY_SIZE(buf_old), "%d", + old_value ? true: false); + vim_snprintf((char *)buf_new, ARRAY_SIZE(buf_new), "%d", + value ? true: false); + vim_snprintf((char *)buf_type, ARRAY_SIZE(buf_type), "%s", + (opt_flags & OPT_LOCAL) ? "local" : "global"); + set_vim_var_string(VV_OPTION_NEW, buf_new, -1); + set_vim_var_string(VV_OPTION_OLD, buf_old, -1); + set_vim_var_string(VV_OPTION_TYPE, buf_type, -1); + apply_autocmds(EVENT_OPTIONSET, + (char_u *) options[opt_idx].fullname, + NULL, false, NULL); + reset_v_option_vars(); + } + comp_col(); /* in case 'ruler' or 'showcmd' changed */ if (curwin->w_curswant != MAXCOL && (options[opt_idx].flags & (P_CURSWANT | P_RALL)) != 0) @@ -4187,6 +4253,23 @@ set_num_option ( options[opt_idx].flags |= P_WAS_SET; + if (!starting && errmsg == NULL) { + char_u buf_old[NUMBUFLEN]; + char_u buf_new[NUMBUFLEN]; + char_u buf_type[7]; + vim_snprintf((char *)buf_old, ARRAY_SIZE(buf_old), "%ld", old_value); + vim_snprintf((char *)buf_new, ARRAY_SIZE(buf_new), "%ld", value); + vim_snprintf((char *)buf_type, ARRAY_SIZE(buf_type), "%s", + (opt_flags & OPT_LOCAL) ? "local" : "global"); + set_vim_var_string(VV_OPTION_NEW, buf_new, -1); + set_vim_var_string(VV_OPTION_OLD, buf_old, -1); + set_vim_var_string(VV_OPTION_TYPE, buf_type, -1); + apply_autocmds(EVENT_OPTIONSET, + (char_u *) options[opt_idx].fullname, + NULL, false, NULL); + reset_v_option_vars(); + } + comp_col(); /* in case 'columns' or 'ls' changed */ if (curwin->w_curswant != MAXCOL && (options[opt_idx].flags & (P_CURSWANT | P_RALL)) != 0) diff --git a/src/nvim/version.c b/src/nvim/version.c index 83a47c207d..2b0d6f22f2 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -206,11 +206,11 @@ static int included_patches[] = { 793, // 792, 791, - // 790, - // 789, + 790, + 789, // 788 NA - // 787, - // 786, + 787, + 786, // 785, 784, // 783 NA diff --git a/test/functional/legacy/autocmd_option_spec.lua b/test/functional/legacy/autocmd_option_spec.lua new file mode 100644 index 0000000000..855e9c6271 --- /dev/null +++ b/test/functional/legacy/autocmd_option_spec.lua @@ -0,0 +1,277 @@ +local helpers = require('test.functional.helpers') +local nvim = helpers.meths +local clear, eq, neq = helpers.clear, helpers.eq, helpers.neq +local curbuf, buf = helpers.curbuf, helpers.bufmeths +local source, execute = helpers.source, helpers.execute + +local function declare_hook_function() + source([[ + fu! AutoCommand(match, bufnr, winnr) + let l:acc = { + \ 'option' : a:match, + \ 'oldval' : v:option_old, + \ 'newval' : v:option_new, + \ 'scope' : v:option_type, + \ 'attr' : { + \ 'bufnr' : a:bufnr, + \ 'winnr' : a:winnr, + \ } + \ } + call add(g:ret, l:acc) + endfu + ]]) +end + +local function set_hook(pattern) + execute( + 'au OptionSet ' + .. pattern .. + ' :call AutoCommand(expand("<amatch>"), bufnr("%"), winnr())' + ) +end + +local function init_var() + execute('let g:ret = []') +end + +local function get_result() + local ret = nvim.get_var('ret') + init_var() + return ret +end + +local function expected_table(option, oldval, newval, scope, attr) + return { + option = option, + oldval = tostring(oldval), + newval = tostring(newval), + scope = scope, + attr = attr, + } +end + +local function expected_combination(...) + local args = {...} + local ret = get_result() + + if not (#args == #ret) then + local expecteds = {} + for _, v in pairs(args) do + table.insert(expecteds, expected_table(unpack(v))) + end + eq(expecteds, ret) + return + end + + for i, v in ipairs(args) do + local attr = v[5] + if not attr then + -- remove attr entries + ret[i].attr = nil + else + -- remove attr entries which are not required + for k in pairs(ret[i].attr) do + if not attr[k] then + ret[i].attr[k] = nil + end + end + end + eq(expected_table(unpack(v)), ret[i]) + end +end + +local function expected_empty() + eq({}, get_result()) +end + +local function make_buffer() + local old_buf = curbuf() + execute('new') + local new_buf = curbuf() + execute('wincmd p') -- move previous window + + neq(old_buf, new_buf) + eq(old_buf, curbuf()) + + return new_buf +end + +describe('au OptionSet', function() + describe('with any opton (*)', function() + + before_each(function() + clear() + declare_hook_function() + init_var() + set_hook('*') + end) + + it('should be called in setting number option', function() + execute('set nu') + expected_combination({'number', 0, 1, 'global'}) + + execute('setlocal nonu') + expected_combination({'number', 1, 0, 'local'}) + + execute('setglobal nonu') + expected_combination({'number', 1, 0, 'global'}) + end) + + it('should be called in setting autoindent option',function() + execute('setlocal ai') + expected_combination({'autoindent', 0, 1, 'local'}) + + execute('setglobal ai') + expected_combination({'autoindent', 0, 1, 'global'}) + + execute('set noai') + expected_combination({'autoindent', 1, 0, 'global'}) + end) + + it('should be called in inverting global autoindent option',function() + execute('set ai!') + expected_combination({'autoindent', 0, 1, 'global'}) + end) + + it('should be called in being unset local autoindent option',function() + execute('setlocal ai') + expected_combination({'autoindent', 0, 1, 'local'}) + + execute('setlocal ai<') + expected_combination({'autoindent', 1, 0, 'local'}) + end) + + it('should be called in setting global list and number option at the same time',function() + execute('set list nu') + expected_combination( + {'list', 0, 1, 'global'}, + {'number', 0, 1, 'global'} + ) + end) + + it('should not print anything, use :noa', function() + execute('noa set nolist nonu') + expected_empty() + end) + + it('should be called in setting local acd', function() + execute('setlocal acd') + expected_combination({'autochdir', 0, 1, 'local'}) + end) + + it('should be called in setting autoread', function() + execute('set noar') + expected_combination({'autoread', 1, 0, 'global'}) + + execute('setlocal ar') + expected_combination({'autoread', 0, 1, 'local'}) + end) + + it('should be called in inverting global autoread', function() + execute('setglobal invar') + expected_combination({'autoread', 1, 0, 'global'}) + end) + + it('should be called in setting backspace option through :let', function() + execute('let &bs=""') + expected_combination({'backspace', 'indent,eol,start', '', 'global'}) + end) + + describe('being set by setbufvar()', function() + it('should not trigger because option name is invalid', function() + execute('call setbufvar(1, "&l:bk", 1)') + expected_empty() + end) + + it('should trigger using correct option name', function() + execute('call setbufvar(1, "&backup", 1)') + expected_combination({'backup', 0, 1, 'local'}) + end) + + it('should trigger if the current buffer is different from the targetted buffer', function() + local new_buffer = make_buffer() + local new_bufnr = buf.get_number(new_buffer) + + execute('call setbufvar(' .. new_bufnr .. ', "&buftype", "nofile")') + expected_combination({'buftype', '', 'nofile', 'local', {bufnr = new_bufnr}}) + end) + end) + end) + + describe('with specific option', function() + + before_each(function() + clear() + declare_hook_function() + init_var() + end) + + it('should be called iff setting readonly', function() + set_hook('readonly') + + execute('set nu') + expected_empty() + + execute('setlocal ro') + expected_combination({'readonly', 0, 1, 'local'}) + + execute('setglobal ro') + expected_combination({'readonly', 0, 1, 'global'}) + + execute('set noro') + expected_combination({'readonly', 1, 0, 'global'}) + end) + + describe('being set by setbufvar()', function() + it('should not trigger because option name does not match with backup', function() + set_hook('backup') + + execute('call setbufvar(1, "&l:bk", 1)') + expected_empty() + end) + + it('should trigger, use correct option name backup', function() + set_hook('backup') + + execute('call setbufvar(1, "&backup", 1)') + expected_combination({'backup', 0, 1, 'local'}) + end) + + it('should trigger if the current buffer is different from the targetted buffer', function() + set_hook('buftype') + + local new_buffer = make_buffer() + local new_bufnr = buf.get_number(new_buffer) + + execute('call setbufvar(' .. new_bufnr .. ', "&buftype", "nofile")') + expected_combination({'buftype', '', 'nofile', 'local', {bufnr = new_bufnr}}) + end) + end) + + describe('being set by neovim api', function() + it('should trigger if a boolean option be set globally', function() + set_hook('autochdir') + + nvim.set_option('autochdir', true) + eq(true, nvim.get_option('autochdir')) + expected_combination({'autochdir', '0', '1', 'global'}) + end) + + it('should trigger if a number option be set globally', function() + set_hook('cmdheight') + + nvim.set_option('cmdheight', 5) + eq(5, nvim.get_option('cmdheight')) + expected_combination({'cmdheight', 1, 5, 'global'}) + end) + + it('should trigger if a string option be set globally', function() + set_hook('ambiwidth') + + nvim.set_option('ambiwidth', 'double') + eq('double', nvim.get_option('ambiwidth')) + expected_combination({'ambiwidth', 'single', 'double', 'global'}) + end) + end) + end) +end) |