diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 893a810f3b..b07a4987e0 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1271,7 +1271,7 @@ vim.inspect() *vim.inspect()* • https://github.com/kikito/inspect.lua • https://github.com/mpeterv/vinspect -vim.keycode({str}) *vim.keycode()* +vim.keycode({str}, {info}) *vim.keycode()* Translates keycodes. Example: >lua @@ -1280,10 +1280,20 @@ vim.keycode({str}) *vim.keycode()* < Parameters: ~ - • {str} (`string`) String to be converted. + • {str} (`string`) String to be converted. + • {info} (`boolean?`) Also returns a normalized detailed info about the + key as a second return value. - Return: ~ + 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. + • {mod} (`('M'|'T'|'C'|'S'|'2'|'3'|'4'|'D')[]`) A list of single + character modifiers of the key. See also: ~ • |nvim_replace_termcodes()| diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index a7aa26f103..129c653e46 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -313,6 +313,8 @@ 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 3dfe2a192c..87272e3d10 100644 --- a/runtime/lua/vim/_core/editor.lua +++ b/runtime/lua/vim/_core/editor.lua @@ -1260,6 +1260,23 @@ function vim.print(...) return vim._print(false, ...) end +--- @class vim.keycode.info +--- @inlinedoc +--- +--- The key chord normalized without modifiers. +--- @field key string +--- +--- Non-normalized version of key chord, +--- only present when it differs from {key}. +--- Example: `lt` and `<`. +--- @field key_orig string? +--- +--- The original key chord with modifiers. +--- @field key_raw string +--- +--- A list of single character modifiers of the key. +--- @field mod ('M'|'T'|'C'|'S'|'2'|'3'|'4'|'D')[] + --- Translates keycodes. --- --- Example: @@ -1269,11 +1286,21 @@ end --- vim.g.mapleader = k'' --- ``` --- +--- --- @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: --- @see |nvim_replace_termcodes()| -function vim.keycode(str) - return vim.api.nvim_replace_termcodes(str, true, true, true) +function vim.keycode(str, info) + if info then + local keycode = vim.keycode(str) + return keycode, vim._keyarg(keycode) + else + return vim.api.nvim_replace_termcodes(str, true, true, true) + end end --- @param server_addr string diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index bbb3ef09d5..3052401b02 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), -1, true); + put_on_cmdline(get_special_key_name(s->c, mod_mask, NULL), -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 9f69c2f59e..101a186073 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); + char *p = get_special_key_name(c, mod_mask, NULL); 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 9827604d67..e0eb352128 100644 --- a/src/nvim/keycodes.c +++ b/src/nvim/keycodes.c @@ -28,12 +28,7 @@ #include "keycodes.c.generated.h" // Some useful tables. - -static const struct modmasktable { - uint16_t mod_mask; ///< Bit-mask for particular key modifier. - uint16_t mod_flag; ///< Bit(s) for particular key modifier. - char name; ///< Single letter name of modifier. -} mod_mask_table[] = { +const struct modmasktable mod_mask_table[] = { { MOD_MASK_ALT, MOD_MASK_ALT, 'M' }, { MOD_MASK_META, MOD_MASK_META, 'T' }, { MOD_MASK_CTRL, MOD_MASK_CTRL, 'C' }, @@ -260,9 +255,10 @@ int handle_x_keys(const int 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) +char *get_special_key_name(int c, int modifiers, struct keycode_data *data) { static char string[MAX_KEY_NAME_LEN + 1]; + bool is_ascii_ctrl = false; string[0] = '<'; int idx = 1; @@ -293,31 +289,38 @@ char *get_special_key_name(int c, int modifiers) // extract modifiers. if (c > 0 && utf_char2len(c) == 1) { - if (table_idx < 0 - && (!vim_isprintc(c) || (c & 0x7f) == ' ') - && (c & 0x80)) { - c &= 0x7f; - modifiers |= MOD_MASK_ALT; - // try again, to find the un-alted key in the special key table - table_idx = find_special_key_in_table(c); - } if (table_idx < 0 && !vim_isprintc(c) && c < ' ') { c += '@'; modifiers |= MOD_MASK_CTRL; + + is_ascii_ctrl = true; } } + if (data) { + data->modifiers = 0; + } + // translate the modifier into a string for (int i = 0; mod_mask_table[i].name != 'A'; i++) { if ((modifiers & mod_mask_table[i].mod_mask) == mod_mask_table[i].mod_flag) { string[idx++] = mod_mask_table[i].name; string[idx++] = '-'; + + if (data) { + data->modifiers |= mod_mask_table[i].mod_flag; + } } } 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 = (String){ &string[idx], 4 }; + } + string[idx++] = 't'; string[idx++] = '_'; string[idx++] = (char)(uint8_t)KEY2TERMCAP0(c); @@ -326,11 +329,37 @@ char *get_special_key_name(int c, int modifiers) // Not a special key, only modifiers, output directly. int len = utf_char2len(c); 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 }; + if (!is_ascii_ctrl) { + data->modifiers |= MOD_MASK_SHIFT; + } + data->key_orig = (String){ &string[idx], (size_t)len }; + } else { + data->key_orig = (String){ NULL, 0 }; + data->key = (String){ &string[idx], (size_t)len }; + } + } + string[idx++] = (char)(uint8_t)c; } else if (len > 1) { + if (data) { + data->key = (String){ &string[idx], (size_t)len }; + data->key_orig = (String){ NULL, 0 }; + } + idx += utf_char2bytes(c, string + idx); } else { 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) }; + } + while (*s) { string[idx++] = *s++; } @@ -338,6 +367,18 @@ char *get_special_key_name(int c, int modifiers) } } else { // use name of special key const String *s = &key_names_table[table_idx].name; + + if (data) { + if (0 <= c && c <= 0x7f) { + data->_key_mem = (char)c; + data->key = (String){ &data->_key_mem, 1 }; + data->key_orig = *s; + } else { + data->key = *s; + data->key_orig = (String){ NULL, 0 }; + } + } + if ((int)s->size + idx + 2 <= MAX_KEY_NAME_LEN) { STRCPY(string + idx, s->data); idx += (int)s->size; diff --git a/src/nvim/keycodes.h b/src/nvim/keycodes.h index 2fa0226c7e..ba847e3562 100644 --- a/src/nvim/keycodes.h +++ b/src/nvim/keycodes.h @@ -1,7 +1,9 @@ #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" // Keycode definitions for special keys. // @@ -504,4 +506,10 @@ enum { FSK_SIMPLIFY = 0x08, ///< simplify , etc. }; +extern const struct modmasktable { + uint16_t mod_mask; ///< Bit-mask for particular key modifier. + uint16_t mod_flag; ///< Bit(s) for particular key modifier. + char name; ///< Single letter name of modifier. +} mod_mask_table[]; + #include "keycodes.h.generated.h" diff --git a/src/nvim/lua/stdlib.c b/src/nvim/lua/stdlib.c index 4efcb3228d..47a58f88a1 100644 --- a/src/nvim/lua/stdlib.c +++ b/src/nvim/lua/stdlib.c @@ -31,6 +31,7 @@ #include "nvim/ex_eval.h" #include "nvim/fold.h" #include "nvim/globals.h" +#include "nvim/keycodes.h" #include "nvim/lua/base64.h" #include "nvim/lua/converter.h" #include "nvim/lua/spell.h" @@ -41,6 +42,7 @@ #include "nvim/mbyte_defs.h" #include "nvim/memline.h" #include "nvim/memory.h" +#include "nvim/message_defs.h" #include "nvim/pos_defs.h" #include "nvim/regexp.h" #include "nvim/regexp_defs.h" @@ -678,6 +680,51 @@ 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) +{ + luaL_argcheck(L, lua_isstring(L, 1), 1, "string expected"); + + char *escaped = vim_strsave_escape_ks((char *)lua_tostring(L, 1)); + const char *p = escaped; + + lua_newtable(L); + + struct keycode_data data; + + while (*p != NUL) { + const char *key = str2special(&p, true, true, &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) { + lua_pushlstring(L, &mod_mask_table[i].name, 1); + lua_rawseti(L, -2, (int)lua_objlen(L, -2) + 1); + } + } + lua_setfield(L, -2, "mod"); + + lua_pushlstring(L, data.key.data, data.key.size); + lua_setfield(L, -2, "key"); + + lua_pushstring(L, key); + lua_setfield(L, -2, "key_raw"); + + if (data.key_orig.size != 0) { + lua_pushlstring(L, data.key_orig.data, data.key_orig.size); + lua_setfield(L, -2, "key_orig"); + } + + lua_rawseti(L, -2, (int)lua_objlen(L, -2) + 1); + } + + xfree(escaped); + + return 1; +} + // Access to internal functions. For use in runtime/ static void nlua_state_add_internal(lua_State *const lstate) { @@ -695,6 +742,9 @@ 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"); } 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 c878bc339e..c277d5a564 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)); + ga_concat(&ga, get_special_key_name(c, modifiers, NULL)); 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), fd) < 0) { + if (fputs(get_special_key_name(c, modifiers, NULL), fd) < 0) { return FAIL; } continue; diff --git a/src/nvim/message.c b/src/nvim/message.c index 8bc448a331..faf2426ba8 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -1986,7 +1986,7 @@ int msg_outtrans_special(const char *strstart, bool from, int maxlen) text = ""; str++; } else { - text = str2special(&str, from, false); + text = str2special(&str, from, false, NULL); } if (text[0] != NUL && text[1] == NUL) { // single-byte character or illegal byte @@ -2024,7 +2024,7 @@ char *str2special_save(const char *const str, const bool replace_spaces, const char *p = str; while (*p != NUL) { - ga_concat(&ga, str2special(&p, replace_spaces, replace_others)); + ga_concat(&ga, str2special(&p, replace_spaces, replace_others, NULL)); } ga_append(&ga, NUL); return (char *)ga.ga_data; @@ -2049,14 +2049,14 @@ char *str2special_arena(const char *const str, const bool replace_spaces, const char *p = str; size_t len = 0; while (*p) { - len += strlen(str2special(&p, replace_spaces, replace_others)); + len += strlen(str2special(&p, replace_spaces, replace_others, NULL)); } char *buf = arena_alloc(arena, len + 1, false); size_t pos = 0; p = str; while (*p) { - const char *s = str2special(&p, replace_spaces, replace_others); + const char *s = str2special(&p, replace_spaces, replace_others, NULL); size_t s_len = strlen(s); memcpy(buf + pos, s, s_len); pos += s_len; @@ -2078,8 +2078,8 @@ char *str2special_arena(const char *const str, const bool replace_spaces, /// 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) - FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_RET + const TriState replace_others, struct keycode_data *data) + FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_RET { static char buf[7]; @@ -2088,6 +2088,12 @@ const char *str2special(const char **const sp, const bool replace_spaces, // string if it is a multi-byte character. const char *const p = mb_unescape(sp); if (p != NULL) { + if (data) { + data->modifiers = 0; + data->key = (String){ (char *)p, strlen(p) }; + data->key_orig = (String){ NULL, 0 }; + } + return p; } } @@ -2134,8 +2140,22 @@ 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); + return get_special_key_name(c, modifiers, data); } + + if (data) { + data->modifiers = 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 }; + } else { + data->key = (String){ buf, 1 }; + data->key_orig = (String){ NULL, 0 }; + } + } + buf[0] = (char)c; buf[1] = NUL; return buf; @@ -2150,7 +2170,7 @@ void str2specialbuf(const char *sp, char *buf, size_t len) FUNC_ATTR_NONNULL_ALL { while (*sp) { - const char *s = str2special(&sp, false, false); + const char *s = str2special(&sp, false, false, NULL); const size_t s_len = strlen(s); if (len <= s_len) { break; diff --git a/src/nvim/message_defs.h b/src/nvim/message_defs.h index 90b6756026..5e8aa2a76c 100644 --- a/src/nvim/message_defs.h +++ b/src/nvim/message_defs.h @@ -29,3 +29,10 @@ 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 b39136a0d8..646e5c0f77 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), fd) < 0) { + if (fputs(get_special_key_name((int)wc, 0, NULL), 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), sizeof(NameBuff)); + xstrlcpy(NameBuff, get_special_key_name((int)wc, 0, NULL), 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 c08cf9e31b..10b2de33a1 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); + char *keyname = key == K_EVENT ? "K_EVENT" : get_special_key_name(key, mod_mask, NULL); DLOG("input: %s", keyname); #endif diff --git a/test/functional/editor/keycode_spec.lua b/test/functional/editor/keycode_spec.lua new file mode 100644 index 0000000000..648e46bc3d --- /dev/null +++ b/test/functional/editor/keycode_spec.lua @@ -0,0 +1,63 @@ +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)