Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public async Task LoadTopLevelCommands(IServiceProvider serviceProvider)
SupportsPinning = true;

// Load pinned commands from saved settings
pinnedCommands = LoadPinnedCommands(four, providerSettings);
pinnedCommands = LoadPinnedCommands(four, settings);
}

Id = model.Id;
Expand Down Expand Up @@ -240,12 +240,6 @@ private void RecallFromCache()
}
}

private record TopLevelObjects(
ICommandItem[]? Commands,
IFallbackCommandItem[]? Fallbacks,
ICommandItem[]? PinnedCommands,
ICommandItem[]? DockBands);

private void InitializeCommands(
TopLevelObjects objects,
IServiceProvider serviceProvider,
Expand Down Expand Up @@ -275,7 +269,24 @@ private void InitializeCommands(

if (objects.PinnedCommands is not null)
{
topLevelList.AddRange(objects.PinnedCommands.Select(c => make(c, TopLevelType.Normal)));
foreach (var pinnedCommand in objects.PinnedCommands)
{
var pinnedItem = make(pinnedCommand, TopLevelType.Normal);
var alreadyExists = false;
foreach (var existingItem in topLevelList)
{
if (existingItem.Id == pinnedItem.Id)
{
alreadyExists = true;
break;
}
}

if (!alreadyExists)
{
topLevelList.Add(pinnedItem);
}
}
}

TopLevelItems = topLevelList.ToArray();
Expand Down Expand Up @@ -367,11 +378,11 @@ private void InitializeCommands(
return null;
}

private ICommandItem[] LoadPinnedCommands(ICommandProvider4 model, ProviderSettings providerSettings)
private ICommandItem[] LoadPinnedCommands(ICommandProvider4 model, SettingsModel settings)
{
var pinnedItems = new List<ICommandItem>();

foreach (var pinnedId in providerSettings.PinnedCommandIds)
foreach (var pinnedId in settings.GetPinnedCommandIds(ProviderId))
{
try
{
Expand Down Expand Up @@ -410,12 +421,9 @@ private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
public void PinCommand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);

if (!providerSettings.PinnedCommandIds.Contains(commandId))
if (settings.TryPinCommand(ProviderId, commandId))
{
providerSettings.PinnedCommandIds.Add(commandId);

// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
SettingsModel.SaveSettings(settings, false);
Expand All @@ -425,9 +433,8 @@ public void PinCommand(string commandId, IServiceProvider serviceProvider)
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);

if (providerSettings.PinnedCommandIds.Remove(commandId))
if (settings.TryUnpinCommand(ProviderId, commandId))
{
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
Expand Down Expand Up @@ -489,4 +496,10 @@ internal void PinDockBand(TopLevelViewModel bandVm)
this.DockBandItems = bands.ToArray();
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs());
}

private record TopLevelObjects(
ICommandItem[]? Commands,
IFallbackCommandItem[]? Fallbacks,
ICommandItem[]? PinnedCommands,
ICommandItem[]? DockBands);
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ public sealed partial class MainListPage : DynamicListPage,

// Stable separator instances so that the VM cache and InPlaceUpdateList
// recognise them across successive GetItems() calls
private readonly Separator _pinnedSeparator = new(Resources.home_sections_pinned_title);
private readonly Separator _resultsSeparator = new(Resources.results);
private readonly Separator _fallbacksSeparator = new(Resources.fallbacks);

private TopLevelViewModel[]? _cachedPinnedViewModels;
private TopLevelViewModel[]? _cachedRegularViewModels;
private bool _defaultViewDirty = true;

private RoScored<IListItem>[]? _filteredItems;
private RoScored<IListItem>[]? _filteredApps;

Expand Down Expand Up @@ -80,6 +85,7 @@ public MainListPage(

_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
_tlcManager.PinnedCommands.CollectionChanged += PinnedCommands_CollectionChanged;

// The all apps page will kick off a BG thread to start loading apps.
// We just want to know when it is done.
Expand Down Expand Up @@ -110,8 +116,15 @@ private void TlcManager_PropertyChanged(object? sender, System.ComponentModel.Pr
}
}

private void PinnedCommands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
_defaultViewDirty = true;
RaiseItemsChanged();
}

private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
_defaultViewDirty = true;
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
if (_includeApps != _filteredItemsIncludesApps)
{
Expand Down Expand Up @@ -172,65 +185,108 @@ public override IListItem[] GetItems()
{
lock (_tlcManager.TopLevelCommands)
{
// Either return the top-level commands (no search text), or the merged and
// filtered results.
if (string.IsNullOrWhiteSpace(SearchText))
{
var allCommands = _tlcManager.TopLevelCommands;
return string.IsNullOrWhiteSpace(SearchText) ? GetDefaultViewItems() : GetSearchViewItems();
}
}

// First pass: count eligible commands
var eligibleCount = 0;
for (var i = 0; i < allCommands.Count; i++)
{
var cmd = allCommands[i];
if (!cmd.IsFallback && !string.IsNullOrEmpty(cmd.Title))
{
eligibleCount++;
}
}
private IListItem[] GetSearchViewItems()
{
var validScoredFallbacks = _scoredFallbackItems?
.Where(s => !string.IsNullOrWhiteSpace(s.Item.Title))
.ToList();

var validFallbacks = _fallbackItems?
.Where(s => !string.IsNullOrWhiteSpace(s.Item.Title))
.ToList();

return MainListPageResultFactory.Create(
_filteredItems,
validScoredFallbacks,
_filteredApps,
validFallbacks,
_resultsSeparator,
_fallbacksSeparator,
AppResultLimit);
}

if (eligibleCount == 0)
{
return [];
}
private IListItem[] GetDefaultViewItems()
{
if (_defaultViewDirty)
{
RebuildDefaultViewCache();
}

// +1 for the separator
var result = new IListItem[eligibleCount + 1];
result[0] = _resultsSeparator;
var pinned = _cachedPinnedViewModels!;
var regular = _cachedRegularViewModels!;
var pinnedCount = pinned.Length;
var regularCount = regular.Length;

// Second pass: populate
var writeIndex = 1;
for (var i = 0; i < allCommands.Count; i++)
var sectionCount = (pinnedCount > 0 ? 1 : 0) + (regularCount > 0 ? 1 : 0);
if (sectionCount == 0)
{
return [];
}

var result = new IListItem[pinnedCount + regularCount + sectionCount];
var writeIndex = 0;
if (pinnedCount > 0)
{
result[writeIndex++] = _pinnedSeparator;
Array.Copy(pinned, 0, result, writeIndex, pinnedCount);
writeIndex += pinnedCount;
}

if (regularCount > 0)
{
result[writeIndex++] = _resultsSeparator;
Array.Copy(regular, 0, result, writeIndex, regularCount);
}

return result;
}

private void RebuildDefaultViewCache()
{
var allCommands = _tlcManager.TopLevelCommands;
var pinnedSettings = _tlcManager.PinnedCommands;

// Resolve pinned VMs in settings order
var pinned = new List<TopLevelViewModel>(pinnedSettings.Count);
for (var i = 0; i < pinnedSettings.Count; i++)
{
var s = pinnedSettings[i];
for (var j = 0; j < allCommands.Count; j++)
{
var cmd = allCommands[j];
if (IsEligibleTopLevelCommand(cmd) &&
cmd.CommandProviderId == s.ProviderId &&
cmd.Id == s.CommandId)
{
var cmd = allCommands[i];
if (!cmd.IsFallback && !string.IsNullOrEmpty(cmd.Title))
{
result[writeIndex++] = cmd;
}
pinned.Add(cmd);
break;
}

return result;
}
else
}

// Single pass for regular items
var regular = new List<TopLevelViewModel>(allCommands.Count);
for (var i = 0; i < allCommands.Count; i++)
{
var candidate = allCommands[i];
if (IsEligibleTopLevelCommand(candidate) && !_tlcManager.IsPinned(candidate.CommandProviderId, candidate.Id))
{
var validScoredFallbacks = _scoredFallbackItems?
.Where(s => !string.IsNullOrWhiteSpace(s.Item.Title))
.ToList();

var validFallbacks = _fallbackItems?
.Where(s => !string.IsNullOrWhiteSpace(s.Item.Title))
.ToList();

return MainListPageResultFactory.Create(
_filteredItems,
validScoredFallbacks,
_filteredApps,
validFallbacks,
_resultsSeparator,
_fallbacksSeparator,
AppResultLimit);
regular.Add(candidate);
}
}

_cachedPinnedViewModels = [.. pinned];
_cachedRegularViewModels = [.. regular];
_defaultViewDirty = false;
}

private static bool IsEligibleTopLevelCommand(TopLevelViewModel command)
{
return !command.IsFallback && !string.IsNullOrEmpty(command.Title);
}

private void ClearResults()
Expand Down Expand Up @@ -409,11 +465,10 @@ public override void UpdateSearchText(string oldSearch, string newSearch)
var allNewApps = AllAppsCommandProvider.Page.GetItems().Cast<AppListItem>().ToList();

// We need to remove pinned apps from allNewApps so they don't show twice.
// Pinned app command IDs are stored in ProviderSettings.PinnedCommandIds.
_settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
var pinnedCommandIds = providerSettings?.PinnedCommandIds;
// Pinned app command IDs are stored in SettingsModel.PinnedCommands.
var pinnedCommandIds = _settings.GetPinnedCommandIds(AllAppsCommandProvider.WellKnownId);

if (pinnedCommandIds is not null && pinnedCommandIds.Count > 0)
if (pinnedCommandIds.Count > 0)
{
newApps = allNewApps.Where(li => li.Command != null && !pinnedCommandIds.Contains(li.Command.Id));
}
Expand Down Expand Up @@ -643,7 +698,15 @@ private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)

public void Receive(ClearSearchMessage message) => SearchText = string.Empty;

public void Receive(UpdateFallbackItemsMessage message) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
public void Receive(UpdateFallbackItemsMessage message)
{
// MovePinnedCommand modifies SettingsModel directly without going
// through Pin/UnpinCommandItemMessage, so refresh the cached pinned
// state to pick up reorder changes.
_tlcManager.RebuildPinnedCache();
_defaultViewDirty = true;
RaiseItemsChanged();
}

private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender);

Expand All @@ -656,6 +719,7 @@ public void Dispose()

_tlcManager.PropertyChanged -= TlcManager_PropertyChanged;
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
_tlcManager.PinnedCommands.CollectionChanged -= PinnedCommands_CollectionChanged;

if (_settings is not null)
{
Expand Down
6 changes: 6 additions & 0 deletions src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ public static class Icons

public static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon

public static IconInfo MoveUpIcon => new("\uE74A"); // Up icon

public static IconInfo MoveDownIcon => new("\uE74B"); // Down icon

public static IconInfo MoveToTopIcon => new("\uE898"); // Down icon

public static IconInfo SettingsIcon => new("\uE713"); // Settings icon

public static IconInfo EditIcon => new("\uE70F"); // Edit icon
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

public record PinnedCommandSettings(string ProviderId, string CommandId);

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@
<data name="dock_settings_name" xml:space="preserve">
<value>Dock settings</value>
<comment>Command name for opening dock settings</comment>
</data>
</data>
<data name="ShowDetailsCommand" xml:space="preserve">
<value>Show details</value>
<comment>Name for the command that shows details of an item</comment>
Expand All @@ -296,4 +296,7 @@
<value>Results</value>
<comment>Section title for list of all search results that doesn't fall into any other category</comment>
</data>
<data name="home_sections_pinned_title" xml:space="preserve">
<value>Pinned</value>
</data>
</root>
Loading