// 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.Ext.Shell.Helpers; using Microsoft.CmdPal.Ext.Shell.Pages; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.Shell; internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable { private const string _id = "com.microsoft.cmdpal.builtin.shell.fallback"; private static readonly char[] _systemDirectoryRoots = ['\\', '/']; private readonly Action? _addToHistory; private readonly ITelemetryService _telemetryService; private CancellationTokenSource? _cancellationTokenSource; public FallbackExecuteItem(SettingsManager settings, Action? addToHistory, ITelemetryService telemetryService) : base( new NoOpCommand() { Id = _id }, ResourceLoaderInstance.GetString("shell_command_display_title"), _id) { Title = string.Empty; Subtitle = ResourceLoaderInstance.GetString("generic_run_command"); Icon = Icons.RunV2Icon; // Defined in Icons.cs and contains the execute command icon. _addToHistory = addToHistory; _telemetryService = telemetryService; } public override void UpdateQuery(string query) { // Cancel any ongoing query processing _cancellationTokenSource?.Cancel(); _cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = _cancellationTokenSource.Token; try { DoUpdateQuery(query, cancellationToken); } catch (Exception) { // Handle other exceptions return; } } private void DoUpdateQuery(string query, CancellationToken cancellationToken) { // Check for cancellation at the start if (cancellationToken.IsCancellationRequested) { return; } var searchText = query.Trim(); Expand(ref searchText); if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText)) { Command = null; Title = string.Empty; return; } ShellListPageHelpers.NormalizeCommandLineAndArgs(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; exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath, cancellationToken); pathIsDir = Directory.Exists(exe); } 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 if (cancellationToken.IsCancellationRequested) { return; } if (exeExists) { // TODO we need to probably get rid of the settings for this provider entirely var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, _addToHistory, telemetryService: _telemetryService); Title = exeItem.Title; Subtitle = exeItem.Subtitle; Icon = exeItem.Icon; Command = exeItem.Command; MoreCommands = exeItem.MoreCommands; } else if (pathIsDir) { var pathItem = new PathListItem(exe, query, _addToHistory, _telemetryService); Command = pathItem.Command; MoreCommands = pathItem.MoreCommands; Title = pathItem.Title; Subtitle = pathItem.Subtitle; Icon = pathItem.Icon; } else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri)) { Command = new OpenUrlWithHistoryCommand(searchText, _addToHistory, _telemetryService) { Result = CommandResult.Dismiss() }; Title = searchText; } else { Command = null; Title = string.Empty; } // Final cancellation check if (cancellationToken.IsCancellationRequested) { return; } } public void Dispose() { _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); } internal static bool SuppressFileFallbackIf(string query) { var searchText = query.Trim(); Expand(ref searchText); if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText)) { return false; } ShellHelpers.ParseExecutableAndArgs(searchText, out var exe, out var args); var exeExists = ShellListPageHelpers.FileExistInPath(exe, out var fullExePath); var pathIsDir = Directory.Exists(exe); return exeExists || pathIsDir; } private static void Expand(ref string searchText) { if (searchText.Length == 0) { return; } var singleCharQuery = searchText.Length == 1; searchText = Environment.ExpandEnvironmentVariables(searchText); if (!TryExpandHome(ref searchText)) { TryExpandRoot(ref searchText); } } private static bool TryExpandHome(ref string searchText) { if (searchText[0] == '~') { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (searchText.Length == 1) { searchText = home; } else if (_systemDirectoryRoots.Contains(searchText[1])) { searchText = Path.Combine(home, searchText[2..]); } return true; } return false; } private static bool TryExpandRoot(ref string searchText) { if (_systemDirectoryRoots.Contains(searchText[0]) && (searchText.Length == 1 || !_systemDirectoryRoots.Contains(searchText[1]))) { var root = Path.GetPathRoot(Environment.SystemDirectory); if (root != null) { searchText = searchText.Length == 1 ? root : Path.Combine(root, searchText[1..]); return true; } } return false; } }