fix(zsh): probe the bound keymap for a -M bindmap's previous binding

.zinit-tmp-subst-bindkey records the widget a key carried before the plugin bound it, so
unload can put it back. That probe always read the `main' keymap, even when the intercepted
bind itself carried `-M <keymap>'. Unloading a plugin whose bindmap'' remapped a key bound
with -M therefore restored main's widget into the other keymap, silently replacing whatever
had been bound there.

Pass the same -M <keymap> to the probing `bindkey' whenever opts[-M] is set.

The existing -M case only asserted that a remap does not leak out of `emacs', and it assumed
`main' was linked to `viins' in the test shell -- `main' is `emacs' unless EDITOR/VISUAL is
vi-flavored, so the leak it was meant to catch was invisible. Link `main' to `viins'
explicitly and add a case that pre-binds the remapped key to different widgets in `emacs' and
`main', which makes restoring the wrong keymap's widget detectable.

Signed-off-by: Vladislav Doster <mvdoster@gmail.com>
This commit is contained in:
Vladislav Doster 2026-07-21 23:16:11 -05:00
parent 72c7154664
commit 2007a1ce8f
2 changed files with 36 additions and 8 deletions

View file

@ -126,27 +126,52 @@
}
@test 'bindmap confines a -M remap to that keymap' {
# `main' is linked to `viins' in the test shell, so `emacs' is used to tell
# the keymaps apart.
# `main' defaults to `emacs' unless EDITOR/VISUAL is vi-flavored; pin it to `viins' so a remap
# that leaked out of `emacs' (a dropped -M) is actually observable in `main'.
unsetopt ERR_EXIT
bindkey -A viins main
bindmap_fixture bm-keymap
function zunit-bm-keymap() { }
function zunit-bm-keymap() { ; }
zle -N zunit-bm-keymap
zinit as"null" id-as"test/bm-keymap" bindmap'^A -> "^Y"' \
atload'!bindkey -M emacs "^A" zunit-bm-keymap' for test/bm-keymap
zinit for \
as"null" \
atload'!bindkey -M emacs "^A" zunit-bm-keymap' \
bindmap'^A -> "^Y"' \
id-as"test/bm-keymap" \
@test/bm-keymap
local -a in_emacs=( ${(f)"$(bindings_for zunit-bm-keymap emacs)"} )
local -a in_main=( ${(f)"$(bindings_for zunit-bm-keymap)"} )
assert $#in_emacs equals 1
assert ${#in_emacs} equals 1
assert "${in_emacs[1]}" contains '"^Y"'
assert $#in_main equals 0
assert ${#in_main} equals 0
zinit unload -q test/bm-keymap
in_emacs=( ${(f)"$(bindings_for zunit-bm-keymap emacs)"} )
assert $#in_emacs equals 0
}
@test 'bindmap -M restores the keymap-specific previous binding' {
# Regression: the previous-binding probe read `main' even for a `-M <map>' bind, so unload
# restored the wrong keymap's widget. The remapped key is pre-bound differently in `emacs' and
# `main' so restoring the wrong one is detectable.
unsetopt ERR_EXIT
bindkey -A viins main
bindmap_fixture bm-keymap-prev
function zunit-bm-keymap-prev() { }
zle -N zunit-bm-keymap-prev
bindkey -M emacs '^Y' clear-screen # emacs prior -- must be restored on unload
bindkey '^Y' self-insert # different main prior -- must NOT be what's restored
zinit as"null" id-as"test/bm-keymap-prev" bindmap'^A -> "^Y"' \
atload'!bindkey -M emacs "^A" zunit-bm-keymap-prev' for test/bm-keymap-prev
assert "$(bindkey -M emacs '^Y')" contains 'zunit-bm-keymap-prev'
zinit unload -q test/bm-keymap-prev
assert "$(bindkey -M emacs '^Y')" contains 'clear-screen'
}
@test 'bindmap ! form remaps the widget, not the key' {
unsetopt ERR_EXIT
bindmap_fixture bm-bang

View file

@ -547,7 +547,10 @@ builtin setopt noaliases
[[ $bmap_val = hold ]] && return 0
# Quoted - the key sequence may contain spaces, e.g. bindmap'^X -> "^ "'.
local prev="${(q)${(s: :)$(builtin bindkey "${(Q)string}")}[-1]#undefined-key}"
# Probe the same keymap the bind targets, so unload restores the right prior widget.
local -a Mprobe
(( ${+opts[-M]} )) && Mprobe=( -M "${opts[-M]}" )
local prev="${(q)${(s: :)$(builtin bindkey "${Mprobe[@]}" "${(Q)string}")}[-1]#undefined-key}"
# "-M map" given?
if (( ${+opts[-M]} )); then