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
17 changed files with 740 additions and 99 deletions

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);
}