diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | runtime/doc/api.txt | 54 | ||||
-rw-r--r-- | src/nvim/api/buffer.c | 22 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.c | 228 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.h | 1 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 47 | ||||
-rw-r--r-- | src/nvim/getchar.c | 728 | ||||
-rw-r--r-- | src/nvim/getchar.h | 34 | ||||
-rw-r--r-- | test/functional/api/keymap_spec.lua | 505 |
9 files changed, 1311 insertions, 309 deletions
diff --git a/.gitignore b/.gitignore index cf0a11804d..57a90db188 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ local.mk /runtime/doc/*.html /runtime/doc/tags.ref /runtime/doc/errors.log +compile_commands.json diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 7c87ca7887..46e1c12090 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -807,6 +807,47 @@ nvim_get_keymap({mode}) *nvim_get_keymap()* Array of maparg()-like dictionaries describing mappings. The "buffer" key is always zero. +nvim_set_keymap({mode}, {lhs}, {rhs}, {opts}) *nvim_set_keymap()* + 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>` + + Parameters: ~ + {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". + {lhs} Left-hand-side |{lhs}| of the mapping. + {rhs} Right-hand-side |{rhs}| of the mapping. + {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. + +nvim_del_keymap({mode}, {lhs}) *nvim_del_keymap()* + 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. + nvim_get_commands({opts}) *nvim_get_commands()* Gets a map of global (non-buffer-local) Ex commands. @@ -1296,6 +1337,19 @@ nvim_buf_get_keymap({buffer}, {mode}) *nvim_buf_get_keymap()* Array of maparg()-like dictionaries describing mappings. The "buffer" key holds the associated buffer handle. + *nvim_buf_set_keymap()* +nvim_buf_set_keymap({buffer}, {mode}, {lhs}, {rhs}, {opts}) + Like |nvim_set_keymap|, but for a specific buffer. + + Parameters: ~ + {buffer} Buffer handle, or 0 for the current buffer. + +nvim_buf_del_keymap({buffer}, {mode}, {lhs}) *nvim_buf_del_keymap()* + Like |nvim_del_keymap|, but for a specific buffer. + + Parameters: ~ + {buffer} Buffer handle, or 0 for the current buffer. + nvim_buf_get_commands({buffer}, {opts}) *nvim_buf_get_commands()* Gets a map of buffer-local |user-commands|. 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 <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. /// 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 |<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. 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 "<Nop>", `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<C_UP> 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, "<nop>") == 0) { // "<Nop>" 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 <buffer>, <nowait>, <silent>, <expr> <script> and <unique> in - * any order. */ - for (;; ) { - /* - * Check for "<buffer>": mapping local to buffer. - */ - if (STRNCMP(keys, "<buffer>", 8) == 0) { - keys = skipwhite(keys + 8); - map_table = curbuf->b_maphash; - abbr_table = &curbuf->b_first_abbr; +/// Parse a string of |:map-arguments| into a @ref MapArguments struct. +/// +/// Termcodes, backslashes, CTRL-V's, etc. inside the extracted {lhs} and +/// {rhs} are replaced by @ref set_maparg_lhs_rhs. +/// +/// rhs and orig_rhs in the returned mapargs will be set to null or a pointer +/// to allocated memory and should be freed even on error. +/// +/// @param[in] strargs String of map args, e.g. "<buffer> <expr><silent>". +/// May contain leading or trailing whitespace. +/// @param[in] is_unmap True, if strargs should be parsed like an |:unmap| +/// command. |:unmap| commands interpret *all* text to the +/// right of the last map argument as the {lhs} of the +/// mapping, i.e. a literal ' ' character is treated like +/// a "<space>", rather than separating the {lhs} from the +/// {rhs}. +/// @param[out] mapargs MapArguments struct holding all extracted argument +/// values. +/// @return 0 on success, 1 if invalid arguments are detected. +int str_to_mapargs(const char_u *strargs, bool is_unmap, MapArguments *mapargs) +{ + const char_u *to_parse = strargs; + to_parse = skipwhite(to_parse); + MapArguments parsed_args; // copy these into mapargs "all at once" when done + memset(&parsed_args, 0, sizeof(parsed_args)); + + // Accept <buffer>, <nowait>, <silent>, <expr>, <script>, and <unique> in + // any order. + while (true) { + if (STRNCMP(to_parse, "<buffer>", 8) == 0) { + to_parse = skipwhite(to_parse + 8); + parsed_args.buffer = true; continue; } - /* - * Check for "<nowait>": don't wait for more characters. - */ - if (STRNCMP(keys, "<nowait>", 8) == 0) { - keys = skipwhite(keys + 8); - nowait = true; + if (STRNCMP(to_parse, "<nowait>", 8) == 0) { + to_parse = skipwhite(to_parse + 8); + parsed_args.nowait = true; continue; } - /* - * Check for "<silent>": don't echo commands. - */ - if (STRNCMP(keys, "<silent>", 8) == 0) { - keys = skipwhite(keys + 8); - silent = true; + if (STRNCMP(to_parse, "<silent>", 8) == 0) { + to_parse = skipwhite(to_parse + 8); + parsed_args.silent = true; continue; } // Ignore obsolete "<special>" modifier. - if (STRNCMP(keys, "<special>", 9) == 0) { - keys = skipwhite(keys + 9); + if (STRNCMP(to_parse, "<special>", 9) == 0) { + to_parse = skipwhite(to_parse + 9); continue; } - /* - * Check for "<script>": remap script-local mappings only - */ - if (STRNCMP(keys, "<script>", 8) == 0) { - keys = skipwhite(keys + 8); - noremap = REMAP_SCRIPT; + if (STRNCMP(to_parse, "<script>", 8) == 0) { + to_parse = skipwhite(to_parse + 8); + parsed_args.script = true; continue; } - /* - * Check for "<expr>": {rhs} is an expression. - */ - if (STRNCMP(keys, "<expr>", 6) == 0) { - keys = skipwhite(keys + 6); - expr = true; + if (STRNCMP(to_parse, "<expr>", 6) == 0) { + to_parse = skipwhite(to_parse + 6); + parsed_args.expr = true; continue; } - /* - * Check for "<unique>": don't overwrite an existing mapping. - */ - if (STRNCMP(keys, "<unique>", 8) == 0) { - keys = skipwhite(keys + 8); - unique = true; + + if (STRNCMP(to_parse, "<unique>", 8) == 0) { + to_parse = skipwhite(to_parse + 8); + parsed_args.unique = true; continue; } break; } - validate_maphash(); - - /* - * Find end of keys and skip CTRL-Vs (and backslashes) in it. - * Accept backslash like CTRL-V when 'cpoptions' does not contain 'B'. - * with :unmap white space is included in the keys, no argument possible. - */ - p = keys; - do_backslash = (vim_strchr(p_cpo, CPO_BSLASH) == NULL); - while (*p && (maptype == 1 || !ascii_iswhite(*p))) { - if ((p[0] == Ctrl_V || (do_backslash && p[0] == '\\')) && p[1] != NUL) { - p++; // skip CTRL-V or backslash + // Find the next whitespace character, call that the end of {lhs}. + // + // If a character (e.g. whitespace) is immediately preceded by a CTRL-V, + // "scan past" that character, i.e. don't "terminate" LHS with that character + // if it's whitespace. + // + // Treat backslash like CTRL-V when 'cpoptions' does not contain 'B'. + // + // With :unmap, literal white space is included in the {lhs}; there is no + // separate {rhs}. + const char_u *lhs_end = to_parse; + bool do_backslash = (vim_strchr(p_cpo, CPO_BSLASH) == NULL); + while (*lhs_end && (is_unmap || !ascii_iswhite(*lhs_end))) { + if ((lhs_end[0] == Ctrl_V || (do_backslash && lhs_end[0] == '\\')) + && lhs_end[1] != NUL) { + lhs_end++; // skip CTRL-V or backslash } - p++; + lhs_end++; } - if (*p != NUL) { - *p++ = NUL; + + // {lhs_end} is a pointer to the "terminating whitespace" after {lhs}. + // Use that to initialize {rhs_start}. + const char_u *rhs_start = skipwhite(lhs_end); + + // Given {lhs} might be larger than MAXMAPLEN before replace_termcodes + // (e.g. "<Space>" is longer than ' '), so first copy into a buffer. + size_t orig_lhs_len = (size_t)(lhs_end - to_parse); + char_u *lhs_to_replace = xcalloc(orig_lhs_len + 1, sizeof(char_u)); + xstrlcpy((char *)lhs_to_replace, (char *)to_parse, orig_lhs_len + 1); + + size_t orig_rhs_len = STRLEN(rhs_start); + set_maparg_lhs_rhs(lhs_to_replace, orig_lhs_len, + rhs_start, orig_rhs_len, + CPO_TO_CPO_FLAGS, &parsed_args); + + xfree(lhs_to_replace); + + *mapargs = parsed_args; + + if (parsed_args.lhs_len > MAXMAPLEN) { + return 1; } + return 0; +} - p = skipwhite(p); - rhs = p; - hasarg = (*rhs != NUL); - haskey = (*keys != NUL); +/// Actually set/unset a mapping or abbreviation. +/// +/// Parameters are like @ref buf_do_map unless otherwise noted. +/// @param args Fully parsed and "preprocessed" arguments for the +/// (un)map/abbrev command. Termcodes should have already been +/// replaced; whitespace, `<` and `>` signs, etc. in {lhs} and +/// {rhs} are assumed to be literal components of the mapping. +int buf_do_map_explicit(int maptype, MapArguments *args, int mode, + bool is_abbrev, buf_T *buf) +{ + mapblock_T *mp, **mpp; + char_u *p; + int n; + int len = 0; // init for GCC + int did_it = false; + int did_local = false; + int round; + int retval = 0; + int hash; + int new_hash; + mapblock_T **abbr_table; + mapblock_T **map_table; + int noremap; - /* check for :unmap without argument */ - if (maptype == 1 && !haskey) { - retval = 1; - goto theend; + map_table = maphash; + abbr_table = &first_abbr; + + // For ":noremap" don't remap, otherwise do remap. + if (maptype == 2) { + noremap = REMAP_NONE; + } else { + noremap = REMAP_YES; } - // If mapping has been given as ^V<C_UP> 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 (*keys_buf and *arg_buf). - // replace_termcodes() also removes CTRL-Vs and sometimes backslashes. - if (haskey) { - keys = replace_termcodes(keys, STRLEN(keys), &keys_buf, true, true, true, - CPO_TO_CPO_FLAGS); + if (args->buffer) { + // If <buffer> was given, we'll be searching through the buffer's + // mappings/abbreviations, not the globals. + map_table = buf->b_maphash; + abbr_table = &buf->b_first_abbr; } - orig_rhs = rhs; - if (hasarg) { - if (STRICMP(rhs, "<nop>") == 0) { // "<Nop>" means nothing - rhs = (char_u *)""; - } else { - rhs = replace_termcodes(rhs, STRLEN(rhs), &arg_buf, false, true, true, - CPO_TO_CPO_FLAGS); - } + if (args->script) { + noremap = REMAP_SCRIPT; } - // + validate_maphash(); + + bool has_lhs = (args->lhs[0] != NUL); + bool has_rhs = (args->rhs[0] != NUL) || args->rhs_is_noop; + + // check for :unmap without argument + if (maptype == 1 && !has_lhs) { + retval = 1; + goto theend; + } + + char_u *lhs = (char_u *)&args->lhs; + char_u *rhs = (char_u *)args->rhs; + char_u *orig_rhs = args->orig_rhs; + // check arguments and translate function keys - // - if (haskey) { - len = (int)STRLEN(keys); - if (len > MAXMAPLEN) { /* maximum length of MAXMAPLEN chars */ + if (has_lhs) { + len = (int)args->lhs_len; + if (len > MAXMAPLEN) { retval = 1; goto theend; } - if (abbrev && maptype != 1) { - /* - * If an abbreviation ends in a keyword character, the - * rest must be all keyword-char or all non-keyword-char. - * Otherwise we won't be able to find the start of it in a - * vi-compatible way. - */ + if (is_abbrev && maptype != 1) { + // + // If an abbreviation ends in a keyword character, the + // rest must be all keyword-char or all non-keyword-char. + // Otherwise we won't be able to find the start of it in a + // vi-compatible way. + // if (has_mbyte) { int first, last; int same = -1; - first = vim_iswordp(keys); + first = vim_iswordp(lhs); last = first; - p = keys + (*mb_ptr2len)(keys); + p = lhs + (*mb_ptr2len)(lhs); n = 1; - while (p < keys + len) { - ++n; /* nr of (multi-byte) chars */ - last = vim_iswordp(p); /* type of last char */ - if (same == -1 && last != first) - same = n - 1; /* count of same char type */ + while (p < lhs + len) { + n++; // nr of (multi-byte) chars + last = vim_iswordp(p); // type of last char + if (same == -1 && last != first) { + same = n - 1; // count of same char type + } p += (*mb_ptr2len)(p); } if (last && n > 2 && same >= 0 && same < n - 1) { retval = 1; goto theend; } - } else if (vim_iswordc(keys[len - 1])) /* ends in keyword char */ - for (n = 0; n < len - 2; ++n) - if (vim_iswordc(keys[n]) != vim_iswordc(keys[len - 2])) { + } else if (vim_iswordc(lhs[len - 1])) { // ends in keyword char + for (n = 0; n < len - 2; n++) { + if (vim_iswordc(lhs[n]) != vim_iswordc(lhs[len - 2])) { retval = 1; goto theend; } - /* An abbreviation cannot contain white space. */ - for (n = 0; n < len; ++n) - if (ascii_iswhite(keys[n])) { + } // for + } + // An abbreviation cannot contain white space. + for (n = 0; n < len; n++) { + if (ascii_iswhite(lhs[n])) { retval = 1; goto theend; } + } // for } } - if (haskey && hasarg && abbrev) /* if we will add an abbreviation */ - no_abbr = FALSE; /* reset flag that indicates there are - no abbreviations */ + if (has_lhs && has_rhs && is_abbrev) { // if we will add an abbreviation, + no_abbr = false; // reset flag that indicates there are no abbreviations + } - if (!haskey || (maptype != 1 && !hasarg)) + if (!has_lhs || (maptype != 1 && !has_rhs)) { msg_start(); + } - /* - * Check if a new local mapping wasn't already defined globally. - */ - if (map_table == curbuf->b_maphash && haskey && hasarg && maptype != 1) { - /* need to loop over all global hash lists */ - for (hash = 0; hash < 256 && !got_int; ++hash) { - if (abbrev) { - if (hash != 0) /* there is only one abbreviation list */ + // Check if a new local mapping wasn't already defined globally. + if (map_table == buf->b_maphash && has_lhs && has_rhs && maptype != 1) { + // need to loop over all global hash lists + for (hash = 0; hash < 256 && !got_int; hash++) { + if (is_abbrev) { + if (hash != 0) { // there is only one abbreviation list break; + } mp = first_abbr; - } else + } else { mp = maphash[hash]; + } for (; mp != NULL && !got_int; mp = mp->m_next) { - /* check entries with the same mode */ + // check entries with the same mode if ((mp->m_mode & mode) != 0 && mp->m_keylen == len - && unique - && STRNCMP(mp->m_keys, keys, (size_t)len) == 0) { - if (abbrev) + && args->unique + && STRNCMP(mp->m_keys, lhs, (size_t)len) == 0) { + if (is_abbrev) { EMSG2(_("E224: global abbreviation already exists for %s"), - mp->m_keys); - else - EMSG2(_("E225: global mapping already exists for %s"), - mp->m_keys); + mp->m_keys); + } else { + EMSG2(_("E225: global mapping already exists for %s"), mp->m_keys); + } retval = 5; goto theend; } @@ -2789,30 +2834,29 @@ do_map ( } } - /* - * When listing global mappings, also list buffer-local ones here. - */ - if (map_table != curbuf->b_maphash && !hasarg && maptype != 1) { - /* need to loop over all global hash lists */ - for (hash = 0; hash < 256 && !got_int; ++hash) { - if (abbrev) { - if (hash != 0) /* there is only one abbreviation list */ + // When listing global mappings, also list buffer-local ones here. + if (map_table != buf->b_maphash && !has_rhs && maptype != 1) { + // need to loop over all global hash lists + for (hash = 0; hash < 256 && !got_int; hash++) { + if (is_abbrev) { + if (hash != 0) { // there is only one abbreviation list break; - mp = curbuf->b_first_abbr; - } else - mp = curbuf->b_maphash[hash]; + } + mp = buf->b_first_abbr; + } else { + mp = buf->b_maphash[hash]; + } for (; mp != NULL && !got_int; mp = mp->m_next) { - /* check entries with the same mode */ + // check entries with the same mode if ((mp->m_mode & mode) != 0) { - if (!haskey) { /* show all entries */ - showmap(mp, TRUE); - did_local = TRUE; + if (!has_lhs) { // show all entries + showmap(mp, true); + did_local = true; } else { n = mp->m_keylen; - if (STRNCMP(mp->m_keys, keys, - (size_t)(n < len ? n : len)) == 0) { - showmap(mp, TRUE); - did_local = TRUE; + if (STRNCMP(mp->m_keys, lhs, (size_t)(n < len ? n : len)) == 0) { + showmap(mp, true); + did_local = true; } } } @@ -2820,103 +2864,98 @@ do_map ( } } - /* - * Find an entry in the maphash[] list that matches. - * For :unmap we may loop two times: once to try to unmap an entry with a - * matching 'from' part, a second time, if the first fails, to unmap an - * entry with a matching 'to' part. This was done to allow ":ab foo bar" - * to be unmapped by typing ":unab foo", where "foo" will be replaced by - * "bar" because of the abbreviation. - */ + // Find an entry in the maphash[] list that matches. + // For :unmap we may loop two times: once to try to unmap an entry with a + // matching 'from' part, a second time, if the first fails, to unmap an + // entry with a matching 'to' part. This was done to allow ":ab foo bar" + // to be unmapped by typing ":unab foo", where "foo" will be replaced by + // "bar" because of the abbreviation. for (round = 0; (round == 0 || maptype == 1) && round <= 1 - && !did_it && !got_int; ++round) { - /* need to loop over all hash lists */ - for (hash = 0; hash < 256 && !got_int; ++hash) { - if (abbrev) { - if (hash > 0) /* there is only one abbreviation list */ + && !did_it && !got_int; round++) { + // need to loop over all hash lists + for (hash = 0; hash < 256 && !got_int; hash++) { + if (is_abbrev) { + if (hash > 0) { // there is only one abbreviation list break; + } mpp = abbr_table; - } else + } else { mpp = &(map_table[hash]); + } for (mp = *mpp; mp != NULL && !got_int; mp = *mpp) { - - if (!(mp->m_mode & mode)) { /* skip entries with wrong mode */ + if (!(mp->m_mode & mode)) { // skip entries with wrong mode mpp = &(mp->m_next); continue; } - if (!haskey) { /* show all entries */ + if (!has_lhs) { // show all entries showmap(mp, map_table != maphash); - did_it = TRUE; - } else { /* do we have a match? */ - if (round) { /* second round: Try unmap "rhs" string */ + did_it = true; + } else { // do we have a match? + if (round) { // second round: Try unmap "rhs" string n = (int)STRLEN(mp->m_str); p = mp->m_str; } else { n = mp->m_keylen; p = mp->m_keys; } - if (STRNCMP(p, keys, (size_t)(n < len ? n : len)) == 0) { - if (maptype == 1) { /* delete entry */ - /* Only accept a full match. For abbreviations we - * ignore trailing space when matching with the - * "lhs", since an abbreviation can't have - * trailing space. */ - if (n != len && (!abbrev || round || n > len - || *skipwhite(keys + n) != NUL)) { + if (STRNCMP(p, lhs, (size_t)(n < len ? n : len)) == 0) { + if (maptype == 1) { // delete entry + // Only accept a full match. For abbreviations we + // ignore trailing space when matching with the + // "lhs", since an abbreviation can't have + // trailing space. + if (n != len && (!is_abbrev || round || n > len + || *skipwhite(lhs + n) != NUL)) { mpp = &(mp->m_next); continue; } - /* - * We reset the indicated mode bits. If nothing is - * left the entry is deleted below. - */ + // We reset the indicated mode bits. If nothing is + // left the entry is deleted below. mp->m_mode &= ~mode; - did_it = TRUE; /* remember we did something */ - } else if (!hasarg) { /* show matching entry */ + did_it = true; // remember we did something + } else if (!has_rhs) { // show matching entry showmap(mp, map_table != maphash); - did_it = TRUE; - } else if (n != len) { /* new entry is ambiguous */ + did_it = true; + } else if (n != len) { // new entry is ambiguous mpp = &(mp->m_next); continue; - } else if (unique) { - if (abbrev) - EMSG2(_("E226: abbreviation already exists for %s"), - p); - else + } else if (args->unique) { + if (is_abbrev) { + EMSG2(_("E226: abbreviation already exists for %s"), p); + } else { EMSG2(_("E227: mapping already exists for %s"), p); + } retval = 5; goto theend; - } else { /* new rhs for existing entry */ - mp->m_mode &= ~mode; /* remove mode bits */ - if (mp->m_mode == 0 && !did_it) { /* reuse entry */ + } else { // new rhs for existing entry + mp->m_mode &= ~mode; // remove mode bits + if (mp->m_mode == 0 && !did_it) { // reuse entry xfree(mp->m_str); mp->m_str = vim_strsave(rhs); xfree(mp->m_orig_str); mp->m_orig_str = vim_strsave(orig_rhs); mp->m_noremap = noremap; - mp->m_nowait = nowait; - mp->m_silent = silent; + mp->m_nowait = args->nowait; + mp->m_silent = args->silent; mp->m_mode = mode; - mp->m_expr = expr; + mp->m_expr = args->expr; mp->m_script_ID = current_SID; - did_it = TRUE; + did_it = true; } } - if (mp->m_mode == 0) { // entry can be deleted + if (mp->m_mode == 0) { // entry can be deleted mapblock_free(mpp); - continue; // continue with *mpp + continue; // continue with *mpp } - /* - * May need to put this entry into another hash list. - */ + // May need to put this entry into another hash list. new_hash = MAP_HASH(mp->m_mode, mp->m_keys[0]); - if (!abbrev && new_hash != hash) { + if (!is_abbrev && new_hash != hash) { *mpp = mp->m_next; mp->m_next = map_table[new_hash]; map_table[new_hash] = mp; - continue; /* continue with *mpp */ + continue; // continue with *mpp } } } @@ -2925,13 +2964,13 @@ do_map ( } } - if (maptype == 1) { /* delete entry */ + if (maptype == 1) { // delete entry if (!did_it) { - retval = 2; /* no match */ - } else if (*keys == Ctrl_C) { + retval = 2; // no match + } else if (*lhs == Ctrl_C) { // If CTRL-C has been unmapped, reuse it for Interrupting. - if (map_table == curbuf->b_maphash) { - curbuf->b_mapped_ctrl_c &= ~mode; + if (map_table == buf->b_maphash) { + buf->b_mapped_ctrl_c &= ~mode; } else { mapped_ctrl_c &= ~mode; } @@ -2939,48 +2978,46 @@ do_map ( goto theend; } - if (!haskey || !hasarg) { /* print entries */ - if (!did_it - && !did_local - ) { - if (abbrev) + if (!has_lhs || !has_rhs) { // print entries + if (!did_it && !did_local) { + if (is_abbrev) { MSG(_("No abbreviation found")); - else + } else { MSG(_("No mapping found")); + } } - goto theend; /* listing finished */ + goto theend; // listing finished } - if (did_it) /* have added the new entry already */ + if (did_it) { // have added the new entry already goto theend; + } - /* - * Get here when adding a new entry to the maphash[] list or abbrlist. - */ + // Get here when adding a new entry to the maphash[] list or abbrlist. mp = xmalloc(sizeof(mapblock_T)); // If CTRL-C has been mapped, don't always use it for Interrupting. - if (*keys == Ctrl_C) { - if (map_table == curbuf->b_maphash) { - curbuf->b_mapped_ctrl_c |= mode; + if (*lhs == Ctrl_C) { + if (map_table == buf->b_maphash) { + buf->b_mapped_ctrl_c |= mode; } else { mapped_ctrl_c |= mode; } } - mp->m_keys = vim_strsave(keys); + mp->m_keys = vim_strsave(lhs); mp->m_str = vim_strsave(rhs); mp->m_orig_str = vim_strsave(orig_rhs); mp->m_keylen = (int)STRLEN(mp->m_keys); mp->m_noremap = noremap; - mp->m_nowait = nowait; - mp->m_silent = silent; + mp->m_nowait = args->nowait; + mp->m_silent = args->silent; mp->m_mode = mode; - mp->m_expr = expr; + mp->m_expr = args->expr; mp->m_script_ID = current_SID; - /* add the new entry in front of the abbrlist or maphash[] list */ - if (abbrev) { + // add the new entry in front of the abbrlist or maphash[] list + if (is_abbrev) { mp->m_next = *abbr_table; *abbr_table = mp; } else { @@ -2990,11 +3027,86 @@ do_map ( } theend: - xfree(keys_buf); - xfree(arg_buf); return retval; } +/// Like @ref do_map, but you can specify the target buffer. +int buf_do_map(int maptype, char_u *arg, int mode, bool is_abbrev, buf_T *buf) +{ + MapArguments parsed_args; + int result = str_to_mapargs(arg, maptype == 1, &parsed_args); + switch (result) { + case 0: + break; + case 1: + result = 1; // invalid arguments + goto FREE_AND_RETURN; + default: + assert(false && "Unknown return code from str_to_mapargs!"); + result = -1; + goto FREE_AND_RETURN; + } // switch + + result = buf_do_map_explicit(maptype, &parsed_args, mode, is_abbrev, buf); + +FREE_AND_RETURN: + xfree(parsed_args.rhs); + xfree(parsed_args.orig_rhs); + return result; +} + + +/// Set or remove a mapping or an abbreviation in the current buffer, OR +/// display (matching) mappings/abbreviations. +/// +/// ```vim +/// 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} +/// +/// 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 +/// ``` +/// +/// @param maptype 0 for |:map|, 1 for |:unmap|, 2 for |noremap|. +/// @param arg C-string containing the arguments of the map/abbrev +/// command, i.e. everything except the initial `:[X][nore]map`. +/// - Cannot be a read-only string; it will be modified. +/// @param mode Bitflags representing the mode in which to set the mapping. +/// See @ref get_map_mode. +/// @param is_abbrev True if setting an abbreviation, false otherwise. +/// +/// @return 0 on success. On failure, will return one of the following: +/// - 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, bool is_abbrev) +{ + return buf_do_map(maptype, arg, mode, is_abbrev, curbuf); +} + /* * Delete one entry from the abbrlist or maphash[]. * "mpp" is a pointer to the m_next field of the PREVIOUS entry! diff --git a/src/nvim/getchar.h b/src/nvim/getchar.h index 4f548d975a..a40ea7730a 100644 --- a/src/nvim/getchar.h +++ b/src/nvim/getchar.h @@ -5,6 +5,7 @@ #include "nvim/types.h" #include "nvim/buffer_defs.h" #include "nvim/ex_cmds_defs.h" +#include "nvim/vim.h" /// Values for "noremap" argument of ins_typebuf() /// @@ -23,6 +24,39 @@ typedef enum { FLUSH_INPUT // flush typebuf and inchar() input } flush_buffers_T; +/// All possible |:map-arguments| usable in a |:map| command. +/// +/// The <special> argument has no effect on mappings and is excluded from this +/// struct declaration. |noremap| is included, since it behaves like a map +/// argument when used in a mapping. +/// +/// @see mapblock_T +struct map_arguments { + bool buffer; + bool expr; + bool noremap; + bool nowait; + bool script; + bool silent; + bool unique; + + /// The {lhs} of the mapping. + /// + /// vim limits this to MAXMAPLEN characters, allowing us to use a static + /// buffer. Setting lhs_len to a value larger than MAXMAPLEN can signal + /// that {lhs} was too long and truncated. + char_u lhs[MAXMAPLEN + 1]; + size_t lhs_len; + + char_u *rhs; /// The {rhs} of the mapping. + size_t rhs_len; + bool rhs_is_noop; /// True when the {orig_rhs} is <nop>. + + char_u *orig_rhs; /// The original text of the {rhs}. + size_t orig_rhs_len; +}; +typedef struct map_arguments MapArguments; + #define KEYLEN_PART_KEY -1 /* keylen value for incomplete key-code */ #define KEYLEN_PART_MAP -2 /* keylen value for incomplete mapping */ #define KEYLEN_REMOVED 9999 /* keylen value for removed sequence */ diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua index f52372bee3..47478dce5d 100644 --- a/test/functional/api/keymap_spec.lua +++ b/test/functional/api/keymap_spec.lua @@ -1,15 +1,19 @@ local helpers = require('test.functional.helpers')(after_each) local global_helpers = require('test.helpers') +local bufmeths = helpers.bufmeths local clear = helpers.clear local command = helpers.command local curbufmeths = helpers.curbufmeths -local eq = helpers.eq +local eq, neq = helpers.eq, helpers.neq +local expect_err = helpers.expect_err +local feed = helpers.feed local funcs = helpers.funcs local meths = helpers.meths local source = helpers.source local shallowcopy = global_helpers.shallowcopy +local sleep = global_helpers.sleep describe('nvim_get_keymap', function() before_each(clear) @@ -308,3 +312,502 @@ describe('nvim_get_keymap', function() eq({space_table}, meths.get_keymap('n')) end) end) + +describe('nvim_[set/del]_keymap', function() + before_each(clear) + + -- generate_expected is truthy when we want to generate an expected output for + -- maparg(); mapargs() won't take '!' as an input, though it will return '!' + -- in its output if getting a mapping set with |:map!| + local function normalize_mapmode(mode, generate_expected) + if not generate_expected and mode == '!' then + -- can't retrieve mapmode-ic mappings with '!', but can with 'i' or 'c'. + mode = 'i' + elseif mode == '' or mode == ' ' or mode == 'm' then + mode = generate_expected and ' ' or 'm' + end + return mode + end + + -- Generate a mapargs dict, for comparison against the mapping that was + -- actually set + local function generate_mapargs(mode, lhs, rhs, opts) + if not opts then + opts = {} + end + + local to_return = {} + to_return.mode = normalize_mapmode(mode, true) + to_return.noremap = not opts.noremap and 0 or 1 + to_return.lhs = lhs + to_return.rhs = rhs + to_return.silent = not opts.silent and 0 or 1 + to_return.nowait = not opts.nowait and 0 or 1 + to_return.expr = not opts.expr and 0 or 1 + to_return.sid = not opts.sid and 0 or opts.sid + to_return.buffer = not opts.buffer and 0 or opts.buffer + + -- mode 't' doesn't print when calling maparg + if mode == 't' then + to_return.mode = '' + end + + return to_return + end + + -- Retrieve a mapargs dict from neovim, if one exists + local function get_mapargs(mode, lhs) + return funcs.maparg(lhs, normalize_mapmode(mode), false, true) + end + + -- Test error handling + it('throws errors when given empty lhs', function() + -- escape parentheses in lua string, else comparison fails erroneously + expect_err('Invalid %(empty%) LHS', + meths.set_keymap, '', '', 'rhs', {}) + expect_err('Invalid %(empty%) LHS', + meths.set_keymap, '', '', '', {}) + + expect_err('Invalid %(empty%) LHS', meths.del_keymap, '', '') + end) + + it('throws errors when given an lhs longer than MAXMAPLEN', function() + -- assume MAXMAPLEN of 50 chars, as declared in vim.h + local MAXMAPLEN = 50 + local lhs = '' + for i=1,MAXMAPLEN do + lhs = lhs..(i % 10) + end + + -- exactly 50 chars should be fine + meths.set_keymap('', lhs, 'rhs', {}) + + -- del_keymap should unmap successfully + meths.del_keymap('', lhs) + eq({}, get_mapargs('', lhs)) + + -- 51 chars should produce an error + lhs = lhs..'1' + expect_err('LHS exceeds maximum map length: '..lhs, + meths.set_keymap, '', lhs, 'rhs', {}) + expect_err('LHS exceeds maximum map length: '..lhs, + meths.del_keymap, '', lhs) + end) + + it('does not throw errors when rhs is longer than MAXMAPLEN', function() + local MAXMAPLEN = 50 + local rhs = '' + for i=1,MAXMAPLEN do + rhs = rhs..(i % 10) + end + rhs = rhs..'1' + meths.set_keymap('', 'lhs', rhs, {}) + eq(generate_mapargs('', 'lhs', rhs), + get_mapargs('', 'lhs')) + end) + + it('throws errors when given too-long mode shortnames', function() + expect_err('Shortname is too long: map', + meths.set_keymap, 'map', 'lhs', 'rhs', {}) + + expect_err('Shortname is too long: vmap', + meths.set_keymap, 'vmap', 'lhs', 'rhs', {}) + + expect_err('Shortname is too long: xnoremap', + meths.set_keymap, 'xnoremap', 'lhs', 'rhs', {}) + + expect_err('Shortname is too long: map', meths.del_keymap, 'map', 'lhs') + expect_err('Shortname is too long: vmap', meths.del_keymap, 'vmap', 'lhs') + expect_err('Shortname is too long: xnoremap', meths.del_keymap, 'xnoremap', 'lhs') + end) + + it('throws errors when given unrecognized mode shortnames', function() + expect_err('Invalid mode shortname: ?', + meths.set_keymap, '?', 'lhs', 'rhs', {}) + + expect_err('Invalid mode shortname: y', + meths.set_keymap, 'y', 'lhs', 'rhs', {}) + + expect_err('Invalid mode shortname: p', + meths.set_keymap, 'p', 'lhs', 'rhs', {}) + + expect_err('Invalid mode shortname: ?', meths.del_keymap, '?', 'lhs') + expect_err('Invalid mode shortname: y', meths.del_keymap, 'y', 'lhs') + expect_err('Invalid mode shortname: p', meths.del_keymap, 'p', 'lhs') + end) + + it('throws errors when optnames are almost right', function() + expect_err('Invalid key: silentt', + meths.set_keymap, 'n', 'lhs', 'rhs', {silentt = true}) + expect_err('Invalid key: sidd', + meths.set_keymap, 'n', 'lhs', 'rhs', {sidd = false}) + expect_err('Invalid key: nowaiT', + meths.set_keymap, 'n', 'lhs', 'rhs', {nowaiT = false}) + end) + + it('does not recognize <buffer> as an option', function() + expect_err('Invalid key: buffer', + meths.set_keymap, 'n', 'lhs', 'rhs', {buffer = true}) + end) + + local optnames = {'nowait', 'silent', 'script', 'expr', 'unique'} + for _, opt in ipairs(optnames) do + -- note: need '%' to escape hyphens, which have special meaning in lua + it('throws an error when given non-boolean value for '..opt, function() + local opts = {} + opts[opt] = 2 + expect_err('Gave non%-boolean value for an opt: '..opt, + meths.set_keymap, 'n', 'lhs', 'rhs', opts) + end) + end + + -- Perform tests of basic functionality + it('can set ordinary mappings', function() + meths.set_keymap('n', 'lhs', 'rhs', {}) + eq(generate_mapargs('n', 'lhs', 'rhs'), get_mapargs('n', 'lhs')) + + meths.set_keymap('v', 'lhs', 'rhs', {}) + eq(generate_mapargs('v', 'lhs', 'rhs'), get_mapargs('v', 'lhs')) + end) + + it('doesn\'t throw when lhs or rhs have leading/trailing WS', function() + meths.set_keymap('n', ' lhs', 'rhs', {}) + eq(generate_mapargs('n', '<Space><Space><Space>lhs', 'rhs'), + get_mapargs('n', ' lhs')) + + meths.set_keymap('n', 'lhs ', 'rhs', {}) + eq(generate_mapargs('n', 'lhs<Space><Space><Space><Space>', 'rhs'), + get_mapargs('n', 'lhs ')) + + meths.set_keymap('v', ' lhs ', '\trhs\t\f', {}) + eq(generate_mapargs('v', '<Space>lhs<Space><Space>', '\trhs\t\f'), + get_mapargs('v', ' lhs ')) + end) + + it('can set noremap mappings', function() + meths.set_keymap('x', 'lhs', 'rhs', {noremap = true}) + eq(generate_mapargs('x', 'lhs', 'rhs', {noremap = true}), + get_mapargs('x', 'lhs')) + + meths.set_keymap('t', 'lhs', 'rhs', {noremap = true}) + eq(generate_mapargs('t', 'lhs', 'rhs', {noremap = true}), + get_mapargs('t', 'lhs')) + end) + + it('can unmap mappings', function() + meths.set_keymap('v', 'lhs', 'rhs', {}) + meths.del_keymap('v', 'lhs') + eq({}, get_mapargs('v', 'lhs')) + + meths.set_keymap('t', 'lhs', 'rhs', {noremap = true}) + meths.del_keymap('t', 'lhs') + eq({}, get_mapargs('t', 'lhs')) + end) + + -- Test some edge cases + it('accepts "!" and " " and "" as synonyms for mapmode-nvo', function() + local nvo_shortnames = {'', ' ', '!'} + for _, name in ipairs(nvo_shortnames) do + meths.set_keymap(name, 'lhs', 'rhs', {}) + meths.del_keymap(name, 'lhs') + eq({}, get_mapargs(name, 'lhs')) + end + end) + + local special_chars = {'<C-U>', '<S-Left>', '<F12><F2><Tab>', '<Space><Tab>'} + for _, lhs in ipairs(special_chars) do + for _, rhs in ipairs(special_chars) do + local mapmode = '!' + it('can set mappings with special characters, lhs: '..lhs..', rhs: '..rhs, + function() + meths.set_keymap(mapmode, lhs, rhs, {}) + eq(generate_mapargs(mapmode, lhs, rhs), get_mapargs(mapmode, lhs)) + end) + end + end + + it('can set mappings containing literal keycodes', function() + meths.set_keymap('n', '\n\r\n', 'rhs', {}) + local expected = generate_mapargs('n', '<NL><CR><NL>', 'rhs') + eq(expected, get_mapargs('n', '<C-j><CR><C-j>')) + end) + + it('can set mappings whose RHS is a <Nop>', function() + meths.set_keymap('i', 'lhs', '<Nop>', {}) + command('normal ilhs') + eq({''}, curbufmeths.get_lines(0, -1, 0)) -- imap to <Nop> does nothing + eq(generate_mapargs('i', 'lhs', '<Nop>', {}), + get_mapargs('i', 'lhs')) + + -- also test for case insensitivity + meths.set_keymap('i', 'lhs', '<nOp>', {}) + command('normal ilhs') + eq({''}, curbufmeths.get_lines(0, -1, 0)) + -- note: RHS in returned mapargs() dict reflects the original RHS + -- provided by the user + eq(generate_mapargs('i', 'lhs', '<nOp>', {}), + get_mapargs('i', 'lhs')) + + meths.set_keymap('i', 'lhs', '<NOP>', {}) + command('normal ilhs') + eq({''}, curbufmeths.get_lines(0, -1, 0)) + eq(generate_mapargs('i', 'lhs', '<NOP>', {}), + get_mapargs('i', 'lhs')) + end) + + it('treats an empty RHS in a mapping like a <Nop>', function() + meths.set_keymap('i', 'lhs', '', {}) + command('normal ilhs') + eq({''}, curbufmeths.get_lines(0, -1, 0)) + eq(generate_mapargs('i', 'lhs', '', {}), + get_mapargs('i', 'lhs')) + end) + + it('can set and unset <M-">', function() + -- Taken from the legacy test: test_mapping.vim. Exposes a bug in which + -- replace_termcodes changes the length of the mapping's LHS, but + -- do_map continues to use the *old* length of LHS. + meths.set_keymap('i', '<M-">', 'foo', {}) + meths.del_keymap('i', '<M-">') + eq({}, get_mapargs('i', '<M-">')) + end) + + it('interprets control sequences in expr-quotes correctly when called ' + ..'inside vim', function() + command([[call nvim_set_keymap('i', "\<space>", "\<tab>", {})]]) + eq(generate_mapargs('i', '<Space>', '\t', {}), + get_mapargs('i', '<Space>')) + feed('i ') + eq({'\t'}, curbufmeths.get_lines(0, -1, 0)) + end) + + it('throws appropriate error messages when setting <unique> maps', function() + meths.set_keymap('l', 'lhs', 'rhs', {}) + expect_err('E227: mapping already exists for lhs', + meths.set_keymap, 'l', 'lhs', 'rhs', {unique = true}) + -- different mapmode, no error should be thrown + meths.set_keymap('t', 'lhs', 'rhs', {unique = true}) + end) + + it('can set <expr> mappings whose RHS change dynamically', function() + meths.command_output([[ + function! FlipFlop() abort + if !exists('g:flip') | let g:flip = 0 | endif + let g:flip = !g:flip + return g:flip + endfunction + ]]) + eq(1, meths.call_function('FlipFlop', {})) + eq(0, meths.call_function('FlipFlop', {})) + eq(1, meths.call_function('FlipFlop', {})) + eq(0, meths.call_function('FlipFlop', {})) + + meths.set_keymap('i', 'lhs', 'FlipFlop()', {expr = true}) + command('normal ilhs') + eq({'1'}, curbufmeths.get_lines(0, -1, 0)) + + command('normal! ggVGd') + + command('normal ilhs') + eq({'0'}, curbufmeths.get_lines(0, -1, 0)) + end) + + it('can set mappings that do trigger other mappings', function() + meths.set_keymap('i', 'mhs', 'rhs', {}) + meths.set_keymap('i', 'lhs', 'mhs', {}) + + command('normal imhs') + eq({'rhs'}, curbufmeths.get_lines(0, -1, 0)) + + command('normal! ggVGd') + + command('normal ilhs') + eq({'rhs'}, curbufmeths.get_lines(0, -1, 0)) + end) + + it("can set noremap mappings that don't trigger other mappings", function() + meths.set_keymap('i', 'mhs', 'rhs', {}) + meths.set_keymap('i', 'lhs', 'mhs', {noremap = true}) + + command('normal imhs') + eq({'rhs'}, curbufmeths.get_lines(0, -1, 0)) + + command('normal! ggVGd') + + command('normal ilhs') -- shouldn't trigger mhs-to-rhs mapping + eq({'mhs'}, curbufmeths.get_lines(0, -1, 0)) + end) + + it("can set nowait mappings that fire without waiting", function() + meths.set_keymap('i', '123456', 'longer', {}) + meths.set_keymap('i', '123', 'shorter', {nowait = true}) + + -- feed keys one at a time; if all keys arrive atomically, the longer + -- mapping will trigger + local keys = 'i123456' + for c in string.gmatch(keys, '.') do + feed(c) + sleep(5) + end + eq({'shorter456'}, curbufmeths.get_lines(0, -1, 0)) + end) + + -- Perform exhaustive tests of basic functionality + local mapmodes = {'n', 'v', 'x', 's', 'o', '!', 'i', 'l', 'c', 't', ' ', ''} + for _, mapmode in ipairs(mapmodes) do + it('can set/unset normal mappings in mapmode '..mapmode, function() + meths.set_keymap(mapmode, 'lhs', 'rhs', {}) + eq(generate_mapargs(mapmode, 'lhs', 'rhs'), + get_mapargs(mapmode, 'lhs')) + + -- some mapmodes (like 'o') will prevent other mapmodes (like '!') from + -- taking effect, so unmap after each mapping + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + end + + for _, mapmode in ipairs(mapmodes) do + it('can set/unset noremap mappings using mapmode '..mapmode, function() + meths.set_keymap(mapmode, 'lhs', 'rhs', {noremap = true}) + eq(generate_mapargs(mapmode, 'lhs', 'rhs', {noremap = true}), + get_mapargs(mapmode, 'lhs')) + + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + end + + -- Test map-arguments, using optnames from above + -- remove some map arguments that are harder to test, or were already tested + optnames = {'nowait', 'silent', 'expr', 'noremap'} + for _, mapmode in ipairs(mapmodes) do + local printable_mode = normalize_mapmode(mapmode) + + -- Test with single mappings + for _, maparg in ipairs(optnames) do + it('can set/unset '..printable_mode..'-mappings with maparg: '..maparg, + function() + meths.set_keymap(mapmode, 'lhs', 'rhs', {[maparg] = true}) + eq(generate_mapargs(mapmode, 'lhs', 'rhs', {[maparg] = true}), + get_mapargs(mapmode, 'lhs')) + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + it ('can set/unset '..printable_mode..'-mode mappings with maparg '.. + maparg..', whose value is false', function() + meths.set_keymap(mapmode, 'lhs', 'rhs', {[maparg] = false}) + eq(generate_mapargs(mapmode, 'lhs', 'rhs'), + get_mapargs(mapmode, 'lhs')) + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + end + + -- Test with triplets of mappings, one of which is false + for i = 1, (#optnames - 2) do + local opt1, opt2, opt3 = optnames[i], optnames[i + 1], optnames[i + 2] + it('can set/unset '..printable_mode..'-mode mappings with mapargs '.. + opt1..', '..opt2..', '..opt3, function() + local opts = {[opt1] = true, [opt2] = false, [opt3] = true} + meths.set_keymap(mapmode, 'lhs', 'rhs', opts) + eq(generate_mapargs(mapmode, 'lhs', 'rhs', opts), + get_mapargs(mapmode, 'lhs')) + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + end + end +end) + +describe('nvim_buf_[set/del]_keymap', function() + before_each(clear) + + -- nvim_set_keymap is implemented as a wrapped call to nvim_buf_set_keymap, + -- so its tests also effectively test nvim_buf_set_keymap + + -- here, we mainly test for buffer specificity and other special cases + + -- switch to the given buffer, abandoning any changes in the current buffer + local function switch_to_buf(bufnr) + command(bufnr..'buffer!') + end + + -- `set hidden`, then create two buffers and return their bufnr's + -- If start_from_first is truthy, the first buffer will be open when + -- the function returns; if falsy, the second buffer will be open. + local function make_two_buffers(start_from_first) + command('set hidden') + + local first_buf = meths.call_function('bufnr', {'%'}) + command('new') + local second_buf = meths.call_function('bufnr', {'%'}) + neq(second_buf, first_buf) -- sanity check + + if start_from_first then + switch_to_buf(first_buf) + end + + return first_buf, second_buf + end + + it('rejects negative bufnr values', function() + expect_err('Wrong type for argument 1, expecting Buffer', + bufmeths.set_keymap, -1, '', 'lhs', 'rhs', {}) + end) + + it('can set mappings active in the current buffer but not others', function() + local first, second = make_two_buffers(true) + + bufmeths.set_keymap(0, '', 'lhs', 'irhs<Esc>', {}) + command('normal lhs') + eq({'rhs'}, bufmeths.get_lines(0, 0, 1, 1)) + + -- mapping should have no effect in new buffer + switch_to_buf(second) + command('normal lhs') + eq({''}, bufmeths.get_lines(0, 0, 1, 1)) + + -- mapping should remain active in old buffer + switch_to_buf(first) + command('normal ^lhs') + eq({'rhsrhs'}, bufmeths.get_lines(0, 0, 1, 1)) + end) + + it('can set local mappings in buffer other than current', function() + local first = make_two_buffers(false) + bufmeths.set_keymap(first, '', 'lhs', 'irhs<Esc>', {}) + + -- shouldn't do anything + command('normal lhs') + eq({''}, bufmeths.get_lines(0, 0, 1, 1)) + + -- should take effect + switch_to_buf(first) + command('normal lhs') + eq({'rhs'}, bufmeths.get_lines(0, 0, 1, 1)) + end) + + it('can disable mappings made in another buffer, inside that buffer', function() + local first = make_two_buffers(false) + bufmeths.set_keymap(first, '', 'lhs', 'irhs<Esc>', {}) + bufmeths.del_keymap(first, '', 'lhs') + switch_to_buf(first) + + -- shouldn't do anything + command('normal lhs') + eq({''}, bufmeths.get_lines(0, 0, 1, 1)) + end) + + it("can't disable mappings given wrong buffer handle", function() + local first, second = make_two_buffers(false) + bufmeths.set_keymap(first, '', 'lhs', 'irhs<Esc>', {}) + expect_err('E31: No such mapping', + bufmeths.del_keymap, second, '', 'lhs') + + -- should still work + switch_to_buf(first) + command('normal lhs') + eq({'rhs'}, bufmeths.get_lines(0, 0, 1, 1)) + end) +end) |