diff options
-rw-r--r-- | runtime/doc/builtin.txt | 49 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/vimfn.lua | 53 | ||||
-rw-r--r-- | src/nvim/eval.lua | 51 | ||||
-rw-r--r-- | src/nvim/eval/typval.c | 12 | ||||
-rw-r--r-- | src/nvim/getchar.c | 63 | ||||
-rw-r--r-- | test/functional/terminal/buffer_spec.lua | 92 | ||||
-rw-r--r-- | test/old/testdir/test_functions.vim | 71 |
7 files changed, 233 insertions, 158 deletions
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index d4a6bbf9d8..3d9010aa2c 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -3079,14 +3079,16 @@ getchangelist([{buf}]) *getchangelist()* Return: ~ (`table[]`) -getchar([{expr}]) *getchar()* +getchar([{expr} [, {opts}]]) *getchar()* Get a single character from the user or input stream. - If {expr} is omitted, wait until a character is available. + If {expr} is omitted or is -1, wait until a character is + available. If {expr} is 0, only get a character when one is available. Return zero otherwise. If {expr} is 1, only check if a character is available, it is not consumed. Return zero if no character available. - If you prefer always getting a string use |getcharstr()|. + If you prefer always getting a string use |getcharstr()|, or + specify |FALSE| as "number" in {opts}. Without {expr} and when {expr} is 0 a whole character or special key is returned. If it is a single character, the @@ -3096,7 +3098,8 @@ getchar([{expr}]) *getchar()* starting with 0x80 (decimal: 128). This is the same value as the String "\<Key>", e.g., "\<Left>". The returned value is also a String when a modifier (shift, control, alt) was used - that is not included in the character. + that is not included in the character. |keytrans()| can also + be used to convert a returned String into a readable form. When {expr} is 0 and Esc is typed, there will be a short delay while Vim waits to see if this is the start of an escape @@ -3108,6 +3111,24 @@ getchar([{expr}]) *getchar()* Use getcharmod() to obtain any additional modifiers. + The optional argument {opts} is a Dict and supports the + following items: + + number If |TRUE|, return a Number when getting + a single character. + If |FALSE|, the return value is always + converted to a String, and an empty + String (instead of 0) is returned when + no character is available. + (default: |TRUE|) + + simplify If |TRUE|, include modifiers in the + character if possible. E.g., return + the same value for CTRL-I and <Tab>. + If |FALSE|, don't include modifiers in + the character. + (default: |TRUE|) + When the user clicks a mouse button, the mouse event will be returned. The position can then be found in |v:mouse_col|, |v:mouse_lnum|, |v:mouse_winid| and |v:mouse_win|. @@ -3145,10 +3166,11 @@ getchar([{expr}]) *getchar()* < Parameters: ~ - • {expr} (`0|1?`) + • {expr} (`-1|0|1?`) + • {opts} (`table?`) Return: ~ - (`integer`) + (`integer|string`) getcharmod() *getcharmod()* The result is a Number which is the state of the modifiers for @@ -3213,19 +3235,12 @@ getcharsearch() *getcharsearch()* (`table`) getcharstr([{expr}]) *getcharstr()* - Get a single character from the user or input stream as a - string. - If {expr} is omitted, wait until a character is available. - If {expr} is 0 or false, only get a character when one is - available. Return an empty string otherwise. - If {expr} is 1 or true, only check if a character is - available, it is not consumed. Return an empty string - if no character is available. - Otherwise this works like |getchar()|, except that a number - result is converted to a string. + The same as |getchar()|, except that this always returns a + String, and "number" isn't allowed in {opts}. Parameters: ~ - • {expr} (`0|1?`) + • {expr} (`-1|0|1?`) + • {opts} (`table?`) Return: ~ (`string`) diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 4b5b276023..c0be6b089c 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -2748,12 +2748,14 @@ function vim.fn.getcellwidths() end function vim.fn.getchangelist(buf) end --- Get a single character from the user or input stream. ---- If {expr} is omitted, wait until a character is available. +--- If {expr} is omitted or is -1, wait until a character is +--- available. --- If {expr} is 0, only get a character when one is available. --- Return zero otherwise. --- If {expr} is 1, only check if a character is available, it is --- not consumed. Return zero if no character available. ---- If you prefer always getting a string use |getcharstr()|. +--- If you prefer always getting a string use |getcharstr()|, or +--- specify |FALSE| as "number" in {opts}. --- --- Without {expr} and when {expr} is 0 a whole character or --- special key is returned. If it is a single character, the @@ -2763,7 +2765,8 @@ function vim.fn.getchangelist(buf) end --- starting with 0x80 (decimal: 128). This is the same value as --- the String "\<Key>", e.g., "\<Left>". The returned value is --- also a String when a modifier (shift, control, alt) was used ---- that is not included in the character. +--- that is not included in the character. |keytrans()| can also +--- be used to convert a returned String into a readable form. --- --- When {expr} is 0 and Esc is typed, there will be a short delay --- while Vim waits to see if this is the start of an escape @@ -2775,6 +2778,24 @@ function vim.fn.getchangelist(buf) end --- --- Use getcharmod() to obtain any additional modifiers. --- +--- The optional argument {opts} is a Dict and supports the +--- following items: +--- +--- number If |TRUE|, return a Number when getting +--- a single character. +--- If |FALSE|, the return value is always +--- converted to a String, and an empty +--- String (instead of 0) is returned when +--- no character is available. +--- (default: |TRUE|) +--- +--- simplify If |TRUE|, include modifiers in the +--- character if possible. E.g., return +--- the same value for CTRL-I and <Tab>. +--- If |FALSE|, don't include modifiers in +--- the character. +--- (default: |TRUE|) +--- --- When the user clicks a mouse button, the mouse event will be --- returned. The position can then be found in |v:mouse_col|, --- |v:mouse_lnum|, |v:mouse_winid| and |v:mouse_win|. @@ -2811,9 +2832,10 @@ function vim.fn.getchangelist(buf) end --- endfunction --- < --- ---- @param expr? 0|1 ---- @return integer -function vim.fn.getchar(expr) end +--- @param expr? -1|0|1 +--- @param opts? table +--- @return integer|string +function vim.fn.getchar(expr, opts) end --- The result is a Number which is the state of the modifiers for --- the last obtained character with getchar() or in another way. @@ -2872,20 +2894,13 @@ function vim.fn.getcharpos(expr) end --- @return table function vim.fn.getcharsearch() end ---- Get a single character from the user or input stream as a ---- string. ---- If {expr} is omitted, wait until a character is available. ---- If {expr} is 0 or false, only get a character when one is ---- available. Return an empty string otherwise. ---- If {expr} is 1 or true, only check if a character is ---- available, it is not consumed. Return an empty string ---- if no character is available. ---- Otherwise this works like |getchar()|, except that a number ---- result is converted to a string. ---- ---- @param expr? 0|1 +--- The same as |getchar()|, except that this always returns a +--- String, and "number" isn't allowed in {opts}. +--- +--- @param expr? -1|0|1 +--- @param opts? table --- @return string -function vim.fn.getcharstr(expr) end +function vim.fn.getcharstr(expr, opts) end --- Return completion pattern of the current command-line. --- Only works when the command line is being edited, thus diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index ecb213c811..82e3992287 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -3471,15 +3471,17 @@ M.funcs = { signature = 'getchangelist([{buf}])', }, getchar = { - args = { 0, 1 }, + args = { 0, 2 }, desc = [=[ Get a single character from the user or input stream. - If {expr} is omitted, wait until a character is available. + If {expr} is omitted or is -1, wait until a character is + available. If {expr} is 0, only get a character when one is available. Return zero otherwise. If {expr} is 1, only check if a character is available, it is not consumed. Return zero if no character available. - If you prefer always getting a string use |getcharstr()|. + If you prefer always getting a string use |getcharstr()|, or + specify |FALSE| as "number" in {opts}. Without {expr} and when {expr} is 0 a whole character or special key is returned. If it is a single character, the @@ -3489,7 +3491,8 @@ M.funcs = { starting with 0x80 (decimal: 128). This is the same value as the String "\<Key>", e.g., "\<Left>". The returned value is also a String when a modifier (shift, control, alt) was used - that is not included in the character. + that is not included in the character. |keytrans()| can also + be used to convert a returned String into a readable form. When {expr} is 0 and Esc is typed, there will be a short delay while Vim waits to see if this is the start of an escape @@ -3501,6 +3504,24 @@ M.funcs = { Use getcharmod() to obtain any additional modifiers. + The optional argument {opts} is a Dict and supports the + following items: + + number If |TRUE|, return a Number when getting + a single character. + If |FALSE|, the return value is always + converted to a String, and an empty + String (instead of 0) is returned when + no character is available. + (default: |TRUE|) + + simplify If |TRUE|, include modifiers in the + character if possible. E.g., return + the same value for CTRL-I and <Tab>. + If |FALSE|, don't include modifiers in + the character. + (default: |TRUE|) + When the user clicks a mouse button, the mouse event will be returned. The position can then be found in |v:mouse_col|, |v:mouse_lnum|, |v:mouse_winid| and |v:mouse_win|. @@ -3538,9 +3559,9 @@ M.funcs = { < ]=], name = 'getchar', - params = { { 'expr', '0|1' } }, - returns = 'integer', - signature = 'getchar([{expr}])', + params = { { 'expr', '-1|0|1' }, { 'opts', 'table' } }, + returns = 'integer|string', + signature = 'getchar([{expr} [, {opts}]])', }, getcharmod = { desc = [=[ @@ -3613,21 +3634,13 @@ M.funcs = { signature = 'getcharsearch()', }, getcharstr = { - args = { 0, 1 }, + args = { 0, 2 }, desc = [=[ - Get a single character from the user or input stream as a - string. - If {expr} is omitted, wait until a character is available. - If {expr} is 0 or false, only get a character when one is - available. Return an empty string otherwise. - If {expr} is 1 or true, only check if a character is - available, it is not consumed. Return an empty string - if no character is available. - Otherwise this works like |getchar()|, except that a number - result is converted to a string. + The same as |getchar()|, except that this always returns a + String, and "number" isn't allowed in {opts}. ]=], name = 'getcharstr', - params = { { 'expr', '0|1' } }, + params = { { 'expr', '-1|0|1' }, { 'opts', 'table' } }, returns = 'string', signature = 'getcharstr([{expr}])', }, diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index ed1031577c..48b2e82c0a 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -2234,6 +2234,18 @@ dictitem_T *tv_dict_find(const dict_T *const d, const char *const key, const ptr return TV_DICT_HI2DI(hi); } +/// Check if a key is present in a dictionary. +/// +/// @param[in] d Dictionary to check. +/// @param[in] key Dictionary key. +/// +/// @return whether the key is present in the dictionary. +bool tv_dict_has_key(const dict_T *const d, const char *const key) + FUNC_ATTR_NONNULL_ARG(2) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT +{ + return tv_dict_find(d, key, -1) != NULL; +} + /// Get a typval item from a dictionary and copy it into "rettv". /// /// @param[in] d Dictionary to check. diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index 6ec84ff543..c9d2165fb0 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -1866,22 +1866,47 @@ bool char_avail(void) return retval != NUL; } +static int no_reduce_keys = 0; ///< Do not apply modifiers to the key. + /// "getchar()" and "getcharstr()" functions -static void getchar_common(typval_T *argvars, typval_T *rettv) +static void getchar_common(typval_T *argvars, typval_T *rettv, bool allow_number) FUNC_ATTR_NONNULL_ALL { varnumber_T n; bool error = false; + bool simplify = true; + + if (argvars[0].v_type != VAR_UNKNOWN + && tv_check_for_opt_dict_arg(argvars, 1) == FAIL) { + return; + } + + if (argvars[0].v_type != VAR_UNKNOWN && argvars[1].v_type == VAR_DICT) { + dict_T *d = argvars[1].vval.v_dict; + + if (allow_number) { + allow_number = tv_dict_get_bool(d, "number", true); + } else if (tv_dict_has_key(d, "number")) { + semsg(_(e_invarg2), "number"); + error = true; + } + + simplify = tv_dict_get_bool(d, "simplify", true); + } no_mapping++; allow_keys++; - while (true) { + if (!simplify) { + no_reduce_keys++; + } + while (!error) { if (msg_col > 0) { // Position the cursor. Needed after a message that ends in a space. ui_cursor_goto(msg_row, msg_col); } - if (argvars[0].v_type == VAR_UNKNOWN) { + if (argvars[0].v_type == VAR_UNKNOWN + || (argvars[0].v_type == VAR_NUMBER && argvars[0].vval.v_number == -1)) { // getchar(): blocking wait. // TODO(bfredl): deduplicate shared logic with state_enter ? if (!char_avail()) { @@ -1916,14 +1941,16 @@ static void getchar_common(typval_T *argvars, typval_T *rettv) } no_mapping--; allow_keys--; + if (!simplify) { + no_reduce_keys--; + } set_vim_var_nr(VV_MOUSE_WIN, 0); set_vim_var_nr(VV_MOUSE_WINID, 0); set_vim_var_nr(VV_MOUSE_LNUM, 0); set_vim_var_nr(VV_MOUSE_COL, 0); - rettv->vval.v_number = n; - if (n != 0 && (IS_SPECIAL(n) || mod_mask != 0)) { + if (n != 0 && (!allow_number || IS_SPECIAL(n) || mod_mask != 0)) { char temp[10]; // modifier: 3, mbyte-char: 6, NUL: 1 int i = 0; @@ -1970,35 +1997,23 @@ static void getchar_common(typval_T *argvars, typval_T *rettv) set_vim_var_nr(VV_MOUSE_COL, col + 1); } } + } else if (!allow_number) { + rettv->v_type = VAR_STRING; + } else { + rettv->vval.v_number = n; } } /// "getchar()" function void f_getchar(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { - getchar_common(argvars, rettv); + getchar_common(argvars, rettv, true); } /// "getcharstr()" function void f_getcharstr(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { - getchar_common(argvars, rettv); - - if (rettv->v_type != VAR_NUMBER) { - return; - } - - char temp[7]; // mbyte-char: 6, NUL: 1 - const varnumber_T n = rettv->vval.v_number; - int i = 0; - - if (n != 0) { - i += utf_char2bytes((int)n, temp); - } - assert(i < 7); - temp[i] = NUL; - rettv->v_type = VAR_STRING; - rettv->vval.v_string = xmemdupz(temp, (size_t)i); + getchar_common(argvars, rettv, false); } /// "getcharmod()" function @@ -2058,7 +2073,7 @@ static int check_simplify_modifier(int max_offset) { // We want full modifiers in Terminal mode so that the key can be correctly // encoded - if (State & MODE_TERMINAL) { + if ((State & MODE_TERMINAL) || no_reduce_keys > 0) { return 0; } diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index d36dc60a93..4635259e33 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -557,7 +557,7 @@ describe('terminal input', function() '--cmd', 'set notermguicolors', '-c', - 'while 1 | redraw | echo keytrans(getcharstr()) | endwhile', + 'while 1 | redraw | echo keytrans(getcharstr(-1, #{simplify: 0})) | endwhile', }) screen:expect([[ ^ | @@ -566,7 +566,10 @@ describe('terminal input', function() | {3:-- TERMINAL --} | ]]) - for _, key in ipairs({ + local keys = { + '<Tab>', + '<CR>', + '<Esc>', '<M-Tab>', '<M-CR>', '<M-Esc>', @@ -632,7 +635,14 @@ describe('terminal input', function() '<S-ScrollWheelRight>', '<ScrollWheelLeft>', '<ScrollWheelRight>', - }) do + } + -- FIXME: The escape sequence to enable kitty keyboard mode doesn't work on Windows + if not is_os('win') then + table.insert(keys, '<C-I>') + table.insert(keys, '<C-M>') + table.insert(keys, '<C-[>') + end + for _, key in ipairs(keys) do feed(key) screen:expect(([[ | @@ -643,82 +653,6 @@ describe('terminal input', function() ]]):format(key:gsub('<%d+,%d+>$', ''))) end end) - - -- TODO(bfredl): getcharstr() erases the distinction between <C-I> and <Tab>. - -- If it was enhanced or replaced this could get folded into the test above. - it('can send TAB/C-I and ESC/C-[ separately', function() - if - skip( - is_os('win'), - "The escape sequence to enable kitty keyboard mode doesn't work on Windows" - ) - then - return - end - clear() - local screen = tt.setup_child_nvim({ - '-u', - 'NONE', - '-i', - 'NONE', - '--cmd', - 'colorscheme vim', - '--cmd', - 'set notermguicolors', - '--cmd', - 'noremap <Tab> <cmd>echo "Tab!"<cr>', - '--cmd', - 'noremap <C-i> <cmd>echo "Ctrl-I!"<cr>', - '--cmd', - 'noremap <Esc> <cmd>echo "Esc!"<cr>', - '--cmd', - 'noremap <C-[> <cmd>echo "Ctrl-[!"<cr>', - }) - - screen:expect([[ - ^ | - {4:~ }|*3 - {5:[No Name] 0,0-1 All}| - | - {3:-- TERMINAL --} | - ]]) - - feed('<tab>') - screen:expect([[ - ^ | - {4:~ }|*3 - {5:[No Name] 0,0-1 All}| - Tab! | - {3:-- TERMINAL --} | - ]]) - - feed('<c-i>') - screen:expect([[ - ^ | - {4:~ }|*3 - {5:[No Name] 0,0-1 All}| - Ctrl-I! | - {3:-- TERMINAL --} | - ]]) - - feed('<Esc>') - screen:expect([[ - ^ | - {4:~ }|*3 - {5:[No Name] 0,0-1 All}| - Esc! | - {3:-- TERMINAL --} | - ]]) - - feed('<c-[>') - screen:expect([[ - ^ | - {4:~ }|*3 - {5:[No Name] 0,0-1 All}| - Ctrl-[! | - {3:-- TERMINAL --} | - ]]) - end) end) if is_os('win') then diff --git a/test/old/testdir/test_functions.vim b/test/old/testdir/test_functions.vim index 01e6001dcc..f57743900a 100644 --- a/test/old/testdir/test_functions.vim +++ b/test/old/testdir/test_functions.vim @@ -2390,6 +2390,77 @@ func Test_getchar() call assert_equal("\<M-F2>", getchar(0)) call assert_equal(0, getchar(0)) + call feedkeys("\<Tab>", '') + call assert_equal(char2nr("\<Tab>"), getchar()) + call feedkeys("\<Tab>", '') + call assert_equal(char2nr("\<Tab>"), getchar(-1)) + call feedkeys("\<Tab>", '') + call assert_equal(char2nr("\<Tab>"), getchar(-1, {})) + call feedkeys("\<Tab>", '') + call assert_equal(char2nr("\<Tab>"), getchar(-1, #{number: v:true})) + call assert_equal(0, getchar(0)) + call assert_equal(0, getchar(1)) + call assert_equal(0, getchar(0, #{number: v:true})) + call assert_equal(0, getchar(1, #{number: v:true})) + + call feedkeys("\<Tab>", '') + call assert_equal("\<Tab>", getcharstr()) + call feedkeys("\<Tab>", '') + call assert_equal("\<Tab>", getcharstr(-1)) + call feedkeys("\<Tab>", '') + call assert_equal("\<Tab>", getcharstr(-1, {})) + call feedkeys("\<Tab>", '') + call assert_equal("\<Tab>", getchar(-1, #{number: v:false})) + call assert_equal('', getcharstr(0)) + call assert_equal('', getcharstr(1)) + call assert_equal('', getchar(0, #{number: v:false})) + call assert_equal('', getchar(1, #{number: v:false})) + + " Nvim: <M-x> is never simplified + " for key in ["C-I", "C-X", "M-x"] + for key in ["C-I", "C-X"] + let lines =<< eval trim END + call feedkeys("\<*{key}>", '') + call assert_equal(char2nr("\<{key}>"), getchar()) + call feedkeys("\<*{key}>", '') + call assert_equal(char2nr("\<{key}>"), getchar(-1)) + call feedkeys("\<*{key}>", '') + call assert_equal(char2nr("\<{key}>"), getchar(-1, {{}})) + call feedkeys("\<*{key}>", '') + call assert_equal(char2nr("\<{key}>"), getchar(-1, {{'number': 1}})) + call feedkeys("\<*{key}>", '') + call assert_equal(char2nr("\<{key}>"), getchar(-1, {{'simplify': 1}})) + call feedkeys("\<*{key}>", '') + call assert_equal("\<*{key}>", getchar(-1, {{'simplify': v:false}})) + call assert_equal(0, getchar(0)) + call assert_equal(0, getchar(1)) + END + call CheckLegacyAndVim9Success(lines) + + let lines =<< eval trim END + call feedkeys("\<*{key}>", '') + call assert_equal("\<{key}>", getcharstr()) + call feedkeys("\<*{key}>", '') + call assert_equal("\<{key}>", getcharstr(-1)) + call feedkeys("\<*{key}>", '') + call assert_equal("\<{key}>", getcharstr(-1, {{}})) + call feedkeys("\<*{key}>", '') + call assert_equal("\<{key}>", getchar(-1, {{'number': 0}})) + call feedkeys("\<*{key}>", '') + call assert_equal("\<{key}>", getcharstr(-1, {{'simplify': 1}})) + call feedkeys("\<*{key}>", '') + call assert_equal("\<*{key}>", getcharstr(-1, {{'simplify': v:false}})) + call assert_equal('', getcharstr(0)) + call assert_equal('', getcharstr(1)) + END + call CheckLegacyAndVim9Success(lines) + endfor + + call assert_fails('call getchar(1, 1)', 'E1206:') + call assert_fails('call getcharstr(1, 1)', 'E1206:') + call assert_fails('call getcharstr(1, #{number: v:true})', 'E475:') + call assert_fails('call getcharstr(1, #{number: v:false})', 'E475:') + call setline(1, 'xxxx') call Ntest_setmouse(1, 3) let v:mouse_win = 9 |