From 24c18205c3b88e3bc55a4c09df4db19b3a0a4ec5 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Fri, 2 Oct 2026 15:23:58 +0200 Subject: [PATCH] Desktop: rebuild the hosts board once the typing in the find box pauses, not once a letter --- .../Views/HostsScreen.axaml | 6 ++- .../Views/HostsScreen.axaml.cs | 42 +++++++++++++++++ .../HostGridTests.cs | 47 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/DodoSSH.Client.App/Views/HostsScreen.axaml b/src/DodoSSH.Client.App/Views/HostsScreen.axaml index 97bdbb0..88b8429 100644 --- a/src/DodoSSH.Client.App/Views/HostsScreen.axaml +++ b/src/DodoSSH.Client.App/Views/HostsScreen.axaml @@ -230,9 +230,13 @@ Ctrl+K is named on it because the palette is the other way to reach a host by typing, and somebody who has found this box should know about the one that also connects on Enter. + + One way, and passed back by the code-behind once the typing pauses: every change to the filter + rebuilds the board's sections and their cards, and a name typed at speed used to do that once a + letter. The view model still filters the moment it is told. See HostsScreen.OnFilterTyped. --> - diff --git a/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs b/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs index 794a00a..f873c71 100644 --- a/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs +++ b/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs @@ -106,6 +106,48 @@ internal sealed partial class HostsScreen : UserControl Board.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Tunnel); WireTheBand(); + + HostFilter.TextChanged += OnFilterTyped; + } + + /// How long the find box waits for the typing to pause before the board is rebuilt. + private static readonly TimeSpan FilterPause = TimeSpan.FromMilliseconds(150); + + /// The wait itself, replaceable so a test can say when a pause is over rather than sleep through one. + internal Func Pause { get; set; } = static span => Task.Delay(span); + + /// How many times the find box has changed, so a wait can tell it is no longer the latest. + private int typed; + + /// Hands the find box's text to the vault once the typing pauses. + /// + /// + /// Every change to the filter rebuilds the board's sections and every card in them, and a name typed at + /// speed used to do that once a letter. Now it is done once, with the whole of what was typed. + /// + /// + /// A wait rather than a binding delay, and on the dispatcher's own queue: the box is bound one way, so a + /// filter the vault sets itself — cleared, or restored — still lands in the box at once, and the + /// change it makes to the box comes back here as the same text, which changes nothing. + /// + /// + private void OnFilterTyped(object? sender, TextChangedEventArgs e) => + _ = PassTheFilterOnAsync(HostFilter.Text ?? string.Empty, ++typed); + + private async Task PassTheFilterOnAsync(string text, int keystroke) + { + await Pause(FilterPause).ConfigureAwait(true); + + // A later letter has its own wait, and it is the one that speaks. + if (keystroke != typed) + { + return; + } + + if (Vault is { } vault && !string.Equals(vault.HostFilter, text, StringComparison.Ordinal)) + { + vault.HostFilter = text; + } } /// The band's own wiring, and the two keys that go with a set. diff --git a/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs b/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs index c8a8424..0dcb961 100644 --- a/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs +++ b/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs @@ -520,6 +520,53 @@ public sealed class HostGridTests : IAsyncLifetime }); } + /// + /// Every change to the filter rebuilds the board's sections and every card in them, and a name typed at + /// speed used to do that once a letter. The box now tells the vault once the typing pauses — and then + /// exactly once, with the whole of what was typed. + /// + [Fact] + public async Task TypingIntoTheFindBox_RebuildsTheBoardOnceThePauseComes_NotOnceALetter() + { + await OnTheBoardAsync((screen, _) => + { + // Each letter's wait, ended by hand: the pause is real time in the application, and this says + // when it is over instead of sleeping through it. + var pauses = new List(); + + screen.Pause = _ => + { + var pause = new TaskCompletionSource(); + pauses.Add(pause); + return pause.Task; + }; + + var rebuilds = 0; + vault.HostSections.CollectionChanged += (_, _) => rebuilds++; + + var box = screen.FindControl("HostFilter").ShouldNotBeNull(); + + box.Text = "p"; + box.Text = "pr"; + box.Text = "pro"; + Dispatcher.UIThread.RunJobs(); + + vault.HostFilter.ShouldBeNullOrEmpty("nothing has reached the vault while the typing goes on"); + rebuilds.ShouldBe(0); + + foreach (var pause in pauses) + { + pause.SetResult(); + } + + Dispatcher.UIThread.RunJobs(); + + vault.HostFilter.ShouldBe("pro"); + rebuilds.ShouldBe(1, "only the last letter's wait passes the text on"); + vault.VisibleHosts.ShouldHaveSingleItem().Label.ShouldBe("prod-db"); + }); + } + // ---- Helpers ---- private static void RightClick(Visual card, Visual window)