diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 1223683f08e0..a67e3e05115b 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1182,6 +1182,7 @@ openas opencode OPENFILENAME opensource +openurl openxmlformats OPTIMIZEFORINVOKE ORPHANEDDIALOGTITLE diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Helpers/SupersedingAsyncGate.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Helpers/SupersedingAsyncGate.cs index f5e4d3e97b01..b1fe19a78165 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Helpers/SupersedingAsyncGate.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Helpers/SupersedingAsyncGate.cs @@ -2,10 +2,6 @@ // 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.Threading; -using System.Threading.Tasks; - namespace Microsoft.CmdPal.Core.Common.Helpers; /// @@ -36,24 +32,36 @@ public SupersedingAsyncGate(Func action) public async Task ExecuteAsync(CancellationToken cancellationToken = default) { TaskCompletionSource tcs; + var startWorker = false; lock (_lock) { + // Cancel the currently running iteration (if any) before replacing its TCS. _currentCancellationSource?.Cancel(); + + // Mark prior waiter as superseded. Fault (not cancel) so callers can distinguish. _currentTcs?.TrySetException(new OperationCanceledException("Superseded by newer call")); - tcs = new(); + // IMPORTANT: RunContinuationsAsynchronously prevents continuations from running + // inside this lock, avoiding reentrancy / potential deadlocks. + tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); _currentTcs = tcs; _callId++; - var shouldStartExecution = _executingTask is null; - if (shouldStartExecution) + if (_executingTask is null) { - _executingTask = Task.Run(ExecuteLoop, CancellationToken.None); + startWorker = true; } } - await using var ctr = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); + // Start worker outside lock to avoid holding lock while scheduling. + if (startWorker) + { + // Fire-and-forget loop; all state transitions guarded by _lock. + _executingTask = Task.Run(ExecuteLoop, cancellationToken); + } + + using var ctr = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); await tcs.Task; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index c59c90814b85..939b42de147d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -188,11 +188,12 @@ private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] Func makeAndAdd = (ICommandItem? i, bool fallback) => { CommandItemViewModel commandItemViewModel = new(new(i), pageContext); - TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider); + TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i); topLevelViewModel.InitializeProperties(); return topLevelViewModel; }; + if (commands is not null) { TopLevelItems = commands diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs index 3a3a9bd405d2..7fc3d6f6ef8b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/BuiltInsCommandProvider.cs @@ -2,7 +2,6 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -27,9 +26,9 @@ public override ICommandItem[] TopLevelCommands() => public override IFallbackCommandItem[] FallbackCommands() => [ - new FallbackCommandItem(quitCommand, displayTitle: Properties.Resources.builtin_quit_subtitle) { Subtitle = Properties.Resources.builtin_quit_subtitle }, + new FallbackCommandItem(quitCommand, displayTitle: Properties.Resources.builtin_quit_subtitle, quitCommand.Id) { Subtitle = Properties.Resources.builtin_quit_subtitle }, _fallbackReloadItem, - _fallbackLogItem, + _fallbackLogItem ]; public BuiltInsCommandProvider() diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs index a96d49ff79af..2f44b018e152 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackLogItem.cs @@ -13,8 +13,10 @@ internal sealed partial class FallbackLogItem : FallbackCommandItem { private readonly LogMessagesPage _logMessagesPage; + private const string _id = "com.microsoft.cmdpal.log"; + public FallbackLogItem() - : base(new LogMessagesPage() { Id = "com.microsoft.cmdpal.log" }, Resources.builtin_log_subtitle) + : base(new LogMessagesPage() { Id = _id }, Resources.builtin_log_subtitle, _id) { _logMessagesPage = (LogMessagesPage)Command!; Title = string.Empty; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs index 37a37d9283c1..489c73a53710 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/FallbackReloadItem.cs @@ -2,7 +2,6 @@ // 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 Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; @@ -11,10 +10,13 @@ internal sealed partial class FallbackReloadItem : FallbackCommandItem { private readonly ReloadExtensionsCommand _reloadCommand; + private const string _id = "com.microsoft.cmdpal.reload"; + public FallbackReloadItem() : base( - new ReloadExtensionsCommand() { Id = "com.microsoft.cmdpal.reload" }, - Properties.Resources.builtin_reload_display_title) + new ReloadExtensionsCommand() { Id = _id }, + Properties.Resources.builtin_reload_display_title, + _id) { _reloadCommand = (ReloadExtensionsCommand)Command!; Title = string.Empty; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs index ef7c879d3682..b6950eb9cee6 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -28,20 +28,21 @@ public partial class MainListPage : DynamicListPage, IRecipient, IRecipient, IDisposable { - private readonly string[] _specialFallbacks = [ - "com.microsoft.cmdpal.builtin.run", - "com.microsoft.cmdpal.builtin.calculator" - ]; - + private readonly object _fallbackDebounceLock = new(); private readonly IServiceProvider _serviceProvider; private readonly TopLevelCommandManager _tlcManager; private List>? _filteredItems; private List>? _filteredApps; - private List>? _fallbackItems; + private IEnumerable>? _fallbackItems; private IEnumerable>? _scoredFallbackItems; private bool _includeApps; private bool _filteredItemsIncludesApps; private int _appResultLimit = 10; + private SettingsModel _settings; + + private int _fallbackBatchId; + private Timer? _fallbackItemsChangedDebounceTimer; + private const int FallbackItemsChangedDebounceDelayMs = 75; private InterlockedBoolean _refreshRunning; private InterlockedBoolean _refreshRequested; @@ -74,6 +75,7 @@ public MainListPage(IServiceProvider serviceProvider) WeakReferenceMessenger.Default.Register(this); var settings = _serviceProvider.GetService()!; + _settings = settings; settings.SettingsChanged += SettingsChangedHandler; HotReloadSettings(settings); _includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId); @@ -180,7 +182,10 @@ public override IListItem[] GetItems() // Add fallback items post-sort so they are always at the end of the list // and eventually ordered based on user preference - .Concat(_fallbackItems is not null ? _fallbackItems.Where(w => !string.IsNullOrEmpty(w.Item.Title)) : []) + .Concat(_fallbackItems is not null ? + _fallbackItems + .Where(w => !string.IsNullOrWhiteSpace(w.Item.Title)) + .OrderByDescending(o => o.Score) : []) .Select(s => s.Item) .ToArray(); return items; @@ -250,30 +255,34 @@ public override void UpdateSearchText(string oldSearch, string newSearch) } // prefilter fallbacks - var specialFallbacks = new List(_specialFallbacks.Length); - var commonFallbacks = new List(); + var specialFallbacks = new List(); + var commonFallbacks = new List>(); - foreach (var s in commands) + lock (_settings) { - if (!s.IsFallback) + foreach (var s in commands) { - continue; - } + if (!s.IsFallback) + { + continue; + } - if (_specialFallbacks.Contains(s.CommandProviderId)) - { - specialFallbacks.Add(s); - } - else - { - commonFallbacks.Add(s); + var fallbackSettings = _settings.GetFallbackSettings(s.Id); + if (fallbackSettings.IncludeInGlobalResults) + { + specialFallbacks.Add(s); + } + else + { + commonFallbacks.Add(new Scored() { Item = s, Score = fallbackSettings.WeightBoost }); + } } } // start update of fallbacks; update special fallbacks separately, // so they can finish faster UpdateFallbacks(SearchText, specialFallbacks, token); - UpdateFallbacks(SearchText, commonFallbacks, token); + UpdateFallbacks(SearchText, commonFallbacks.Select(s => s.Item).Cast().ToList(), token); if (token.IsCancellationRequested) { @@ -308,7 +317,7 @@ public override void UpdateSearchText(string oldSearch, string newSearch) } var newFilteredItems = Enumerable.Empty(); - var newFallbacks = Enumerable.Empty(); + var newFallbacks = Enumerable.Empty>(); var newApps = Enumerable.Empty(); if (_filteredItems is not null) @@ -333,7 +342,7 @@ public override void UpdateSearchText(string oldSearch, string newSearch) if (_fallbackItems is not null) { - newFallbacks = _fallbackItems.Select(s => s.Item); + newFallbacks = _fallbackItems; } if (token.IsCancellationRequested) @@ -384,32 +393,24 @@ public override void UpdateSearchText(string oldSearch, string newSearch) } var history = _serviceProvider.GetService()!.RecentCommands!; - Func scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); }; + Func scoreTopLevelItem = (a, b) => { return ScoreTopLevelItem(a, b, history); }; // Produce a list of everything that matches the current filter. - _filteredItems = [.. ListHelpers.FilterListWithScores(newFilteredItems ?? [], SearchText, scoreItem)]; - - if (token.IsCancellationRequested) - { - return; - } - - IEnumerable newFallbacksForScoring = commands.Where(s => s.IsFallback && _specialFallbacks.Contains(s.CommandProviderId)); + _filteredItems = [.. ListHelpers.FilterListWithScores(newFilteredItems ?? [], SearchText, scoreTopLevelItem)]; if (token.IsCancellationRequested) { return; } - _scoredFallbackItems = ListHelpers.FilterListWithScores(newFallbacksForScoring ?? [], SearchText, scoreItem); + _scoredFallbackItems = ListHelpers.FilterListWithScores(specialFallbacks ?? [], SearchText, scoreTopLevelItem); if (token.IsCancellationRequested) { return; } - // Defaulting scored to 1 but we'll eventually use user rankings - _fallbackItems = [.. newFallbacks.Select(f => new Scored { Item = f, Score = 1 })]; + _fallbackItems = newFallbacks; if (token.IsCancellationRequested) { @@ -419,7 +420,7 @@ public override void UpdateSearchText(string oldSearch, string newSearch) // Produce a list of filtered apps with the appropriate limit if (newApps.Any()) { - var scoredApps = ListHelpers.FilterListWithScores(newApps, SearchText, scoreItem); + var scoredApps = ListHelpers.FilterListWithScores(newApps, SearchText, scoreTopLevelItem); if (token.IsCancellationRequested) { @@ -447,33 +448,105 @@ public override void UpdateSearchText(string oldSearch, string newSearch) private void UpdateFallbacks(string newSearch, IReadOnlyList commands, CancellationToken token) { - _ = Task.Run( - () => + if (commands.Count == 0) { - var needsToUpdate = false; + return; + } + + // Start a new batch; invalidate any previous in‑flight batch. + var batchId = Interlocked.Increment(ref _fallbackBatchId); + + // Cancel any pending debounce raise from prior batch. + lock (_fallbackDebounceLock) + { + _fallbackItemsChangedDebounceTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + + // Track remaining updates so we can force a final RaiseItemsChanged. + var remaining = commands.Count; - foreach (var command in commands) + foreach (var command in commands) + { + _ = Task.Run( + () => { - if (token.IsCancellationRequested) + try { - return; - } + if (token.IsCancellationRequested) + { + return; + } - var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch); - needsToUpdate = needsToUpdate || changedVisibility; - } + // Skip if this batch became stale (new search started). + if (batchId != _fallbackBatchId) + { + return; + } - if (needsToUpdate) - { - if (token.IsCancellationRequested) + var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch); + + if (token.IsCancellationRequested || batchId != _fallbackBatchId) + { + return; + } + + if (changedVisibility) + { + // Visibility/text changed—schedule a debounced raise. + ScheduleDebouncedFallbackRaise(batchId); + } + } + catch (Exception ex) { - return; + Logger.LogError("Fallback update failed", ex); + } + finally + { + // When the last one finishes, force a raise (cancels debounce). + if (Interlocked.Decrement(ref remaining) == 0) + { + // Last completion may happen before debounce fires; ensure current batch. + if (batchId == _fallbackBatchId && !token.IsCancellationRequested) + { + lock (_fallbackDebounceLock) + { + _fallbackItemsChangedDebounceTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + + RaiseItemsChanged(); + } + } } + }, + token); + } + } - RaiseItemsChanged(); + // Add helper methods somewhere in the class (e.g., before UpdateFallbacks) + private void ScheduleDebouncedFallbackRaise(int batchId) + { + lock (_fallbackDebounceLock) + { + // Ignore stale batches + if (batchId != _fallbackBatchId) + { + return; } - }, - token); + + _fallbackItemsChangedDebounceTimer ??= new Timer(FallbackItemsChangedDebounceTimerCallback!, null, Timeout.Infinite, Timeout.Infinite); + + // Restart debounce timer + _fallbackItemsChangedDebounceTimer.Change(FallbackItemsChangedDebounceDelayMs, Timeout.Infinite); + } + } + + private void FallbackItemsChangedDebounceTimerCallback(object? state) + { + // Ensure this callback is for the current batch + var currentBatch = Volatile.Read(ref _fallbackBatchId); + RaiseItemsChanged(); + + // We do NOT dispose the timer here—keep it for reuse. } private bool ActuallyLoading() @@ -600,7 +673,11 @@ private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem) private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender); - private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails; + private void HotReloadSettings(SettingsModel settings) + { + _settings = settings; + ShowDetails = settings.ShowAppDetails; + } public void Dispose() { @@ -617,6 +694,13 @@ public void Dispose() } WeakReferenceMessenger.Default.UnregisterAll(this); + + lock (_fallbackDebounceLock) + { + _fallbackItemsChangedDebounceTimer?.Dispose(); + _fallbackItemsChangedDebounceTimer = null; + } + GC.SuppressFinalize(this); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettings.cs new file mode 100644 index 000000000000..c5215c116cdf --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettings.cs @@ -0,0 +1,28 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public class FallbackSettings +{ + public bool IsEnabled { get; set; } = true; + + public bool IncludeInGlobalResults { get; set; } + + public int WeightBoost { get; set; } + + public FallbackSettings() + { + } + + [JsonConstructor] + public FallbackSettings(bool isEnabled, int weightBoost, bool includeInGlobalResults) + { + IsEnabled = isEnabled; + WeightBoost = weightBoost; + IncludeInGlobalResults = includeInGlobalResults; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettingsViewModel.cs new file mode 100644 index 000000000000..37ab0e0d5f5d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettingsViewModel.cs @@ -0,0 +1,101 @@ +// 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 CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.Core.Common.Services; +using Microsoft.CmdPal.Core.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class FallbackSettingsViewModel( + TopLevelViewModel fallback, + FallbackSettings _fallbackSettings, + ProviderSettingsViewModel _providerSettings, + IServiceProvider _serviceProvider, + SettingsViewModel _settingsViewModel) : ObservableObject +{ + private readonly SettingsModel _settings = _serviceProvider.GetService()!; + + public string DisplayName => string.IsNullOrWhiteSpace(fallback.DisplayTitle) + ? (string.IsNullOrWhiteSpace(fallback.Title) ? _providerSettings.DisplayName : fallback.Title) + : fallback.DisplayTitle; + + public string ExtensionName => _providerSettings.ExtensionName; + + public IExtensionWrapper? Extension => _providerSettings.Extension; + + public ProviderSettingsViewModel ProviderSettingsViewModel => _providerSettings; + + public string ExtensionVersion => _providerSettings.ExtensionVersion; + + public string ProviderId => fallback.CommandProviderId; + + public IconInfoViewModel Icon => _providerSettings.Icon; + + public ContentPageViewModel? SettingsPage => _providerSettings.SettingsPage; + + public string Id => fallback.Id; + + public bool IsEnabled + { + get => _fallbackSettings.IsEnabled; + set + { + if (value != _fallbackSettings.IsEnabled) + { + _fallbackSettings.IsEnabled = value; + + if (!_fallbackSettings.IsEnabled) + { + _fallbackSettings.IncludeInGlobalResults = false; + } + + Save(); + OnPropertyChanged(nameof(IsEnabled)); + } + } + } + + public bool IncludeInGlobalResults + { + get => _fallbackSettings.IncludeInGlobalResults; + set + { + if (value != _fallbackSettings.IncludeInGlobalResults) + { + _fallbackSettings.IncludeInGlobalResults = value; + + if (!_fallbackSettings.IsEnabled) + { + _fallbackSettings.IsEnabled = true; + } + + Save(); + OnPropertyChanged(nameof(IncludeInGlobalResults)); + } + } + } + + public int WeightBoost + { + get => _fallbackSettings.WeightBoost; + set + { + if (value != _fallbackSettings.WeightBoost) + { + _fallbackSettings.WeightBoost = value; + } + } + } + + private void Save() + { + _settingsViewModel.ApplyFallbackSort(); + SettingsModel.SaveSettings(_settings); + WeakReferenceMessenger.Default.Send(new()); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs index 1e20040d57dc..c43b6e2c0311 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs @@ -10,8 +10,6 @@ public class ProviderSettings { public bool IsEnabled { get; set; } = true; - public Dictionary FallbackCommands { get; set; } = []; - [JsonIgnore] public string ProviderDisplayName { get; set; } = string.Empty; @@ -44,14 +42,4 @@ public void Connect(CommandProviderWrapper wrapper) throw new InvalidDataException("Did you add a built-in command and forget to set the Id? Make sure you do that!"); } } - - public bool IsFallbackEnabled(TopLevelViewModel command) - { - return FallbackCommands.TryGetValue(command.Id, out var enabled) ? enabled : true; - } - - public void SetFallbackEnabled(TopLevelViewModel command, bool enabled) - { - FallbackCommands[command.Id] = enabled; - } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs index 52b2bea003c1..7b4ff36c1ef5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs @@ -28,7 +28,7 @@ public partial class ProviderSettingsViewModel( public string ExtensionSubtext => IsEnabled ? HasFallbackCommands ? - $"{ExtensionName}, {TopLevelCommands.Count} commands, {FallbackCommands.Count} fallback commands" : + $"{ExtensionName}, {TopLevelCommands.Count} commands, {_provider.FallbackItems.Length} fallback commands" : $"{ExtensionName}, {TopLevelCommands.Count} commands" : Resources.builtin_disabled_extension; @@ -158,8 +158,6 @@ public List FallbackCommands } } - public bool HasFallbackCommands => _provider.FallbackItems?.Length > 0; - private List BuildFallbackViewModels() { var thisProvider = _provider; @@ -169,6 +167,8 @@ private List BuildFallbackViewModels() return [.. providersCommands]; } + public bool HasFallbackCommands => _provider.FallbackItems?.Length > 0; + private void Save() => SettingsModel.SaveSettings(_settings); private void InitializeSettingsPage() diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs index a06ec8adf7a2..5cd2d6a6998b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs @@ -44,11 +44,13 @@ public partial class SettingsModel : ObservableObject public bool AllowExternalReload { get; set; } - public Dictionary ProviderSettings { get; set; } = []; + public Dictionary ProviderSettings { get; set; } = new(); - public Dictionary Aliases { get; set; } = []; + public Dictionary FallbackSettings { get; set; } = new(); - public List CommandHotkeys { get; set; } = []; + public Dictionary Aliases { get; set; } = new(); + + public List CommandHotkeys { get; set; } = new(); public MonitorBehavior SummonOn { get; set; } = MonitorBehavior.ToMouse; @@ -81,6 +83,18 @@ public ProviderSettings GetProviderSettings(CommandProviderWrapper provider) return settings; } + public FallbackSettings GetFallbackSettings(string fallbackId) + { + FallbackSettings? settings; + if (!FallbackSettings.TryGetValue(fallbackId, out settings)) + { + settings = new FallbackSettings(); + FallbackSettings[fallbackId] = settings; + } + + return settings; + } + public static SettingsModel LoadSettings() { if (string.IsNullOrEmpty(FilePath)) @@ -171,20 +185,6 @@ internal static string SettingsJsonPath() // now, the settings is just next to the exe return Path.Combine(directory, "settings.json"); } - - // [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] - // private static readonly JsonSerializerOptions _serializerOptions = new() - // { - // WriteIndented = true, - // Converters = { new JsonStringEnumConverter() }, - // }; - // private static readonly JsonSerializerOptions _deserializerOptions = new() - // { - // PropertyNameCaseInsensitive = true, - // IncludeFields = true, - // Converters = { new JsonStringEnumConverter() }, - // AllowTrailingCommas = true, - // }; } [JsonSerializable(typeof(float))] diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs index 380f2340bad8..92c4d67c92a0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs @@ -4,6 +4,8 @@ using System.Collections.ObjectModel; using System.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.Extensions.DependencyInjection; @@ -142,6 +144,10 @@ public bool DisableAnimations public SettingsExtensionsViewModel Extensions { get; } + private ObservableCollection _fallbackSettings = new(); + + public ObservableCollection FallbackSettings => _fallbackSettings; + public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler) { _settings = settings; @@ -152,12 +158,30 @@ public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvide foreach (var item in activeProviders) { - var providerSettings = settings.GetProviderSettings(item); + var providerSettings = _settings.GetProviderSettings(item); var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider); CommandProviders.Add(settingsModel); + + if (item.FallbackItems is not null && item.FallbackItems.Length > 0) + { + foreach (var fallback in item.FallbackItems) + { + // If the underlying command still has no Id (should be rare now), skip registering until it gets one. + if (string.IsNullOrEmpty(fallback.Id)) + { + continue; + } + + var fallbackSettings = _settings.GetFallbackSettings(fallback.Id); + var fallbackSettingsModel = new FallbackSettingsViewModel(fallback, fallbackSettings, settingsModel, _serviceProvider, this); + _fallbackSettings.Add(fallbackSettingsModel); + } + } } + ApplyFallbackSort(); + Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler); } @@ -168,5 +192,35 @@ private IEnumerable GetCommandProviders() return allProviders; } + // ReorderFallbacks is called after the UI collection has been reordered. + // Assign descending WeightBoost values (highest priority = largest boost) once, + // then persist settings. + public void ReorderFallbacks(FallbackSettingsViewModel _, List allFallbacks) + { + // Highest weight to first item + var weight = allFallbacks.Count; + foreach (var f in allFallbacks) + { + // Each setter persists WeightBoost on the underlying FallbackSettings object + f.WeightBoost = weight--; + } + + Save(); + ApplyFallbackSort(); + + WeakReferenceMessenger.Default.Send(new()); + } + + public void ApplyFallbackSort() + { + _fallbackSettings = new(_fallbackSettings + .OrderByDescending(o => o.IsEnabled) + .ThenByDescending(o => o.IncludeInGlobalResults) + .ThenByDescending(o => o.WeightBoost) + .ToList()); + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackSettings))); + } + private void Save() => SettingsModel.SaveSettings(_settings); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs index fc5e36d1e224..2a65d930ae15 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -4,11 +4,9 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Messages; -using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -27,7 +25,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem private readonly string _commandProviderId; - private string IdFromModel => _commandItemViewModel.Command.Id; + private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id; + + private string _fallbackId = string.Empty; private string _generatedId = string.Empty; @@ -41,7 +41,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem [ObservableProperty] public partial ObservableCollection Tags { get; set; } = []; - public string Id => string.IsNullOrEmpty(IdFromModel) ? _generatedId : IdFromModel; + public string Id => string.IsNullOrWhiteSpace(IdFromModel) ? _generatedId : IdFromModel; public CommandPaletteHost ExtensionHost { get; private set; } @@ -158,14 +158,20 @@ public bool IsDirectAlias public bool IsEnabled { - get => _providerSettings.IsFallbackEnabled(this); - set + get { - if (value != IsEnabled) + if (IsFallback) + { + if (_settings.FallbackSettings.TryGetValue(_fallbackId, out var fallbackSettings)) + { + return fallbackSettings.IsEnabled; + } + + return true; + } + else { - _providerSettings.SetFallbackEnabled(this, value); - Save(); - WeakReferenceMessenger.Default.Send(new()); + return _providerSettings.IsEnabled; } } } @@ -177,7 +183,8 @@ public TopLevelViewModel( string commandProviderId, SettingsModel settings, ProviderSettings providerSettings, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + ICommandItem? commandItem) { _serviceProvider = serviceProvider; _settings = settings; @@ -187,6 +194,10 @@ public TopLevelViewModel( IsFallback = isFallback; ExtensionHost = extensionHost; + if (isFallback && commandItem is FallbackCommandItem fallback) + { + _fallbackId = fallback.Id; + } item.PropertyChanged += Item_PropertyChanged; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml index 72b51fd7247c..c933004c05b0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionPage.xaml @@ -127,7 +127,7 @@ Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Visibility="{x:Bind ViewModel.HasFallbackCommands}" /> - @@ -146,12 +146,12 @@ - + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/FallbacksPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/FallbacksPage.xaml.cs new file mode 100644 index 000000000000..b29df86a1914 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/FallbacksPage.xaml.cs @@ -0,0 +1,51 @@ +// 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 CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.ApplicationModel.DataTransfer; + +namespace Microsoft.CmdPal.UI.Settings; + +public sealed partial class FallbacksPage : Page +{ + private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + + private readonly SettingsViewModel? viewModel; + + public FallbacksPage() + { + this.InitializeComponent(); + + var settings = App.Current.Services.GetService()!; + viewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler); + } + + private void ListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) + { + if (args.DropResult == DataPackageOperation.Move && + args.Items.Count > 0) + { + var item = args.Items[0]; + if (item is FallbackSettingsViewModel droppedCommand) + { + viewModel?.ReorderFallbacks(droppedCommand, sender.Items.Cast().ToList()); + } + } + } + + private void Button_Click(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is ProviderSettingsViewModel vm) + { + WeakReferenceMessenger.Default.Send(new(vm)); + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs index 5d042a09e356..31c81df1cebd 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs @@ -10,7 +10,6 @@ using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; using WinUIEx; using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw index 13cda5925bff..5a0580e1653b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw @@ -431,8 +431,8 @@ Right-click to remove the key combination, thereby deactivating the shortcut. Last Position Reopen the window where it was last closed - - + + Settings @@ -475,6 +475,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut. Navigated to {0} page + + Include in the Global result + + + Show results on queries without direct activation command + Settings (Ctrl+,) @@ -493,28 +499,28 @@ Right-click to remove the key combination, thereby deactivating the shortcut. Reloading extensions.. - + Discover more extensions - + Find more extensions on the Microsoft Store or WinGet. - + Learn how to create your own extensions - + Find extensions on the Microsoft Store - + Microsoft Store - + Find extensions on WinGet - + Microsoft Store - + Search extensions \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs index 4367c6781033..4edeb59b8f7d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs @@ -13,8 +13,10 @@ public sealed partial class FallbackCalculatorItem : FallbackCommandItem private readonly CopyTextCommand _copyCommand = new(string.Empty); private readonly ISettingsInterface _settings; + private const string _id = "com.microsoft.cmdpal.builtin.calculator.fallback"; + public FallbackCalculatorItem(ISettingsInterface settings) - : base(new NoOpCommand(), Resources.calculator_title) + : base(new NoOpCommand(), Resources.calculator_title, _id) { Command = _copyCommand; _copyCommand.Name = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs index 2b408f24dc5f..6ee5a525d037 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs @@ -25,8 +25,10 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System private Func _suppressCallback; + private const string _id = "com.microsoft.cmdpal.builtin.indexer.fallback"; + public FallbackOpenFileItem() - : base(_baseCommandWithId, Resources.Indexer_Find_Path_fallback_display_title) + : base(_baseCommandWithId, Resources.Indexer_Find_Path_fallback_display_title, _id) { Title = string.Empty; Subtitle = string.Empty; @@ -118,9 +120,9 @@ public override void UpdateQuery(string query) // Exit 4: We found more than one result. Make our command take // us to the file search page, prepopulated with this search. var indexerPage = new IndexerPage(query, _searchEngine, _queryCookie, results); - Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query); + Title = Resources.Indexer_Title; Icon = Icons.FileExplorerIcon; - Subtitle = Resources.Indexer_Subtitle; + Subtitle = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query); Command = indexerPage; return; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs index ab557ba2581e..8a7fe102e3b0 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs @@ -17,10 +17,13 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos private readonly ITelemetryService _telemetryService; private CancellationTokenSource? _cancellationTokenSource; + private const string _id = "com.microsoft.cmdpal.builtin.shell.fallback"; + public FallbackExecuteItem(SettingsManager settings, Action? addToHistory, ITelemetryService telemetryService) : base( - new NoOpCommand() { Id = "com.microsoft.run.fallback" }, - ResourceLoaderInstance.GetString("shell_command_display_title")) + new NoOpCommand() { Id = "com.microsoft.shell.fallback" }, + ResourceLoaderInstance.GetString("shell_command_display_title"), + _id) { Title = string.Empty; Subtitle = ResourceLoaderInstance.GetString("generic_run_command"); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs index adaa9f7c26b8..8624953891da 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs @@ -12,8 +12,10 @@ namespace Microsoft.CmdPal.Ext.System; internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem { + private const string _id = "com.microsoft.cmdpal.builtin.system.fallback"; + public FallbackSystemCommandItem(ISettingsInterface settings) - : base(new NoOpCommand(), Resources.Microsoft_plugin_ext_fallback_display_title) + : base(new NoOpCommand(), Resources.Microsoft_plugin_ext_fallback_display_title, _id) { Title = string.Empty; Subtitle = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs index d4b99943395b..bc8b57a14bc9 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs @@ -16,9 +16,10 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem private readonly HashSet _validOptions; private ISettingsInterface _settingsManager; private DateTime? _timestamp; + private const string _id = "com.microsoft.cmdpal.builtin.timedate.fallback"; public FallbackTimeDateItem(ISettingsInterface settings, DateTime? timestamp = null) - : base(new NoOpCommand(), Resources.Microsoft_plugin_timedate_fallback_display_title) + : base(new NoOpCommand(), Resources.Microsoft_plugin_timedate_fallback_display_title, _id) { Title = string.Empty; Subtitle = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs index c942e668d33d..994db69f37d2 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs @@ -17,9 +17,10 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle); private string _title; + private const string _id = "com.microsoft.cmdpal.builtin.websearch.execute.fallback"; public FallbackExecuteSearchItem(SettingsManager settings) - : base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title) + : base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title, _id) { _executeItem = (SearchWebCommand)this.Command!; Title = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs index 9f5d9d86ca44..5d2f7eb2df1a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs @@ -18,8 +18,10 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url); private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser); + private const string _id = "com.microsoft.cmdpal.builtin.websearch.openurl.fallback"; + public FallbackOpenURLItem(SettingsManager settings) - : base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title) + : base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title, _id) { _executeItem = (OpenURLCommand)this.Command!; Title = string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/FallbackWindowsSettingsItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/FallbackWindowsSettingsItem.cs index a63a2965bd4b..b6105ca00504 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/FallbackWindowsSettingsItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Pages/FallbackWindowsSettingsItem.cs @@ -2,11 +2,8 @@ // 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.Generic; using System.Globalization; using System.Linq; -using System.Xml.Linq; -using Microsoft.CmdPal.Ext.WindowsSettings.Classes; using Microsoft.CmdPal.Ext.WindowsSettings.Commands; using Microsoft.CmdPal.Ext.WindowsSettings.Helpers; using Microsoft.CmdPal.Ext.WindowsSettings.Properties; @@ -21,8 +18,10 @@ internal sealed partial class FallbackWindowsSettingsItem : FallbackCommandItem private readonly string _title = Resources.settings_fallback_title; private readonly string _subtitle = Resources.settings_fallback_subtitle; + private const string _id = "com.microsoft.cmdpal.builtin.windows.settings.fallback"; + public FallbackWindowsSettingsItem(Classes.WindowsSettings windowsSettings) - : base(new NoOpCommand(), Resources.settings_title) + : base(new NoOpCommand(), Resources.settings_title, _id) { Icon = Icons.WindowsSettingsIcon; _windowsSettings = windowsSettings; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs index 13740eb1a173..c573ba6a7e2a 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs @@ -8,9 +8,10 @@ public partial class FallbackCommandItem : CommandItem, IFallbackCommandItem, IF { private IFallbackHandler? _fallbackHandler; - public FallbackCommandItem(ICommand command, string displayTitle) + public FallbackCommandItem(ICommand command, string displayTitle, string id) : base(command) { + Id = id; DisplayTitle = displayTitle; if (command is IFallbackHandler f) { @@ -26,5 +27,7 @@ public IFallbackHandler? FallbackHandler public virtual string DisplayTitle { get; } + public virtual string Id { get; } + public virtual void UpdateQuery(string query) => _fallbackHandler?.UpdateQuery(query); } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl index 68fd928955bc..bc554ee25620 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl @@ -361,6 +361,7 @@ namespace Microsoft.CommandPalette.Extensions interface IFallbackCommandItem requires ICommandItem { IFallbackHandler FallbackHandler{ get; }; String DisplayTitle { get; }; + String Id { get; }; }; [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]