# HG changeset patch # User Bram Moolenaar # Date 1651675503 -7200 # Node ID 38f7a132bba35008d670be0ba88906831c711047 # Parent 1bb2e8f5cc4b71b36003d9791e45012553b34055 patch 8.2.4861: it is not easy to restore saved mappings Commit: https://github.com/vim/vim/commit/51d04d16f21e19d6eded98f9530d84089102f925 Author: Ernie Rael Date: Wed May 4 15:40:22 2022 +0100 patch 8.2.4861: it is not easy to restore saved mappings Problem: It is not easy to restore saved mappings. Solution: Make mapset() accept a dict argument. (Ernie Rael, closes https://github.com/vim/vim/issues/10295) diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -5310,6 +5310,7 @@ maparg({name} [, {mode} [, {abbr} [, {di "lnum" The line number in "sid", zero if unknown. "nowait" Do not wait for other, longer mappings. (|:map-|). + "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}) *maps ... 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()* diff --git a/src/errors.h b/src/errors.h --- a/src/errors.h +++ b/src/errors.h @@ -3262,4 +3262,6 @@ EXTERN char e_no_script_file_name_to_sub #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 diff --git a/src/evalfunc.c b/src/evalfunc.c --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -422,6 +422,21 @@ arg_string_or_list_any(type_T *type, typ } /* + * 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 */ static int @@ -998,8 +1013,8 @@ static argcheck_T arg3_string[] = {arg_s 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}, diff --git a/src/map.c b/src/map.c --- 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 * } 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, typv 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); } } @@ -2490,6 +2494,56 @@ f_mapcheck(typval_T *argvars, typval_T * } /* + * 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 */ void @@ -2518,25 +2572,51 @@ f_mapset(typval_T *argvars, typval_T *re 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); diff --git a/src/proto/typval.pro b/src/proto/typval.pro --- a/src/proto/typval.pro +++ b/src/proto/typval.pro @@ -9,6 +9,7 @@ varnumber_T tv_get_number_chk(typval_T * 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); diff --git a/src/testdir/test_map_functions.vim b/src/testdir/test_map_functions.vim --- a/src/testdir/test_map_functions.vim +++ b/src/testdir/test_map_functions.vim @@ -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': 'isfoo', 'buffer': 0}, + \ 'rhs': 'isfoo', 'buffer': 0, 'abbr': 0}, \ maparg('foo', '', 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('') map 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('') 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('') + 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 xx 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 + 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 + + # 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('', a:d.lhs) if a:check_alt diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim --- a/src/testdir/test_vim9_builtin.vim +++ b/src/testdir/test_vim9_builtin.vim @@ -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 but got list', 'E1206: Dictionary required for argument 3']) enddef diff --git a/src/typval.c b/src/typval.c --- a/src/typval.c +++ b/src/typval.c @@ -360,6 +360,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. */ int diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -747,6 +747,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 4861, +/**/ 4860, /**/ 4859,