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 <zeertzjq@outlook.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index e222d7c..46c3ba8 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 @@
 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 @@
 		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 @@
 		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 @@
 
 		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 @@
 		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 9467528..d8b1ff6 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3172,7 +3172,8 @@
 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 41444f4..69b6a4f 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -388,6 +388,20 @@
 }
 
 /*
+ * 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.
  */
     static int
@@ -1195,6 +1209,7 @@
 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 @@
 			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 @@
 			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 c1628ee..06f4ad4 100644
--- a/src/getchar.c
+++ b/src/getchar.c
@@ -2384,14 +2384,33 @@
  * "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 @@
 
     ++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 @@
     }
     --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 @@
 	    }
 	}
     }
+    else if (!allow_number)
+	rettv->v_type = VAR_STRING;
+    else
+	rettv->vval.v_number = n;
 }
 
 /*
@@ -2500,7 +2528,7 @@
     void
 f_getchar(typval_T *argvars, typval_T *rettv)
 {
-    getchar_common(argvars, rettv);
+    getchar_common(argvars, rettv, TRUE);
 }
 
 /*
@@ -2509,25 +2537,7 @@
     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 b706183..90dcc54 100644
--- a/src/proto/typval.pro
+++ b/src/proto/typval.pro
@@ -18,7 +18,9 @@
 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 e31e2ed..4672fc0 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -2562,6 +2562,75 @@
   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 cfaf0ac..80ed2b2 100644
--- a/src/testdir/test_vim9_builtin.vim
+++ b/src/testdir/test_vim9_builtin.vim
@@ -1838,8 +1838,10 @@
   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 @@
 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 @@
 
 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 e57d898..cd39a0d 100644
--- a/src/typval.c
+++ b/src/typval.c
@@ -527,6 +527,20 @@
 }
 
 /*
+ * 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.
  */
@@ -539,6 +553,18 @@
 }
 
 /*
+ * 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.
  */
     int
diff --git a/src/version.c b/src/version.c
index 21e2787..7137c67 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1068,
+/**/
     1067,
 /**/
     1066,