From fbf2c414ad3409e8359ff744765e7486043bb4f7 Mon Sep 17 00:00:00 2001 From: Yilin Yang Date: Sun, 12 May 2019 11:44:48 +0200 Subject: API: nvim_set_keymap, nvim_del_keymap #9924 closes #9136 - Treat empty {rhs} like - 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 "". 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 "", but also needs to distinguish that from something like ":map lhs" 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 --- src/nvim/api/buffer.c | 22 ++ src/nvim/api/private/helpers.c | 228 +++++++++++++ src/nvim/api/private/helpers.h | 1 + src/nvim/api/vim.c | 47 +++ src/nvim/getchar.c | 728 ++++++++++++++++++++++++----------------- src/nvim/getchar.h | 34 ++ 6 files changed, 752 insertions(+), 308 deletions(-) (limited to 'src') diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 4a0b8d13d0..61e042ef40 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -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 diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index c2b382804d..aa9b3f5f18 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -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 + parsed_args.rhs_is_noop = true; + } else { + // the given RHS was nonempty and not a , 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 + // 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. /// diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 0634764f13..cc74824402 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -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" diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 37f2d3c367..53aad1fe39 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -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 ||. |keycodes| are still replaced as +/// usual. +/// +/// `call nvim_set_keymap('n', ' ', '', {'nowait': v:true})` +/// +/// Is equivalent to, +/// +/// `nmap ` +/// +/// @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 || 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. diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index 0020b57482..af9861d665 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -2502,286 +2502,331 @@ int fix_input_buffer(char_u *buf, int len) return len; } -/* - * map[!] : show all key mappings - * map[!] {lhs} : show key mapping for {lhs} - * map[!] {lhs} {rhs} : set key mapping for {lhs} to {rhs} - * noremap[!] {lhs} {rhs} : same, but no remapping for {rhs} - * unmap[!] {lhs} : remove key mapping for {lhs} - * abbr : show all abbreviations - * abbr {lhs} : show abbreviations for {lhs} - * abbr {lhs} {rhs} : set abbreviation for {lhs} to {rhs} - * noreabbr {lhs} {rhs} : same, but no remapping for {rhs} - * unabbr {lhs} : remove abbreviation for {lhs} - * - * maptype: 0 for :map, 1 for :unmap, 2 for noremap. - * - * arg is pointer to any arguments. Note: arg cannot be a read-only string, - * it will be modified. - * - * for :map mode is NORMAL + VISUAL + SELECTMODE + OP_PENDING - * for :map! mode is INSERT + CMDLINE - * for :cmap mode is CMDLINE - * for :imap mode is INSERT - * for :lmap mode is LANGMAP - * for :nmap mode is NORMAL - * for :vmap mode is VISUAL + SELECTMODE - * for :xmap mode is VISUAL - * for :smap mode is SELECTMODE - * for :omap mode is OP_PENDING - * for :tmap mode is TERM_FOCUS - * - * for :abbr mode is INSERT + CMDLINE - * for :iabbr mode is INSERT - * for :cabbr mode is CMDLINE - * - * Return 0 for success - * 1 for invalid arguments - * 2 for no match - * 4 for out of mem (deprecated, WON'T HAPPEN) - * 5 for entry not unique - */ -int -do_map ( - int maptype, - char_u *arg, - int mode, - int abbrev /* not a mapping but an abbreviation */ -) +/// Replace termcodes in the given LHS and RHS and store the results into the +/// `lhs` and `rhs` of the given @ref MapArguments struct. +/// +/// `rhs` and `orig_rhs` will both point to new allocated buffers. `orig_rhs` +/// will hold a copy of the given `orig_rhs`. +/// +/// The `*_len` variables will be set appropriately. If the length of +/// the final `lhs` exceeds `MAXMAPLEN`, `lhs_len` will be set equal to the +/// original larger length and `lhs` will be truncated. +/// +/// If RHS is equal to "", `rhs` will be the empty string, `rhs_len` +/// will be zero, and `rhs_is_noop` will be set to true. +/// +/// Any memory allocated by @ref replace_termcodes is freed before this function +/// returns. +/// +/// @param[in] orig_lhs Original mapping LHS, with characters to replace. +/// @param[in] orig_lhs_len `strlen` of orig_lhs. +/// @param[in] orig_rhs Original mapping RHS, with characters to replace. +/// @param[in] orig_rhs_len `strlen` of orig_rhs. +/// @param[in] cpo_flags See param docs for @ref replace_termcodes. +/// @param[out] mapargs MapArguments struct holding the replaced strings. +void set_maparg_lhs_rhs(const char_u *orig_lhs, const size_t orig_lhs_len, + const char_u *orig_rhs, const size_t orig_rhs_len, + int cpo_flags, MapArguments *mapargs) { - char_u *keys; - mapblock_T *mp, **mpp; - char_u *rhs; - char_u *p; - int n; - int len = 0; /* init for GCC */ - int hasarg; - int haskey; - int did_it = FALSE; - int did_local = FALSE; - int round; - char_u *keys_buf = NULL; - char_u *arg_buf = NULL; - int retval = 0; - int do_backslash; - int hash; - int new_hash; - mapblock_T **abbr_table; - mapblock_T **map_table; - bool unique = false; - bool nowait = false; - bool silent = false; - bool expr = false; - int noremap; - char_u *orig_rhs; + char_u *lhs_buf = NULL; + char_u *rhs_buf = NULL; - keys = arg; - map_table = maphash; - abbr_table = &first_abbr; + // If mapping has been given as ^V say, then replace the term codes + // with the appropriate two bytes. If it is a shifted special key, unshift + // it too, giving another two bytes. + // + // replace_termcodes() may move the result to allocated memory, which + // needs to be freed later (*lhs_buf and *rhs_buf). + // replace_termcodes() also removes CTRL-Vs and sometimes backslashes. + char_u *replaced = replace_termcodes(orig_lhs, orig_lhs_len, &lhs_buf, + true, true, true, cpo_flags); + mapargs->lhs_len = STRLEN(replaced); + xstrlcpy((char *)mapargs->lhs, (char *)replaced, sizeof(mapargs->lhs)); + + mapargs->orig_rhs_len = orig_rhs_len; + mapargs->orig_rhs = xcalloc(mapargs->orig_rhs_len + 1, sizeof(char_u)); + xstrlcpy((char *)mapargs->orig_rhs, (char *)orig_rhs, + mapargs->orig_rhs_len + 1); + + if (STRICMP(orig_rhs, "") == 0) { // "" means nothing + mapargs->rhs = xcalloc(1, sizeof(char_u)); // single null-char + mapargs->rhs_len = 0; + mapargs->rhs_is_noop = true; + } else { + replaced = replace_termcodes(orig_rhs, orig_rhs_len, &rhs_buf, + false, true, true, cpo_flags); + mapargs->rhs_len = STRLEN(replaced); + mapargs->rhs_is_noop = false; + mapargs->rhs = xcalloc(mapargs->rhs_len + 1, sizeof(char_u)); + xstrlcpy((char *)mapargs->rhs, (char *)replaced, mapargs->rhs_len + 1); + } - /* For ":noremap" don't remap, otherwise do remap. */ - if (maptype == 2) - noremap = REMAP_NONE; - else - noremap = REMAP_YES; + xfree(lhs_buf); + xfree(rhs_buf); +} - /* Accept , , ,