changeset 31503:b9a4699d6a35 v9.0.1084

patch 9.0.1084: code handling low level MS-Windows events cannot be tested Commit: https://github.com/vim/vim/commit/20b795e0eba6c933868c8f7cf62fb85d4f007688 Author: Christopher Plewright <chris@createng.com> Date: Tue Dec 20 20:01:58 2022 +0000 patch 9.0.1084: code handling low level MS-Windows events cannot be tested Problem: Code handling low level MS-Windows events cannot be tested. Solution: Add test_mswin_event() and tests using it. (Christopher Plewright, closes #11622)
author Bram Moolenaar <Bram@vim.org>
date Tue, 20 Dec 2022 21:15:05 +0100
parents 9940db8ec7d7
children 19e5fba4fffb
files runtime/doc/builtin.txt runtime/doc/testing.txt runtime/doc/usr_41.txt src/evalfunc.c src/gui_w32.c src/os_win32.c src/proto/gui_w32.pro src/proto/os_win32.pro src/proto/testing.pro src/term.c src/testdir/Make_all.mak src/testdir/mouse.vim src/testdir/test_gui.vim src/testdir/test_mswin_event.vim src/testdir/test_termcodes.vim src/testing.c src/version.c
diffstat 17 files changed, 1499 insertions(+), 83 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -666,6 +666,8 @@ test_garbagecollect_soon()	none	free mem
 test_getvalue({string})		any	get value of an internal variable
 test_gui_event({event}, {args})	bool	generate a GUI event for testing
 test_ignore_error({expr})	none	ignore a specific error
+test_mswin_event({event}, {args})
+				bool	generate MS-Windows event for testing
 test_null_blob()		Blob	null value for testing
 test_null_channel()		Channel	null value for testing
 test_null_dict()		Dict	null value for testing
--- a/runtime/doc/testing.txt
+++ b/runtime/doc/testing.txt
@@ -94,7 +94,7 @@ test_gui_event({event}, {args})
 		    "findrepl"  search and replace text.
 		    "mouse"	mouse button click event.
 		    "scrollbar" move or drag the scrollbar.
-		    "sendevent" send a low-level GUI event.
+		    "key"	send a low-level keyboard event.
 		    "tabline"	select a tab page by mouse click.
 		    "tabmenu"	select a tabline menu entry.
 
@@ -178,8 +178,8 @@ test_gui_event({event}, {args})
 		    dragging:	1 to drag the scrollbar and 0 to click in the
 				scrollbar.
 
-		"sendevent":
-		  Send a low-level GUI event (e.g. key-up or down).
+		"key":
+		  Send a low-level keyboard event (e.g. key-up or down).
 		  Currently only supported on MS-Windows.
 		  The supported items in {args} are:
 		    event:	The supported string values are:
@@ -223,6 +223,72 @@ test_ignore_error({expr})			 *test_ignor
 		Can also be used as a |method|: >
 			GetErrorText()->test_ignore_error()
 
+				
+test_mswin_event({event}, {args})		*test_mswin_event()*
+		Generate a low-level MS-Windows {event} with arguments {args}
+		for testing Vim functionality.  It works for MS-Windows GUI 
+		and for the console.
+		
+		{event} is a String and the supported values are:
+		    "mouse"	mouse event.
+		    "key"	keyboard event.
+
+		"mouse":
+		  Inject either a mouse button click, or a mouse move, event.
+		  The supported items in {args} are:
+		    button:	mouse button.  The supported values are:
+				    0	right mouse button
+				    1	middle mouse button
+				    2	left mouse button
+				    3	mouse button release
+				    4	scroll wheel down
+				    5	scroll wheel up
+				    6	scroll wheel left
+				    7	scroll wheel right
+		    row:	mouse click row number.  The first row of the
+				Vim window is 1 and the last row is 'lines'.
+		    col:	mouse click column number.  The maximum value
+				of {col} is 'columns'.
+				Note: row and col are always interpreted as
+				screen cells for the console application.
+				But, they may be interpreted as pixels
+				for the GUI, depending on "cell".
+		    multiclick:	set to 1 to inject a double-click mouse event.
+		    modifiers:	key modifiers.  The supported values are:
+				    4	shift is pressed
+				    8	alt is pressed
+				   16	ctrl is pressed
+		    move:	Optional; if used and TRUE then a mouse move
+			        event can be generated.
+				Only {args} row: and col: are used and
+				required.
+				Only results in an event when 'mousemoveevent'
+				is set or a popup uses mouse move events.
+		    cell:	Optional for the GUI: when present and TRUE
+				then "move" uses screen cells instead of pixel
+				positions.  Not used by the console.
+
+		"key":
+		  Send a low-level keyboard event (e.g. keyup or keydown).
+		  The supported items in {args} are:
+		    event:	The supported string values are:
+				    keyup   generate a keyup event
+				    keydown generate a keydown event
+		    keycode:    Keycode to use for a keyup or a keydown event.
+		    modifiers:	Optional; key modifiers.
+				The supported values are:
+				    2	shift is pressed
+				    4	ctrl is pressed
+				    8	alt is pressed
+				Note: These values are different from the
+				mouse modifiers.
+								*E1291*
+		Returns TRUE if the event is successfully added, FALSE if
+		there is a failure.
+
+		Can also be used as a |method|: >
+			GetEvent()->test_mswin_event({args})
+<
 
 test_null_blob()					*test_null_blob()*
 		Return a |Blob| that is null. Only useful for testing.
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1186,6 +1186,7 @@ Testing:				    *test-functions*
 	test_getvalue()		get value of an internal variable
 	test_gui_event()	generate a GUI event for testing
 	test_ignore_error()	ignore a specific error message
+	test_mswin_event()	generate an MS-Windows event
 	test_null_blob()	return a null Blob
 	test_null_channel()	return a null Channel
 	test_null_dict()	return a null Dict
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -2694,6 +2694,8 @@ static funcentry_T global_functions[] =
 			ret_bool,	    f_test_gui_event},
     {"test_ignore_error", 1, 1, FEARG_1,    arg1_string,
 			ret_void,	    f_test_ignore_error},
+    {"test_mswin_event", 2, 2, FEARG_1,     arg2_string_dict,
+			ret_number,	    f_test_mswin_event},
     {"test_null_blob",	0, 0, 0,	    NULL,
 			ret_blob,	    f_test_null_blob},
     {"test_null_channel", 0, 0, 0,	    NULL,
@@ -4387,7 +4389,12 @@ f_feedkeys(typval_T *argvars, typval_T *
 
     if (*keys != NUL || execute)
     {
-	if (lowlevel)
+	if (lowlevel
+#ifdef FEAT_VTP
+		&& (!is_term_win32()
+		    || (keys[0] == 3 && ctrl_c_interrupts && typed))
+#endif
+	   )
 	{
 #ifdef USE_INPUT_BUF
 	    ch_log(NULL, "feedkeys() lowlevel: %s", keys);
--- a/src/gui_w32.c
+++ b/src/gui_w32.c
@@ -8643,41 +8643,176 @@ netbeans_draw_multisign_indicator(int ro
 #endif
 
 #if defined(FEAT_EVAL) || defined(PROTO)
-    int
-test_gui_w32_sendevent(dict_T *args)
-{
-    char_u	*event;
-    INPUT	inputs[1];
-
-    event = dict_get_string(args, "event", TRUE);
-    if (event == NULL)
+
+// TODO: at the moment, this is just a copy of test_gui_mouse_event.
+// But, we could instead generate actual Win32 mouse event messages,
+// ie. to make it consistent wih test_gui_w32_sendevent_keyboard.
+    static int
+test_gui_w32_sendevent_mouse(dict_T *args)
+{
+    if (!dict_has_key(args, "row") || !dict_has_key(args, "col"))
+	return FALSE;
+
+    // Note: "move" is optional, requires fewer arguments
+    int move = (int)dict_get_bool(args, "move", FALSE);
+
+    if (!move && (!dict_has_key(args, "button")
+	    || !dict_has_key(args, "multiclick")
+	    || !dict_has_key(args, "modifiers")))
 	return FALSE;
 
-    ZeroMemory(inputs, sizeof(inputs));
-
-    if (STRICMP(event, "keydown") == 0 || STRICMP(event, "keyup") == 0)
-    {
-	WORD	    vkCode;
-
-	vkCode = dict_get_number_def(args, "keycode", 0);
+    int row = (int)dict_get_number(args, "row");
+    int col = (int)dict_get_number(args, "col");
+
+    if (move)
+    {
+	// the "move" argument expects row and col coordnates to be in pixels,
+	// unless "cell" is specified and is TRUE.
+	if (dict_get_bool(args, "cell", FALSE))
+	{
+	    // calculate the middle of the character cell
+	    // Note: Cell coordinates are 1-based from vimscript
+	    int pY = (row - 1) * gui.char_height + gui.char_height / 2;
+	    int pX = (col - 1) * gui.char_width + gui.char_width / 2;
+	    gui_mouse_moved(pX, pY);
+	}
+	else
+	    gui_mouse_moved(col, row);
+    }
+    else
+    {
+	int button = (int)dict_get_number(args, "button");
+	int repeated_click = (int)dict_get_number(args, "multiclick");
+	int_u mods = (int)dict_get_number(args, "modifiers");
+
+	// Reset the scroll values to known values.
+	// XXX: Remove this when/if the scroll step is made configurable.
+	mouse_set_hor_scroll_step(6);
+	mouse_set_vert_scroll_step(3);
+
+	gui_send_mouse_event(button, TEXT_X(col - 1), TEXT_Y(row - 1),
+							repeated_click, mods);
+    }
+    return TRUE;
+}
+
+    static int
+test_gui_w32_sendevent_keyboard(dict_T *args)
+{
+    INPUT inputs[1];
+    INPUT modkeys[3];
+    SecureZeroMemory(inputs, sizeof(INPUT));
+    SecureZeroMemory(modkeys, 3 * sizeof(INPUT));
+
+    char_u *event = dict_get_string(args, "event", TRUE);
+
+    if (event && (STRICMP(event, "keydown") == 0
+				      || STRICMP(event, "keyup") == 0))
+    {
+	WORD vkCode = dict_get_number_def(args, "keycode", 0);
 	if (vkCode <= 0 || vkCode >= 0xFF)
 	{
 	    semsg(_(e_invalid_argument_nr), (long)vkCode);
 	    return FALSE;
 	}
 
+	BOOL isModKey = (vkCode == VK_SHIFT || vkCode == VK_CONTROL
+	    || vkCode == VK_MENU || vkCode == VK_LSHIFT || vkCode == VK_RSHIFT
+	    || vkCode == VK_LCONTROL || vkCode == VK_RCONTROL
+	    || vkCode == VK_LMENU || vkCode == VK_RMENU );
+
+	BOOL unwrapMods = FALSE;
+	int mods = (int)dict_get_number(args, "modifiers");
+
+	// If there are modifiers in the args, and it is not a keyup event and
+	// vkCode is not a modifier key, then we generate virtual modifier key
+	// messages before sending the actual key message.
+	if(mods && STRICMP(event, "keydown") == 0 && !isModKey)
+	{
+	    int n = 0;
+	    if (mods & MOD_MASK_SHIFT)
+	    {
+		modkeys[n].type = INPUT_KEYBOARD;
+		modkeys[n].ki.wVk = VK_LSHIFT;
+		n++;
+	    }
+	    if (mods & MOD_MASK_CTRL)
+	    {
+		modkeys[n].type = INPUT_KEYBOARD;
+		modkeys[n].ki.wVk = VK_LCONTROL;
+		n++;
+	    }
+	    if (mods & MOD_MASK_ALT)
+	    {
+		modkeys[n].type = INPUT_KEYBOARD;
+		modkeys[n].ki.wVk = VK_LMENU;
+		n++;
+	    }
+	    if (n)
+	    {
+		(void)SetForegroundWindow(s_hwnd);
+		SendInput(n, modkeys, sizeof(INPUT));
+	    }
+	}
+
 	inputs[0].type = INPUT_KEYBOARD;
 	inputs[0].ki.wVk = vkCode;
 	if (STRICMP(event, "keyup") == 0)
+	{
 	    inputs[0].ki.dwFlags = KEYEVENTF_KEYUP;
+	    if(!isModKey)
+		unwrapMods = TRUE;
+	}
+
 	(void)SetForegroundWindow(s_hwnd);
 	SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
+	vim_free(event);
+
+	if (unwrapMods)
+	{
+	    modkeys[0].type = INPUT_KEYBOARD;
+	    modkeys[0].ki.wVk = VK_LSHIFT;
+	    modkeys[0].ki.dwFlags = KEYEVENTF_KEYUP;
+
+	    modkeys[1].type = INPUT_KEYBOARD;
+	    modkeys[1].ki.wVk = VK_LCONTROL;
+	    modkeys[1].ki.dwFlags = KEYEVENTF_KEYUP;
+
+	    modkeys[2].type = INPUT_KEYBOARD;
+	    modkeys[2].ki.wVk = VK_LMENU;
+	    modkeys[2].ki.dwFlags = KEYEVENTF_KEYUP;
+
+	    (void)SetForegroundWindow(s_hwnd);
+	    SendInput(3, modkeys, sizeof(INPUT));
+	}
     }
     else
-	semsg(_(e_invalid_argument_str), event);
-
-    vim_free(event);
-
+    {
+	if (event == NULL)
+	{
+	    semsg(_(e_missing_argument_str), "event");
+	}
+	else
+	{
+	    semsg(_(e_invalid_value_for_argument_str_str), "event", event);
+	    vim_free(event);
+	}
+	return FALSE;
+    }
     return TRUE;
 }
-#endif
+
+    int
+test_gui_w32_sendevent(char_u *event, dict_T *args)
+{
+    if (STRICMP(event, "key") == 0)
+	return test_gui_w32_sendevent_keyboard(args);
+    else if (STRICMP(event, "mouse") == 0)
+	return test_gui_w32_sendevent_mouse(args);
+    else
+    {
+	semsg(_(e_invalid_value_for_argument_str_str), "event", event);
+	return FALSE;
+    }
+}
+#endif
--- a/src/os_win32.c
+++ b/src/os_win32.c
@@ -177,6 +177,25 @@ static void gotoxy(unsigned x, unsigned 
 static void standout(void);
 static int s_cursor_visible = TRUE;
 static int did_create_conin = FALSE;
+// The 'input_record_buffer' is an internal dynamic fifo queue of MS-Windows
+// console INPUT_RECORD events that are normally read from the console input
+// buffer.  This provides an injection point for testing the low-level handling
+// of INPUT_RECORDs.
+typedef struct input_record_buffer_node_S
+{
+    INPUT_RECORD ir;
+    struct input_record_buffer_node_S *next;
+} input_record_buffer_node_T;
+typedef struct input_record_buffer_S
+{
+    input_record_buffer_node_T *head;
+    input_record_buffer_node_T *tail;
+    int length;
+} input_record_buffer_T;
+static input_record_buffer_T input_record_buffer;
+static int peek_input_record_buffer(INPUT_RECORD* irEvents, int nMaxLength);
+static int read_input_record_buffer(INPUT_RECORD* irEvents, int nMaxLength);
+static int write_input_record_buffer(INPUT_RECORD* irEvents, int nLength);
 #endif
 #ifdef FEAT_GUI_MSWIN
 static int s_dont_use_vimrun = TRUE;
@@ -224,7 +243,7 @@ static int default_console_color_fg = 0x
 static void set_console_color_rgb(void);
 static void reset_console_color_rgb(void);
 static void restore_console_color_rgb(void);
-#endif
+#endif  // !FEAT_GUI_MSWIN || VIMDLL
 
 // This flag is newly created from Windows 10
 #ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
@@ -319,6 +338,13 @@ read_console_input(
     int i;
     static INPUT_RECORD s_irPseudo;
 
+    if (s_dwMax == 0 && input_record_buffer.length > 0)
+    {
+	dwEvents = read_input_record_buffer(s_irCache, IRSIZE);
+	s_dwIndex = 0;
+	s_dwMax = dwEvents;
+    }
+
     if (nLength == -2)
 	return (s_dwMax > 0) ? TRUE : FALSE;
 
@@ -431,7 +457,7 @@ wait_for_single_object(
     return WaitForSingleObject(hHandle, dwMilliseconds);
 }
 # endif
-#endif
+#endif   // !FEAT_GUI_MSWIN || VIMDLL
 
     static void
 get_exe_name(void)
@@ -1014,7 +1040,7 @@ win32_kbd_patch_key(
 	return 1;
     }
 
-    if (pker->uChar.UnicodeChar != 0)
+    if (pker->uChar.UnicodeChar > 0 && pker->uChar.UnicodeChar < 0xfffd)
 	return 1;
 
     CLEAR_FIELD(abKeystate);
@@ -1080,7 +1106,8 @@ decode_key_event(
 
     // special cases
     if ((nModifs & CTRL) != 0 && (nModifs & ~CTRL) == 0
-					    && pker->uChar.UnicodeChar == NUL)
+					&& (pker->uChar.UnicodeChar == NUL
+					|| pker->uChar.UnicodeChar == 0xfffd))
     {
 	// Ctrl-6 is Ctrl-^
 	if (pker->wVirtualKeyCode == '6')
@@ -1168,7 +1195,113 @@ decode_key_event(
     return (*pch != NUL);
 }
 
-#endif // FEAT_GUI_MSWIN
+# if defined(FEAT_EVAL)
+    static int
+encode_key_event(dict_T *args, INPUT_RECORD *ir)
+{
+    static int s_dwMods = 0;
+
+    char_u *event = dict_get_string(args, "event", TRUE);
+    if (event && (STRICMP(event, "keydown") == 0
+					|| STRICMP(event, "keyup") == 0))
+    {
+	WORD vkCode = dict_get_number_def(args, "keycode", 0);
+	if (vkCode <= 0 || vkCode >= 0xFF)
+	{
+	    semsg(_(e_invalid_argument_nr), (long)vkCode);
+	    return FALSE;
+	}
+
+	ir->EventType = KEY_EVENT;
+	KEY_EVENT_RECORD ker;
+	ZeroMemory(&ker, sizeof(ker));
+	ker.bKeyDown = STRICMP(event, "keydown") == 0;
+	ker.wRepeatCount = 1;
+	ker.wVirtualScanCode = 0;
+	ker.dwControlKeyState = 0;
+	int mods = (int)dict_get_number(args, "modifiers");
+	// Encode the win32 console key modifiers from Vim keyboard modifiers.
+	if (mods)
+	{
+	    // If "modifiers" is explicitly set in the args, then we reset any
+	    // remembered modifer key state that may have been set from earlier
+	    // mod-key-down events, even if they are not yet unset by earlier
+	    // mod-key-up events.
+	    s_dwMods = 0;
+	    if (mods & MOD_MASK_SHIFT)
+		ker.dwControlKeyState |= SHIFT_PRESSED;
+	    if (mods & MOD_MASK_CTRL)
+		ker.dwControlKeyState |= LEFT_CTRL_PRESSED;
+	    if (mods & MOD_MASK_ALT)
+		ker.dwControlKeyState |= LEFT_ALT_PRESSED;
+	}
+
+	if (vkCode == VK_LSHIFT || vkCode == VK_RSHIFT || vkCode == VK_SHIFT)
+	{
+	    if (STRICMP(event, "keydown") == 0)
+		s_dwMods |= SHIFT_PRESSED;
+	    else
+		s_dwMods &= ~SHIFT_PRESSED;
+	}
+	else if (vkCode == VK_LCONTROL || vkCode == VK_CONTROL)
+	{
+	    if (STRICMP(event, "keydown") == 0)
+		s_dwMods |= LEFT_CTRL_PRESSED;
+	    else
+		s_dwMods &= ~LEFT_CTRL_PRESSED;
+	}
+	else if (vkCode == VK_RCONTROL)
+	{
+	    if (STRICMP(event, "keydown") == 0)
+		s_dwMods |= RIGHT_CTRL_PRESSED;
+	    else
+		s_dwMods &= ~RIGHT_CTRL_PRESSED;
+	}
+	else if (vkCode == VK_LMENU || vkCode == VK_MENU)
+	{
+	    if (STRICMP(event, "keydown") == 0)
+		s_dwMods |= LEFT_ALT_PRESSED;
+	    else
+		s_dwMods &= ~LEFT_ALT_PRESSED;
+	}
+	else if (vkCode == VK_RMENU)
+	{
+	    if (STRICMP(event, "keydown") == 0)
+		s_dwMods |= RIGHT_ALT_PRESSED;
+	    else
+		s_dwMods &= ~RIGHT_ALT_PRESSED;
+	}
+	ker.dwControlKeyState |= s_dwMods;
+	ker.wVirtualKeyCode = vkCode;
+	win32_kbd_patch_key(&ker);
+
+	for (int i = ARRAY_LENGTH(VirtKeyMap);
+	     --i >= 0 && !ker.uChar.UnicodeChar; )
+	{
+	    if (VirtKeyMap[i].wVirtKey == vkCode)
+		ker.uChar.UnicodeChar = 0xfffd;  // REPLACEMENT CHARACTER
+	}
+
+	ir->Event.KeyEvent = ker;
+	vim_free(event);
+    }
+    else
+    {
+	if (event == NULL)
+	{
+	    semsg(_(e_missing_argument_str), "event");
+	}
+	else
+	{
+	    semsg(_(e_invalid_value_for_argument_str_str), "event", event);
+	    vim_free(event);
+	}
+	return FALSE;
+    }
+    return TRUE;
+}
+# endif  // FEAT_EVAL
+#endif // !FEAT_GUI_MSWIN || VIMDLL
 
 
 /*
@@ -1179,7 +1312,7 @@ decode_key_event(
 mch_setmouse(int on UNUSED)
 {
 }
-#else
+#else  // !FEAT_GUI_MSWIN || VIMDLL
 static int g_fMouseAvail = FALSE;   // mouse present
 static int g_fMouseActive = FALSE;  // mouse enabled
 static int g_nMouseClick = -1;	    // mouse status
@@ -1234,21 +1367,21 @@ mch_bevalterm_changed(void)
 
 /*
  * Win32 console mouse scroll event handler.
- * Loosely based on the _OnMouseWheel() function in gui_w32.c
+ * Console version of the _OnMouseWheel() function in gui_w32.c
  *
  * This encodes the mouse scroll direction and keyboard modifiers into
  * g_nMouseClick, and the mouse position into g_xMouse and g_yMouse
  *
  * The direction of the scroll is decoded from two fields of the win32 console
  * mouse event record;
- *    1. The axis - vertical or horizontal flag - from dwEventFlags, and
+ *    1. The orientation - vertical or horizontal flag - from dwEventFlags
  *    2. The sign - positive or negative (aka delta flag) - from dwButtonState
  *
- * When scroll axis is HORIZONTAL
+ * When scroll orientation is HORIZONTAL
  *    -  If the high word of the dwButtonState member contains a positive
  *	 value, the wheel was rotated to the right.
  *    -  Otherwise, the wheel was rotated to the left.
- * When scroll axis is VERTICAL
+ * When scroll orientation is VERTICAL
  *    -  If the high word of the dwButtonState member contains a positive value,
  *       the wheel was rotated forward, away from the user.
  *    -  Otherwise, the wheel was rotated backward, toward the user.
@@ -1594,8 +1727,231 @@ decode_mouse_event(
     return TRUE;
 }
 
-#endif // FEAT_GUI_MSWIN
-
+# ifdef FEAT_EVAL
+    static int
+encode_mouse_event(dict_T *args, INPUT_RECORD *ir)
+{
+    int		button;
+    int		row;
+    int		col;
+    int		repeated_click;
+    int_u	mods;
+    int		move;
+
+    if (!dict_has_key(args, "row") || !dict_has_key(args, "col"))
+	return FALSE;
+
+    // Note: "move" is optional, requires fewer arguments
+    move = (int)dict_get_bool(args, "move", FALSE);
+    if (!move && (!dict_has_key(args, "button")
+	    || !dict_has_key(args, "multiclick")
+	    || !dict_has_key(args, "modifiers")))
+	return FALSE;
+
+    row = (int)dict_get_number(args, "row") - 1;
+    col = (int)dict_get_number(args, "col") - 1;
+
+    ir->EventType = MOUSE_EVENT;
+    MOUSE_EVENT_RECORD mer;
+    ZeroMemory(&mer, sizeof(mer));
+    mer.dwMousePosition.X  = col;
+    mer.dwMousePosition.Y  = row;
+
+    if (move)
+    {
+	mer.dwButtonState = 0;
+	mer.dwEventFlags = MOUSE_MOVED;
+    }
+    else
+    {
+	button = (int)dict_get_number(args, "button");
+	repeated_click = (int)dict_get_number(args, "multiclick");
+	mods = (int)dict_get_number(args, "modifiers");
+	// Reset the scroll values to known values.
+	// XXX: Remove this when/if the scroll step is made configurable.
+	mouse_set_hor_scroll_step(6);
+	mouse_set_vert_scroll_step(3);
+
+	switch (button)
+	{
+	    case MOUSE_LEFT:
+		mer.dwButtonState = FROM_LEFT_1ST_BUTTON_PRESSED;
+		mer.dwEventFlags = repeated_click == 1 ? DOUBLE_CLICK : 0;
+		break;
+	    case MOUSE_MIDDLE:
+		mer.dwButtonState = FROM_LEFT_2ND_BUTTON_PRESSED;
+		mer.dwEventFlags = repeated_click == 1 ? DOUBLE_CLICK : 0;
+		break;
+	    case MOUSE_RIGHT:
+		mer.dwButtonState = RIGHTMOST_BUTTON_PRESSED;
+		mer.dwEventFlags = repeated_click == 1 ? DOUBLE_CLICK : 0;
+		break;
+	    case MOUSE_RELEASE:
+		// umm?  Assume Left Release?
+		mer.dwEventFlags = 0;
+
+	    case MOUSE_MOVE:
+		mer.dwButtonState = 0;
+		mer.dwEventFlags = MOUSE_MOVED;
+		break;
+	    case MOUSE_X1:
+		mer.dwButtonState = FROM_LEFT_3RD_BUTTON_PRESSED;
+		break;
+	    case MOUSE_X2:
+		mer.dwButtonState = FROM_LEFT_4TH_BUTTON_PRESSED;
+		break;
+	    case MOUSE_4:  // KE_MOUSEDOWN;
+		mer.dwButtonState = -1;
+		mer.dwEventFlags = MOUSE_WHEELED;
+		break;
+	    case MOUSE_5:  // KE_MOUSEUP;
+		mer.dwButtonState = +1;
+		mer.dwEventFlags = MOUSE_WHEELED;
+		break;
+	    case MOUSE_6:  // KE_MOUSELEFT;
+		mer.dwButtonState = -1;
+		mer.dwEventFlags = MOUSE_HWHEELED;
+		break;
+	    case MOUSE_7:  // KE_MOUSERIGHT;
+		mer.dwButtonState = +1;
+		mer.dwEventFlags = MOUSE_HWHEELED;
+		break;
+	    default:
+		semsg(_(e_invalid_argument_str), "button");
+		return FALSE;
+	}
+    }
+
+    mer.dwControlKeyState = 0;
+    if (mods != 0)
+    {
+	// Encode the win32 console key modifiers from Vim MOUSE modifiers.
+	if (mods & MOUSE_SHIFT)
+	    mer.dwControlKeyState |= SHIFT_PRESSED;
+	if (mods & MOUSE_CTRL)
+	    mer.dwControlKeyState |= LEFT_CTRL_PRESSED;
+	if (mods & MOUSE_ALT)
+	    mer.dwControlKeyState |= LEFT_ALT_PRESSED;
+    }
+    ir->Event.MouseEvent = mer;
+    return TRUE;
+}
+# endif  // FEAT_EVAL
+
+    static int
+write_input_record_buffer(INPUT_RECORD* irEvents, int nLength)
+{
+    int nCount = 0;
+    while (nCount < nLength)
+    {
+	input_record_buffer.length++;
+	input_record_buffer_node_T *event_node =
+				    malloc(sizeof(input_record_buffer_node_T));
+	event_node->ir = irEvents[nCount++];
+	event_node->next = NULL;
+	if (input_record_buffer.tail == NULL)
+	{
+	    input_record_buffer.head = event_node;
+	    input_record_buffer.tail = event_node;
+	}
+	else
+	{
+	    input_record_buffer.tail->next = event_node;
+	    input_record_buffer.tail = input_record_buffer.tail->next;
+	}
+    }
+    return nCount;
+}
+
+    static int
+read_input_record_buffer(INPUT_RECORD* irEvents, int nMaxLength)
+{
+    int nCount = 0;
+    while (nCount < nMaxLength && input_record_buffer.head != NULL)
+    {
+	input_record_buffer.length--;
+	input_record_buffer_node_T *pop_head = input_record_buffer.head;
+	irEvents[nCount++] = pop_head->ir;
+	input_record_buffer.head = pop_head->next;
+	vim_free(pop_head);
+	if (input_record_buffer.length == 0)
+	    input_record_buffer.tail = NULL;
+    }
+    return nCount;
+}
+    static int
+peek_input_record_buffer(INPUT_RECORD* irEvents, int nMaxLength)
+{
+    int nCount = 0;
+    input_record_buffer_node_T *temp =  input_record_buffer.head;
+    while (nCount < nMaxLength && temp != NULL)
+    {
+	irEvents[nCount++] = temp->ir;
+	temp = temp->next;
+    }
+    return nCount;
+}
+#endif // !FEAT_GUI_MSWIN || VIMDLL
+
+#ifdef FEAT_EVAL
+/*
+ * The 'test_mswin_event' function is for testing Vim's low-level handling of
+ * user input events.  ie, this manages the encoding of INPUT_RECORD events
+ * so that we have a way to test how Vim decodes INPUT_RECORD events in Windows
+ * consoles.
+ *
+ * The 'test_mswin_event' function is based on 'test_gui_event'.  In fact, when
+ * the Windows GUI is running, the arguments; 'event' and 'args', are the same.
+ * So, it acts as an alias for 'test_gui_event' for the Windows GUI.
+ *
+ * When the Windows console is running, the arguments; 'event' and 'args', are
+ * a subset of what 'test_gui_event' handles, ie, only "key" and "mouse"
+ * events are encoded as INPUT_RECORD events.
+ *
+ * Note: INPUT_RECORDs are only used by the Windows console, not the GUI.  The
+ * GUI sends MSG structs instead.
+ */
+    int
+test_mswin_event(char_u *event, dict_T *args)
+{
+    int lpEventsWritten = 0;
+
+# if defined(VIMDLL) || defined(FEAT_GUI_MSWIN)
+    if (gui.in_use)
+	return test_gui_w32_sendevent(event, args);
+# endif
+
+# if defined(VIMDLL) || !defined(FEAT_GUI_MSWIN)
+
+// Currently implemented event record types are; KEY_EVENT and MOUSE_EVENT
+// Potentially could also implement: FOCUS_EVENT and WINDOW_BUFFER_SIZE_EVENT
+// Maybe also:  MENU_EVENT
+
+    INPUT_RECORD ir;
+    BOOL input_encoded = FALSE;
+    if (STRCMP(event, "key") == 0)
+	input_encoded = encode_key_event(args, &ir);
+    else if (STRCMP(event, "mouse") == 0)
+	input_encoded = encode_mouse_event(args, &ir);
+    else
+    {
+	semsg(_(e_invalid_value_for_argument_str_str), "event", event);
+	return FALSE;
+    }
+
+    // Ideally, WriteConsoleInput would be used to inject these low-level
+    // events.  But, this doesnt work well in the CI test environment.  So
+    // implementing an input_record_buffer instead.
+    if (input_encoded)
+	lpEventsWritten = write_input_record_buffer(&ir, 1);
+
+    if (STRCMP(event, "mouse") == 0)
+	exec_normal(TRUE, TRUE, TRUE);
+
+# endif
+    return lpEventsWritten;
+}
+#endif // FEAT_EVAL
 
 #ifdef MCH_CURSOR_SHAPE
 /*
--- a/src/proto/gui_w32.pro
+++ b/src/proto/gui_w32.pro
@@ -96,5 +96,5 @@ void gui_mch_post_balloon(BalloonEval *b
 BalloonEval *gui_mch_create_beval_area(void *target, char_u *mesg, void (*mesgCB)(BalloonEval *, int), void *clientData);
 void gui_mch_destroy_beval_area(BalloonEval *beval);
 void netbeans_draw_multisign_indicator(int row);
-int test_gui_w32_sendevent(dict_T *args);
+int test_gui_w32_sendevent(char_u *event, dict_T *args);
 /* vim: set ft=c : */
--- a/src/proto/os_win32.pro
+++ b/src/proto/os_win32.pro
@@ -9,6 +9,7 @@ void dyn_libintl_end(void);
 void PlatformId(void);
 void mch_setmouse(int on);
 void mch_bevalterm_changed(void);
+int test_mswin_event(char_u *event, dict_T *args);
 void mch_update_cursor(void);
 int mch_char_avail(void);
 int mch_check_messages(void);
--- a/src/proto/testing.pro
+++ b/src/proto/testing.pro
@@ -33,6 +33,7 @@ void f_test_null_string(typval_T *argvar
 void f_test_unknown(typval_T *argvars, typval_T *rettv);
 void f_test_void(typval_T *argvars, typval_T *rettv);
 void f_test_setmouse(typval_T *argvars, typval_T *rettv);
+void f_test_mswin_event(typval_T *argvars, typval_T *rettv);
 void f_test_gui_event(typval_T *argvars, typval_T *rettv);
 void f_test_settime(typval_T *argvars, typval_T *rettv);
 /* vim: set ft=c : */
--- a/src/term.c
+++ b/src/term.c
@@ -827,7 +827,7 @@ static tcap_entry_T builtin_pcansi[] = {
 };
 
 /*
- * These codes are valid for the Win32 Console .  The entries that start with
+ * These codes are valid for the Win32 Console.  The entries that start with
  * ESC | are translated into console calls in os_win32.c.  The function keys
  * are also translated in os_win32.c.
  */
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -210,6 +210,7 @@ NEW_TESTS = \
 	test_modeless \
 	test_modeline \
 	test_move \
+	test_mswin_event \
 	test_mzscheme \
 	test_nested_function \
 	test_netbeans \
@@ -454,6 +455,7 @@ NEW_TESTS_RES = \
 	test_mksession.res \
 	test_modeless.res \
 	test_modeline.res \
+	test_mswin_event.res \
 	test_mzscheme.res \
 	test_nested_function.res \
 	test_netbeans.res \
--- a/src/testdir/mouse.vim
+++ b/src/testdir/mouse.vim
@@ -20,6 +20,27 @@ else
   let g:Ttymouse_netterm = []
 endif
 
+" Vim Mouse Codes.
+" Used by the GUI and by MS-Windows Consoles.
+" Keep these in sync with vim.h
+let s:MOUSE_CODE = {
+  \ 'BTN_LEFT'    :  0x00,
+  \ 'BTN_MIDDLE'  :  0x01,
+  \ 'BTN_RIGHT'   :  0x02,
+  \ 'BTN_RELEASE' :  0x03,
+  \ 'BTN_X1'      : 0x300,
+  \ 'BTN_X2'      : 0x400,
+  \ 'SCRL_DOWN'   : 0x100,
+  \ 'SCRL_UP'     : 0x200,
+  \ 'SCRL_LEFT'   : 0x500,
+  \ 'SCRL_RIGHT'  : 0x600,
+  \ 'MOVE'        : 0x700,
+  \ 'MOD_SHIFT'   :  0x04,
+  \ 'MOD_ALT'     :  0x08,
+  \ 'MOD_CTRL'    :  0x10,
+  \ }
+
+
 " Helper function to emit a terminal escape code.
 func TerminalEscapeCode(code, row, col, m)
   if &ttymouse ==# 'xterm2'
@@ -47,6 +68,31 @@ func NettermEscapeCode(row, col)
     return printf("\<Esc>}%d,%d\r", a:row, a:col)
 endfunc
 
+" Send low level mouse event to MS-Windows consoles or GUI
+func MSWinMouseEvent(button, row, col, move, multiclick, modifiers)
+    let args = { }
+    let args.button = a:button
+    " Scroll directions are inverted in the GUI, no idea why.
+    if has('gui_running')
+      if a:button == s:MOUSE_CODE.SCRL_UP
+        let args.button = s:MOUSE_CODE.SCRL_DOWN
+      elseif a:button == s:MOUSE_CODE.SCRL_DOWN
+        let args.button = s:MOUSE_CODE.SCRL_UP
+      elseif a:button == s:MOUSE_CODE.SCRL_LEFT
+        let args.button = s:MOUSE_CODE.SCRL_RIGHT
+      elseif a:button == s:MOUSE_CODE.SCRL_RIGHT
+        let args.button = s:MOUSE_CODE.SCRL_LEFT
+      endif
+    endif
+    let args.row = a:row
+    let args.col = a:col
+    let args.move = a:move
+    let args.multiclick = a:multiclick
+    let args.modifiers = a:modifiers
+    call test_mswin_event("mouse", args)
+    unlet args
+endfunc
+
 func MouseLeftClickCode(row, col)
   if &ttymouse ==# 'dec'
     return DecEscapeCode(2, 4, a:row, a:col)
@@ -58,7 +104,11 @@ func MouseLeftClickCode(row, col)
 endfunc
 
 func MouseLeftClick(row, col)
-  call feedkeys(MouseLeftClickCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_LEFT, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseLeftClickCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseMiddleClickCode(row, col)
@@ -70,7 +120,11 @@ func MouseMiddleClickCode(row, col)
 endfunc
 
 func MouseMiddleClick(row, col)
-  call feedkeys(MouseMiddleClickCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_MIDDLE, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseMiddleClickCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseRightClickCode(row, col)
@@ -82,7 +136,11 @@ func MouseRightClickCode(row, col)
 endfunc
 
 func MouseRightClick(row, col)
-  call feedkeys(MouseRightClickCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RIGHT, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseRightClickCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseCtrlLeftClickCode(row, col)
@@ -91,7 +149,12 @@ func MouseCtrlLeftClickCode(row, col)
 endfunc
 
 func MouseCtrlLeftClick(row, col)
-  call feedkeys(MouseCtrlLeftClickCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_LEFT, a:row, a:col, 0, 0,
+                                                         \ s:MOUSE_CODE.MOD_CTRL)
+  else
+    call feedkeys(MouseCtrlLeftClickCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseCtrlRightClickCode(row, col)
@@ -100,7 +163,12 @@ func MouseCtrlRightClickCode(row, col)
 endfunc
 
 func MouseCtrlRightClick(row, col)
-  call feedkeys(MouseCtrlRightClickCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RIGHT, a:row, a:col, 0, 0,
+                                                       \ s:MOUSE_CODE.MOD_CTRL)
+  else
+    call feedkeys(MouseCtrlRightClickCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseAltLeftClickCode(row, col)
@@ -109,7 +177,12 @@ func MouseAltLeftClickCode(row, col)
 endfunc
 
 func MouseAltLeftClick(row, col)
-  call feedkeys(MouseAltLeftClickCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_LEFT, a:row, a:col, 0, 0,
+                                                       \ s:MOUSE_CODE.MOD_ALT)
+  else
+    call feedkeys(MouseAltLeftClickCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseAltRightClickCode(row, col)
@@ -118,7 +191,12 @@ func MouseAltRightClickCode(row, col)
 endfunc
 
 func MouseAltRightClick(row, col)
-  call feedkeys(MouseAltRightClickCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RIGHT, a:row, a:col, 0, 0,
+                                                       \ s:MOUSE_CODE.MOD_ALT)
+  else
+    call feedkeys(MouseAltRightClickCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseLeftReleaseCode(row, col)
@@ -132,7 +210,11 @@ func MouseLeftReleaseCode(row, col)
 endfunc
 
 func MouseLeftRelease(row, col)
-  call feedkeys(MouseLeftReleaseCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RELEASE, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseLeftReleaseCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseMiddleReleaseCode(row, col)
@@ -144,7 +226,11 @@ func MouseMiddleReleaseCode(row, col)
 endfunc
 
 func MouseMiddleRelease(row, col)
-  call feedkeys(MouseMiddleReleaseCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RELEASE, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseMiddleReleaseCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseRightReleaseCode(row, col)
@@ -156,7 +242,11 @@ func MouseRightReleaseCode(row, col)
 endfunc
 
 func MouseRightRelease(row, col)
-  call feedkeys(MouseRightReleaseCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RELEASE, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseRightReleaseCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseLeftDragCode(row, col)
@@ -168,7 +258,11 @@ func MouseLeftDragCode(row, col)
 endfunc
 
 func MouseLeftDrag(row, col)
-  call feedkeys(MouseLeftDragCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_LEFT, a:row, a:col, 1, 0, 0)
+  else
+    call feedkeys(MouseLeftDragCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseWheelUpCode(row, col)
@@ -176,7 +270,11 @@ func MouseWheelUpCode(row, col)
 endfunc
 
 func MouseWheelUp(row, col)
-  call feedkeys(MouseWheelUpCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_UP, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseWheelUpCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseWheelDownCode(row, col)
@@ -184,7 +282,11 @@ func MouseWheelDownCode(row, col)
 endfunc
 
 func MouseWheelDown(row, col)
-  call feedkeys(MouseWheelDownCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_DOWN, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseWheelDownCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseWheelLeftCode(row, col)
@@ -192,7 +294,11 @@ func MouseWheelLeftCode(row, col)
 endfunc
 
 func MouseWheelLeft(row, col)
-  call feedkeys(MouseWheelLeftCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_LEFT, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseWheelLeftCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 func MouseWheelRightCode(row, col)
@@ -200,7 +306,67 @@ func MouseWheelRightCode(row, col)
 endfunc
 
 func MouseWheelRight(row, col)
-  call feedkeys(MouseWheelRightCode(a:row, a:col), 'Lx!')
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_RIGHT, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseWheelRightCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseShiftWheelUpCode(row, col)
+  " todo feed shift mod.
+  return TerminalEscapeCode(0x40, a:row, a:col, 'M')
+endfunc
+
+func MouseShiftWheelUp(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_UP, a:row, a:col, 0, 0,
+                                                      \ s:MOUSE_CODE.MOD_SHIFT)
+  else
+    call feedkeys(MouseShiftWheelUpCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseShiftWheelDownCode(row, col)
+  " todo feed shift mod.
+  return TerminalEscapeCode(0x41, a:row, a:col, 'M')
+endfunc
+
+func MouseShiftWheelDown(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_DOWN, a:row, a:col, 0, 0,
+                                                      \ s:MOUSE_CODE.MOD_SHIFT)
+  else
+    call feedkeys(MouseShiftWheelDownCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseShiftWheelLeftCode(row, col)
+  " todo feed shift mod.
+  return TerminalEscapeCode(0x42, a:row, a:col, 'M')
+endfunc
+
+func MouseShiftWheelLeft(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_LEFT, a:row, a:col, 0, 0,
+                                                      \ s:MOUSE_CODE.MOD_SHIFT)
+  else
+    call feedkeys(MouseShiftWheelLeftCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseShiftWheelRightCode(row, col)
+	" todo feed shift mod.
+  return TerminalEscapeCode(0x43, a:row, a:col, 'M')
+endfunc
+
+func MouseShiftWheelRight(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_RIGHT, a:row, a:col, 0, 0,
+                                                      \ s:MOUSE_CODE.MOD_SHIFT)
+  else
+    call feedkeys(MouseShiftWheelRightCode(a:row, a:col), 'Lx!')
+  endif
 endfunc
 
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/testdir/test_gui.vim
+++ b/src/testdir/test_gui.vim
@@ -1281,7 +1281,7 @@ func Test_gui_mouse_move_event()
     let g:eventlist = g:eventlist[1 : ]
   endif
 
-  call assert_equal([#{row: 4, col: 31}, #{row: 11, col: 31}], g:eventlist)
+  call assert_equal([#{row: 3, col: 30}, #{row: 10, col: 30}], g:eventlist)
 
   " wiggle the mouse around within a screen cell, shouldn't trigger events
   call extend(args, #{cell: v:false})
@@ -1638,10 +1638,10 @@ endfunc
 " Test for sending low level key presses
 func SendKeys(keylist)
   for k in a:keylist
-    call test_gui_event("sendevent", #{event: "keydown", keycode: k})
+    call test_gui_event("key", #{event: "keydown", keycode: k})
   endfor
   for k in reverse(a:keylist)
-    call test_gui_event("sendevent", #{event: "keyup", keycode: k})
+    call test_gui_event("key", #{event: "keyup", keycode: k})
   endfor
 endfunc
 
new file mode 100644
--- /dev/null
+++ b/src/testdir/test_mswin_event.vim
@@ -0,0 +1,651 @@
+" Test MS-Windows console event handling.
+
+source check.vim
+CheckMSWindows
+" The mswin events should also work in gui
+
+source mouse.vim
+
+" Helper function for sending a sequence of low level key presses
+" The modifer key(s) can be included as normal key presses in the sequence
+func SendKeys(keylist)
+  for k in a:keylist
+    call test_mswin_event("key", #{event: "keydown", keycode: k})
+  endfor
+  for k in reverse(copy(a:keylist))
+    call test_mswin_event("key", #{event: "keyup", keycode: k})
+  endfor
+endfunc
+
+" Send an individual key press
+" the modifers for the key press can be specified in the modifiers arg.
+func SendKey(key, modifiers)
+  let args = { }
+  let args.keycode = a:key
+  let args.modifiers = a:modifiers
+  let args.event = "keydown"
+  call test_mswin_event("key", args)
+  let args.event = "keyup"
+  call test_mswin_event("key", args)
+  unlet args
+endfunc
+
+" Test MS-Windows console key events
+func Test_mswin_key_event()
+  CheckMSWindows
+  new
+
+  " flush out any garbage left in the buffer
+  while getchar(0)
+  endwhile
+
+  let VK = #{
+	\ SPACE	     : 0x20,
+	\ SHIFT      : 0x10,
+	\ LSHIFT     : 0xA0,
+	\ RSHIFT     : 0xA1,
+	\ CONTROL    : 0x11,
+	\ LCONTROL   : 0xA2,
+	\ RCONTROL   : 0xA3,
+	\ MENU	     : 0x12,
+	\ ALT	     : 0x12,
+	\ LMENU      : 0xA4,
+	\ LALT	     : 0xA4,
+	\ RMENU      : 0xA5,
+	\ RALT	     : 0xA5,
+	\ OEM_1      : 0xBA,
+	\ OEM_2      : 0xBF,
+	\ OEM_3      : 0xC0,
+	\ OEM_4      : 0xDB,
+	\ OEM_5      : 0xDC,
+	\ OEM_6      : 0xDD,
+	\ OEM_7      : 0xDE,
+	\ OEM_PLUS   : 0xBB,
+	\ OEM_COMMA  : 0xBC,
+	\ OEM_MINUS  : 0xBD,
+	\ OEM_PERIOD : 0xBE,
+	\ PRIOR      : 0x21,
+	\ NEXT	     : 0x22,
+	\ END	     : 0x23,
+	\ HOME	     : 0x24,
+	\ LEFT	     : 0x25,
+	\ UP	     : 0x26,
+	\ RIGHT      : 0x27,
+	\ DOWN	     : 0x28,
+	\ KEY_0      : 0x30,
+	\ KEY_1      : 0x31,
+	\ KEY_2      : 0x32,
+	\ KEY_3      : 0x33,
+	\ KEY_4      : 0x34,
+	\ KEY_5      : 0x35,
+	\ KEY_6      : 0x36,
+	\ KEY_7      : 0x37,
+	\ KEY_8      : 0x38,
+	\ KEY_9      : 0x39,
+	\ NUMPAD0    : 0x60,
+	\ NUMPAD1    : 0x61,
+	\ NUMPAD2    : 0x62,
+	\ NUMPAD3    : 0x63,
+	\ NUMPAD4    : 0x64,
+	\ NUMPAD5    : 0x65,
+	\ NUMPAD6    : 0x66,
+	\ NUMPAD7    : 0x67,
+	\ NUMPAD8    : 0x68,
+	\ NUMPAD9    : 0x69,
+	\ MULTIPLY   : 0x6A,
+	\ ADD	     : 0x6B,
+	\ SUBTRACT   : 0x6D,
+	\ F1	     : 0x70,
+	\ F2	     : 0x71,
+	\ F3	     : 0x72,
+	\ F4	     : 0x73,
+	\ F5	     : 0x74,
+	\ F6	     : 0x75,
+	\ F7	     : 0x76,
+	\ F8	     : 0x77,
+	\ F9	     : 0x78,
+	\ F10	     : 0x79,
+	\ F11	     : 0x7A,
+	\ F12	     : 0x7B,
+	\ KEY_A      : 0x41,
+	\ KEY_B      : 0x42,
+	\ KEY_C      : 0x43,
+	\ KEY_D      : 0x44,
+	\ KEY_E      : 0x45,
+	\ KEY_F      : 0x46,
+	\ KEY_G      : 0x47,
+	\ KEY_H      : 0x48,
+	\ KEY_I      : 0x49,
+	\ KEY_J      : 0x4A,
+	\ KEY_K      : 0x4B,
+	\ KEY_L      : 0x4C,
+	\ KEY_M      : 0x4D,
+	\ KEY_N      : 0x4E,
+	\ KEY_O      : 0x4F,
+	\ KEY_P      : 0x50,
+	\ KEY_Q      : 0x51,
+	\ KEY_R      : 0x52,
+	\ KEY_S      : 0x53,
+	\ KEY_T      : 0x54,
+	\ KEY_U      : 0x55,
+	\ KEY_V      : 0x56,
+	\ KEY_W      : 0x57,
+	\ KEY_X      : 0x58,
+	\ KEY_Y      : 0x59,
+	\ KEY_Z      : 0x5A	
+	\ }
+
+  let vim_MOD_MASK_SHIFT = 0x02
+  let vim_MOD_MASK_CTRL  = 0x04
+  let vim_MOD_MASK_ALT   = 0x08
+  
+  let vim_key_modifiers = [
+    \ ["",       0,   []],
+    \ ["S-",     2,   [VK.SHIFT]],
+    \ ["C-",     4,   [VK.CONTROL]],
+    \ ["C-S-",   6,   [VK.CONTROL, VK.SHIFT]],
+    \ ["A-",     8,   [VK.MENU]],
+    \ ["A-S-",   10,  [VK.MENU, VK.SHIFT]],
+    \ ["A-C-",   12,  [VK.MENU, VK.CONTROL]],
+    \ ["A-C-S-", 14,  [VK.MENU, VK.CONTROL, VK.SHIFT]],
+    \]
+
+  " Some punctuation characters
+  " Assuming Standard US PC Keyboard layout
+  let test_punctuation_keys = [
+	\ [[VK.SPACE], ' '],
+	\ [[VK.OEM_1], ';'],
+	\ [[VK.OEM_2], '/'],
+	\ [[VK.OEM_3], '`'],
+	\ [[VK.OEM_4], '['],
+	\ [[VK.OEM_5], '\'],
+	\ [[VK.OEM_6], ']'],
+	\ [[VK.OEM_7], ''''],
+	\ [[VK.OEM_PLUS], '='],
+	\ [[VK.OEM_COMMA], ','],
+	\ [[VK.OEM_MINUS], '-'],
+	\ [[VK.OEM_PERIOD], '.'],
+	\ [[VK.SHIFT, VK.OEM_1], ':'],
+	\ [[VK.SHIFT, VK.OEM_2], '?'],
+	\ [[VK.SHIFT, VK.OEM_3], '~'],
+	\ [[VK.SHIFT, VK.OEM_4], '{'],
+	\ [[VK.SHIFT, VK.OEM_5], '|'],
+	\ [[VK.SHIFT, VK.OEM_6], '}'],
+	\ [[VK.SHIFT, VK.OEM_7], '"'],
+	\ [[VK.SHIFT, VK.OEM_PLUS], '+'],
+	\ [[VK.SHIFT, VK.OEM_COMMA], '<'],
+	\ [[VK.SHIFT, VK.OEM_MINUS], '_'],
+	\ [[VK.SHIFT, VK.OEM_PERIOD], '>'],
+	\ [[VK.SHIFT, VK.KEY_1], '!'],
+	\ [[VK.SHIFT, VK.KEY_2], '@'],
+	\ [[VK.SHIFT, VK.KEY_3], '#'],
+	\ [[VK.SHIFT, VK.KEY_4], '$'],
+	\ [[VK.SHIFT, VK.KEY_5], '%'],
+	\ [[VK.SHIFT, VK.KEY_6], '^'],
+	\ [[VK.SHIFT, VK.KEY_7], '&'],
+	\ [[VK.SHIFT, VK.KEY_8], '*'],
+	\ [[VK.SHIFT, VK.KEY_9], '('],
+	\ [[VK.SHIFT, VK.KEY_0], ')'],
+	\ [[VK.LSHIFT, VK.KEY_9], '('],
+	\ [[VK.RSHIFT, VK.KEY_0], ')']
+	\ ]
+
+  for [kcodes, kstr] in test_punctuation_keys
+    call SendKeys(kcodes)
+    let ch = getcharstr(0)
+    call assert_equal($"{kstr}", $"{ch}")
+    let mod_mask = getcharmod()
+    " the mod_mask is zero when no modifiers are used
+    " and when the virtual termcap maps shift the character
+    call assert_equal(0, mod_mask, $"key = {kstr}")
+  endfor
+  
+  " flush out any garbage left in the buffer
+  while getchar(0)
+  endwhile
+
+  for [kcodes, kstr] in test_punctuation_keys
+    let modifiers = 0
+    let key = kcodes[0]
+
+    for key in kcodes
+      if index([VK.SHIFT, VK.LSHIFT, VK.RSHIFT], key) >= 0
+        let modifiers = modifiers + vim_MOD_MASK_SHIFT
+      endif
+      if index([VK.CONTROL, VK.LCONTROL, VK.RCONTROL], key) >= 0
+        let modifiers = modifiers + vim_MOD_MASK_CTRL
+      endif
+      if index([VK.ALT, VK.LALT, VK.RALT], key) >= 0
+        let modifiers = modifiers + vim_MOD_MASK_ALT
+      endif
+    endfor
+
+    call SendKey(key, modifiers)
+    let ch = getcharstr(0)
+    call assert_equal($"{kstr}", $"{ch}")
+    let mod_mask = getcharmod()
+    " workaround for the virtual termcap maps changing the character instead
+    " of sending Shift
+    if index([VK.SHIFT, VK.LSHIFT, VK.RSHIFT], kcodes[0]) >= 0
+      let modifiers = modifiers - vim_MOD_MASK_SHIFT
+    endif
+    call assert_equal(modifiers, mod_mask, $"key = {kstr}")
+  endfor
+
+  " flush out any garbage left in the buffer
+  while getchar(0)
+  endwhile
+
+" Test keyboard codes for digits
+" (0x30 - 0x39) : VK_0 - VK_9 are the same as ASCII '0' - '9'
+  for kc in range(48, 57)
+    call SendKeys([kc])
+    let ch = getcharstr(0)
+    call assert_equal(nr2char(kc), ch)
+    call SendKey(kc, 0)
+    let ch = getcharstr(0)
+    call assert_equal(nr2char(kc), ch)
+  endfor
+
+" Test keyboard codes for Alt-0 to Alt-9
+" Expect +128 from the digit char codes
+  for modkey in [VK.ALT, VK.LALT, VK.RALT]
+    for kc in range(48, 57)
+      call SendKeys([modkey, kc])
+      let ch = getchar(0)
+      call assert_equal(kc+128, ch)
+      call SendKey(kc, vim_MOD_MASK_ALT)
+      let ch = getchar(0)
+      call assert_equal(kc+128, ch)
+    endfor
+  endfor
+
+" Test for lowercase 'a' to 'z', VK codes 65(0x41) - 90(0x5A)
+" Note: VK_A-VK_Z virtual key codes coincide with uppercase ASCII codes A-Z.
+" eg VK_A is 65, and the ASCII character code for uppercase 'A' is also 65.
+" Caution: these are interpreted as lowercase when Shift is NOT pressed. 
+" eg, sending VK_A (65) 'A' Key code without shift modifier, will produce ASCII
+" char 'a' (91) as the output.  The ASCII codes for the lowercase letters are
+" numbered 32 higher than their uppercase versions.
+  for kc in range(65, 90)
+    call SendKeys([kc])
+    let ch = getcharstr(0)
+    call assert_equal(nr2char(kc + 32), ch)
+    call SendKey(kc, 0)
+    let ch = getcharstr(0)
+    call assert_equal(nr2char(kc + 32), ch)
+  endfor
+
+"  Test for Uppercase 'A' - 'Z' keys
+"  ie. with VK_SHIFT, expect the keycode = character code.
+  for kc in range(65, 90)
+    call SendKeys([VK.SHIFT, kc])
+    let ch = getcharstr(0)
+    call assert_equal(nr2char(kc), ch)
+    call SendKey(kc, vim_MOD_MASK_SHIFT)
+    let ch = getcharstr(0)
+    call assert_equal(nr2char(kc), ch)
+  endfor
+
+  " Test for <Ctrl-A> to <Ctrl-Z> keys
+ "  Same as for lowercase, except with Ctrl Key
+ "  Expect the unicode characters 0x01 to 0x1A
+   for modkey in [VK.CONTROL, VK.LCONTROL, VK.RCONTROL]
+    for kc in range(65, 90)
+      call SendKeys([modkey, kc])
+      let ch = getcharstr(0)
+      call assert_equal(nr2char(kc - 64), ch)
+      call SendKey(kc, vim_MOD_MASK_CTRL)
+      let ch = getcharstr(0)
+      call assert_equal(nr2char(kc - 64), ch)
+    endfor
+  endfor
+
+  if !has("gui_running")
+  " Test for <Alt-A> to <Alt-Z> keys
+  "  Expect the unicode characters 0xE1 to 0xFA
+  "  ie. 160 higher than the lowercase equivalent
+    for kc in range(65, 90)
+      call SendKeys([VK.LMENU, kc])
+      let ch = getchar(0)
+      call assert_equal(kc+160, ch)
+      call SendKey(kc, vim_MOD_MASK_ALT)
+      let ch = getchar(0)
+      call assert_equal(kc+160, ch)
+    endfor
+  endif
+
+  if !has("gui_running")
+    " Test for Function Keys 'F1' to 'F12'
+    for n in range(1, 12)
+      let kstr = $"F{n}"
+      let keycode = eval('"\<' .. kstr .. '>"')
+      call SendKeys([111+n])
+      let ch = getcharstr(0)
+      call assert_equal(keycode, $"{ch}", $"key = <{kstr}>")
+    endfor
+  endif
+
+  bw!
+endfunc
+
+"  Test MS-Windows console mouse events
+func Test_mswin_mouse_event()
+  CheckMSWindows
+  new
+
+  set mousemodel=extend
+  call test_override('no_query_mouse', 1)
+  call WaitForResponses()
+
+  let msg = ''
+
+  call setline(1, ['one two three', 'four five six'])
+
+  " Test mouse movement
+  " by default, no mouse move events are generated
+  " this setting enables it to generate move events
+  set mousemev
+
+  if !has('gui_running')
+    " console version needs a button pressed,
+    " otherwise it ignores mouse movements.
+    call MouseLeftClick(2, 3)
+  endif
+  call MSWinMouseEvent(0x700, 8, 13, 0, 0, 0)
+  if has('gui_running')
+    call feedkeys("\<Esc>", 'Lx!')
+  endif
+  let pos = getmousepos()
+  call assert_equal(8, pos.screenrow)
+  call assert_equal(13, pos.screencol)
+
+  if !has('gui_running')
+    call MouseLeftClick(2, 3)
+    call MSWinMouseEvent(0x700, 6, 4, 1, 0, 0)
+    let pos = getmousepos()
+    call assert_equal(6, pos.screenrow)
+    call assert_equal(4, pos.screencol)
+  endif
+
+  " test cells vs pixels
+  if has('gui_running')
+    let args = { }
+    let args.row = 9
+    let args.col = 7
+    let args.move = 1
+    let args.cell = 1
+    call test_mswin_event("mouse", args)
+    call feedkeys("\<Esc>", 'Lx!')
+    let pos = getmousepos()
+    call assert_equal(9, pos.screenrow)
+    call assert_equal(7, pos.screencol)
+
+    let args.cell = 0
+    call test_mswin_event("mouse", args)
+    call feedkeys("\<Esc>", 'Lx!')
+    let pos = getmousepos()
+    call assert_equal(1, pos.screenrow)
+    call assert_equal(1, pos.screencol)
+
+    unlet args
+  endif
+
+  " finish testing mouse movement
+  set mousemev&
+
+  " place the cursor using left click and release in normal mode
+  call MouseLeftClick(2, 4)
+  call MouseLeftRelease(2, 4)
+  if has('gui_running')
+    call feedkeys("\<Esc>", 'Lx!')
+  endif
+  call assert_equal([0, 2, 4, 0], getpos('.'))
+
+  " select and yank a word
+  let @" = ''
+  call MouseLeftClick(1, 9)
+  let args = #{button: 0, row: 1, col: 9, multiclick: 1, modifiers: 0}
+  call test_mswin_event('mouse', args)
+  call MouseLeftRelease(1, 9)
+  call feedkeys("y", 'Lx!')
+  call assert_equal('three', @")
+
+  " create visual selection using right click
+  let @" = ''
+
+  call MouseLeftClick(2 ,6)
+  call MouseLeftRelease(2, 6)
+  call MouseRightClick(2, 13)
+  call MouseRightRelease(2, 13)
+  call feedkeys("y", 'Lx!')
+  call assert_equal('five six', @")
+
+  " paste using middle mouse button
+  let @* = 'abc '
+  call feedkeys('""', 'Lx!')
+  call MouseMiddleClick(1, 9)
+  call MouseMiddleRelease(1, 9)
+  if has('gui_running')
+    call feedkeys("\<Esc>", 'Lx!')
+  endif
+  call assert_equal(['one two abc three', 'four five six'], getline(1, '$'))
+
+  " test mouse scrolling (aka touchpad scrolling.)
+  %d _
+  set scrolloff=0
+  call setline(1, range(1, 100))
+
+  " Scroll Down
+  call MouseWheelDown(2, 1)
+  call MouseWheelDown(2, 1)
+  call MouseWheelDown(2, 1)
+  call feedkeys("H", 'Lx!')
+  call assert_equal(10, line('.'))
+
+  " Scroll Up
+  call MouseWheelUp(2, 1)
+  call MouseWheelUp(2, 1)
+  call feedkeys("H", 'Lx!')
+  call assert_equal(4, line('.'))
+
+  " Shift Scroll Down
+  call MouseShiftWheelDown(2, 1)
+  call feedkeys("H", 'Lx!')
+  " should scroll from where it is (4) + visible buffer height - cmdheight
+  let shift_scroll_height = line('w$') - line('w0') - &cmdheight 
+  call assert_equal(4 + shift_scroll_height, line('.'))
+
+  " Shift Scroll Up
+  call MouseShiftWheelUp(2, 1)
+  call feedkeys("H", 'Lx!')
+  call assert_equal(4, line('.'))
+
+  if !has('gui_running')
+    " Shift Scroll Down (using MOD)
+    call MSWinMouseEvent(0x100, 2, 1, 0, 0, 0x04)
+    call feedkeys("H", 'Lx!')
+    " should scroll from where it is (4) + visible buffer height - cmdheight
+    let shift_scroll_height = line('w$') - line('w0') - &cmdheight 
+    call assert_equal(4 + shift_scroll_height, line('.'))
+
+    " Shift Scroll Up (using MOD)
+    call MSWinMouseEvent(0x200, 2, 1, 0, 0, 0x04)
+    call feedkeys("H", 'Lx!')
+    call assert_equal(4, line('.'))
+  endif
+
+  set scrolloff&
+
+  %d _
+  set nowrap
+  " make the buffer 500 wide.
+  call setline(1, range(10)->join('')->repeat(50))
+  " Scroll Right
+  call MouseWheelRight(1, 5)
+  call MouseWheelRight(1, 10)
+  call MouseWheelRight(1, 15)
+  call feedkeys('g0', 'Lx!')
+  call assert_equal(19, col('.'))
+
+  " Scroll Left
+  call MouseWheelLeft(1, 15)
+  call MouseWheelLeft(1, 10)
+  call feedkeys('g0', 'Lx!')
+  call assert_equal(7, col('.'))
+
+  " Shift Scroll Right
+  call MouseShiftWheelRight(1, 10)
+  call feedkeys('g0', 'Lx!')
+  " should scroll from where it is (7) + window width
+  call assert_equal(7 + winwidth(0), col('.'))
+ 
+  " Shift Scroll Left
+  call MouseShiftWheelLeft(1, 50)
+  call feedkeys('g0', 'Lx!')
+  call assert_equal(7, col('.'))
+  set wrap&
+
+  %d _
+  call setline(1, repeat([repeat('a', 60)], 10))
+
+  " record various mouse events
+  let mouseEventNames = [
+        \ 'LeftMouse', 'LeftRelease', '2-LeftMouse', '3-LeftMouse',
+        \ 'S-LeftMouse', 'A-LeftMouse', 'C-LeftMouse', 'MiddleMouse',
+        \ 'MiddleRelease', '2-MiddleMouse', '3-MiddleMouse',
+        \ 'S-MiddleMouse', 'A-MiddleMouse', 'C-MiddleMouse',
+        \ 'RightMouse', 'RightRelease', '2-RightMouse',
+        \ '3-RightMouse', 'S-RightMouse', 'A-RightMouse', 'C-RightMouse',
+        \ ]
+  let mouseEventCodes = map(copy(mouseEventNames), "'<' .. v:val .. '>'")
+  let g:events = []
+  for e in mouseEventCodes
+    exe 'nnoremap ' .. e .. ' <Cmd>call add(g:events, "' ..
+          \ substitute(e, '[<>]', '', 'g') .. '")<CR>'
+  endfor
+
+  " Test various mouse buttons 
+  "(0 - Left, 1 - Middle, 2 - Right, 
+  " 0x300 - MOUSE_X1/FROM_LEFT_3RD_BUTTON,
+  " 0x400 - MOUSE_X2/FROM_LEFT_4TH_BUTTON)
+  for button in [0, 1, 2, 0x300, 0x400]
+    " Single click
+    let args = #{button: button, row: 2, col: 5, multiclick: 0, modifiers: 0}
+    call test_mswin_event('mouse', args)
+    let args.button = 3
+    call test_mswin_event('mouse', args)
+
+    " Double Click
+    let args.button = button
+    call test_mswin_event('mouse', args)
+    let args.multiclick = 1
+    call test_mswin_event('mouse', args)
+    let args.button = 3
+    let args.multiclick = 0
+    call test_mswin_event('mouse', args)
+
+    " Triple Click
+    let args.button = button
+    call test_mswin_event('mouse', args)
+    let args.multiclick = 1
+    call test_mswin_event('mouse', args)
+    call test_mswin_event('mouse', args)
+    let args.button = 3
+    let args.multiclick = 0
+    call test_mswin_event('mouse', args)
+
+    " Shift click
+    let args = #{button: button, row: 3, col: 7, multiclick: 0, modifiers: 4}
+    call test_mswin_event('mouse', args)
+    let args.button = 3
+    call test_mswin_event('mouse', args)
+
+    " Alt click
+    let args.button = button
+    let args.modifiers = 8
+    call test_mswin_event('mouse', args)
+    let args.button = 3
+    call test_mswin_event('mouse', args)
+
+    " Ctrl click
+    let args.button = button
+    let args.modifiers = 16
+    call test_mswin_event('mouse', args)
+    let args.button = 3
+    call test_mswin_event('mouse', args)
+
+    call feedkeys("\<Esc>", 'Lx!')
+  endfor
+
+  if has('gui_running')
+    call assert_equal(['LeftMouse', 'LeftRelease', 'LeftMouse',
+	\ '2-LeftMouse', 'LeftMouse', '2-LeftMouse', '3-LeftMouse',
+	\ 'S-LeftMouse', 'A-LeftMouse', 'C-LeftMouse', 'MiddleMouse',
+	\ 'MiddleRelease', 'MiddleMouse', '2-MiddleMouse', 'MiddleMouse',
+	\ '2-MiddleMouse', '3-MiddleMouse', 'S-MiddleMouse', 'A-MiddleMouse',
+	\ 'C-MiddleMouse', 'RightMouse', 'RightRelease', 'RightMouse',
+	\ '2-RightMouse', 'RightMouse', '2-RightMouse', '3-RightMouse',
+	\ 'S-RightMouse', 'A-RightMouse', 'C-RightMouse'],
+	\ g:events)
+  else
+    call assert_equal(['MiddleRelease', 'LeftMouse', '2-LeftMouse',
+	\ '3-LeftMouse', 'S-LeftMouse', 'MiddleMouse', '2-MiddleMouse',
+	\ '3-MiddleMouse', 'MiddleMouse', 'S-MiddleMouse', 'RightMouse',
+	\ '2-RightMouse', '3-RightMouse'],
+	\ g:events)
+  endif
+
+  for e in mouseEventCodes
+    exe 'nunmap ' .. e
+  endfor
+
+  bw!
+  call test_override('no_query_mouse', 0)
+  set mousemodel&
+endfunc
+
+
+"  Test MS-Windows test_mswin_event error handling
+func Test_mswin_event_error_handling()
+
+  let args = #{button: 0xfff, row: 2, col: 4, move: 0, multiclick: 0, modifiers: 0}
+  if !has('gui_running')
+    call assert_fails("call test_mswin_event('mouse', args)",'E475:')
+  endif
+  let args = #{button: 0, row: 2, col: 4, move: 0, multiclick: 0, modifiers: 0}
+  call assert_fails("call test_mswin_event('a1b2c3', args)", 'E475:')
+  call assert_fails("call test_mswin_event(test_null_string(), {})", 'E475:')
+  
+  call assert_fails("call test_mswin_event([], args)", 'E1174:')
+  call assert_fails("call test_mswin_event('abc', [])", 'E1206:')
+  
+  call assert_false(test_mswin_event('mouse', test_null_dict()))
+  let args = #{row: 2, col: 4, multiclick: 0, modifiers: 0}
+  call assert_false(test_mswin_event('mouse', args))
+  let args = #{button: 0, col: 4, multiclick: 0, modifiers: 0}
+  call assert_false(test_mswin_event('mouse', args))
+  let args = #{button: 0, row: 2, multiclick: 0, modifiers: 0}
+  call assert_false(test_mswin_event('mouse', args))
+  let args = #{button: 0, row: 2, col: 4, modifiers: 0}
+  call assert_false(test_mswin_event('mouse', args))
+  let args = #{button: 0, row: 2, col: 4, multiclick: 0}
+  call assert_false(test_mswin_event('mouse', args))
+
+  call assert_false(test_mswin_event('key', test_null_dict()))
+  call assert_fails("call test_mswin_event('key', [])", 'E1206:')
+  call assert_fails("call test_mswin_event('key', {'event': 'keydown', 'keycode': 0x0})", 'E1291:')
+  call assert_fails("call test_mswin_event('key', {'event': 'keydown', 'keycode': [15]})", 'E745:')
+  call assert_fails("call test_mswin_event('key', {'event': 'keys', 'keycode': 0x41})", 'E475:')
+  call assert_fails("call test_mswin_event('key', {'keycode': 0x41})", 'E417:')
+  call assert_fails("call test_mswin_event('key', {'event': 'keydown'})", 'E1291:')
+
+  call assert_fails("sandbox call test_mswin_event('key', {'event': 'keydown', 'keycode': 61 })", 'E48:')
+
+  " flush out any garbage left in the buffer.
+  while getchar(0)
+  endwhile
+endfunc
+
+
+" vim: shiftwidth=2 sts=2 expandtab
--- a/src/testdir/test_termcodes.vim
+++ b/src/testdir/test_termcodes.vim
@@ -437,25 +437,22 @@ func Test_1xterm_mouse_wheel()
     call assert_equal(1, line('w0'), msg)
     call assert_equal([0, 7, 1, 0], getpos('.'), msg)
 
-    if has('gui')
-      " Horizontal wheel scrolling currently only works when vim is
-      " compiled with gui enabled.
-      call MouseWheelRight(1, 1)
-      call assert_equal(7, 1 + virtcol(".") - wincol(), msg)
-      call assert_equal([0, 7, 7, 0], getpos('.'), msg)
-
-      call MouseWheelRight(1, 1)
-      call assert_equal(13, 1 + virtcol(".") - wincol(), msg)
-      call assert_equal([0, 7, 13, 0], getpos('.'), msg)
-
-      call MouseWheelLeft(1, 1)
-      call assert_equal(7, 1 + virtcol(".") - wincol(), msg)
-      call assert_equal([0, 7, 13, 0], getpos('.'), msg)
-
-      call MouseWheelLeft(1, 1)
-      call assert_equal(1, 1 + virtcol(".") - wincol(), msg)
-      call assert_equal([0, 7, 13, 0], getpos('.'), msg)
-    endif
+    call MouseWheelRight(1, 1)
+    call assert_equal(7, 1 + virtcol(".") - wincol(), msg)
+    call assert_equal([0, 7, 7, 0], getpos('.'), msg)
+
+    call MouseWheelRight(1, 1)
+    call assert_equal(13, 1 + virtcol(".") - wincol(), msg)
+    call assert_equal([0, 7, 13, 0], getpos('.'), msg)
+
+    call MouseWheelLeft(1, 1)
+    call assert_equal(7, 1 + virtcol(".") - wincol(), msg)
+    call assert_equal([0, 7, 13, 0], getpos('.'), msg)
+
+    call MouseWheelLeft(1, 1)
+    call assert_equal(1, 1 + virtcol(".") - wincol(), msg)
+    call assert_equal([0, 7, 13, 0], getpos('.'), msg)
+
   endfor
 
   let &mouse = save_mouse
--- a/src/testing.c
+++ b/src/testing.c
@@ -1388,13 +1388,18 @@ test_gui_mouse_event(dict_T *args)
 
     if (move)
     {
+	int pY = row;
+	int pX = col;
+	// the "move" argument expects row and col coordnates to be in pixels,
+	// unless "cell" is specified and is TRUE.
 	if (dict_get_bool(args, "cell", FALSE))
 	{
-	    // click in the middle of the character cell
-	    row = row * gui.char_height + gui.char_height / 2;
-	    col = col * gui.char_width + gui.char_width / 2;
+	    // calculate the middle of the character cell
+	    // Note: Cell coordinates are 1-based from vimscript
+	    pY = (row - 1) * gui.char_height + gui.char_height / 2;
+	    pX = (col - 1) * gui.char_width + gui.char_width / 2;
 	}
-	gui_mouse_moved(col, row);
+	gui_mouse_moved(pX, pY);
     }
     else
     {
@@ -1489,6 +1494,30 @@ test_gui_tabmenu_event(dict_T *args UNUS
 # endif
 
     void
+f_test_mswin_event(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
+{
+# ifdef MSWIN
+    rettv->v_type = VAR_BOOL;
+    rettv->vval.v_number = FALSE;
+
+    if (sandbox != 0)
+    {
+	emsg(_(e_not_allowed_in_sandbox));
+	return;
+    }
+
+    if (check_for_string_arg(argvars, 0) == FAIL
+	    || check_for_dict_arg(argvars, 1) == FAIL
+	    || argvars[1].vval.v_dict == NULL)
+	return;
+
+    char_u *event = tv_get_string(&argvars[0]);
+    rettv->vval.v_number = test_mswin_event(event, argvars[1].vval.v_dict);
+
+# endif
+}
+
+    void
 f_test_gui_event(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
 {
 # ifdef FEAT_GUI
@@ -1515,6 +1544,10 @@ f_test_gui_event(typval_T *argvars UNUSE
     else if (STRCMP(event, "findrepl") == 0)
 	rettv->vval.v_number = test_gui_find_repl(argvars[1].vval.v_dict);
 #  endif
+#  ifdef MSWIN
+    else if (STRCMP(event, "key") == 0 || STRCMP(event, "mouse") == 0)
+	rettv->vval.v_number = test_mswin_event(event, argvars[1].vval.v_dict);
+#  endif
     else if (STRCMP(event, "mouse") == 0)
 	rettv->vval.v_number = test_gui_mouse_event(argvars[1].vval.v_dict);
     else if (STRCMP(event, "scrollbar") == 0)
@@ -1523,10 +1556,6 @@ f_test_gui_event(typval_T *argvars UNUSE
 	rettv->vval.v_number = test_gui_tabline_event(argvars[1].vval.v_dict);
     else if (STRCMP(event, "tabmenu") == 0)
 	rettv->vval.v_number = test_gui_tabmenu_event(argvars[1].vval.v_dict);
-#  ifdef FEAT_GUI_MSWIN
-    else if (STRCMP(event, "sendevent") == 0)
-	rettv->vval.v_number = test_gui_w32_sendevent(argvars[1].vval.v_dict);
-#  endif
     else
     {
 	semsg(_(e_invalid_argument_str), event);
--- a/src/version.c
+++ b/src/version.c
@@ -696,6 +696,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1084,
+/**/
     1083,
 /**/
     1082,