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.
What's wrong
KeybindingService.BindChord(KeybindingService.csL60-L87) does not check whether the chord is already bound to another command in the profile. It returnstrueand both commands stay bound to the same chord. The API gives the caller no way to find out about the conflict or resolve it.ExecuteChordandFindCommandByChordthen return the first match fromProfile.FindCommand(Profile.csL159-L170). "First" here is the enumeration order of the underlyingDictionary<string, Chord>. That order is neither binding order nor any documented rule.Dictionaryreuses the slot a removed entry leaves behind, so anUnbindChordof an unrelated command changes which command a shared chord runs.Reproduction (verified against current
main, 03d498c)UnbindChord("open")line,ExecuteChord(Ctrl+S)returnssave.ExecuteChord(Ctrl+S)returnssaveAll.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
trueresult, 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 inSetChords:false, or throw, when the chord is already bound to a different command. Pair it with a lookup such asGetCommandsForChord(chord)orFindConflicts(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:
BindChord("save", Ctrl+S)followed byBindChord("saveAll", Ctrl+S), the profile has at most one command bound to Ctrl+S, and the result ofExecuteChord(Ctrl+S)follows from the documented policy.UnbindChord("open")line.BindChordstate 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.