1
0
forked from aniani/vim

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>
This commit is contained in:
zeertzjq
2025-02-02 09:14:35 +01:00
committed by Christian Brabandt
parent cbc1f409c1
commit e0a2ab397f
9 changed files with 198 additions and 52 deletions

View File

@@ -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 VIM REFERENCE MANUAL by Bram Moolenaar
@@ -228,12 +228,12 @@ getbufvar({buf}, {varname} [, {def}])
getcellpixels() List get character cell pixel size getcellpixels() List get character cell pixel size
getcellwidths() List get character cell width overrides getcellwidths() List get character cell width overrides
getchangelist([{buf}]) List list of change list items 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 get one character from the user
getcharmod() Number modifiers for the last typed character getcharmod() Number modifiers for the last typed character
getcharpos({expr}) List position of cursor, mark, etc. getcharpos({expr}) List position of cursor, mark, etc.
getcharsearch() Dict last character search 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 getcmdcomplpat() String return the completion pattern of the
current command-line completion current command-line completion
getcmdcompltype() String return the type of the current getcmdcompltype() String return the type of the current
@@ -3918,14 +3918,16 @@ getchangelist([{buf}]) *getchangelist()*
Return type: list<any> Return type: list<any>
getchar([{expr}]) *getchar()* getchar([{expr} [, {opts}]]) *getchar()*
Get a single character from the user or input stream. 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. If {expr} is 0, only get a character when one is available.
Return zero otherwise. Return zero otherwise.
If {expr} is 1, only check if a character is available, it is If {expr} is 1, only check if a character is available, it is
not consumed. Return zero if no character available. 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 Without {expr} and when {expr} is 0 a whole character or
special key is returned. If it is a single character, the 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 starting with 0x80 (decimal: 128). This is the same value as
the String "\<Key>", e.g., "\<Left>". The returned value is the String "\<Key>", e.g., "\<Left>". The returned value is
also a String when a modifier (shift, control, alt) was used 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 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 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. 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 When the user clicks a mouse button, the mouse event will be
returned. The position can then be found in |v:mouse_col|, returned. The position can then be found in |v:mouse_col|,
|v:mouse_lnum|, |v:mouse_winid| and |v:mouse_win|. |v:mouse_lnum|, |v:mouse_winid| and |v:mouse_win|.
@@ -4062,17 +4083,9 @@ getcharsearch() *getcharsearch()*
Return type: dict<any> Return type: dict<any>
getcharstr([{expr}]) *getcharstr()* getcharstr([{expr} [, {opts}]]) *getcharstr()*
Get a single character from the user or input stream as a The same as |getchar()|, except that this always returns a
string. String, and "number" isn't allowed in {opts}.
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.
Return type: |String| Return type: |String|

View File

@@ -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[] EXTERN char e_legacy_must_be_followed_by_command[]
INIT(= N_("E1234: legacy must be followed by a command")); INIT(= N_("E1234: legacy must be followed by a command"));
#ifdef FEAT_EVAL #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[] EXTERN char e_cannot_use_str_itself_it_is_imported[]
INIT(= N_("E1236: Cannot use %s itself, it is imported")); INIT(= N_("E1236: Cannot use %s itself, it is imported"));
#endif #endif

View File

@@ -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); 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. * 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 arg13_cursor[] = {arg_cursor1, arg_number, arg_number};
static argcheck_T arg12_deepcopy[] = {arg_any, arg_bool}; 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_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_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_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}; 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}, ret_list_any, f_getcellwidths},
{"getchangelist", 0, 1, FEARG_1, arg1_buffer, {"getchangelist", 0, 1, FEARG_1, arg1_buffer,
ret_list_any, f_getchangelist}, ret_list_any, f_getchangelist},
{"getchar", 0, 1, 0, arg1_bool, {"getchar", 0, 2, 0, arg12_getchar,
ret_any, f_getchar}, ret_any, f_getchar},
{"getcharmod", 0, 0, 0, NULL, {"getcharmod", 0, 0, 0, NULL,
ret_number, f_getcharmod}, ret_number, f_getcharmod},
@@ -2103,7 +2118,7 @@ static funcentry_T global_functions[] =
ret_list_number, f_getcharpos}, ret_list_number, f_getcharpos},
{"getcharsearch", 0, 0, 0, NULL, {"getcharsearch", 0, 0, 0, NULL,
ret_dict_any, f_getcharsearch}, ret_dict_any, f_getcharsearch},
{"getcharstr", 0, 1, 0, arg1_bool, {"getcharstr", 0, 2, 0, arg12_getchar,
ret_string, f_getcharstr}, ret_string, f_getcharstr},
{"getcmdcomplpat", 0, 0, 0, NULL, {"getcmdcomplpat", 0, 0, 0, NULL,
ret_string, f_getcmdcomplpat}, ret_string, f_getcmdcomplpat},

View File

@@ -2384,14 +2384,33 @@ char_avail(void)
* "getchar()" and "getcharstr()" functions * "getchar()" and "getcharstr()" functions
*/ */
static void static void
getchar_common(typval_T *argvars, typval_T *rettv) getchar_common(typval_T *argvars, typval_T *rettv, int allow_number)
{ {
varnumber_T n; varnumber_T n;
int error = FALSE; 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; 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 #ifdef MESSAGE_QUEUE
// vpeekc() used to check for messages, but that caused problems, invoking // 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 // 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; ++no_mapping;
++allow_keys; ++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. // getchar(): blocking wait.
n = plain_vgetc_nopaste(); n = plain_vgetc_nopaste();
else if (tv_get_bool_chk(&argvars[0], &error)) else if (tv_get_bool_chk(&argvars[0], &error))
@@ -2427,14 +2450,15 @@ getchar_common(typval_T *argvars, typval_T *rettv)
} }
--no_mapping; --no_mapping;
--allow_keys; --allow_keys;
if (!simplify)
--no_reduce_keys;
set_vim_var_nr(VV_MOUSE_WIN, 0); set_vim_var_nr(VV_MOUSE_WIN, 0);
set_vim_var_nr(VV_MOUSE_WINID, 0); set_vim_var_nr(VV_MOUSE_WINID, 0);
set_vim_var_nr(VV_MOUSE_LNUM, 0); set_vim_var_nr(VV_MOUSE_LNUM, 0);
set_vim_var_nr(VV_MOUSE_COL, 0); set_vim_var_nr(VV_MOUSE_COL, 0);
rettv->vval.v_number = n; if (n != 0 && (!allow_number || IS_SPECIAL(n) || mod_mask != 0))
if (n != 0 && (IS_SPECIAL(n) || mod_mask != 0))
{ {
char_u temp[10]; // modifier: 3, mbyte-char: 6, NUL: 1 char_u temp[10]; // modifier: 3, mbyte-char: 6, NUL: 1
int i = 0; 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 void
f_getchar(typval_T *argvars, typval_T *rettv) 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 void
f_getcharstr(typval_T *argvars, typval_T *rettv) f_getcharstr(typval_T *argvars, typval_T *rettv)
{ {
getchar_common(argvars, rettv); getchar_common(argvars, rettv, FALSE);
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);
} }
/* /*

View File

@@ -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_opt_number_arg(typval_T *args, int idx);
int check_for_float_or_nr_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_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_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_blob_arg(typval_T *args, int idx);
int check_for_list_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); int check_for_nonnull_list_arg(typval_T *args, int idx);

View File

@@ -2562,6 +2562,75 @@ func Test_getchar()
call assert_equal("\<M-F2>", getchar(0)) call assert_equal("\<M-F2>", getchar(0))
call assert_equal(0, 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 setline(1, 'xxxx')
call test_setmouse(1, 3) call test_setmouse(1, 3)
let v:mouse_win = 9 let v:mouse_win = 9

View File

@@ -1838,8 +1838,10 @@ def Test_getchar()
endwhile endwhile
getchar(true)->assert_equal(0) getchar(true)->assert_equal(0)
getchar(1)->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.CheckSourceDefExecAndScriptFailure(['getchar(2)'], 'E1023: Using a Number as a Bool: 2')
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.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 enddef
def Test_getcharpos() def Test_getcharpos()
@@ -1851,8 +1853,14 @@ def Test_getcharpos()
enddef enddef
def Test_getcharstr() def Test_getcharstr()
v9.CheckSourceDefAndScriptFailure(['getcharstr(2)'], ['E1013: Argument 1: type mismatch, expected bool but got number', 'E1212: Bool required for argument 1']) while len(getcharstr(0)) > 0
v9.CheckSourceDefAndScriptFailure(['getcharstr("1")'], ['E1013: Argument 1: type mismatch, expected bool but got string', 'E1212: Bool required for argument 1']) 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 enddef
def Test_getcompletion() def Test_getcompletion()
@@ -4989,7 +4997,7 @@ enddef
def Test_win_findbuf() 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']) 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(''))) assert_equal([win_getid()], win_findbuf(bufnr('')))
enddef enddef

View File

@@ -526,6 +526,20 @@ check_for_bool_arg(typval_T *args, int idx)
return OK; 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'. * Check for an optional bool argument at 'idx'.
* Return FAIL if the type is wrong. * 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); 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. * Give an error and return FAIL unless "args[idx]" is a blob.
*/ */

View File

@@ -704,6 +704,8 @@ static char *(features[]) =
static int included_patches[] = static int included_patches[] =
{ /* Add new patch number below this line */ { /* Add new patch number below this line */
/**/
1068,
/**/ /**/
1067, 1067,
/**/ /**/