CmdPal: Add history to the new run page (#40427)

_⚠️ targets #39955_

This adds history support to the new run page.

* It'll initialize the history with the history from the run dialog, if
there is any.
* Any new commands that are run, or files/dirs that are opened will also
get added to the history
* history will persist across reboots
This commit is contained in:
Mike Griese
2025-07-23 06:51:30 -05:00
committed by GitHub
parent b5584eee76
commit 3b3df5b74f
24 changed files with 483 additions and 43 deletions

View File

@@ -282,3 +282,9 @@ xef
xes xes
PACKAGEVERSIONNUMBER PACKAGEVERSIONNUMBER
APPXMANIFESTVERSION APPXMANIFESTVERSION
# MRU lists
CACHEWRITE
MRUCMPPROC
MRUINFO
REGSTR

View File

@@ -0,0 +1,27 @@
// 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.Collections.Generic;
namespace Microsoft.CmdPal.Common.Services;
public interface IRunHistoryService
{
/// <summary>
/// Gets the run history.
/// </summary>
/// <returns>A list of run history items.</returns>
IReadOnlyList<string> GetRunHistory();
/// <summary>
/// Clears the run history.
/// </summary>
void ClearRunHistory();
/// <summary>
/// Adds a run history item.
/// </summary>
/// <param name="item">The run history item to add.</param>
void AddRunHistoryItem(string item);
}

View File

@@ -21,8 +21,12 @@ public partial class AppStateModel : ObservableObject
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// STATE HERE // STATE HERE
// Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)!
// Make sure that any new types you add are added to JsonSerializationContext!
public RecentCommandsManager RecentCommands { get; set; } = new(); public RecentCommandsManager RecentCommands { get; set; } = new();
public List<string> RunHistory { get; set; } = [];
// END SETTINGS // END SETTINGS
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@@ -86,7 +90,7 @@ public partial class AppStateModel : ObservableObject
{ {
foreach (var item in newSettings) foreach (var item in newSettings)
{ {
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null; savedSettings[item.Key] = item.Value?.DeepClone();
} }
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options); var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
@@ -121,20 +125,4 @@ public partial class AppStateModel : ObservableObject
// now, the settings is just next to the exe // now, the settings is just next to the exe
return Path.Combine(directory, "state.json"); return Path.Combine(directory, "state.json");
} }
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
// private static readonly JsonSerializerOptions _serializerOptions = new()
// {
// WriteIndented = true,
// Converters = { new JsonStringEnumConverter() },
// };
// private static readonly JsonSerializerOptions _deserializerOptions = new()
// {
// PropertyNameCaseInsensitive = true,
// IncludeFields = true,
// AllowTrailingCommas = true,
// PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
// ReadCommentHandling = JsonCommentHandling.Skip,
// };
} }

View File

@@ -101,6 +101,7 @@ public partial class App : Application
var files = new IndexerCommandsProvider(); var files = new IndexerCommandsProvider();
files.SuppressFallbackWhen(ShellCommandsProvider.SuppressFileFallbackIf); files.SuppressFallbackWhen(ShellCommandsProvider.SuppressFileFallbackIf);
services.AddSingleton<ICommandProvider>(allApps); services.AddSingleton<ICommandProvider>(allApps);
services.AddSingleton<ICommandProvider, ShellCommandsProvider>(); services.AddSingleton<ICommandProvider, ShellCommandsProvider>();
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>(); services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
services.AddSingleton<ICommandProvider>(files); services.AddSingleton<ICommandProvider>(files);
@@ -146,6 +147,7 @@ public partial class App : Application
services.AddSingleton(state); services.AddSingleton(state);
services.AddSingleton<IExtensionService, ExtensionService>(); services.AddSingleton<IExtensionService, ExtensionService>();
services.AddSingleton<TrayIconService>(); services.AddSingleton<TrayIconService>();
services.AddSingleton<IRunHistoryService, RunHistoryService>();
services.AddSingleton<IRootPageService, PowerToysRootPageService>(); services.AddSingleton<IRootPageService, PowerToysRootPageService>();
services.AddSingleton<IAppHostService, PowerToysAppHostService>(); services.AddSingleton<IAppHostService, PowerToysAppHostService>();

View File

@@ -336,8 +336,6 @@ public sealed partial class SearchBar : UserControl,
// ... Move the cursor to the end of the input // ... Move the cursor to the end of the input
FilterBox.Select(FilterBox.Text.Length, 0); FilterBox.Select(FilterBox.Text.Length, 0);
} }
// TODO! deal with suggestion
} }
else if (property == nameof(ListViewModel.InitialSearchText)) else if (property == nameof(ListViewModel.InitialSearchText))
{ {

View File

@@ -0,0 +1,50 @@
// 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 Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels;
namespace Microsoft.CmdPal.UI;
internal sealed class RunHistoryService : IRunHistoryService
{
private readonly AppStateModel _appStateModel;
public RunHistoryService(AppStateModel appStateModel)
{
_appStateModel = appStateModel;
}
public IReadOnlyList<string> GetRunHistory()
{
if (_appStateModel.RunHistory.Count == 0)
{
var history = Microsoft.Terminal.UI.RunHistory.CreateRunHistory();
_appStateModel.RunHistory.AddRange(history);
}
return _appStateModel.RunHistory;
}
public void ClearRunHistory()
{
_appStateModel.RunHistory.Clear();
}
public void AddRunHistoryItem(string item)
{
// insert at the beginning of the list
if (string.IsNullOrWhiteSpace(item))
{
return; // Do not add empty or whitespace items
}
_appStateModel.RunHistory.Remove(item);
// Add the item to the front of the history
_appStateModel.RunHistory.Insert(0, item);
AppStateModel.SaveState(_appStateModel);
}
}

View File

@@ -383,4 +383,5 @@ namespace winrt::Microsoft::Terminal::UI::implementation
icon.Height(targetSize); icon.Height(targetSize);
return icon; return icon;
} }
} }

View File

@@ -13,6 +13,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const int targetSize=24); static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const int targetSize=24);
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath); static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath);
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize); static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
}; };
} }

View File

@@ -153,6 +153,9 @@
<ClInclude Include="IconPathConverter.h"> <ClInclude Include="IconPathConverter.h">
<DependentUpon>IconPathConverter.idl</DependentUpon> <DependentUpon>IconPathConverter.idl</DependentUpon>
</ClInclude> </ClInclude>
<ClInclude Include="RunHistory.h">
<DependentUpon>RunHistory.idl</DependentUpon>
</ClInclude>
<ClInclude Include="ResourceString.h"> <ClInclude Include="ResourceString.h">
<DependentUpon>ResourceString.idl</DependentUpon> <DependentUpon>ResourceString.idl</DependentUpon>
</ClInclude> </ClInclude>
@@ -168,6 +171,9 @@
<ClCompile Include="IconPathConverter.cpp"> <ClCompile Include="IconPathConverter.cpp">
<DependentUpon>IconPathConverter.idl</DependentUpon> <DependentUpon>IconPathConverter.idl</DependentUpon>
</ClCompile> </ClCompile>
<ClCompile Include="RunHistory.cpp">
<DependentUpon>RunHistory.idl</DependentUpon>
</ClCompile>
<ClCompile Include="ResourceString.cpp"> <ClCompile Include="ResourceString.cpp">
<DependentUpon>ResourceString.idl</DependentUpon> <DependentUpon>ResourceString.idl</DependentUpon>
</ClCompile> </ClCompile>
@@ -176,6 +182,7 @@
<ItemGroup> <ItemGroup>
<Midl Include="Converters.idl" /> <Midl Include="Converters.idl" />
<Midl Include="IconPathConverter.idl" /> <Midl Include="IconPathConverter.idl" />
<Midl Include="RunHistory.idl" />
<Midl Include="IDirectKeyListener.idl" /> <Midl Include="IDirectKeyListener.idl" />
<Midl Include="ResourceString.idl" /> <Midl Include="ResourceString.idl" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,87 @@
#include "pch.h"
#include "RunHistory.h"
#include "RunHistory.g.cpp"
using namespace winrt::Windows;
namespace winrt::Microsoft::Terminal::UI::implementation
{
// Run history
// Largely copied from the Run work circa 2022.
winrt::Windows::Foundation::Collections::IVector<hstring> RunHistory::CreateRunHistory()
{
// Load MRU history
std::vector<hstring> history;
wil::unique_hmodule _comctl;
HANDLE(WINAPI* _createMRUList)(MRUINFO* lpmi);
int(WINAPI* _enumMRUList)(HANDLE hMRU,int nItem,void* lpData,UINT uLen);
void(WINAPI *_freeMRUList)(HANDLE hMRU);
int(WINAPI *_addMRUString)(HANDLE hMRU, LPCWSTR szString);
// Lazy load comctl32.dll
// Theoretically, we could cache this into a magic static, but we shouldn't need to actually do this more than once in CmdPal
_comctl.reset(LoadLibraryExW(L"comctl32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32));
_createMRUList = reinterpret_cast<decltype(_createMRUList)>(GetProcAddress(_comctl.get(), "CreateMRUListW"));
FAIL_FAST_LAST_ERROR_IF(!_createMRUList);
_enumMRUList = reinterpret_cast<decltype(_enumMRUList)>(GetProcAddress(_comctl.get(), "EnumMRUListW"));
FAIL_FAST_LAST_ERROR_IF(!_enumMRUList);
_freeMRUList = reinterpret_cast<decltype(_freeMRUList)>(GetProcAddress(_comctl.get(), "FreeMRUList"));
FAIL_FAST_LAST_ERROR_IF(!_freeMRUList);
_addMRUString = reinterpret_cast<decltype(_addMRUString)>(GetProcAddress(_comctl.get(), "AddMRUStringW"));
FAIL_FAST_LAST_ERROR_IF(!_addMRUString);
static const WCHAR c_szRunMRU[] = REGSTR_PATH_EXPLORER L"\\RunMRU";
MRUINFO mi = {
sizeof(mi),
26,
MRU_CACHEWRITE,
HKEY_CURRENT_USER,
c_szRunMRU,
NULL // NOTE: use default string compare
// since this is a GLOBAL MRU
};
if (const auto hMruList = _createMRUList(&mi))
{
auto freeMRUList = wil::scope_exit([=]() {
_freeMRUList(hMruList);
});
for (int nMax = _enumMRUList(hMruList, -1, NULL, 0), i = 0; i < nMax; ++i)
{
WCHAR szCommand[MAX_PATH + 2];
const auto length = _enumMRUList(hMruList, i, szCommand, ARRAYSIZE(szCommand));
if (length > 1)
{
// clip off the null-terminator
std::wstring_view text{ szCommand, wil::safe_cast<size_t>(length - 1) };
//#pragma disable warning(C26493)
#pragma warning( push )
#pragma warning( disable : 26493 )
if (text.back() == L'\\')
{
// old MRU format has a slash at the end with the show cmd
text = { szCommand, wil::safe_cast<size_t>(length - 2) };
#pragma warning( pop )
if (text.empty())
{
continue;
}
}
history.emplace_back(text);
}
}
}
// Update dropdown & initial value
return winrt::single_threaded_observable_vector<winrt::hstring>(std::move(history));
}
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "RunHistory.g.h"
#include "types.h"
namespace winrt::Microsoft::Terminal::UI::implementation
{
struct RunHistory
{
RunHistory() = default;
static winrt::Windows::Foundation::Collections::IVector<hstring> CreateRunHistory();
private:
winrt::Windows::Foundation::Collections::IVector<hstring> _mruHistory;
};
}
namespace winrt::Microsoft::Terminal::UI::factory_implementation
{
BASIC_FACTORY(RunHistory);
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.UI
{
static runtimeclass RunHistory
{
static Windows.Foundation.Collections.IVector<String> CreateRunHistory();
};
}

View File

@@ -64,6 +64,8 @@
// WIL // WIL
#include <wil/com.h> #include <wil/com.h>
#include <wil/resource.h>
#include <wil/safecast.h>
#include <wil/stl.h> #include <wil/stl.h>
#include <wil/filesystem.h> #include <wil/filesystem.h>
// Due to the use of RESOURCE_SUPPRESS_STL in result.h, we need to include resource.h first, which happens // Due to the use of RESOURCE_SUPPRESS_STL in result.h, we need to include resource.h first, which happens
@@ -90,6 +92,7 @@
#include <winrt/Windows.ApplicationModel.Resources.h> #include <winrt/Windows.ApplicationModel.Resources.h>
#include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Graphics.Imaging.h> #include <winrt/Windows.Graphics.Imaging.h>
#include <Windows.Graphics.Imaging.Interop.h> #include <Windows.Graphics.Imaging.Interop.h>

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#define MRU_CACHEWRITE 0x0002
#define REGSTR_PATH_EXPLORER TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer")
// https://learn.microsoft.com/en-us/windows/win32/shell/mrucmpproc
typedef int(CALLBACK* MRUCMPPROC)(
LPCTSTR pString1,
LPCTSTR pString2);
// https://learn.microsoft.com/en-us/windows/win32/shell/mruinfo
struct MRUINFO
{
DWORD cbSize;
UINT uMax;
UINT fFlags;
HKEY hKey;
LPCTSTR lpszSubKey;
MRUCMPPROC lpfnCompare;
};

View File

@@ -15,10 +15,11 @@ namespace Microsoft.CmdPal.Ext.Shell;
internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable
{ {
private readonly Action<string>? _addToHistory;
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
private Task? _currentUpdateTask; private Task? _currentUpdateTask;
public FallbackExecuteItem(SettingsManager settings) public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory)
: base( : base(
new NoOpCommand() { Id = "com.microsoft.run.fallback" }, new NoOpCommand() { Id = "com.microsoft.run.fallback" },
Resources.shell_command_display_title) Resources.shell_command_display_title)
@@ -26,6 +27,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
Title = string.Empty; Title = string.Empty;
Subtitle = Properties.Resources.generic_run_command; Subtitle = Properties.Resources.generic_run_command;
Icon = Icons.RunV2Icon; // Defined in Icons.cs and contains the execute command icon. Icon = Icons.RunV2Icon; // Defined in Icons.cs and contains the execute command icon.
_addToHistory = addToHistory;
} }
public override void UpdateQuery(string query) public override void UpdateQuery(string query)
@@ -142,7 +144,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
if (exeExists) if (exeExists)
{ {
// TODO we need to probably get rid of the settings for this provider entirely // TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath); var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, _addToHistory);
Title = exeItem.Title; Title = exeItem.Title;
Subtitle = exeItem.Subtitle; Subtitle = exeItem.Subtitle;
Icon = exeItem.Icon; Icon = exeItem.Icon;
@@ -151,7 +153,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
} }
else if (pathIsDir) else if (pathIsDir)
{ {
var pathItem = new PathListItem(exe, query); var pathItem = new PathListItem(exe, query, _addToHistory);
Title = pathItem.Title; Title = pathItem.Title;
Subtitle = pathItem.Subtitle; Subtitle = pathItem.Subtitle;
Icon = pathItem.Icon; Icon = pathItem.Icon;
@@ -160,7 +162,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
} }
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri)) else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{ {
Command = new OpenUrlCommand(searchText) { Result = CommandResult.Dismiss() }; Command = new OpenUrlWithHistoryCommand(searchText, _addToHistory) { Result = CommandResult.Dismiss() };
Title = searchText; Title = searchText;
} }
else else

View File

@@ -7,7 +7,9 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Shell.Commands; using Microsoft.CmdPal.Ext.Shell.Commands;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell.Helpers; namespace Microsoft.CmdPal.Ext.Shell.Helpers;
@@ -94,4 +96,80 @@ public class ShellListPageHelpers
} }
} }
} }
internal static ListItem? ListItemForCommandString(string query, Action<string>? addToHistory)
{
var li = new ListItem();
var searchText = query.Trim();
var expanded = Environment.ExpandEnvironmentVariables(searchText);
searchText = expanded;
if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText))
{
return null;
}
ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args);
var exeExists = false;
var pathIsDir = false;
var fullExePath = string.Empty;
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
// Use Task.Run with timeout - this will actually timeout even if the sync operations don't respond to cancellation
var pathResolutionTask = Task.Run(
() =>
{
// Don't check cancellation token here - let the Task timeout handle it
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath);
pathIsDir = Directory.Exists(expanded);
},
CancellationToken.None); // Use None here since we're handling timeout differently
// Wait for either completion or timeout
pathResolutionTask.Wait(cts.Token);
}
catch (OperationCanceledException)
{
}
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, addToHistory);
li.Command = exeItem.Command;
li.Title = exeItem.Title;
li.Subtitle = exeItem.Subtitle;
li.Icon = exeItem.Icon;
li.MoreCommands = exeItem.MoreCommands;
}
else if (pathIsDir)
{
var pathItem = new PathListItem(exe, query, addToHistory);
li.Command = pathItem.Command;
li.Title = pathItem.Title;
li.Subtitle = pathItem.Subtitle;
li.Icon = pathItem.Icon;
li.MoreCommands = pathItem.MoreCommands;
}
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
li.Command = new OpenUrlWithHistoryCommand(searchText) { Result = CommandResult.Dismiss() };
li.Title = searchText;
}
else
{
return null;
}
if (li != null)
{
li.TextToSuggest = searchText;
}
return li;
}
} }

View File

@@ -14,6 +14,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" /> <ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.CommandLine" /> <PackageReference Include="System.CommandLine" />

View File

@@ -0,0 +1,30 @@
// 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.IO;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell;
internal sealed partial class OpenUrlWithHistoryCommand : OpenUrlCommand
{
private readonly Action<string>? _addToHistory;
private readonly string _url;
public OpenUrlWithHistoryCommand(string url, Action<string>? addToHistory = null)
: base(url)
{
_addToHistory = addToHistory;
_url = url;
}
public override CommandResult Invoke()
{
_addToHistory?.Invoke(_url);
var result = base.Invoke();
return result;
}
}

View File

@@ -13,6 +13,7 @@ namespace Microsoft.CmdPal.Ext.Shell.Pages;
internal sealed partial class RunExeItem : ListItem internal sealed partial class RunExeItem : ListItem
{ {
private readonly Lazy<IconInfo> _icon; private readonly Lazy<IconInfo> _icon;
private readonly Action<string>? _addToHistory;
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; } public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
@@ -22,7 +23,9 @@ internal sealed partial class RunExeItem : ListItem
private string _args = string.Empty; private string _args = string.Empty;
public RunExeItem(string exe, string args, string fullExePath) private string FullString => string.IsNullOrEmpty(_args) ? Exe : $"{Exe} {_args}";
public RunExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
{ {
FullExePath = fullExePath; FullExePath = fullExePath;
Exe = exe; Exe = exe;
@@ -41,6 +44,8 @@ internal sealed partial class RunExeItem : ListItem
return t.Result; return t.Result;
}); });
_addToHistory = addToHistory;
UpdateArgs(args); UpdateArgs(args);
MoreCommands = [ MoreCommands = [
@@ -89,16 +94,22 @@ internal sealed partial class RunExeItem : ListItem
public void Run() public void Run()
{ {
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args); ShellHelpers.OpenInShell(FullExePath, _args);
} }
public void RunAsAdmin() public void RunAsAdmin()
{ {
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.Administrator); ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.Administrator);
} }
public void RunAsOther() public void RunAsOther()
{ {
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.OtherUser); ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.OtherUser);
} }
} }

View File

@@ -8,6 +8,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers; using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Properties; using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
@@ -20,7 +21,11 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
private readonly ShellListPageHelpers _helper; private readonly ShellListPageHelpers _helper;
private readonly List<ListItem> _topLevelItems = []; private readonly List<ListItem> _topLevelItems = [];
private readonly List<ListItem> _historyItems = []; private readonly Dictionary<string, ListItem> _historyItems = [];
private readonly List<ListItem> _currentHistoryItems = [];
private readonly IRunHistoryService _historyService;
private RunExeItem? _exeItem; private RunExeItem? _exeItem;
private List<ListItem> _pathItems = []; private List<ListItem> _pathItems = [];
private ListItem? _uriItem; private ListItem? _uriItem;
@@ -28,13 +33,16 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
private Task? _currentSearchTask; private Task? _currentSearchTask;
public ShellListPage(SettingsManager settingsManager, bool addBuiltins = false) private bool _loadedInitialHistory;
public ShellListPage(SettingsManager settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
{ {
Icon = Icons.RunV2Icon; Icon = Icons.RunV2Icon;
Id = "com.microsoft.cmdpal.shell"; Id = "com.microsoft.cmdpal.shell";
Name = Resources.cmd_plugin_name; Name = Resources.cmd_plugin_name;
PlaceholderText = Resources.list_placeholder_text; PlaceholderText = Resources.list_placeholder_text;
_helper = new(settingsManager); _helper = new(settingsManager);
_historyService = runHistoryService;
EmptyContent = new CommandItem() EmptyContent = new CommandItem()
{ {
@@ -68,8 +76,6 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
_cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _cancellationTokenSource.Token; var cancellationToken = _cancellationTokenSource.Token;
IsLoading = true;
try try
{ {
// Save the latest search task // Save the latest search task
@@ -139,6 +145,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
_pathItems.Clear(); _pathItems.Clear();
_exeItem = null; _exeItem = null;
_uriItem = null; _uriItem = null;
_currentHistoryItems.Clear();
_currentHistoryItems.AddRange(_historyItems.Values);
return; return;
} }
@@ -206,6 +216,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
&& (!exeExists || pathIsDir) && (!exeExists || pathIsDir)
&& couldResolvePath) && couldResolvePath)
{ {
IsLoading = true;
await CreatePathItemsAsync(expanded, searchText, cancellationToken); await CreatePathItemsAsync(expanded, searchText, cancellationToken);
} }
@@ -231,18 +242,53 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
_uriItem = null; _uriItem = null;
} }
var histItemsNotInSearch =
_historyItems
.Where(kv => !kv.Key.Equals(newSearch, StringComparison.OrdinalIgnoreCase));
if (_exeItem != null)
{
// If we have an exe item, we want to remove it from the history items
histItemsNotInSearch = histItemsNotInSearch
.Where(kv => !kv.Value.Title.Equals(_exeItem.Title, StringComparison.OrdinalIgnoreCase));
}
if (_uriItem != null)
{
// If we have an uri item, we want to remove it from the history items
histItemsNotInSearch = histItemsNotInSearch
.Where(kv => !kv.Value.Title.Equals(_uriItem.Title, StringComparison.OrdinalIgnoreCase));
}
// Filter the history items based on the search text
var filterHistory = (string query, KeyValuePair<string, ListItem> pair) =>
{
// Fuzzy search on the key (command string)
var score = StringMatcher.FuzzySearch(query, pair.Key).Score;
return score;
};
var filteredHistory =
ListHelpers.FilterList<KeyValuePair<string, ListItem>>(
histItemsNotInSearch,
searchText,
filterHistory)
.Select(p => p.Value);
_currentHistoryItems.Clear();
_currentHistoryItems.AddRange(filteredHistory);
// Final cancellation check // Final cancellation check
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
} }
private static ListItem PathToListItem(string path, string originalPath, string args = "") private static ListItem PathToListItem(string path, string originalPath, string args = "", Action<string>? addToHistory = null)
{ {
var pathItem = new PathListItem(path, originalPath); var pathItem = new PathListItem(path, originalPath, addToHistory);
// Is this path an executable? If so, then make a RunExeItem // Is this path an executable? If so, then make a RunExeItem
if (IsExecutable(path)) if (IsExecutable(path))
{ {
var exeItem = new RunExeItem(Path.GetFileName(path), args, path); var exeItem = new RunExeItem(Path.GetFileName(path), args, path, addToHistory);
exeItem.MoreCommands = [ exeItem.MoreCommands = [
.. exeItem.MoreCommands, .. exeItem.MoreCommands,
@@ -255,24 +301,30 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
public override IListItem[] GetItems() public override IListItem[] GetItems()
{ {
if (!_loadedInitialHistory)
{
LoadInitialHistory();
}
var filteredTopLevel = ListHelpers.FilterList(_topLevelItems, SearchText); var filteredTopLevel = ListHelpers.FilterList(_topLevelItems, SearchText);
List<ListItem> uriItems = _uriItem != null ? [_uriItem] : []; List<ListItem> uriItems = _uriItem != null ? [_uriItem] : [];
List<ListItem> exeItems = _exeItem != null ? [_exeItem] : []; List<ListItem> exeItems = _exeItem != null ? [_exeItem] : [];
return return
exeItems exeItems
.Concat(filteredTopLevel) .Concat(filteredTopLevel)
.Concat(_historyItems) .Concat(_currentHistoryItems)
.Concat(_pathItems) .Concat(_pathItems)
.Concat(uriItems) .Concat(uriItems)
.ToArray(); .ToArray();
} }
internal static RunExeItem CreateExeItem(string exe, string args, string fullExePath) internal static RunExeItem CreateExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
{ {
// PathToListItem will return a RunExeItem if it can find a executable. // PathToListItem will return a RunExeItem if it can find a executable.
// It will ALSO add the file search commands to the RunExeItem. // It will ALSO add the file search commands to the RunExeItem.
return PathToListItem(fullExePath, exe, args) as RunExeItem ?? return PathToListItem(fullExePath, exe, args, addToHistory) as RunExeItem ??
new RunExeItem(exe, args, fullExePath); new RunExeItem(exe, args, fullExePath, addToHistory);
} }
private void CreateAndAddExeItems(string exe, string args, string fullExePath) private void CreateAndAddExeItems(string exe, string args, string fullExePath)
@@ -284,7 +336,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
} }
else else
{ {
_exeItem = CreateExeItem(exe, args, fullExePath); _exeItem = CreateExeItem(exe, args, fullExePath, AddToHistory);
} }
} }
@@ -442,6 +494,40 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
}; };
} }
private void LoadInitialHistory()
{
var hist = _historyService.GetRunHistory();
var histItems = hist
.Select(h => (h, ShellListPageHelpers.ListItemForCommandString(h, AddToHistory)))
.Where(tuple => tuple.Item2 != null)
.Select(tuple => (tuple.h, tuple.Item2!))
.ToList();
_historyItems.Clear();
// Add all the history items to the _historyItems dictionary
foreach (var (h, item) in histItems)
{
_historyItems[h] = item;
}
_currentHistoryItems.Clear();
_currentHistoryItems.AddRange(histItems.Select(tuple => tuple.Item2));
_loadedInitialHistory = true;
}
internal void AddToHistory(string commandString)
{
if (string.IsNullOrWhiteSpace(commandString))
{
return; // Do not add empty or whitespace items
}
_historyService.AddRunHistoryItem(commandString);
LoadInitialHistory();
DoUpdateSearchText(SearchText);
}
public void Dispose() public void Dispose()
{ {
_cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Cancel();

View File

@@ -16,8 +16,8 @@ internal sealed partial class PathListItem : ListItem
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; } public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
public PathListItem(string path, string originalDir) public PathListItem(string path, string originalDir, Action<string>? addToHistory)
: base(new OpenUrlCommand(path)) : base(new OpenUrlWithHistoryCommand(path, addToHistory))
{ {
var fileName = Path.GetFileName(path); var fileName = Path.GetFileName(path);
_isDirectory = Directory.Exists(path); _isDirectory = Directory.Exists(path);
@@ -50,6 +50,7 @@ internal sealed partial class PathListItem : ListItem
new CommandContextItem(new CopyTextCommand(path) { Name = Properties.Resources.copy_path_command_name }) { } new CommandContextItem(new CopyTextCommand(path) { Name = Properties.Resources.copy_path_command_name }) { }
]; ];
// TODO: Follow-up during 0.4. Add the indexer commands here.
// MoreCommands = [ // MoreCommands = [
// new CommandContextItem(new OpenWithCommand(indexerItem)), // new CommandContextItem(new OpenWithCommand(indexerItem)),
// new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }), // new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers; using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Pages; using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CmdPal.Ext.Shell.Properties; using Microsoft.CmdPal.Ext.Shell.Properties;
@@ -15,18 +16,24 @@ public partial class ShellCommandsProvider : CommandProvider
private readonly CommandItem _shellPageItem; private readonly CommandItem _shellPageItem;
private readonly SettingsManager _settingsManager = new(); private readonly SettingsManager _settingsManager = new();
private readonly ShellListPage _shellListPage;
private readonly FallbackCommandItem _fallbackItem; private readonly FallbackCommandItem _fallbackItem;
private readonly IRunHistoryService _historyService;
public ShellCommandsProvider() public ShellCommandsProvider(IRunHistoryService runHistoryService)
{ {
_historyService = runHistoryService;
Id = "Run"; Id = "Run";
DisplayName = Resources.cmd_plugin_name; DisplayName = Resources.cmd_plugin_name;
Icon = Icons.RunV2Icon; Icon = Icons.RunV2Icon;
Settings = _settingsManager.Settings; Settings = _settingsManager.Settings;
_fallbackItem = new FallbackExecuteItem(_settingsManager); _shellListPage = new ShellListPage(_settingsManager, _historyService);
_shellPageItem = new CommandItem(new ShellListPage(_settingsManager)) _fallbackItem = new FallbackExecuteItem(_settingsManager, _shellListPage.AddToHistory);
_shellPageItem = new CommandItem(_shellListPage)
{ {
Icon = Icons.RunV2Icon, Icon = Icons.RunV2Icon,
Title = Resources.shell_command_name, Title = Resources.shell_command_name,

View File

@@ -43,7 +43,6 @@ public partial class ListHelpers
} }
public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction) public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
where T : class
{ {
var scores = items var scores = items
.Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) }) .Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) })

View File

@@ -4,7 +4,7 @@
namespace Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CommandPalette.Extensions.Toolkit;
public sealed partial class OpenUrlCommand : InvokableCommand public partial class OpenUrlCommand : InvokableCommand
{ {
private readonly string _target; private readonly string _target;