API: nvim_set_keymap, nvim_del_keymap #9924

closes #9136

- Treat empty {rhs} like <Nop>

- getchar.c: Pull "repl. MapArg termcodes" into func
  The "preprocessing code" surrounding the replace_termcodes calls needs
  to invoke replace_termcodes, and also check if RHS is equal to "<Nop>".
  To reduce code duplication, factor this out into a helper function.

  Also add an rhs_is_noop flag to MapArguments; buf_do_map_explicit
  expects an empty {rhs} string for "<Nop>", but also needs to distinguish
  that from something like ":map lhs<cr>" where no {rhs} was provided.

- getchar.c: Use allocated buffer for rhs in MapArgs
  Since the MAXMAPLEN limit does not apply to the RHS of a mapping (or
  else an RHS that calls a really long autoload function from a plugin
  would be incorrectly rejected as being too long), use an allocated
  buffer for RHS rather than a static buffer of length MAXMAPLEN + 1.

- Mappings LHS and RHS can contain literal space characters, newlines, etc.

- getchar.c: replace_termcodes in str_to_mapargs
  It makes sense to do this; str_to_mapargs is, intuitively, supposed to
  take a "raw" command string and parse it into a totally "do_map-ready"
  struct.

- api/vim.c: Update lhs, rhs len after replace_termcodes
  Fixes a bug in which replace_termcodes changes the length of lhs or rhs,
  but the later search through the mappings/abbreviations hashtables
  still uses the old length value. This would cause the search to fail
  erroneously and throw 'E31: No such mapping' errors or 'E24: No such
  abbreviation' errors.

- getchar: Create new map_arguments struct
  So that a string of map arguments can be parsed into a more useful, more
  portable data structure.

- getchar.c: Add buf_do_map function
  Exactly the same as the old do_map, but replace the hardcoded references
  to the global `buf_T* curbuf` with a function parameter so that we can
  invoke it from nvim_buf_set_keymap.

- Remove gettext calls in do_map error handling
This commit is contained in:
Yilin Yang
2019-05-12 11:44:48 +02:00
committed by Justin M. Keyes
parent 24f9dd73d5
commit fbf2c414ad
9 changed files with 1313 additions and 311 deletions

View File

@@ -15,6 +15,7 @@
#include "nvim/buffer.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/getchar.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/misc1.h"
@@ -576,6 +577,27 @@ ArrayOf(Dictionary) nvim_buf_get_keymap(Buffer buffer, String mode, Error *err)
return keymap_array(mode, buf);
}
/// Like |nvim_set_keymap|, but for a specific buffer.
///
/// @param buffer Buffer handle, or 0 for the current buffer.
void nvim_buf_set_keymap(Buffer buffer, String mode, String lhs, String rhs,
Dictionary opts, Error *err)
FUNC_API_SINCE(6)
{
modify_keymap(buffer, false, mode, lhs, rhs, opts, err);
}
/// Like |nvim_del_keymap|, but for a specific buffer.
///
/// @param buffer Buffer handle, or 0 for the current buffer.
void nvim_buf_del_keymap(Buffer buffer, String mode, String lhs, Error *err)
FUNC_API_SINCE(6)
{
String rhs = { .data = "", .size = 0 };
Dictionary opts = ARRAY_DICT_INIT;
modify_keymap(buffer, true, mode, lhs, rhs, opts, err);
}
/// Gets a map of buffer-local |user-commands|.
///
/// @param buffer Buffer handle, or 0 for current buffer

View File

@@ -744,6 +744,234 @@ String ga_take_string(garray_T *ga)
return str;
}
/// Set, tweak, or remove a mapping in a mode. Acts as the implementation for
/// functions like @ref nvim_buf_set_keymap.
///
/// Arguments are handled like @ref nvim_set_keymap unless noted.
/// @param buffer Buffer handle for a specific buffer, or 0 for the current
/// buffer, or -1 to signify global behavior ("all buffers")
/// @param is_unmap When true, removes the mapping that matches {lhs}.
void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs,
String rhs, Dictionary opts, Error *err)
{
char *err_msg = NULL; // the error message to report, if any
char *err_arg = NULL; // argument for the error message format string
ErrorType err_type = kErrorTypeNone;
char_u *lhs_buf = NULL;
char_u *rhs_buf = NULL;
bool global = (buffer == -1);
if (global) {
buffer = 0;
}
buf_T *target_buf = find_buffer_by_handle(buffer, err);
MapArguments parsed_args;
memset(&parsed_args, 0, sizeof(parsed_args));
if (parse_keymap_opts(opts, &parsed_args, err)) {
goto FAIL_AND_FREE;
}
parsed_args.buffer = !global;
set_maparg_lhs_rhs((char_u *)lhs.data, lhs.size,
(char_u *)rhs.data, rhs.size,
CPO_TO_CPO_FLAGS, &parsed_args);
if (parsed_args.lhs_len > MAXMAPLEN) {
err_msg = "LHS exceeds maximum map length: %s";
err_arg = lhs.data;
err_type = kErrorTypeValidation;
goto FAIL_WITH_MESSAGE;
}
if (mode.size > 1) {
err_msg = "Shortname is too long: %s";
err_arg = mode.data;
err_type = kErrorTypeValidation;
goto FAIL_WITH_MESSAGE;
}
int mode_val; // integer value of the mapping mode, to be passed to do_map()
char_u *p = (char_u *)((mode.size) ? mode.data : "m");
if (STRNCMP(p, "!", 2) == 0) {
mode_val = get_map_mode(&p, true); // mapmode-ic
} else {
mode_val = get_map_mode(&p, false);
if (mode_val == VISUAL + SELECTMODE + NORMAL + OP_PENDING) {
// get_map_mode will treat "unrecognized" mode shortnames like "map"
// if it does, and the given shortname wasn't "m" or " ", then error
if (STRNCMP(p, "m", 2) && STRNCMP(p, " ", 2)) {
err_msg = "Invalid mode shortname: %s";
err_arg = (char *)p;
err_type = kErrorTypeValidation;
goto FAIL_WITH_MESSAGE;
}
}
}
if (parsed_args.lhs_len == 0) {
err_msg = "Invalid (empty) LHS";
err_arg = "";
err_type = kErrorTypeValidation;
goto FAIL_WITH_MESSAGE;
}
bool is_noremap = parsed_args.noremap;
assert(!(is_unmap && is_noremap));
if (!is_unmap && (parsed_args.rhs_len == 0 && !parsed_args.rhs_is_noop)) {
if (rhs.size == 0) { // assume that the user wants RHS to be a <Nop>
parsed_args.rhs_is_noop = true;
} else {
// the given RHS was nonempty and not a <Nop>, but was parsed as if it
// were empty?
assert(false && "Failed to parse nonempty RHS!");
err_msg = "Parsing of nonempty RHS failed: %s";
err_arg = rhs.data;
err_type = kErrorTypeException;
goto FAIL_WITH_MESSAGE;
}
} else if (is_unmap && parsed_args.rhs_len) {
err_msg = "Gave nonempty RHS in unmap command: %s";
err_arg = (char *)parsed_args.rhs;
err_type = kErrorTypeValidation;
goto FAIL_WITH_MESSAGE;
}
// buf_do_map_explicit reads noremap/unmap as its own argument
int maptype_val = 0;
if (is_unmap) {
maptype_val = 1;
} else if (is_noremap) {
maptype_val = 2;
}
switch (buf_do_map_explicit(maptype_val, &parsed_args, mode_val,
0, target_buf)) {
case 0:
break;
case 1:
api_set_error(err, kErrorTypeException, (char *)e_invarg, 0);
goto FAIL_AND_FREE;
case 2:
api_set_error(err, kErrorTypeException, (char *)e_nomap, 0);
goto FAIL_AND_FREE;
case 5:
api_set_error(err, kErrorTypeException,
"E227: mapping already exists for %s", parsed_args.lhs);
goto FAIL_AND_FREE;
default:
assert(false && "Unrecognized return code!");
goto FAIL_AND_FREE;
} // switch
xfree(lhs_buf);
xfree(rhs_buf);
xfree(parsed_args.rhs);
xfree(parsed_args.orig_rhs);
return;
FAIL_WITH_MESSAGE:
api_set_error(err, err_type, err_msg, err_arg);
FAIL_AND_FREE:
xfree(lhs_buf);
xfree(rhs_buf);
xfree(parsed_args.rhs);
xfree(parsed_args.orig_rhs);
return;
}
/// Read in the given opts, setting corresponding flags in `out`.
///
/// @param opts A dictionary passed to @ref nvim_set_keymap or
/// @ref nvim_buf_set_keymap.
/// @param[out] out MapArguments object in which to set parsed
/// |:map-arguments| flags.
/// @param[out] err Error details, if any.
///
/// @returns Zero on success, nonzero on failure.
Integer parse_keymap_opts(Dictionary opts, MapArguments *out, Error *err)
{
char *err_msg = NULL; // the error message to report, if any
char *err_arg = NULL; // argument for the error message format string
ErrorType err_type = kErrorTypeNone;
out->buffer = false;
out->nowait = false;
out->silent = false;
out->script = false;
out->expr = false;
out->unique = false;
for (size_t i = 0; i < opts.size; i++) {
KeyValuePair *key_and_val = &opts.items[i];
char *optname = key_and_val->key.data;
if (key_and_val->value.type != kObjectTypeBoolean) {
err_msg = "Gave non-boolean value for an opt: %s";
err_arg = optname;
err_type = kErrorTypeValidation;
goto FAIL_WITH_MESSAGE;
}
bool was_valid_opt = false;
switch (optname[0]) {
// note: strncmp up to and including the null terminator, so that
// "nowaitFoobar" won't match against "nowait"
// don't recognize 'buffer' as a key; user shouldn't provide <buffer>
// when calling nvim_set_keymap or nvim_buf_set_keymap, since it can be
// inferred from which function they called
case 'n':
if (STRNCMP(optname, "noremap", 8) == 0) {
was_valid_opt = true;
out->noremap = key_and_val->value.data.boolean;
} else if (STRNCMP(optname, "nowait", 7) == 0) {
was_valid_opt = true;
out->nowait = key_and_val->value.data.boolean;
}
break;
case 's':
if (STRNCMP(optname, "silent", 7) == 0) {
was_valid_opt = true;
out->silent = key_and_val->value.data.boolean;
} else if (STRNCMP(optname, "script", 7) == 0) {
was_valid_opt = true;
out->script = key_and_val->value.data.boolean;
}
break;
case 'e':
if (STRNCMP(optname, "expr", 5) == 0) {
was_valid_opt = true;
out->expr = key_and_val->value.data.boolean;
}
break;
case 'u':
if (STRNCMP(optname, "unique", 7) == 0) {
was_valid_opt = true;
out->unique = key_and_val->value.data.boolean;
}
break;
default:
break;
} // switch
if (!was_valid_opt) {
err_msg = "Invalid key: %s";
err_arg = optname;
err_type = kErrorTypeValidation;
goto FAIL_WITH_MESSAGE;
}
} // for
return 0;
FAIL_WITH_MESSAGE:
api_set_error(err, err_type, err_msg, err_arg);
return 1;
}
/// Collects `n` buffer lines into array `l`, optionally replacing newlines
/// with NUL.
///

View File

@@ -5,6 +5,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/vim.h"
#include "nvim/getchar.h"
#include "nvim/memory.h"
#include "nvim/ex_eval.h"
#include "nvim/lib/kvec.h"

View File

@@ -1262,6 +1262,53 @@ ArrayOf(Dictionary) nvim_get_keymap(String mode)
return keymap_array(mode, NULL);
}
/// Sets a global |mapping| for the given mode.
///
/// To set a buffer-local mapping, use |nvim_buf_set_keymap|.
///
/// Unlike ordinary Ex mode |:map| commands, special characters like literal
/// spaces and newlines are treated as an actual part of the {lhs} or {rhs}.
/// An empty {rhs} is treated like a |<Nop>|. |keycodes| are still replaced as
/// usual.
///
/// `call nvim_set_keymap('n', ' <NL>', '', {'nowait': v:true})`
///
/// Is equivalent to,
///
/// `nmap <nowait> <Space><NL> <Nop>`
///
/// @param mode Mode short-name (the first character of an map command,
/// e.g. "n", "i", "v", "x", etc.) OR the string "!" (for
/// |:map!|). |:map| can be represented with a single space " ",
/// an empty string, or "m".
/// @param lhs Left-hand-side |{lhs}| of the mapping.
/// @param rhs Right-hand-side |{rhs}| of the mapping.
/// @param opts |dict| of optional parameters. Accepts all |:map-arguments|
/// as keys excluding |<buffer>| but also including |noremap|.
/// Values should all be Booleans. Unrecognized keys will result
/// in an error.
/// @param[out] err Error details, if any.
void nvim_set_keymap(String mode, String lhs, String rhs,
Dictionary opts, Error *err)
FUNC_API_SINCE(6)
{
modify_keymap(-1, false, mode, lhs, rhs, opts, err);
}
/// Unmap a global |mapping| for the given mode.
///
/// To unmap a buffer-local mapping, use |nvim_buf_del_keymap|.
///
/// Arguments are handled like |nvim_set_keymap|. Like with ordinary |:unmap|
/// commands (and `nvim_set_keymap`), the given {lhs} is interpreted literally:
/// for instance, trailing whitespace is treated as part of the {lhs}.
/// |keycodes| are still replaced as usual.
void nvim_del_keymap(String mode, String lhs, Error *err)
FUNC_API_SINCE(6)
{
nvim_buf_del_keymap(-1, mode, lhs, err);
}
/// Gets a map of global (non-buffer-local) Ex commands.
///
/// Currently only |user-commands| are supported, not builtin Ex commands.