Merge pull request #4304 from bfredl/yank

Add v:event variable and TextYankPost autocommand
This commit is contained in:
Björn Linse 2016-02-29 16:09:28 +01:00
commit 1dd986562f
10 changed files with 413 additions and 28 deletions

View File

@ -2552,7 +2552,7 @@ def CheckBraces(filename, clean_lines, linenum, error):
# If should always have a brace # If should always have a brace
for blockstart in ('if', 'while', 'for'): 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(blockstart)
pos = line.find('(', pos) pos = line.find('(', pos)
if pos > 0: if pos > 0:

View File

@ -308,6 +308,8 @@ Name triggered by ~
|InsertCharPre| when a character was typed in Insert mode, before |InsertCharPre| when a character was typed in Insert mode, before
inserting it inserting it
|TextYankPost| when some text is yanked or deleted
|TextChanged| after a change was made to the text in Normal mode |TextChanged| after a change was made to the text in Normal mode
|TextChangedI| after a change was made to the text in Insert 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|. It is not allowed to change the text |textlock|.
The event is not triggered when 'paste' is The event is not triggered when 'paste' is
set. 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*
InsertEnter Just before starting Insert mode. Also for InsertEnter Just before starting Insert mode. Also for
Replace mode and Virtual Replace mode. The Replace mode and Virtual Replace mode. The

View File

@ -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 < If v:errors is set to anything but a list it is made an empty
list by the assert function. 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* *exception-variable*
v:exception The value of the exception most recently caught and not v:exception The value of the exception most recently caught and not
finished. See also |v:throwpoint| and |throw-variables|. finished. See also |v:throwpoint| and |throw-variables|.

View File

@ -83,6 +83,7 @@ return {
'TermResponse', -- after setting "v:termresponse" 'TermResponse', -- after setting "v:termresponse"
'TextChanged', -- text was modified 'TextChanged', -- text was modified
'TextChangedI', -- text was modified in Insert mode 'TextChangedI', -- text was modified in Insert mode
'TextYankPost', -- after a yank or delete was done (y, d, c)
'User', -- user defined autocommand 'User', -- user defined autocommand
'VimEnter', -- after starting Vim 'VimEnter', -- after starting Vim
'VimLeave', -- before exiting Vim 'VimLeave', -- before exiting Vim

View File

@ -378,6 +378,7 @@ static struct vimvar {
{ VV_NAME("option_type", VAR_STRING), VV_RO }, { VV_NAME("option_type", VAR_STRING), VV_RO },
{ VV_NAME("errors", VAR_LIST), 0 }, { VV_NAME("errors", VAR_LIST), 0 },
{ VV_NAME("msgpack_types", VAR_DICT), VV_RO }, { VV_NAME("msgpack_types", VAR_DICT), VV_RO },
{ VV_NAME("event", VAR_DICT), VV_RO },
}; };
/* shorthand */ /* shorthand */
@ -545,6 +546,10 @@ void eval_init(void)
set_vim_var_dict(VV_MSGPACK_TYPES, msgpack_types_dict); set_vim_var_dict(VV_MSGPACK_TYPES, msgpack_types_dict);
set_vim_var_dict(VV_COMPLETED_ITEM, dict_alloc()); 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_list(VV_ERRORS, list_alloc());
set_vim_var_nr(VV_SEARCHFORWARD, 1L); set_vim_var_nr(VV_SEARCHFORWARD, 1L);
set_vim_var_nr(VV_HLSEARCH, 1L); set_vim_var_nr(VV_HLSEARCH, 1L);
@ -6017,6 +6022,27 @@ static void rettv_dict_alloc(typval_T *rettv)
++d->dv_refcount; ++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 * 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; 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. * 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; char_u *strregname;
int regname; int regname;
char_u buf[NUMBUFLEN + 2];
long reglen = 0;
if (argvars[0].v_type != VAR_UNKNOWN) { if (argvars[0].v_type != VAR_UNKNOWN) {
strregname = get_tv_string_chk(&argvars[0]); strregname = get_tv_string_chk(&argvars[0]);
@ -10597,18 +10639,13 @@ static void f_getregtype(typval_T *argvars, typval_T *rettv)
if (regname == 0) if (regname == 0)
regname = '"'; regname = '"';
buf[0] = NUL; colnr_T reglen = 0;
buf[1] = NUL; char buf[NUMBUFLEN + 2];
switch (get_reg_type(regname, &reglen)) { char_u reg_type = get_reg_type(regname, &reglen);
case MLINE: buf[0] = 'V'; break; format_reg_type(reg_type, reglen, buf, ARRAY_SIZE(buf));
case MCHAR: buf[0] = 'v'; break;
case MBLOCK:
buf[0] = Ctrl_V;
sprintf((char *)buf + 1, "%" PRId64, (int64_t)(reglen + 1));
break;
}
rettv->v_type = VAR_STRING; 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) { if (val != NULL) {
++val->dv_refcount; ++val->dv_refcount;
// Set readonly // Set readonly
size_t todo = val->dv_hashtab.ht_used; dict_set_keys_readonly(val);
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;
}
} }
} }

View File

@ -113,7 +113,8 @@ enum {
VV_OPTION_TYPE, VV_OPTION_TYPE,
VV_ERRORS, VV_ERRORS,
VV_MSGPACK_TYPES, VV_MSGPACK_TYPES,
VV_LEN, /* number of v: vars */ VV_EVENT,
VV_LEN, // number of v: vars
}; };
/// Maximum number of function arguments /// Maximum number of function arguments

View File

@ -19,6 +19,7 @@
#include "nvim/ex_cmds.h" #include "nvim/ex_cmds.h"
#include "nvim/ex_cmds2.h" #include "nvim/ex_cmds2.h"
#include "nvim/ex_getln.h" #include "nvim/ex_getln.h"
#include "nvim/fileio.h"
#include "nvim/fold.h" #include "nvim/fold.h"
#include "nvim/getchar.h" #include "nvim/getchar.h"
#include "nvim/indent.h" #include "nvim/indent.h"
@ -1409,8 +1410,9 @@ int op_delete(oparg_T *oap)
op_yank_reg(oap, false, reg, false); op_yank_reg(oap, false, reg, false);
} }
if(oap->regname == 0) { if (oap->regname == 0) {
set_clipboard(0, reg); 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); yankreg_T *reg = get_yank_register(oap->regname, YREG_YANK);
op_yank_reg(oap, message, reg, is_append_register(oap->regname)); op_yank_reg(oap, message, reg, is_append_register(oap->regname));
set_clipboard(oap->regname, reg); set_clipboard(oap->regname, reg);
yank_do_autocmd(oap, reg);
return true; return true;
} }
@ -2524,6 +2528,58 @@ static void yank_copy_line(yankreg_T *reg, struct block_def *bd, long y_idx)
*pnew = NUL; *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. * Put contents of register "regname" into the text.
@ -4631,7 +4687,7 @@ theend:
* Used for getregtype() * Used for getregtype()
* Returns MAUTO for error. * 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) { switch (regname) {
case '%': /* file name */ 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); yankreg_T *reg = get_yank_register(regname, YREG_PASTE);
if (reg->y_array != NULL) { if (reg->y_array != NULL) {
if (reglen != NULL && reg->y_type == MBLOCK) if (reg_width != NULL && reg->y_type == MBLOCK) {
*reglen = reg->y_width; *reg_width = reg->y_width;
}
return reg->y_type; return reg->y_type;
} }
return MAUTO; 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`. /// When `flags` has `kGRegList` return a list with text `s`.
/// Otherwise just return `s`. /// Otherwise just return `s`.
/// ///

View File

@ -2,7 +2,11 @@
#define NVIM_POS_H #define NVIM_POS_H
typedef long linenr_T; // line number type 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 MAXLNUM 0x7fffffff // maximum (invalid) line number
#define MAXCOL 0x7fffffff // maximum column number, 31 bits #define MAXCOL 0x7fffffff // maximum column number, 31 bits

View File

@ -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)

View File

@ -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)