diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index b07a4987e0..c183a7e0cd 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1271,32 +1271,45 @@ vim.inspect() *vim.inspect()* • https://github.com/kikito/inspect.lua • https://github.com/mpeterv/vinspect -vim.keycode({str}, {info}) *vim.keycode()* - Translates keycodes. +vim.keycode({keys}, {info}) *vim.keycode()* + Converts keys from |key-notation| to the internal encoding. Optionally + returns structured key-chord info as retval 2. - Example: >lua + Inverse of |keytrans()|, which converts the internal encoding back to + |key-notation|. + + Examples: >lua local k = vim.keycode vim.g.mapleader = k'' + + -- Split a key sequence into chords, e.g. to inspect modifiers. + local _, chords = vim.keycode('v', true) + assert(chords[1].key == 'w' and chords[1].mod[1] == 'C') + + -- keytrans() is the inverse: internal encoding => key-notation. + assert(vim.fn.keytrans(vim.keycode('')) == '') < Parameters: ~ - • {str} (`string`) String to be converted. - • {info} (`boolean?`) Also returns a normalized detailed info about the - key as a second return value. + • {keys} (`string`) Keys in |key-notation|. + • {info} (`boolean?`) Also return key-chord info. Return (multiple): ~ - (`string`) - (`table?`) List of dicts where each dict represents a key chord, and - has fields: - • {key} (`string`) The key chord normalized without modifiers. - • {key_orig} (`string?`) Non-normalized version of key chord, only - present when it differs from {key}. Example: `lt` and `<`. - • {key_raw} (`string`) The original key chord with modifiers. + (`string`) Internal bytes representation of the given `keys`. + (`table?`) List of parsed key-chords, each with fields: + • {key} (`string`) Key without modifiers. Example: `` has `key` + `a`. + • {key_alt} (`string?`) Alternative spelling of `key`, or nil if There + Is No Alternative (TINA). Example: `key="<"` has `key_alt="lt"`. + • {keys} (`string`) Full key-chord in canonical key-notation (as + produced by |keytrans()|), including modifiers. Example: `` has + `keys=""`. • {mod} (`('M'|'T'|'C'|'S'|'2'|'3'|'4'|'D')[]`) A list of single character modifiers of the key. See also: ~ • |nvim_replace_termcodes()| + • |keytrans()| vim.lua_omnifunc({find_start}) *vim.lua_omnifunc()* Omnifunc for completing Lua values from the runtime Lua interpreter, diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 129c653e46..1259391921 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -313,7 +313,6 @@ LUA • |vim.pack.get()| output includes revision of a pending update. • |vim.pack.get()| can fetch new updates before computing the output. • |vim.o| now accepts table style values for assignment. -• |vim.pos| and |vim.range| can now convert between mark positions. • |vim.keycode()| returns structured info as return value 2. OPTIONS diff --git a/runtime/lua/vim/_core/editor.lua b/runtime/lua/vim/_core/editor.lua index 87272e3d10..8e1971402f 100644 --- a/runtime/lua/vim/_core/editor.lua +++ b/runtime/lua/vim/_core/editor.lua @@ -1260,46 +1260,55 @@ function vim.print(...) return vim._print(false, ...) end ---- @class vim.keycode.info +--- @class vim.keycode.chord --- @inlinedoc --- ---- The key chord normalized without modifiers. +--- Key without modifiers. Example: `` has `key` `a`. --- @field key string --- ---- Non-normalized version of key chord, ---- only present when it differs from {key}. ---- Example: `lt` and `<`. ---- @field key_orig string? +--- Alternative spelling of `key`, or nil if There Is No Alternative (TINA). +--- Example: `key="<"` has `key_alt="lt"`. +--- @field key_alt string? --- ---- The original key chord with modifiers. ---- @field key_raw string +--- Full key-chord in canonical key-notation (as produced by |keytrans()|), including modifiers. +--- Example: `` has `keys=""`. +--- @field keys string --- --- A list of single character modifiers of the key. --- @field mod ('M'|'T'|'C'|'S'|'2'|'3'|'4'|'D')[] ---- Translates keycodes. +--- Converts keys from [key-notation] to the internal encoding. Optionally returns +--- structured key-chord info as retval 2. --- ---- Example: +--- Inverse of [keytrans()], which converts the internal encoding back to [key-notation]. +--- +--- Examples: --- --- ```lua --- local k = vim.keycode --- vim.g.mapleader = k'' +--- +--- -- Split a key sequence into chords, e.g. to inspect modifiers. +--- local _, chords = vim.keycode('v', true) +--- assert(chords[1].key == 'w' and chords[1].mod[1] == 'C') +--- +--- -- keytrans() is the inverse: internal encoding => key-notation. +--- assert(vim.fn.keytrans(vim.keycode('')) == '') --- ``` --- --- ---- @param str string String to be converted. ---- @param info boolean? Also returns a normalized ---- detailed info about the key as a second return value. ---- @return string ---- @return vim.keycode.info[]? List of dicts where ---- each dict represents a key chord, and has fields: +--- @param keys string Keys in [key-notation]. +--- @param info boolean? Also return key-chord info. +--- @return string # Internal bytes representation of the given `keys`. +--- @return vim.keycode.chord[]? # List of parsed key-chords, each with fields: --- @see |nvim_replace_termcodes()| -function vim.keycode(str, info) +--- @see |keytrans()| +function vim.keycode(keys, info) if info then - local keycode = vim.keycode(str) - return keycode, vim._keyarg(keycode) + local keycode = vim.keycode(keys) + return keycode, vim._core.keyparse(keycode) else - return vim.api.nvim_replace_termcodes(str, true, true, true) + return vim.api.nvim_replace_termcodes(keys, true, true, true) end end diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua index 2700bccd27..999bc57dcf 100644 --- a/runtime/lua/vim/_meta/builtin.lua +++ b/runtime/lua/vim/_meta/builtin.lua @@ -202,6 +202,12 @@ function vim._core.ui_flush() end --- @return boolean function vim._core.check_interrupt() end +--- @nodoc +--- Parses `keys` (internal representation) into a list of key chords. See |vim.keycode()|. +--- @param keys string +--- @return vim.keycode.chord[] +function vim._core.keyparse(keys) end + --- Subscribe to |ui-events|, similar to |nvim_ui_attach()| but receive events in a Lua callback. --- Used to implement screen elements like popupmenu or message handling in Lua. --- diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 3052401b02..bbb3ef09d5 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -2391,7 +2391,7 @@ static int command_line_handle_key(CommandLineState *s) end: // put the character in the command line if (IS_SPECIAL(s->c) || mod_mask != 0) { - put_on_cmdline(get_special_key_name(s->c, mod_mask, NULL), -1, true); + put_on_cmdline(get_special_key_name(s->c, mod_mask), -1, true); } else { int j = utf_char2bytes(s->c, IObuff); IObuff[j] = NUL; // exclude composing chars diff --git a/src/nvim/insert.c b/src/nvim/insert.c index 101a186073..9f69c2f59e 100644 --- a/src/nvim/insert.c +++ b/src/nvim/insert.c @@ -1904,7 +1904,7 @@ static void insert_special(int c, int allow_modmask, int ctrlv) allow_modmask = true; } if (IS_SPECIAL(c) || (mod_mask && allow_modmask)) { - char *p = get_special_key_name(c, mod_mask, NULL); + char *p = get_special_key_name(c, mod_mask); int len = (int)strlen(p); c = (uint8_t)p[len - 1]; if (len > 2) { diff --git a/src/nvim/keycodes.c b/src/nvim/keycodes.c index e0eb352128..bf1fd3a03a 100644 --- a/src/nvim/keycodes.c +++ b/src/nvim/keycodes.c @@ -254,8 +254,8 @@ int handle_x_keys(const int key) return key; } -/// @return a string which contains the name of the given key when the given modifiers are down. -char *get_special_key_name(int c, int modifiers, struct keycode_data *data) +/// Gets a key "chord" (key + modifiers) as a string, and optionally as structured `data`. +char *get_special_key(int c, int modifiers, struct keychord *data) { static char string[MAX_KEY_NAME_LEN + 1]; bool is_ascii_ctrl = false; @@ -298,7 +298,7 @@ char *get_special_key_name(int c, int modifiers, struct keycode_data *data) } if (data) { - data->modifiers = 0; + data->mods = 0; } // translate the modifier into a string @@ -309,7 +309,7 @@ char *get_special_key_name(int c, int modifiers, struct keycode_data *data) string[idx++] = '-'; if (data) { - data->modifiers |= mod_mask_table[i].mod_flag; + data->mods |= mod_mask_table[i].mod_flag; } } } @@ -317,7 +317,7 @@ char *get_special_key_name(int c, int modifiers, struct keycode_data *data) if (table_idx < 0) { // unknown special key, may output t_xx if (IS_SPECIAL(c)) { if (data) { - data->key_orig = (String){ NULL, 0 }; + data->key_alt = (String){ NULL, 0 }; data->key = (String){ &string[idx], 4 }; } @@ -331,14 +331,14 @@ char *get_special_key_name(int c, int modifiers, struct keycode_data *data) if (len == 1 && vim_isprintc(c)) { if (data) { if ('A' <= c && c <= 'Z') { - data->_key_mem = (char)c + 32; - data->key = (String){ &data->_key_mem, 1 }; + data->key_mem = (char)c + 32; + data->key = (String){ &data->key_mem, 1 }; if (!is_ascii_ctrl) { - data->modifiers |= MOD_MASK_SHIFT; + data->mods |= MOD_MASK_SHIFT; } - data->key_orig = (String){ &string[idx], (size_t)len }; + data->key_alt = (String){ &string[idx], (size_t)len }; } else { - data->key_orig = (String){ NULL, 0 }; + data->key_alt = (String){ NULL, 0 }; data->key = (String){ &string[idx], (size_t)len }; } } @@ -347,7 +347,7 @@ char *get_special_key_name(int c, int modifiers, struct keycode_data *data) } else if (len > 1) { if (data) { data->key = (String){ &string[idx], (size_t)len }; - data->key_orig = (String){ NULL, 0 }; + data->key_alt = (String){ NULL, 0 }; } idx += utf_char2bytes(c, string + idx); @@ -355,9 +355,9 @@ char *get_special_key_name(int c, int modifiers, struct keycode_data *data) char *s = transchar(c); if (data) { - data->_key_mem = (char)c; - data->key = (String){ &data->_key_mem, 1 }; - data->key_orig = (String){ &string[idx], strlen(s) }; + data->key_mem = (char)c; + data->key = (String){ &data->key_mem, 1 }; + data->key_alt = (String){ &string[idx], strlen(s) }; } while (*s) { @@ -370,12 +370,12 @@ char *get_special_key_name(int c, int modifiers, struct keycode_data *data) if (data) { if (0 <= c && c <= 0x7f) { - data->_key_mem = (char)c; - data->key = (String){ &data->_key_mem, 1 }; - data->key_orig = *s; + data->key_mem = (char)c; + data->key = (String){ &data->key_mem, 1 }; + data->key_alt = *s; } else { data->key = *s; - data->key_orig = (String){ NULL, 0 }; + data->key_alt = (String){ NULL, 0 }; } } @@ -390,6 +390,12 @@ char *get_special_key_name(int c, int modifiers, struct keycode_data *data) return string; } +/// @return String representing the key "chord" (key + modifiers). +char *get_special_key_name(int c, int modifiers) +{ + return get_special_key(c, modifiers, NULL); +} + /// Try translating a <> name ("keycode"). /// /// @param[in,out] srcp Source from which <> are translated. Is advanced to diff --git a/src/nvim/keycodes.h b/src/nvim/keycodes.h index ba847e3562..fca9e1b115 100644 --- a/src/nvim/keycodes.h +++ b/src/nvim/keycodes.h @@ -1,9 +1,8 @@ #pragma once -#include "nvim/api/private/defs.h" #include "nvim/ascii_defs.h" #include "nvim/eval/typval_defs.h" // IWYU pragma: keep -#include "nvim/message_defs.h" +#include "nvim/keycodes_defs.h" // IWYU pragma: keep // Keycode definitions for special keys. // diff --git a/src/nvim/keycodes_defs.h b/src/nvim/keycodes_defs.h new file mode 100644 index 0000000000..3176bdebf3 --- /dev/null +++ b/src/nvim/keycodes_defs.h @@ -0,0 +1,14 @@ +#pragma once + +#include "nvim/api/private/defs.h" // IWYU pragma: keep + +/// Structured info for a key chord. +/// +/// XXX: The `key`/`key_alt` strings may point into `key_mem` or into a static buffer owned by +/// str2special()/get_special_key(). All are only valid until the next such call. +struct keychord { + int mods; ///< Modifier mask (see `mod_mask_table`), e.g. `` yields Ctrl and Shift. + char key_mem; ///< Backing storage for a single-byte `key`/`key_alt` (see above). + String key; ///< Key without modifiers, normalized, e.g. `` => `a`, `` => `<`. + String key_alt; ///< Alternative spelling of `key`, e.g. `lt` for `<`. Empty (NULL) if same. +}; diff --git a/src/nvim/lua/stdlib.c b/src/nvim/lua/stdlib.c index 47a58f88a1..fe281c61d7 100644 --- a/src/nvim/lua/stdlib.c +++ b/src/nvim/lua/stdlib.c @@ -42,7 +42,7 @@ #include "nvim/mbyte_defs.h" #include "nvim/memline.h" #include "nvim/memory.h" -#include "nvim/message_defs.h" +#include "nvim/message.h" #include "nvim/pos_defs.h" #include "nvim/regexp.h" #include "nvim/regexp_defs.h" @@ -680,8 +680,9 @@ static int nlua_with(lua_State *L) return rets; } -// Parses internal representation of key into detailed dict about each key chord. -static int nlua_keyarg(lua_State *L) +/// Parses the internal representation of `keys` (as produced by nvim_replace_termcodes()) into +/// a structured list of dicts representing one or more key-chords. +static int nlua_keyparse(lua_State *L) { luaL_argcheck(L, lua_isstring(L, 1), 1, "string expected"); @@ -690,16 +691,17 @@ static int nlua_keyarg(lua_State *L) lua_newtable(L); - struct keycode_data data; + struct keychord data; while (*p != NUL) { - const char *key = str2special(&p, true, true, &data); + // kTrue (same flags as keytrans()) so `keys` == keytrans(vim.keycode(…)). + const char *keys = str2special(&p, true, kTrue, &data); lua_createtable(L, 0, 3); lua_newtable(L); for (int i = 0; mod_mask_table[i].name != 'A'; i++) { - if ((data.modifiers & mod_mask_table[i].mod_mask) == mod_mask_table[i].mod_flag) { + if ((data.mods & mod_mask_table[i].mod_mask) == mod_mask_table[i].mod_flag) { lua_pushlstring(L, &mod_mask_table[i].name, 1); lua_rawseti(L, -2, (int)lua_objlen(L, -2) + 1); } @@ -709,12 +711,12 @@ static int nlua_keyarg(lua_State *L) lua_pushlstring(L, data.key.data, data.key.size); lua_setfield(L, -2, "key"); - lua_pushstring(L, key); - lua_setfield(L, -2, "key_raw"); + lua_pushstring(L, keys); + lua_setfield(L, -2, "keys"); - if (data.key_orig.size != 0) { - lua_pushlstring(L, data.key_orig.data, data.key_orig.size); - lua_setfield(L, -2, "key_orig"); + if (data.key_alt.size != 0) { + lua_pushlstring(L, data.key_alt.data, data.key_alt.size); + lua_setfield(L, -2, "key_alt"); } lua_rawseti(L, -2, (int)lua_objlen(L, -2) + 1); @@ -743,8 +745,11 @@ static void nlua_state_add_internal(lua_State *const lstate) lua_pushcfunction(lstate, &nlua_with); lua_setfield(lstate, -2, "_with_c"); - lua_pushcfunction(lstate, &nlua_keyarg); - lua_setfield(lstate, -2, "_keyarg"); + // vim._core.keyparse + lua_getfield(lstate, -1, "_core"); + lua_pushcfunction(lstate, &nlua_keyparse); + lua_setfield(lstate, -2, "keyparse"); + lua_pop(lstate, 1); } void nlua_state_add_stdlib(lua_State *const lstate, bool is_thread) diff --git a/src/nvim/mapping.c b/src/nvim/mapping.c index c277d5a564..c878bc339e 100644 --- a/src/nvim/mapping.c +++ b/src/nvim/mapping.c @@ -1223,7 +1223,7 @@ static char *translate_mapping(const char *const str_in, const char *const cpo_v str += 2; } if (IS_SPECIAL(c) || modifiers) { // special key - ga_concat(&ga, get_special_key_name(c, modifiers, NULL)); + ga_concat(&ga, get_special_key_name(c, modifiers)); continue; // for (str) } } @@ -1948,7 +1948,7 @@ int put_escstr(FILE *fd, const char *strstart, int what) str += 2; } if (IS_SPECIAL(c) || modifiers) { // special key - if (fputs(get_special_key_name(c, modifiers, NULL), fd) < 0) { + if (fputs(get_special_key_name(c, modifiers), fd) < 0) { return FAIL; } continue; diff --git a/src/nvim/message.c b/src/nvim/message.c index faf2426ba8..f1fdb4b546 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -2072,13 +2072,14 @@ char *str2special_arena(const char *const str, const bool replace_spaces, /// lhs of mapping and keytrans(), but not rhs. /// @param[in] replace_others kTrue/kNone: Convert `<` into ``. /// kTrue: Convert `|` into ``, `\` into ``. +/// @param[out] data If non-NULL, gets structured info about the key chord. /// /// @return Converted key code, in a static buffer. Buffer is always one and the /// same, so save converted string somewhere before running str2special /// for the second time. /// On illegal byte return a string with only that byte. const char *str2special(const char **const sp, const bool replace_spaces, - const TriState replace_others, struct keycode_data *data) + const TriState replace_others, struct keychord *data) FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_RET { static char buf[7]; @@ -2089,9 +2090,9 @@ const char *str2special(const char **const sp, const bool replace_spaces, const char *const p = mb_unescape(sp); if (p != NULL) { if (data) { - data->modifiers = 0; + data->mods = 0; data->key = (String){ (char *)p, strlen(p) }; - data->key_orig = (String){ NULL, 0 }; + data->key_alt = (String){ NULL, 0 }; } return p; @@ -2140,19 +2141,19 @@ const char *str2special(const char **const sp, const bool replace_spaces, || (replace_spaces && c == ' ') || (replace_others != kFalse && c == '<') || (replace_others == kTrue && (c == '|' || c == '\\'))) { - return get_special_key_name(c, modifiers, data); + return get_special_key(c, modifiers, data); } if (data) { - data->modifiers = 0; + data->mods = 0; if ('A' <= c && c <= 'Z') { - data->_key_mem = (char)c + 32; - data->key = (String){ &data->_key_mem, 1 }; - data->modifiers |= MOD_MASK_SHIFT; - data->key_orig = (String){ buf, 1 }; + data->key_mem = (char)c + 32; + data->key = (String){ &data->key_mem, 1 }; + data->mods |= MOD_MASK_SHIFT; + data->key_alt = (String){ buf, 1 }; } else { data->key = (String){ buf, 1 }; - data->key_orig = (String){ NULL, 0 }; + data->key_alt = (String){ NULL, 0 }; } } diff --git a/src/nvim/message.h b/src/nvim/message.h index 7c0fab83ab..bcf07cfb5f 100644 --- a/src/nvim/message.h +++ b/src/nvim/message.h @@ -6,6 +6,7 @@ #include "nvim/ex_cmds_defs.h" // IWYU pragma: keep #include "nvim/grid_defs.h" +#include "nvim/keycodes_defs.h" // IWYU pragma: keep #include "nvim/macros_defs.h" #include "nvim/message_defs.h" // IWYU pragma: keep diff --git a/src/nvim/message_defs.h b/src/nvim/message_defs.h index 5e8aa2a76c..90b6756026 100644 --- a/src/nvim/message_defs.h +++ b/src/nvim/message_defs.h @@ -29,10 +29,3 @@ typedef struct msg_hist { bool append; ///< Message should be appended to previous entry, as opposed ///< to on a new line (|ui-messages|->msg_show->append). } MessageHistoryEntry; - -struct keycode_data { - int modifiers; ///< Bitmask of modifiers. - char _key_mem; ///< Storage for {key} to point to. - String key; ///< A "normalized" version of key. - String key_orig; ///< Alternative version of {key}. Empty string means unset. -}; diff --git a/src/nvim/option.c b/src/nvim/option.c index 646e5c0f77..b39136a0d8 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -4795,7 +4795,7 @@ static int put_set(FILE *fd, char *cmd, OptIndex opt_idx, void *varp) OptInt wc; if (wc_use_keyname(varp, &wc)) { // print 'wildchar' and 'wildcharm' as a key name - if (fputs(get_special_key_name((int)wc, 0, NULL), fd) < 0) { + if (fputs(get_special_key_name((int)wc, 0), fd) < 0) { return FAIL; } } else if (fprintf(fd, "%" PRId64, value_num) < 0) { @@ -6372,7 +6372,7 @@ static void option_value2string(vimoption_T *opt, int opt_flags) OptInt wc = 0; if (wc_use_keyname(varp, &wc)) { - xstrlcpy(NameBuff, get_special_key_name((int)wc, 0, NULL), sizeof(NameBuff)); + xstrlcpy(NameBuff, get_special_key_name((int)wc, 0), sizeof(NameBuff)); } else if (wc != 0) { xstrlcpy(NameBuff, transchar((int)wc), sizeof(NameBuff)); } else { diff --git a/src/nvim/state.c b/src/nvim/state.c index 10b2de33a1..c08cf9e31b 100644 --- a/src/nvim/state.c +++ b/src/nvim/state.c @@ -93,7 +93,7 @@ getkey: } #ifdef NVIM_LOG_DEBUG - char *keyname = key == K_EVENT ? "K_EVENT" : get_special_key_name(key, mod_mask, NULL); + char *keyname = key == K_EVENT ? "K_EVENT" : get_special_key_name(key, mod_mask); DLOG("input: %s", keyname); #endif diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index a84de55a23..e17335f56a 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -2890,55 +2890,6 @@ describe('API', function() end) end) - describe('nvim_replace_termcodes', function() - it('escapes K_SPECIAL as K_SPECIAL KS_SPECIAL KE_FILLER', function() - eq('\128\254X', n.api.nvim_replace_termcodes('\128', true, true, true)) - end) - - it('leaves non-K_SPECIAL string unchanged', function() - eq('abc', n.api.nvim_replace_termcodes('abc', true, true, true)) - end) - - it('converts ', function() - eq('\\', n.api.nvim_replace_termcodes('', true, true, true)) - end) - - it('converts to K_SPECIAL KS_EXTRA KE_LEFTMOUSE', function() - -- K_SPECIAL KS_EXTRA KE_LEFTMOUSE - -- 0x80 0xfd 0x2c - -- 128 253 44 - eq('\128\253\44', n.api.nvim_replace_termcodes('', true, true, true)) - end) - - it('converts keycodes', function() - eq('\nx\27x\rxxxxx', true, true, true)) - end) - - it('does not convert keycodes if special=false', function() - eq( - 'xxxx', - n.api.nvim_replace_termcodes('xxxx', true, true, false) - ) - end) - - it('does not crash when transforming an empty string', function() - -- Actually does not test anything, because current code will use NULL for - -- an empty string. - -- - -- Problem here is that if String argument has .data in allocated memory - -- then `return str` in vim_replace_termcodes body will make Neovim free - -- `str.data` twice: once when freeing arguments, then when freeing return - -- value. - eq('', api.nvim_replace_termcodes('', true, true, true)) - end) - - -- Not exactly the case, as nvim_replace_termcodes() escapes K_SPECIAL in Unicode - it('translates the result of keytrans() on string with 0x80 byte back', function() - local s = 'ff\128\253\097tt' - eq(s, api.nvim_replace_termcodes(fn.keytrans(s), true, true, true)) - end) - end) - describe('nvim_feedkeys', function() it('K_SPECIAL escaping', function() local function on_setup() diff --git a/test/functional/core/keycode_spec.lua b/test/functional/core/keycode_spec.lua new file mode 100644 index 0000000000..35516ed9ab --- /dev/null +++ b/test/functional/core/keycode_spec.lua @@ -0,0 +1,142 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +local eq = t.eq +local exec_lua = n.exec_lua +local clear = n.clear + +before_each(clear) + +describe('vim.keycode()', function() + local function parse(key, simplify) + local parsed = exec_lua([[return select(2,vim.keycode(...,true))]], key) + return vim.tbl_map(function(tbl) + if simplify == 1 then + return { tbl.mod, tbl.key } + elseif simplify == 2 then + return { tbl.mod, tbl.key, tbl.keys } + end + return tbl + end, parsed) + end + + it('gets structured keychord info', function() + eq({ { mod = { 'M' }, key = 'f', keys = '' } }, parse('')) + eq({ { { 'M' }, 'Home' } }, parse('', 1)) + eq({ { { 'M' }, 'f' }, { {}, 'b' }, { {}, 'c' } }, parse('b', 1)) + + eq({ { { 'M' }, '>' } }, parse('>', 1)) + eq({ { { 'M' }, 't_>>' } }, parse('>>', 1)) + + -- Multiple modifiers, normalized to canonical order (mod_mask_table): M, T, C, S, … + eq({ { mod = { 'C', 'S' }, key = 'Home', keys = '' } }, parse('')) + eq({ { { 'M', 'C' }, 'x', '' } }, parse('', 2)) + eq({ { { 'M', 'C', 'S' }, 'F1', '' } }, parse('', 2)) + end) + + it('normalizes', function() + -- stylua: ignore start + eq({ { {}, '\\', '' } }, parse('\\', 2)) + eq({ { { 'M' }, '\\', '' } }, parse('', 2)) + eq({ { { 'C' }, '\\', '' } }, parse('', 2)) + eq({ { {}, '|', '' } }, parse('|', 2)) + eq({ { { 'M' }, '|', '' } }, parse('', 2)) + eq({ { { 'C' }, '|', '' } }, parse('', 2)) + eq({ { {}, '\127', '\127' } }, parse('\127', 2)) + eq({ { { 'M' }, '\127', '' } }, parse('', 2)) + eq({ { { 'C' }, '\127', '' } }, parse('', 2)) + + eq({ { {}, '<', '' } }, parse('<', 2)) + eq({ { {}, '\t', '' } }, parse('\t', 2)) + eq({ { {}, '\r', '' } }, parse('\r', 2)) + eq({ { {}, '\n', '' } }, parse('\n', 2)) + eq({ { {}, '\27', '' } }, parse('\27', 2)) + eq({ { {}, ' ', '' } }, parse(' ', 2)) + + eq({ { { 'S' }, 'a', 'A' } }, parse('A', 2)) + eq({ { { 'C' }, 'a', '' } }, parse('', 2)) + -- stylua: ignore end + + eq({ { mod = {}, key = '<', keys = '', key_alt = 'lt' } }, parse('<')) + end) + + it('utf8', function() + eq({ { {}, 'ö' }, { { 'M' }, 'ó' }, { {}, 'ú' }, { {}, 'ü' } }, parse('öúü', 1)) + eq({ { {}, 'a' }, { {}, '\226\129\129' }, { {}, 'b' } }, parse('a\226\129\129b', 1)) + eq({ { {}, 'a' }, { {}, '\242\129\129\129' }, { {}, 'b' } }, parse('a\242\129\129\129b', 1)) + end) + + -- Canonical notation: concatenating each chord's `keys` must equal keytrans(vim.keycode(…)). + it('keys concatenate to keytrans()', function() + for _, s in ipairs({ + '|', + '\\', + '<', + '', + '', + 'A', + '', + 'ab', + ' \t', + 'ö', + }) do + local concat, keytrans = exec_lua(function(k) + local enc, chords = vim.keycode(k, true) + local parts = vim.tbl_map(function(c) + return c.keys + end, chords) + return table.concat(parts), vim.fn.keytrans(enc) + end, s) + eq(keytrans, concat) + end + end) +end) + +describe('nvim_replace_termcodes', function() + it('escapes K_SPECIAL as K_SPECIAL KS_SPECIAL KE_FILLER', function() + eq('\128\254X', n.api.nvim_replace_termcodes('\128', true, true, true)) + end) + + it('leaves non-K_SPECIAL string unchanged', function() + eq('abc', n.api.nvim_replace_termcodes('abc', true, true, true)) + end) + + it('converts ', function() + eq('\\', n.api.nvim_replace_termcodes('', true, true, true)) + end) + + it('converts to K_SPECIAL KS_EXTRA KE_LEFTMOUSE', function() + -- K_SPECIAL KS_EXTRA KE_LEFTMOUSE + -- 0x80 0xfd 0x2c + -- 128 253 44 + eq('\128\253\44', n.api.nvim_replace_termcodes('', true, true, true)) + end) + + it('converts keycodes', function() + eq('\nx\27x\rxxxxx', true, true, true)) + end) + + it('does not convert keycodes if special=false', function() + eq( + 'xxxx', + n.api.nvim_replace_termcodes('xxxx', true, true, false) + ) + end) + + it('does not crash when transforming an empty string', function() + -- Actually does not test anything, because current code will use NULL for + -- an empty string. + -- + -- Problem here is that if String argument has .data in allocated memory + -- then `return str` in vim_replace_termcodes body will make Neovim free + -- `str.data` twice: once when freeing arguments, then when freeing return + -- value. + eq('', n.api.nvim_replace_termcodes('', true, true, true)) + end) + + -- Not exactly the case, as nvim_replace_termcodes() escapes K_SPECIAL in Unicode + it('translates the result of keytrans() on string with 0x80 byte back', function() + local s = 'ff\128\253\097tt' + eq(s, n.api.nvim_replace_termcodes(n.fn.keytrans(s), true, true, true)) + end) +end) diff --git a/test/functional/editor/keycode_spec.lua b/test/functional/editor/keycode_spec.lua deleted file mode 100644 index 648e46bc3d..0000000000 --- a/test/functional/editor/keycode_spec.lua +++ /dev/null @@ -1,63 +0,0 @@ -local t = require('test.testutil') -local n = require('test.functional.testnvim')() - -local eq = t.eq -local exec_lua = n.exec_lua -local clear = n.clear - -describe('vim.keycode() detailed info', function() - before_each(clear) - - local function parse(key, simplify) - local parsed = exec_lua([[return select(2,vim.keycode(...,true))]], key) - return vim.tbl_map(function(tbl) - if simplify == 1 then - return { tbl.mod, tbl.key } - elseif simplify == 2 then - return { tbl.mod, tbl.key, tbl.key_raw } - end - return tbl - end, parsed) - end - - it('works', function() - eq({ { mod = { 'M' }, key = 'f', key_raw = '' } }, parse('')) - eq({ { { 'M' }, 'Home' } }, parse('', 1)) - eq({ { { 'M' }, 'f' }, { {}, 'b' }, { {}, 'c' } }, parse('b', 1)) - - eq({ { { 'M' }, '>' } }, parse('>', 1)) - eq({ { { 'M' }, 't_>>' } }, parse('>>', 1)) - end) - - it('normalizes', function() - -- stylua: ignore start - eq({ { {}, '\\', '' } }, parse('\\', 2)) - eq({ { { 'M' }, '\\', '' } }, parse('', 2)) - eq({ { { 'C' }, '\\', '' } }, parse('', 2)) - eq({ { {}, '|', '' } }, parse('|', 2)) - eq({ { { 'M' }, '|', '' } }, parse('', 2)) - eq({ { { 'C' }, '|', '' } }, parse('', 2)) - eq({ { {}, '\127', '\127' } }, parse('\127', 2)) - eq({ { { 'M' }, '\127', '' } }, parse('', 2)) - eq({ { { 'C' }, '\127', '' } }, parse('', 2)) - - eq({ { {}, '<', '' } }, parse('<', 2)) - eq({ { {}, '\t', '' } }, parse('\t', 2)) - eq({ { {}, '\r', '' } }, parse('\r', 2)) - eq({ { {}, '\n', '' } }, parse('\n', 2)) - eq({ { {}, '\27', '' } }, parse('\27', 2)) - eq({ { {}, ' ', '' } }, parse(' ', 2)) - - eq({ { { 'S' }, 'a', 'A' } }, parse('A', 2)) - eq({ { { 'C' }, 'a', '' } }, parse('', 2)) - -- stylua: ignore end - - eq({ { mod = {}, key = '<', key_raw = '', key_orig = 'lt' } }, parse('<')) - end) - - it('handles utf8-chars', function() - eq({ { {}, 'ö' }, { { 'M' }, 'ó' }, { {}, 'ú' }, { {}, 'ü' } }, parse('öúü', 1)) - eq({ { {}, 'a' }, { {}, '\226\129\129' }, { {}, 'b' } }, parse('a\226\129\129b', 1)) - eq({ { {}, 'a' }, { {}, '\242\129\129\129' }, { {}, 'b' } }, parse('a\242\129\129\129b', 1)) - end) -end)