Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 41 additions & 10 deletions src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,54 @@ public AliasManager(TopLevelCommandManager tlcManager, ISettingsService settings

public bool CheckAlias(string searchText)
{
// Try exact match first (handles both direct aliases like ">" and
// indirect aliases when the user types exactly "file ").
if (_settingsService.Settings.Aliases.TryGetValue(searchText, out var alias))
{
try
{
var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
if (topLevelCommand is not null)
{
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
return TryFireAlias(alias, remainingText: string.Empty);
}

WeakReferenceMessenger.Default.Send<PerformCommandMessage>(topLevelCommand.GetPerformCommandMessage());
return true;
}
// For indirect aliases the debounce timer may deliver text beyond
// the prefix (e.g. "file test" when the user typed fast). Check if
// the search text starts with any known indirect alias prefix (#41736).
foreach (var kv in _settingsService.Settings.Aliases)
{
var candidateAlias = kv.Value;
if (!candidateAlias.IsDirect
&& searchText.Length > kv.Key.Length
&& searchText.StartsWith(kv.Key, StringComparison.Ordinal))
{
var extraText = searchText[kv.Key.Length..];
return TryFireAlias(candidateAlias, extraText);
}
catch
}

return false;
}

private bool TryFireAlias(CommandAlias alias, string remainingText)
{
try
{
var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
if (topLevelCommand is not null)
{
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(topLevelCommand.GetPerformCommandMessage());

// If there was text typed after the alias prefix, forward it so
// keystrokes aren't lost (#41736).
if (!string.IsNullOrEmpty(remainingText))
{
WeakReferenceMessenger.Default.Send(new SetSearchTextMessage(remainingText));
}

return true;
}
}
catch
{
}

return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.UI.ViewModels.Messages;

/// <summary>
/// Sent after an alias triggers navigation to forward any text the user
/// typed beyond the alias prefix to the destination page (#41736).
/// </summary>
public record SetSearchTextMessage(string Text);
52 changes: 46 additions & 6 deletions src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public sealed partial class SearchBar : UserControl,
IRecipient<FocusSearchBoxMessage>,
IRecipient<UpdateSuggestionMessage>,
IRecipient<FocusParamMessage>,
IRecipient<SetSearchTextMessage>,
ICurrentPageAware
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
Expand All @@ -40,6 +41,9 @@ public sealed partial class SearchBar : UserControl,
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private bool _isBackspaceHeld;

// Text to apply after the next page navigation completes (alias overflow, #41736).
private string? _pendingSearchText;

// Inline text suggestions
// In 0.4-0.5 we would replace the text of the search box with the TextToSuggest
// This was really cool for navigating paths in run and pretty much nowhere else.
Expand Down Expand Up @@ -74,7 +78,6 @@ public PageViewModel? CurrentPageViewModel

private static void OnCurrentPageViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//// TODO: If the Debounce timer hasn't fired, we may want to store the current Filter in the OldValue/prior VM, but we don't want that to go actually do work...
var @this = (SearchBar)d;

if (@this is not null
Expand All @@ -86,11 +89,31 @@ private static void OnCurrentPageViewModelChanged(DependencyObject d, Dependency
if (@this is not null
&& e.NewValue is PageViewModel page)
{
// TODO: In some cases we probably want commands to clear a filter
// somewhere in the process, so we need to figure out when that is.
@this.FilterBox.Text = page.SearchTextBox;
// Stop any pending debounce so a stale callback doesn't overwrite
// the new page's search text after we set it here (#41736).
@this._debounceTimer.Stop();

// If an alias stored overflow text, apply it instead of the page default.
var pending = @this._pendingSearchText;
@this._pendingSearchText = null;

var textToSet = pending ?? page.SearchTextBox;
@this.FilterBox.Text = textToSet;
@this.FilterBox.Select(@this.FilterBox.Text.Length, 0);

if (pending is not null)
{
// Defer pushing overflow text into the ViewModel so the page
// can finish initializing without a premature cancellation.
@this._queue.TryEnqueue(() =>
{
if (@this.CurrentPageViewModel == page)
{
page.SearchTextBox = textToSet;
}
});
}

page.PropertyChanged += @this.Page_PropertyChanged;

if (page is ListViewModel listViewModel)
Expand Down Expand Up @@ -123,14 +146,25 @@ public SearchBar()
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateSuggestionMessage>(this);
WeakReferenceMessenger.Default.Register<FocusParamMessage>(this);
WeakReferenceMessenger.Default.Register<SetSearchTextMessage>(this);
}

public void ClearSearch()
{
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>
// Cancel any pending debounce to prevent stale text from being
// written after the clear (fixes alias keystroke race #41736).
_debounceTimer.Stop();

// Capture the current page so the queued clear only applies if we're
// still on the same page (avoids clearing a newly-navigated page).
var page = CurrentPageViewModel;
_queue.TryEnqueue(new(() =>
{
if (CurrentPageViewModel != page)
{
return;
}

this.FilterBox.Text = string.Empty;

if (CurrentPageViewModel is not null)
Expand Down Expand Up @@ -499,6 +533,12 @@ public void Receive(GoHomeMessage message)

public void Receive(FocusSearchBoxMessage message) => Focus();

public void Receive(SetSearchTextMessage message)
{
// Store the text to apply after the next page navigation (#41736).
_pendingSearchText = message.Text;
}

private void Focus()
{
this.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
Expand Down