/ src / modules / cmdpal / ext / Microsoft.CmdPal.Ext.Shell / FallbackExecuteItem.cs
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  }