Skip to content

fix(unload): resolve linked keymaps and skip held records on repeat loads - #584

Merged
ss-o merged 2 commits into
nextfrom
bug-583
Oct 1, 2026
Merged

ss-o merged 2 commits into
nextfrom
bug-583

Conversation

@ss-o

@ss-o ss-o commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Description

Follow-up to #582, from its third independent review. Two defects in the repeated-load path, and the wider known limit.

  • Linked keymaps. bindkey -v links main to viins and bindkey -e links it to emacs (an EDITOR containing vi does the first at startup). A plugin that binds the key with -M viins then changes main's binding too, but the takeover check compared viins with the default map and missed it, so p's repeated load kept its records and its unload left "^X^P" undefined-key in both maps while the other plugin was still loaded. .zi-repeat-taken-over now reads the keymap main is linked to (bindkey -lL main), and .zi-repeat-object-key gives the default map, -M main and that keymap one identity. The result is next's behaviour again.
  • Duplicate records. A repeated load keeps the earlier load's records and then appended the same bindkey -N, bindkey -A and zle hook records again, so unload replayed them twice: no such keymap from bindkey -N and -A records, no such widget from a hook such as zle-line-init. The new .zi-add-record appends a bindkey or widget-delete record only when the plugin does not already hold the same text, under emulate -L zsh so a plugin's options (ksh_arrays) do not change the check. The hook case was not in the issue; it is the same record replay and was found while reproducing it.
  • The comment on the repeated-load path and the test header now state the known limit below.

Related issues

Closes #583
Refs #113 (the interleaved case, per-load records and changes made without records stay open there).

Type of change

  • fix - bug fix (non-breaking)
  • test - test addition or correction

Checklist

  • Ordinary work targets next; only same-repository hotfixes target main
  • A next to main promotion uses a merge commit and records both parent SHAs (not a promotion)
  • Commit messages follow Conventional Commits format
  • Any Co-authored-by trailer credits a real human, never a bot, AI agent, or automation (none)
  • I have read the contribution guidelines
  • Existing tests pass (zsh -n zi.zsh / Trunk checks)
  • Documentation updated if needed (internal behaviour; comments updated, no user-facing docs change)

Verification

Zsh 5.9.2, Linux, each in a scratch copy of the tree:

  • tests/repeated-load-ownership.zsh gains 5 scenarios: linked-taken (main linked to viins), linked-taken-emacs, keymap-twice (a bindkey -N plugin loaded twice: the keymap is deleted exactly once, no errors), hook-twice (a zle-line-init plugin loaded twice: no no such widget) and hook-twice-ksh (the same with ksh_arrays set around the plugin's zle -N, from the first review). On next (8bbd62c) with the new test copied in, these 5 fail and the other 12 pass; on this branch 17 of 17 pass. hook-twice-ksh also failed on 51e702b and passes on e8a7a35.
  • 5 hand mutants, each killed: identity without the link (linked-taken, linked-taken-emacs fail), link resolved only for the default map and not for -M main (main-taken), records appended without the check (keymap-twice, hook-twice), link lookup removed (linked-taken, linked-taken-emacs), widget-delete records appended without the check (hook-twice).
  • Full suite on e8a7a35, every test command in zsh-n.yml: 39 of 39 pass (version-reporting.zsh and self-update-reload.zsh from a committed snapshot, as they clone the checkout). zsh -n and zcompile on every file of the workflow matrix: clean.
  • contracts/public-contract-v1.json names none of the touched internal functions or keys.

Known limits:

  • A change made between two loads of a plugin without zi records, by the user or by a plugin loaded in light mode, to a function, a widget or a binding, is not detected. The second load treats it as the plugin's own, and unload removes or restores it from the state before the first load. A single load, the same change and an unload already behave this way on next. Detecting these changes stays with fix(unload): preserve runtime ownership across repeated plugin loads #113.
  • Only bindkey records with a key take part in the takeover check; bindkey -A and -N records do not.
  • zi report still shows only the newest load's functions for a plugin.

…oads

A repeated load of one plugin checks whether another plugin took one of
its bindings over in between. `bindkey -v` and `bindkey -e` link main to
viins or emacs, so a plugin binding a key with `-M viins` changes the
binding of main as well; the check now resolves the keymap main is linked
to when it builds a binding's identity, and the repeated load keeps the
behaviour of next in that case.

A repeated load also kept its earlier records and then appended the same
`bindkey -N`, `bindkey -A` and zle hook records again, so unload replayed
them twice and printed "no such keymap" and "no such widget" errors.
Bindkey and widget-delete records are now appended only when the plugin
does not already hold the same record.

The comment on the repeated-load path and the test header now state the
wider known limit: a change made without zi records, by the user or by a
plugin loaded in light mode, to a function, widget or binding, is not
detected.

Refs #583
Refs #113
@ss-o

ss-o commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Review request record for head 51e702b:

  • 2026-10-01T03:21:15Z: requested copilot-pull-request-reviewer[bot] once through the REST requested_reviewers endpoint. The call succeeded, but the response listed no requested users (only the CODEOWNERS team tsc).
  • By 03:21:42Z the timeline's only review_requested event (03:18:39Z) names the CODEOWNERS team tsc, not Copilot, and there is no Copilot review, so the request did not register. Cause unconfirmed; the same symptom occurred on fix(unload): keep ownership across repeated loads of one plugin #582.

The maintainer has elected the ADR-0026 fallback for this repository class. An independent read-only review of this head is running, and the fallback review of record will be posted on this head.

@ss-o ss-o left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback review under ADR-0026: Copilot request not registered on 51e702b

The Copilot request is recorded in #584 (comment). This review covers 51e702b against next at 8bbd62c. It ran the organization code-review skill against code-review-generic.instructions.md as an independent read-only review (separate session on a different model), and every probe it reported was rerun on the same head.

Verdict

Changes requested: one finding (inline). The rest of the change holds.

  • Correctness: pass, with the inline finding. Link resolution was probed with main linked to viins, emacs and a user keymap, with main unlinked (bindkey -N main), with the plugin running bindkey -v or bindkey -A x main itself, and with the link changing between the two loads. In every case head matched or bettered 8bbd62c. The record check never dropped a record unload needs: a key rebound with different previous widgets keeps distinct records, -A copies keep their incrementing names, and dtrace records are untouched.
  • Public contract: pass. .zi-add-record, .zi-repeat-* and the BINDKEYS__/WIDGETS_DELETE__ keys are internal, and the record format and unload replay are unchanged.
  • Shell-state integrity: pass, with the inline finding. In the known-limit cases (link changed between loads, light mode) head equals or betters 8bbd62c.
  • Tests: pass. 16 of 16 scenarios pass on head, and with head's test on 8bbd62c exactly the four new scenarios fail.
  • Comments: pass. The known-limit comment and the test header match the behaviour.

Finding

  1. Option-dependent record check (zi.zsh line 1277). Inline.

Verification limits

  • Zsh 5.9.x on Linux only.
  • The probes covered zi load, zi light, zi unload and dtrace, not turbo (wait) loads or zi update reloads.
  • Two pre-existing behaviours were seen identically on 8bbd62c and are not caused by this change: the <plugin>-main-N backup keymaps stay after unload, and a viins binding that existed before a repeated -M viins load is not restored (#113).

Comment thread zi.zsh
`.zi-add-record` is called from the zle substitute, which keeps the
plugin's options. With `ksh_arrays` set while a plugin runs `zle -N`,
the `(Ie)` subscript returned 0 for the first record, so a repeated load
still appended the hook record twice and unload printed "no such
widget". The function now starts with `emulate -L zsh`, and a
`hook-twice-ksh` scenario pins the case.

Refs #583
@ss-o

ss-o commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Review request record for head e8a7a35:

  • 2026-10-01T03:35:21Z: requested copilot-pull-request-reviewer[bot] once through the REST requested_reviewers endpoint. The call succeeded, but the response listed no requested users (only the CODEOWNERS team tsc).
  • By 03:35:58Z the timeline still had no review_requested event naming Copilot (the only one names the CODEOWNERS team tsc, at 03:18:39Z), so the request did not register.

The round-1 fallback review on 51e702b is stale after this push, which fixes its one finding. An independent read-only review of e8a7a35 runs next under the same ADR-0026 fallback, and its review of record will be posted on this head. My reply on the round-1 thread also appears as a comment-only review entry; it is not a review of record.

@ss-o ss-o left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback review under ADR-0026: Copilot request not registered on e8a7a35

The Copilot request is recorded in #584 (comment). This review covers e8a7a35 against next at 8bbd62c, the whole change and not only the delta since round 1. It ran the organization code-review skill against code-review-generic.instructions.md as an independent read-only review (separate session on a different model). Its key probes were rerun on the same head with the same results.

Verdict

No findings. The round-1 finding (option-dependent record check, 51e702b) is fixed and its thread is resolved.

  • Correctness: pass. The default map, -M main and the keymap main is linked to share one identity, including a user keymap and a quoted keymap name. The record check drops only text-identical records and keeps the first, so reverse replay still ends at the first record's previous widget.
  • Public contract: pass. Only internal functions and ZI[...] record keys change; the bindkey and zle substitutes keep their signatures and return status. dtrace records are untouched, and bindkey -A copies with incrementing names are all replayed.
  • Shell-state integrity: pass. .zi-add-record runs under emulate -L zsh. With each of 19 plugin-set options (among them ksh_arrays, sh_word_split, no_multibyte, extended_glob, ksh_typeset, no_glob, no_unset, err_return, warn_create_global), head records each object once and unloads with no errors, where 8bbd62c duplicates records and prints three errors. The plugin's own options are unchanged by the substitutes. Widget and keymap names containing ] [ ( ) * $ and spaces behave the same way.
  • Tests: pass. 17 of 17 scenarios pass on head; with head's test on 8bbd62c exactly the five new scenarios fail. zsh -f -n is clean on zi.zsh and the test. CI is green on this head.
  • Comments: pass. The known-limit comment, the test header and the $main_map note on .zi-repeat-object-key match the behaviour.

Probes (base 8bbd62c against head)

Unlinked main, main linked to a user keymap, the plugin running bindkey -v or bindkey -A x main itself, the link changing between loads, a key bound and rebound in one load, dtrace and light mode, a widget and hook created twice, -M viins and -N across three loads, a standalone main keymap, ksh_arrays around zle -N, the 19-option matrix, hostile names, option leakage and a quoted keymap link. In each, head matches 8bbd62c or fixes the reported defect; none is worse.

Verification limits

  • Zsh 5.9.2 on Linux only.
  • No turbo (wait) loads or zi update reloads, and no ZD integration run.
  • Two behaviours seen identically on 8bbd62c are not caused by this change: the <plugin>-main-N backup keymaps stay after unload, and a viins binding that existed before a repeated -M viins load is not restored (#113).

@ss-o
ss-o merged commit 3fa8433 into next Oct 1, 2026
148 of 150 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

fix(unload): repeated-load takeover misses linked keymaps and duplicates keymap records

1 participant