diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index c40040df5be..45c0d72acd9 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -58,6 +58,8 @@ File Content Search: Index Search: Quick Access: + Folder Search: + File Search: Current Action Keyword Done Enabled diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 21a6945a8d9..9420e3d3a31 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -1,13 +1,14 @@ -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; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Plugin.Explorer.Exceptions; +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 static Flow.Launcher.Plugin.Explorer.Settings; using Path = System.IO.Path; namespace Flow.Launcher.Plugin.Explorer.Search @@ -18,6 +19,14 @@ public class SearchManager internal Settings Settings; + private readonly Dictionary _allowedTypesByActionKeyword = new() + { + { ActionKeyword.FileSearchActionKeyword, [ResultType.File] }, + { ActionKeyword.FolderSearchActionKeyword, [ResultType.Folder, ResultType.Volume] }, + { ActionKeyword.SearchActionKeyword, [ResultType.File, ResultType.Folder, ResultType.Volume] }, + }; + + public SearchManager(Settings settings, PluginInitContext context) { Context = context; @@ -44,48 +53,71 @@ public int GetHashCode(Result obj) } } + /// + /// Results for the different types of searches as follows: + /// 1. Search, only include results from: + /// - Files + /// - Folders + /// - Quick Access Links + /// - Path navigation + /// 2. File Content Search, only include results from: + /// - File contents from index search engines i.e. Windows Index, Everything (may not be available due its beta version) + /// 3. Path Search, only include results from: + /// - Path navigation + /// 4. Quick Access Links, only include results from: + /// - Full list of Quick Access Links if query is empty + /// - Matched Quick Access Links if query is not empty + /// - Quick Access Links that are matched on path, e.g. query "window" for results that contain 'window' in the path (even if not in the title), + /// i.e. result with path c:\windows\system32 + /// 5. Folder Search, only include results from: + /// - Folders + /// - Quick Access Links + /// 6. File Search, only include results from: + /// - Files + /// - Quick Access Links + /// 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)) + var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword; + + // No action keyword matched - plugin should not handle this query, return empty results. + var activeActionKeywords = Settings.GetActiveActionKeywords(keyword); + if (activeActionKeywords.Count == 0) { - if (string.IsNullOrEmpty(query.Search) && ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword)) - return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks); + return [.. results]; } - else + + var queryIsEmpty = string.IsNullOrEmpty(query.Search); + if (queryIsEmpty && activeActionKeywords.ContainsKey(ActionKeyword.QuickAccessActionKeyword)) { - // No action keyword matched- plugin should not handle this query, return empty results. - return new List(); + return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks); } - IAsyncEnumerable searchResults; + if (queryIsEmpty) + { + return [.. results]; + } - bool isPathSearch = query.Search.IsLocationPathString() + var isPathSearch = query.Search.IsLocationPathString() || EnvironmentVariables.IsEnvironmentVariableSearch(query.Search) || EnvironmentVariables.HasEnvironmentVar(query.Search); + IAsyncEnumerable searchResults; + string engineName; switch (isPathSearch) { case true - when ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword): - + when CanUsePathSearchByActionKeywords(activeActionKeywords): results.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false)); - - return results.ToList(); + return [.. results]; case false - when ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword): - // Intentionally require enabling of Everything's content search due to its slowness + when activeActionKeywords.ContainsKey(ActionKeyword.FileContentSearchActionKeyword): if (Settings.ContentIndexProvider is EverythingSearchManager && !Settings.EnableEverythingContentSearch) return EverythingContentSearchResult(query); @@ -93,37 +125,41 @@ when ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKey engineName = Enum.GetName(Settings.ContentSearchEngine); break; - case false - when ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword): + case true or false + when activeActionKeywords.ContainsKey(ActionKeyword.QuickAccessActionKeyword): + return QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks); + + case false + when CanUseIndexSearchByActionKeywords(activeActionKeywords): searchResults = Settings.IndexProvider.SearchAsync(query.Search, token); engineName = Enum.GetName(Settings.IndexSearchEngine); break; - - case true or false - when ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword): - return QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks); - default: - return results.ToList(); + return [.. results]; + } - // Merge Quick Access Link results for non-path searches. - results.UnionWith(QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks)); + var actions = activeActionKeywords.Keys.ToList(); + //Merge Quick Access Link results for non-path searches. + results.UnionWith(GetQuickAccessResultsFilteredByActionKeyword(query, actions)); try { await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false)) - if (search.Type == ResultType.File && IsExcludedFile(search)) { + { + if (search.Type == ResultType.File && IsExcludedFile(search)) continue; - } else { - results.Add(ResultManager.CreateResult(query, search)); - } + + if (IsResultTypeFilteredByActionKeyword(search.Type, actions)) + continue; + + results.Add(ResultManager.CreateResult(query, search)); + } } catch (OperationCanceledException) { - return new List(); + return [.. results]; } catch (EngineNotAvailableException) { @@ -137,33 +173,13 @@ when ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword): results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any( excludedPath => FilesFolders.PathContains(excludedPath.Path, r.SubTitle, allowEqual: true))); - return results.ToList(); - } - - private bool ActionKeywordMatch(Query query, Settings.ActionKeyword allowedActionKeyword) - { - var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword; - - return allowedActionKeyword switch - { - Settings.ActionKeyword.SearchActionKeyword => Settings.SearchActionKeywordEnabled && - keyword == Settings.SearchActionKeyword, - Settings.ActionKeyword.PathSearchActionKeyword => Settings.PathSearchKeywordEnabled && - keyword == Settings.PathSearchActionKeyword, - Settings.ActionKeyword.FileContentSearchActionKeyword => Settings.FileContentSearchKeywordEnabled && - keyword == Settings.FileContentSearchActionKeyword, - Settings.ActionKeyword.IndexSearchActionKeyword => Settings.IndexSearchKeywordEnabled && - keyword == Settings.IndexSearchActionKeyword, - Settings.ActionKeyword.QuickAccessActionKeyword => Settings.QuickAccessKeywordEnabled && - keyword == Settings.QuickAccessActionKeyword, - _ => throw new ArgumentOutOfRangeException(nameof(allowedActionKeyword), allowedActionKeyword, "actionKeyword out of range") - }; + return [.. results]; } private List EverythingContentSearchResult(Query query) { - return new List() - { + return + [ new() { Title = Localize.flowlauncher_plugin_everything_enable_content_search(), @@ -176,7 +192,7 @@ private List EverythingContentSearchResult(Query query) return false; } } - }; + ]; } private async Task> PathSearchAsync(Query query, CancellationToken token = default) @@ -197,7 +213,7 @@ private async Task> PathSearchAsync(Query query, CancellationToken // Check that actual location exists, otherwise directory search will throw directory not found exception if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path).LocationExists()) - return results.ToList(); + return [.. results]; var useIndexSearch = Settings.IndexSearchEngine is Settings.IndexSearchEngineOption.WindowsIndex && UseWindowsIndexForDirectorySearch(path); @@ -209,7 +225,7 @@ private async Task> PathSearchAsync(Query query, CancellationToken : ResultManager.CreateOpenCurrentFolderResult(retrievedDirectoryPath, query.ActionKeyword, useIndexSearch)); if (token.IsCancellationRequested) - return new List(); + return [.. results]; IAsyncEnumerable directoryResult; @@ -231,7 +247,7 @@ private async Task> PathSearchAsync(Query query, CancellationToken } if (token.IsCancellationRequested) - return new List(); + return [.. results]; try { @@ -246,14 +262,14 @@ private async Task> PathSearchAsync(Query query, CancellationToken } - return results.ToList(); + return [.. results]; } public bool IsFileContentSearch(string actionKeyword) => actionKeyword == Settings.FileContentSearchActionKeyword; public static bool UseIndexSearch(string path) { - if (Main.Settings.IndexSearchEngine is not Settings.IndexSearchEngineOption.WindowsIndex) + if (Main.Settings.IndexSearchEngine is not IndexSearchEngineOption.WindowsIndex) return false; // Check if the path is using windows index search @@ -275,10 +291,67 @@ private bool UseWindowsIndexForDirectorySearch(string locationPath) private bool IsExcludedFile(SearchResult result) { - string[] excludedFileTypes = Settings.ExcludedFileTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + string[] excludedFileTypes = Settings.ExcludedFileTypes.Split([','], StringSplitOptions.RemoveEmptyEntries); string fileExtension = Path.GetExtension(result.FullPath).TrimStart('.'); return excludedFileTypes.Contains(fileExtension, StringComparer.OrdinalIgnoreCase); } + + private List GetQuickAccessResultsFilteredByActionKeyword(Query query, List actions) + { + if (!Settings.QuickAccessKeywordEnabled) + return []; + + var results = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks); + if (results.Count == 0) + return []; + + return results + .Where(r => r.ContextData is SearchResult result + && !IsResultTypeFilteredByActionKeyword(result.Type, actions)) + .ToList(); + } + private bool IsResultTypeFilteredByActionKeyword(ResultType type, List actions) + { + var actionsWithWhitelist = actions.Intersect(_allowedTypesByActionKeyword.Keys).ToList(); + if (actionsWithWhitelist.Count == 0) return false; + + // Check if ANY active keyword allows this type (union behavior) + foreach (var action in actionsWithWhitelist) + { + if (_allowedTypesByActionKeyword.TryGetValue(action, out var allowedTypes)) + { + if (allowedTypes.Contains(type)) + return false; + } + } + + return true; + } + + private bool CanUseIndexSearchByActionKeywords(Dictionary actions) + { + var keysToUseIndexSearch = new[] + { + ActionKeyword.FileSearchActionKeyword, ActionKeyword.FolderSearchActionKeyword, + ActionKeyword.IndexSearchActionKeyword, ActionKeyword.SearchActionKeyword + }; + + return keysToUseIndexSearch.Any(actions.ContainsKey); + } + + // Action keywords that supports patch search in results. + private bool CanUsePathSearchByActionKeywords(Dictionary actions) + { + var keysThatSupportPathSearch = new[] + { + ActionKeyword.PathSearchActionKeyword, + ActionKeyword.SearchActionKeyword, + }; + + return keysThatSupportPathSearch.Any(actions.ContainsKey); + + } + } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs index 8d62531cd62..e65c03e9be1 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -1,12 +1,13 @@ -using Flow.Launcher.Plugin.Explorer.Search; -using Flow.Launcher.Plugin.Explorer.Search.Everything; -using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; -using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; -using System; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Text.Json.Serialization; +using Flow.Launcher.Plugin.Explorer.Search; +using Flow.Launcher.Plugin.Explorer.Search.Everything; using Flow.Launcher.Plugin.Explorer.Search.IProvider; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; +using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; namespace Flow.Launcher.Plugin.Explorer { @@ -58,6 +59,15 @@ public class Settings public bool QuickAccessKeywordEnabled { get; set; } + + public string FolderSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign; + + public bool FolderSearchKeywordEnabled { get; set; } + + public string FileSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign; + + public bool FileSearchKeywordEnabled { get; set; } + public bool WarnWindowsSearchServiceOff { get; set; } = true; public bool ShowFileSizeInPreviewPanel { get; set; } = true; @@ -160,7 +170,9 @@ internal enum ActionKeyword PathSearchActionKeyword, FileContentSearchActionKeyword, IndexSearchActionKeyword, - QuickAccessActionKeyword + QuickAccessActionKeyword, + FolderSearchActionKeyword, + FileSearchActionKeyword, } internal string GetActionKeyword(ActionKeyword actionKeyword) => actionKeyword switch @@ -170,6 +182,8 @@ internal enum ActionKeyword ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword, ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword, ActionKeyword.QuickAccessActionKeyword => QuickAccessActionKeyword, + ActionKeyword.FolderSearchActionKeyword => FolderSearchActionKeyword, + ActionKeyword.FileSearchActionKeyword => FileSearchActionKeyword, _ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "ActionKeyWord property not found") }; @@ -180,6 +194,8 @@ internal enum ActionKeyword ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword = keyword, ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword = keyword, ActionKeyword.QuickAccessActionKeyword => QuickAccessActionKeyword = keyword, + ActionKeyword.FolderSearchActionKeyword => FolderSearchActionKeyword = keyword, + ActionKeyword.FileSearchActionKeyword => FileSearchActionKeyword = keyword, _ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "ActionKeyWord property not found") }; @@ -190,6 +206,8 @@ internal enum ActionKeyword ActionKeyword.IndexSearchActionKeyword => IndexSearchKeywordEnabled, ActionKeyword.FileContentSearchActionKeyword => FileContentSearchKeywordEnabled, ActionKeyword.QuickAccessActionKeyword => QuickAccessKeywordEnabled, + ActionKeyword.FolderSearchActionKeyword => FolderSearchKeywordEnabled, + ActionKeyword.FileSearchActionKeyword => FileSearchKeywordEnabled, _ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "ActionKeyword enabled status not defined") }; @@ -200,7 +218,27 @@ internal enum ActionKeyword ActionKeyword.IndexSearchActionKeyword => IndexSearchKeywordEnabled = enable, ActionKeyword.FileContentSearchActionKeyword => FileContentSearchKeywordEnabled = enable, ActionKeyword.QuickAccessActionKeyword => QuickAccessKeywordEnabled = enable, + ActionKeyword.FolderSearchActionKeyword => FolderSearchKeywordEnabled = enable, + ActionKeyword.FileSearchActionKeyword => FileSearchKeywordEnabled = enable, _ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "ActionKeyword enabled status not defined") }; + + // Returns a dictionary because some ActionKeywords may use wildcards (*), + // which means multiple ActionKeywords can be considered active at the same time. + // Using a dictionary ensures O(1) lookup time when checking which actions + // are enabled. + internal Dictionary GetActiveActionKeywords(string actionKeywordStr) + { + var result = new Dictionary(); + if (string.IsNullOrEmpty(actionKeywordStr)) return null; + foreach (var action in Enum.GetValues()) + { + var keywordStr = GetActionKeyword(action); + if (string.IsNullOrEmpty(keywordStr)) continue; + var isEnabled = GetActionKeywordEnabled(action); + if (keywordStr == actionKeywordStr && isEnabled) result.Add(action, keywordStr); + } + return result; + } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 2d46c6307cc..956c84db2c6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -279,7 +279,11 @@ private void InitializeActionKeywordModels() new(Settings.ActionKeyword.IndexSearchActionKeyword, "plugin_explorer_actionkeywordview_indexsearch"), new(Settings.ActionKeyword.QuickAccessActionKeyword, - "plugin_explorer_actionkeywordview_quickaccess") + "plugin_explorer_actionkeywordview_quickaccess"), + new(Settings.ActionKeyword.FolderSearchActionKeyword, + "plugin_explorer_actionkeywordview_foldersearch"), + new(Settings.ActionKeyword.FileSearchActionKeyword, + "plugin_explorer_actionkeywordview_filesearch") }; }