Merge pull request #16752 from gpanders/lua-user-commands

feat(api): implement nvim_{add,del}_user_command
This commit is contained in:
Björn Linse 2021-12-28 23:18:07 +01:00 committed by GitHub
commit 7bb593169e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 740 additions and 99 deletions

View File

@ -626,6 +626,56 @@ nvim__stats() *nvim__stats()*
Return: ~
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()*
Calls many API methods atomically.
@ -714,6 +764,12 @@ nvim_del_mark({name}) *nvim_del_mark()*
|nvim_buf_del_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()*
Removes a global (g:) variable.
@ -1790,6 +1846,16 @@ nvim__buf_redraw_range({buffer}, {first}, {last})
nvim__buf_stats({buffer}) *nvim__buf_stats()*
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()*
Activates buffer-update events on a channel, or as Lua
callbacks.
@ -1925,6 +1991,18 @@ nvim_buf_del_mark({buffer}, {name}) *nvim_buf_del_mark()*
|nvim_buf_set_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()*
Removes a buffer-scoped (b:) variable

View File

@ -1247,8 +1247,8 @@ See |:verbose-cmd| for more information.
Command attributes ~
User-defined commands are treated by Vim just like any other Ex commands. They
*command-attributes*
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
completion as filenames, buffers, etc. Exactly how this works depends upon the
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;
}
/// 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 rv = ARRAY_DICT_INIT;

View File

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

View File

@ -961,6 +961,10 @@ Object copy_object(Object obj)
case kObjectTypeDictionary:
return DICTIONARY_OBJ(copy_dictionary(obj.data.dictionary));
case kObjectTypeLuaRef:
return LUAREF_OBJ(api_new_luaref(obj.data.luaref));
default:
abort();
}
@ -1342,3 +1346,184 @@ const char *get_default_stl_hl(win_T *wp)
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;
}
/// 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
size_t xp_pattern_len; // bytes in xp_pattern before cursor
char_u *xp_arg; // completion function
LuaRef xp_luaref; // Ref to Lua completion function
sctx_T xp_script_ctx; // SCTX for completion function
int xp_backslash; // one of the XP_BS_ values
#ifndef BACKSLASH_IN_FILENAME

View File

@ -81,23 +81,7 @@
static int quitmore = 0;
static bool ex_pressedreturn = false;
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
} 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])
garray_T ucmds = { 0, 0, sizeof(ucmd_T), 4, NULL };
// Whether a command index indicates a user command.
#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;
}
if (xp != NULL) {
xp->xp_luaref = uc->uc_compl_luaref;
xp->xp_arg = uc->uc_compl_arg;
xp->xp_script_ctx = uc->uc_script_ctx;
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;
}
static int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t argt, long def,
int flags, int compl, char_u *compl_arg, cmd_addr_T addr_type, bool force)
int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t argt, long def, int flags,
int compl, char_u *compl_arg, LuaRef compl_luaref, cmd_addr_T addr_type,
LuaRef luaref, bool force)
FUNC_ATTR_NONNULL_ARG(1, 3)
{
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_compl_arg);
NLUA_CLEAR_REF(cmd->uc_luaref);
NLUA_CLEAR_REF(cmd->uc_compl_luaref);
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.sc_lnum += sourcing_lnum;
cmd->uc_compl_arg = compl_arg;
cmd->uc_compl_luaref = compl_luaref;
cmd->uc_addr_type = addr_type;
cmd->uc_luaref = luaref;
return OK;
fail:
xfree(rep_buf);
xfree(compl_arg);
NLUA_CLEAR_REF(luaref);
NLUA_CLEAR_REF(compl_luaref);
return FAIL;
}
@ -5301,6 +5293,7 @@ static const char *command_complete[] =
[EXPAND_CSCOPE] = "cscope",
[EXPAND_USER_DEFINED] = "custom",
[EXPAND_USER_LIST] = "customlist",
[EXPAND_USER_LUA] = "<Lua function>",
[EXPAND_DIFF_BUFFERS] = "diff_buffer",
[EXPAND_DIRECTORIES] = "dir",
[EXPAND_ENV_VARS] = "environment",
@ -5702,8 +5695,8 @@ static void ex_command(exarg_T *eap)
} else if (compl > 0 && (argt & EX_EXTRA) == 0) {
emsg(_(e_complete_used_without_nargs));
} else {
uc_add_command(name, end - name, p, argt, def, flags, compl, compl_arg,
addr_type_arg, eap->forceit);
uc_add_command(name, end - name, p, argt, def, flags, compl, compl_arg, LUA_NOREF,
addr_type_arg, LUA_NOREF, eap->forceit);
}
}
@ -5717,11 +5710,13 @@ void ex_comclear(exarg_T *eap)
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_rep);
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;
}
xfree(cmd->uc_name);
xfree(cmd->uc_rep);
xfree(cmd->uc_compl_arg);
free_ucmd(cmd);
--gap->ga_len;
@ -5843,7 +5836,7 @@ static char_u *uc_split_args(char_u *arg, size_t *lenp)
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);
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';
}
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) {
buf += result - 2;
*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;
}
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)
{
char_u *buf;
@ -6174,6 +6175,11 @@ static void do_ucmd(exarg_T *eap)
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.
* First round: "buf" is NULL, compute length, allocate "buf".

View File

@ -32,6 +32,26 @@ typedef struct {
tasave_T tabuf;
} 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
# include "ex_docmd.h.generated.h"
#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) {
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) {
return ExpandPackAddDir(pat, num_file, file);
}
@ -5411,6 +5414,35 @@ static int ExpandUserList(expand_T *xp, int *num_file, char_u ***file)
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.
/// Search from 'runtimepath':
/// 'runtimepath'/{dirnames}/{pat}.vim

View File

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

View File

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

View File

@ -18,6 +18,7 @@
#include "nvim/event/loop.h"
#include "nvim/event/time.h"
#include "nvim/ex_cmds2.h"
#include "nvim/ex_docmd.h"
#include "nvim/ex_getln.h"
#include "nvim/extmark.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,
typval_T *const args, int argcount, bool special, typval_T *ret_tv)
{
@ -1432,3 +1451,48 @@ void nlua_execute_on_key(int c)
#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/eval/typval.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/func_attr.h"
#include "nvim/lua/converter.h"

View File

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

View File

@ -6,8 +6,14 @@ local command = helpers.command
local curbufmeths = helpers.curbufmeths
local eq = helpers.eq
local meths = helpers.meths
local bufmeths = helpers.bufmeths
local matches = helpers.matches
local source = helpers.source
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()
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}))
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 NIL = helpers.NIL
local eq = helpers.eq
local exec_lua = helpers.exec_lua
before_each(clear)
@ -111,6 +112,12 @@ describe('luaeval(vim.api.…)', function()
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(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)
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()
-- 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',
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
eq('Vim(call):E5108: Error executing lua [string "luaeval()"]:1: Expected 1 argument',
remove_trace(exc_exec([[call luaeval("vim.api.nvim__id()")]])))