/ src / modules / launcher / Plugins / Microsoft.Plugin.Folder / Sources / QueryInternalDirectory.cs
QueryInternalDirectory.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 System;
  6  using System.Buffers;
  7  using System.Collections.Generic;
  8  using System.Collections.Immutable;
  9  using System.Globalization;
 10  using System.IO.Abstractions;
 11  using System.Linq;
 12  
 13  using ManagedCommon;
 14  using Microsoft.Plugin.Folder.Sources.Result;
 15  
 16  namespace Microsoft.Plugin.Folder.Sources
 17  {
 18      public class QueryInternalDirectory : IQueryInternalDirectory
 19      {
 20          private static readonly SearchValues<char> PathChars = SearchValues.Create("\\/");
 21          private readonly FolderSettings _settings;
 22          private readonly IQueryFileSystemInfo _queryFileSystemInfo;
 23          private readonly IDirectory _directory;
 24  
 25          private static readonly HashSet<char> SpecialSearchChars = new HashSet<char>
 26          {
 27              '?', '*', '>',
 28          };
 29  
 30          private static string _warningIconPath;
 31  
 32          public QueryInternalDirectory(FolderSettings folderSettings, IQueryFileSystemInfo queryFileSystemInfo, IDirectory directory)
 33          {
 34              _settings = folderSettings;
 35              _queryFileSystemInfo = queryFileSystemInfo;
 36              _directory = directory;
 37          }
 38  
 39          private static bool HasSpecialChars(string search)
 40          {
 41              return search.Any(c => SpecialSearchChars.Contains(c));
 42          }
 43  
 44          public static bool RecursiveSearch(string query)
 45          {
 46              // give the ability to search all folder when it contains a >
 47              return query.Any(c => c.Equals('>'));
 48          }
 49  
 50          private (string Search, string IncompleteName) Process(string search)
 51          {
 52              string incompleteName = string.Empty;
 53              if (HasSpecialChars(search) || !_directory.Exists($@"{search}\"))
 54              {
 55                  // if folder doesn't exist, we want to take the last part and use it afterwards to help the user
 56                  // find the right folder.
 57                  int index = search.AsSpan().LastIndexOfAny(PathChars);
 58  
 59                  // No slashes found, so probably not a folder
 60                  if (index <= 0 || index >= search.Length - 1)
 61                  {
 62                      return default;
 63                  }
 64  
 65                  // Remove everything after the last \ and add *
 66                  // Using InvariantCulture since this is internal
 67                  incompleteName = search.Substring(index + 1)
 68                      .ToLower(CultureInfo.InvariantCulture) + "*";
 69                  search = search.Substring(0, index + 1);
 70                  if (!_directory.Exists(search))
 71                  {
 72                      return default;
 73                  }
 74              }
 75              else
 76              {
 77                  // folder exist, add \ at the end of doesn't exist
 78                  // Using Ordinal since this is internal and is used for a symbol
 79                  if (!search.EndsWith('\\'))
 80                  {
 81                      search += @"\";
 82                  }
 83              }
 84  
 85              return (search, incompleteName);
 86          }
 87  
 88          public IEnumerable<IItemResult> Query(string search)
 89          {
 90              ArgumentNullException.ThrowIfNull(search);
 91  
 92              var processed = Process(search);
 93  
 94              if (processed == default)
 95              {
 96                  yield break;
 97              }
 98  
 99              var (querySearch, incompleteName) = processed;
100              var isRecursive = RecursiveSearch(incompleteName);
101  
102              if (isRecursive)
103              {
104                  // match everything before and after search term using supported wildcard '*', i.e. *searchterm*
105                  if (string.IsNullOrEmpty(incompleteName))
106                  {
107                      incompleteName = "*";
108                  }
109                  else
110                  {
111                      incompleteName = string.Concat("*", incompleteName.AsSpan(1));
112                  }
113              }
114  
115              yield return new CreateOpenCurrentFolderResult(querySearch);
116  
117              // Note: Take 1000 is so that you don't search the whole system before you discard
118              var lookup = _queryFileSystemInfo.MatchFileSystemInfo(querySearch, incompleteName, isRecursive)
119                  .Take(1000)
120                  .ToLookup(r => r.Type);
121  
122              var folderList = lookup[DisplayType.Directory].ToImmutableArray();
123              var fileList = lookup[DisplayType.File].ToImmutableArray();
124  
125              var fileSystemResult = GenerateFolderResults(querySearch, folderList)
126                  .Concat<IItemResult>(GenerateFileResults(querySearch, fileList))
127                  .ToImmutableArray();
128  
129              foreach (var result in fileSystemResult)
130              {
131                  yield return result;
132              }
133  
134              // Show warning message if result has been truncated
135              if (folderList.Length > _settings.MaxFolderResults || fileList.Length > _settings.MaxFileResults)
136              {
137                  yield return GenerateTruncatedItemResult(folderList.Length + fileList.Length, fileSystemResult.Length);
138              }
139          }
140  
141          private IEnumerable<FileItemResult> GenerateFileResults(string search, IEnumerable<DisplayFileInfo> fileList)
142          {
143              return fileList
144                  .Select(fileSystemInfo => new FileItemResult()
145                  {
146                      FilePath = fileSystemInfo.FullName,
147                      Search = search,
148                  })
149                  .OrderBy(x => x.Title)
150                  .Take(_settings.MaxFileResults);
151          }
152  
153          private IEnumerable<FolderItemResult> GenerateFolderResults(string search, IEnumerable<DisplayFileInfo> folderList)
154          {
155              return folderList
156                  .Select(fileSystemInfo => new FolderItemResult(fileSystemInfo)
157                  {
158                      Search = search,
159                  })
160                  .OrderBy(x => x.Title)
161                  .Take(_settings.MaxFolderResults);
162          }
163  
164          private static TruncatedItemResult GenerateTruncatedItemResult(int preTruncationCount, int postTruncationCount)
165          {
166              return new TruncatedItemResult()
167              {
168                  PreTruncationCount = preTruncationCount,
169                  PostTruncationCount = postTruncationCount,
170                  WarningIconPath = _warningIconPath,
171              };
172          }
173  
174          public static void SetWarningIcon(Theme theme)
175          {
176              if (theme == Theme.Light || theme == Theme.HighContrastWhite)
177              {
178                  _warningIconPath = "Images/Warning.light.png";
179              }
180              else
181              {
182                  _warningIconPath = "Images/Warning.dark.png";
183              }
184          }
185      }
186  }