feat(api): implement nvim_{add,del}_user_command

Add support for adding and removing custom user commands with the Nvim
API.
This commit is contained in:
Gregory Anders 2021-12-22 11:40:21 -07:00
parent 9804a2870f
commit eff11b3c3f
17 changed files with 740 additions and 99 deletions

View File

@ -626,6 +626,56 @@ nvim__stats() *nvim__stats()*
Return: ~ Return: ~
Map of various internal stats. Map of various internal stats.
*nvim_add_user_command()*
nvim_add_user_command({name}, {command}, {*opts})
Create a new user command |user-commands|
{name} is the name of the new command. The name must begin
with an uppercase letter.
{command} is the replacement text or Lua function to execute.
Example: >
:call nvim_add_user_command('SayHello', 'echo "Hello world!"', {})
:SayHello
Hello world!
<
Parameters: ~
{name} Name of the new user command. Must begin with
an uppercase letter.
{command} Replacement command to execute when this user
command is executed. When called from Lua, the
command can also be a Lua function. The
function is called with a single table argument
that contains the following keys:
• args: (string) The args passed to the
command, if any |<args>|
• bang: (boolean) "true" if the command was
executed with a ! modifier |<bang>|
• line1: (number) The starting line of the
command range |<line1>|
• line2: (number) The final line of the command
range |<line2>|
• range: (number) The number of items in the
command range: 0, 1, or 2 |<range>|
• count: (number) Any count supplied |<count>|
• reg: (string) The optional register, if
specified |<reg>|
• mods: (string) Command modifiers, if any
|<mods>|
{opts} Optional command attributes. See
|command-attributes| for more details. To use
boolean attributes (such as |:command-bang| or
|:command-bar|) set the value to "true". When
using a Lua function for {command} you can also
provide a "desc" key that will be displayed
when listing commands. In addition to the
string options listed in |:command-complete|,
the "complete" key also accepts a Lua function
which works like the "customlist" completion
mode |:command-complete-customlist|.
nvim_call_atomic({calls}) *nvim_call_atomic()* nvim_call_atomic({calls}) *nvim_call_atomic()*
Calls many API methods atomically. Calls many API methods atomically.
@ -714,6 +764,12 @@ nvim_del_mark({name}) *nvim_del_mark()*
|nvim_buf_del_mark()| |nvim_buf_del_mark()|
|nvim_get_mark()| |nvim_get_mark()|
nvim_del_user_command({name}) *nvim_del_user_command()*
Delete a user-defined command.
Parameters: ~
{name} Name of the command to delete.
nvim_del_var({name}) *nvim_del_var()* nvim_del_var({name}) *nvim_del_var()*
Removes a global (g:) variable. Removes a global (g:) variable.
@ -1790,6 +1846,16 @@ nvim__buf_redraw_range({buffer}, {first}, {last})
nvim__buf_stats({buffer}) *nvim__buf_stats()* nvim__buf_stats({buffer}) *nvim__buf_stats()*
TODO: Documentation TODO: Documentation
*nvim_buf_add_user_command()*
nvim_buf_add_user_command({buffer}, {name}, {command}, {*opts})
Create a new user command |user-commands| in the given buffer.
Parameters: ~
{buffer} Buffer handle, or 0 for current buffer.
See also: ~
nvim_add_user_command
nvim_buf_attach({buffer}, {send_buffer}, {opts}) *nvim_buf_attach()* nvim_buf_attach({buffer}, {send_buffer}, {opts}) *nvim_buf_attach()*
Activates buffer-update events on a channel, or as Lua Activates buffer-update events on a channel, or as Lua
callbacks. callbacks.
@ -1925,6 +1991,18 @@ nvim_buf_del_mark({buffer}, {name}) *nvim_buf_del_mark()*
|nvim_buf_set_mark()| |nvim_buf_set_mark()|
|nvim_del_mark()| |nvim_del_mark()|
*nvim_buf_del_user_command()*
nvim_buf_del_user_command({buffer}, {name})
Delete a buffer-local user-defined command.
Only commands created with |:command-buffer| or
|nvim_buf_add_user_command()| can be deleted with this
function.
Parameters: ~
{buffer} Buffer handle, or 0 for current buffer.
{name} Name of the command to delete.
nvim_buf_del_var({buffer}, {name}) *nvim_buf_del_var()* nvim_buf_del_var({buffer}, {name}) *nvim_buf_del_var()*
Removes a buffer-scoped (b:) variable Removes a buffer-scoped (b:) variable

View File

@ -1247,8 +1247,8 @@ See |:verbose-cmd| for more information.
Command attributes ~ Command attributes ~
*command-attributes*
User-defined commands are treated by Vim just like any other Ex commands. They User-defined commands are treated by Nvim just like any other Ex commands. They
can have arguments, or have a range specified. Arguments are subject to can have arguments, or have a range specified. Arguments are subject to
completion as filenames, buffers, etc. Exactly how this works depends upon the completion as filenames, buffers, etc. Exactly how this works depends upon the
command's attributes, which are specified when the command is defined. command's attributes, which are specified when the command is defined.

View File

@ -1273,6 +1273,63 @@ Object nvim_buf_call(Buffer buffer, LuaRef fun, Error *err)
return res; return res;
} }
/// Create a new user command |user-commands| in the given buffer.
///
/// @param buffer Buffer handle, or 0 for current buffer.
/// @param[out] err Error details, if any.
/// @see nvim_add_user_command
void nvim_buf_add_user_command(Buffer buffer, String name, Object command,
Dict(user_command) *opts, Error *err)
FUNC_API_SINCE(9)
{
buf_T *target_buf = find_buffer_by_handle(buffer, err);
if (ERROR_SET(err)) {
return;
}
buf_T *save_curbuf = curbuf;
curbuf = target_buf;
add_user_command(name, command, opts, UC_BUFFER, err);
curbuf = save_curbuf;
}
/// Delete a buffer-local user-defined command.
///
/// Only commands created with |:command-buffer| or
/// |nvim_buf_add_user_command()| can be deleted with this function.
///
/// @param buffer Buffer handle, or 0 for current buffer.
/// @param name Name of the command to delete.
/// @param[out] err Error details, if any.
void nvim_buf_del_user_command(Buffer buffer, String name, Error *err)
FUNC_API_SINCE(9)
{
garray_T *gap;
if (buffer == -1) {
gap = &ucmds;
} else {
buf_T *buf = find_buffer_by_handle(buffer, err);
gap = &buf->b_ucmds;
}
for (int i = 0; i < gap->ga_len; i++) {
ucmd_T *cmd = USER_CMD_GA(gap, i);
if (!STRCMP(name.data, cmd->uc_name)) {
free_ucmd(cmd);
gap->ga_len -= 1;
if (i < gap->ga_len) {
memmove(cmd, cmd + 1, (size_t)(gap->ga_len - i) * sizeof(ucmd_T));
}
return;
}
}
api_set_error(err, kErrorTypeException, "No such user-defined command: %s", name.data);
}
Dictionary nvim__buf_stats(Buffer buffer, Error *err) Dictionary nvim__buf_stats(Buffer buffer, Error *err)
{ {
Dictionary rv = ARRAY_DICT_INIT; Dictionary rv = ARRAY_DICT_INIT;

View File

@ -33,6 +33,18 @@ return {
get_commands = { get_commands = {
"builtin"; "builtin";
}; };
user_command = {
"addr";
"bang";
"bar";
"complete";
"count";
"desc";
"force";
"nargs";
"range";
"register";
};
float_config = { float_config = {
"row"; "row";
"col"; "col";

View File

@ -961,6 +961,10 @@ Object copy_object(Object obj)
case kObjectTypeDictionary: case kObjectTypeDictionary:
return DICTIONARY_OBJ(copy_dictionary(obj.data.dictionary)); return DICTIONARY_OBJ(copy_dictionary(obj.data.dictionary));
case kObjectTypeLuaRef:
return LUAREF_OBJ(api_new_luaref(obj.data.luaref));
default: default:
abort(); abort();
} }
@ -1342,3 +1346,184 @@ const char *get_default_stl_hl(win_T *wp)
return "StatusLineNC"; return "StatusLineNC";
} }
} }
void add_user_command(String name, Object command, Dict(user_command) *opts, int flags, Error *err)
{
uint32_t argt = 0;
long def = -1;
cmd_addr_T addr_type_arg = ADDR_NONE;
int compl = EXPAND_NOTHING;
char *compl_arg = NULL;
char *rep = NULL;
LuaRef luaref = LUA_NOREF;
LuaRef compl_luaref = LUA_NOREF;
if (HAS_KEY(opts->range) && HAS_KEY(opts->count)) {
api_set_error(err, kErrorTypeValidation, "'range' and 'count' are mutually exclusive");
goto err;
}
if (opts->nargs.type == kObjectTypeInteger) {
switch (opts->nargs.data.integer) {
case 0:
// Default value, nothing to do
break;
case 1:
argt |= EX_EXTRA | EX_NOSPC | EX_NEEDARG;
break;
default:
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
goto err;
}
} else if (opts->nargs.type == kObjectTypeString) {
if (opts->nargs.data.string.size > 1) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
goto err;
}
switch (opts->nargs.data.string.data[0]) {
case '*':
argt |= EX_EXTRA;
break;
case '?':
argt |= EX_EXTRA | EX_NOSPC;
break;
case '+':
argt |= EX_EXTRA | EX_NEEDARG;
break;
default:
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
goto err;
}
} else if (HAS_KEY(opts->nargs)) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
goto err;
}
if (HAS_KEY(opts->complete) && !argt) {
api_set_error(err, kErrorTypeValidation, "'complete' used without 'nargs'");
goto err;
}
if (opts->range.type == kObjectTypeBoolean) {
if (opts->range.data.boolean) {
argt |= EX_RANGE;
addr_type_arg = ADDR_LINES;
}
} else if (opts->range.type == kObjectTypeString) {
if (opts->range.data.string.data[0] == '%' && opts->range.data.string.size == 1) {
argt |= EX_RANGE | EX_DFLALL;
addr_type_arg = ADDR_LINES;
} else {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'");
goto err;
}
} else if (opts->range.type == kObjectTypeInteger) {
argt |= EX_RANGE | EX_ZEROR;
def = opts->range.data.integer;
addr_type_arg = ADDR_LINES;
} else if (HAS_KEY(opts->range)) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'");
goto err;
}
if (opts->count.type == kObjectTypeBoolean) {
if (opts->count.data.boolean) {
argt |= EX_COUNT | EX_ZEROR | EX_RANGE;
addr_type_arg = ADDR_OTHER;
def = 0;
}
} else if (opts->count.type == kObjectTypeInteger) {
argt |= EX_COUNT | EX_ZEROR | EX_RANGE;
addr_type_arg = ADDR_OTHER;
def = opts->count.data.integer;
} else if (HAS_KEY(opts->count)) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'count'");
goto err;
}
if (opts->addr.type == kObjectTypeString) {
if (parse_addr_type_arg((char_u *)opts->addr.data.string.data, (int)opts->addr.data.string.size,
&addr_type_arg) != OK) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'");
goto err;
}
if (addr_type_arg != ADDR_LINES) {
argt |= EX_ZEROR;
}
} else if (HAS_KEY(opts->addr)) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'");
goto err;
}
if (api_object_to_bool(opts->bang, "bang", false, err)) {
argt |= EX_BANG;
} else if (ERROR_SET(err)) {
goto err;
}
if (api_object_to_bool(opts->bar, "bar", false, err)) {
argt |= EX_TRLBAR;
} else if (ERROR_SET(err)) {
goto err;
}
if (api_object_to_bool(opts->register_, "register", false, err)) {
argt |= EX_REGSTR;
} else if (ERROR_SET(err)) {
goto err;
}
bool force = api_object_to_bool(opts->force, "force", false, err);
if (ERROR_SET(err)) {
goto err;
}
if (opts->complete.type == kObjectTypeLuaRef) {
compl = EXPAND_USER_LUA;
compl_luaref = api_new_luaref(opts->complete.data.luaref);
} else if (opts->complete.type == kObjectTypeString) {
if (parse_compl_arg((char_u *)opts->complete.data.string.data,
(int)opts->complete.data.string.size, &compl, &argt,
(char_u **)&compl_arg) != OK) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'");
goto err;
}
} else if (HAS_KEY(opts->complete)) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'");
goto err;
}
switch (command.type) {
case kObjectTypeLuaRef:
luaref = api_new_luaref(command.data.luaref);
if (opts->desc.type == kObjectTypeString) {
rep = opts->desc.data.string.data;
} else {
snprintf((char *)IObuff, IOSIZE, "<Lua function %d>", luaref);
rep = (char *)IObuff;
}
break;
case kObjectTypeString:
rep = command.data.string.data;
break;
default:
api_set_error(err, kErrorTypeValidation, "'command' must be a string or Lua function");
goto err;
}
if (uc_add_command((char_u *)name.data, name.size, (char_u *)rep, argt, def, flags,
compl, (char_u *)compl_arg, compl_luaref, addr_type_arg, luaref,
force) != OK) {
api_set_error(err, kErrorTypeException, "Failed to create user command");
goto err;
}
return;
err:
NLUA_CLEAR_REF(luaref);
NLUA_CLEAR_REF(compl_luaref);
}

View File

@ -2363,3 +2363,52 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *
return result; return result;
} }
/// Create a new user command |user-commands|
///
/// {name} is the name of the new command. The name must begin with an uppercase letter.
///
/// {command} is the replacement text or Lua function to execute.
///
/// Example:
/// <pre>
/// :call nvim_add_user_command('SayHello', 'echo "Hello world!"', {})
/// :SayHello
/// Hello world!
/// </pre>
///
/// @param name Name of the new user command. Must begin with an uppercase letter.
/// @param command Replacement command to execute when this user command is executed. When called
/// from Lua, the command can also be a Lua function. The function is called with a
/// single table argument that contains the following keys:
/// - args: (string) The args passed to the command, if any |<args>|
/// - bang: (boolean) "true" if the command was executed with a ! modifier |<bang>|
/// - line1: (number) The starting line of the command range |<line1>|
/// - line2: (number) The final line of the command range |<line2>|
/// - range: (number) The number of items in the command range: 0, 1, or 2 |<range>|
/// - count: (number) Any count supplied |<count>|
/// - reg: (string) The optional register, if specified |<reg>|
/// - mods: (string) Command modifiers, if any |<mods>|
/// @param opts Optional command attributes. See |command-attributes| for more details. To use
/// boolean attributes (such as |:command-bang| or |:command-bar|) set the value to
/// "true". When using a Lua function for {command} you can also provide a "desc"
/// key that will be displayed when listing commands. In addition to the string
/// options listed in |:command-complete|, the "complete" key also accepts a Lua
/// function which works like the "customlist" completion mode
/// |:command-complete-customlist|.
/// @param[out] err Error details, if any.
void nvim_add_user_command(String name, Object command, Dict(user_command) *opts, Error *err)
FUNC_API_SINCE(9)
{
add_user_command(name, command, opts, 0, err);
}
/// Delete a user-defined command.
///
/// @param name Name of the command to delete.
/// @param[out] err Error details, if any.
void nvim_del_user_command(String name, Error *err)
FUNC_API_SINCE(9)
{
nvim_buf_del_user_command(-1, name, err);
}

View File

@ -192,6 +192,7 @@ struct expand {
int xp_context; // type of expansion int xp_context; // type of expansion
size_t xp_pattern_len; // bytes in xp_pattern before cursor size_t xp_pattern_len; // bytes in xp_pattern before cursor
char_u *xp_arg; // completion function char_u *xp_arg; // completion function
LuaRef xp_luaref; // Ref to Lua completion function
sctx_T xp_script_ctx; // SCTX for completion function sctx_T xp_script_ctx; // SCTX for completion function
int xp_backslash; // one of the XP_BS_ values int xp_backslash; // one of the XP_BS_ values
#ifndef BACKSLASH_IN_FILENAME #ifndef BACKSLASH_IN_FILENAME

View File

@ -81,23 +81,7 @@
static int quitmore = 0; static int quitmore = 0;
static bool ex_pressedreturn = false; static bool ex_pressedreturn = false;
typedef struct ucmd { garray_T ucmds = { 0, 0, sizeof(ucmd_T), 4, NULL };
char_u *uc_name; // The command name
uint32_t uc_argt; // The argument type
char_u *uc_rep; // The command's replacement string
long uc_def; // The default value for a range/count
int uc_compl; // completion type
cmd_addr_T uc_addr_type; // The command's address type
sctx_T uc_script_ctx; // SCTX where the command was defined
char_u *uc_compl_arg; // completion argument if any
} ucmd_T;
#define UC_BUFFER 1 // -buffer: local to current buffer
static garray_T ucmds = { 0, 0, sizeof(ucmd_T), 4, NULL };
#define USER_CMD(i) (&((ucmd_T *)(ucmds.ga_data))[i])
#define USER_CMD_GA(gap, i) (&((ucmd_T *)((gap)->ga_data))[i])
// Whether a command index indicates a user command. // Whether a command index indicates a user command.
#define IS_USER_CMDIDX(idx) ((int)(idx) < 0) #define IS_USER_CMDIDX(idx) ((int)(idx) < 0)
@ -2761,6 +2745,7 @@ static char_u *find_ucmd(exarg_T *eap, char_u *p, int *full, expand_T *xp, int *
*complp = uc->uc_compl; *complp = uc->uc_compl;
} }
if (xp != NULL) { if (xp != NULL) {
xp->xp_luaref = uc->uc_compl_luaref;
xp->xp_arg = uc->uc_compl_arg; xp->xp_arg = uc->uc_compl_arg;
xp->xp_script_ctx = uc->uc_script_ctx; xp->xp_script_ctx = uc->uc_script_ctx;
xp->xp_script_ctx.sc_lnum += sourcing_lnum; xp->xp_script_ctx.sc_lnum += sourcing_lnum;
@ -5171,8 +5156,9 @@ char_u *get_command_name(expand_T *xp, int idx)
return cmdnames[idx].cmd_name; return cmdnames[idx].cmd_name;
} }
static int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t argt, long def, int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t argt, long def, int flags,
int flags, int compl, char_u *compl_arg, cmd_addr_T addr_type, bool force) int compl, char_u *compl_arg, LuaRef compl_luaref, cmd_addr_T addr_type,
LuaRef luaref, bool force)
FUNC_ATTR_NONNULL_ARG(1, 3) FUNC_ATTR_NONNULL_ARG(1, 3)
{ {
ucmd_T *cmd = NULL; ucmd_T *cmd = NULL;
@ -5226,6 +5212,8 @@ static int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t a
XFREE_CLEAR(cmd->uc_rep); XFREE_CLEAR(cmd->uc_rep);
XFREE_CLEAR(cmd->uc_compl_arg); XFREE_CLEAR(cmd->uc_compl_arg);
NLUA_CLEAR_REF(cmd->uc_luaref);
NLUA_CLEAR_REF(cmd->uc_compl_luaref);
break; break;
} }
@ -5256,13 +5244,17 @@ static int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t a
cmd->uc_script_ctx = current_sctx; cmd->uc_script_ctx = current_sctx;
cmd->uc_script_ctx.sc_lnum += sourcing_lnum; cmd->uc_script_ctx.sc_lnum += sourcing_lnum;
cmd->uc_compl_arg = compl_arg; cmd->uc_compl_arg = compl_arg;
cmd->uc_compl_luaref = compl_luaref;
cmd->uc_addr_type = addr_type; cmd->uc_addr_type = addr_type;
cmd->uc_luaref = luaref;
return OK; return OK;
fail: fail:
xfree(rep_buf); xfree(rep_buf);
xfree(compl_arg); xfree(compl_arg);
NLUA_CLEAR_REF(luaref);
NLUA_CLEAR_REF(compl_luaref);
return FAIL; return FAIL;
} }
@ -5301,6 +5293,7 @@ static const char *command_complete[] =
[EXPAND_CSCOPE] = "cscope", [EXPAND_CSCOPE] = "cscope",
[EXPAND_USER_DEFINED] = "custom", [EXPAND_USER_DEFINED] = "custom",
[EXPAND_USER_LIST] = "customlist", [EXPAND_USER_LIST] = "customlist",
[EXPAND_USER_LUA] = "<Lua function>",
[EXPAND_DIFF_BUFFERS] = "diff_buffer", [EXPAND_DIFF_BUFFERS] = "diff_buffer",
[EXPAND_DIRECTORIES] = "dir", [EXPAND_DIRECTORIES] = "dir",
[EXPAND_ENV_VARS] = "environment", [EXPAND_ENV_VARS] = "environment",
@ -5702,8 +5695,8 @@ static void ex_command(exarg_T *eap)
} else if (compl > 0 && (argt & EX_EXTRA) == 0) { } else if (compl > 0 && (argt & EX_EXTRA) == 0) {
emsg(_(e_complete_used_without_nargs)); emsg(_(e_complete_used_without_nargs));
} else { } else {
uc_add_command(name, end - name, p, argt, def, flags, compl, compl_arg, uc_add_command(name, end - name, p, argt, def, flags, compl, compl_arg, LUA_NOREF,
addr_type_arg, eap->forceit); addr_type_arg, LUA_NOREF, eap->forceit);
} }
} }
@ -5717,11 +5710,13 @@ void ex_comclear(exarg_T *eap)
uc_clear(&curbuf->b_ucmds); uc_clear(&curbuf->b_ucmds);
} }
static void free_ucmd(ucmd_T *cmd) void free_ucmd(ucmd_T *cmd)
{ {
xfree(cmd->uc_name); xfree(cmd->uc_name);
xfree(cmd->uc_rep); xfree(cmd->uc_rep);
xfree(cmd->uc_compl_arg); xfree(cmd->uc_compl_arg);
NLUA_CLEAR_REF(cmd->uc_compl_luaref);
NLUA_CLEAR_REF(cmd->uc_luaref);
} }
/* /*
@ -5759,9 +5754,7 @@ static void ex_delcommand(exarg_T *eap)
return; return;
} }
xfree(cmd->uc_name); free_ucmd(cmd);
xfree(cmd->uc_rep);
xfree(cmd->uc_compl_arg);
--gap->ga_len; --gap->ga_len;
@ -5843,7 +5836,7 @@ static char_u *uc_split_args(char_u *arg, size_t *lenp)
return buf; return buf;
} }
static size_t add_cmd_modifier(char_u *buf, char *mod_str, bool *multi_mods) static size_t add_cmd_modifier(char *buf, char *mod_str, bool *multi_mods)
{ {
size_t result = STRLEN(mod_str); size_t result = STRLEN(mod_str);
if (*multi_mods) { if (*multi_mods) {
@ -6044,70 +6037,8 @@ static size_t uc_check_code(char_u *code, size_t len, char_u *buf, ucmd_T *cmd,
*buf = '\0'; *buf = '\0';
} }
bool multi_mods = false; result += uc_mods((char *)buf);
// :aboveleft and :leftabove
if (cmdmod.split & WSP_ABOVE) {
result += add_cmd_modifier(buf, "aboveleft", &multi_mods);
}
// :belowright and :rightbelow
if (cmdmod.split & WSP_BELOW) {
result += add_cmd_modifier(buf, "belowright", &multi_mods);
}
// :botright
if (cmdmod.split & WSP_BOT) {
result += add_cmd_modifier(buf, "botright", &multi_mods);
}
typedef struct {
bool *set;
char *name;
} mod_entry_T;
static mod_entry_T mod_entries[] = {
{ &cmdmod.browse, "browse" },
{ &cmdmod.confirm, "confirm" },
{ &cmdmod.hide, "hide" },
{ &cmdmod.keepalt, "keepalt" },
{ &cmdmod.keepjumps, "keepjumps" },
{ &cmdmod.keepmarks, "keepmarks" },
{ &cmdmod.keeppatterns, "keeppatterns" },
{ &cmdmod.lockmarks, "lockmarks" },
{ &cmdmod.noswapfile, "noswapfile" }
};
// the modifiers that are simple flags
for (size_t i = 0; i < ARRAY_SIZE(mod_entries); i++) {
if (*mod_entries[i].set) {
result += add_cmd_modifier(buf, mod_entries[i].name, &multi_mods);
}
}
// TODO(vim): How to support :noautocmd?
// TODO(vim): How to support :sandbox?
// :silent
if (msg_silent > 0) {
result += add_cmd_modifier(buf, emsg_silent > 0 ? "silent!" : "silent",
&multi_mods);
}
// :tab
if (cmdmod.tab > 0) {
result += add_cmd_modifier(buf, "tab", &multi_mods);
}
// :topleft
if (cmdmod.split & WSP_TOP) {
result += add_cmd_modifier(buf, "topleft", &multi_mods);
}
// TODO(vim): How to support :unsilent?
// :verbose
if (p_verbose > 0) {
result += add_cmd_modifier(buf, "verbose", &multi_mods);
}
// :vertical
if (cmdmod.split & WSP_VERT) {
result += add_cmd_modifier(buf, "vertical", &multi_mods);
}
if (quote && buf != NULL) { if (quote && buf != NULL) {
buf += result - 2; buf += result - 2;
*buf = '"'; *buf = '"';
@ -6152,6 +6083,76 @@ static size_t uc_check_code(char_u *code, size_t len, char_u *buf, ucmd_T *cmd,
return result; return result;
} }
size_t uc_mods(char *buf)
{
size_t result = 0;
bool multi_mods = false;
// :aboveleft and :leftabove
if (cmdmod.split & WSP_ABOVE) {
result += add_cmd_modifier(buf, "aboveleft", &multi_mods);
}
// :belowright and :rightbelow
if (cmdmod.split & WSP_BELOW) {
result += add_cmd_modifier(buf, "belowright", &multi_mods);
}
// :botright
if (cmdmod.split & WSP_BOT) {
result += add_cmd_modifier(buf, "botright", &multi_mods);
}
typedef struct {
bool *set;
char *name;
} mod_entry_T;
static mod_entry_T mod_entries[] = {
{ &cmdmod.browse, "browse" },
{ &cmdmod.confirm, "confirm" },
{ &cmdmod.hide, "hide" },
{ &cmdmod.keepalt, "keepalt" },
{ &cmdmod.keepjumps, "keepjumps" },
{ &cmdmod.keepmarks, "keepmarks" },
{ &cmdmod.keeppatterns, "keeppatterns" },
{ &cmdmod.lockmarks, "lockmarks" },
{ &cmdmod.noswapfile, "noswapfile" }
};
// the modifiers that are simple flags
for (size_t i = 0; i < ARRAY_SIZE(mod_entries); i++) {
if (*mod_entries[i].set) {
result += add_cmd_modifier(buf, mod_entries[i].name, &multi_mods);
}
}
// TODO(vim): How to support :noautocmd?
// TODO(vim): How to support :sandbox?
// :silent
if (msg_silent > 0) {
result += add_cmd_modifier(buf, emsg_silent > 0 ? "silent!" : "silent", &multi_mods);
}
// :tab
if (cmdmod.tab > 0) {
result += add_cmd_modifier(buf, "tab", &multi_mods);
}
// :topleft
if (cmdmod.split & WSP_TOP) {
result += add_cmd_modifier(buf, "topleft", &multi_mods);
}
// TODO(vim): How to support :unsilent?
// :verbose
if (p_verbose > 0) {
result += add_cmd_modifier(buf, "verbose", &multi_mods);
}
// :vertical
if (cmdmod.split & WSP_VERT) {
result += add_cmd_modifier(buf, "vertical", &multi_mods);
}
return result;
}
static void do_ucmd(exarg_T *eap) static void do_ucmd(exarg_T *eap)
{ {
char_u *buf; char_u *buf;
@ -6174,6 +6175,11 @@ static void do_ucmd(exarg_T *eap)
cmd = USER_CMD_GA(&curbuf->b_ucmds, eap->useridx); cmd = USER_CMD_GA(&curbuf->b_ucmds, eap->useridx);
} }
if (cmd->uc_luaref > 0) {
nlua_do_ucmd(cmd, eap);
return;
}
/* /*
* Replace <> in the command by the arguments. * Replace <> in the command by the arguments.
* First round: "buf" is NULL, compute length, allocate "buf". * First round: "buf" is NULL, compute length, allocate "buf".

View File

@ -32,6 +32,26 @@ typedef struct {
tasave_T tabuf; tasave_T tabuf;
} save_state_T; } save_state_T;
typedef struct ucmd {
char_u *uc_name; // The command name
uint32_t uc_argt; // The argument type
char_u *uc_rep; // The command's replacement string
long uc_def; // The default value for a range/count
int uc_compl; // completion type
cmd_addr_T uc_addr_type; // The command's address type
sctx_T uc_script_ctx; // SCTX where the command was defined
char_u *uc_compl_arg; // completion argument if any
LuaRef uc_compl_luaref; // Reference to Lua completion function
LuaRef uc_luaref; // Reference to Lua function
} ucmd_T;
#define UC_BUFFER 1 // -buffer: local to current buffer
extern garray_T ucmds;
#define USER_CMD(i) (&((ucmd_T *)(ucmds.ga_data))[i])
#define USER_CMD_GA(gap, i) (&((ucmd_T *)((gap)->ga_data))[i])
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ex_docmd.h.generated.h" # include "ex_docmd.h.generated.h"
#endif #endif

View File

@ -4983,6 +4983,9 @@ static int ExpandFromContext(expand_T *xp, char_u *pat, int *num_file, char_u **
if (xp->xp_context == EXPAND_USER_LIST) { if (xp->xp_context == EXPAND_USER_LIST) {
return ExpandUserList(xp, num_file, file); return ExpandUserList(xp, num_file, file);
} }
if (xp->xp_context == EXPAND_USER_LUA) {
return ExpandUserLua(xp, num_file, file);
}
if (xp->xp_context == EXPAND_PACKADD) { if (xp->xp_context == EXPAND_PACKADD) {
return ExpandPackAddDir(pat, num_file, file); return ExpandPackAddDir(pat, num_file, file);
} }
@ -5411,6 +5414,35 @@ static int ExpandUserList(expand_T *xp, int *num_file, char_u ***file)
return OK; return OK;
} }
static int ExpandUserLua(expand_T *xp, int *num_file, char_u ***file)
{
typval_T rettv;
nlua_call_user_expand_func(xp, &rettv);
if (rettv.v_type != VAR_LIST) {
tv_clear(&rettv);
return FAIL;
}
list_T *const retlist = rettv.vval.v_list;
garray_T ga;
ga_init(&ga, (int)sizeof(char *), 3);
// Loop over the items in the list.
TV_LIST_ITER_CONST(retlist, li, {
if (TV_LIST_ITEM_TV(li)->v_type != VAR_STRING
|| TV_LIST_ITEM_TV(li)->vval.v_string == NULL) {
continue; // Skip non-string items and empty strings.
}
GA_APPEND(char *, &ga, xstrdup((const char *)TV_LIST_ITEM_TV(li)->vval.v_string));
});
tv_list_unref(retlist);
*file = ga.ga_data;
*num_file = ga.ga_len;
return OK;
}
/// Expand color scheme, compiler or filetype names. /// Expand color scheme, compiler or filetype names.
/// Search from 'runtimepath': /// Search from 'runtimepath':
/// 'runtimepath'/{dirnames}/{pat}.vim /// 'runtimepath'/{dirnames}/{pat}.vim

View File

@ -441,8 +441,8 @@ local function process_function(fn)
local cparam = string.format('arg%u', j) local cparam = string.format('arg%u', j)
local param_type = real_type(param[1]) local param_type = real_type(param[1])
local lc_param_type = real_type(param[1]):lower() local lc_param_type = real_type(param[1]):lower()
local extra = ((param_type == "Object" or param_type == "Dictionary") and "false, ") or "" local extra = param_type == "Dictionary" and "false, " or ""
if param[1] == "DictionaryOf(LuaRef)" then if param[1] == "Object" or param[1] == "DictionaryOf(LuaRef)" then
extra = "true, " extra = "true, "
end end
local errshift = 0 local errshift = 0

View File

@ -26,6 +26,17 @@ local defspipe = io.open(defs_file, 'wb')
local keysets = require'api.keysets' local keysets = require'api.keysets'
local keywords = {
register = true,
}
local function sanitize(key)
if keywords[key] then
return key .. "_"
end
return key
end
for name, keys in pairs(keysets) do for name, keys in pairs(keysets) do
local neworder, hashfun = hashy.hashy_hash(name, keys, function (idx) local neworder, hashfun = hashy.hashy_hash(name, keys, function (idx)
return name.."_table["..idx.."].str" return name.."_table["..idx.."].str"
@ -33,7 +44,7 @@ for name, keys in pairs(keysets) do
defspipe:write("typedef struct {\n") defspipe:write("typedef struct {\n")
for _, key in ipairs(neworder) do for _, key in ipairs(neworder) do
defspipe:write(" Object "..key..";\n") defspipe:write(" Object "..sanitize(key)..";\n")
end end
defspipe:write("} KeyDict_"..name..";\n\n") defspipe:write("} KeyDict_"..name..";\n\n")
@ -41,7 +52,7 @@ for name, keys in pairs(keysets) do
funcspipe:write("KeySetLink "..name.."_table[] = {\n") funcspipe:write("KeySetLink "..name.."_table[] = {\n")
for _, key in ipairs(neworder) do for _, key in ipairs(neworder) do
funcspipe:write(' {"'..key..'", offsetof(KeyDict_'..name..", "..key..")},\n") funcspipe:write(' {"'..key..'", offsetof(KeyDict_'..name..", "..sanitize(key)..")},\n")
end end
funcspipe:write(' {NULL, 0},\n') funcspipe:write(' {NULL, 0},\n')
funcspipe:write("};\n\n") funcspipe:write("};\n\n")

View File

@ -18,6 +18,7 @@
#include "nvim/event/loop.h" #include "nvim/event/loop.h"
#include "nvim/event/time.h" #include "nvim/event/time.h"
#include "nvim/ex_cmds2.h" #include "nvim/ex_cmds2.h"
#include "nvim/ex_docmd.h"
#include "nvim/ex_getln.h" #include "nvim/ex_getln.h"
#include "nvim/extmark.h" #include "nvim/extmark.h"
#include "nvim/func_attr.h" #include "nvim/func_attr.h"
@ -914,6 +915,24 @@ void nlua_typval_call(const char *str, size_t len, typval_T *const args, int arg
} }
} }
void nlua_call_user_expand_func(expand_T *xp, typval_T *ret_tv)
FUNC_ATTR_NONNULL_ALL
{
lua_State *const lstate = global_lstate;
nlua_pushref(lstate, xp->xp_luaref);
lua_pushstring(lstate, (char *)xp->xp_pattern);
lua_pushstring(lstate, (char *)xp->xp_line);
lua_pushinteger(lstate, xp->xp_col);
if (nlua_pcall(lstate, 3, 1)) {
nlua_error(lstate, _("E5108: Error executing Lua function: %.*s"));
return;
}
nlua_pop_typval(lstate, ret_tv);
}
static void nlua_typval_exec(const char *lcmd, size_t lcmd_len, const char *name, static void nlua_typval_exec(const char *lcmd, size_t lcmd_len, const char *name,
typval_T *const args, int argcount, bool special, typval_T *ret_tv) typval_T *const args, int argcount, bool special, typval_T *ret_tv)
{ {
@ -1432,3 +1451,48 @@ void nlua_execute_on_key(int c)
#endif #endif
} }
void nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap)
{
lua_State *const lstate = global_lstate;
nlua_pushref(lstate, cmd->uc_luaref);
lua_newtable(lstate);
lua_pushboolean(lstate, eap->forceit == 1);
lua_setfield(lstate, -2, "bang");
lua_pushinteger(lstate, eap->line1);
lua_setfield(lstate, -2, "line1");
lua_pushinteger(lstate, eap->line2);
lua_setfield(lstate, -2, "line2");
lua_pushstring(lstate, (const char *)eap->arg);
lua_setfield(lstate, -2, "args");
lua_pushstring(lstate, (const char *)&eap->regname);
lua_setfield(lstate, -2, "reg");
lua_pushinteger(lstate, eap->addr_count);
lua_setfield(lstate, -2, "range");
if (eap->addr_count > 0) {
lua_pushinteger(lstate, eap->line2);
} else {
lua_pushinteger(lstate, cmd->uc_def);
}
lua_setfield(lstate, -2, "count");
// The size of this buffer is chosen empirically to be large enough to hold
// every possible modifier (with room to spare). If the list of possible
// modifiers grows this may need to be updated.
char buf[200] = { 0 };
(void)uc_mods(buf);
lua_pushstring(lstate, buf);
lua_setfield(lstate, -2, "mods");
if (nlua_pcall(lstate, 1, 0)) {
nlua_error(lstate, _("Error executing Lua callback: %.*s"));
}
}

View File

@ -7,6 +7,7 @@
#include "nvim/api/private/defs.h" #include "nvim/api/private/defs.h"
#include "nvim/eval/typval.h" #include "nvim/eval/typval.h"
#include "nvim/ex_cmds_defs.h" #include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/func_attr.h" #include "nvim/func_attr.h"
#include "nvim/lua/converter.h" #include "nvim/lua/converter.h"

View File

@ -143,6 +143,7 @@ enum {
EXPAND_COMPILER, EXPAND_COMPILER,
EXPAND_USER_DEFINED, EXPAND_USER_DEFINED,
EXPAND_USER_LIST, EXPAND_USER_LIST,
EXPAND_USER_LUA,
EXPAND_SHELLCMD, EXPAND_SHELLCMD,
EXPAND_CSCOPE, EXPAND_CSCOPE,
EXPAND_SIGN, EXPAND_SIGN,

View File

@ -6,8 +6,14 @@ local command = helpers.command
local curbufmeths = helpers.curbufmeths local curbufmeths = helpers.curbufmeths
local eq = helpers.eq local eq = helpers.eq
local meths = helpers.meths local meths = helpers.meths
local bufmeths = helpers.bufmeths
local matches = helpers.matches
local source = helpers.source local source = helpers.source
local pcall_err = helpers.pcall_err local pcall_err = helpers.pcall_err
local exec_lua = helpers.exec_lua
local assert_alive = helpers.assert_alive
local feed = helpers.feed
local funcs = helpers.funcs
describe('nvim_get_commands', function() describe('nvim_get_commands', function()
local cmd_dict = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='echo "Hello World"', name='Hello', nargs='1', range=NIL, register=false, script_id=0, } local cmd_dict = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='echo "Hello World"', name='Hello', nargs='1', range=NIL, register=false, script_id=0, }
@ -78,3 +84,118 @@ describe('nvim_get_commands', function()
eq({Cmd2=cmd2, Cmd3=cmd3, Cmd4=cmd4, Finger=cmd1, TestCmd=cmd0}, meths.get_commands({builtin=false})) eq({Cmd2=cmd2, Cmd3=cmd3, Cmd4=cmd4, Finger=cmd1, TestCmd=cmd0}, meths.get_commands({builtin=false}))
end) end)
end) end)
describe('nvim_add_user_command', function()
before_each(clear)
it('works with strings', function()
meths.add_user_command('SomeCommand', 'let g:command_fired = <args>', {nargs = 1})
meths.command('SomeCommand 42')
eq(42, meths.eval('g:command_fired'))
end)
it('works with Lua functions', function()
exec_lua [[
result = {}
vim.api.nvim_add_user_command('CommandWithLuaCallback', function(opts)
result = opts
end, {
nargs = "*",
bang = true,
count = 2,
})
]]
eq({
args = "hello",
bang = false,
line1 = 1,
line2 = 1,
mods = "",
range = 0,
count = 2,
reg = "",
}, exec_lua [[
vim.api.nvim_command('CommandWithLuaCallback hello')
return result
]])
eq({
args = "",
bang = true,
line1 = 10,
line2 = 10,
mods = "botright",
range = 1,
count = 10,
reg = "",
}, exec_lua [[
vim.api.nvim_command('botright 10CommandWithLuaCallback!')
return result
]])
eq({
args = "",
bang = false,
line1 = 1,
line2 = 42,
mods = "",
range = 1,
count = 42,
reg = "",
}, exec_lua [[
vim.api.nvim_command('CommandWithLuaCallback 42')
return result
]])
end)
it('can define buffer-local commands', function()
local bufnr = meths.create_buf(false, false)
bufmeths.add_user_command(bufnr, "Hello", "", {})
matches("Not an editor command: Hello", pcall_err(meths.command, "Hello"))
meths.set_current_buf(bufnr)
meths.command("Hello")
assert_alive()
end)
it('can use a Lua complete function', function()
exec_lua [[
vim.api.nvim_add_user_command('Test', '', {
nargs = "*",
complete = function(arg, cmdline, pos)
local options = {"aaa", "bbb", "ccc"}
local t = {}
for _, v in ipairs(options) do
if string.find(v, "^" .. arg) then
table.insert(t, v)
end
end
return t
end,
})
]]
feed(':Test a<Tab>')
eq('Test aaa', funcs.getcmdline())
feed('<C-U>Test b<Tab>')
eq('Test bbb', funcs.getcmdline())
end)
end)
describe('nvim_del_user_command', function()
before_each(clear)
it('can delete global commands', function()
meths.add_user_command('Hello', 'echo "Hi"', {})
meths.command('Hello')
meths.del_user_command('Hello')
matches("Not an editor command: Hello", pcall_err(meths.command, "Hello"))
end)
it('can delete buffer-local commands', function()
bufmeths.add_user_command(0, 'Hello', 'echo "Hi"', {})
meths.command('Hello')
bufmeths.del_user_command(0, 'Hello')
matches("Not an editor command: Hello", pcall_err(meths.command, "Hello"))
end)
end)

View File

@ -8,6 +8,7 @@ local clear = helpers.clear
local eval = helpers.eval local eval = helpers.eval
local NIL = helpers.NIL local NIL = helpers.NIL
local eq = helpers.eq local eq = helpers.eq
local exec_lua = helpers.exec_lua
before_each(clear) before_each(clear)
@ -111,6 +112,12 @@ describe('luaeval(vim.api.…)', function()
eq(7, eval([[type(luaeval('vim.api.nvim__id(nil)'))]])) eq(7, eval([[type(luaeval('vim.api.nvim__id(nil)'))]]))
eq({foo=1, bar={42, {{baz=true}, 5}}}, funcs.luaeval('vim.api.nvim__id({foo=1, bar={42, {{baz=true}, 5}}})')) eq({foo=1, bar={42, {{baz=true}, 5}}}, funcs.luaeval('vim.api.nvim__id({foo=1, bar={42, {{baz=true}, 5}}})'))
eq(true, funcs.luaeval('vim.api.nvim__id(vim.api.nvim__id)(true)'))
eq(42, exec_lua [[
local f = vim.api.nvim__id({42, vim.api.nvim__id})
return f[2](f[1])
]])
end) end)
it('correctly converts container objects with type_idx to API objects', function() it('correctly converts container objects with type_idx to API objects', function()
@ -159,12 +166,8 @@ describe('luaeval(vim.api.…)', function()
it('errors out correctly when working with API', function() it('errors out correctly when working with API', function()
-- Conversion errors -- Conversion errors
eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua type',
remove_trace(exc_exec([[call luaeval("vim.api.nvim__id(vim.api.nvim__id)")]])))
eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua table', eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua table',
remove_trace(exc_exec([[call luaeval("vim.api.nvim__id({1, foo=42})")]]))) remove_trace(exc_exec([[call luaeval("vim.api.nvim__id({1, foo=42})")]])))
eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Cannot convert given lua type',
remove_trace(exc_exec([[call luaeval("vim.api.nvim__id({42, vim.api.nvim__id})")]])))
-- Errors in number of arguments -- Errors in number of arguments
eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 1 argument', eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 1 argument',
remove_trace(exc_exec([[call luaeval("vim.api.nvim__id()")]]))) remove_trace(exc_exec([[call luaeval("vim.api.nvim__id()")]])))