vim-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 vim/vim#10295)

51d04d16f2

Co-authored-by: Ernie Rael <errael@raelity.com>
This commit is contained in:
zeertzjq 2023-11-09 19:48:50 +08:00
parent d4dbfb092b
commit f748a73a35
9 changed files with 350 additions and 41 deletions

View File

@ -4185,6 +4185,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()|.
@ -4246,9 +4247,17 @@ mapnew({expr1}, {expr2}) *mapnew()*
don't want that use |deepcopy()| first.
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*
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 ensure 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: >vim
@ -4257,8 +4266,21 @@ 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: >vim
let save_maps = maplist()->filter(
\ {_, m -> m.lhs == 'K'})
nnoremap K somethingelse
cnoremap K somethingelse2
" ...
unmap K
for d in save_maps
call mapset(d)
endfor
match({expr}, {pat} [, {start} [, {count}]]) *match()*
When {expr} is a |List| then this returns the index of the

View File

@ -955,7 +955,7 @@ operator to add quotes around text in the current line: >
\ ->setline(".")}'<CR>g@
==============================================================================
2. Abbreviations *abbreviations* *Abbreviations*
2. Abbreviations *abbreviation* *abbreviations* *Abbreviations*
Abbreviations are used in Insert mode, Replace mode and Command-line mode.
If you enter a word that is an abbreviation, it is replaced with the word it

View File

@ -5046,6 +5046,7 @@ function vim.fn.map(expr1, expr2) end
--- "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()|.
@ -5124,9 +5125,17 @@ function vim.fn.maplist() end
--- @return any
function vim.fn.mapnew(expr1, expr2) end
--- Restore a mapping from a dictionary returned by |maparg()|.
--- {mode} and {abbr} should be the same as for the call to
--- |maparg()|. *E460*
--- 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 ensure 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: >vim
@ -5135,12 +5144,25 @@ function vim.fn.mapnew(expr1, expr2) end
--- " ...
--- 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: >vim
--- let save_maps = maplist()->filter(
--- \ {_, m -> m.lhs == 'K'})
--- nnoremap K somethingelse
--- cnoremap K somethingelse2
--- " ...
--- unmap K
--- for d in save_maps
--- call mapset(d)
--- endfor
---
--- @param mode string
--- @param abbr any
--- @param dict any
--- @param abbr? any
--- @param dict? any
--- @return any
function vim.fn.mapset(mode, abbr, dict) end

View File

@ -1116,8 +1116,7 @@ func s:DeleteCommands()
if exists('s:k_map_saved')
if !empty(s:k_map_saved) && !s:k_map_saved.buffer
nunmap K
" call mapset(s:k_map_saved)
call mapset('n', 0, s:k_map_saved)
call mapset(s:k_map_saved)
elseif empty(s:k_map_saved)
nunmap K
endif
@ -1126,8 +1125,7 @@ func s:DeleteCommands()
if exists('s:plus_map_saved')
if !empty(s:plus_map_saved) && !s:plus_map_saved.buffer
nunmap +
" call mapset(s:plus_map_saved)
call mapset('n', 0, s:plus_map_saved)
call mapset(s:plus_map_saved)
elseif empty(s:plus_map_saved)
nunmap +
endif
@ -1136,8 +1134,7 @@ func s:DeleteCommands()
if exists('s:minus_map_saved')
if !empty(s:minus_map_saved) && !s:minus_map_saved.buffer
nunmap -
" call mapset(s:minus_map_saved)
call mapset('n', 0, s:minus_map_saved)
call mapset(s:minus_map_saved)
elseif empty(s:minus_map_saved)
nunmap -
endif

View File

@ -6194,6 +6194,7 @@ M.funcs = {
"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()|.
@ -6287,12 +6288,20 @@ M.funcs = {
signature = 'mapnew({expr1}, {expr2})',
},
mapset = {
args = 3,
args = { 1, 3 },
base = 1,
desc = [=[
Restore a mapping from a dictionary returned by |maparg()|.
{mode} and {abbr} should be the same as for the call to
|maparg()|. *E460*
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 ensure 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: >vim
@ -6301,8 +6310,21 @@ M.funcs = {
" ...
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: >vim
let save_maps = maplist()->filter(
\ {_, m -> m.lhs == 'K'})
nnoremap K somethingelse
cnoremap K somethingelse2
" ...
unmap K
for d in save_maps
call mapset(d)
endfor
]=],
name = 'mapset',
params = { { 'mode', 'string' }, { 'abbr', 'any' }, { 'dict', 'any' } },

View File

@ -123,6 +123,8 @@ static const char e_mapping_already_exists_for_str[]
= N_("E227: Mapping already exists for %s");
static const char e_entries_missing_in_mapset_dict_argument[]
= N_("E460: Entries missing in mapset() dict argument");
static const char e_illegal_map_mode_string_str[]
= N_("E1276: Illegal map mode string: '%s'");
/// Get the start of the hashed map list for "state" and first character "c".
mapblock_T *get_maphash_list(int state, int c)
@ -2070,11 +2072,12 @@ void f_hasmapto(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
///
/// @param mp The maphash that contains the mapping information
/// @param buffer_value The "buffer" value
/// @param abbr True if abbreviation
/// @param compatible True for compatible with old maparg() dict
///
/// @return A Dictionary.
static Dictionary mapblock_fill_dict(const mapblock_T *const mp, const char *lhsrawalt,
const int buffer_value, const bool compatible)
const int buffer_value, const bool abbr, const bool compatible)
FUNC_ATTR_NONNULL_ARG(1)
{
Dictionary dict = ARRAY_DICT_INIT;
@ -2121,6 +2124,7 @@ static Dictionary mapblock_fill_dict(const mapblock_T *const mp, const char *lhs
PUT(dict, "replace_keycodes", INTEGER_OBJ(1));
}
PUT(dict, "mode", CSTR_AS_OBJ(mapmode));
PUT(dict, "abbr", INTEGER_OBJ(abbr ? 1 : 0));
return dict;
}
@ -2193,7 +2197,7 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact)
if (mp != NULL && (rhs != NULL || rhs_lua != LUA_NOREF)) {
Dictionary dict = mapblock_fill_dict(mp,
did_simplify ? keys_simplified : NULL,
buffer_local, true);
buffer_local, abbr, true);
(void)object_to_vim(DICTIONARY_OBJ(dict), rettv, NULL);
api_free_dictionary(dict);
} else {
@ -2206,21 +2210,99 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact)
xfree(alt_keys_buf);
}
/// 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(const char *const mode_string, const bool abbr)
{
const char *p = mode_string;
const int MASK_V = MODE_VISUAL | MODE_SELECT;
const int MASK_MAP = MODE_VISUAL | MODE_SELECT | MODE_NORMAL | MODE_OP_PENDING;
const int MASK_BANG = MODE_INSERT | MODE_CMDLINE;
if (*p == NUL) {
p = " "; // compatibility
}
int mode = 0;
int modec;
while ((modec = (uint8_t)(*p++))) {
int tmode;
switch (modec) {
case 'i':
tmode = MODE_INSERT; break;
case 'l':
tmode = MODE_LANGMAP; break;
case 'c':
tmode = MODE_CMDLINE; break;
case 'n':
tmode = MODE_NORMAL; break;
case 'x':
tmode = MODE_VISUAL; break;
case 's':
tmode = MODE_SELECT; break;
case 'o':
tmode = MODE_OP_PENDING; break;
case 't':
tmode = MODE_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 f_mapset(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
const char *which;
char buf[NUMBUFLEN];
const char *which = tv_get_string_buf_chk(&argvars[0], buf);
int is_abbr;
dict_T *d;
// If first arg is a dict, then that's the only arg permitted.
const bool dict_only = argvars[0].v_type == VAR_DICT;
if (dict_only) {
d = argvars[0].vval.v_dict;
which = tv_dict_get_string(d, "mode", false);
is_abbr = (int)tv_dict_get_bool(d, "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;
}
const int mode = get_map_mode((char **)&which, 0);
const bool is_abbr = tv_get_number(&argvars[1]) != 0;
is_abbr = (int)tv_get_bool(&argvars[1]);
if (tv_check_for_dict_arg(argvars, 2) == FAIL) {
return;
}
dict_T *d = argvars[2].vval.v_dict;
d = argvars[2].vval.v_dict;
}
const int mode = get_map_mode_string(which, is_abbr);
if (mode == 0) {
semsg(_(e_illegal_map_mode_string_str), which);
return;
}
// Get the values in the same order as above in get_maparg().
char *lhs = tv_dict_get_string(d, "lhs", false);
@ -2322,7 +2404,7 @@ void f_maplist(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
Dictionary dict = mapblock_fill_dict(mp,
did_simplify ? keys_buf : NULL,
buffer_local, true);
buffer_local, abbr, true);
typval_T d = TV_INITIAL_VALUE;
(void)object_to_vim(DICTIONARY_OBJ(dict), &d, NULL);
assert(d.v_type == VAR_DICT);
@ -2751,7 +2833,8 @@ ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf)
// Check for correct mode
if (int_mode & current_maphash->m_mode) {
ADD(mappings,
DICTIONARY_OBJ(mapblock_fill_dict(current_maphash, NULL, buffer_value, false)));
DICTIONARY_OBJ(mapblock_fill_dict(current_maphash, NULL,
buffer_value, false, false)));
}
}
}

View File

@ -36,6 +36,7 @@ describe('nvim_get_keymap', function()
buffer=0,
nowait=0,
mode='n',
abbr=0,
noremap=1,
lnum=0,
}
@ -262,6 +263,7 @@ describe('nvim_get_keymap', function()
scriptversion=1,
buffer=0,
nowait=0,
abbr=0,
noremap=1,
lnum=0,
}
@ -325,6 +327,7 @@ describe('nvim_get_keymap', function()
lhsraw='| |',
rhs='| |',
mode='n',
abbr=0,
script=0,
silent=0,
expr=0,
@ -372,6 +375,7 @@ describe('nvim_get_keymap', function()
buffer=0,
nowait=0,
mode='n',
abbr=0,
noremap=0,
lnum=0,
}, mapargs[1])
@ -391,6 +395,7 @@ describe('nvim_get_keymap', function()
buffer=0,
nowait=0,
mode='n',
abbr=0,
noremap=0,
lnum=0,
desc='map description'
@ -426,6 +431,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function()
local to_return = {}
to_return.mode = normalize_mapmode(mode, true)
to_return.abbr = mode:sub(-1) == 'a' and 1 or 0
to_return.noremap = not opts.noremap and 0 or 1
to_return.lhs = lhs
to_return.rhs = rhs

View File

@ -30,6 +30,7 @@ describe('maparg()', function()
buffer=0,
nowait=0,
mode='n',
abbr=0,
noremap=1,
lnum=0,
}
@ -156,6 +157,7 @@ describe('maparg()', function()
buffer = 0,
expr = 0,
mode = 'n',
abbr = 0,
noremap = 1,
nowait = 0,
script = 0,

View File

@ -21,13 +21,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
@ -35,7 +35,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
@ -43,8 +43,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'))
@ -278,6 +287,152 @@ func Test_mapset()
call assert_fails('call mapset("i", 0, {})', 'E460:')
endfunc
func 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
func s:GetModes(abbr = v:false)
return maplist(a:abbr)->filter({_, m -> m.lhs == 'XZ'})
\ ->mapnew({_, m -> m.mode})
endfunc
func s:UnmapAll(lhs)
const unmap_cmds = [ 'unmap', 'unmap!', 'tunmap', 'lunmap' ]
for cmd in unmap_cmds
try | call execute(cmd .. ' ' .. a:lhs) | catch /E31/ | endtry
endfor
endfunc
let tmap = {}
" some mapset(mode, abbr, dict) tests using get_map_mode_str
map XZ x
let tmap = maplist()->filter({_, m -> m.lhs == 'XZ'})[0]->copy()
" this splits the mapping into 2 mappings
call mapset('ox', v:false, tmap)
call assert_equal(2, len(s:GetModes()))
call mapset('o', v:false, tmap)
call assert_equal(3, len(s:GetModes()))
" test that '' acts like ' ', and that the 3 mappings become 1
call mapset('', v:false, tmap)
call assert_equal([' '], s:GetModes())
" dict's mode/abbr are ignored
call s:UnmapAll('XZ')
let tmap.mode = '!'
let tmap.abbr = v:true
call mapset('o', v:false, tmap)
call assert_equal(['o'], s:GetModes())
" test the 3 arg version handles bad mode string, dict not used
call assert_fails("call mapset('vi', v:false, {})", 'E1276:')
" get the abbreviations out of the way
abbreviate XZ ZX
let tmap = maplist(v:true)->filter({_, m -> m.lhs == 'XZ'})[0]->copy()
abclear
" 'ic' is the default ab command, shows up as '!'
let tmap.mode = 'ic'
call mapset(tmap)
call assert_equal(['!'], s:GetModes(v:true))
abclear
let tmap.mode = 'i'
call mapset(tmap)
call assert_equal(['i'], s:GetModes(v:true))
abclear
let tmap.mode = 'c'
call mapset(tmap)
call assert_equal(['c'], s:GetModes(v:true))
abclear
let tmap.mode = '!'
call mapset(tmap)
call assert_equal(['!'], s:GetModes(v:true))
call assert_fails("call mapset(#{mode: ' !', abbr: 1})", 'E1276:')
call assert_fails("call mapset(#{mode: 'cl', abbr: 1})", 'E1276:')
call assert_fails("call mapset(#{mode: 'in', abbr: 1})", 'E1276:')
" the map commands
map XZ x
let tmap = maplist()->filter({_, m -> m.lhs == 'XZ'})[0]->copy()
" try the combos
call s:UnmapAll('XZ')
" 'nxso' is ' ', the unadorned :map
let tmap.mode = 'nxso'
call mapset(tmap)
call assert_equal([' '], s:GetModes())
cal s:UnmapAll('XZ')
" 'ic' is '!'
let tmap.mode = 'ic'
call mapset(tmap)
call assert_equal(['!'], s:GetModes())
call s:UnmapAll('XZ')
" 'xs' is really 'v'
let tmap.mode = 'xs'
call mapset(tmap)
call assert_equal(['v'], s:GetModes())
" try the individual modes
call s:UnmapAll('XZ')
let tmap.mode = 'n'
call mapset(tmap)
call assert_equal(['n'], s:GetModes())
call s:UnmapAll('XZ')
let tmap.mode = 'x'
call mapset(tmap)
call assert_equal(['x'], s:GetModes())
call s:UnmapAll('XZ')
let tmap.mode = 's'
call mapset(tmap)
call assert_equal(['s'], s:GetModes())
call s:UnmapAll('XZ')
let tmap.mode = 'o'
call mapset(tmap)
call assert_equal(['o'], s:GetModes())
call s:UnmapAll('XZ')
let tmap.mode = 'i'
call mapset(tmap)
call assert_equal(['i'], s:GetModes())
call s:UnmapAll('XZ')
let tmap.mode = 'c'
call mapset(tmap)
call assert_equal(['c'], s:GetModes())
call s:UnmapAll('XZ')
let tmap.mode = 't'
call mapset(tmap)
call assert_equal(['t'], s:GetModes())
call s:UnmapAll('XZ')
let tmap.mode = 'l'
call mapset(tmap)
call assert_equal(['l'], s:GetModes())
call s:UnmapAll('XZ')
" get errors for modes that can't be in one mapping
call assert_fails("call mapset(#{mode: 'nxsoi', abbr: 0})", 'E1276:')
call assert_fails("call mapset(#{mode: ' !', abbr: 0})", 'E1276:')
call assert_fails("call mapset(#{mode: 'ix', abbr: 0})", 'E1276:')
call assert_fails("call mapset(#{mode: 'tl', abbr: 0})", 'E1276:')
call assert_fails("call mapset(#{mode: ' l', abbr: 0})", 'E1276:')
call assert_fails("call mapset(#{mode: ' t', abbr: 0})", 'E1276:')
endfunc
func Check_ctrlb_map(d, check_alt)
call assert_equal('<C-B>', a:d.lhs)
if a:check_alt