patch 8.2.4099: Vim9: cannot use Vim9 syntax in mapping

Problem:    Vim9: cannot use Vim9 syntax in mapping.
Solution:   Add <ScriptCmd> to use the script context for a command.
diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt
index e87d430..1072d56 100644
--- a/runtime/doc/map.txt
+++ b/runtime/doc/map.txt
@@ -284,6 +284,10 @@
 expression-mapped: >
 	nmap ! f!<Ignore>x
 
+When defining a mapping in a |Vim9| script, the expression will be evaluated
+in the context of that script.  This means that script-local items can be
+accessed in the expression.
+
 Be very careful about side effects!  The expression is evaluated while
 obtaining characters, you may very well make the command dysfunctional.
 For this reason the following is blocked:
@@ -342,9 +346,24 @@
 Unlike <expr> mappings, there are no special restrictions on the <Cmd>
 command: it is executed as if an (unrestricted) |autocommand| was invoked.
 
+						*<ScriptCmd>*
+<ScriptCmd> is like <Cmd> but sets the context to the script the mapping was
+defined in, for the duration of the command execution.  This is especially
+useful for |Vim9| script.  It also works to access an import, which is useful
+in a plugin using an autoload script: >
+	vim9script
+	import autoload 'implementation.vim' as impl
+	nnoremap <silent> <F4> <ScriptCmd>impl.DoTheWork()<CR>
+
+No matter where <F4> is typed, the "impl" import will be found in the script
+context of where the mapping was defined.  And since it's an autoload import,
+the "implementation.vim" script will only be loaded once <F4> is typed, not
+when the mapping is defined.
+
 Note:
-- Because <Cmd> avoids mode-changes it does not trigger |CmdlineEnter| and
-  |CmdlineLeave| events, because no user interaction is expected.
+- Because <Cmd> and <ScriptCmd> avoid mode-changes it does not trigger
+  |CmdlineEnter| and |CmdlineLeave| events, because no user interaction is
+  expected.
 - For the same reason, |keycodes| like <C-R><C-W> are interpreted as plain,
   unmapped keys.
 - The command is not echo'ed, no need for <silent>.
@@ -356,12 +375,13 @@
   Visual mode.  Use |:smap| to handle Select mode differently.
 
 							*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.
+<Cmd> and <ScriptCmd> commands must terminate, that is, they must be followed
+by <CR> in the {rhs} of the mapping definition.  |Command-line| mode is never
+entered.
 
 							*E1137*
-<Cmd> commands can have only normal characters and cannot contain special
-characters like function keys.
+<Cmd> and <ScriptCmd> commands can have only normal characters and cannot
+contain special characters like function keys.
 
 
 1.3 MAPPING AND MODES					*:map-modes*
diff --git a/src/edit.c b/src/edit.c
index 739f078..0363129 100644
--- a/src/edit.c
+++ b/src/edit.c
@@ -1055,8 +1055,9 @@
 	case K_IGNORE:	// Something mapped to nothing
 	    break;
 
-	case K_COMMAND:		// <Cmd>command<CR>
-	    do_cmdline(NULL, getcmdkeycmd, NULL, 0);
+	case K_COMMAND:		    // <Cmd>command<CR>
+	case K_SCRIPT_COMMAND:	    // <ScriptCmd>command<CR>
+	    do_cmdkey_command(c, 0);
 #ifdef FEAT_TERMINAL
 	    if (term_use_loop())
 		// Started a terminal that gets the input, exit Insert mode.
diff --git a/src/ex_getln.c b/src/ex_getln.c
index c63c896..5dc43d8 100644
--- a/src/ex_getln.c
+++ b/src/ex_getln.c
@@ -1772,11 +1772,11 @@
 	    c = safe_vgetc();
 	} while (c == K_IGNORE || c == K_NOP);
 
-	if (c == K_COMMAND)
+	if (c == K_COMMAND || c == K_SCRIPT_COMMAND)
 	{
 	    int	    clen = ccline.cmdlen;
 
-	    if (do_cmdline(NULL, getcmdkeycmd, NULL, DOCMD_NOWAIT) == OK)
+	    if (do_cmdkey_command(c, DOCMD_NOWAIT) == OK)
 	    {
 		if (clen == ccline.cmdlen)
 		    trigger_cmdlinechanged = FALSE;
diff --git a/src/getchar.c b/src/getchar.c
index 08c1a95..6434b4d 100644
--- a/src/getchar.c
+++ b/src/getchar.c
@@ -83,6 +83,10 @@
 
 static int	last_recorded_len = 0;	// number of last recorded chars
 
+#ifdef FEAT_EVAL
+mapblock_T	*last_used_map = NULL;
+#endif
+
 static int	read_readbuf(buffheader_T *buf, int advance);
 static void	init_typebuf(void);
 static void	may_sync_undo(void);
@@ -2893,6 +2897,7 @@
 #ifdef FEAT_EVAL
 	    if (save_m_expr)
 		vim_free(map_str);
+	    last_used_map = mp;
 #endif
 	}
 #ifdef FEAT_EVAL
@@ -3708,7 +3713,7 @@
  * Function passed to do_cmdline() to get the command after a <Cmd> key from
  * typeahead.
  */
-    char_u *
+    static char_u *
 getcmdkeycmd(
 	int		promptc UNUSED,
 	void		*cookie UNUSED,
@@ -3774,7 +3779,7 @@
 	    c1 = NUL;  // end the line
 	else if (c1 == ESC)
 	    aborted = TRUE;
-	else if (c1 == K_COMMAND)
+	else if (c1 == K_COMMAND || c1 == K_SCRIPT_COMMAND)
 	{
 	    // give a nicer error message for this special case
 	    emsg(_(e_cmd_mapping_must_end_with_cr_before_second_cmd));
@@ -3804,3 +3809,35 @@
 
     return (char_u *)line_ga.ga_data;
 }
+
+    int
+do_cmdkey_command(int key, int flags)
+{
+    int	    res;
+#ifdef FEAT_EVAL
+    sctx_T  save_current_sctx = {0, 0, 0, 0};
+
+    if (key == K_SCRIPT_COMMAND && last_used_map != NULL)
+    {
+	save_current_sctx = current_sctx;
+	current_sctx = last_used_map->m_script_ctx;
+    }
+#endif
+
+    res = do_cmdline(NULL, getcmdkeycmd, NULL, flags);
+
+#ifdef FEAT_EVAL
+    if (save_current_sctx.sc_sid > 0)
+	current_sctx = save_current_sctx;
+#endif
+
+    return res;
+}
+
+#if defined(FEAT_EVAL) || defined(PROTO)
+    void
+reset_last_used_map(void)
+{
+    last_used_map = NULL;
+}
+#endif
diff --git a/src/insexpand.c b/src/insexpand.c
index 51177fb..b7b6c02 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -2281,7 +2281,8 @@
 
     // Ignore end of Select mode mapping and mouse scroll buttons.
     if (c == K_SELECT || c == K_MOUSEDOWN || c == K_MOUSEUP
-	    || c == K_MOUSELEFT || c == K_MOUSERIGHT || c == K_COMMAND)
+	    || c == K_MOUSELEFT || c == K_MOUSERIGHT
+	    || c == K_COMMAND || c == K_SCRIPT_COMMAND)
 	return retval;
 
 #ifdef FEAT_PROP_POPUP
diff --git a/src/keymap.h b/src/keymap.h
index 4ce5144..12268dd 100644
--- a/src/keymap.h
+++ b/src/keymap.h
@@ -276,6 +276,7 @@
     , KE_MOUSEMOVE_XY = 101	// KE_MOUSEMOVE with coordinates
     , KE_CANCEL = 102		// return from vgetc()
     , KE_COMMAND = 103		// <Cmd> special key
+    , KE_SCRIPT_COMMAND = 104	// <ScriptCmd> special key
 };
 
 /*
@@ -480,6 +481,7 @@
 #define K_CURSORHOLD	TERMCAP2KEY(KS_EXTRA, KE_CURSORHOLD)
 
 #define K_COMMAND	TERMCAP2KEY(KS_EXTRA, KE_COMMAND)
+#define K_SCRIPT_COMMAND TERMCAP2KEY(KS_EXTRA, KE_SCRIPT_COMMAND)
 
 // Bits for modifier mask
 // 0x01 cannot be used, because the modifier must be 0x02 or higher
diff --git a/src/misc2.c b/src/misc2.c
index aa39822..971fbdf 100644
--- a/src/misc2.c
+++ b/src/misc2.c
@@ -1057,6 +1057,7 @@
     {K_CURSORHOLD,	(char_u *)"CursorHold"},
     {K_IGNORE,		(char_u *)"Ignore"},
     {K_COMMAND,		(char_u *)"Cmd"},
+    {K_SCRIPT_COMMAND,	(char_u *)"ScriptCmd"},
     {K_FOCUSGAINED,	(char_u *)"FocusGained"},
     {K_FOCUSLOST,	(char_u *)"FocusLost"},
     {0,			NULL}
diff --git a/src/normal.c b/src/normal.c
index df7ae56..efc7cfd 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -373,6 +373,7 @@
     {K_CURSORHOLD, nv_cursorhold, NV_KEEPREG,		0},
     {K_PS,	nv_edit,	0,			0},
     {K_COMMAND,	nv_colon,	0,			0},
+    {K_SCRIPT_COMMAND, nv_colon, 0,			0},
 };
 
 // Number of commands in nv_cmds[].
@@ -3429,7 +3430,9 @@
 {
     int	old_p_im;
     int	cmd_result;
-    int	is_cmdkey = cap->cmdchar == K_COMMAND;
+    int	is_cmdkey = cap->cmdchar == K_COMMAND
+					   || cap->cmdchar == K_SCRIPT_COMMAND;
+    int	flags;
 
     if (VIsual_active && !is_cmdkey)
 	nv_operator(cap);
@@ -3459,8 +3462,11 @@
 	old_p_im = p_im;
 
 	// get a command line and execute it
-	cmd_result = do_cmdline(NULL, is_cmdkey ? getcmdkeycmd : getexline, NULL,
-			    cap->oap->op_type != OP_NOP ? DOCMD_KEEPLINE : 0);
+	flags = cap->oap->op_type != OP_NOP ? DOCMD_KEEPLINE : 0;
+	if (is_cmdkey)
+	    cmd_result = do_cmdkey_command(cap->cmdchar, flags);
+	else
+	    cmd_result = do_cmdline(NULL, getexline, NULL, flags);
 
 	// If 'insertmode' changed, enter or exit Insert mode
 	if (p_im != old_p_im)
diff --git a/src/ops.c b/src/ops.c
index 8b0f79c..6a37844 100644
--- a/src/ops.c
+++ b/src/ops.c
@@ -3501,6 +3501,14 @@
     int		rv_arg;		// extra argument
 } redo_VIsual_T;
 
+    static int
+is_ex_cmdchar(cmdarg_T *cap)
+{
+    return cap->cmdchar == ':'
+	|| cap->cmdchar == K_COMMAND
+	|| cap->cmdchar == K_SCRIPT_COMMAND;
+}
+
 /*
  * Handle an operator after Visual mode or when the movement is finished.
  * "gui_yank" is true when yanking text for the clipboard.
@@ -3583,8 +3591,7 @@
 		&& ((!VIsual_active || oap->motion_force)
 		    // Also redo Operator-pending Visual mode mappings
 		    || (VIsual_active
-			  && (cap->cmdchar == ':' || cap->cmdchar == K_COMMAND)
-						  && oap->op_type != OP_COLON))
+			    && is_ex_cmdchar(cap) && oap->op_type != OP_COLON))
 		&& cap->cmdchar != 'D'
 #ifdef FEAT_FOLDING
 		&& oap->op_type != OP_FOLD
@@ -3608,7 +3615,7 @@
 		    AppendToRedobuffLit(cap->searchbuf, -1);
 		AppendToRedobuff(NL_STR);
 	    }
-	    else if (cap->cmdchar == ':' || cap->cmdchar == K_COMMAND)
+	    else if (is_ex_cmdchar(cap))
 	    {
 		// do_cmdline() has stored the first typed line in
 		// "repeat_cmdline".  When several lines are typed repeating
@@ -3806,7 +3813,7 @@
 			    get_op_char(oap->op_type),
 			    get_extra_op_char(oap->op_type),
 			    oap->motion_force, cap->cmdchar, cap->nchar);
-		else if (cap->cmdchar != ':' && cap->cmdchar != K_COMMAND)
+		else if (!is_ex_cmdchar(cap))
 		{
 		    int opchar = get_op_char(oap->op_type);
 		    int extra_opchar = get_extra_op_char(oap->op_type);
diff --git a/src/proto/getchar.pro b/src/proto/getchar.pro
index 9745ddf..2d791e5 100644
--- a/src/proto/getchar.pro
+++ b/src/proto/getchar.pro
@@ -52,5 +52,6 @@
 void vungetc(int c);
 int fix_input_buffer(char_u *buf, int len);
 int input_available(void);
-char_u *getcmdkeycmd(int promptc, void *cookie, int indent, getline_opt_T do_concat);
+int do_cmdkey_command(int key, int flags);
+void reset_last_used_map(void);
 /* vim: set ft=c : */
diff --git a/src/terminal.c b/src/terminal.c
index 281c673..f32d3ad 100644
--- a/src/terminal.c
+++ b/src/terminal.c
@@ -2229,7 +2229,8 @@
 	    break;
 
 	case K_COMMAND:
-	    return do_cmdline(NULL, getcmdkeycmd, NULL, 0);
+	case K_SCRIPT_COMMAND:
+	    return do_cmdkey_command(c, 0);
     }
     if (typed)
 	mouse_was_outside = FALSE;
diff --git a/src/testdir/test_vim9_import.vim b/src/testdir/test_vim9_import.vim
index 2e57fe2..f5e4497 100644
--- a/src/testdir/test_vim9_import.vim
+++ b/src/testdir/test_vim9_import.vim
@@ -1337,6 +1337,9 @@
       export def Toggle(): string
         return ":g:toggle_called = 'yes'\<CR>"
       enddef
+      export def Doit()
+        g:doit_called = 'yes'
+      enddef
   END
   writefile(lines, 'Xdir/autoload/toggle.vim')
 
@@ -1346,6 +1349,8 @@
       import autoload 'toggle.vim'
 
       nnoremap <silent> <expr> tt toggle.Toggle() 
+      nnoremap <silent> xx <ScriptCmd>toggle.Doit()<CR>
+      nnoremap <silent> yy <Cmd>toggle.Doit()<CR>
   END
   CheckScriptSuccess(lines)
   assert_false(exists("g:toggle_loaded"))
@@ -1355,7 +1360,14 @@
   assert_equal('yes', g:toggle_loaded)
   assert_equal('yes', g:toggle_called)
 
+  feedkeys("xx", 'xt')
+  assert_equal('yes', g:doit_called)
+
+  assert_fails('call feedkeys("yy", "xt")', 'E121: Undefined variable: toggle')
+
   nunmap tt
+  nunmap xx
+  nunmap yy
   unlet g:toggle_loaded
   unlet g:toggle_called
   delete('Xdir', 'rf')
diff --git a/src/version.c b/src/version.c
index 879a0e8..5bf6df4 100644
--- a/src/version.c
+++ b/src/version.c
@@ -751,6 +751,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    4099,
+/**/
     4098,
 /**/
     4097,