From 8bb891e34c5c62c542c842f070e46d3e89aaa288 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 22 Jan 2023 23:09:25 -0600 Subject: [PATCH 1/4] Refactor Search Manager 1. Return SearchResult instead of Result (and cast outside) 2. Make PathSearch return IAsyncEnumerable (instead of plain result) 3. Use Iterator for Environmental Path Search --- .../SharedCommands/FilesFolders.cs | 2 +- Plugins/Flow.Launcher.Plugin.Explorer/Main.cs | 12 +- .../Search/EnvironmentVariables.cs | 50 ++--- .../Search/QuickAccessLinks/QuickAccess.cs | 35 ++- .../Search/ResultManager.cs | 49 ++-- .../Search/SearchManager.cs | 212 ++++++++++-------- .../Search/SearchResult.cs | 1 + 7 files changed, 202 insertions(+), 159 deletions(-) diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index bd8d32ff511..3df2487427d 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -246,7 +246,7 @@ public static string ReturnPreviousDirectoryIfIncompleteString(string path) /// Sub path /// If , when and are equal, returns /// - public static bool PathContains(string parentPath, string subPath, bool allowEqual = false) + public static bool PathContains(this string parentPath, string subPath, bool allowEqual = false) { var rel = Path.GetRelativePath(parentPath.EnsureTrailingSlash(), subPath); return (rel != "." || allowEqual) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 82a5d544122..65e7b93d71c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -42,11 +43,13 @@ public Task InitAsync(PluginInitContext context) contextMenu = new ContextMenu(Context, Settings, viewModel); searchManager = new SearchManager(Settings, Context); ResultManager.Init(Context, Settings); - + SortOptionTranslationHelper.API = context.API; - EverythingApiDllImport.Load(Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "EverythingSDK", + EverythingApiDllImport.Load(Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, + "EverythingSDK", Environment.Is64BitProcess ? "x64" : "x86")); + return Task.CompletedTask; } @@ -59,7 +62,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) { try { - return await searchManager.SearchAsync(query, token); + return (await searchManager.SearchAsync(query, token)).Select(r => ResultManager.CreateResult(query, r)).ToList(); } catch (Exception e) when (e is SearchException or EngineNotAvailableException) { @@ -75,11 +78,12 @@ public async Task> QueryAsync(Query query, CancellationToken token) IcoPath = e is EngineNotAvailableException { ErrorIcon: { } iconPath } ? iconPath : Constants.GeneralSearchErrorImagePath, - AsyncAction = e is EngineNotAvailableException {Action: { } action} + AsyncAction = e is EngineNotAvailableException { Action: { } action } ? action : _ => { Clipboard.SetDataObject(e.ToString()); + return new ValueTask(true); } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs index e526fb85a1f..d93162f53d6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs @@ -18,6 +18,7 @@ private static Dictionary EnvStringPaths { LoadEnvironmentStringPaths(); } + return _envStringPaths; } } @@ -25,19 +26,20 @@ private static Dictionary EnvStringPaths internal static bool IsEnvironmentVariableSearch(string search) { return search.StartsWith("%") - && search != "%%" - && !search.Contains('\\') - && EnvStringPaths.Count > 0; + && search != "%%" + && !search.Contains('\\') + && EnvStringPaths.Count > 0; } public static bool HasEnvironmentVar(string search) { // "c:\foo %appdata%\" returns false var splited = search.Split(Path.DirectorySeparatorChar); - return splited.Any(dir => dir.StartsWith('%') && - dir.EndsWith('%') && - dir.Length > 2 && - dir.Split('%').Length == 3); + + return splited.Any(dir => dir.StartsWith('%') && + dir.EndsWith('%') && + dir.Length > 2 && + dir.Split('%').Length == 3); } private static void LoadEnvironmentStringPaths() @@ -66,10 +68,8 @@ private static void LoadEnvironmentStringPaths() } } - internal static List GetEnvironmentStringPathSuggestions(string querySearch, Query query, PluginInitContext context) + internal static IEnumerable GetEnvironmentStringPathSuggestions(string querySearch, Query query, PluginInitContext context) { - var results = new List(); - var search = querySearch; if (querySearch.EndsWith("%") && search.Length > 1) @@ -81,30 +81,28 @@ internal static List GetEnvironmentStringPathSuggestions(string querySea { var expandedPath = EnvStringPaths[search]; - results.Add(ResultManager.CreateFolderResult($"%{search}%", expandedPath, expandedPath, query)); - - return results; + yield return new SearchResult + { + Name = $"%{search}%", FullPath = expandedPath + }; } } - if (querySearch == "%") - { - search = ""; // Get all paths - } - else - { - search = search.Substring(1); - } + ReadOnlyMemory slice = querySearch == "%" ? "".AsMemory() : // Get all paths + search.AsMemory()[1..]; - foreach (var p in EnvStringPaths) + foreach (var pair in EnvStringPaths) { - if (p.Key.StartsWith(search, StringComparison.InvariantCultureIgnoreCase)) + if (pair.Key.AsSpan().StartsWith(slice.Span, StringComparison.InvariantCultureIgnoreCase)) { - results.Add(ResultManager.CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query)); + yield return new SearchResult + { + Name = $"%{pair.Key}%", + FullPath = pair.Value, + Type = ResultType.EnvironmentalVariable + }; } } - - return results; } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs index cdd2c93e69c..b9264bd4d02 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs @@ -8,36 +8,35 @@ internal static class QuickAccess { private const int quickAccessResultScore = 100; - internal static List AccessLinkListMatched(Query query, IEnumerable accessLinks) + internal static IEnumerable AccessLinkListMatched(Query query, IEnumerable accessLinks) { if (string.IsNullOrEmpty(query.Search)) - return new List(); + return Enumerable.Empty(); string search = query.Search.ToLower(); - var queriedAccessLinks = - accessLinks + return accessLinks .Where(x => x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || x.Path.Contains(search, StringComparison.OrdinalIgnoreCase)) .OrderBy(x => x.Type) - .ThenBy(x => x.Name); - - return queriedAccessLinks.Select(l => l.Type switch - { - ResultType.Folder => ResultManager.CreateFolderResult(l.Name, l.Path, l.Path, query, quickAccessResultScore), - ResultType.File => ResultManager.CreateFileResult(l.Path, query, quickAccessResultScore), - _ => throw new ArgumentOutOfRangeException() - }).ToList(); + .ThenBy(x => x.Name) + .Select(x => new SearchResult() + { + FullPath = x.Path, + Score = quickAccessResultScore, + Type = x.Type, + WindowsIndexed = false + }); } - internal static List AccessLinkListAll(Query query, IEnumerable accessLinks) + internal static IEnumerable AccessLinkListAll(Query query, IEnumerable accessLinks) => accessLinks .OrderBy(x => x.Type) .ThenBy(x => x.Name) - .Select(l => l.Type switch + .Select(l => new SearchResult() { - ResultType.Folder => ResultManager.CreateFolderResult(l.Name, l.Path, l.Path, query), - ResultType.File => ResultManager.CreateFileResult(l.Path, query, quickAccessResultScore), - _ => throw new ArgumentOutOfRangeException() - }).ToList(); + FullPath = l.Path, + Type = l.Type, + Score = quickAccessResultScore + }); } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index ed4f39735bd..5f4eb484e0e 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -13,6 +13,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search public static class ResultManager { private static PluginInitContext Context; + private static Settings Settings { get; set; } public static void Init(PluginInitContext context, Settings settings) @@ -57,15 +58,30 @@ public static Result CreateResult(Query query, SearchResult result) { return result.Type switch { - ResultType.Folder or ResultType.Volume => CreateFolderResult(Path.GetFileName(result.FullPath), - result.FullPath, result.FullPath, query, 0, result.WindowsIndexed), + ResultType.Folder => CreateFolderResult(Path.GetFileName(result.FullPath), + result.FullPath, + result.FullPath, + query, + 0, + result.WindowsIndexed), + ResultType.CurrentFolder => CreateOpenCurrentFolderResult(result.FullPath, query.ActionKeyword, result.WindowsIndexed), + ResultType.Volume => CreateDriveSpaceDisplayResult(result.FullPath, + query.ActionKeyword, + result.WindowsIndexed), ResultType.File => CreateFileResult( - result.FullPath, query, 0, result.WindowsIndexed), + result.FullPath, + query, + 0, + result.WindowsIndexed), + ResultType.EnvironmentalVariable => CreateFolderResult(result.Name, + result.FullPath, + result.FullPath, + query), _ => throw new ArgumentOutOfRangeException() }; } - internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool windowsIndexed = false) + private static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool windowsIndexed = false) { return new Result { @@ -82,11 +98,13 @@ internal static Result CreateFolderResult(string title, string subtitle, string try { Context.API.OpenDirectory(path); + return true; } catch (Exception ex) { MessageBox.Show(ex.Message, "Could not start " + path); + return false; } } @@ -136,6 +154,7 @@ internal static Result CreateDriveSpaceDisplayResult(string path, string actionK Action = c => { Context.API.OpenDirectory(path); + return true; }, TitleToolTip = path, @@ -171,6 +190,7 @@ private static string ToReadableSize(long pDrvSize, int pi) Space = " TB"; var returnStr = $"{Convert.ToInt32(drvSize)}{Space}"; + if (mok != 0) { returnStr = pi switch @@ -202,6 +222,7 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK Action = _ => { Context.API.OpenDirectory(folderPath); + return true; }, ContextData = new SearchResult @@ -213,7 +234,7 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK }; } - internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) + private static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) { Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) ? new Result.PreviewInfo { @@ -281,22 +302,16 @@ internal static Result CreateFileResult(string filePath, Query query, int score WindowsIndexed = windowsIndexed } }; + return result; } - public static bool IsMedia(string extension) + private static bool IsMedia(string extension) { - if (string.IsNullOrEmpty(extension)) - { - return false; - } - else - { - return MediaExtensions.Contains(extension.ToLowerInvariant()); - } + return !string.IsNullOrEmpty(extension) && MediaExtensions.Contains(extension.ToLowerInvariant()); } - public static readonly string[] MediaExtensions = + private static readonly string[] MediaExtensions = { ".jpg", ".png", ".avi", ".mkv", ".bmp", ".gif", ".wmv", ".mp3", ".flac", ".mp4" }; @@ -306,6 +321,8 @@ public enum ResultType { Volume, Folder, - File + CurrentFolder, + File, + EnvironmentalVariable } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 51c4c3d9d54..05667bcdd18 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -1,9 +1,9 @@ using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo; -using Flow.Launcher.Plugin.Explorer.Search.Everything; using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.SharedCommands; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -26,92 +26,87 @@ public SearchManager(Settings settings, PluginInitContext context) /// /// Note: A path that ends with "\" and one that doesn't will not be regarded as equal. /// - public class PathEqualityComparator : IEqualityComparer + public class PathEqualityComparator : IEqualityComparer { private static PathEqualityComparator instance; + public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator(); - public bool Equals(Result x, Result y) + public bool Equals(SearchResult x, SearchResult y) { - return x.Title.Equals(y.Title, StringComparison.OrdinalIgnoreCase) - && string.Equals(x.SubTitle, y.SubTitle, StringComparison.OrdinalIgnoreCase); + return x.FullPath.Equals(y.FullPath, StringComparison.OrdinalIgnoreCase); } - public int GetHashCode(Result obj) + public int GetHashCode(SearchResult obj) { - return HashCode.Combine(obj.Title.ToLowerInvariant(), obj.SubTitle?.ToLowerInvariant() ?? ""); + return obj.FullPath.GetHashCode(); } } - internal async Task> SearchAsync(Query query, CancellationToken token) + internal async Task> SearchAsync(Query query, CancellationToken token) { - var results = new HashSet(PathEqualityComparator.Instance); - - // This allows the user to type the below action keywords and see/search the list of quick folder links - if (ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword)) - { - if (string.IsNullOrEmpty(query.Search) && ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword)) - return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks); + var results = new HashSet(PathEqualityComparator.Instance); - var quickAccessLinks = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks); + var task = GetQueryTask(query); - results.UnionWith(quickAccessLinks); - } - else + // This allows the user to type the below action keywords and see/search the list of quick folder links + if (task.HasFlag(SearchTask.QuickAccessSearch)) { - return new List(); + if (string.IsNullOrEmpty(query.Search)) + { + results.UnionWith(QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks)); + } + else + { + var quickAccessLinks = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks); + results.UnionWith(quickAccessLinks); + } } - IAsyncEnumerable searchResults; + IAsyncEnumerable searchResults = null; bool isPathSearch = query.Search.IsLocationPathString() || IsEnvironmentVariableSearch(query.Search); - string engineName; + string engineName = ""; - switch (isPathSearch) + if (task.HasFlag(SearchTask.PathSearch) && isPathSearch) { - case true - when ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword): - - results.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false)); - - return results.ToList(); - - case false - when ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword): + await foreach (var path in PathSearchAsync(query, token).ConfigureAwait(false)) + { + results.Add(path); + } - // Intentionally require enabling of Everything's content search due to its slowness - if (Settings.ContentIndexProvider is EverythingSearchManager && !Settings.EnableEverythingContentSearch) - return EverythingContentSearchResult(query); + return results.ToList(); + } - searchResults = Settings.ContentIndexProvider.ContentSearchAsync("", query.Search, token); - engineName = Enum.GetName(Settings.ContentSearchEngine); - break; + if (task.HasFlag(SearchTask.IndexSearch)) + { + searchResults = Settings.IndexProvider.SearchAsync(query.Search, token); + engineName = Enum.GetName(Settings.IndexSearchEngine); + } - case false - when ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword): + if (task.HasFlag(SearchTask.FileContentSearch)) + { + if (!Settings.EnableEverythingContentSearch && Settings.ContentSearchEngine == Settings.ContentIndexSearchEngineOption.Everything) + ThrowEverythingContentSearchUnavailable(query); - searchResults = Settings.IndexProvider.SearchAsync(query.Search, token); - engineName = Enum.GetName(Settings.IndexSearchEngine); - break; - default: - return results.ToList(); + searchResults = Settings.ContentIndexProvider.ContentSearchAsync("", query.Search, token); + engineName = Enum.GetName(Settings.ContentSearchEngine); } try { - await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false)) - results.Add(ResultManager.CreateResult(query, search)); + if (searchResults != null) + { + await foreach (var result in searchResults.WithCancellation(token)) + { + results.Add(result); + } + } } catch (OperationCanceledException) { - return new List(); + return null; } catch (EngineNotAvailableException) { @@ -123,11 +118,33 @@ when ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) } results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any( - excludedPath => FilesFolders.PathContains(excludedPath.Path, r.SubTitle))); + excludedPath => excludedPath.Path.PathContains(r.FullPath))); return results.ToList(); } + private SearchTask GetQueryTask(Query query) + { + SearchTask task = SearchTask.None; + + if (ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)) + task |= SearchTask.IndexSearch | SearchTask.PathSearch | SearchTask.QuickAccessSearch; + + if (ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword)) + task |= SearchTask.QuickAccessSearch; + + if (ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword)) + task |= SearchTask.PathSearch; + + if (ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword)) + task |= SearchTask.IndexSearch; + + if (ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword)) + task |= SearchTask.FileContentSearch; + + return task; + } + private bool ActionKeywordMatch(Query query, Settings.ActionKeyword allowedActionKeyword) { var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword; @@ -148,33 +165,35 @@ private bool ActionKeywordMatch(Query query, Settings.ActionKeyword allowedActio }; } - private List EverythingContentSearchResult(Query query) + [DoesNotReturn] + private void ThrowEverythingContentSearchUnavailable(Query query) { - return new List() - { - new() + throw new EngineNotAvailableException(nameof(Settings.ContentIndexSearchEngineOption.Everything), + Context.API.GetTranslation("flowlauncher_plugin_everything_enable_content_search_tips"), + Context.API.GetTranslation("flowlauncher_plugin_everything_enable_content_search"), + _ => { - Title = Context.API.GetTranslation("flowlauncher_plugin_everything_enable_content_search"), - SubTitle = Context.API.GetTranslation("flowlauncher_plugin_everything_enable_content_search_tips"), - IcoPath = "Images/index_error.png", - Action = c => - { - Settings.EnableEverythingContentSearch = true; - Context.API.ChangeQuery(query.RawQuery, true); - return false; - } - } - }; + Settings.EnableEverythingContentSearch = true; + Context.API.ChangeQuery(query.RawQuery, true); + + return ValueTask.FromResult(false); + }); + } - private async Task> PathSearchAsync(Query query, CancellationToken token = default) + private async IAsyncEnumerable PathSearchAsync(Query query, CancellationToken token = default) { var querySearch = query.Search; - var results = new HashSet(PathEqualityComparator.Instance); - if (EnvironmentVariables.IsEnvironmentVariableSearch(querySearch)) - return EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, Context); + { + foreach (var envResult in EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, Context)) + { + yield return envResult; + } + + yield break; + } // Query is a location path with a full environment variable, eg. %appdata%\somefolder\, c:\users\%USERNAME%\downloads var needToExpand = EnvironmentVariables.HasEnvironmentVar(querySearch); @@ -182,19 +201,25 @@ private async Task> PathSearchAsync(Query query, CancellationToken // Check that actual location exists, otherwise directory search will throw directory not found exception if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath).LocationExists()) - return results.ToList(); + yield break; var useIndexSearch = Settings.IndexSearchEngine is Settings.IndexSearchEngineOption.WindowsIndex && UseWindowsIndexForDirectorySearch(locationPath); var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath); - results.Add(retrievedDirectoryPath.EndsWith(":\\") - ? ResultManager.CreateDriveSpaceDisplayResult(retrievedDirectoryPath, query.ActionKeyword, useIndexSearch) - : ResultManager.CreateOpenCurrentFolderResult(retrievedDirectoryPath, query.ActionKeyword, useIndexSearch)); + yield return new SearchResult() + { + FullPath = retrievedDirectoryPath, + Type = retrievedDirectoryPath.EndsWith(":\\") + ? ResultType.Volume + : ResultType.CurrentFolder, + Score = 100, + WindowsIndexed = useIndexSearch + }; if (token.IsCancellationRequested) - return new List(); + yield break; IAsyncEnumerable directoryResult; @@ -208,7 +233,6 @@ private async Task> PathSearchAsync(Query query, CancellationToken query.Search[(recursiveIndicatorIndex + 1)..], true, token); - } else { @@ -216,22 +240,12 @@ private async Task> PathSearchAsync(Query query, CancellationToken } if (token.IsCancellationRequested) - return new List(); + yield break; - try + await foreach (var directory in directoryResult.WithCancellation(token).ConfigureAwait(false)) { - await foreach (var directory in directoryResult.WithCancellation(token).ConfigureAwait(false)) - { - results.Add(ResultManager.CreateResult(query, directory)); - } + yield return directory; } - catch (Exception e) - { - throw new SearchException(Enum.GetName(Settings.PathEnumerationEngine), e.Message, e); - } - - - return results.ToList(); } public bool IsFileContentSearch(string actionKeyword) => actionKeyword == Settings.FileContentSearchActionKeyword; @@ -246,11 +260,21 @@ private bool UseWindowsIndexForDirectorySearch(string locationPath) && WindowsIndex.WindowsIndex.PathIsIndexed(pathToDirectory); } - internal static bool IsEnvironmentVariableSearch(string search) + private static bool IsEnvironmentVariableSearch(string search) { return search.StartsWith("%") && search != "%%" && !search.Contains('\\'); } } + + [Flags] + internal enum SearchTask + { + None = 0, + IndexSearch = 1 << 0, + QuickAccessSearch = 1 << 1, + PathSearch = 1 << 2, + FileContentSearch = 1 << 3, + } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs index 92c24559d6e..44315bb7598 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs @@ -4,6 +4,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { public record struct SearchResult { + public string Name { get; init; } public string FullPath { get; init; } public ResultType Type { get; init; } public int Score { get; init; } From 00c156f60c2f64aa913c8873ac61ad102f8b05c1 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 22 Jan 2023 23:15:01 -0600 Subject: [PATCH 2/4] null check to avoid error --- Plugins/Flow.Launcher.Plugin.Explorer/Main.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 65e7b93d71c..5ca8e201294 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -62,7 +62,10 @@ public async Task> QueryAsync(Query query, CancellationToken token) { try { - return (await searchManager.SearchAsync(query, token)).Select(r => ResultManager.CreateResult(query, r)).ToList(); + // null means cancelled + return (await searchManager.SearchAsync(query, token))? + .Select(r => ResultManager.CreateResult(query, r)) + .ToList(); } catch (Exception e) when (e is SearchException or EngineNotAvailableException) { From 0f5e4559326db5d7c748841e8fe92347e5221a91 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Mon, 23 Jan 2023 11:19:24 -0600 Subject: [PATCH 3/4] fix unit test and ignorecase hashcode --- Flow.Launcher.Test/Plugins/ExplorerTest.cs | 120 +++++++++++------- .../Search/SearchManager.cs | 2 +- 2 files changed, 76 insertions(+), 46 deletions(-) diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index e9d37433f4e..4639d431ae1 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -57,7 +57,8 @@ public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestric var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath); // Then - Assert.IsTrue(result == expectedString, + Assert.AreEqual(result, + expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual: {result}{Environment.NewLine}"); } @@ -74,18 +75,23 @@ public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_Then var queryString = queryConstructor.Directory(folderPath); // Then - Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), + Assert.AreEqual(queryString.Replace(" ", " "), + expectedString.Replace(" ", " "), $"Expected string: {expectedString}{Environment.NewLine} " + $"Actual string was: {queryString}{Environment.NewLine}"); } [SupportedOSPlatform("windows7.0")] - [TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" + - " FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" + - " AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" + - " ORDER BY System.FileName")] + [TestCase("C:\\SomeFolder", + "flow.launcher.sln", + "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" + + " FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" + + " AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" + + " ORDER BY System.FileName")] public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString( - string folderPath, string userSearchString, string expectedString) + string folderPath, + string userSearchString, + string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -109,12 +115,14 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereR } [SupportedOSPlatform("windows7.0")] - [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " + - "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + - "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] + [TestCase("flow.launcher.sln", + "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " + + "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] [TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( - string userSearchString, string expectedString) + string userSearchString, + string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -135,7 +143,8 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShould [SupportedOSPlatform("windows7.0")] [TestCase(@"some words", @"FREETEXT('some words')")] public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString( - string querySearchString, string expectedString) + string querySearchString, + string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -144,16 +153,19 @@ public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSe var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString); // Then - Assert.IsTrue(resultString == expectedString, + Assert.AreEqual(resultString, + expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } [SupportedOSPlatform("windows7.0")] - [TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + - "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] + [TestCase("some words", + "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + + "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString( - string userSearchString, string expectedString) + string userSearchString, + string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -162,7 +174,8 @@ public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseE var resultString = queryConstructor.FileContent(userSearchString); // Then - Assert.IsTrue(resultString == expectedString, + Assert.AreEqual(resultString, + expectedString, $"Expected query string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } @@ -202,7 +215,8 @@ public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString( var result = FilesFolders.IsLocationPathString(querySearchString); //Then - Assert.IsTrue(result == expectedResult, + Assert.AreEqual(result, + expectedResult, $"Expected query search string check result is: {expectedResult} {Environment.NewLine} " + $"Actual check result is {result} {Environment.NewLine}"); @@ -212,10 +226,13 @@ public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString( [TestCase(@"C:\SomeFolder\SomeApp\SomeFile", true, @"C:\SomeFolder\SomeApp\")] [TestCase(@"C:\NonExistentFolder\SomeApp", false, "")] public void GivenAPartialPath_WhenPreviousLevelDirectoryExists_ThenShouldReturnThePreviousDirectoryPathString( - string path, bool previousDirectoryExists, string expectedString) + string path, + bool previousDirectoryExists, + string expectedString) { // When Func previousLocationExists = null; + if (previousDirectoryExists) { previousLocationExists = PreviousLocationExistsReturnsTrue; @@ -229,7 +246,8 @@ public void GivenAPartialPath_WhenPreviousLevelDirectoryExists_ThenShouldReturnT var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path); //Then - Assert.IsTrue(previousDirectoryPath == expectedString, + Assert.AreEqual(previousDirectoryPath, + expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {previousDirectoryPath} {Environment.NewLine}"); } @@ -237,12 +255,14 @@ public void GivenAPartialPath_WhenPreviousLevelDirectoryExists_ThenShouldReturnT [TestCase(@"C:\NonExistentFolder\SomeApp", @"C:\NonExistentFolder\")] [TestCase(@"C:\NonExistentFolder\SomeApp\", @"C:\NonExistentFolder\SomeApp\")] public void WhenGivenAPath_ThenShouldReturnThePreviousDirectoryPathIfIncompleteOrOriginalString( - string path, string expectedString) + string path, + string expectedString) { var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); //Then - Assert.IsTrue(returnedPath == expectedString, + Assert.AreEqual(returnedPath, + expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {returnedPath} {Environment.NewLine}"); } @@ -280,23 +300,24 @@ public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSear [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "p", true, false, "p c:\\somefolder\\someotherfolder\\")] [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "", true, true, "c:\\somefolder\\someotherfolder\\")] public void GivenFolderResult_WhenGetPath_ThenPathShouldBeExpectedString( - string path, - ResultType type, + string path, + ResultType type, string actionKeyword, - bool pathSearchKeywordEnabled, + bool pathSearchKeywordEnabled, bool searchActionKeywordEnabled, string expectedResult) { // Given - var settings = new Settings() + var settings = new Settings { PathSearchKeywordEnabled = pathSearchKeywordEnabled, PathSearchActionKeyword = "p", SearchActionKeywordEnabled = searchActionKeywordEnabled, SearchActionKeyword = Query.GlobalPluginWildcardSign }; + ResultManager.Init(new PluginInitContext(), settings); - + // When var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); @@ -317,13 +338,14 @@ public void GivenFileResult_WhenGetPath_ThenPathShouldBeExpectedString( string expectedResult) { // Given - var settings = new Settings() + var settings = new Settings { PathSearchKeywordEnabled = pathSearchKeywordEnabled, PathSearchActionKeyword = "p", SearchActionKeywordEnabled = searchActionKeywordEnabled, SearchActionKeyword = "e" }; + ResultManager.Init(new PluginInitContext(), settings); // When @@ -346,8 +368,12 @@ public void GivenQueryWithFolderTypeResult_WhenGetAutoComplete_ThenResultShouldB string expectedResult) { // Given - var query = new Query() { ActionKeyword = actionKeyword }; - var settings = new Settings() + var query = new Query + { + ActionKeyword = actionKeyword + }; + + var settings = new Settings { PathSearchKeywordEnabled = pathSearchKeywordEnabled, PathSearchActionKeyword = "p", @@ -356,6 +382,7 @@ public void GivenQueryWithFolderTypeResult_WhenGetAutoComplete_ThenResultShouldB QuickAccessActionKeyword = "q", IndexSearchActionKeyword = "i" }; + ResultManager.Init(new PluginInitContext(), settings); // When @@ -378,8 +405,12 @@ public void GivenQueryWithFileTypeResult_WhenGetAutoComplete_ThenResultShouldBeE string expectedResult) { // Given - var query = new Query() { ActionKeyword = actionKeyword }; - var settings = new Settings() + var query = new Query + { + ActionKeyword = actionKeyword + }; + + var settings = new Settings { QuickAccessActionKeyword = "q", IndexSearchActionKeyword = "i", @@ -388,6 +419,7 @@ public void GivenQueryWithFileTypeResult_WhenGetAutoComplete_ThenResultShouldBeE SearchActionKeywordEnabled = searchActionKeywordEnabled, SearchActionKeyword = Query.GlobalPluginWildcardSign }; + ResultManager.Init(new PluginInitContext(), settings); // When @@ -404,15 +436,14 @@ public void GivenTwoPaths_WhenCompared_ThenShouldBeExpectedSameOrDifferent(strin { // Given var comparator = PathEqualityComparator.Instance; - var result1 = new Result + var result1 = new SearchResult { - Title = Path.GetFileName(path1), - SubTitle = path1 + FullPath = path1 }; - var result2 = new Result + + var result2 = new SearchResult { - Title = Path.GetFileName(path2), - SubTitle = path2 + FullPath = path2 }; // When, Then @@ -425,22 +456,21 @@ public void GivenTwoPaths_WhenComparedHasCode_ThenShouldBeSame(string path1, str { // Given var comparator = PathEqualityComparator.Instance; - var result1 = new Result + var result1 = new SearchResult { - Title = Path.GetFileName(path1), - SubTitle = path1 + FullPath = path1 }; - var result2 = new Result + + var result2 = new SearchResult { - Title = Path.GetFileName(path2), - SubTitle = path2 + FullPath = path2 }; var hash1 = comparator.GetHashCode(result1); var hash2 = comparator.GetHashCode(result2); // When, Then - Assert.IsTrue(hash1 == hash2); + Assert.AreEqual(hash1, hash2); } [TestCase(@"%appdata%", true)] diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 05667bcdd18..a050b8fccc9 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -39,7 +39,7 @@ public bool Equals(SearchResult x, SearchResult y) public int GetHashCode(SearchResult obj) { - return obj.FullPath.GetHashCode(); + return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.FullPath); } } From aafce3ab238fec4236ea680de98f21a1842b36fe Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Mon, 23 Jan 2023 11:20:10 -0600 Subject: [PATCH 4/4] Use InvariantCultureIgnoreCase instead of OrdinalIgnoreCase --- Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index a050b8fccc9..d7038c363be 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -34,12 +34,12 @@ public class PathEqualityComparator : IEqualityComparer public bool Equals(SearchResult x, SearchResult y) { - return x.FullPath.Equals(y.FullPath, StringComparison.OrdinalIgnoreCase); + return x.FullPath.Equals(y.FullPath, StringComparison.InvariantCultureIgnoreCase); } public int GetHashCode(SearchResult obj) { - return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.FullPath); + return StringComparer.InvariantCultureIgnoreCase.GetHashCode(obj.FullPath); } }