patch 8.2.4861: it is not easy to restore saved mappings v8.2.4861
authorErnie Rael <errael@raelity.com>
Wed, 4 May 2022 14:40:22 +0000 (15:40 +0100)
committerBram Moolenaar <Bram@vim.org>
Wed, 4 May 2022 14:40:22 +0000 (15:40 +0100)
Problem:    It is not easy to restore saved mappings.
Solution:   Make mapset() accept a dict argument. (Ernie Rael, closes #10295)

runtime/doc/builtin.txt
src/errors.h
src/evalfunc.c
src/map.c
src/proto/typval.pro
src/testdir/test_map_functions.vim
src/testdir/test_vim9_builtin.vim
src/typval.c
src/version.c

index beeb7364917a419486b840d3f0e79016555d91c2..12fb509a7826d96925d05fd6fcc6936e8e2e9151 100644 (file)
@@ -5310,6 +5310,7 @@ maparg({name} [, {mode} [, {abbr} [, {dict}]]])                   *maparg()*
                  "lnum"     The line number in "sid", zero if unknown.
                  "nowait"   Do not wait for other, longer mappings.
                             (|:map-<nowait>|).
+                 "abbr"     True if this is an |abbreviation|.
 
                The dictionary can be used to restore a mapping with
                |mapset()|.
@@ -5380,9 +5381,18 @@ mapnew({expr1}, {expr2})                                 *mapnew()*
 
 
 mapset({mode}, {abbr}, {dict})                                 *mapset()*
-               Restore a mapping from a dictionary returned by |maparg()|.
-               {mode} and {abbr} should be the same as for the call to
-               |maparg()|. *E460*
+mapset({dict})
+               Restore a mapping from a dictionary, possibly returned by
+               |maparg()| or |maplist()|.  A buffer mapping, when dict.buffer
+               is true, is set on the current buffer; it is up to the caller
+               to insure that the intended buffer is the current buffer. This
+               feature allows copying mappings from one buffer to another.
+               The dict.mode value may restore a single mapping that covers
+               more than one mode, like with mode values of '!', ' ', 'nox',
+               or 'v'. *E1276*
+
+               In the first form, {mode} and {abbr} should be the same as
+               for the call to |maparg()|. *E460*
                {mode} is used to define the mode in which the mapping is set,
                not the "mode" entry in {dict}.
                Example for saving and restoring a mapping: >
@@ -5391,8 +5401,22 @@ mapset({mode}, {abbr}, {dict})                                   *mapset()*
                        ...
                        call mapset('n', 0, save_map)
 <              Note that if you are going to replace a map in several modes,
-               e.g. with `:map!`, you need to save the mapping for all of
-               them, since they can differ.
+               e.g. with `:map!`, you need to save/restore the mapping for
+               all of them, when they might differ.
+
+               In the second form, with {dict} as the only argument, mode
+               and abbr are taken from the dict.
+               Example: >
+                       vim9script
+                       var save_maps = maplist()->filter(
+                                               (_, m) => m.lhs == 'K')
+                       nnoremap K somethingelse
+                       cnoremap K somethingelse2
+                       # ...
+                       unmap K
+                       for d in save_maps
+                           mapset(d)
+                       endfor
 
 
 match({expr}, {pat} [, {start} [, {count}]])                   *match()*
index 0c6134034d9469c291dbbf7a04a693db20d6a433..fc815ee315496573564c6cb427aa2fcd21f15ffd 100644 (file)
@@ -3262,4 +3262,6 @@ EXTERN char e_no_script_file_name_to_substitute_for_script[]
 #ifdef FEAT_EVAL
 EXTERN char e_string_or_function_required_for_arrow_parens_expr[]
        INIT(= N_("E1275: String or function required for ->(expr)"));
+EXTERN char e_illegal_map_mode_string_str[]
+       INIT(= N_("E1276: Illegal map mode string: '%s'"));
 #endif
index 4456f696db6c000e021f8740046aeaaa1f922393..f0650260d343d1ab2be00ab3c17f1bac27655aa9 100644 (file)
@@ -421,6 +421,21 @@ arg_string_or_list_any(type_T *type, type_T *decl_type UNUSED, argcontext_T *con
     return FAIL;
 }
 
+/*
+ * Check "type" is a string or a dict of 'any'
+ */
+    static int
+arg_string_or_dict_any(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+{
+    if (type->tt_type == VAR_ANY
+           || type->tt_type == VAR_UNKNOWN
+           || type->tt_type == VAR_STRING
+           || type->tt_type == VAR_DICT)
+       return OK;
+    arg_type_mismatch(&t_string, type, context->arg_idx + 1);
+    return FAIL;
+}
+
 /*
  * Check "type" is a string or a blob
  */
@@ -998,8 +1013,8 @@ static argcheck_T arg3_string[] = {arg_string, arg_string, arg_string};
 static argcheck_T arg3_string_any_dict[] = {arg_string, NULL, arg_dict_any};
 static argcheck_T arg3_string_any_string[] = {arg_string, NULL, arg_string};
 static argcheck_T arg3_string_bool_bool[] = {arg_string, arg_bool, arg_bool};
-static argcheck_T arg3_string_bool_dict[] = {arg_string, arg_bool, arg_dict_any};
 static argcheck_T arg3_string_number_bool[] = {arg_string, arg_number, arg_bool};
+static argcheck_T arg3_string_or_dict_bool_dict[] = {arg_string_or_dict_any, arg_bool, arg_dict_any};
 static argcheck_T arg3_string_string_bool[] = {arg_string, arg_string, arg_bool};
 static argcheck_T arg3_string_string_dict[] = {arg_string, arg_string, arg_dict_any};
 static argcheck_T arg3_string_string_number[] = {arg_string, arg_string, arg_number};
@@ -2053,7 +2068,7 @@ static funcentry_T global_functions[] =
                        ret_list_dict_any,  f_maplist},
     {"mapnew",         2, 2, FEARG_1,      arg2_mapnew,
                        ret_first_cont,     f_mapnew},
-    {"mapset",         3, 3, FEARG_1,      arg3_string_bool_dict,
+    {"mapset",         1, 3, FEARG_1,      arg3_string_or_dict_bool_dict,
                        ret_void,           f_mapset},
     {"match",          2, 4, FEARG_1,      arg24_match_func,
                        ret_any,            f_match},
index ad2e8313e1e96ca5f1bdeb87e55e4bb5124d0983..6ea73ebce2934b491f18d0c262ff872381a6676d 100644 (file)
--- a/src/map.c
+++ b/src/map.c
@@ -2283,7 +2283,8 @@ mapblock2dict(
        mapblock_T  *mp,
        dict_T      *dict,
        char_u      *lhsrawalt,     // may be NULL
-       int         buffer_local)   // false if not buffer local mapping
+       int         buffer_local,   // false if not buffer local mapping
+       int         abbr)           // true if abbreviation
 {
     char_u         *lhs = str2special_save(mp->m_keys, TRUE);
     char_u         *mapmode = map_mode_to_chars(mp->m_mode);
@@ -2307,6 +2308,7 @@ mapblock2dict(
     dict_add_number(dict, "buffer", (long)buffer_local);
     dict_add_number(dict, "nowait", mp->m_nowait ? 1L : 0L);
     dict_add_string(dict, "mode", mapmode);
+    dict_add_number(dict, "abbr", abbr ? 1L : 0L);
 
     vim_free(mapmode);
 }
@@ -2381,7 +2383,8 @@ get_maparg(typval_T *argvars, typval_T *rettv, int exact)
     }
     else if (rettv_dict_alloc(rettv) != FAIL && rhs != NULL)
        mapblock2dict(mp, rettv->vval.v_dict,
-                         did_simplify ? keys_simplified : NULL, buffer_local);
+                         did_simplify ? keys_simplified : NULL,
+                         buffer_local, abbr);
 
     vim_free(keys_buf);
     vim_free(alt_keys_buf);
@@ -2448,7 +2451,8 @@ f_maplist(typval_T *argvars UNUSED, typval_T *rettv)
                vim_free(lhs);
 
                mapblock2dict(mp, d,
-                                did_simplify ? keys_buf : NULL, buffer_local);
+                                did_simplify ? keys_buf : NULL,
+                                buffer_local, abbr);
                vim_free(keys_buf);
            }
        }
@@ -2489,6 +2493,56 @@ f_mapcheck(typval_T *argvars, typval_T *rettv)
     get_maparg(argvars, rettv, FALSE);
 }
 
+/*
+ * Get the mapping mode from the mode string.
+ * It may contain multiple characters, eg "nox", or "!", or ' '
+ * Return 0 if there is an error.
+ */
+    static int
+get_map_mode_string(char_u *mode_string, int abbr)
+{
+    char_u     *p = mode_string;
+    int                mode = 0;
+    int                tmode;
+    int                modec;
+    const int  MASK_V = VISUAL + SELECTMODE;
+    const int  MASK_MAP = VISUAL + SELECTMODE + NORMAL + OP_PENDING;
+    const int  MASK_BANG = INSERT + CMDLINE;
+
+    if (*p == NUL)
+       p = (char_u *)" ";      // compatibility
+    while ((modec = *p++))
+    {
+       switch (modec)
+       {
+           case 'i': tmode = INSERT;           break;
+           case 'l': tmode = LANGMAP;          break;
+           case 'c': tmode = CMDLINE;          break;
+           case 'n': tmode = NORMAL;           break;
+           case 'x': tmode = VISUAL;           break;
+           case 's': tmode = SELECTMODE;       break;
+           case 'o': tmode = OP_PENDING;       break;
+           case 't': tmode = TERMINAL;         break;
+           case 'v': tmode = MASK_V;           break;
+           case '!': tmode = MASK_BANG;        break;
+           case ' ': tmode = MASK_MAP;         break;
+           default:
+                     return 0; // error, unknown mode character
+       }
+       mode |= tmode;
+    }
+    if ((abbr && (mode & ~MASK_BANG) != 0)
+       || (!abbr && (mode & (mode-1)) != 0 // more than one bit set
+           && (
+               // false if multiple bits set in mode and mode is fully
+               // contained in one mask
+               !(((mode & MASK_BANG) != 0 && (mode & ~MASK_BANG) == 0)
+                   || ((mode & MASK_MAP) != 0 && (mode & ~MASK_MAP) == 0)))))
+       return 0;
+
+    return mode;
+}
+
 /*
  * "mapset()" function
  */
@@ -2518,25 +2572,51 @@ f_mapset(typval_T *argvars, typval_T *rettv UNUSED)
     mapblock_T  **abbr_table = &first_abbr;
     int                nowait;
     char_u     *arg;
+    int                dict_only;
 
+    // If first arg is a dict, then that's the only arg permitted.
+    dict_only = argvars[0].v_type == VAR_DICT;
     if (in_vim9script()
-           && (check_for_string_arg(argvars, 0) == FAIL
-               || check_for_bool_arg(argvars, 1) == FAIL
-               || check_for_dict_arg(argvars, 2) == FAIL))
+           && (check_for_string_or_dict_arg(argvars, 0) == FAIL
+               || (dict_only && check_for_unknown_arg(argvars, 1) == FAIL)
+               || (!dict_only
+                   && (check_for_string_arg(argvars, 0) == FAIL
+                       || check_for_bool_arg(argvars, 1) == FAIL
+                       || check_for_dict_arg(argvars, 2) == FAIL))))
        return;
 
-    which = tv_get_string_buf_chk(&argvars[0], buf);
-    if (which == NULL)
-       return;
-    mode = get_map_mode(&which, 0);
-    is_abbr = (int)tv_get_bool(&argvars[1]);
+    if (dict_only)
+    {
+       d = argvars[0].vval.v_dict;
+       which = dict_get_string(d, (char_u *)"mode", FALSE);
+       is_abbr = dict_get_bool(d, (char_u *)"abbr", -1);
+       if (which == NULL || is_abbr < 0)
+       {
+           emsg(_(e_entries_missing_in_mapset_dict_argument));
+           return;
+       }
+    }
+    else
+    {
+       which = tv_get_string_buf_chk(&argvars[0], buf);
+       if (which == NULL)
+           return;
+       is_abbr = (int)tv_get_bool(&argvars[1]);
 
-    if (argvars[2].v_type != VAR_DICT)
+       if (argvars[2].v_type != VAR_DICT)
+       {
+           emsg(_(e_dictionary_required));
+           return;
+       }
+       d = argvars[2].vval.v_dict;
+    }
+    mode = get_map_mode_string(which, is_abbr);
+    if (mode == 0)
     {
-       emsg(_(e_key_not_present_in_dictionary));
+       semsg(_(e_illegal_map_mode_string_str), which);
        return;
     }
-    d = argvars[2].vval.v_dict;
+
 
     // Get the values in the same order as above in get_maparg().
     lhs = dict_get_string(d, (char_u *)"lhs", FALSE);
index b744bbcad13d0801afc2979920271cce1ba9134d..459cf7c73846cf93721d42a5527f978955969a7f 100644 (file)
@@ -9,6 +9,7 @@ varnumber_T tv_get_number_chk(typval_T *varp, int *denote);
 varnumber_T tv_get_bool(typval_T *varp);
 varnumber_T tv_get_bool_chk(typval_T *varp, int *denote);
 float_T tv_get_float(typval_T *varp);
+int check_for_unknown_arg(typval_T *args, int idx);
 int check_for_string_arg(typval_T *args, int idx);
 int check_for_nonempty_string_arg(typval_T *args, int idx);
 int check_for_opt_string_arg(typval_T *args, int idx);
index 66f3479034128758434d613e8a454b81ac18cfd9..21c899048a1988a7cce85fac180551646b1ecb86 100644 (file)
@@ -19,13 +19,13 @@ func Test_maparg()
         \ 'lhsraw': "foo\x80\xfc\x04V", 'lhsrawalt': "foo\x16",
         \ 'mode': ' ', 'nowait': 0, 'expr': 0, 'sid': sid, 'scriptversion': 1,
         \ 'lnum': lnum + 1, 
-       \ 'rhs': 'is<F4>foo', 'buffer': 0},
+       \ 'rhs': 'is<F4>foo', 'buffer': 0, 'abbr': 0},
        \ maparg('foo<C-V>', '', 0, 1))
   call assert_equal({'silent': 1, 'noremap': 1, 'script': 1, 'lhs': 'bar',
         \ 'lhsraw': 'bar', 'mode': 'v',
         \ 'nowait': 0, 'expr': 1, 'sid': sid, 'scriptversion': 1,
         \ 'lnum': lnum + 2,
-       \ 'rhs': 'isbar', 'buffer': 1},
+       \ 'rhs': 'isbar', 'buffer': 1, 'abbr': 0},
         \ 'bar'->maparg('', 0, 1))
   let lnum = expand('<sflnum>')
   map <buffer> <nowait> foo bar
@@ -33,7 +33,7 @@ func Test_maparg()
         \ 'lhsraw': 'foo', 'mode': ' ',
         \ 'nowait': 1, 'expr': 0, 'sid': sid, 'scriptversion': 1,
         \ 'lnum': lnum + 1, 'rhs': 'bar',
-       \ 'buffer': 1},
+       \ 'buffer': 1, 'abbr': 0},
         \ maparg('foo', '', 0, 1))
   let lnum = expand('<sflnum>')
   tmap baz foo
@@ -41,8 +41,17 @@ func Test_maparg()
         \ 'lhsraw': 'baz', 'mode': 't',
         \ 'nowait': 0, 'expr': 0, 'sid': sid, 'scriptversion': 1,
         \ 'lnum': lnum + 1, 'rhs': 'foo',
-       \ 'buffer': 0},
+       \ 'buffer': 0, 'abbr': 0},
         \ maparg('baz', 't', 0, 1))
+  let lnum = expand('<sflnum>')
+  iab A B
+  call assert_equal({'silent': 0, 'noremap': 0, 'script': 0, 'lhs': 'A',
+        \ 'lhsraw': 'A', 'mode': 'i',
+        \ 'nowait': 0, 'expr': 0, 'sid': sid, 'scriptversion': 1,
+        \ 'lnum': lnum + 1, 'rhs': 'B',
+       \ 'buffer': 0, 'abbr': 1},
+        \ maparg('A', 'i', 1, 1))
+  iuna A
 
   map abc x<char-114>x
   call assert_equal("xrx", maparg('abc'))
@@ -250,10 +259,156 @@ func Test_mapset()
   bwipe!
 
   call assert_fails('call mapset([], v:false, {})', 'E730:')
-  call assert_fails('call mapset("i", 0, "")', 'E716:')
+  call assert_fails('call mapset("i", 0, "")', 'E715:')
   call assert_fails('call mapset("i", 0, {})', 'E460:')
 endfunc
 
+def Test_mapset_arg1_dir()
+  # This test is mostly about get_map_mode_string.
+  # Once the code gets past that, it's common with the 3 arg mapset.
+
+  # GetModes() return list of modes for 'XZ' lhs using maplist.
+  # There is one list item per mapping
+  def GetModes(abbr: bool = false): list<string>
+    return maplist(abbr)->filter((_, m) => m.lhs == 'XZ')
+                ->mapnew((_, m) => m.mode)
+  enddef
+
+  const unmap_cmds = [ 'unmap', 'unmap!', 'tunmap', 'lunmap' ]
+  def UnmapAll(lhs: string)
+    for cmd in unmap_cmds
+      try | execute(cmd .. ' ' .. lhs) | catch /E31/ | endtry
+    endfor
+  enddef
+
+  var tmap: dict<any>
+
+  # some mapset(mode, abbr, dict) tests using get_map_mode_str
+  map XZ x
+  tmap = maplist()->filter((_, m) => m.lhs == 'XZ')[0]->copy()
+  # this splits the mapping into 2 mappings
+  mapset('ox', false, tmap)
+  assert_equal(2, len(GetModes()))
+  mapset('o', false, tmap)
+  assert_equal(3, len(GetModes()))
+  # test that '' acts like ' ', and that the 3 mappings become 1
+  mapset('', false, tmap)
+  assert_equal([' '], GetModes())
+  # dict's mode/abbr are ignored
+  UnmapAll('XZ')
+  tmap.mode = '!'
+  tmap.abbr = true
+  mapset('o', false, tmap)
+  assert_equal(['o'], GetModes())
+
+  # test the 3 arg version handles bad mode string, dict not used
+  assert_fails("mapset('vi', false, {})", 'E1276:')
+
+
+  # get the abbreviations out of the way
+  abbreviate XZ ZX
+  tmap = maplist(true)->filter((_, m) => m.lhs == 'XZ')[0]->copy()
+
+  abclear
+  # 'ic' is the default ab command, shows up as '!'
+  tmap.mode = 'ic'
+  mapset(tmap)
+  assert_equal(['!'], GetModes(true))
+
+  abclear
+  tmap.mode = 'i'
+  mapset(tmap)
+  assert_equal(['i'], GetModes(true))
+
+  abclear
+  tmap.mode = 'c'
+  mapset(tmap)
+  assert_equal(['c'], GetModes(true))
+
+  abclear
+  tmap.mode = '!'
+  mapset(tmap)
+  assert_equal(['!'], GetModes(true))
+
+  assert_fails("mapset({mode: ' !', abbr: 1})", 'E1276:')
+  assert_fails("mapset({mode: 'cl', abbr: 1})", 'E1276:')
+  assert_fails("mapset({mode: 'in', abbr: 1})", 'E1276:')
+
+  # the map commands
+  map XZ x
+  tmap = maplist()->filter((_, m) => m.lhs == 'XZ')[0]->copy()
+
+  # try the combos
+  UnmapAll('XZ')
+  # 'nxso' is ' ', the unadorned :map
+  tmap.mode = 'nxso'
+  mapset(tmap)
+  assert_equal([' '], GetModes())
+
+  UnmapAll('XZ')
+  # 'ic' is '!'
+  tmap.mode = 'ic'
+  mapset(tmap)
+  assert_equal(['!'], GetModes())
+
+  UnmapAll('XZ')
+  # 'xs' is really 'v'
+  tmap.mode = 'xs'
+  mapset(tmap)
+  assert_equal(['v'], GetModes())
+
+  # try the individual modes
+  UnmapAll('XZ')
+  tmap.mode = 'n'
+  mapset(tmap)
+  assert_equal(['n'], GetModes())
+
+  UnmapAll('XZ')
+  tmap.mode = 'x'
+  mapset(tmap)
+  assert_equal(['x'], GetModes())
+
+  UnmapAll('XZ')
+  tmap.mode = 's'
+  mapset(tmap)
+  assert_equal(['s'], GetModes())
+
+  UnmapAll('XZ')
+  tmap.mode = 'o'
+  mapset(tmap)
+  assert_equal(['o'], GetModes())
+
+  UnmapAll('XZ')
+  tmap.mode = 'i'
+  mapset(tmap)
+  assert_equal(['i'], GetModes())
+
+  UnmapAll('XZ')
+  tmap.mode = 'c'
+  mapset(tmap)
+  assert_equal(['c'], GetModes())
+
+  UnmapAll('XZ')
+  tmap.mode = 't'
+  mapset(tmap)
+  assert_equal(['t'], GetModes())
+
+  UnmapAll('XZ')
+  tmap.mode = 'l'
+  mapset(tmap)
+  assert_equal(['l'], GetModes())
+
+  UnmapAll('XZ')
+
+  # get errors for modes that can't be in one mapping
+  assert_fails("mapset({mode: 'nxsoi', abbr: 0})", 'E1276:')
+  assert_fails("mapset({mode: ' !', abbr: 0})", 'E1276:')
+  assert_fails("mapset({mode: 'ix', abbr: 0})", 'E1276:')
+  assert_fails("mapset({mode: 'tl', abbr: 0})", 'E1276:')
+  assert_fails("mapset({mode: ' l', abbr: 0})", 'E1276:')
+  assert_fails("mapset({mode: ' t', abbr: 0})", 'E1276:')
+enddef
+
 func Check_ctrlb_map(d, check_alt)
   call assert_equal('<C-B>', a:d.lhs)
   if a:check_alt
index e15993d7205b9a28a9447e28bfe4e20125a5a231..7b64fc444a91d2c0c913d539c088aaacaffd0e1e 100644 (file)
@@ -2463,7 +2463,8 @@ def Test_maparg()
         sid: SID(),
         scriptversion: 999999,
         rhs: 'bar',
-        buffer: 0})
+        buffer: 0,
+        abbr: 0})
   unmap foo
   v9.CheckDefAndScriptFailure(['maparg(1)'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1'])
   v9.CheckDefAndScriptFailure(['maparg("a", 2)'], ['E1013: Argument 2: type mismatch, expected string but got number', 'E1174: String required for argument 2'])
@@ -2545,7 +2546,7 @@ def Test_mapnew()
 enddef
 
 def Test_mapset()
-  v9.CheckDefAndScriptFailure(['mapset(1, true, {})'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1'])
+  v9.CheckDefAndScriptFailure(['mapset(1, true, {})'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1223: String or Dictionary required for argument 1'])
   v9.CheckDefAndScriptFailure(['mapset("a", 2, {})'], ['E1013: Argument 2: type mismatch, expected bool but got number', 'E1212: Bool required for argument 2'])
   v9.CheckDefAndScriptFailure(['mapset("a", false, [])'], ['E1013: Argument 3: type mismatch, expected dict<any> but got list<unknown>', 'E1206: Dictionary required for argument 3'])
 enddef
index 4f6e41dfdd6c99709225e9c72863bb1bddfd13e9..b131ba2c1516dbeecd62470b4cdb6b5678ee2807 100644 (file)
@@ -359,6 +359,20 @@ tv_get_float(typval_T *varp)
 }
 #endif
 
+/*
+ * Give an error and return FAIL unless "args[idx]" is unknown
+ */
+    int
+check_for_unknown_arg(typval_T *args, int idx)
+{
+    if (args[idx].v_type != VAR_UNKNOWN)
+    {
+       semsg(_(e_too_many_arguments), idx + 1);
+       return FAIL;
+    }
+    return OK;
+}
+
 /*
  * Give an error and return FAIL unless "args[idx]" is a string.
  */
index c3ecb62554ccaf50d48dc9ab2f499edaf4170f6a..51db458263560f60b4c74492ca50fdf0ee01861b 100644 (file)
@@ -746,6 +746,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    4861,
 /**/
     4860,
 /**/