patch 9.1.1068: getchar() can't distinguish between C-I and Tab

Commit: 
https://github.com/vim/vim/commit/e0a2ab397fd13a71efec85b017d5d4d62baf7f63
Author: zeertzjq <zeert...@outlook.com>
Date:   Sun Feb 2 09:14:35 2025 +0100

    patch 9.1.1068: getchar() can't distinguish between C-I and Tab
    
    Problem:  getchar() can't distinguish between C-I and Tab.
    Solution: Add {opts} to pass extra flags to getchar() and getcharstr(),
              with "number" and "simplify" keys.
    
    related: #10603
    closes: #16554
    
    Signed-off-by: zeertzjq <zeert...@outlook.com>
    Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index e222d7c5a..46c3ba855 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -1,4 +1,4 @@
-*builtin.txt*  For Vim version 9.1.  Last change: 2025 Feb 01
+*builtin.txt*  For Vim version 9.1.  Last change: 2025 Feb 02
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -228,12 +228,12 @@ getbufvar({buf}, {varname} [, {def}])
 getcellpixels()                        List    get character cell pixel size
 getcellwidths()                        List    get character cell width 
overrides
 getchangelist([{buf}])         List    list of change list items
-getchar([{expr}])              Number or String
+getchar([{expr} [, {opts}]])   Number or String
                                        get one character from the user
 getcharmod()                   Number  modifiers for the last typed character
 getcharpos({expr})             List    position of cursor, mark, etc.
 getcharsearch()                        Dict    last character search
-getcharstr([{expr}])           String  get one character from the user
+getcharstr([{expr} [, {opts}]])        String  get one character from the user
 getcmdcomplpat()               String  return the completion pattern of the
                                        current command-line completion
 getcmdcompltype()              String  return the type of the current
@@ -3918,14 +3918,16 @@ getchangelist([{buf}])                                  
*getchangelist()*
                Return type: list<any>
 
 
-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
@@ -3935,7 +3937,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
@@ -3947,6 +3950,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|.
@@ -4062,17 +4083,9 @@ getcharsearch()                                          
*getcharsearch()*
                Return type: dict<any>
 
 
-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.
+getcharstr([{expr} [, {opts}]])                                *getcharstr()*
+               The same as |getchar()|, except that this always returns a
+               String, and "number" isn't allowed in {opts}.
 
                Return type: |String|
 
diff --git a/src/errors.h b/src/errors.h
index 94675289c..d8b1ff68f 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3172,7 +3172,8 @@ EXTERN char 
e_exists_compiled_can_only_be_used_in_def_function[]
 EXTERN char e_legacy_must_be_followed_by_command[]
        INIT(= N_("E1234: legacy must be followed by a command"));
 #ifdef FEAT_EVAL
-// E1235 unused
+EXTERN char e_bool_or_number_required_for_argument_nr[]
+       INIT(= N_("E1235: Bool or Number required for argument %d"));
 EXTERN char e_cannot_use_str_itself_it_is_imported[]
        INIT(= N_("E1236: Cannot use %s itself, it is imported"));
 #endif
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 41444f497..69b6a4fa8 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -387,6 +387,20 @@ arg_bool(type_T *type, type_T *decl_type UNUSED, 
argcontext_T *context)
     return check_arg_type(&t_bool, type, context);
 }
 
+/*
+ * Check "type" is a bool or a number.
+ */
+    static int
+arg_bool_or_nr(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+{
+    if (type->tt_type == VAR_BOOL
+           || type->tt_type == VAR_NUMBER
+           || type_any_or_unknown(type))
+       return OK;
+    arg_type_mismatch(&t_number, type, context->arg_idx + 1);
+    return FAIL;
+}
+
 /*
  * Check "type" is a list of 'any' or a blob.
  */
@@ -1195,6 +1209,7 @@ static argcheck_T arg24_count[] = 
{arg_string_or_list_or_dict, arg_any, arg_bool
 static argcheck_T arg13_cursor[] = {arg_cursor1, arg_number, arg_number};
 static argcheck_T arg12_deepcopy[] = {arg_any, arg_bool};
 static argcheck_T arg12_execute[] = {arg_string_or_list_string, arg_string};
+static argcheck_T arg12_getchar[] = {arg_bool_or_nr, arg_dict_any};
 static argcheck_T arg23_extend[] = {arg_list_or_dict_mod, arg_same_as_prev, 
arg_extend3};
 static argcheck_T arg23_extendnew[] = {arg_list_or_dict, 
arg_same_struct_as_prev, arg_extend3};
 static argcheck_T arg23_get[] = {arg_get1, arg_string_or_nr, arg_any};
@@ -2095,7 +2110,7 @@ static funcentry_T global_functions[] =
                        ret_list_any,       f_getcellwidths},
     {"getchangelist",  0, 1, FEARG_1,      arg1_buffer,
                        ret_list_any,       f_getchangelist},
-    {"getchar",                0, 1, 0,            arg1_bool,
+    {"getchar",                0, 2, 0,            arg12_getchar,
                        ret_any,            f_getchar},
     {"getcharmod",     0, 0, 0,            NULL,
                        ret_number,         f_getcharmod},
@@ -2103,7 +2118,7 @@ static funcentry_T global_functions[] =
                        ret_list_number,    f_getcharpos},
     {"getcharsearch",  0, 0, 0,            NULL,
                        ret_dict_any,       f_getcharsearch},
-    {"getcharstr",     0, 1, 0,            arg1_bool,
+    {"getcharstr",     0, 2, 0,            arg12_getchar,
                        ret_string,         f_getcharstr},
     {"getcmdcomplpat", 0, 0, 0,            NULL,
                        ret_string,         f_getcmdcomplpat},
diff --git a/src/getchar.c b/src/getchar.c
index c1628ee5a..06f4ad435 100644
--- a/src/getchar.c
+++ b/src/getchar.c
@@ -2384,14 +2384,33 @@ char_avail(void)
  * "getchar()" and "getcharstr()" functions
  */
     static void
-getchar_common(typval_T *argvars, typval_T *rettv)
+getchar_common(typval_T *argvars, typval_T *rettv, int allow_number)
 {
     varnumber_T                n;
     int                        error = FALSE;
+    int                        simplify = TRUE;
 
-    if (in_vim9script() && check_for_opt_bool_arg(argvars, 0) == FAIL)
+    if ((in_vim9script()
+               && check_for_opt_bool_or_number_arg(argvars, 0) == FAIL)
+           || (argvars[0].v_type != VAR_UNKNOWN
+                   && 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 = dict_get_bool(d, "number", TRUE);
+       else if (dict_has_key(d, "number"))
+       {
+           semsg(_(e_invalid_argument_str), "number");
+           error = TRUE;
+       }
+
+       simplify = dict_get_bool(d, "simplify", TRUE);
+    }
+
 #ifdef MESSAGE_QUEUE
     // vpeekc() used to check for messages, but that caused problems, invoking
     // a callback where it was not expected.  Some plugins use getchar(1) in a
@@ -2404,9 +2423,13 @@ getchar_common(typval_T *argvars, typval_T *rettv)
 
     ++no_mapping;
     ++allow_keys;
-    for (;;)
+    if (!simplify)
+       ++no_reduce_keys;
+    while (!error)
     {
-       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.
            n = plain_vgetc_nopaste();
        else if (tv_get_bool_chk(&argvars[0], &error))
@@ -2427,14 +2450,15 @@ 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_u          temp[10];   // modifier: 3, mbyte-char: 6, NUL: 1
        int             i = 0;
@@ -2492,6 +2516,10 @@ getchar_common(typval_T *argvars, typval_T *rettv)
            }
        }
     }
+    else if (!allow_number)
+       rettv->v_type = VAR_STRING;
+    else
+       rettv->vval.v_number = n;
 }
 
 /*
@@ -2500,7 +2528,7 @@ getchar_common(typval_T *argvars, typval_T *rettv)
     void
 f_getchar(typval_T *argvars, typval_T *rettv)
 {
-    getchar_common(argvars, rettv);
+    getchar_common(argvars, rettv, TRUE);
 }
 
 /*
@@ -2509,25 +2537,7 @@ f_getchar(typval_T *argvars, typval_T *rettv)
     void
 f_getcharstr(typval_T *argvars, typval_T *rettv)
 {
-    getchar_common(argvars, rettv);
-
-    if (rettv->v_type != VAR_NUMBER)
-       return;
-
-    char_u             temp[7];   // mbyte-char: 6, NUL: 1
-    varnumber_T        n = rettv->vval.v_number;
-    int                i = 0;
-
-    if (n != 0)
-    {
-       if (has_mbyte)
-           i += (*mb_char2bytes)(n, temp + i);
-       else
-           temp[i++] = n;
-    }
-    temp[i] = NUL;
-    rettv->v_type = VAR_STRING;
-    rettv->vval.v_string = vim_strnsave(temp, i);
+    getchar_common(argvars, rettv, FALSE);
 }
 
 /*
diff --git a/src/proto/typval.pro b/src/proto/typval.pro
index b70618342..90dcc5434 100644
--- a/src/proto/typval.pro
+++ b/src/proto/typval.pro
@@ -18,7 +18,9 @@ int check_for_number_arg(typval_T *args, int idx);
 int check_for_opt_number_arg(typval_T *args, int idx);
 int check_for_float_or_nr_arg(typval_T *args, int idx);
 int check_for_bool_arg(typval_T *args, int idx);
+int check_for_bool_or_number_arg(typval_T *args, int idx);
 int check_for_opt_bool_arg(typval_T *args, int idx);
+int check_for_opt_bool_or_number_arg(typval_T *args, int idx);
 int check_for_blob_arg(typval_T *args, int idx);
 int check_for_list_arg(typval_T *args, int idx);
 int check_for_nonnull_list_arg(typval_T *args, int idx);
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index e31e2ed73..4672fc0c4 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -2562,6 +2562,75 @@ 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}))
+
+  for key in ["C-I", "C-X", "M-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 v9.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 v9.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 test_setmouse(1, 3)
   let v:mouse_win = 9
diff --git a/src/testdir/test_vim9_builtin.vim 
b/src/testdir/test_vim9_builtin.vim
index cfaf0ace2..80ed2b230 100644
--- a/src/testdir/test_vim9_builtin.vim
+++ b/src/testdir/test_vim9_builtin.vim
@@ -1838,8 +1838,10 @@ def Test_getchar()
   endwhile
   getchar(true)->assert_equal(0)
   getchar(1)->assert_equal(0)
-  v9.CheckSourceDefAndScriptFailure(['getchar(2)'], ['E1013: Argument 1: type 
mismatch, expected bool but got number', 'E1212: Bool required for argument 1'])
-  v9.CheckSourceDefAndScriptFailure(['getchar("1")'], ['E1013: Argument 1: 
type mismatch, expected bool but got string', 'E1212: Bool required for 
argument 1'])
+  v9.CheckSourceDefExecAndScriptFailure(['getchar(2)'], 'E1023: Using a Number 
as a Bool: 2')
+  v9.CheckSourceDefExecAndScriptFailure(['getchar(-2)'], 'E1023: Using a 
Number as a Bool: -2')
+  v9.CheckSourceDefAndScriptFailure(['getchar("1")'], ['E1013: Argument 1: 
type mismatch, expected number but got string', 'E1235: Bool or Number required 
for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['getchar(1, 1)'], ['E1013: Argument 2: 
type mismatch, expected dict<any> but got number', 'E1206: Dictionary required 
for argument 2'])
 enddef
 
 def Test_getcharpos()
@@ -1851,8 +1853,14 @@ def Test_getcharpos()
 enddef
 
 def Test_getcharstr()
-  v9.CheckSourceDefAndScriptFailure(['getcharstr(2)'], ['E1013: Argument 1: 
type mismatch, expected bool but got number', 'E1212: Bool required for 
argument 1'])
-  v9.CheckSourceDefAndScriptFailure(['getcharstr("1")'], ['E1013: Argument 1: 
type mismatch, expected bool but got string', 'E1212: Bool required for 
argument 1'])
+  while len(getcharstr(0)) > 0
+  endwhile
+  getcharstr(true)->assert_equal('')
+  getcharstr(1)->assert_equal('')
+  v9.CheckSourceDefExecAndScriptFailure(['getcharstr(2)'], 'E1023: Using a 
Number as a Bool: 2')
+  v9.CheckSourceDefExecAndScriptFailure(['getcharstr(-2)'], 'E1023: Using a 
Number as a Bool: -2')
+  v9.CheckSourceDefAndScriptFailure(['getcharstr("1")'], ['E1013: Argument 1: 
type mismatch, expected number but got string', 'E1235: Bool or Number required 
for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['getcharstr(1, 1)'], ['E1013: Argument 2: 
type mismatch, expected dict<any> but got number', 'E1206: Dictionary required 
for argument 2'])
 enddef
 
 def Test_getcompletion()
@@ -4989,7 +4997,7 @@ enddef
 
 def Test_win_findbuf()
   v9.CheckSourceDefAndScriptFailure(['win_findbuf("a")'], ['E1013: Argument 1: 
type mismatch, expected number but got string', 'E1210: Number required for 
argument 1'])
-  assert_equal([], win_findbuf(1000))
+  assert_equal([], win_findbuf(9999))
   assert_equal([win_getid()], win_findbuf(bufnr('')))
 enddef
 
diff --git a/src/typval.c b/src/typval.c
index e57d89815..cd39a0d97 100644
--- a/src/typval.c
+++ b/src/typval.c
@@ -526,6 +526,20 @@ check_for_bool_arg(typval_T *args, int idx)
     return OK;
 }
 
+/*
+ * Give an error and return FAIL unless "args[idx]" is a bool or a number.
+ */
+    int
+check_for_bool_or_number_arg(typval_T *args, int idx)
+{
+    if (args[idx].v_type != VAR_BOOL && args[idx].v_type != VAR_NUMBER)
+    {
+       semsg(_(e_bool_or_number_required_for_argument_nr), idx + 1);
+       return FAIL;
+    }
+    return OK;
+}
+
 /*
  * Check for an optional bool argument at 'idx'.
  * Return FAIL if the type is wrong.
@@ -538,6 +552,18 @@ check_for_opt_bool_arg(typval_T *args, int idx)
     return check_for_bool_arg(args, idx);
 }
 
+/*
+ * Check for an optional bool or number argument at 'idx'.
+ * Return FAIL if the type is wrong.
+ */
+    int
+check_for_opt_bool_or_number_arg(typval_T *args, int idx)
+{
+    if (args[idx].v_type == VAR_UNKNOWN)
+       return OK;
+    return check_for_bool_or_number_arg(args, idx);
+}
+
 /*
  * Give an error and return FAIL unless "args[idx]" is a blob.
  */
diff --git a/src/version.c b/src/version.c
index 21e2787d1..7137c675b 100644
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1068,
 /**/
     1067,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to vim_dev+unsubscr...@googlegroups.com.
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1teVMl-000RKF-6U%40256bit.org.

Raspunde prin e-mail lui