Skip to content

Dispatch editor shortcuts through ktsu.Keybinding.Core [patch] - #210

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/nifty-bohr-v129ba
Sep 22, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/nifty-bohr-v129ba

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #201

What this changes

Schema.Editor wrote each chord down twice — once as an if/else if chain over ImGui's key state in ProcessKeyboardShortcuts, and again as a literal string beside the menu item it fires ("Ctrl+Shift+S" and friends). Nothing tied the two together, so a chord could be rebound in one place and go on being advertised as the other. That drift is the bug this prevents.

EditorShortcuts now holds the seven bindings once:

(New,           "New",     "Ctrl+N"),
(Open,          "Open",    "Ctrl+O"),
(Save,          "Save",    "Ctrl+S"),
(SaveAs,        "Save As", "Ctrl+Shift+S"),
(Undo,          "Undo",    "Ctrl+Z"),
(Redo,          "Redo",    "Ctrl+Y"),
(RedoAlternate, "Redo",    "Ctrl+Shift+Z"),

The menu labels are rendered from the same binding the dispatcher matches (Shortcuts.Label(EditorShortcuts.SaveAs) in place of "Ctrl+Shift+S"), so the two cannot disagree.

Why ktsu.Keybinding.Core costs nothing here

ktsu.Keybinding.Core was already a PackageReference of Schema.Editor and went entirely unused, so this adopts a dependency that is already paid for rather than taking one on.

I measured the library's behaviour against what the editor needs before writing anything:

  • Chord.Parse / ToString round-trip exactly on all seven chords — Ctrl+N, Ctrl+O, Ctrl+S, Ctrl+Shift+S, Ctrl+Z, Ctrl+Shift+Z, Ctrl+Y all come back as the identical string. So the menu reads exactly as it did; there is no visible change to the UI.
  • Chord equality ignores modifier order and case (Ctrl+Shift+S == Shift+Ctrl+S) but correctly separates Ctrl+Z from Ctrl+Shift+Z.
  • FindCommandByChord resolves each chord to the right command, including the two shared-key pairs the old chain had to be ordered carefully to get right.

Nothing is persisted. KeybindingService is built over an in-memory CommandRegistry and ProfileManager — both have parameterless constructors — so no IKeybindingRepository is involved and the editor neither reads nor writes a keybinding file. That also means this is independent of ktsu-dev/Keybinding#93, which the triage note flagged as worth watching: since nothing here persists, a change to Keybinding's persistence shape cannot reach this code.

One deliberate behaviour change

The old chain tested the modifiers each arm cared about and ignored the rest:

else if (ctrl && ImGui.IsKeyPressed(ImGuiKey.N, false)) { New(); }

so Ctrl+Shift+N started a new document just as Ctrl+N did, and Ctrl+Alt+O opened one. A chord now has to match exactly, so those no longer fire. Neither is a chord the editor advertises. The chords that were always meant to be distinguished — Ctrl+S against Ctrl+Shift+S, Ctrl+Z against Ctrl+Shift+Z — still are, and no longer depend on the order the else if arms happen to be written in.

This is pinned explicitly by AnExtraModifierIsNotTheSameChord rather than left to be discovered.

Testing

New: Schema.Editor.Test/EditorShortcutTests.cs — 8 frameless tests. EditorShortcuts.FindCommand takes the pressed key as a name rather than an ImGuiKey precisely so the table can be exercised without a rasterizer. The load-bearing pair is MenuLabelIsTheChordThatFiresTheCommand and TheChordOnTheLabelSelectsThatCommand, which check the join in both directions.

Proved the tests fail without the fix. Rebinding Save As from Ctrl+Shift+S to Ctrl+Alt+S — exactly the drift the old code could not catch — fails two tests:

failed MenuLabelIsTheChordThatFiresTheCommand
  Assert.AreEqual(label, shortcuts.Label(commandId))
failed ShiftDistinguishesSaveAsFromSave
  Assert.AreEqual(EditorShortcuts.SaveAs, Press("S", shift: true))

Restored, both pass.

Regression: the existing frame-driven tests/Schema.Editor.UITests/ShortcutTests is unchanged and still passes — that suite presses the chords into a live frame, so it is what says ImGui's real key state still reaches the table.

suite result
Schema.Editor.Test 26/26 passed (18 existing + 8 new)
Schema.Editor.UITests 190/190 passed
whole solution 822/822 passed, 0 failed

The three app-launch-failed errors in a full-solution run are this container missing the .NET 8 and 9 runtimes for the multi-targeted suites' other legs. They reproduce identically on clean main, and both projects this PR touches are net10.0-only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JjErQAuuWQwrRBqqHTkgzZ


Generated by Claude Code

The editor wrote each chord down twice: once as an if/else if chain over
ImGui's key state, and again as a literal string beside the menu item it
fires ("Ctrl+Shift+S" and friends). Nothing tied the two together, so a
chord could be rebound in one place and go on being advertised as the
other.

EditorShortcuts now holds the seven bindings once. The menu labels are
rendered from the same binding the dispatcher matches, so the two cannot
drift apart. The matching is ktsu.Keybinding.Core's, which Schema.Editor
already referenced and did not use, so this takes on no new dependency.
Chord.ToString spells a chord exactly as the menu used to hard-code it,
so the menu reads as it did before.

Nothing is persisted: KeybindingService is built over an in-memory
CommandRegistry and ProfileManager, so no repository is involved and the
editor neither reads nor writes a keybinding file.

One behaviour changes deliberately. The old chain tested the modifiers
each arm cared about and ignored the rest, so Ctrl+Shift+N started a new
document just as Ctrl+N did, and Ctrl+Alt+O opened one. A chord now has
to match exactly, so those no longer fire. The chords that were always
meant to be distinguished - Ctrl+S against Ctrl+Shift+S, Ctrl+Z against
Ctrl+Shift+Z - still are, and no longer depend on the order the arms
happen to be written in.

Covered by EditorShortcutTests, which needs no frame: it pins the label
against the chord that fires the command, both ways round. The existing
frame-driven ShortcutTests are unchanged and still pass, which is what
says ImGui's real key state still reaches the table.

Fixes #201

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjErQAuuWQwrRBqqHTkgzZ
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit a294766 into main Sep 22, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/nifty-bohr-v129ba branch September 22, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delegate editor keyboard shortcuts to ktsu.Keybinding.Core

2 participants