Initial prototype for extension list search

Adds a rough implementation that appears to work, but it hasn’t been tested. Don’t rely on it yet—I’m too tired to test properly or write a polished commit message.
This commit is contained in:
Jiří Polášek
2025-08-29 01:00:41 +02:00
parent ef6f4b2c3d
commit 456ace19e7
7 changed files with 308 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
// 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;
public record ReloadFinishedMessage();

View File

@@ -0,0 +1,142 @@
// 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.
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Provides filtering over the list of provider settings view models.
/// Intended to be used by the UI to bind a TextBox (SearchText) and an ItemsRepeater (FilteredProviders).
/// </summary>
public partial class SettingsExtensionsViewModel : ObservableObject
{
private readonly ObservableCollection<ProviderSettingsViewModel> _source;
private readonly TaskScheduler _uiScheduler;
public ObservableCollection<ProviderSettingsViewModel> FilteredProviders { get; } = [];
private string _searchText = string.Empty;
public string SearchText
{
get => _searchText;
set
{
if (_searchText != value)
{
_searchText = value;
OnPropertyChanged();
ApplyFilter();
}
}
}
public string ItemCounterText
{
get
{
var hasQuery = !string.IsNullOrWhiteSpace(_searchText);
var count = hasQuery ? FilteredProviders.Count : _source.Count;
var suffix = hasQuery ? "extensions found" : "extensions installed";
return $"{count} {suffix}";
}
}
private bool _showManualReloadOverlay;
public bool ShowManualReloadOverlay
{
get => _showManualReloadOverlay;
private set
{
if (_showManualReloadOverlay != value)
{
_showManualReloadOverlay = value;
OnPropertyChanged();
}
}
}
public bool ShowNoResultsPanel => !string.IsNullOrWhiteSpace(_searchText) && FilteredProviders.Count == 0;
public bool HasResults => !ShowNoResultsPanel;
public IRelayCommand<string?> OpenStoreWithExtensionCommand { get; }
public IRelayCommand ReloadExtensionsCommand { get; }
public SettingsExtensionsViewModel(ObservableCollection<ProviderSettingsViewModel> source, TaskScheduler uiScheduler)
{
_source = source;
_uiScheduler = uiScheduler;
_source.CollectionChanged += Source_CollectionChanged;
ApplyFilter();
OpenStoreWithExtensionCommand = new RelayCommand<string?>(OpenStoreWithExtension);
ReloadExtensionsCommand = new RelayCommand(ReloadExtensions);
WeakReferenceMessenger.Default.Register<ReloadFinishedMessage>(this, (_, _) =>
{
Task.Factory.StartNew(() => ShowManualReloadOverlay = false, CancellationToken.None, TaskCreationOptions.None, _uiScheduler);
});
}
private void Source_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
ApplyFilter();
}
private void ApplyFilter()
{
var query = _searchText;
var filtered = ListHelpers.FilterList(_source, query, Matches);
ListHelpers.InPlaceUpdateList(FilteredProviders, filtered);
OnPropertyChanged(nameof(ItemCounterText));
OnPropertyChanged(nameof(HasResults));
OnPropertyChanged(nameof(ShowNoResultsPanel));
}
private static int Matches(string query, ProviderSettingsViewModel item)
{
if (string.IsNullOrWhiteSpace(query))
{
return 100;
}
return Contains(item.DisplayName, query)
|| Contains(item.ExtensionName, query)
|| Contains(item.ExtensionSubtext, query)
? 100
: 0;
}
private static bool Contains(string? haystack, string needle)
{
return !string.IsNullOrEmpty(haystack) && haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
}
private void OpenStoreWithExtension(string? query)
{
const string ExtensionsAssocUri = "ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette";
ShellHelpers.OpenInShell(ExtensionsAssocUri);
}
private void ReloadExtensions()
{
ShowManualReloadOverlay = true;
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>();
}
}

View File

@@ -120,6 +120,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsExtensionsViewModel Extensions { get; }
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
{
_settings = settings;
@@ -135,6 +137,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider);
CommandProviders.Add(settingsModel);
}
Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler);
}
private IEnumerable<CommandProviderWrapper> GetCommandProviders()

View File

@@ -259,6 +259,9 @@ public partial class TopLevelCommandManager : ObservableObject,
IsLoading = false;
// Send on the current thread; receivers should marshal to UI if needed
WeakReferenceMessenger.Default.Send<ReloadFinishedMessage>();
return true;
}

View File

@@ -14,19 +14,107 @@
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">
<Page.Resources>
<converters:BoolNegationConverter x:Key="InvertedBoolConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</Page.Resources>
<Page.KeyboardAccelerators>
<KeyboardAccelerator Modifiers="Control" Key="F" Invoked="OnFindInvoked"/>
</Page.KeyboardAccelerators>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="1">
<ScrollViewer Grid.Row="1" x:Name="RootScrollViewer">
<Grid Padding="16">
<StackPanel
MaxWidth="1000"
HorizontalAlignment="Stretch"
Spacing="{StaticResource SettingsCardSpacing}">
<ItemsRepeater ItemsSource="{x:Bind viewModel.CommandProviders, Mode=OneWay}" Layout="{StaticResource VerticalStackLayout}">
<!-- toolbar 1 -->
<Grid Padding="0,0,0,8">
<AutoSuggestBox x:Uid="Settings_ExtensionsPage_SearchBox"
x:Name="SearchBox"
Width="320"
MaxWidth="320"
HorizontalAlignment="Left"
Text="{x:Bind viewModel.Extensions.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<AutoSuggestBox.QueryIcon>
<SymbolIcon Symbol="Find" />
</AutoSuggestBox.QueryIcon>
</AutoSuggestBox>
</Grid>
<!-- toolbar 2 -->
<Grid Padding="0,8,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="ItemCounterTextBlock"
Grid.Column="0"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind viewModel.Extensions.ItemCounterText, Mode=OneWay}" />
<!-- Right actions: spinner + label (when reloading) then button -->
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center"
Visibility="{x:Bind viewModel.Extensions.ShowManualReloadOverlay, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressRing Width="16" Height="16" IsActive="True" />
<TextBlock x:Uid="Settings_ExtensionsPage_Reloading_Text"
Style="{StaticResource BodyTextBlockStyle}"
VerticalAlignment="Center" />
</StackPanel>
<Button x:Uid="Settings_ExtensionsPage_More_Button"
x:Name="MoreButton"
VerticalAlignment="Center">
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedRight">
<MenuFlyoutItem x:Uid="Settings_ExtensionsPage_More_Reload_MenuFlyoutItem"
Command="{x:Bind viewModel.Extensions.ReloadExtensionsCommand}">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE777;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
</Button.Flyout>
<FontIcon Glyph="&#xE10C;" />
</Button>
</StackPanel>
</Grid>
<!-- Empty state when no results match the current search -->
<Grid x:Name="NoResultsPanel"
x:Load="{x:Bind viewModel.Extensions.ShowNoResultsPanel, Mode=OneWay}"
Padding="48"
CornerRadius="4">
<StackPanel Spacing="8"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock x:Uid="Settings_ExtensionsPage_NoResults_Primary"
Style="{StaticResource BodyStrongTextBlockStyle}"
TextAlignment="Center" />
<TextBlock x:Uid="Settings_ExtensionsPage_NoResults_Secondary"
Style="{StaticResource BodyTextBlockStyle}"
TextAlignment="Center" />
<HyperlinkButton x:Uid="Settings_ExtensionsPage_NoResults_Hyperlink"
HorizontalAlignment="Center"
Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}"
Margin="0,16,0,0" />
</StackPanel>
</Grid>
<ItemsRepeater x:Name="ProvidersRepeater"
x:Load="{x:Bind viewModel.Extensions.HasResults, Mode=OneWay}"
ItemsSource="{x:Bind viewModel.Extensions.FilteredProviders, Mode=OneWay}"
Layout="{StaticResource VerticalStackLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="viewModels:ProviderSettingsViewModel">
<controls:SettingsCard
@@ -54,6 +142,16 @@
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<TextBlock x:Uid="Settings_ExtensionsPage_RelatedItemsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard
x:Uid="Settings_ExtensionsPage_ManageExtensions_SettingsCard"
HeaderIcon="{ui:FontIcon Glyph=&#xE719;}"
IsClickEnabled="True"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}" />
</StackPanel>
</Grid>
</ScrollViewer>

View File

@@ -8,6 +8,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace Microsoft.CmdPal.UI.Settings;
@@ -35,4 +36,10 @@ public sealed partial class ExtensionsPage : Page
}
}
}
private void OnFindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
SearchBox?.Focus(FocusState.Keyboard);
args.Handled = true;
}
}

View File

@@ -441,4 +441,49 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="NavigationPaneOpened" xml:space="preserve">
<value>Navigation page opened</value>
</data>
<data name="Settings_ExtensionsPage_ManageExtensions_SettingsCard.Header" xml:space="preserve">
<value>Get more extension in the Microsoft Store</value>
</data>
<data name="Settings_ExtensionsPage_ManageExtensions_SettingsCard.Description" xml:space="preserve">
<value>Opens the Microsoft Store to show available third-party extensions</value>
</data>
<data name="Settings_ExtensionsPage_RelatedItemsHeader.Text" xml:space="preserve">
<value>Related links</value>
</data>
<data name="Settings_ExtensionsPage_SearchBox.PlaceholderText" xml:space="preserve">
<value>Search extensions</value>
</data>
<data name="Settings_ExtensionsPage_NoResults_Primary.Text" xml:space="preserve">
<value>No extensions found</value>
</data>
<data name="Settings_ExtensionsPage_NoResults_Secondary.Text" xml:space="preserve">
<value>Try a different search term</value>
</data>
<data name="Settings_ExtensionsPage_NoResults_Hyperlink.Content" xml:space="preserve">
<value>Browse Microsoft Store for Command Palette extensions</value>
</data>
<data name="Settings_ExtensionsPage_SearchBox.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Press Ctrl+F to focus the search box</value>
</data>
<data name="Settings_ExtensionsPage_SearchBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.AcceleratorKey" xml:space="preserve">
<value>Ctrl+F</value>
</data>
<data name="Settings_ExtensionsPage_SearchBox.AccessKey" xml:space="preserve">
<value>S</value>
</data>
<data name="Settings_ExtensionsPage_NoResults_Hyperlink.AccessKey" xml:space="preserve">
<value>B</value>
</data>
<data name="Settings_ExtensionsPage_ManageExtensions_SettingsCard.AccessKey" xml:space="preserve">
<value>M</value>
</data>
<data name="Settings_ExtensionsPage_More_Button.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>See more</value>
</data>
<data name="Settings_ExtensionsPage_More_Reload_MenuFlyoutItem.Text" xml:space="preserve">
<value>Reload extensions</value>
</data>
<data name="Settings_ExtensionsPage_Reloading_Text.Text" xml:space="preserve">
<value>Reloading extensions…</value>
</data>
</root>