CmdPal: entirely redo the Run page (#39955)

This entirely rewrites the shell page. It feels a lot more like the old
run dialog now.

* It's got icons for files & exes
* it can handle network paths
* it can handle `commands /with args...`
* it'll suggest files in that path as you type
* it handles `%environmentVariables%`
* it handles `"Paths with\spaces in them"`
* it shows you the path as a suggestion, in the text box, as you move
the selection


References:
Closes #39044
Closes #39419
Closes #38298
Closes #40311


### Remaining todo's
* [x] Remove the `GenerateAppxManifest` change, and file something to
fix that. We are still generating msix's on every build, wtf
* [x] Clean-up code
* [x] Double-check loc
* [x] Remove a bunch of debug printing that we don't need anymore
* [ ] File a separate PR for moving the file (indexer) commands into a
common project, and re-use those here
* [x] Add history support again! I totally tore that out
  * did that in #40427 
* [x] make `shell:` paths and weird URI's just work. Good test is
`x-cmdpal://settings`

### further optimizations that probably aren't blocking
* [x] Our fast up-to-date is clearly broken, but I think that's been
broken since early 0.91
* [x] If the exe doesn't change, we don't need to create a new ListItem
for it. We can just re-use the current one, and just change the args
* [ ] if the directory hasn't changed, but we typed more chars (e.g.
`c:\windows\s` -> `c:\windows\sys`), we should cache the ListItem's from
the first query, and re-use them if possible.
This commit is contained in:
Mike Griese
2025-07-22 14:47:31 -05:00
committed by GitHub
parent 6ff59488eb
commit 6623d0a2ee
24 changed files with 1091 additions and 148 deletions

View File

@@ -2,39 +2,197 @@
// 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.Ext.Shell.Commands;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell;
internal sealed partial class FallbackExecuteItem : FallbackCommandItem
internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable
{
private readonly ExecuteItem _executeItem;
private readonly SettingsManager _settings;
private CancellationTokenSource? _cancellationTokenSource;
private Task? _currentUpdateTask;
public FallbackExecuteItem(SettingsManager settings)
: base(
new ExecuteItem(string.Empty, settings) { Id = "com.microsoft.run.fallback" },
new NoOpCommand() { Id = "com.microsoft.run.fallback" },
Resources.shell_command_display_title)
{
_settings = settings;
_executeItem = (ExecuteItem)this.Command!;
Title = string.Empty;
_executeItem.Name = string.Empty;
Subtitle = Properties.Resources.generic_run_command;
Icon = Icons.RunV2Icon; // Defined in Icons.cs and contains the execute command icon.
}
public override void UpdateQuery(string query)
{
_executeItem.Cmd = query;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.generic_run_command;
Title = query;
MoreCommands = [
new CommandContextItem(new ExecuteItem(query, _settings, RunAsType.Administrator)),
new CommandContextItem(new ExecuteItem(query, _settings, RunAsType.OtherUser)),
];
// Cancel any ongoing query processing
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _cancellationTokenSource.Token;
try
{
// Save the latest update task
_currentUpdateTask = DoUpdateQueryAsync(query, cancellationToken);
}
catch (OperationCanceledException)
{
// DO NOTHING HERE
return;
}
catch (Exception)
{
// Handle other exceptions
return;
}
// Await the task to ensure only the latest one gets processed
_ = ProcessUpdateResultsAsync(_currentUpdateTask);
}
private async Task ProcessUpdateResultsAsync(Task updateTask)
{
try
{
await updateTask;
}
catch (OperationCanceledException)
{
// Handle cancellation gracefully
}
catch (Exception)
{
// Handle other exceptions
}
}
private async Task DoUpdateQueryAsync(string query, CancellationToken cancellationToken)
{
// Check for cancellation at the start
cancellationToken.ThrowIfCancellationRequested();
var searchText = query.Trim();
var expanded = Environment.ExpandEnvironmentVariables(searchText);
searchText = expanded;
if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText))
{
Command = null;
Title = string.Empty;
return;
}
ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args);
// Check for cancellation before file system operations
cancellationToken.ThrowIfCancellationRequested();
var exeExists = false;
var fullExePath = string.Empty;
var pathIsDir = false;
try
{
// Create a timeout for file system operations (200ms)
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var timeoutToken = combinedCts.Token;
// Use Task.Run with timeout for file system operations
var fileSystemTask = Task.Run(
() =>
{
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath);
pathIsDir = Directory.Exists(exe);
},
CancellationToken.None);
// Wait for either completion or timeout
await fileSystemTask.WaitAsync(timeoutToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Main cancellation token was cancelled, re-throw
throw;
}
catch (TimeoutException)
{
// Timeout occurred - use defaults
return;
}
catch (OperationCanceledException)
{
// Timeout occurred (from WaitAsync) - use defaults
return;
}
catch (Exception)
{
// Handle any other exceptions that might bubble up
return;
}
// Check for cancellation before updating UI properties
cancellationToken.ThrowIfCancellationRequested();
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath);
Title = exeItem.Title;
Subtitle = exeItem.Subtitle;
Icon = exeItem.Icon;
Command = exeItem.Command;
MoreCommands = exeItem.MoreCommands;
}
else if (pathIsDir)
{
var pathItem = new PathListItem(exe, query);
Title = pathItem.Title;
Subtitle = pathItem.Subtitle;
Icon = pathItem.Icon;
Command = pathItem.Command;
MoreCommands = pathItem.MoreCommands;
}
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
Command = new OpenUrlCommand(searchText) { Result = CommandResult.Dismiss() };
Title = searchText;
}
else
{
Command = null;
Title = string.Empty;
}
// Final cancellation check
cancellationToken.ThrowIfCancellationRequested();
}
public void Dispose()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
}
internal static bool SuppressFileFallbackIf(string query)
{
var searchText = query.Trim();
var expanded = Environment.ExpandEnvironmentVariables(searchText);
searchText = expanded;
if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText))
{
return false;
}
ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args);
var exeExists = ShellListPageHelpers.FileExistInPath(exe, out var fullExePath);
var pathIsDir = Directory.Exists(exe);
return exeExists || pathIsDir;
}
}