// 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.Messages; using Microsoft.CmdPal.UI.ViewModels.Models; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.DependencyInjection; using WyHash; namespace Microsoft.CmdPal.UI.ViewModels; /// /// Abstraction of a top-level command. Currently owns just a live ICommandItem /// from an extension (or in-proc command provider), but in the future will /// also support stub top-level items. /// public partial class TopLevelCommandItemWrapper : ListItem { private readonly IServiceProvider _serviceProvider; private readonly string _commandProviderId; public ExtensionObject Model { get; } public bool IsFallback { get; private set; } private readonly string _idFromModel = string.Empty; private string _generatedId = string.Empty; public string Id => string.IsNullOrEmpty(_idFromModel) ? _generatedId : _idFromModel; private readonly TopLevelCommandWrapper _topLevelCommand; public CommandAlias? Alias { get; private set; } private HotkeySettings? _hotkey; public HotkeySettings? Hotkey { get => _hotkey; set { UpdateHotkey(); UpdateTags(); } } public CommandPaletteHost ExtensionHost { get => _topLevelCommand.ExtensionHost; } public TopLevelCommandItemWrapper( ExtensionObject commandItem, bool isFallback, CommandPaletteHost extensionHost, string commandProviderId, IServiceProvider serviceProvider) : base(new TopLevelCommandWrapper( commandItem.Unsafe?.Command ?? new NoOpCommand(), extensionHost)) { _serviceProvider = serviceProvider; _topLevelCommand = (TopLevelCommandWrapper)this.Command!; _commandProviderId = commandProviderId; IsFallback = isFallback; // TODO: In reality, we should do an async fetch when we're created // from an extension object. Probably have an // `static async Task FromExtension(ExtensionObject)` // or a // `async Task PromoteStub(ExtensionObject)` Model = commandItem; try { var model = Model.Unsafe; if (model == null) { return; } _topLevelCommand.UnsafeInitializeProperties(); _idFromModel = _topLevelCommand.Id; Title = model.Title; Subtitle = model.Subtitle; Icon = model.Icon; MoreCommands = model.MoreCommands; model.PropChanged += Model_PropChanged; _topLevelCommand.PropChanged += Model_PropChanged; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } GenerateId(); UpdateAlias(); UpdateHotkey(); UpdateTags(); } private void GenerateId() { // Use WyHash64 to generate stable ID hashes. // manually seeding with 0, so that the hash is stable across launches var result = WyHash64.ComputeHash64(_commandProviderId + Title + Subtitle, seed: 0); _generatedId = $"{_commandProviderId}{result}"; } public void UpdateAlias(CommandAlias? newAlias) { _serviceProvider.GetService()!.UpdateAlias(Id, newAlias); UpdateAlias(); UpdateTags(); } private void UpdateAlias() { // Add tags for the alias, if we have one. var aliases = _serviceProvider.GetService(); if (aliases != null) { Alias = aliases.AliasFromId(Id); } } private void UpdateHotkey() { var settings = _serviceProvider.GetService()!; var hotkey = settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault(); if (hotkey != null) { _hotkey = hotkey.Hotkey; } } private void UpdateTags() { var tags = new List(); if (Hotkey != null) { tags.Add(new Tag() { Text = Hotkey.ToString() }); } if (Alias != null) { tags.Add(new Tag() { Text = Alias.SearchPrefix }); } this.Tags = tags.ToArray(); } private void Model_PropChanged(object sender, IPropChangedEventArgs args) { try { var propertyName = args.PropertyName; var model = Model.Unsafe; if (model == null) { return; // throw? } switch (propertyName) { case nameof(_topLevelCommand.Name): case nameof(Title): this.Title = model.Title; break; case nameof(Subtitle): this.Subtitle = model.Subtitle; break; case nameof(Icon): var listIcon = model.Icon; Icon = model.Icon; break; case nameof(MoreCommands): this.MoreCommands = model.MoreCommands; break; case nameof(Command): this.Command = model.Command; break; } } catch { } } public void TryUpdateFallbackText(string newQuery) { if (!IsFallback) { return; } _ = Task.Run(() => { try { var model = 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(); } } } catch (Exception) { } }); } }