fix(lua): vim.keycode cleanup #40817

This commit is contained in:
Justin M. Keyes
2026-07-18 14:31:36 -04:00
committed by GitHub
parent a2dfa195b2
commit 6f5fae3f8c
19 changed files with 279 additions and 203 deletions

View File

@@ -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'<bs>'
-- Split a key sequence into chords, e.g. to inspect modifiers.
local _, chords = vim.keycode('<C-w>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('<C-a>')) == '<C-A>')
<
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: `<C-A>` 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: `<A-f>` has
`keys="<M-f>"`.
• {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,

View File

@@ -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

View File

@@ -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: `<C-A>` 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: `<A-f>` has `keys="<M-f>"`.
--- @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'<bs>'
---
--- -- Split a key sequence into chords, e.g. to inspect modifiers.
--- local _, chords = vim.keycode('<C-w>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('<C-a>')) == '<C-A>')
--- ```
---
---
--- @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

View File

@@ -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.
---

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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.
//

14
src/nvim/keycodes_defs.h Normal file
View File

@@ -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. `<C-A>` 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. `<C-A>` => `a`, `<lt>` => `<`.
String key_alt; ///< Alternative spelling of `key`, e.g. `lt` for `<`. Empty (NULL) if same.
};

View File

@@ -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)

View File

@@ -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;

View File

@@ -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 `<lt>`.
/// kTrue: Convert `|` into `<Bar>`, `\` into `<Bslash>`.
/// @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 };
}
}

View File

@@ -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

View File

@@ -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.
};

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 <expressions>', function()
eq('\\', n.api.nvim_replace_termcodes('<Leader>', true, true, true))
end)
it('converts <LeftMouse> 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('<LeftMouse>', true, true, true))
end)
it('converts keycodes', function()
eq('\nx\27x\rx<x', n.api.nvim_replace_termcodes('<NL>x<Esc>x<CR>x<lt>x', true, true, true))
end)
it('does not convert keycodes if special=false', function()
eq(
'<NL>x<Esc>x<CR>x<lt>x',
n.api.nvim_replace_termcodes('<NL>x<Esc>x<CR>x<lt>x', 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()

View File

@@ -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 = '<M-f>' } }, parse('<A-f>'))
eq({ { { 'M' }, 'Home' } }, parse('<A-Home>', 1))
eq({ { { 'M' }, 'f' }, { {}, 'b' }, { {}, 'c' } }, parse('<A-f>b<char-99>', 1))
eq({ { { 'M' }, '>' } }, parse('<A->>', 1))
eq({ { { 'M' }, 't_>>' } }, parse('<A-t_>>>', 1))
-- Multiple modifiers, normalized to canonical order (mod_mask_table): M, T, C, S, …
eq({ { mod = { 'C', 'S' }, key = 'Home', keys = '<C-S-Home>' } }, parse('<C-S-Home>'))
eq({ { { 'M', 'C' }, 'x', '<M-C-X>' } }, parse('<M-C-x>', 2))
eq({ { { 'M', 'C', 'S' }, 'F1', '<M-C-S-F1>' } }, parse('<C-A-S-F1>', 2))
end)
it('normalizes', function()
-- stylua: ignore start
eq({ { {}, '\\', '<Bslash>' } }, parse('\\', 2))
eq({ { { 'M' }, '\\', '<M-Bslash>' } }, parse('<A-\\>', 2))
eq({ { { 'C' }, '\\', '<C-\\>' } }, parse('<C-\\>', 2))
eq({ { {}, '|', '<Bar>' } }, parse('|', 2))
eq({ { { 'M' }, '|', '<M-Bar>' } }, parse('<A-|>', 2))
eq({ { { 'C' }, '|', '<C-Bar>' } }, parse('<C-|>', 2))
eq({ { {}, '\127', '\127' } }, parse('\127', 2))
eq({ { { 'M' }, '\127', '<M-^?>' } }, parse('<A-\127>', 2))
eq({ { { 'C' }, '\127', '<C-^?>' } }, parse('<C-\127>', 2))
eq({ { {}, '<', '<lt>' } }, parse('<', 2))
eq({ { {}, '\t', '<Tab>' } }, parse('\t', 2))
eq({ { {}, '\r', '<CR>' } }, parse('\r', 2))
eq({ { {}, '\n', '<NL>' } }, parse('\n', 2))
eq({ { {}, '\27', '<Esc>' } }, parse('\27', 2))
eq({ { {}, ' ', '<Space>' } }, parse(' ', 2))
eq({ { { 'S' }, 'a', 'A' } }, parse('A', 2))
eq({ { { 'C' }, 'a', '<C-A>' } }, parse('<C-a>', 2))
-- stylua: ignore end
eq({ { mod = {}, key = '<', keys = '<lt>', key_alt = 'lt' } }, parse('<'))
end)
it('utf8', function()
eq({ { {}, 'ö' }, { { 'M' }, 'ó' }, { {}, 'ú' }, { {}, 'ü' } }, parse('ö<A-ó>úü', 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-|>',
'<C-\\>',
'A',
'<C-a>',
'<C-S-Home>ab',
' \t',
'ö<A-ó>',
}) 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 <expressions>', function()
eq('\\', n.api.nvim_replace_termcodes('<Leader>', true, true, true))
end)
it('converts <LeftMouse> 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('<LeftMouse>', true, true, true))
end)
it('converts keycodes', function()
eq('\nx\27x\rx<x', n.api.nvim_replace_termcodes('<NL>x<Esc>x<CR>x<lt>x', true, true, true))
end)
it('does not convert keycodes if special=false', function()
eq(
'<NL>x<Esc>x<CR>x<lt>x',
n.api.nvim_replace_termcodes('<NL>x<Esc>x<CR>x<lt>x', 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)

View File

@@ -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 = '<M-f>' } }, parse('<A-f>'))
eq({ { { 'M' }, 'Home' } }, parse('<A-Home>', 1))
eq({ { { 'M' }, 'f' }, { {}, 'b' }, { {}, 'c' } }, parse('<A-f>b<char-99>', 1))
eq({ { { 'M' }, '>' } }, parse('<A->>', 1))
eq({ { { 'M' }, 't_>>' } }, parse('<A-t_>>>', 1))
end)
it('normalizes', function()
-- stylua: ignore start
eq({ { {}, '\\', '<Bslash>' } }, parse('\\', 2))
eq({ { { 'M' }, '\\', '<M-Bslash>' } }, parse('<A-\\>', 2))
eq({ { { 'C' }, '\\', '<C-\\>' } }, parse('<C-\\>', 2))
eq({ { {}, '|', '<Bar>' } }, parse('|', 2))
eq({ { { 'M' }, '|', '<M-Bar>' } }, parse('<A-|>', 2))
eq({ { { 'C' }, '|', '<C-Bar>' } }, parse('<C-|>', 2))
eq({ { {}, '\127', '\127' } }, parse('\127', 2))
eq({ { { 'M' }, '\127', '<M-^?>' } }, parse('<A-\127>', 2))
eq({ { { 'C' }, '\127', '<C-^?>' } }, parse('<C-\127>', 2))
eq({ { {}, '<', '<lt>' } }, parse('<', 2))
eq({ { {}, '\t', '<Tab>' } }, parse('\t', 2))
eq({ { {}, '\r', '<CR>' } }, parse('\r', 2))
eq({ { {}, '\n', '<NL>' } }, parse('\n', 2))
eq({ { {}, '\27', '<Esc>' } }, parse('\27', 2))
eq({ { {}, ' ', '<Space>' } }, parse(' ', 2))
eq({ { { 'S' }, 'a', 'A' } }, parse('A', 2))
eq({ { { 'C' }, 'a', '<C-A>' } }, parse('<C-a>', 2))
-- stylua: ignore end
eq({ { mod = {}, key = '<', key_raw = '<lt>', key_orig = 'lt' } }, parse('<'))
end)
it('handles utf8-chars', function()
eq({ { {}, 'ö' }, { { 'M' }, 'ó' }, { {}, 'ú' }, { {}, 'ü' } }, parse('ö<A-ó>úü', 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)