[FEATURE] Folder and File Action Keywords (#4093)

This commit is contained in:
Diego Henrique 2025-12-14 07:18:21 -03:00 committed by GitHub
parent ebc1a85190
commit 9066913710
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 197 additions and 80 deletions

View file

@ -58,6 +58,8 @@
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">File Content Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_indexsearch">Index Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_quickaccess">Quick Access:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_foldersearch">Folder Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_filesearch">File Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_current">Current Action Keyword</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_done">Done</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_enabled">Enabled</system:String>

View file

@ -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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal Settings Settings;
private readonly Dictionary<ActionKeyword, ResultType[]> _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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}
}
/// <summary>
/// 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
/// </summary>
internal async Task<List<Result>> SearchAsync(Query query, CancellationToken token)
{
var results = new HashSet<Result>(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);
}
else
{
// No action keyword matched- plugin should not handle this query, return empty results.
return new List<Result>();
return [.. results];
}
IAsyncEnumerable<SearchResult> searchResults;
var queryIsEmpty = string.IsNullOrEmpty(query.Search);
if (queryIsEmpty && activeActionKeywords.ContainsKey(ActionKeyword.QuickAccessActionKeyword))
{
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
}
bool isPathSearch = query.Search.IsLocationPathString()
if (queryIsEmpty)
{
return [.. results];
}
var isPathSearch = query.Search.IsLocationPathString()
|| EnvironmentVariables.IsEnvironmentVariableSearch(query.Search)
|| EnvironmentVariables.HasEnvironmentVar(query.Search);
IAsyncEnumerable<SearchResult> 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
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<Result>();
return [.. results];
}
catch (EngineNotAvailableException)
{
@ -137,33 +173,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search
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<Result> EverythingContentSearchResult(Query query)
{
return new List<Result>()
{
return
[
new()
{
Title = Localize.flowlauncher_plugin_everything_enable_content_search(),
@ -176,7 +192,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return false;
}
}
};
];
}
private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
@ -197,7 +213,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
// 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
: ResultManager.CreateOpenCurrentFolderResult(retrievedDirectoryPath, query.ActionKeyword, useIndexSearch));
if (token.IsCancellationRequested)
return new List<Result>();
return [.. results];
IAsyncEnumerable<SearchResult> directoryResult;
@ -231,7 +247,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}
if (token.IsCancellationRequested)
return new List<Result>();
return [.. results];
try
{
@ -246,14 +262,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}
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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
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<Result> GetQuickAccessResultsFilteredByActionKeyword(Query query, List<ActionKeyword> 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<ActionKeyword> 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<ActionKeyword, string> 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<ActionKeyword, string> actions)
{
var keysThatSupportPathSearch = new[]
{
ActionKeyword.PathSearchActionKeyword,
ActionKeyword.SearchActionKeyword,
};
return keysThatSupportPathSearch.Any(actions.ContainsKey);
}
}
}

View file

@ -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 @@ namespace Flow.Launcher.Plugin.Explorer
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 @@ namespace Flow.Launcher.Plugin.Explorer
PathSearchActionKeyword,
FileContentSearchActionKeyword,
IndexSearchActionKeyword,
QuickAccessActionKeyword
QuickAccessActionKeyword,
FolderSearchActionKeyword,
FileSearchActionKeyword,
}
internal string GetActionKeyword(ActionKeyword actionKeyword) => actionKeyword switch
@ -170,6 +182,8 @@ namespace Flow.Launcher.Plugin.Explorer
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 @@ namespace Flow.Launcher.Plugin.Explorer
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 @@ namespace Flow.Launcher.Plugin.Explorer
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 @@ namespace Flow.Launcher.Plugin.Explorer
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<ActionKeyword, string> GetActiveActionKeywords(string actionKeywordStr)
{
var result = new Dictionary<ActionKeyword, string>();
if (string.IsNullOrEmpty(actionKeywordStr)) return null;
foreach (var action in Enum.GetValues<ActionKeyword>())
{
var keywordStr = GetActionKeyword(action);
if (string.IsNullOrEmpty(keywordStr)) continue;
var isEnabled = GetActionKeywordEnabled(action);
if (keywordStr == actionKeywordStr && isEnabled) result.Add(action, keywordStr);
}
return result;
}
}
}

View file

@ -279,7 +279,11 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
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")
};
}