From 491aecd5c466732fc73c0ca4719b09ae0f4f8b40 Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Sun, 27 Sep 2026 10:26:17 +0000 Subject: [PATCH] Trim the id in the Command(CommandId) constructor as the string one does [patch] CommandRegistry keys commands on Command.Id but trims the id every lookup is given, so a Command built from a CommandId with surrounding whitespace registered under a key no lookup could produce: it showed up in listings but could never be found, bound or unregistered. The CommandId constructor now trims like the string constructor, and rejects a whitespace-only id. Fixes #123 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X65VbCUfvG15D4o8afrvpk --- Keybinding.Test/CommandIdWhitespaceTests.cs | 51 +++++++++++++++++++++ Keybinding/Models/Command.cs | 11 ++++- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 Keybinding.Test/CommandIdWhitespaceTests.cs diff --git a/Keybinding.Test/CommandIdWhitespaceTests.cs b/Keybinding.Test/CommandIdWhitespaceTests.cs new file mode 100644 index 0000000..8b59204 --- /dev/null +++ b/Keybinding.Test/CommandIdWhitespaceTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Keybinding.Test; + +using ktsu.Keybinding.Core.Models; +using ktsu.Keybinding.Core.Services; + +[TestClass] +public class CommandIdWhitespaceTests +{ + private static Command CreatePaddedCommand() => + new(CommandId.Create(" file.save "), CommandName.Create("Save")); + + [TestMethod] + public void Constructor_CommandIdWithWhitespace_TrimsId() + { + Assert.AreEqual("file.save", CreatePaddedCommand().Id.ToString()); + } + + [TestMethod] + public void Constructor_CommandIdWithWhitespace_EqualsStringConstructedCommand() + { + Assert.AreEqual(new Command(" file.save ", "Save"), CreatePaddedCommand()); + } + + [TestMethod] + public void Constructor_WhitespaceOnlyCommandId_Throws() + { + Assert.ThrowsExactly(() => new Command(CommandId.Create(" "), CommandName.Create("Save"))); + } + + [TestMethod] + public void Register_CommandIdWithWhitespace_CanBeFoundBoundAndUnregistered() + { + CommandRegistry registry = new(); + Assert.IsTrue(registry.RegisterCommand(CreatePaddedCommand())); + + Assert.IsTrue(registry.IsCommandRegistered("file.save")); + Assert.IsTrue(registry.IsCommandRegistered(" file.save ")); + Assert.IsNotNull(registry.GetCommand("file.save")); + + ProfileManager profiles = new(); + profiles.CreateProfile("p", "Profile"); + profiles.SetActiveProfile("p"); + KeybindingService service = new(registry, profiles); + Assert.IsTrue(service.BindChord(" file.save ", Chord.Parse("Ctrl+S"))); + + Assert.IsTrue(registry.UnregisterCommand(" file.save ")); + Assert.IsFalse(registry.IsCommandRegistered("file.save")); + } +} diff --git a/Keybinding/Models/Command.cs b/Keybinding/Models/Command.cs index 2ed0653..8a9d851 100644 --- a/Keybinding/Models/Command.cs +++ b/Keybinding/Models/Command.cs @@ -20,7 +20,16 @@ public Command(CommandId id, CommandName name, CommandDescription? description = Ensure.NotNull(id); Ensure.NotNull(name); - Id = id; + // Trim as the string constructor does: the registry's lookups trim the id they are given, so an + // untrimmed id would be stored under a key no lookup can produce. + string rawId = id.ToString(); + string trimmedId = rawId.Trim(); + if (trimmedId.Length == 0) + { + throw new ArgumentException("Command ID cannot be null or whitespace", nameof(id)); + } + + Id = trimmedId == rawId ? id : CommandId.Create(trimmedId); Name = name; Description = description; Category = category;