FallbackExecuteItem.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using Microsoft.CmdPal.Core.Common.Services; 6 using Microsoft.CmdPal.Ext.Shell.Helpers; 7 using Microsoft.CmdPal.Ext.Shell.Pages; 8 using Microsoft.CommandPalette.Extensions.Toolkit; 9 10 namespace Microsoft.CmdPal.Ext.Shell; 11 12 internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable 13 { 14 private const string _id = "com.microsoft.cmdpal.builtin.shell.fallback"; 15 private static readonly char[] _systemDirectoryRoots = ['\\', '/']; 16 17 private readonly Action<string>? _addToHistory; 18 private readonly ITelemetryService _telemetryService; 19 private CancellationTokenSource? _cancellationTokenSource; 20 21 public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory, ITelemetryService telemetryService) 22 : base( 23 new NoOpCommand() { Id = _id }, 24 ResourceLoaderInstance.GetString("shell_command_display_title"), 25 _id) 26 { 27 Title = string.Empty; 28 Subtitle = ResourceLoaderInstance.GetString("generic_run_command"); 29 Icon = Icons.RunV2Icon; // Defined in Icons.cs and contains the execute command icon. 30 _addToHistory = addToHistory; 31 _telemetryService = telemetryService; 32 } 33 34 public override void UpdateQuery(string query) 35 { 36 // Cancel any ongoing query processing 37 _cancellationTokenSource?.Cancel(); 38 39 _cancellationTokenSource = new CancellationTokenSource(); 40 var cancellationToken = _cancellationTokenSource.Token; 41 42 try 43 { 44 DoUpdateQuery(query, cancellationToken); 45 } 46 catch (Exception) 47 { 48 // Handle other exceptions 49 return; 50 } 51 } 52 53 private void DoUpdateQuery(string query, CancellationToken cancellationToken) 54 { 55 // Check for cancellation at the start 56 if (cancellationToken.IsCancellationRequested) 57 { 58 return; 59 } 60 61 var searchText = query.Trim(); 62 Expand(ref searchText); 63 64 if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText)) 65 { 66 Command = null; 67 Title = string.Empty; 68 return; 69 } 70 71 ShellListPageHelpers.NormalizeCommandLineAndArgs(searchText, out var exe, out var args); 72 73 // Check for cancellation before file system operations 74 cancellationToken.ThrowIfCancellationRequested(); 75 76 var exeExists = false; 77 var fullExePath = string.Empty; 78 var pathIsDir = false; 79 80 try 81 { 82 // Create a timeout for file system operations (200ms) 83 using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); 84 using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); 85 var timeoutToken = combinedCts.Token; 86 87 exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath, cancellationToken); 88 pathIsDir = Directory.Exists(exe); 89 } 90 catch (TimeoutException) 91 { 92 // Timeout occurred - use defaults 93 return; 94 } 95 catch (OperationCanceledException) 96 { 97 // Timeout occurred (from WaitAsync) - use defaults 98 return; 99 } 100 catch (Exception) 101 { 102 // Handle any other exceptions that might bubble up 103 return; 104 } 105 106 // Check for cancellation before updating UI properties 107 if (cancellationToken.IsCancellationRequested) 108 { 109 return; 110 } 111 112 if (exeExists) 113 { 114 // TODO we need to probably get rid of the settings for this provider entirely 115 var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, _addToHistory, telemetryService: _telemetryService); 116 Title = exeItem.Title; 117 Subtitle = exeItem.Subtitle; 118 Icon = exeItem.Icon; 119 Command = exeItem.Command; 120 MoreCommands = exeItem.MoreCommands; 121 } 122 else if (pathIsDir) 123 { 124 var pathItem = new PathListItem(exe, query, _addToHistory, _telemetryService); 125 Command = pathItem.Command; 126 MoreCommands = pathItem.MoreCommands; 127 Title = pathItem.Title; 128 Subtitle = pathItem.Subtitle; 129 Icon = pathItem.Icon; 130 } 131 else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri)) 132 { 133 Command = new OpenUrlWithHistoryCommand(searchText, _addToHistory, _telemetryService) { Result = CommandResult.Dismiss() }; 134 Title = searchText; 135 } 136 else 137 { 138 Command = null; 139 Title = string.Empty; 140 } 141 142 // Final cancellation check 143 if (cancellationToken.IsCancellationRequested) 144 { 145 return; 146 } 147 } 148 149 public void Dispose() 150 { 151 _cancellationTokenSource?.Cancel(); 152 _cancellationTokenSource?.Dispose(); 153 } 154 155 internal static bool SuppressFileFallbackIf(string query) 156 { 157 var searchText = query.Trim(); 158 Expand(ref searchText); 159 160 if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText)) 161 { 162 return false; 163 } 164 165 ShellHelpers.ParseExecutableAndArgs(searchText, out var exe, out var args); 166 var exeExists = ShellListPageHelpers.FileExistInPath(exe, out var fullExePath); 167 var pathIsDir = Directory.Exists(exe); 168 169 return exeExists || pathIsDir; 170 } 171 172 private static void Expand(ref string searchText) 173 { 174 if (searchText.Length == 0) 175 { 176 return; 177 } 178 179 var singleCharQuery = searchText.Length == 1; 180 181 searchText = Environment.ExpandEnvironmentVariables(searchText); 182 183 if (!TryExpandHome(ref searchText)) 184 { 185 TryExpandRoot(ref searchText); 186 } 187 } 188 189 private static bool TryExpandHome(ref string searchText) 190 { 191 if (searchText[0] == '~') 192 { 193 var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); 194 195 if (searchText.Length == 1) 196 { 197 searchText = home; 198 } 199 else if (_systemDirectoryRoots.Contains(searchText[1])) 200 { 201 searchText = Path.Combine(home, searchText[2..]); 202 } 203 204 return true; 205 } 206 207 return false; 208 } 209 210 private static bool TryExpandRoot(ref string searchText) 211 { 212 if (_systemDirectoryRoots.Contains(searchText[0]) && (searchText.Length == 1 || !_systemDirectoryRoots.Contains(searchText[1]))) 213 { 214 var root = Path.GetPathRoot(Environment.SystemDirectory); 215 if (root != null) 216 { 217 searchText = searchText.Length == 1 ? root : Path.Combine(root, searchText[1..]); 218 return true; 219 } 220 } 221 222 return false; 223 } 224 }