Update Fallback commands async, once (#38157)

The problem: 

> * we need to go update all the Fallback commands. (these are ones that extensions can use to react to the search text - basically, "what the user typed wasn't found immediately, but here's something they can fall back on"
>   * this is wacky, because the way I had it, I update each item, and if it "changes visibility", then we need to update the main list, because we've already removed it from the list. So we need to re-update the list to account for that
>     * you missed it reading that (and i missed it writing it) but that basically means we re-populate the list F={num fallbacks} times, because each one sends the "do it again" message
>     * That results in us basically creating (F+1)*(N=num items+apps) view models, initializing them, and not needing most of them

The crux here being a single thread, to update all the fallback items,
that then only raises _one_ items changed at the very end.

I don't love this, one misbehaving fallback could stop all the others. In theory, we should do a parallel update of all these things, with a like, 1s timeout on each leg. 

But it has gotta be faster till we can do #38140 (or similar)

Closes: (not sure I filed one). But the first typed character _felt_ slow.
This commit is contained in:
Mike Griese
2025-03-26 06:36:37 -05:00
committed by GitHub
parent d597bd267d
commit 60bbf070e1
2 changed files with 58 additions and 29 deletions

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Collections.Specialized;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Ext.Apps;
@@ -101,12 +102,7 @@ public partial class MainListPage : DynamicListPage,
var commands = _tlcManager.TopLevelCommands;
lock (commands)
{
// This gets called on a background thread, because ListViewModel
// updates the .SearchText of all extensions on a BG thread.
foreach (var command in commands)
{
command.TryUpdateFallbackText(newSearch);
}
UpdateFallbacks(newSearch, commands.ToImmutableArray());
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
if (string.IsNullOrEmpty(newSearch))
@@ -137,6 +133,26 @@ public partial class MainListPage : DynamicListPage,
}
}
private void UpdateFallbacks(string newSearch, IReadOnlyList<TopLevelViewModel> commands)
{
// fire and forget
_ = Task.Run(() =>
{
var needsToUpdate = false;
foreach (var command in commands)
{
var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch);
needsToUpdate = needsToUpdate || changedVisibility;
}
if (needsToUpdate)
{
RaiseItemsChanged();
}
});
}
private bool ActuallyLoading()
{
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>()!;

View File

@@ -4,8 +4,7 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -237,32 +236,46 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
}
}
public void TryUpdateFallbackText(string newQuery)
internal bool SafeUpdateFallbackTextSynchronous(string newQuery)
{
if (!IsFallback)
{
return;
return false;
}
_ = Task.Run(() =>
try
{
try
{
var model = _commandItemViewModel.Model.Unsafe;
if (model is IFallbackCommandItem fallback)
{
var wasEmpty = string.IsNullOrEmpty(Title);
fallback.FallbackHandler.UpdateQuery(newQuery);
var isEmpty = string.IsNullOrEmpty(Title);
if (wasEmpty != isEmpty)
{
WeakReferenceMessenger.Default.Send<UpdateFallbackItemsMessage>();
}
}
}
catch (Exception)
{
}
});
return UnsafeUpdateFallbackSynchronous(newQuery);
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
return false;
}
/// <summary>
/// Calls UpdateQuery on our command, if we're a fallback item. This does
/// RPC work, so make sure you're calling it on a BG thread.
/// </summary>
/// <param name="newQuery">The new search text to pass to the extension</param>
/// <returns>true if our Title changed across this call</returns>
private bool UnsafeUpdateFallbackSynchronous(string newQuery)
{
var model = _commandItemViewModel.Model.Unsafe;
// RPC to check type
if (model is IFallbackCommandItem fallback)
{
var wasEmpty = string.IsNullOrEmpty(Title);
// RPC for method
fallback.FallbackHandler.UpdateQuery(newQuery);
var isEmpty = string.IsNullOrEmpty(Title);
return wasEmpty != isEmpty;
}
return false;
}
}