Skip to content

Binding a chord that is already in use succeeds silently, and which command it then runs depends on unrelated earlier unbinds #152

Description

@matt-edmondson

What's wrong

KeybindingService.BindChord (KeybindingService.cs L60-L87) does not check whether the chord is already bound to another command in the profile. It returns true and both commands stay bound to the same chord. The API gives the caller no way to find out about the conflict or resolve it.

ExecuteChord and FindCommandByChord then return the first match from Profile.FindCommand (Profile.cs L159-L170). "First" here is the enumeration order of the underlying Dictionary<string, Chord>. That order is neither binding order nor any documented rule. Dictionary reuses the slot a removed entry leaves behind, so an UnbindChord of an unrelated command changes which command a shared chord runs.

Reproduction (verified against current main, 03d498c)

var m = new KeybindingManager(dir);
foreach (var id in new[] { "open", "save", "saveAll" }) m.Commands.RegisterCommand(new Command(id, id));
m.CreateDefaultProfile();
var k = m.Keybindings;
k.BindChord("open", Chord.Parse("Ctrl+O"));
k.BindChord("save", Chord.Parse("Ctrl+S"));
// k.UnbindChord("open");                        // <- toggle this line
k.BindChord("saveAll", Chord.Parse("Ctrl+S"));   // returns true; "save" is still bound to Ctrl+S
k.ExecuteChord(Chord.Parse("Ctrl+S"));
  • Without the UnbindChord("open") line, ExecuteChord(Ctrl+S) returns save.
  • With it, ExecuteChord(Ctrl+S) returns saveAll.

Removing the Ctrl+O binding changes what Ctrl+S does. A settings UI that lets a user "rebind Ctrl+S to Save All" gets a true result, but whether the new binding takes effect depends on the profile's edit history.

Suggested fix / acceptance criteria

Choose one documented policy and enforce it in BindChord (both overloads) and in SetChords:

  • Replace: binding a chord that another command uses removes the other command's binding. Most editors behave this way when you rebind a shortcut.
  • Reject: return false, or throw, when the chord is already bound to a different command. Pair it with a lookup such as GetCommandsForChord(chord) or FindConflicts(chord) so a UI can show the conflict.

Whichever policy you choose, do the check and the write under the profile's chord lock (added in #142) so two concurrent binds can't both succeed.

Acceptance:

  • After BindChord("save", Ctrl+S) followed by BindChord("saveAll", Ctrl+S), the profile has at most one command bound to Ctrl+S, and the result of ExecuteChord(Ctrl+S) follows from the documented policy.
  • Unbinding an unrelated command never changes what another chord executes. The repro above gives the same result with and without the UnbindChord("open") line.
  • The interface docs for BindChord state the policy.

Related: #136 (FindCommandByChord and ExecuteChord disagree when a chord is shared) and #116. Both deal with the effects of a shared chord. This issue is about allowing the shared chord to exist in the first place and about the arbitrary order used to resolve it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions