aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregory Anders <greg@gpanders.com>2021-12-22 11:40:21 -0700
committerGregory Anders <greg@gpanders.com>2021-12-28 14:08:44 -0700
commiteff11b3c3fcb9aa777deafb0a33b1523aa05b603 (patch)
treeff517e6b769f8604a1a6f24826405594859ec95b
parent9804a2870f6f308f788f939f52958e3fbd2adaac (diff)
downloadrneovim-eff11b3c3fcb9aa777deafb0a33b1523aa05b603.tar.gz
rneovim-eff11b3c3fcb9aa777deafb0a33b1523aa05b603.tar.bz2
rneovim-eff11b3c3fcb9aa777deafb0a33b1523aa05b603.zip
feat(api): implement nvim_{add,del}_user_command
Add support for adding and removing custom user commands with the Nvim API.
-rw-r--r--runtime/doc/api.txt78
-rw-r--r--runtime/doc/map.txt4
-rw-r--r--src/nvim/api/buffer.c57
-rw-r--r--src/nvim/api/keysets.lua12
-rw-r--r--src/nvim/api/private/helpers.c185
-rw-r--r--src/nvim/api/vim.c49
-rw-r--r--src/nvim/ex_cmds_defs.h1
-rw-r--r--src/nvim/ex_docmd.c184
-rw-r--r--src/nvim/ex_docmd.h20
-rw-r--r--src/nvim/ex_getln.c32
-rw-r--r--src/nvim/generators/gen_api_dispatch.lua4
-rw-r--r--src/nvim/generators/gen_keysets.lua15
-rw-r--r--src/nvim/lua/executor.c64
-rw-r--r--src/nvim/lua/executor.h1
-rw-r--r--src/nvim/vim.h1
-rw-r--r--test/functional/api/command_spec.lua121
-rw-r--r--test/functional/lua/api_spec.lua11
17 files changed, 740 insertions, 99 deletions
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
index 8fb6290e50..13b7c2f61a 100644
--- a/runtime/doc/api.txt
+++ b/runtime/doc/api.txt
@@ -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
diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt
index 238ef39bd3..f6d9e45d64 100644
--- a/runtime/doc/map.txt
+++ b/runtime/doc/map.txt
@@ -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.
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c
index ce9c4e27ad..bacb991c4b 100644
--- a/src/nvim/api/buffer.c
+++ b/src/nvim/api/buffer.c
@@ -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;
diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua
index f3e7f2f1dc..e47dec9eb7 100644
--- a/src/nvim/api/keysets.lua
+++ b/src/nvim/api/keysets.lua
@@ -33,6 +33,18 @@ return {
get_commands = {
"builtin";
};
+ user_command = {
+ "addr";
+ "bang";
+ "bar";
+ "complete";
+ "count";
+ "desc";
+ "force";
+ "nargs";
+ "range";
+ "register";
+ };
float_config = {
"row";
"col";
diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c
index 9b407eab8b..962fce6952 100644
--- a/src/nvim/api/private/helpers.c
+++ b/src/nvim/api/private/helpers.c
@@ -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);
+}
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c
index 4a0222234d..ac979d0064 100644
--- a/src/nvim/api/vim.c
+++ b/src/nvim/api/vim.c
@@ -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);
+}
diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h
index ea899b660b..e5eab61f9e 100644
--- a/src/nvim/ex_cmds_defs.h
+++ b/src/nvim/ex_cmds_defs.h
@@ -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
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index 230cbd4b60..7bf4a4a65a 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -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;
-
- // :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?
+ result += uc_mods((char *)buf);
- // :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".
diff --git a/src/nvim/ex_docmd.h b/src/nvim/ex_docmd.h
index a302d4a3c5..abf6ec347b 100644
--- a/src/nvim/ex_docmd.h
+++ b/src/nvim/ex_docmd.h
@@ -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
diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c
index ba2238ace2..67fd5a4efc 100644
--- a/src/nvim/ex_getln.c
+++ b/src/nvim/ex_getln.c
@@ -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
diff --git a/src/nvim/generators/gen_api_dispatch.lua b/src/nvim/generators/gen_api_dispatch.lua
index 21f8c3855e..c6dd25154b 100644
--- a/src/nvim/generators/gen_api_dispatch.lua
+++ b/src/nvim/generators/gen_api_dispatch.lua
@@ -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
diff --git a/src/nvim/generators/gen_keysets.lua b/src/nvim/generators/gen_keysets.lua
index 63ef202fe1..01d8c1d357 100644
--- a/src/nvim/generators/gen_keysets.lua
+++ b/src/nvim/generators/gen_keysets.lua
@@ -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")
diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c
index 107ff22913..7d43d21d53 100644
--- a/src/nvim/lua/executor.c
+++ b/src/nvim/lua/executor.c
@@ -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"));
+ }
+}
+
diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h
index a1f66bd02b..bf78f7ec5e 100644
--- a/src/nvim/lua/executor.h
+++ b/src/nvim/lua/executor.h
@@ -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"
diff --git a/src/nvim/vim.h b/src/nvim/vim.h
index 2f8ddd1e88..6e0e9922a6 100644
--- a/src/nvim/vim.h
+++ b/src/nvim/vim.h
@@ -143,6 +143,7 @@ enum {
EXPAND_COMPILER,
EXPAND_USER_DEFINED,
EXPAND_USER_LIST,
+ EXPAND_USER_LUA,
EXPAND_SHELLCMD,
EXPAND_CSCOPE,
EXPAND_SIGN,
diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua
index 6f929ad1ca..6c2c136edc 100644
--- a/test/functional/api/command_spec.lua
+++ b/test/functional/api/command_spec.lua
@@ -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)
diff --git a/test/functional/lua/api_spec.lua b/test/functional/lua/api_spec.lua
index 81e00bba6d..f6038e23fe 100644
--- a/test/functional/lua/api_spec.lua
+++ b/test/functional/lua/api_spec.lua
@@ -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()")]])))