From 2007a1ce8fc597aecb15802d32dfb457a373ea36 Mon Sep 17 00:00:00 2001 From: Vladislav Doster Date: Tue, 21 Jul 2026 23:16:11 -0500 Subject: [PATCH] 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 '. 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 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 --- tests/bindmap.zunit | 39 ++++++++++++++++++++++++++++++++------- zinit.zsh | 5 ++++- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/tests/bindmap.zunit b/tests/bindmap.zunit index f3fead0e..4c78f690 100644 --- a/tests/bindmap.zunit +++ b/tests/bindmap.zunit @@ -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 ' 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 diff --git a/zinit.zsh b/zinit.zsh index 894e66ce..fed3c966 100644 --- a/zinit.zsh +++ b/zinit.zsh @@ -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