changeset 27447:4050f0554902 v8.2.4252

patch 8.2.4252: generating the normal command table at runtime is inefficient Commit: https://github.com/vim/vim/commit/4dc0dd869972ddafc7d9ee5ea765645b818a6dc9 Author: Yegappan Lakshmanan <yegappan@yahoo.com> Date: Sat Jan 29 13:06:40 2022 +0000 patch 8.2.4252: generating the normal command table at runtime is inefficient Problem: Generating the normal command table at runtime is inefficient. Solution: Generate the table with a Vim script and put it in a header file. (Yegappan Lakshmanan, closes #9648)
author Bram Moolenaar <Bram@vim.org>
date Sat, 29 Jan 2022 14:15:04 +0100
parents d5dcb8d0cf5e
children ca9bc0a44d8a
files Filelist runtime/doc/builtin.txt runtime/doc/usr_41.txt src/Make_cyg_ming.mak src/Make_mvc.mak src/Make_vms.mms src/Makefile src/create_nvcmdidxs.vim src/evalfunc.c src/main.c src/normal.c src/nv_cmdidxs.h src/proto/normal.pro src/version.c
diffstat 14 files changed, 407 insertions(+), 72 deletions(-) [+]
line wrap: on
line diff
--- a/Filelist
+++ b/Filelist
@@ -113,6 +113,7 @@ SRC_ALL =	\
 		src/nbdebug.h \
 		src/netbeans.c \
 		src/normal.c \
+		src/nv_cmdidxs.h \
 		src/ops.c \
 		src/option.c \
 		src/option.h \
@@ -443,6 +444,7 @@ SRC_UNIX =	\
 		src/configure \
 		src/configure.ac \
 		src/create_cmdidxs.vim \
+		src/create_nvcmdidxs.vim \
 		src/gui_at_fs.c \
 		src/gui_at_sb.c \
 		src/gui_at_sb.h \
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -292,6 +292,7 @@ inputrestore()			Number	restore typeahea
 inputsave()			Number	save and clear typeahead
 inputsecret({prompt} [, {text}]) String	like input() but hiding the text
 insert({object}, {item} [, {idx}]) List	insert {item} in {object} [before {idx}]
+internal_get_nv_cmdchar({idx})	Number	command character at this index
 interrupt()			none	interrupt script execution
 invert({expr})			Number	bitwise invert
 isdirectory({directory})	Number	|TRUE| if {directory} is a directory
@@ -4622,6 +4623,11 @@ insert({object}, {item} [, {idx}])			*in
 
 		Can also be used as a |method|: >
 			mylist->insert(item)
+<
+						*internal_get_nv_cmdchar()*
+internal_get_nv_cmdchar({idx})
+		Return the normal/visual mode command character at the
+		specified index. To be used only during the Vim build process.
 
 interrupt()						*interrupt()*
 		Interrupt script execution.  It works more or less like the
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1110,6 +1110,7 @@ Testing:				    *test-functions*
 	assert_nobeep()		assert that a command does not cause a beep
 	assert_fails()		assert that a command fails
 	assert_report()		report a test failure
+	internal_get_nv_cmdchar()  normal/visual command character at an index
 	test_alloc_fail()	make memory allocation fail
 	test_autochdir()	enable 'autochdir' during startup
 	test_override()		test with Vim internal overrides
--- a/src/Make_cyg_ming.mak
+++ b/src/Make_cyg_ming.mak
@@ -1147,6 +1147,16 @@ endif
 cmdidxs: ex_cmds.h
 	vim --clean -X --not-a-term -u create_cmdidxs.vim
 
+# Run vim script to generate the normal/visual mode command lookup table.
+# This only needs to be run when a new normal/visual mode command has been
+# added.  If this fails because you don't have Vim yet:
+#   - change nv_cmds[] in normal.c to add the new normal/visual mode command.
+#   - build Vim
+#   - run "make nvcmdidxs" using the new Vim to generate nv_cmdidxs.h
+#   - rebuild Vim to use the newly generated nv_cmdidxs.h file.
+nvcmdidxs: normal.c
+	./$(TARGET) --clean -X --not-a-term -u create_nvcmdidxs.vim
+
 ###########################################################################
 INCL =	vim.h alloc.h ascii.h ex_cmds.h feature.h errors.h globals.h \
 	keymap.h macros.h option.h os_dos.h os_win32.h proto.h regexp.h \
@@ -1209,6 +1219,8 @@ endif
 
 $(OUTDIR)/misc1.o: misc1.c $(INCL) version.h
 
+$(OUTDIR)/normal.o: normal.c $(INCL) nv_cmdidxs.h
+
 $(OUTDIR)/netbeans.o: netbeans.c $(INCL) version.h
 
 $(OUTDIR)/version.o: version.c $(INCL) version.h
--- a/src/Make_mvc.mak
+++ b/src/Make_mvc.mak
@@ -1446,6 +1446,16 @@ clean: testclean
 cmdidxs: ex_cmds.h
 	vim --clean -X --not-a-term -u create_cmdidxs.vim
 
+# Run vim script to generate the normal/visual mode command lookup table.
+# This only needs to be run when a new normal/visual mode command has been
+# added.  If this fails because you don't have Vim yet:
+#   - change nv_cmds[] in normal.c to add the new normal/visual mode command.
+#   - build Vim
+#   - run "make nvcmdidxs" using the new Vim to generate nv_cmdidxs.h
+#   - rebuild Vim to use the newly generated nv_cmdidxs.h file.
+nvcmdidxs: normal.c
+	.\$(VIM) --clean -X --not-a-term -u create_nvcmdidxs.vim
+
 test:
 	cd testdir
 	$(MAKE) /NOLOGO -f Make_dos.mak
@@ -1709,7 +1719,7 @@ lib$(MZSCHEME_MAIN_LIB)$(MZSCHEME_VER).l
 
 $(OUTDIR)/channel.obj:	$(OUTDIR) channel.c $(INCL)
 
-$(OUTDIR)/normal.obj:	$(OUTDIR) normal.c  $(INCL)
+$(OUTDIR)/normal.obj:	$(OUTDIR) normal.c  $(INCL) nv_cmdidxs.h
 
 $(OUTDIR)/option.obj:	$(OUTDIR) option.c  $(INCL) optiondefs.h
 
--- a/src/Make_vms.mms
+++ b/src/Make_vms.mms
@@ -977,7 +977,7 @@ mbyte.obj : mbyte.c vim.h [.auto]config.
 normal.obj : normal.c vim.h [.auto]config.h feature.h os_unix.h \
  ascii.h keymap.h termdefs.h macros.h structs.h regexp.h \
  gui.h beval.h [.proto]gui_beval.pro option.h ex_cmds.h proto.h \
- errors.h globals.h
+ errors.h globals.h nv_cmdidxs.h
 ops.obj : ops.c vim.h [.auto]config.h feature.h os_unix.h   \
  ascii.h keymap.h termdefs.h macros.h structs.h regexp.h gui.h beval.h \
  [.proto]gui_beval.pro option.h ex_cmds.h proto.h errors.h globals.h
--- a/src/Makefile
+++ b/src/Makefile
@@ -2144,6 +2144,16 @@ autoconf:
 cmdidxs: ex_cmds.h
 	vim --clean -X --not-a-term -u create_cmdidxs.vim
 
+# Run vim script to generate the normal/visual mode command lookup table.
+# This only needs to be run when a new normal/visual mode command has been
+# added.  If this fails because you don't have Vim yet:
+#   - change nv_cmds[] in normal.c to add the new normal/visual mode command.
+#   - build Vim
+#   - run "make nvcmdidxs" using the new Vim to generate nv_cmdidxs.h
+#   - rebuild Vim to use the newly generated nv_cmdidxs.h file.
+nvcmdidxs: normal.c
+	./$(VIMTARGET) --clean -X --not-a-term -u create_nvcmdidxs.vim
+
 
 # The normal command to compile a .c file to its .o file.
 # Without or with ALL_CFLAGS.
@@ -4002,7 +4012,7 @@ objects/move.o: move.c vim.h protodef.h 
 objects/normal.o: normal.c vim.h protodef.h auto/config.h feature.h os_unix.h \
  auto/osdef.h ascii.h keymap.h termdefs.h macros.h option.h beval.h \
  proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \
- proto.h globals.h errors.h
+ proto.h globals.h errors.h nv_cmdidxs.h
 objects/ops.o: ops.c vim.h protodef.h auto/config.h feature.h os_unix.h \
  auto/osdef.h ascii.h keymap.h termdefs.h macros.h option.h beval.h \
  proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \
new file mode 100644
--- /dev/null
+++ b/src/create_nvcmdidxs.vim
@@ -0,0 +1,72 @@
+vim9script
+
+# This script generates the table nv_cmd_idx[] which contains the index in
+# nv_cmds[] table (normal.c) for each of the command character supported in
+# normal/visual mode.
+# This is used to speed up the command lookup in nv_cmds[].
+#
+# Script should be run using "make nvcmdidxs", every time the nv_cmds[] table
+# in src/normal.c changes.
+
+def Create_nvcmdidxs_table()
+  var nv_cmdtbl: list<dict<number>> = []
+
+  # Generate the table of normal/visual mode command characters and their
+  # corresponding index.
+  var idx: number = 0
+  var ch: number
+  while true
+    ch = internal_get_nv_cmdchar(idx)
+    if ch == -1
+      break
+    endif
+    add(nv_cmdtbl, {idx: idx, cmdchar: ch})
+    idx += 1
+  endwhile
+
+  # sort the table by the command character
+  sort(nv_cmdtbl, (a, b) => a.cmdchar - b.cmdchar)
+
+  # Compute the highest index upto which the command character can be directly
+  # used as an index.
+  var nv_max_linear: number = 0
+  for i in range(nv_cmdtbl->len())
+    if i != nv_cmdtbl[i].cmdchar
+      nv_max_linear = i - 1
+      break
+    endif
+  endfor
+
+  # Generate a header file with the table
+  var output: list<string> =<< trim END
+    /*
+     * Automatically generated code by the create_nvcmdidxs.vim script.
+     *
+     * Table giving the index in nv_cmds[] to lookup based on
+     * the command character.
+     */
+
+    // nv_cmd_idx[<normal mode command character>] => nv_cmds[] index
+    static const unsigned short nv_cmd_idx[] =
+    {
+  END
+
+  # Add each command character in comment and the corresponding index
+  var tbl: list<string> = mapnew(nv_cmdtbl, (k, v) =>
+        '  /* ' .. printf('%5d', v.cmdchar) .. ' */ ' ..
+        printf('%3d', v.idx) .. ','
+    )
+  output += tbl
+
+  output += [ '};', '',
+              '// The highest index for which',
+              '// nv_cmds[idx].cmd_char == nv_cmd_idx[nv_cmds[idx].cmd_char]']
+  output += ['static const int nv_max_linear = ' .. nv_max_linear .. ';']
+
+  writefile(output, "nv_cmdidxs.h")
+enddef
+
+Create_nvcmdidxs_table()
+quit
+
+# vim: shiftwidth=2 sts=2 expandtab
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -1737,6 +1737,8 @@ static funcentry_T global_functions[] =
 			ret_string,	    f_inputsecret},
     {"insert",		2, 3, FEARG_1,	    arg23_insert,
 			ret_first_arg,	    f_insert},
+    {"internal_get_nv_cmdchar",	1, 1, FEARG_1,    arg1_number,
+			ret_number,	    f_internal_get_nv_cmdchar},
     {"interrupt",	0, 0, 0,	    NULL,
 			ret_void,	    f_interrupt},
     {"invert",		1, 1, FEARG_1,	    arg1_number,
--- a/src/main.c
+++ b/src/main.c
@@ -901,9 +901,6 @@ common_init(mparm_T *paramp)
     qnx_init();		// PhAttach() for clipboard, (and gui)
 #endif
 
-    // Init the table of Normal mode commands.
-    init_normal_cmds();
-
     /*
      * Allocate space for the generic buffers (needed for set_init_1() and
      * emsg()).
--- a/src/normal.c
+++ b/src/normal.c
@@ -19,7 +19,6 @@ static int	VIsual_mode_orig = NUL;		// s
 #ifdef FEAT_EVAL
 static void	set_vcount_ca(cmdarg_T *cap, int *set_prevcount);
 #endif
-static int	nv_compare(const void *s1, const void *s2);
 static void	unshift_special(cmdarg_T *cap);
 #ifdef FEAT_CMDL_INFO
 static void	del_from_showcmd(int);
@@ -128,6 +127,34 @@ static void	nv_drop(cmdarg_T *cap);
 #endif
 static void	nv_cursorhold(cmdarg_T *cap);
 
+#ifdef FEAT_GUI
+#define NV_VER_SCROLLBAR	nv_ver_scrollbar
+#define NV_HOR_SCROLLBAR	nv_hor_scrollbar
+#else
+#define NV_VER_SCROLLBAR nv_error
+#define NV_HOR_SCROLLBAR nv_error
+#endif
+
+#ifdef FEAT_GUI_TABLINE
+#define NV_TABLINE	nv_tabline
+#define NV_TABMENU	nv_tabmenu
+#else
+#define NV_TABLINE	nv_error
+#define NV_TABMENU	nv_error
+#endif
+
+#ifdef FEAT_NETBEANS_INTG
+#define NV_NBCMD	nv_nbcmd
+#else
+#define NV_NBCMD	nv_error
+#endif
+
+#ifdef FEAT_DND
+#define NV_DROP		nv_drop
+#else
+#define NV_DROP		nv_error
+#endif
+
 /*
  * Function to be called for a Normal or Visual mode command.
  * The argument is a cmdarg_T.
@@ -159,8 +186,14 @@ typedef void (*nv_func_T)(cmdarg_T *cap)
 
 /*
  * This table contains one entry for every Normal or Visual mode command.
- * The order doesn't matter, init_normal_cmds() will create a sorted index.
+ * The order doesn't matter, this will be sorted by the create_nvcmdidx.vim
+ * script to generate the nv_cmd_idx[] lookup table.
  * It is faster when all keys from zero to '~' are present.
+ *
+ * After changing the "nv_cmds" table:
+ * 1. Build Vim with "make"
+ * 2. Run "make nvcmdidxs" to re-generate the nv_cmdidxs.h file.
+ * 3. Build Vim with "make" to use the newly generated index table.
  */
 static const struct nv_cmd
 {
@@ -193,8 +226,6 @@ static const struct nv_cmd
     {Ctrl_T,	nv_tagpop,	NV_NCW,			0},
     {Ctrl_U,	nv_halfpage,	0,			0},
     {Ctrl_V,	nv_visual,	0,			FALSE},
-    {'V',	nv_visual,	0,			FALSE},
-    {'v',	nv_visual,	0,			FALSE},
     {Ctrl_W,	nv_window,	0,			0},
     {Ctrl_X,	nv_addsub,	0,			0},
     {Ctrl_Y,	nv_scroll_line,	0,			FALSE},
@@ -258,6 +289,7 @@ static const struct nv_cmd
     {'S',	nv_subst,	NV_KEEPREG,		0},
     {'T',	nv_csearch,	NV_NCH_ALW|NV_LANG,	BACKWARD},
     {'U',	nv_Undo,	0,			0},
+    {'V',	nv_visual,	0,			FALSE},
     {'W',	nv_wordcmd,	0,			TRUE},
     {'X',	nv_abbrev,	NV_KEEPREG,		0},
     {'Y',	nv_abbrev,	NV_KEEPREG,		0},
@@ -289,6 +321,7 @@ static const struct nv_cmd
     {'s',	nv_subst,	NV_KEEPREG,		0},
     {'t',	nv_csearch,	NV_NCH_ALW|NV_LANG,	FORWARD},
     {'u',	nv_undo,	0,			0},
+    {'v',	nv_visual,	0,			FALSE},
     {'w',	nv_wordcmd,	0,			FALSE},
     {'x',	nv_abbrev,	NV_KEEPREG,		0},
     {'y',	nv_operator,	0,			0},
@@ -356,20 +389,12 @@ static const struct nv_cmd
     {K_F1,	nv_help,	NV_NCW,			0},
     {K_XF1,	nv_help,	NV_NCW,			0},
     {K_SELECT,	nv_select,	0,			0},
-#ifdef FEAT_GUI
-    {K_VER_SCROLLBAR, nv_ver_scrollbar, 0,		0},
-    {K_HOR_SCROLLBAR, nv_hor_scrollbar, 0,		0},
-#endif
-#ifdef FEAT_GUI_TABLINE
-    {K_TABLINE, nv_tabline,	0,			0},
-    {K_TABMENU, nv_tabmenu,	0,			0},
-#endif
-#ifdef FEAT_NETBEANS_INTG
-    {K_F21,	nv_nbcmd,	NV_NCH_ALW,		0},
-#endif
-#ifdef FEAT_DND
-    {K_DROP,	nv_drop,	NV_STS,			0},
-#endif
+    {K_VER_SCROLLBAR, NV_VER_SCROLLBAR, 0,		0},
+    {K_HOR_SCROLLBAR, NV_HOR_SCROLLBAR, 0,		0},
+    {K_TABLINE, NV_TABLINE,	0,			0},
+    {K_TABMENU, NV_TABMENU,	0,			0},
+    {K_F21,	NV_NBCMD,	NV_NCH_ALW,		0},
+    {K_DROP,	NV_DROP,	NV_STS,			0},
     {K_CURSORHOLD, nv_cursorhold, NV_KEEPREG,		0},
     {K_PS,	nv_edit,	0,			0},
     {K_COMMAND,	nv_colon,	0,			0},
@@ -379,55 +404,42 @@ static const struct nv_cmd
 // Number of commands in nv_cmds[].
 #define NV_CMDS_SIZE ARRAY_LENGTH(nv_cmds)
 
-#ifndef PROTO  // cproto doesn't like this
-// Sorted index of commands in nv_cmds[].
-static short nv_cmd_idx[NV_CMDS_SIZE];
-#endif
-
-// The highest index for which
-// nv_cmds[idx].cmd_char == nv_cmd_idx[nv_cmds[idx].cmd_char]
-static int nv_max_linear;
-
-/*
- * Compare functions for qsort() below, that checks the command character
- * through the index in nv_cmd_idx[].
- */
-    static int
-nv_compare(const void *s1, const void *s2)
-{
-    int		c1, c2;
-
-    // The commands are sorted on absolute value.
-    c1 = nv_cmds[*(const short *)s1].cmd_char;
-    c2 = nv_cmds[*(const short *)s2].cmd_char;
-    if (c1 < 0)
-	c1 = -c1;
-    if (c2 < 0)
-	c2 = -c2;
-    return c1 - c2;
-}
-
-/*
- * Initialize the nv_cmd_idx[] table.
+// Include the lookuptable generated by create_nvcmdidx.vim.
+#include "nv_cmdidxs.h"
+
+#if defined(FEAT_EVAL) || defined(PROTO)
+/*
+ * Return the command character for the given command index. This function is
+ * used to auto-generate nv_cmd_idx[].
  */
     void
-init_normal_cmds(void)
-{
-    int		i;
-
-    // Fill the index table with a one to one relation.
-    for (i = 0; i < (int)NV_CMDS_SIZE; ++i)
-	nv_cmd_idx[i] = i;
-
-    // Sort the commands by the command character.
-    qsort((void *)&nv_cmd_idx, (size_t)NV_CMDS_SIZE, sizeof(short), nv_compare);
-
-    // Find the first entry that can't be indexed by the command character.
-    for (i = 0; i < (int)NV_CMDS_SIZE; ++i)
-	if (i != nv_cmds[nv_cmd_idx[i]].cmd_char)
-	    break;
-    nv_max_linear = i - 1;
-}
+f_internal_get_nv_cmdchar(typval_T *argvars, typval_T *rettv)
+{
+    int	idx;
+    int	cmd_char;
+
+    rettv->v_type = VAR_NUMBER;
+    rettv->vval.v_number = -1;
+
+    if (check_for_number_arg(argvars, 0) == FAIL)
+	return;
+
+    idx = tv_get_number(&argvars[0]);
+    if (idx < 0 || idx >= (int)NV_CMDS_SIZE)
+	return;
+
+    cmd_char = nv_cmds[idx].cmd_char;
+
+    // We use the absolute value of the character.  Special keys have a
+    // negative value, but are sorted on their absolute value.
+    if (cmd_char < 0)
+	cmd_char = -cmd_char;
+
+    rettv->vval.v_number = cmd_char;
+
+    return;
+}
+#endif
 
 /*
  * Search for a command in the commands table.
new file mode 100644
--- /dev/null
+++ b/src/nv_cmdidxs.h
@@ -0,0 +1,209 @@
+/*
+ * Automatically generated code by the create_nvcmdidxs.vim script.
+ *
+ * Table giving the index in nv_cmds[] to lookup based on
+ * the command character.
+ */
+
+// nv_cmd_idx[<normal mode command character>] => nv_cmds[] index
+static const unsigned short nv_cmd_idx[] =
+{
+  /*     0 */   0,
+  /*     1 */   1,
+  /*     2 */   2,
+  /*     3 */   3,
+  /*     4 */   4,
+  /*     5 */   5,
+  /*     6 */   6,
+  /*     7 */   7,
+  /*     8 */   8,
+  /*     9 */   9,
+  /*    10 */  10,
+  /*    11 */  11,
+  /*    12 */  12,
+  /*    13 */  13,
+  /*    14 */  14,
+  /*    15 */  15,
+  /*    16 */  16,
+  /*    17 */  17,
+  /*    18 */  18,
+  /*    19 */  19,
+  /*    20 */  20,
+  /*    21 */  21,
+  /*    22 */  22,
+  /*    23 */  23,
+  /*    24 */  24,
+  /*    25 */  25,
+  /*    26 */  26,
+  /*    27 */  27,
+  /*    28 */  28,
+  /*    29 */  29,
+  /*    30 */  30,
+  /*    31 */  31,
+  /*    32 */  32,
+  /*    33 */  33,
+  /*    34 */  34,
+  /*    35 */  35,
+  /*    36 */  36,
+  /*    37 */  37,
+  /*    38 */  38,
+  /*    39 */  39,
+  /*    40 */  40,
+  /*    41 */  41,
+  /*    42 */  42,
+  /*    43 */  43,
+  /*    44 */  44,
+  /*    45 */  45,
+  /*    46 */  46,
+  /*    47 */  47,
+  /*    48 */  48,
+  /*    49 */  49,
+  /*    50 */  50,
+  /*    51 */  51,
+  /*    52 */  52,
+  /*    53 */  53,
+  /*    54 */  54,
+  /*    55 */  55,
+  /*    56 */  56,
+  /*    57 */  57,
+  /*    58 */  58,
+  /*    59 */  59,
+  /*    60 */  60,
+  /*    61 */  61,
+  /*    62 */  62,
+  /*    63 */  63,
+  /*    64 */  64,
+  /*    65 */  65,
+  /*    66 */  66,
+  /*    67 */  67,
+  /*    68 */  68,
+  /*    69 */  69,
+  /*    70 */  70,
+  /*    71 */  71,
+  /*    72 */  72,
+  /*    73 */  73,
+  /*    74 */  74,
+  /*    75 */  75,
+  /*    76 */  76,
+  /*    77 */  77,
+  /*    78 */  78,
+  /*    79 */  79,
+  /*    80 */  80,
+  /*    81 */  81,
+  /*    82 */  82,
+  /*    83 */  83,
+  /*    84 */  84,
+  /*    85 */  85,
+  /*    86 */  86,
+  /*    87 */  87,
+  /*    88 */  88,
+  /*    89 */  89,
+  /*    90 */  90,
+  /*    91 */  91,
+  /*    92 */  92,
+  /*    93 */  93,
+  /*    94 */  94,
+  /*    95 */  95,
+  /*    96 */  96,
+  /*    97 */  97,
+  /*    98 */  98,
+  /*    99 */  99,
+  /*   100 */ 100,
+  /*   101 */ 101,
+  /*   102 */ 102,
+  /*   103 */ 103,
+  /*   104 */ 104,
+  /*   105 */ 105,
+  /*   106 */ 106,
+  /*   107 */ 107,
+  /*   108 */ 108,
+  /*   109 */ 109,
+  /*   110 */ 110,
+  /*   111 */ 111,
+  /*   112 */ 112,
+  /*   113 */ 113,
+  /*   114 */ 114,
+  /*   115 */ 115,
+  /*   116 */ 116,
+  /*   117 */ 117,
+  /*   118 */ 118,
+  /*   119 */ 119,
+  /*   120 */ 120,
+  /*   121 */ 121,
+  /*   122 */ 122,
+  /*   123 */ 123,
+  /*   124 */ 124,
+  /*   125 */ 125,
+  /*   126 */ 126,
+  /*   163 */ 127,
+  /*  1277 */ 156,
+  /*  1533 */ 158,
+  /* 11517 */ 132,
+  /* 11773 */ 134,
+  /* 12029 */ 135,
+  /* 12285 */ 138,
+  /* 12541 */ 139,
+  /* 12581 */ 180,
+  /* 12619 */ 174,
+  /* 12651 */ 181,
+  /* 12797 */ 140,
+  /* 12835 */ 175,
+  /* 13053 */ 141,
+  /* 13131 */ 166,
+  /* 13309 */ 142,
+  /* 13347 */ 160,
+  /* 13387 */ 170,
+  /* 13565 */ 143,
+  /* 13643 */ 168,
+  /* 13821 */ 150,
+  /* 14122 */ 171,
+  /* 14144 */ 169,
+  /* 14374 */ 179,
+  /* 14845 */ 182,
+  /* 16966 */ 188,
+  /* 17515 */ 177,
+  /* 17917 */ 133,
+  /* 18173 */ 136,
+  /* 18795 */ 152,
+  /* 19453 */ 129,
+  /* 19709 */ 128,
+  /* 19965 */ 130,
+  /* 20075 */ 167,
+  /* 20221 */ 131,
+  /* 20477 */ 153,
+  /* 20587 */ 165,
+  /* 20733 */ 178,
+  /* 21328 */ 191,
+  /* 22013 */ 161,
+  /* 22269 */ 164,
+  /* 22525 */ 176,
+  /* 22767 */ 187,
+  /* 22768 */ 186,
+  /* 22773 */ 183,
+  /* 22776 */ 185,
+  /* 22777 */ 184,
+  /* 22781 */ 172,
+  /* 23037 */ 144,
+  /* 23293 */ 145,
+  /* 23549 */ 146,
+  /* 23805 */ 147,
+  /* 24061 */ 148,
+  /* 24317 */ 149,
+  /* 24573 */ 189,
+  /* 24829 */ 190,
+  /* 25085 */ 151,
+  /* 25195 */ 154,
+  /* 25707 */ 157,
+  /* 25853 */ 137,
+  /* 26621 */ 192,
+  /* 26731 */ 173,
+  /* 26877 */ 193,
+  /* 26917 */ 163,
+  /* 27755 */ 159,
+  /* 29291 */ 162,
+  /* 30059 */ 155,
+};
+
+// The highest index for which
+// nv_cmds[idx].cmd_char == nv_cmd_idx[nv_cmds[idx].cmd_char]
+static const int nv_max_linear = 126;
--- a/src/proto/normal.pro
+++ b/src/proto/normal.pro
@@ -1,5 +1,5 @@
 /* normal.c */
-void init_normal_cmds(void);
+void f_internal_get_nv_cmdchar(typval_T *argvars, typval_T *rettv);
 void normal_cmd(oparg_T *oap, int toplevel);
 void check_visual_highlight(void);
 void end_visual_mode(void);
--- a/src/version.c
+++ b/src/version.c
@@ -751,6 +751,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    4252,
+/**/
     4251,
 /**/
     4250,