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..a37707ec76a 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -1,13 +1,15 @@ -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 System.Windows.Documents; 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 +20,13 @@ public class SearchManager internal Settings Settings; + private readonly Dictionary> _typesToFilterByActionKeyword = new() + { + { ActionKeyword.FileSearchActionKeyword, [ResultType.Folder, ResultType.Volume] }, + { ActionKeyword.FolderSearchActionKeyword, [ResultType.File] }, + }; + + public SearchManager(Settings settings, PluginInitContext context) { Context = context; @@ -31,7 +40,7 @@ public class PathEqualityComparator : IEqualityComparer { private static PathEqualityComparator instance; public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator(); - + public bool Equals(Result x, Result y) { return x.Title.Equals(y.Title, StringComparison.OrdinalIgnoreCase) @@ -48,44 +57,56 @@ internal async Task> SearchAsync(Query query, CancellationToken tok { 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 []; } - else + + var isPathSearch = query.Search.IsLocationPathString() + || EnvironmentVariables.IsEnvironmentVariableSearch(query.Search) + || EnvironmentVariables.HasEnvironmentVar(query.Search); + + 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 query is empty and active keyword is folder or search, return empty results. + if (queryIsEmpty && (activeActionKeywords.ContainsKey(ActionKeyword.FolderSearchActionKeyword) + || activeActionKeywords.ContainsKey(ActionKeyword.FileSearchActionKeyword))) + { + return []; + } - bool isPathSearch = query.Search.IsLocationPathString() - || EnvironmentVariables.IsEnvironmentVariableSearch(query.Search) - || EnvironmentVariables.HasEnvironmentVar(query.Search); + // When file search is active, do not include path search in the active keywords. + // This prevents unwanted PathSearch results (e.g., drives or raw volume paths). + if (isPathSearch && !activeActionKeywords.ContainsKey(ActionKeyword.PathSearchActionKeyword) + && !activeActionKeywords.ContainsKey(ActionKeyword.FileSearchActionKeyword)) + { + activeActionKeywords.Add(ActionKeyword.PathSearchActionKeyword, keyword); + } + + IAsyncEnumerable searchResults; string engineName; switch (isPathSearch) { case true - when ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) - || ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword): - + when activeActionKeywords.ContainsKey(ActionKeyword.PathSearchActionKeyword) + || activeActionKeywords.ContainsKey(ActionKeyword.SearchActionKeyword): 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 +114,37 @@ 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)); + } + //Merge Quick Access Link results for non-path searches. + results.UnionWith(GetQuickAccessResultsFilteredByActionKeyword(query, activeActionKeywords)); try { await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false)) - if (search.Type == ResultType.File && IsExcludedFile(search)) { + { + if (ShouldSkipResultByTypeAndActionKeyword(activeActionKeywords, search)) + { continue; - } else { - results.Add(ResultManager.CreateResult(query, search)); } + results.Add(ResultManager.CreateResult(query, search)); + } } catch (OperationCanceledException) { - return new List(); + return [.. results]; } catch (EngineNotAvailableException) { @@ -137,33 +158,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 +177,7 @@ private List EverythingContentSearchResult(Query query) return false; } } - }; + ]; } private async Task> PathSearchAsync(Query query, CancellationToken token = default) @@ -197,7 +198,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 +210,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 +232,7 @@ private async Task> PathSearchAsync(Query query, CancellationToken } if (token.IsCancellationRequested) - return new List(); + return [.. results]; try { @@ -246,14 +247,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 +276,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 bool ShouldSkipResultByTypeAndActionKeyword(Dictionary actions, SearchResult search) + { + // Is excluded file type + if (search.Type == ResultType.File && IsExcludedFile(search)) + { + return true; + } + return IsResultTypeFilteredByActionKeyword(search.Type, actions); + + } + + private List GetQuickAccessResultsFilteredByActionKeyword(Query query, Dictionary actions) + { + var results = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks) ?? []; + if (results.Count == 0) + { + return results; + } + + return results + .Where(r => r.ContextData is SearchResult result + && !IsResultTypeFilteredByActionKeyword(result.Type, actions)) + .ToList(); + } + private bool IsResultTypeFilteredByActionKeyword(ResultType type, Dictionary actions) + { + foreach (var action in actions.Keys) + { + if (_typesToFilterByActionKeyword.TryGetValue(action, out var typesToFilter)) + { + return typesToFilter.Contains(type); + } + } + + return false; + } + + private bool CanUseIndexSearchByActionKeywords(Dictionary actions) + { + List keysToUseSearch = + [ + ActionKeyword.FileSearchActionKeyword, + ActionKeyword.FolderSearchActionKeyword, + ActionKeyword.IndexSearchActionKeyword, + ActionKeyword.SearchActionKeyword + ]; + foreach (var key in keysToUseSearch) + { + var contains = actions.ContainsKey(key); + if (!contains) continue; + return contains; + } + + return false; + } + } } 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") }; }