diff --git a/Keybinding.Test/ExecuteChordTests.cs b/Keybinding.Test/ExecuteChordTests.cs index 5368849..dde8f22 100644 --- a/Keybinding.Test/ExecuteChordTests.cs +++ b/Keybinding.Test/ExecuteChordTests.cs @@ -58,6 +58,35 @@ public void ExecuteChord_OnlyBindingUnregistered_ReturnsNull() Assert.IsNull(_service.ExecuteChord(chord)); } + [TestMethod] + public void FindCommandByChord_FirstBindingUnregistered_AgreesWithExecuteChord() + { + Chord chord = Chord.Parse("Ctrl+S"); + _service.BindChord("a", chord); + _service.BindChord("b", chord); + + _registry.UnregisterCommand("a"); + + Assert.AreEqual("b", _service.FindCommandByChord(chord), "A stale binding should not hide the command that will run."); + Assert.AreEqual("b", _service.FindCommandByChord("p", chord)); + Assert.AreEqual(_service.ExecuteChord(chord), _service.FindCommandByChord(chord)); + Assert.AreEqual(_service.ExecuteChord("p", chord), _service.FindCommandByChord("p", chord)); + } + + [TestMethod] + public void FindCommandByChord_OnlyBindingUnregistered_ReturnsNull() + { + Chord chord = Chord.Parse("Ctrl+S"); + _service.BindChord("a", chord); + + _registry.UnregisterCommand("a"); + + Assert.IsNull(_service.FindCommandByChord(chord), "A chord bound only to an unregistered command does nothing."); + Assert.IsNull(_service.FindCommandByChord("p", chord)); + Assert.AreEqual(_service.ExecuteChord(chord), _service.FindCommandByChord(chord)); + Assert.AreEqual(_service.ExecuteChord("p", chord), _service.FindCommandByChord("p", chord)); + } + [TestMethod] public void ExecuteChord_UnknownProfile_ReturnsNull() { diff --git a/Keybinding.Test/ProfileChordAccessTests.cs b/Keybinding.Test/ProfileChordAccessTests.cs index 3f54cb4..e90a37e 100644 --- a/Keybinding.Test/ProfileChordAccessTests.cs +++ b/Keybinding.Test/ProfileChordAccessTests.cs @@ -82,6 +82,6 @@ public void FindAndExecuteChord_UseTheLockedSnapshot() registry.UnregisterCommand("b"); Assert.IsNull(service.ExecuteChord(CtrlB), "A binding whose command is unregistered is skipped"); - Assert.AreEqual("b", service.FindCommandByChord(CtrlB), "FindCommandByChord does not filter by registration"); + Assert.IsNull(service.FindCommandByChord(CtrlB), "FindCommandByChord skips it too, matching ExecuteChord"); } } diff --git a/Keybinding/Contracts/IKeybindingService.cs b/Keybinding/Contracts/IKeybindingService.cs index 4f1431b..3dc7966 100644 --- a/Keybinding/Contracts/IKeybindingService.cs +++ b/Keybinding/Contracts/IKeybindingService.cs @@ -169,14 +169,16 @@ public interface IKeybindingService public bool HasChordBinding(string profileId, string commandId); /// - /// Finds the command ID bound to a specific chord in the active profile + /// Finds the command ID bound to a specific chord in the active profile, skipping bindings whose command is + /// no longer registered, so the result is the command would run /// /// The chord to search for /// The command ID if found, null otherwise public string? FindCommandByChord(Chord chord); /// - /// Finds the command ID bound to a specific chord in a specific profile + /// Finds the command ID bound to a specific chord in a specific profile, skipping bindings whose command is + /// no longer registered, so the result is the command would run /// /// The profile ID /// The chord to search for diff --git a/Keybinding/Services/KeybindingService.cs b/Keybinding/Services/KeybindingService.cs index 647bc27..4f78001 100644 --- a/Keybinding/Services/KeybindingService.cs +++ b/Keybinding/Services/KeybindingService.cs @@ -243,11 +243,7 @@ public Chord ParseChord(string chordString) return null; } - // A chord can be bound to more than one command, and unregistering a command leaves its - // bindings in place, so skip bindings whose command is no longer registered rather than - // giving up on the first match. - Profile? profile = _profileManager.GetProfile(profileId); - string? commandId = profile?.FindCommand(chord, _commandRegistry.IsCommandRegistered); + string? commandId = FindRegisteredCommand(profileId, chord); // In a real implementation, this would trigger command execution // For now, we just return the command ID that would be executed @@ -295,7 +291,19 @@ public bool HasChordBinding(string profileId, string commandId) return null; } + return FindRegisteredCommand(profileId, chord); + } + + /// + /// Finds the command a chord runs: the first binding whose command is still registered. + /// ExecuteChord and FindCommandByChord share this so they cannot disagree. + /// + private string? FindRegisteredCommand(string profileId, Chord chord) + { + // A chord can be bound to more than one command, and unregistering a command leaves its + // bindings in place, so skip bindings whose command is no longer registered rather than + // giving up on the first match. Profile? profile = _profileManager.GetProfile(profileId); - return profile?.FindCommand(chord); + return profile?.FindCommand(chord, _commandRegistry.IsCommandRegistered); } }