aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/scripts/close_unresponsive.js19
-rw-r--r--.github/workflows/issue-open-check.yml34
-rw-r--r--runtime/doc/eval.txt8
-rw-r--r--runtime/doc/map.txt21
-rw-r--r--runtime/doc/repeat.txt1
-rw-r--r--src/nvim/edit.c2
-rw-r--r--src/nvim/eval.c3
-rw-r--r--src/nvim/eval/userfunc.c12
-rw-r--r--src/nvim/getchar.c40
-rw-r--r--src/nvim/globals.h7
-rw-r--r--src/nvim/menu.c4
-rw-r--r--src/nvim/ops.c6
-rw-r--r--src/nvim/popupmenu.c2
-rw-r--r--src/nvim/runtime.c2
-rw-r--r--test/functional/ex_cmds/cmd_map_spec.lua4
-rw-r--r--test/old/testdir/test_mapping.vim482
-rw-r--r--test/old/testdir/test_normal.vim23
17 files changed, 618 insertions, 52 deletions
diff --git a/.github/scripts/close_unresponsive.js b/.github/scripts/close_unresponsive.js
index b7a92207ba..f0e8bbe93e 100644
--- a/.github/scripts/close_unresponsive.js
+++ b/.github/scripts/close_unresponsive.js
@@ -19,13 +19,18 @@ module.exports = async ({ github, context }) => {
const numbers = issues.data.map((e) => e.number);
for (const number of numbers) {
- const timeline = await github.rest.issues.listEventsForTimeline({
- owner: owner,
- repo: repo,
- issue_number: number,
- });
- const data = timeline.data.filter(labeledEvent);
- const latest_response_label = data[data.length - 1];
+ const events = await github.paginate(
+ github.rest.issues.listEventsForTimeline,
+ {
+ owner: owner,
+ repo: repo,
+ issue_number: number,
+ },
+ (response) => response.data.filter(labeledEvent)
+ );
+
+ const latest_response_label = events[events.length - 1];
+
const created_at = new Date(latest_response_label.created_at);
const now = new Date();
const diff = now - created_at;
diff --git a/.github/workflows/issue-open-check.yml b/.github/workflows/issue-open-check.yml
new file mode 100644
index 0000000000..2471670dc6
--- /dev/null
+++ b/.github/workflows/issue-open-check.yml
@@ -0,0 +1,34 @@
+name: Issue Open Check
+
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ issue-open-check:
+ permissions:
+ issues: write
+ runs-on: ubuntu-latest
+ steps:
+ - name: check issue title
+ id: check-issue
+ uses: actions/github-script@v6
+ with:
+ script: |
+ const title = context.payload.issue.title;
+ const titleSplit = title.split(/\s+/).map(e => e.toLowerCase());
+ const keywords = ['api', 'treesitter', 'ui', 'lsp', 'doc'];
+ var match = new Set();
+ for(const keyword of keywords) {
+ if(titleSplit.includes(keyword)) {
+ match.add(keyword)
+ }
+ }
+ if(match.size !== 0){
+ github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ labels: Array.from(match)
+ })
+ }
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index 05fdf2f5bb..09c44a88af 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -4270,10 +4270,10 @@ The input is in the variable "line", the results in the variables "file",
getting the scriptnames in a Dictionary ~
*scriptnames-dictionary*
-The |:scriptnames| command can be used to get a list of all script files that
-have been sourced. There is no equivalent function or variable for this
-(because it's rarely needed). In case you need to manipulate the list this
-code can be used: >
+The `:scriptnames` command can be used to get a list of all script files that
+have been sourced. There is also the `getscriptinfo()` function, but the
+information returned is not exactly the same. In case you need to manipulate
+the output of `scriptnames` this code can be used: >
" Get the output of ":scriptnames" in the scriptnames_output variable.
let scriptnames_output = ''
redir => scriptnames_output
diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt
index 164e2d4ec5..37158e2e76 100644
--- a/runtime/doc/map.txt
+++ b/runtime/doc/map.txt
@@ -290,7 +290,7 @@ Therefore the following is blocked for <expr> mappings:
- Moving the cursor is allowed, but it is restored afterwards.
- If the cmdline is changed, the old text and cursor position are restored.
If you want the mapping to do any of these let the returned characters do
-that. (Or use a |<Cmd>| mapping instead.)
+that, or use a |<Cmd>| mapping instead.
You can use getchar(), it consumes typeahead if there is any. E.g., if you
have these mappings: >
@@ -324,20 +324,22 @@ be seen as a special key.
*<Cmd>* *:map-cmd*
The <Cmd> pseudokey begins a "command mapping", which executes the command
-directly (without changing modes). Where you might use ":...<CR>" in the
+directly without changing modes. Where you might use ":...<CR>" in the
{rhs} of a mapping, you can instead use "<Cmd>...<CR>".
Example: >
- noremap x <Cmd>echo mode(1)<cr>
+ noremap x <Cmd>echo mode(1)<CR>
<
-This is more flexible than `:<C-U>` in visual and operator-pending mode, or
-`<C-O>:` in insert-mode, because the commands are executed directly in the
-current mode (instead of always going to normal-mode). Visual-mode is
+This is more flexible than `:<C-U>` in Visual and Operator-pending mode, or
+`<C-O>:` in Insert mode, because the commands are executed directly in the
+current mode, instead of always going to Normal mode. Visual mode is
preserved, so tricks with |gv| are not needed. Commands can be invoked
-directly in cmdline-mode (which would otherwise require timer hacks).
+directly in Command-line mode (which would otherwise require timer hacks).
+Example of using <Cmd> halfway Insert mode: >
+ nnoremap <F3> aText <Cmd>echo mode(1)<CR> Added<Esc>
Unlike <expr> mappings, there are no special restrictions on the <Cmd>
command: it is executed as if an (unrestricted) |autocommand| was invoked
-or an async event event was processed.
+or an async event was processed.
Note:
- Because <Cmd> avoids mode-changes (unlike ":") it does not trigger
@@ -350,7 +352,7 @@ Note:
- In Visual mode you can use `line('v')` and `col('v')` to get one end of the
Visual area, the cursor is at the other end.
- *E5520*
+ *E1255* *E1136*
<Cmd> commands must terminate, that is, they must be followed by <CR> in the
{rhs} of the mapping definition. |Command-line| mode is never entered.
@@ -636,6 +638,7 @@ not to be matched with any key sequence. This is useful in plugins
*<MouseMove>*
The special key name "<MouseMove>" can be used to handle mouse movement. It
needs to be enabled with 'mousemoveevent'.
+The |getmousepos()| function can be used to obtain the mouse position.
*<Char>* *<Char->*
To map a character by its decimal, octal or hexadecimal number the <Char>
diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt
index 23030761dd..8644ce4b38 100644
--- a/runtime/doc/repeat.txt
+++ b/runtime/doc/repeat.txt
@@ -336,6 +336,7 @@ For writing a Vim script, see chapter 41 of the user manual |usr_41.txt|.
:scr[iptnames] List all sourced script names, in the order they were
first sourced. The number is used for the script ID
|<SID>|.
+ Also see `getscriptinfo()`.
:scr[iptnames][!] {scriptId} *:script*
Edit script {scriptId}. Although ":scriptnames name"
diff --git a/src/nvim/edit.c b/src/nvim/edit.c
index 2078fc4251..c544daee08 100644
--- a/src/nvim/edit.c
+++ b/src/nvim/edit.c
@@ -876,7 +876,7 @@ static int insert_handle_key(InsertState *s)
state_handle_k_event();
goto check_pum;
- case K_COMMAND: // some command
+ case K_COMMAND: // <Cmd>command<CR>
do_cmdline(NULL, getcmdkeycmd, NULL, 0);
goto check_pum;
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index 13299b0253..cb1f4d26fb 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -3677,9 +3677,6 @@ static int eval_index_inner(typval_T *rettv, bool is_range, typval_T *var1, typv
} else if (n2 >= len) {
n2 = len;
}
- if (exclusive) {
- n2--;
- }
if (n1 >= len || n2 < 0 || n1 > n2) {
v = NULL;
} else {
diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c
index fbb5e8d10c..7e20a298dd 100644
--- a/src/nvim/eval/userfunc.c
+++ b/src/nvim/eval/userfunc.c
@@ -467,12 +467,10 @@ char *deref_func_name(const char *name, int *lenp, partial_T **const partialp, b
/// @param name function name
void emsg_funcname(const char *errmsg, const char *name)
{
- char *p;
+ char *p = (char *)name;
- if ((uint8_t)(*name) == K_SPECIAL) {
+ if ((uint8_t)name[0] == K_SPECIAL && name[1] != NUL && name[2] != NUL) {
p = concat_str("<SNR>", name + 3);
- } else {
- p = (char *)name;
}
semsg(_(errmsg), p);
@@ -1863,8 +1861,7 @@ char *trans_function_name(char **pp, bool skip, int flags, funcdict_T *fdp, part
// Check for hard coded <SNR>: already translated function ID (from a user
// command).
- if ((unsigned char)(*pp)[0] == K_SPECIAL && (unsigned char)(*pp)[1] == KS_EXTRA
- && (*pp)[2] == KE_SNR) {
+ if ((uint8_t)(*pp)[0] == K_SPECIAL && (uint8_t)(*pp)[1] == KS_EXTRA && (*pp)[2] == KE_SNR) {
*pp += 3;
len = get_id_len((const char **)pp) + 3;
return xmemdupz(start, (size_t)len);
@@ -2140,7 +2137,6 @@ void ex_function(exarg_T *eap)
char *theline;
char *line_to_free = NULL;
char c;
- int saved_did_emsg;
bool saved_wait_return = need_wait_return;
char *name = NULL;
char *p;
@@ -2234,7 +2230,7 @@ void ex_function(exarg_T *eap)
// An error in a function call during evaluation of an expression in magic
// braces should not cause the function not to be defined.
- saved_did_emsg = did_emsg;
+ const int saved_did_emsg = did_emsg;
did_emsg = false;
//
diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c
index a0e8350287..ca555937ab 100644
--- a/src/nvim/getchar.c
+++ b/src/nvim/getchar.c
@@ -92,8 +92,8 @@ static buffheader_T readbuf2 = { { NULL, { NUL } }, NULL, 0, 0 };
static int typeahead_char = 0; // typeahead char that's not flushed
-// when block_redo is true redo buffer will not be changed
-// used by edit() to repeat insertions and 'V' command for redoing
+/// When block_redo is true the redo buffer will not be changed.
+/// Used by edit() to repeat insertions.
static int block_redo = false;
static int KeyNoremap = 0; // remapping flags
@@ -134,6 +134,10 @@ static size_t last_recorded_len = 0; // number of last recorded chars
#endif
static const char e_recursive_mapping[] = N_("E223: Recursive mapping");
+static const char e_cmd_mapping_must_end_with_cr[]
+ = N_("E1255: <Cmd> mapping must end with <CR>");
+static const char e_cmd_mapping_must_end_with_cr_before_second_cmd[]
+ = N_("E1136: <Cmd> mapping must end with <CR> before second <Cmd>");
// Free and clear a buffer.
void free_buff(buffheader_T *buf)
@@ -550,6 +554,25 @@ void AppendToRedobuffLit(const char *str, int len)
}
}
+/// Append "s" to the redo buffer, leaving 3-byte special key codes unmodified
+/// and escaping other K_SPECIAL bytes.
+void AppendToRedobuffSpec(const char *s)
+{
+ if (block_redo) {
+ return;
+ }
+
+ while (*s != NUL) {
+ if ((uint8_t)(*s) == K_SPECIAL && s[1] != NUL && s[2] != NUL) {
+ // Insert special key literally.
+ add_buff(&redobuff, s, 3L);
+ s += 3;
+ } else {
+ add_char_buff(&redobuff, mb_cptr2char_adv(&s));
+ }
+ }
+}
+
/// Append a character to the redo buffer.
/// Translates special keys, NUL, K_SPECIAL and multibyte characters.
void AppendCharToRedobuff(int c)
@@ -2884,7 +2907,8 @@ int fix_input_buffer(uint8_t *buf, int len)
return len;
}
-/// Get command argument for <Cmd> key
+/// Function passed to do_cmdline() to get the command after a <Cmd> key from
+/// typeahead.
char *getcmdkeycmd(int promptc, void *cookie, int indent, bool do_concat)
{
garray_T line_ga;
@@ -2894,6 +2918,7 @@ char *getcmdkeycmd(int promptc, void *cookie, int indent, bool do_concat)
ga_init(&line_ga, 1, 32);
+ // no mapping for these characters
no_mapping++;
got_int = false;
@@ -2903,16 +2928,17 @@ char *getcmdkeycmd(int promptc, void *cookie, int indent, bool do_concat)
if (vgetorpeek(false) == NUL) {
// incomplete <Cmd> is an error, because there is not much the user
// could do in this state.
- emsg(e_cmdmap_err);
+ emsg(_(e_cmd_mapping_must_end_with_cr));
aborted = true;
break;
}
// Get one character at a time.
c1 = vgetorpeek(true);
+
// Get two extra bytes for special keys
if (c1 == K_SPECIAL) {
- c1 = vgetorpeek(true); // no mapping for these chars
+ c1 = vgetorpeek(true);
c2 = vgetorpeek(true);
if (c1 == KS_MODIFIER) {
cmod = c2;
@@ -2928,8 +2954,8 @@ char *getcmdkeycmd(int promptc, void *cookie, int indent, bool do_concat)
} else if (c1 == ESC) {
aborted = true;
} else if (c1 == K_COMMAND) {
- // special case to give nicer error message
- emsg(e_cmdmap_repeated);
+ // give a nicer error message for this special case
+ emsg(_(e_cmd_mapping_must_end_with_cr_before_second_cmd));
aborted = true;
} else if (c1 == K_SNR) {
ga_concat(&line_ga, "<SNR>");
diff --git a/src/nvim/globals.h b/src/nvim/globals.h
index 698f4a98c7..bedb529d04 100644
--- a/src/nvim/globals.h
+++ b/src/nvim/globals.h
@@ -993,7 +993,8 @@ EXTERN const char e_notset[] INIT(= N_("E764: Option '%s' is not set"));
EXTERN const char e_invalidreg[] INIT(= N_("E850: Invalid register name"));
EXTERN const char e_dirnotf[] INIT(= N_("E919: Directory not found in '%s': \"%s\""));
EXTERN const char e_au_recursive[] INIT(= N_("E952: Autocommand caused recursive behavior"));
-EXTERN const char e_menuothermode[] INIT(= N_("E328: Menu only exists in another mode"));
+EXTERN const char e_menu_only_exists_in_another_mode[]
+INIT(= N_("E328: Menu only exists in another mode"));
EXTERN const char e_autocmd_close[] INIT(= N_("E813: Cannot close autocmd window"));
EXTERN const char e_listarg[] INIT(= N_("E686: Argument of %s must be a List"));
EXTERN const char e_unsupportedoption[] INIT(= N_("E519: Option not supported"));
@@ -1003,10 +1004,6 @@ EXTERN const char e_cannot_edit_other_buf[] INIT(= N_("E788: Not allowed to edit
EXTERN const char e_using_number_as_bool_nr[] INIT(= N_("E1023: Using a Number as a Bool: %d"));
EXTERN const char e_not_callable_type_str[] INIT(= N_("E1085: Not a callable type: %s"));
-EXTERN const char e_cmdmap_err[] INIT(= N_("E5520: <Cmd> mapping must end with <CR>"));
-EXTERN const char e_cmdmap_repeated[]
-INIT(= N_("E5521: <Cmd> mapping must end with <CR> before second <Cmd>"));
-
EXTERN const char e_api_error[] INIT(= N_("E5555: API call: %s"));
EXTERN const char e_luv_api_disabled[] INIT(= N_("E5560: %s must not be called in a lua loop callback"));
diff --git a/src/nvim/menu.c b/src/nvim/menu.c
index 16fea2ccf1..898e3ddd27 100644
--- a/src/nvim/menu.c
+++ b/src/nvim/menu.c
@@ -559,7 +559,7 @@ static int remove_menu(vimmenu_T **menup, char *name, int modes, bool silent)
}
} else if (*name != NUL) {
if (!silent) {
- emsg(_(e_menuothermode));
+ emsg(_(e_menu_only_exists_in_another_mode));
}
return FAIL;
}
@@ -760,7 +760,7 @@ static vimmenu_T *find_menu(vimmenu_T *menu, char *name, int modes)
emsg(_(e_notsubmenu));
return NULL;
} else if ((menu->modes & modes) == 0x0) {
- emsg(_(e_menuothermode));
+ emsg(_(e_menu_only_exists_in_another_mode));
return NULL;
} else if (*p == NUL) { // found a full match
return menu;
diff --git a/src/nvim/ops.c b/src/nvim/ops.c
index bb66bb5731..c1511ab8da 100644
--- a/src/nvim/ops.c
+++ b/src/nvim/ops.c
@@ -5858,7 +5858,11 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
if (repeat_cmdline == NULL) {
ResetRedobuff();
} else {
- AppendToRedobuffLit(repeat_cmdline, -1);
+ if (cap->cmdchar == ':') {
+ AppendToRedobuffLit(repeat_cmdline, -1);
+ } else {
+ AppendToRedobuffSpec(repeat_cmdline);
+ }
AppendToRedobuff(NL_STR);
XFREE_CLEAR(repeat_cmdline);
}
diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c
index d404aa9647..6d5d063a75 100644
--- a/src/nvim/popupmenu.c
+++ b/src/nvim/popupmenu.c
@@ -1083,7 +1083,7 @@ void pum_show_popupmenu(vimmenu_T *menu)
// When there are only Terminal mode menus, using "popup Edit" results in
// pum_size being zero.
if (pum_size <= 0) {
- emsg(e_menuothermode);
+ emsg(_(e_menu_only_exists_in_another_mode));
return;
}
diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c
index b4a23d544e..34e94d6021 100644
--- a/src/nvim/runtime.c
+++ b/src/nvim/runtime.c
@@ -2409,7 +2409,7 @@ void f_getscriptinfo(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
return;
}
if (sid <= 0) {
- semsg(e_invargNval, "sid", tv_get_string(&sid_di->di_tv));
+ semsg(_(e_invargNval), "sid", tv_get_string(&sid_di->di_tv));
return;
}
} else {
diff --git a/test/functional/ex_cmds/cmd_map_spec.lua b/test/functional/ex_cmds/cmd_map_spec.lua
index 919d167712..12867179bd 100644
--- a/test/functional/ex_cmds/cmd_map_spec.lua
+++ b/test/functional/ex_cmds/cmd_map_spec.lua
@@ -90,7 +90,7 @@ describe('mappings with <Cmd>', function()
{1:~ }|
{1:~ }|
{1:~ }|
- {2:E5521: <Cmd> mapping must end with <CR> before second <Cmd>} |
+ {2:E1136: <Cmd> mapping must end with <CR> before second <Cmd>} |
]])
command('noremap <F3> <Cmd>let x = 3')
@@ -103,7 +103,7 @@ describe('mappings with <Cmd>', function()
{1:~ }|
{1:~ }|
{1:~ }|
- {2:E5520: <Cmd> mapping must end with <CR>} |
+ {2:E1255: <Cmd> mapping must end with <CR>} |
]])
eq(0, eval('x'))
end)
diff --git a/test/old/testdir/test_mapping.vim b/test/old/testdir/test_mapping.vim
index e25c3c333e..8c957d4665 100644
--- a/test/old/testdir/test_mapping.vim
+++ b/test/old/testdir/test_mapping.vim
@@ -3,6 +3,7 @@
source shared.vim
source check.vim
source screendump.vim
+source term_util.vim
func Test_abbreviation()
" abbreviation with 0x80 should work
@@ -968,6 +969,469 @@ func Test_map_cpo_special_keycode()
mapclear!
endfunc
+" Test for <Cmd> key in maps to execute commands
+func Test_map_cmdkey()
+ new
+
+ " Error cases
+ let x = 0
+ noremap <F3> <Cmd><Cmd>let x = 1<CR>
+ call assert_fails('call feedkeys("\<F3>", "xt")', 'E1136:')
+ call assert_equal(0, x)
+
+ noremap <F3> <Cmd>let x = 3
+ call assert_fails('call feedkeys("\<F3>", "xt!")', 'E1255:')
+ call assert_equal(0, x)
+
+ " works in various modes and sees the correct mode()
+ noremap <F3> <Cmd>let m = mode(1)<CR>
+ noremap! <F3> <Cmd>let m = mode(1)<CR>
+
+ " normal mode
+ call feedkeys("\<F3>", 'xt')
+ call assert_equal('n', m)
+
+ " visual mode
+ call feedkeys("v\<F3>", 'xt!')
+ call assert_equal('v', m)
+ " shouldn't leave the visual mode
+ call assert_equal('v', mode(1))
+ call feedkeys("\<Esc>", 'xt')
+ call assert_equal('n', mode(1))
+
+ " visual mapping in select mode
+ call feedkeys("gh\<F3>", 'xt!')
+ call assert_equal('v', m)
+ " shouldn't leave select mode
+ call assert_equal('s', mode(1))
+ call feedkeys("\<Esc>", 'xt')
+ call assert_equal('n', mode(1))
+
+ " select mode mapping
+ snoremap <F3> <Cmd>let m = mode(1)<cr>
+ call feedkeys("gh\<F3>", 'xt!')
+ call assert_equal('s', m)
+ " shouldn't leave select mode
+ call assert_equal('s', mode(1))
+ call feedkeys("\<Esc>", 'xt')
+ call assert_equal('n', mode(1))
+
+ " operator-pending mode
+ call feedkeys("d\<F3>", 'xt!')
+ call assert_equal('no', m)
+ " leaves the operator-pending mode
+ call assert_equal('n', mode(1))
+
+ " insert mode
+ call feedkeys("i\<F3>abc", 'xt')
+ call assert_equal('i', m)
+ call assert_equal('abc', getline('.'))
+
+ " replace mode
+ call feedkeys("0R\<F3>two", 'xt')
+ call assert_equal('R', m)
+ call assert_equal('two', getline('.'))
+
+ " virtual replace mode
+ call setline('.', "one\ttwo")
+ call feedkeys("4|gR\<F3>xxx", 'xt')
+ call assert_equal('Rv', m)
+ call assert_equal("onexxx\ttwo", getline('.'))
+
+ " cmdline mode
+ call feedkeys(":\<F3>\"xxx\<CR>", 'xt!')
+ call assert_equal('c', m)
+ call assert_equal('"xxx', @:)
+
+ " terminal mode
+ if CanRunVimInTerminal()
+ tnoremap <F3> <Cmd>let m = mode(1)<CR>
+ let buf = Run_shell_in_terminal({})
+ call feedkeys("\<F3>", 'xt')
+ call assert_equal('t', m)
+ call assert_equal('t', mode(1))
+ call StopShellInTerminal(buf)
+ call TermWait(buf)
+ close!
+ tunmap <F3>
+ endif
+
+ " invoke cmdline mode recursively
+ noremap! <F2> <Cmd>norm! :foo<CR>
+ %d
+ call setline(1, ['some short lines', 'of test text'])
+ call feedkeys(":bar\<F2>x\<C-B>\"\r", 'xt')
+ call assert_equal('"barx', @:)
+ unmap! <F2>
+
+ " test for calling a <SID> function
+ let lines =<< trim END
+ map <F2> <Cmd>call <SID>do_it()<CR>
+ func s:do_it()
+ let g:x = 32
+ endfunc
+ END
+ call writefile(lines, 'Xscript')
+ source Xscript
+ call feedkeys("\<F2>", 'xt')
+ call assert_equal(32, g:x)
+ call delete('Xscript')
+
+ unmap <F3>
+ unmap! <F3>
+ %bw!
+endfunc
+
+" text object enters visual mode
+func TextObj()
+ if mode() !=# "v"
+ normal! v
+ end
+ call cursor(1, 3)
+ normal! o
+ call cursor(2, 4)
+endfunc
+
+func s:cmdmap(lhs, rhs)
+ exe 'noremap ' .. a:lhs .. ' <Cmd>' .. a:rhs .. '<CR>'
+ exe 'noremap! ' .. a:lhs .. ' <Cmd>' .. a:rhs .. '<CR>'
+endfunc
+
+func s:cmdunmap(lhs)
+ exe 'unmap ' .. a:lhs
+ exe 'unmap! ' .. a:lhs
+endfunc
+
+" Map various <Fx> keys used by the <Cmd> key tests
+func s:setupMaps()
+ call s:cmdmap('<F3>', 'let m = mode(1)')
+ call s:cmdmap('<F4>', 'normal! ww')
+ call s:cmdmap('<F5>', 'normal! "ay')
+ call s:cmdmap('<F6>', 'throw "very error"')
+ call s:cmdmap('<F7>', 'call TextObj()')
+ call s:cmdmap('<F8>', 'startinsert')
+ call s:cmdmap('<F9>', 'stopinsert')
+endfunc
+
+" Remove the mappings setup by setupMaps()
+func s:cleanupMaps()
+ call s:cmdunmap('<F3>')
+ call s:cmdunmap('<F4>')
+ call s:cmdunmap('<F5>')
+ call s:cmdunmap('<F6>')
+ call s:cmdunmap('<F7>')
+ call s:cmdunmap('<F8>')
+ call s:cmdunmap('<F9>')
+endfunc
+
+" Test for <Cmd> mapping in normal mode
+func Test_map_cmdkey_normal_mode()
+ new
+ call s:setupMaps()
+
+ " check v:count and v:register works
+ call s:cmdmap('<F2>', 'let s = [mode(1), v:count, v:register]')
+ call feedkeys("\<F2>", 'xt')
+ call assert_equal(['n', 0, '"'], s)
+ call feedkeys("7\<F2>", 'xt')
+ call assert_equal(['n', 7, '"'], s)
+ call feedkeys("\"e\<F2>", 'xt')
+ call assert_equal(['n', 0, 'e'], s)
+ call feedkeys("5\"k\<F2>", 'xt')
+ call assert_equal(['n', 5, 'k'], s)
+ call s:cmdunmap('<F2>')
+
+ call setline(1, ['some short lines', 'of test text'])
+ call feedkeys("\<F7>y", 'xt')
+ call assert_equal("me short lines\nof t", @")
+ call assert_equal('v', getregtype('"'))
+ call assert_equal([0, 1, 3, 0], getpos("'<"))
+ call assert_equal([0, 2, 4, 0], getpos("'>"))
+
+ " startinsert
+ %d
+ call feedkeys("\<F8>abc", 'xt')
+ call assert_equal('abc', getline(1))
+
+ " feedkeys are not executed immediately
+ noremap ,a <Cmd>call feedkeys("aalpha") \| let g:a = getline(2)<CR>
+ %d
+ call setline(1, ['some short lines', 'of test text'])
+ call cursor(2, 3)
+ call feedkeys(",a\<F3>", 'xt')
+ call assert_equal('of test text', g:a)
+ call assert_equal('n', m)
+ call assert_equal(['some short lines', 'of alphatest text'], getline(1, '$'))
+ nunmap ,a
+
+ " feedkeys(..., 'x') is executed immediately, but insert mode is aborted
+ noremap ,b <Cmd>call feedkeys("abeta", 'x') \| let g:b = getline(2)<CR>
+ call feedkeys(",b\<F3>", 'xt')
+ call assert_equal('n', m)
+ call assert_equal('of alphabetatest text', g:b)
+ nunmap ,b
+
+ call s:cleanupMaps()
+ %bw!
+endfunc
+
+" Test for <Cmd> mapping with the :normal command
+func Test_map_cmdkey_normal_cmd()
+ new
+ noremap ,x <Cmd>call append(1, "xx") \| call append(1, "aa")<CR>
+ noremap ,f <Cmd>nosuchcommand<CR>
+ noremap ,e <Cmd>throw "very error" \| call append(1, "yy")<CR>
+ noremap ,m <Cmd>echoerr "The message." \| call append(1, "zz")<CR>
+ noremap ,w <Cmd>for i in range(5) \| if i==1 \| echoerr "Err" \| endif \| call append(1, i) \| endfor<CR>
+
+ call setline(1, ['some short lines', 'of test text'])
+ exe "norm ,x\r"
+ call assert_equal(['some short lines', 'aa', 'xx', 'of test text'], getline(1, '$'))
+
+ call assert_fails('norm ,f', 'E492:')
+ call assert_fails('norm ,e', 'very error')
+ call assert_fails('norm ,m', 'The message.')
+ call assert_equal(['some short lines', 'aa', 'xx', 'of test text'], getline(1, '$'))
+
+ %d
+ let caught_err = 0
+ try
+ exe "normal ,w"
+ catch /Vim(echoerr):Err/
+ let caught_err = 1
+ endtry
+ call assert_equal(1, caught_err)
+ call assert_equal(['', '0'], getline(1, '$'))
+
+ %d
+ call assert_fails('normal ,w', 'Err')
+ call assert_equal(['', '4', '3', '2' ,'1', '0'], getline(1, '$'))
+ call assert_equal(1, line('.'))
+
+ nunmap ,x
+ nunmap ,f
+ nunmap ,e
+ nunmap ,m
+ nunmap ,w
+ %bw!
+endfunc
+
+" Test for <Cmd> mapping in visual mode
+func Test_map_cmdkey_visual_mode()
+ new
+ set showmode
+ call s:setupMaps()
+
+ call setline(1, ['some short lines', 'of test text'])
+ call feedkeys("v\<F4>", 'xt!')
+ call assert_equal(['v', 1, 12], [mode(1), col('v'), col('.')])
+
+ " can invoke an opeartor, ending the visual mode
+ let @a = ''
+ call feedkeys("\<F5>", 'xt!')
+ call assert_equal('n', mode(1))
+ call assert_equal('some short l', @a)
+
+ " error doesn't interrupt visual mode
+ call assert_fails('call feedkeys("ggvw\<F6>", "xt!")', 'E605:')
+ call assert_equal(['v', 1, 6], [mode(1), col('v'), col('.')])
+ call feedkeys("\<F7>", 'xt!')
+ call assert_equal(['v', 1, 3, 2, 4], [mode(1), line('v'), col('v'), line('.'), col('.')])
+
+ " startinsert gives "-- (insert) VISUAL --" mode
+ call feedkeys("\<F8>", 'xt!')
+ call assert_equal(['v', 1, 3, 2, 4], [mode(1), line('v'), col('v'), line('.'), col('.')])
+ redraw!
+ call assert_match('^-- (insert) VISUAL --', Screenline(&lines))
+ call feedkeys("\<Esc>new ", 'x')
+ call assert_equal(['some short lines', 'of new test text'], getline(1, '$'))
+
+ call s:cleanupMaps()
+ set showmode&
+ %bw!
+endfunc
+
+" Test for <Cmd> mapping in select mode
+func Test_map_cmdkey_select_mode()
+ new
+ set showmode
+ call s:setupMaps()
+
+ snoremap <F1> <cmd>throw "very error"<CR>
+ snoremap <F2> <cmd>normal! <c-g>"by<CR>
+ call setline(1, ['some short lines', 'of test text'])
+
+ call feedkeys("gh\<F4>", "xt!")
+ call assert_equal(['s', 1, 12], [mode(1), col('v'), col('.')])
+ redraw!
+ call assert_match('^-- SELECT --', Screenline(&lines))
+
+ " visual mapping in select mode restarts select mode after operator
+ let @a = ''
+ call feedkeys("\<F5>", 'xt!')
+ call assert_equal('s', mode(1))
+ call assert_equal('some short l', @a)
+
+ " select mode mapping works, and does not restart select mode
+ let @b = ''
+ call feedkeys("\<F2>", 'xt!')
+ call assert_equal('n', mode(1))
+ call assert_equal('some short l', @b)
+
+ " error doesn't interrupt temporary visual mode
+ call assert_fails('call feedkeys("\<Esc>ggvw\<C-G>\<F6>", "xt!")', 'E605:')
+ redraw!
+ call assert_match('^-- VISUAL --', Screenline(&lines))
+ " quirk: restoration of select mode is not performed
+ call assert_equal(['v', 1, 6], [mode(1), col('v'), col('.')])
+
+ " error doesn't interrupt select mode
+ call assert_fails('call feedkeys("\<Esc>ggvw\<C-G>\<F1>", "xt!")', 'E605:')
+ redraw!
+ call assert_match('^-- SELECT --', Screenline(&lines))
+ call assert_equal(['s', 1, 6], [mode(1), col('v'), col('.')])
+
+ call feedkeys("\<F7>", 'xt!')
+ redraw!
+ call assert_match('^-- SELECT --', Screenline(&lines))
+ call assert_equal(['s', 1, 3, 2, 4], [mode(1), line('v'), col('v'), line('.'), col('.')])
+
+ " startinsert gives "-- SELECT (insert) --" mode
+ call feedkeys("\<F8>", 'xt!')
+ redraw!
+ call assert_match('^-- (insert) SELECT --', Screenline(&lines))
+ call assert_equal(['s', 1, 3, 2, 4], [mode(1), line('v'), col('v'), line('.'), col('.')])
+ call feedkeys("\<Esc>new ", 'x')
+ call assert_equal(['some short lines', 'of new test text'], getline(1, '$'))
+
+ sunmap <F1>
+ sunmap <F2>
+ call s:cleanupMaps()
+ set showmode&
+ %bw!
+endfunc
+
+" Test for <Cmd> mapping in operator-pending mode
+func Test_map_cmdkey_op_pending_mode()
+ new
+ call s:setupMaps()
+
+ call setline(1, ['some short lines', 'of test text'])
+ call feedkeys("d\<F4>", 'xt')
+ call assert_equal(['lines', 'of test text'], getline(1, '$'))
+ call assert_equal(['some short '], getreg('"', 1, 1))
+ " create a new undo point
+ let &undolevels = &undolevels
+
+ call feedkeys(".", 'xt')
+ call assert_equal(['test text'], getline(1, '$'))
+ call assert_equal(['lines', 'of '], getreg('"', 1, 1))
+ " create a new undo point
+ let &undolevels = &undolevels
+
+ call feedkeys("uu", 'xt')
+ call assert_equal(['some short lines', 'of test text'], getline(1, '$'))
+
+ " error aborts operator-pending, operator not performed
+ call assert_fails('call feedkeys("d\<F6>", "xt")', 'E605:')
+ call assert_equal(['some short lines', 'of test text'], getline(1, '$'))
+
+ call feedkeys("\"bd\<F7>", 'xt')
+ call assert_equal(['soest text'], getline(1, '$'))
+ call assert_equal(['me short lines', 'of t'], getreg('b', 1, 1))
+
+ " startinsert aborts operator
+ call feedkeys("d\<F8>cc", 'xt')
+ call assert_equal(['soccest text'], getline(1, '$'))
+
+ call s:cleanupMaps()
+ %bw!
+endfunc
+
+" Test for <Cmd> mapping in insert mode
+func Test_map_cmdkey_insert_mode()
+ new
+ call s:setupMaps()
+
+ call setline(1, ['some short lines', 'of test text'])
+ " works the same as <C-O>w<C-O>w
+ call feedkeys("iindeed \<F4>little ", 'xt')
+ call assert_equal(['indeed some short little lines', 'of test text'], getline(1, '$'))
+ call assert_fails('call feedkeys("i\<F6> 2", "xt")', 'E605:')
+ call assert_equal(['indeed some short little 2 lines', 'of test text'], getline(1, '$'))
+
+ " Note when entering visual mode from InsertEnter autocmd, an async event,
+ " or a <Cmd> mapping, vim ends up in undocumented "INSERT VISUAL" mode.
+ call feedkeys("i\<F7>stuff ", 'xt')
+ call assert_equal(['indeed some short little 2 lines', 'of stuff test text'], getline(1, '$'))
+ call assert_equal(['v', 1, 3, 2, 9], [mode(1), line('v'), col('v'), line('.'), col('.')])
+
+ call feedkeys("\<F5>", 'xt')
+ call assert_equal(['deed some short little 2 lines', 'of stuff '], getreg('a', 1, 1))
+
+ " also works as part of abbreviation
+ abbr foo <Cmd>let g:y = 17<CR>bar
+ exe "normal i\<space>foo "
+ call assert_equal(17, g:y)
+ call assert_equal('in bar deed some short little 2 lines', getline(1))
+ unabbr foo
+
+ " :startinsert does nothing
+ call setline(1, 'foo bar')
+ call feedkeys("ggi\<F8>vim", 'xt')
+ call assert_equal('vimfoo bar', getline(1))
+
+ " :stopinsert works
+ call feedkeys("ggi\<F9>Abc", 'xt')
+ call assert_equal('vimfoo barbc', getline(1))
+
+ call s:cleanupMaps()
+ %bw!
+endfunc
+
+" Test for <Cmd> mapping in insert-completion mode
+func Test_map_cmdkey_insert_complete_mode()
+ new
+ call s:setupMaps()
+
+ call setline(1, 'some short lines')
+ call feedkeys("os\<C-X>\<C-N>\<F3>\<C-N> ", 'xt')
+ call assert_equal('ic', m)
+ call assert_equal(['some short lines', 'short '], getline(1, '$'))
+
+ call s:cleanupMaps()
+ %bw!
+endfunc
+
+" Test for <Cmd> mapping in cmdline mode
+func Test_map_cmdkey_cmdline_mode()
+ new
+ call s:setupMaps()
+
+ call setline(1, ['some short lines', 'of test text'])
+ let x = 0
+ call feedkeys(":let x\<F3>= 10\r", 'xt')
+ call assert_equal('c', m)
+ call assert_equal(10, x)
+
+ " exception doesn't leave cmdline mode
+ call assert_fails('call feedkeys(":let x\<F6>= 20\r", "xt")', 'E605:')
+ call assert_equal(20, x)
+
+ " move cursor in the buffer from cmdline mode
+ call feedkeys(":let x\<F4>= 30\r", 'xt')
+ call assert_equal(30, x)
+ call assert_equal(12, col('.'))
+
+ " :startinsert takes effect after leaving cmdline mode
+ call feedkeys(":let x\<F8>= 40\rnew ", 'xt')
+ call assert_equal(40, x)
+ call assert_equal('some short new lines', getline(1))
+
+ call s:cleanupMaps()
+ %bw!
+endfunc
+
func Test_map_cmdkey_redo()
func SelectDash()
call search('^---\n\zs', 'bcW')
@@ -1002,6 +1466,24 @@ func Test_map_cmdkey_redo()
call delete('Xcmdtext')
delfunc SelectDash
ounmap i-
+
+ new
+ call setline(1, 'aaa bbb ccc ddd')
+
+ " command can contain special keys
+ onoremap ix <Cmd>let g:foo ..= '…'<Bar>normal! <C-Right><CR>
+ let g:foo = ''
+ call feedkeys('0dix.', 'xt')
+ call assert_equal('……', g:foo)
+ call assert_equal('ccc ddd', getline(1))
+ unlet g:foo
+
+ " command line ending in "0" is handled without errors
+ onoremap ix <Cmd>eval 0<CR>
+ call feedkeys('dix.', 'xt')
+
+ ounmap ix
+ bwipe!
endfunc
" Test for using <script> with a map to remap characters in rhs
diff --git a/test/old/testdir/test_normal.vim b/test/old/testdir/test_normal.vim
index 330a16dffb..a854c9538f 100644
--- a/test/old/testdir/test_normal.vim
+++ b/test/old/testdir/test_normal.vim
@@ -3648,11 +3648,32 @@ func Test_horiz_motion()
bwipe!
endfunc
-" Test for using a : command in operator pending mode
+" Test for using a ":" command in operator pending mode
func Test_normal_colon_op()
new
call setline(1, ['one', 'two'])
call assert_beeps("normal! Gc:d\<CR>")
+ call assert_equal(['one'], getline(1, '$'))
+
+ call setline(1, ['one…two…three!'])
+ normal! $
+ " Using ":" as a movement is characterwise exclusive
+ call feedkeys("d:normal! F…\<CR>", 'xt')
+ call assert_equal(['one…two!'], getline(1, '$'))
+ " Check that redoing a command with 0x80 bytes works
+ call feedkeys('.', 'xt')
+ call assert_equal(['one!'], getline(1, '$'))
+
+ call setline(1, ['one', 'two', 'three', 'four', 'five'])
+ " Add this to the command history
+ call feedkeys(":normal! G0\<CR>", 'xt')
+ " Use :normal! with control characters in operator pending mode
+ call feedkeys("d:normal! \<C-V>\<C-P>\<C-V>\<C-P>\<CR>", 'xt')
+ call assert_equal(['one', 'two', 'five'], getline(1, '$'))
+ " Check that redoing a command with control characters works
+ call feedkeys('.', 'xt')
+ call assert_equal(['five'], getline(1, '$'))
+
bwipe!
endfunc