diff options
-rwxr-xr-x | clint.py | 2 | ||||
-rw-r--r-- | runtime/doc/autocmd.txt | 14 | ||||
-rw-r--r-- | runtime/doc/eval.txt | 16 | ||||
-rw-r--r-- | src/nvim/auevents.lua | 1 | ||||
-rw-r--r-- | src/nvim/eval.c | 72 | ||||
-rw-r--r-- | src/nvim/eval.h | 3 | ||||
-rw-r--r-- | src/nvim/ops.c | 96 | ||||
-rw-r--r-- | src/nvim/pos.h | 6 | ||||
-rw-r--r-- | test/functional/autocmd/textyankpost_spec.lua | 216 | ||||
-rw-r--r-- | test/functional/eval/vvar_event_spec.lua | 15 |
10 files changed, 413 insertions, 28 deletions
@@ -2552,7 +2552,7 @@ def CheckBraces(filename, clean_lines, linenum, error): # If should always have a brace for blockstart in ('if', 'while', 'for'): - if Match(r'\s*{0}[^{{]*$'.format(blockstart), line): + if Match(r'\s*{0}(?!\w)[^{{]*$'.format(blockstart), line): pos = line.find(blockstart) pos = line.find('(', pos) if pos > 0: diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index e17281821c..ec5818e16f 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -308,6 +308,8 @@ Name triggered by ~ |InsertCharPre| when a character was typed in Insert mode, before inserting it +|TextYankPost| when some text is yanked or deleted + |TextChanged| after a change was made to the text in Normal mode |TextChangedI| after a change was made to the text in Insert mode @@ -722,6 +724,18 @@ InsertCharPre When a character is typed in Insert mode, It is not allowed to change the text |textlock|. The event is not triggered when 'paste' is set. + *TextYankPost* +TextYankPost Just after a |yank| or |deleting| command, but not + if the black hole register |quote_| is used nor + for |setreg()|. Pattern must be * because its + meaning may change in the future. + Sets these |v:event| keys: + operator + regcontents + regname + regtype + Recursion is ignored. + It is not allowed to change the text |textlock|. *InsertEnter* InsertEnter Just before starting Insert mode. Also for Replace mode and Virtual Replace mode. The diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index ad736e9c81..51e2b505f5 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1386,6 +1386,22 @@ v:errors Errors found by assert functions, such as |assert_true()|. < If v:errors is set to anything but a list it is made an empty list by the assert function. + *v:event* *event-variable* +v:event Dictionary of event data for the current |autocommand|. The + available keys differ per event type and are specified at the + documentation for each |event|. The possible keys are: + operator The operation performed. Unlike + |v:operator|, it is set also for an Ex + mode command. For instance, |:yank| is + translated to "|y|". + regcontents Text stored in the register as a + |readfile()|-style list of lines. + regname Requested register (e.g "x" for "xyy) + or the empty string for an unnamed + operation. + regtype Type of register as returned by + |getregtype()|. + *v:exception* *exception-variable* v:exception The value of the exception most recently caught and not finished. See also |v:throwpoint| and |throw-variables|. diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua index aa4a8d8332..8d891effae 100644 --- a/src/nvim/auevents.lua +++ b/src/nvim/auevents.lua @@ -83,6 +83,7 @@ return { 'TermResponse', -- after setting "v:termresponse" 'TextChanged', -- text was modified 'TextChangedI', -- text was modified in Insert mode + 'TextYankPost', -- after a yank or delete was done (y, d, c) 'User', -- user defined autocommand 'VimEnter', -- after starting Vim 'VimLeave', -- before exiting Vim diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 76deedfad0..33b8415336 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -378,6 +378,7 @@ static struct vimvar { { VV_NAME("option_type", VAR_STRING), VV_RO }, { VV_NAME("errors", VAR_LIST), 0 }, { VV_NAME("msgpack_types", VAR_DICT), VV_RO }, + { VV_NAME("event", VAR_DICT), VV_RO }, }; /* shorthand */ @@ -545,6 +546,10 @@ void eval_init(void) set_vim_var_dict(VV_MSGPACK_TYPES, msgpack_types_dict); set_vim_var_dict(VV_COMPLETED_ITEM, dict_alloc()); + + dict_T *v_event = dict_alloc(); + v_event->dv_lock = VAR_FIXED; + set_vim_var_dict(VV_EVENT, v_event); set_vim_var_list(VV_ERRORS, list_alloc()); set_vim_var_nr(VV_SEARCHFORWARD, 1L); set_vim_var_nr(VV_HLSEARCH, 1L); @@ -6017,6 +6022,27 @@ static void rettv_dict_alloc(typval_T *rettv) ++d->dv_refcount; } +/// Clear all the keys of a Dictionary. "d" remains a valid empty Dictionary. +/// +/// @param d The Dictionary to clear +void dict_clear(dict_T *d) + FUNC_ATTR_NONNULL_ALL +{ + hash_lock(&d->dv_hashtab); + assert(d->dv_hashtab.ht_locked > 0); + + size_t todo = d->dv_hashtab.ht_used; + for (hashitem_T *hi = d->dv_hashtab.ht_array; todo > 0; hi++) { + if (!HASHITEM_EMPTY(hi)) { + dictitem_free(HI2DI(hi)); + hash_remove(&d->dv_hashtab, hi); + todo--; + } + } + + hash_unlock(&d->dv_hashtab); +} + /* * Unreference a Dictionary: decrement the reference count and free it when it @@ -6258,6 +6284,24 @@ int dict_add_list(dict_T *d, char *key, list_T *list) return OK; } +/// Set all existing keys in "dict" as read-only. +/// +/// This does not protect against adding new keys to the Dictionary. +/// +/// @param dict The dict whose keys should be frozen +void dict_set_keys_readonly(dict_T *dict) + FUNC_ATTR_NONNULL_ALL +{ + size_t todo = dict->dv_hashtab.ht_used; + for (hashitem_T *hi = dict->dv_hashtab.ht_array; todo > 0 ; hi++) { + if (HASHITEM_EMPTY(hi)) { + continue; + } + todo--; + HI2DI(hi)->di_flags |= DI_FLAGS_RO | DI_FLAGS_FIX; + } +} + /* * Get the number of items in a Dictionary. */ @@ -10579,8 +10623,6 @@ static void f_getregtype(typval_T *argvars, typval_T *rettv) { char_u *strregname; int regname; - char_u buf[NUMBUFLEN + 2]; - long reglen = 0; if (argvars[0].v_type != VAR_UNKNOWN) { strregname = get_tv_string_chk(&argvars[0]); @@ -10597,18 +10639,13 @@ static void f_getregtype(typval_T *argvars, typval_T *rettv) if (regname == 0) regname = '"'; - buf[0] = NUL; - buf[1] = NUL; - switch (get_reg_type(regname, ®len)) { - case MLINE: buf[0] = 'V'; break; - case MCHAR: buf[0] = 'v'; break; - case MBLOCK: - buf[0] = Ctrl_V; - sprintf((char *)buf + 1, "%" PRId64, (int64_t)(reglen + 1)); - break; - } + colnr_T reglen = 0; + char buf[NUMBUFLEN + 2]; + char_u reg_type = get_reg_type(regname, ®len); + format_reg_type(reg_type, reglen, buf, ARRAY_SIZE(buf)); + rettv->v_type = VAR_STRING; - rettv->vval.v_string = vim_strsave(buf); + rettv->vval.v_string = (char_u *)xstrdup(buf); } /* @@ -18162,14 +18199,7 @@ void set_vim_var_dict(int idx, dict_T *val) if (val != NULL) { ++val->dv_refcount; // Set readonly - size_t todo = val->dv_hashtab.ht_used; - for (hashitem_T *hi = val->dv_hashtab.ht_array; todo > 0 ; ++hi) { - if (HASHITEM_EMPTY(hi)) { - continue; - } - --todo; - HI2DI(hi)->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX; - } + dict_set_keys_readonly(val); } } diff --git a/src/nvim/eval.h b/src/nvim/eval.h index 79a1341d98..f51b0f4921 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -113,7 +113,8 @@ enum { VV_OPTION_TYPE, VV_ERRORS, VV_MSGPACK_TYPES, - VV_LEN, /* number of v: vars */ + VV_EVENT, + VV_LEN, // number of v: vars }; /// Maximum number of function arguments diff --git a/src/nvim/ops.c b/src/nvim/ops.c index b1adc85e1d..ab6e0d2e7d 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -19,6 +19,7 @@ #include "nvim/ex_cmds.h" #include "nvim/ex_cmds2.h" #include "nvim/ex_getln.h" +#include "nvim/fileio.h" #include "nvim/fold.h" #include "nvim/getchar.h" #include "nvim/indent.h" @@ -1409,8 +1410,9 @@ int op_delete(oparg_T *oap) op_yank_reg(oap, false, reg, false); } - if(oap->regname == 0) { + if (oap->regname == 0) { set_clipboard(0, reg); + yank_do_autocmd(oap, reg); } } @@ -2309,6 +2311,8 @@ bool op_yank(oparg_T *oap, bool message) yankreg_T *reg = get_yank_register(oap->regname, YREG_YANK); op_yank_reg(oap, message, reg, is_append_register(oap->regname)); set_clipboard(oap->regname, reg); + yank_do_autocmd(oap, reg); + return true; } @@ -2524,6 +2528,58 @@ static void yank_copy_line(yankreg_T *reg, struct block_def *bd, long y_idx) *pnew = NUL; } +/// Execute autocommands for TextYankPost. +/// +/// @param oap Operator arguments. +/// @param reg The yank register used. +static void yank_do_autocmd(oparg_T *oap, yankreg_T *reg) + FUNC_ATTR_NONNULL_ALL +{ + static bool recursive = false; + + if (recursive || !has_event(EVENT_TEXTYANKPOST)) { + // No autocommand was defined + // or we yanked from this autocommand. + return; + } + + recursive = true; + + // set v:event to a dictionary with information about the yank + dict_T *dict = get_vim_var_dict(VV_EVENT); + + // the yanked text + list_T *list = list_alloc(); + for (linenr_T i = 0; i < reg->y_size; i++) { + list_append_string(list, reg->y_array[i], -1); + } + list->lv_lock = VAR_FIXED; + dict_add_list(dict, "regcontents", list); + + // the register type + char buf[NUMBUFLEN+2]; + format_reg_type(reg->y_type, reg->y_width, buf, ARRAY_SIZE(buf)); + dict_add_nr_str(dict, "regtype", 0, (char_u *)buf); + + // name of requested register or the empty string for an unnamed operation. + buf[0] = (char)oap->regname; + buf[1] = NUL; + dict_add_nr_str(dict, "regname", 0, (char_u *)buf); + + // kind of operation (yank/delete/change) + buf[0] = get_op_char(oap->op_type); + buf[1] = NUL; + dict_add_nr_str(dict, "operator", 0, (char_u *)buf); + + dict_set_keys_readonly(dict); + textlock++; + apply_autocmds(EVENT_TEXTYANKPOST, NULL, NULL, false, curbuf); + textlock--; + dict_clear(dict); + + recursive = false; +} + /* * Put contents of register "regname" into the text. @@ -4631,7 +4687,7 @@ theend: * Used for getregtype() * Returns MAUTO for error. */ -char_u get_reg_type(int regname, long *reglen) +char_u get_reg_type(int regname, colnr_T *reg_width) { switch (regname) { case '%': /* file name */ @@ -4654,13 +4710,45 @@ char_u get_reg_type(int regname, long *reglen) yankreg_T *reg = get_yank_register(regname, YREG_PASTE); if (reg->y_array != NULL) { - if (reglen != NULL && reg->y_type == MBLOCK) - *reglen = reg->y_width; + if (reg_width != NULL && reg->y_type == MBLOCK) { + *reg_width = reg->y_width; + } return reg->y_type; } return MAUTO; } +/// Format the register type as a string. +/// +/// @param reg_type The register type. +/// @param reg_width The width, only used if "reg_type" is MBLOCK. +/// @param[out] buf Buffer to store formatted string. The allocated size should +/// be at least NUMBUFLEN+2 to always fit the value. +/// @param buf_len The allocated size of the buffer. +void format_reg_type(char_u reg_type, colnr_T reg_width, + char* buf, size_t buf_len) + FUNC_ATTR_NONNULL_ALL +{ + assert(buf_len > 1); + switch (reg_type) { + case MLINE: + buf[0] = 'V'; + buf[1] = NUL; + break; + case MCHAR: + buf[0] = 'v'; + buf[1] = NUL; + break; + case MBLOCK: + snprintf(buf, buf_len, CTRL_V_STR "%" PRIdCOLNR, reg_width + 1); + break; + case MAUTO: + buf[0] = NUL; + break; + } +} + + /// When `flags` has `kGRegList` return a list with text `s`. /// Otherwise just return `s`. /// diff --git a/src/nvim/pos.h b/src/nvim/pos.h index 7071df51e8..864f3fe866 100644 --- a/src/nvim/pos.h +++ b/src/nvim/pos.h @@ -2,7 +2,11 @@ #define NVIM_POS_H typedef long linenr_T; // line number type -typedef int colnr_T; // column number type + +/// Column number type +typedef int colnr_T; +/// Format used to print values which have colnr_T type +#define PRIdCOLNR "d" #define MAXLNUM 0x7fffffff // maximum (invalid) line number #define MAXCOL 0x7fffffff // maximum column number, 31 bits diff --git a/test/functional/autocmd/textyankpost_spec.lua b/test/functional/autocmd/textyankpost_spec.lua new file mode 100644 index 0000000000..965b19581a --- /dev/null +++ b/test/functional/autocmd/textyankpost_spec.lua @@ -0,0 +1,216 @@ +local helpers = require('test.functional.helpers') +local clear, eval, eq, insert = helpers.clear, helpers.eval, helpers.eq, helpers.insert +local feed, execute, expect, command = helpers.feed, helpers.execute, helpers.expect, helpers.command +local curbufmeths, funcs, neq = helpers.curbufmeths, helpers.funcs, helpers.neq + +describe('TextYankPost', function() + before_each(function() + clear() + + -- emulate the clipboard so system clipboard isn't affected + execute('let &rtp = "test/functional/fixtures,".&rtp') + + execute('let g:count = 0') + execute('autocmd TextYankPost * let g:event = copy(v:event)') + execute('autocmd TextYankPost * let g:count += 1') + + curbufmeths.set_line_slice(0, -1, true, true, { + 'foo\0bar', + 'baz text', + }) + end) + + it('is executed after yank and handles register types', function() + feed('yy') + eq({ + operator = 'y', + regcontents = { 'foo\nbar' }, + regname = '', + regtype = 'V' + }, eval('g:event')) + eq(1, eval('g:count')) + + -- v:event is cleared after the autocommand is done + eq({}, eval('v:event')) + + feed('+yw') + eq({ + operator = 'y', + regcontents = { 'baz ' }, + regname = '', + regtype = 'v' + }, eval('g:event')) + eq(2, eval('g:count')) + + feed('<c-v>eky') + eq({ + operator = 'y', + regcontents = { 'foo', 'baz' }, + regname = '', + regtype = "\0223" -- ^V + block width + }, eval('g:event')) + eq(3, eval('g:count')) + end) + + it('makes v:event immutable', function() + feed('yy') + eq({ + operator = 'y', + regcontents = { 'foo\nbar' }, + regname = '', + regtype = 'V' + }, eval('g:event')) + + execute('set debug=msg') + -- the regcontents should not be changed without copy. + local status, err = pcall(command,'call extend(g:event.regcontents, ["more text"])') + eq(status,false) + neq(nil, string.find(err, ':E742:')) + + -- can't mutate keys inside the autocommand + execute('autocmd! TextYankPost * let v:event.regcontents = 0') + status, err = pcall(command,'normal yy') + eq(status,false) + neq(nil, string.find(err, ':E46:')) + + -- can't add keys inside the autocommand + execute('autocmd! TextYankPost * let v:event.mykey = 0') + status, err = pcall(command,'normal yy') + eq(status,false) + neq(nil, string.find(err, ':E742:')) + end) + + it('is not invoked recursively', function() + execute('autocmd TextYankPost * normal "+yy') + feed('yy') + eq({ + operator = 'y', + regcontents = { 'foo\nbar' }, + regname = '', + regtype = 'V' + }, eval('g:event')) + eq(1, eval('g:count')) + eq({ 'foo\nbar' }, funcs.getreg('+',1,1)) + end) + + it('is executed after delete and change', function() + feed('dw') + eq({ + operator = 'd', + regcontents = { 'foo' }, + regname = '', + regtype = 'v' + }, eval('g:event')) + eq(1, eval('g:count')) + + feed('dd') + eq({ + operator = 'd', + regcontents = { '\nbar' }, + regname = '', + regtype = 'V' + }, eval('g:event')) + eq(2, eval('g:count')) + + feed('cwspam<esc>') + eq({ + operator = 'c', + regcontents = { 'baz' }, + regname = '', + regtype = 'v' + }, eval('g:event')) + eq(3, eval('g:count')) + end) + + it('is not executed after black-hole operation', function() + feed('"_dd') + eq(0, eval('g:count')) + + feed('"_cwgood<esc>') + eq(0, eval('g:count')) + + expect([[ + good text]]) + feed('"_yy') + eq(0, eval('g:count')) + + execute('delete _') + eq(0, eval('g:count')) + end) + + it('gives the correct register name', function() + feed('$"byiw') + eq({ + operator = 'y', + regcontents = { 'bar' }, + regname = 'b', + regtype = 'v' + }, eval('g:event')) + + feed('"*yy') + eq({ + operator = 'y', + regcontents = { 'foo\nbar' }, + regname = '*', + regtype = 'V' + }, eval('g:event')) + + execute("set clipboard=unnamed") + + -- regname still shows the name the user requested + feed('yy') + eq({ + operator = 'y', + regcontents = { 'foo\nbar' }, + regname = '', + regtype = 'V' + }, eval('g:event')) + + feed('"*yy') + eq({ + operator = 'y', + regcontents = { 'foo\nbar' }, + regname = '*', + regtype = 'V' + }, eval('g:event')) + end) + + it('works with Ex commands', function() + execute('1delete +') + eq({ + operator = 'd', + regcontents = { 'foo\nbar' }, + regname = '+', + regtype = 'V' + }, eval('g:event')) + eq(1, eval('g:count')) + + execute('yank') + eq({ + operator = 'y', + regcontents = { 'baz text' }, + regname = '', + regtype = 'V' + }, eval('g:event')) + eq(2, eval('g:count')) + + execute('normal yw') + eq({ + operator = 'y', + regcontents = { 'baz ' }, + regname = '', + regtype = 'v' + }, eval('g:event')) + eq(3, eval('g:count')) + + execute('normal! dd') + eq({ + operator = 'd', + regcontents = { 'baz text' }, + regname = '', + regtype = 'V' + }, eval('g:event')) + eq(4, eval('g:count')) + end) + +end) diff --git a/test/functional/eval/vvar_event_spec.lua b/test/functional/eval/vvar_event_spec.lua new file mode 100644 index 0000000000..bbac86524f --- /dev/null +++ b/test/functional/eval/vvar_event_spec.lua @@ -0,0 +1,15 @@ +local helpers = require('test.functional.helpers') +local clear, eval, eq = helpers.clear, helpers.eval, helpers.eq +local command = helpers.command +describe('v:event', function() + before_each(clear) + it('is empty before any autocommand', function() + eq({}, eval('v:event')) + end) + + it('is immutable', function() + eq(false, pcall(command, 'let v:event = {}')) + eq(false, pcall(command, 'let v:event.mykey = {}')) + end) +end) + |