CmdPal: Fix SUI crash ; Allow extensions to be disabled (#38040)

Both `TopLevelCommandItemWrapper` and `TopLevelViewModel` were really the same thing. The latter was from an earlier prototype, and the former is a more correct, safer abstraction. We really should have only ever used the former, but alas, we only used it for the SUI, and it piggy-backed off the latter, and that meant the latter's bugs became the former's.


tldr: I made the icon access safe in the SUI. 

And while I was doing this, because we now have a cleaner VM abstraction here in the host, we can actually cleanly disable extensions, because the `CommandProviderWrapper` knows which `ViewModel`s it made. 

Closes https://github.com/zadjii-msft/PowerToys/issues/426
Closes https://github.com/zadjii-msft/PowerToys/issues/478
Closes https://github.com/zadjii-msft/PowerToys/issues/577
This commit is contained in:
Mike Griese
2025-03-20 15:36:10 -05:00
committed by GitHub
parent 57cbcc2c3e
commit 14919dff10
34 changed files with 590 additions and 528 deletions

View File

@@ -16,7 +16,8 @@ using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class TopLevelCommandManager : ObservableObject,
IRecipient<ReloadCommandsMessage>
IRecipient<ReloadCommandsMessage>,
IPageContext
{
private readonly IServiceProvider _serviceProvider;
private readonly TaskScheduler _taskScheduler;
@@ -24,6 +25,8 @@ public partial class TopLevelCommandManager : ObservableObject,
private readonly List<CommandProviderWrapper> _builtInCommands = [];
private readonly List<CommandProviderWrapper> _extensionCommandProviders = [];
TaskScheduler IPageContext.Scheduler => _taskScheduler;
public TopLevelCommandManager(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
@@ -31,7 +34,7 @@ public partial class TopLevelCommandManager : ObservableObject,
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
}
public ObservableCollection<TopLevelCommandItemWrapper> TopLevelCommands { get; set; } = [];
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
[ObservableProperty]
public partial bool IsLoading { get; private set; } = true;
@@ -58,35 +61,43 @@ public partial class TopLevelCommandManager : ObservableObject,
// May be called from a background thread
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
await commandProvider.LoadTopLevelCommands();
WeakReference<IPageContext> weakSelf = new(this);
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
var settings = _serviceProvider.GetService<SettingsModel>()!;
var makeAndAdd = (ICommandItem? i, bool fallback) =>
{
TopLevelCommandItemWrapper wrapper = new(
new(i), fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, _serviceProvider);
var commandItemViewModel = new CommandItemViewModel(new(i), weakSelf);
var topLevelViewModel = new TopLevelViewModel(commandItemViewModel, fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, settings, _serviceProvider);
lock (TopLevelCommands)
{
TopLevelCommands.Add(wrapper);
TopLevelCommands.Add(topLevelViewModel);
}
};
await Task.Factory.StartNew(
() =>
{
foreach (var i in commandProvider.TopLevelItems)
lock (TopLevelCommands)
{
makeAndAdd(i, false);
}
foreach (var item in commandProvider.TopLevelItems)
{
TopLevelCommands.Add(item);
}
foreach (var i in commandProvider.FallbackItems)
{
makeAndAdd(i, true);
foreach (var item in commandProvider.FallbackItems)
{
TopLevelCommands.Add(item);
}
}
},
CancellationToken.None,
TaskCreationOptions.None,
_taskScheduler);
commandProvider.CommandsChanged -= CommandProvider_CommandsChanged;
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
}
@@ -108,8 +119,8 @@ public partial class TopLevelCommandManager : ObservableObject,
{
// Work on a clone of the list, so that we can just do one atomic
// update to the actual observable list at the end
List<TopLevelCommandItemWrapper> clone = [.. TopLevelCommands];
List<TopLevelCommandItemWrapper> newItems = [];
List<TopLevelViewModel> clone = [.. TopLevelCommands];
List<TopLevelViewModel> newItems = [];
var startIndex = -1;
var firstCommand = sender.TopLevelItems[0];
var commandsToRemove = sender.TopLevelItems.Length + sender.FallbackItems.Length;
@@ -122,7 +133,8 @@ public partial class TopLevelCommandManager : ObservableObject,
var wrapper = clone[i];
try
{
var thisCommand = wrapper.Model.Unsafe;
// TODO! this can be safer, we're not directly exposing ICommandItem's out of CPW anymore
var thisCommand = wrapper.ItemViewModel.Model.Unsafe;
if (thisCommand != null)
{
var isTheSame = thisCommand == firstCommand;
@@ -138,16 +150,21 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
WeakReference<IPageContext> weakSelf = new(this);
// Fetch the new items
await sender.LoadTopLevelCommands();
await sender.LoadTopLevelCommands(_serviceProvider, weakSelf);
var settings = _serviceProvider.GetService<SettingsModel>()!;
foreach (var i in sender.TopLevelItems)
{
newItems.Add(new(new(i), false, sender.ExtensionHost, sender.ProviderId, _serviceProvider));
newItems.Add(i);
}
foreach (var i in sender.FallbackItems)
{
newItems.Add(new(new(i), true, sender.ExtensionHost, sender.ProviderId, _serviceProvider));
newItems.Add(i);
}
// Slice out the old commands
@@ -253,7 +270,7 @@ public partial class TopLevelCommandManager : ObservableObject,
async () =>
{
// Then find all the top-level commands that belonged to that extension
List<TopLevelCommandItemWrapper> commandsToRemove = [];
List<TopLevelViewModel> commandsToRemove = [];
lock (TopLevelCommands)
{
foreach (var extension in extensions)
@@ -292,7 +309,7 @@ public partial class TopLevelCommandManager : ObservableObject,
});
}
public TopLevelCommandItemWrapper? LookupCommand(string id)
public TopLevelViewModel? LookupCommand(string id)
{
lock (TopLevelCommands)
{
@@ -310,4 +327,10 @@ public partial class TopLevelCommandManager : ObservableObject,
public void Receive(ReloadCommandsMessage message) =>
ReloadAllCommandsAsync().ConfigureAwait(false);
void IPageContext.ShowException(Exception ex, string? extensionHint)
{
var errorMessage = $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n";
CommandPaletteHost.Instance.Log(errorMessage);
}
}