Skip to content

Commit 9066913

Browse files
authored
[FEATURE] Folder and File Action Keywords (#4093)
1 parent ebc1a85 commit 9066913

File tree

4 files changed

+195
-78
lines changed

4 files changed

+195
-78
lines changed

Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">File Content Search:</system:String>
5959
<system:String x:Key="plugin_explorer_actionkeywordview_indexsearch">Index Search:</system:String>
6060
<system:String x:Key="plugin_explorer_actionkeywordview_quickaccess">Quick Access:</system:String>
61+
<system:String x:Key="plugin_explorer_actionkeywordview_foldersearch">Folder Search:</system:String>
62+
<system:String x:Key="plugin_explorer_actionkeywordview_filesearch">File Search:</system:String>
6163
<system:String x:Key="plugin_explorer_actionkeyword_current">Current Action Keyword</system:String>
6264
<system:String x:Key="plugin_explorer_actionkeyword_done">Done</system:String>
6365
<system:String x:Key="plugin_explorer_actionkeyword_enabled">Enabled</system:String>

Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs

Lines changed: 144 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
2-
using Flow.Launcher.Plugin.Explorer.Search.Everything;
3-
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
4-
using Flow.Launcher.Plugin.SharedCommands;
5-
using System;
1+
using System;
62
using System.Collections.Generic;
73
using System.Linq;
84
using System.Threading;
95
using System.Threading.Tasks;
106
using Flow.Launcher.Plugin.Explorer.Exceptions;
7+
using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
8+
using Flow.Launcher.Plugin.Explorer.Search.Everything;
9+
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
10+
using Flow.Launcher.Plugin.SharedCommands;
11+
using static Flow.Launcher.Plugin.Explorer.Settings;
1112
using Path = System.IO.Path;
1213

1314
namespace Flow.Launcher.Plugin.Explorer.Search
@@ -18,6 +19,14 @@ public class SearchManager
1819

1920
internal Settings Settings;
2021

22+
private readonly Dictionary<ActionKeyword, ResultType[]> _allowedTypesByActionKeyword = new()
23+
{
24+
{ ActionKeyword.FileSearchActionKeyword, [ResultType.File] },
25+
{ ActionKeyword.FolderSearchActionKeyword, [ResultType.Folder, ResultType.Volume] },
26+
{ ActionKeyword.SearchActionKeyword, [ResultType.File, ResultType.Folder, ResultType.Volume] },
27+
};
28+
29+
2130
public SearchManager(Settings settings, PluginInitContext context)
2231
{
2332
Context = context;
@@ -44,86 +53,113 @@ public int GetHashCode(Result obj)
4453
}
4554
}
4655

56+
/// <summary>
57+
/// Results for the different types of searches as follows:
58+
/// 1. Search, only include results from:
59+
/// - Files
60+
/// - Folders
61+
/// - Quick Access Links
62+
/// - Path navigation
63+
/// 2. File Content Search, only include results from:
64+
/// - File contents from index search engines i.e. Windows Index, Everything (may not be available due its beta version)
65+
/// 3. Path Search, only include results from:
66+
/// - Path navigation
67+
/// 4. Quick Access Links, only include results from:
68+
/// - Full list of Quick Access Links if query is empty
69+
/// - Matched Quick Access Links if query is not empty
70+
/// - 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),
71+
/// i.e. result with path c:\windows\system32
72+
/// 5. Folder Search, only include results from:
73+
/// - Folders
74+
/// - Quick Access Links
75+
/// 6. File Search, only include results from:
76+
/// - Files
77+
/// - Quick Access Links
78+
/// </summary>
4779
internal async Task<List<Result>> SearchAsync(Query query, CancellationToken token)
4880
{
4981
var results = new HashSet<Result>(PathEqualityComparator.Instance);
5082

51-
// This allows the user to type the below action keywords and see/search the list of quick folder links
52-
if (ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)
53-
|| ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword)
54-
|| ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword)
55-
|| ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword)
56-
|| ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword))
83+
var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword;
84+
85+
// No action keyword matched - plugin should not handle this query, return empty results.
86+
var activeActionKeywords = Settings.GetActiveActionKeywords(keyword);
87+
if (activeActionKeywords.Count == 0)
5788
{
58-
if (string.IsNullOrEmpty(query.Search) && ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword))
59-
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
89+
return [.. results];
6090
}
61-
else
91+
92+
var queryIsEmpty = string.IsNullOrEmpty(query.Search);
93+
if (queryIsEmpty && activeActionKeywords.ContainsKey(ActionKeyword.QuickAccessActionKeyword))
6294
{
63-
// No action keyword matched- plugin should not handle this query, return empty results.
64-
return new List<Result>();
95+
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
6596
}
6697

67-
IAsyncEnumerable<SearchResult> searchResults;
98+
if (queryIsEmpty)
99+
{
100+
return [.. results];
101+
}
68102

69-
bool isPathSearch = query.Search.IsLocationPathString()
103+
var isPathSearch = query.Search.IsLocationPathString()
70104
|| EnvironmentVariables.IsEnvironmentVariableSearch(query.Search)
71105
|| EnvironmentVariables.HasEnvironmentVar(query.Search);
72106

107+
IAsyncEnumerable<SearchResult> searchResults;
108+
73109
string engineName;
74110

75111
switch (isPathSearch)
76112
{
77113
case true
78-
when ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword)
79-
|| ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword):
80-
114+
when CanUsePathSearchByActionKeywords(activeActionKeywords):
81115
results.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false));
82-
83-
return results.ToList();
116+
return [.. results];
84117

85118
case false
86-
when ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword):
87-
88119
// Intentionally require enabling of Everything's content search due to its slowness
120+
when activeActionKeywords.ContainsKey(ActionKeyword.FileContentSearchActionKeyword):
89121
if (Settings.ContentIndexProvider is EverythingSearchManager && !Settings.EnableEverythingContentSearch)
90122
return EverythingContentSearchResult(query);
91123

92124
searchResults = Settings.ContentIndexProvider.ContentSearchAsync("", query.Search, token);
93125
engineName = Enum.GetName(Settings.ContentSearchEngine);
94126
break;
95127

96-
case false
97-
when ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword)
98-
|| ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword):
128+
case true or false
129+
when activeActionKeywords.ContainsKey(ActionKeyword.QuickAccessActionKeyword):
130+
return QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
131+
99132

133+
case false
134+
when CanUseIndexSearchByActionKeywords(activeActionKeywords):
100135
searchResults = Settings.IndexProvider.SearchAsync(query.Search, token);
101136
engineName = Enum.GetName(Settings.IndexSearchEngine);
102137
break;
103-
104-
case true or false
105-
when ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword):
106-
return QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
107-
108138
default:
109-
return results.ToList();
139+
return [.. results];
140+
110141
}
111142

112-
// Merge Quick Access Link results for non-path searches.
113-
results.UnionWith(QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks));
114143

144+
var actions = activeActionKeywords.Keys.ToList();
145+
//Merge Quick Access Link results for non-path searches.
146+
results.UnionWith(GetQuickAccessResultsFilteredByActionKeyword(query, actions));
115147
try
116148
{
117149
await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false))
118-
if (search.Type == ResultType.File && IsExcludedFile(search)) {
150+
{
151+
if (search.Type == ResultType.File && IsExcludedFile(search))
119152
continue;
120-
} else {
121-
results.Add(ResultManager.CreateResult(query, search));
122-
}
153+
154+
if (IsResultTypeFilteredByActionKeyword(search.Type, actions))
155+
continue;
156+
157+
results.Add(ResultManager.CreateResult(query, search));
158+
}
123159
}
124160
catch (OperationCanceledException)
125161
{
126-
return new List<Result>();
162+
return [.. results];
127163
}
128164
catch (EngineNotAvailableException)
129165
{
@@ -137,33 +173,13 @@ when ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword):
137173
results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any(
138174
excludedPath => FilesFolders.PathContains(excludedPath.Path, r.SubTitle, allowEqual: true)));
139175

140-
return results.ToList();
141-
}
142-
143-
private bool ActionKeywordMatch(Query query, Settings.ActionKeyword allowedActionKeyword)
144-
{
145-
var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword;
146-
147-
return allowedActionKeyword switch
148-
{
149-
Settings.ActionKeyword.SearchActionKeyword => Settings.SearchActionKeywordEnabled &&
150-
keyword == Settings.SearchActionKeyword,
151-
Settings.ActionKeyword.PathSearchActionKeyword => Settings.PathSearchKeywordEnabled &&
152-
keyword == Settings.PathSearchActionKeyword,
153-
Settings.ActionKeyword.FileContentSearchActionKeyword => Settings.FileContentSearchKeywordEnabled &&
154-
keyword == Settings.FileContentSearchActionKeyword,
155-
Settings.ActionKeyword.IndexSearchActionKeyword => Settings.IndexSearchKeywordEnabled &&
156-
keyword == Settings.IndexSearchActionKeyword,
157-
Settings.ActionKeyword.QuickAccessActionKeyword => Settings.QuickAccessKeywordEnabled &&
158-
keyword == Settings.QuickAccessActionKeyword,
159-
_ => throw new ArgumentOutOfRangeException(nameof(allowedActionKeyword), allowedActionKeyword, "actionKeyword out of range")
160-
};
176+
return [.. results];
161177
}
162178

163179
private List<Result> EverythingContentSearchResult(Query query)
164180
{
165-
return new List<Result>()
166-
{
181+
return
182+
[
167183
new()
168184
{
169185
Title = Localize.flowlauncher_plugin_everything_enable_content_search(),
@@ -176,7 +192,7 @@ private List<Result> EverythingContentSearchResult(Query query)
176192
return false;
177193
}
178194
}
179-
};
195+
];
180196
}
181197

182198
private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
@@ -197,7 +213,7 @@ private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken
197213

198214
// Check that actual location exists, otherwise directory search will throw directory not found exception
199215
if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path).LocationExists())
200-
return results.ToList();
216+
return [.. results];
201217

202218
var useIndexSearch = Settings.IndexSearchEngine is Settings.IndexSearchEngineOption.WindowsIndex
203219
&& UseWindowsIndexForDirectorySearch(path);
@@ -209,7 +225,7 @@ private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken
209225
: ResultManager.CreateOpenCurrentFolderResult(retrievedDirectoryPath, query.ActionKeyword, useIndexSearch));
210226

211227
if (token.IsCancellationRequested)
212-
return new List<Result>();
228+
return [.. results];
213229

214230
IAsyncEnumerable<SearchResult> directoryResult;
215231

@@ -231,7 +247,7 @@ private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken
231247
}
232248

233249
if (token.IsCancellationRequested)
234-
return new List<Result>();
250+
return [.. results];
235251

236252
try
237253
{
@@ -246,14 +262,14 @@ private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken
246262
}
247263

248264

249-
return results.ToList();
265+
return [.. results];
250266
}
251267

252268
public bool IsFileContentSearch(string actionKeyword) => actionKeyword == Settings.FileContentSearchActionKeyword;
253269

254270
public static bool UseIndexSearch(string path)
255271
{
256-
if (Main.Settings.IndexSearchEngine is not Settings.IndexSearchEngineOption.WindowsIndex)
272+
if (Main.Settings.IndexSearchEngine is not IndexSearchEngineOption.WindowsIndex)
257273
return false;
258274

259275
// Check if the path is using windows index search
@@ -275,10 +291,67 @@ private bool UseWindowsIndexForDirectorySearch(string locationPath)
275291

276292
private bool IsExcludedFile(SearchResult result)
277293
{
278-
string[] excludedFileTypes = Settings.ExcludedFileTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
294+
string[] excludedFileTypes = Settings.ExcludedFileTypes.Split([','], StringSplitOptions.RemoveEmptyEntries);
279295
string fileExtension = Path.GetExtension(result.FullPath).TrimStart('.');
280296

281297
return excludedFileTypes.Contains(fileExtension, StringComparer.OrdinalIgnoreCase);
282298
}
299+
300+
private List<Result> GetQuickAccessResultsFilteredByActionKeyword(Query query, List<ActionKeyword> actions)
301+
{
302+
if (!Settings.QuickAccessKeywordEnabled)
303+
return [];
304+
305+
var results = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
306+
if (results.Count == 0)
307+
return [];
308+
309+
return results
310+
.Where(r => r.ContextData is SearchResult result
311+
&& !IsResultTypeFilteredByActionKeyword(result.Type, actions))
312+
.ToList();
313+
}
314+
private bool IsResultTypeFilteredByActionKeyword(ResultType type, List<ActionKeyword> actions)
315+
{
316+
var actionsWithWhitelist = actions.Intersect(_allowedTypesByActionKeyword.Keys).ToList();
317+
if (actionsWithWhitelist.Count == 0) return false;
318+
319+
// Check if ANY active keyword allows this type (union behavior)
320+
foreach (var action in actionsWithWhitelist)
321+
{
322+
if (_allowedTypesByActionKeyword.TryGetValue(action, out var allowedTypes))
323+
{
324+
if (allowedTypes.Contains(type))
325+
return false;
326+
}
327+
}
328+
329+
return true;
330+
}
331+
332+
private bool CanUseIndexSearchByActionKeywords(Dictionary<ActionKeyword, string> actions)
333+
{
334+
var keysToUseIndexSearch = new[]
335+
{
336+
ActionKeyword.FileSearchActionKeyword, ActionKeyword.FolderSearchActionKeyword,
337+
ActionKeyword.IndexSearchActionKeyword, ActionKeyword.SearchActionKeyword
338+
};
339+
340+
return keysToUseIndexSearch.Any(actions.ContainsKey);
341+
}
342+
343+
// Action keywords that supports patch search in results.
344+
private bool CanUsePathSearchByActionKeywords(Dictionary<ActionKeyword, string> actions)
345+
{
346+
var keysThatSupportPathSearch = new[]
347+
{
348+
ActionKeyword.PathSearchActionKeyword,
349+
ActionKeyword.SearchActionKeyword,
350+
};
351+
352+
return keysThatSupportPathSearch.Any(actions.ContainsKey);
353+
354+
}
355+
283356
}
284357
}

0 commit comments

Comments
 (0)