vim-patch:9.1.1070: Cannot control cursor positioning of getchar() (#32303)

Problem:  Cannot control cursor positioning of getchar().
Solution: Add "cursor" flag to {opts}, with possible values "hide",
          "keep" and "msg".

related: vim/vim#10603
closes: vim/vim#16569

edf0f7db28
This commit is contained in:
zeertzjq 2025-02-03 08:09:03 +08:00 committed by GitHub
parent 87e806186c
commit af069c5c05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 209 additions and 8 deletions

View File

@ -3114,6 +3114,14 @@ getchar([{expr} [, {opts}]]) *getchar()*
The optional argument {opts} is a Dict and supports the
following items:
cursor A String specifying cursor behavior
when waiting for a character.
"hide": hide the cursor.
"keep": keep current cursor unchanged.
"msg": move cursor to message area.
(default: automagically decide
between "keep" and "msg")
number If |TRUE|, return a Number when getting
a single character.
If |FALSE|, the return value is always

View File

@ -410,6 +410,11 @@ UI
• |:checkhealth| can display in a floating window, controlled by the
|g:health| variable.
VIMSCRIPT
• |getchar()| and |getcharstr()| have optional {opts} |Dict| argument to control:
cursor behavior, return type, and whether to simplify the returned key.
==============================================================================
CHANGED FEATURES *news-changed*

View File

@ -2781,6 +2781,14 @@ function vim.fn.getchangelist(buf) end
--- The optional argument {opts} is a Dict and supports the
--- following items:
---
--- cursor A String specifying cursor behavior
--- when waiting for a character.
--- "hide": hide the cursor.
--- "keep": keep current cursor unchanged.
--- "msg": move cursor to message area.
--- (default: automagically decide
--- between "keep" and "msg")
---
--- number If |TRUE|, return a Number when getting
--- a single character.
--- If |FALSE|, the return value is always

View File

@ -3507,6 +3507,14 @@ M.funcs = {
The optional argument {opts} is a Dict and supports the
following items:
cursor A String specifying cursor behavior
when waiting for a character.
"hide": hide the cursor.
"keep": keep current cursor unchanged.
"msg": move cursor to message area.
(default: automagically decide
between "keep" and "msg")
number If |TRUE|, return a Number when getting
a single character.
If |FALSE|, the return value is always

View File

@ -1872,9 +1872,11 @@ static int no_reduce_keys = 0; ///< Do not apply modifiers to the key.
static void getchar_common(typval_T *argvars, typval_T *rettv, bool allow_number)
FUNC_ATTR_NONNULL_ALL
{
varnumber_T n;
varnumber_T n = 0;
const int called_emsg_start = called_emsg;
bool error = false;
bool simplify = true;
char cursor_flag = NUL;
if (argvars[0].v_type != VAR_UNKNOWN
&& tv_check_for_opt_dict_arg(argvars, 1) == FAIL) {
@ -1888,10 +1890,28 @@ static void getchar_common(typval_T *argvars, typval_T *rettv, bool 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);
const char *cursor_str = tv_dict_get_string(d, "cursor", false);
if (cursor_str != NULL) {
if (strcmp(cursor_str, "hide") != 0
&& strcmp(cursor_str, "keep") != 0
&& strcmp(cursor_str, "msg") != 0) {
semsg(_(e_invargNval), "cursor", cursor_str);
} else {
cursor_flag = cursor_str[0];
}
}
}
if (called_emsg != called_emsg_start) {
return;
}
if (cursor_flag == 'h') {
ui_busy_start();
}
no_mapping++;
@ -1899,9 +1919,8 @@ static void getchar_common(typval_T *argvars, typval_T *rettv, bool allow_number
if (!simplify) {
no_reduce_keys++;
}
while (!error) {
if (msg_col > 0) {
// Position the cursor. Needed after a message that ends in a space.
while (true) {
if (cursor_flag == 'm' || (cursor_flag == NUL && msg_col > 0)) {
ui_cursor_goto(msg_row, msg_col);
}
@ -1945,6 +1964,10 @@ static void getchar_common(typval_T *argvars, typval_T *rettv, bool allow_number
no_reduce_keys--;
}
if (cursor_flag == 'h') {
ui_busy_stop();
}
set_vim_var_nr(VV_MOUSE_WIN, 0);
set_vim_var_nr(VV_MOUSE_WINID, 0);
set_vim_var_nr(VV_MOUSE_LNUM, 0);

View File

@ -0,0 +1,92 @@
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local clear = n.clear
local exec = n.exec
local feed = n.feed
local async_command = n.async_meths.nvim_command
describe('getchar()', function()
before_each(clear)
-- oldtest: Test_getchar_cursor_position()
it('cursor positioning', function()
local screen = Screen.new(40, 6)
exec([[
call setline(1, ['foobar', 'foobar', 'foobar'])
call cursor(3, 6)
]])
screen:expect([[
foobar |*2
fooba^r |
{1:~ }|*2
|
]])
-- Default: behaves like "msg" when immediately after printing a message,
-- even if :sleep moved cursor elsewhere.
for _, cmd in ipairs({
'echo 1234 | call getchar()',
'echo 1234 | call getchar(-1, {})',
"echo 1234 | call getchar(-1, #{cursor: 'msg'})",
'echo 1234 | sleep 1m | call getchar()',
'echo 1234 | sleep 1m | call getchar(-1, {})',
"echo 1234 | sleep 1m | call getchar(-1, #{cursor: 'msg'})",
}) do
async_command(cmd)
screen:expect([[
foobar |*3
{1:~ }|*2
1234^ |
]])
feed('a')
screen:expect([[
foobar |*2
fooba^r |
{1:~ }|*2
1234 |
]])
end
-- Default: behaves like "keep" when not immediately after printing a message.
for _, cmd in ipairs({
'call getchar()',
'call getchar(-1, {})',
"call getchar(-1, #{cursor: 'keep'})",
"echo 1234 | sleep 1m | call getchar(-1, #{cursor: 'keep'})",
}) do
async_command(cmd)
screen:expect_unchanged()
feed('a')
screen:expect_unchanged()
end
async_command("call getchar(-1, #{cursor: 'msg'})")
screen:expect([[
foobar |*3
{1:~ }|*2
^1234 |
]])
feed('a')
screen:expect([[
foobar |*2
fooba^r |
{1:~ }|*2
1234 |
]])
async_command("call getchar(-1, #{cursor: 'hide'})")
screen:expect([[
foobar |*3
{1:~ }|*2
1234 |
]])
feed('a')
screen:expect([[
foobar |*2
fooba^r |
{1:~ }|*2
1234 |
]])
end)
end)

View File

@ -2458,6 +2458,14 @@ func Test_getchar()
call assert_fails('call getchar(1, 1)', 'E1206:')
call assert_fails('call getcharstr(1, 1)', 'E1206:')
call assert_fails('call getchar(1, #{cursor: "foo"})', 'E475:')
call assert_fails('call getcharstr(1, #{cursor: "foo"})', 'E475:')
call assert_fails('call getchar(1, #{cursor: 0z})', 'E976:')
call assert_fails('call getcharstr(1, #{cursor: 0z})', 'E976:')
call assert_fails('call getchar(1, #{simplify: 0z})', 'E974:')
call assert_fails('call getcharstr(1, #{simplify: 0z})', 'E974:')
call assert_fails('call getchar(1, #{number: []})', 'E745:')
call assert_fails('call getchar(1, #{number: {}})', 'E728:')
call assert_fails('call getcharstr(1, #{number: v:true})', 'E475:')
call assert_fails('call getcharstr(1, #{number: v:false})', 'E475:')
@ -2476,10 +2484,59 @@ func Test_getchar()
enew!
endfunc
func Test_getchar_cursor_position()
CheckRunVimInTerminal
let lines =<< trim END
call setline(1, ['foobar', 'foobar', 'foobar'])
call cursor(3, 6)
nnoremap <F1> <Cmd>echo 1234<Bar>call getchar()<CR>
nnoremap <F2> <Cmd>call getchar()<CR>
nnoremap <F3> <Cmd>call getchar(-1, {})<CR>
nnoremap <F4> <Cmd>call getchar(-1, #{cursor: 'msg'})<CR>
nnoremap <F5> <Cmd>call getchar(-1, #{cursor: 'keep'})<CR>
nnoremap <F6> <Cmd>call getchar(-1, #{cursor: 'hide'})<CR>
END
call writefile(lines, 'XgetcharCursorPos', 'D')
let buf = RunVimInTerminal('-S XgetcharCursorPos', {'rows': 6})
call WaitForAssert({-> assert_equal([3, 6], term_getcursor(buf)[0:1])})
call term_sendkeys(buf, "\<F1>")
call WaitForAssert({-> assert_equal([6, 5], term_getcursor(buf)[0:1])})
call assert_true(term_getcursor(buf)[2].visible)
call term_sendkeys(buf, 'a')
call WaitForAssert({-> assert_equal([3, 6], term_getcursor(buf)[0:1])})
call assert_true(term_getcursor(buf)[2].visible)
for key in ["\<F2>", "\<F3>", "\<F4>"]
call term_sendkeys(buf, key)
call WaitForAssert({-> assert_equal([6, 1], term_getcursor(buf)[0:1])})
call assert_true(term_getcursor(buf)[2].visible)
call term_sendkeys(buf, 'a')
call WaitForAssert({-> assert_equal([3, 6], term_getcursor(buf)[0:1])})
call assert_true(term_getcursor(buf)[2].visible)
endfor
call term_sendkeys(buf, "\<F5>")
call TermWait(buf, 50)
call assert_equal([3, 6], term_getcursor(buf)[0:1])
call assert_true(term_getcursor(buf)[2].visible)
call term_sendkeys(buf, 'a')
call TermWait(buf, 50)
call assert_equal([3, 6], term_getcursor(buf)[0:1])
call assert_true(term_getcursor(buf)[2].visible)
call term_sendkeys(buf, "\<F6>")
call WaitForAssert({-> assert_false(term_getcursor(buf)[2].visible)})
call term_sendkeys(buf, 'a')
call WaitForAssert({-> assert_true(term_getcursor(buf)[2].visible)})
call assert_equal([3, 6], term_getcursor(buf)[0:1])
call StopVimInTerminal(buf)
endfunc
func Test_libcall_libcallnr()
if !has('libcall')
return
endif
CheckFeature libcall
if has('win32')
let libc = 'msvcrt.dll'