/ 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 }