Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<system:String x:Key="plugin_explorer_add">Add</system:String>
<system:String x:Key="plugin_explorer_generalsetting_header">General Setting</system:String>
<system:String x:Key="plugin_explorer_manageactionkeywords_header">Customise Action Keywords</system:String>
<system:String x:Key="plugin_explorer_exclude_quickaccess_from_actionkeywords">Exclude Quick Access results when using action keywords</system:String>
<system:String x:Key="plugin_explorer_manage_quick_access_links_header">Customise Quick Access</system:String>
<system:String x:Key="plugin_explorer_quickaccesslinks_header">Quick Access Links</system:String>
<system:String x:Key="plugin_explorer_everything_setting_header">Everything Setting</system:String>
Expand All @@ -58,6 +59,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>
Expand Down
160 changes: 86 additions & 74 deletions Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -47,45 +48,43 @@ public int GetHashCode(Result obj)
internal async Task<List<Result>> SearchAsync(Query query, CancellationToken token)
{
var results = new HashSet<Result>(PathEqualityComparator.Instance);
var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword;
var isPathSearch = query.Search.IsLocationPathString()
|| EnvironmentVariables.IsEnvironmentVariableSearch(query.Search)
|| EnvironmentVariables.HasEnvironmentVar(query.Search);

// 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 action keyword is enabled and matched, get the active action keyword.
var activeActionKeyword = Settings.GetActiveActionKeyword(keyword);

// No action keyword matched - plugin should not handle this query, return empty results.
if (activeActionKeyword == null && !isPathSearch)
{
if (string.IsNullOrEmpty(query.Search) && ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword))
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
return [];
}
else

// If no action keyword matched but the query is a path search, set active action keyword to path search.
if (activeActionKeyword == null && isPathSearch) activeActionKeyword = ActionKeyword.PathSearchActionKeyword;

// This allows the user to type the below action keywords and see/search the list of quick folder links
if (string.IsNullOrEmpty(query.Search)
&& activeActionKeyword.Equals(ActionKeyword.QuickAccessActionKeyword))
{
// No action keyword matched- plugin should not handle this query, return empty results.
return new List<Result>();
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
}

IAsyncEnumerable<SearchResult> searchResults;

bool isPathSearch = query.Search.IsLocationPathString()
|| EnvironmentVariables.IsEnvironmentVariableSearch(query.Search)
|| EnvironmentVariables.HasEnvironmentVar(query.Search);

string engineName;

switch (isPathSearch)
switch (activeActionKeyword.Equals(ActionKeyword.PathSearchActionKeyword))
{
Comment on lines +65 to 80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Path search regression when wildcard keyword is active

Because GetActiveActionKeyword returns ActionKeyword.SearchActionKeyword for the default *, Line 66 never flips activeActionKeyword to PathSearchActionKeyword for plain path queries (e.g., typing C:\ or %appdata%). The path branch is therefore skipped and we fall into the index-search path, which no longer enumerates drives/folders or expands environment variables — a blocking regression of core Explorer behavior. Please force the active keyword over to path search whenever the query is classified as a path (while still allowing explicit non-search keywords to win).

Apply this diff to restore the path-search flow:

-            if (activeActionKeyword == null && isPathSearch) activeActionKeyword = ActionKeyword.PathSearchActionKeyword;
+            if (isPathSearch && (activeActionKeyword is null || activeActionKeyword == ActionKeyword.SearchActionKeyword))
+            {
+                activeActionKeyword = ActionKeyword.PathSearchActionKeyword;
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If no action keyword matched but the query is a path search, set active action keyword to path search.
if (activeActionKeyword == null && isPathSearch) activeActionKeyword = ActionKeyword.PathSearchActionKeyword;
// This allows the user to type the below action keywords and see/search the list of quick folder links
if (string.IsNullOrEmpty(query.Search)
&& activeActionKeyword.Equals(ActionKeyword.QuickAccessActionKeyword))
{
// No action keyword matched- plugin should not handle this query, return empty results.
return new List<Result>();
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
}
IAsyncEnumerable<SearchResult> searchResults;
bool isPathSearch = query.Search.IsLocationPathString()
|| EnvironmentVariables.IsEnvironmentVariableSearch(query.Search)
|| EnvironmentVariables.HasEnvironmentVar(query.Search);
string engineName;
switch (isPathSearch)
switch (activeActionKeyword.Equals(ActionKeyword.PathSearchActionKeyword))
{
// If no action keyword matched but the query is a path search, set active action keyword to path search.
if (isPathSearch && (activeActionKeyword is null || activeActionKeyword == ActionKeyword.SearchActionKeyword))
{
activeActionKeyword = ActionKeyword.PathSearchActionKeyword;
}
// This allows the user to type the below action keywords and see/search the list of quick folder links
if (string.IsNullOrEmpty(query.Search)
&& activeActionKeyword.Equals(ActionKeyword.QuickAccessActionKeyword))
{
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
}
IAsyncEnumerable<SearchResult> searchResults;
string engineName;
switch (activeActionKeyword.Equals(ActionKeyword.PathSearchActionKeyword))
{
🤖 Prompt for AI Agents
In Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs around lines 65
to 80, the code only switches activeActionKeyword to PathSearchActionKeyword
when activeActionKeyword is null, which misses the case where
GetActiveActionKeyword returns the default SearchActionKeyword (wildcard '*')
for plain path queries; change the condition so that if isPathSearch is true and
activeActionKeyword is either null or equals ActionKeyword.SearchActionKeyword,
set activeActionKeyword = ActionKeyword.PathSearchActionKeyword, leaving any
other explicit non-search keywords untouched.

case true
when ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword):

case true:
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 activeActionKeyword.Equals(ActionKeyword.FileContentSearchActionKeyword):
if (Settings.ContentIndexProvider is EverythingSearchManager && !Settings.EnableEverythingContentSearch)
return EverythingContentSearchResult(query);

Expand All @@ -94,36 +93,31 @@ when ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKey
break;

case false
when ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword):
when activeActionKeyword.Equals(ActionKeyword.QuickAccessActionKeyword):
return QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);

default:
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();
}

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

MergeQuickAccessInResultsIfQueryMatch(results, query, activeActionKeyword);
try
{
await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false))
if (search.Type == ResultType.File && IsExcludedFile(search)) {
{
if (ShouldSkip(activeActionKeyword!.Value, search))
{
continue;
} else {
results.Add(ResultManager.CreateResult(query, search));
}
results.Add(ResultManager.CreateResult(query, search));
}
}
catch (OperationCanceledException)
{
return new List<Result>();
return [];
}
catch (EngineNotAvailableException)
{
Expand All @@ -137,33 +131,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<Result> EverythingContentSearchResult(Query query)
{
return new List<Result>()
{
return
[
new()
{
Title = Localize.flowlauncher_plugin_everything_enable_content_search(),
Expand All @@ -176,7 +150,7 @@ private List<Result> EverythingContentSearchResult(Query query)
return false;
}
}
};
];
}

private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
Expand All @@ -197,7 +171,7 @@ private async Task<List<Result>> 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);
Expand All @@ -209,7 +183,7 @@ private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken
: ResultManager.CreateOpenCurrentFolderResult(retrievedDirectoryPath, query.ActionKeyword, useIndexSearch));

if (token.IsCancellationRequested)
return new List<Result>();
return [];

IAsyncEnumerable<SearchResult> directoryResult;

Expand All @@ -231,7 +205,7 @@ private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken
}

if (token.IsCancellationRequested)
return new List<Result>();
return [];

try
{
Expand All @@ -246,14 +220,14 @@ private async Task<List<Result>> 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
Expand All @@ -275,10 +249,48 @@ 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 ShouldSkip(ActionKeyword actionKeywordActive, SearchResult search)
{
// Is excluded file type
if (search.Type == ResultType.File && IsExcludedFile(search))
{
return true;
}

// Action keyword specific filtering for folders
if (actionKeywordActive.Equals(ActionKeyword.FolderSearchActionKeyword)
&& search.Type != ResultType.Folder)
{
return true;
}

// Action keyword specific filtering for files
if (actionKeywordActive.Equals(ActionKeyword.FileSearchActionKeyword)
&& search.Type != ResultType.File)
{
return true;
}

return false;
}

private void MergeQuickAccessInResultsIfQueryMatch(HashSet<Result> results, Query query, ActionKeyword? activeActionKeyword)
{
if (activeActionKeyword != null
&& activeActionKeyword != ActionKeyword.QuickAccessActionKeyword
&& Settings.ExcludeQuickAccessFromActionKeywords)
{
return;
}

var quickAccessMatched = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
if (quickAccessMatched != null && quickAccessMatched.Count > 0) results.UnionWith(quickAccessMatched);
}
}
}
49 changes: 42 additions & 7 deletions Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -58,6 +59,17 @@ 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 ExcludeQuickAccessFromActionKeywords { get; set; } = false;

public bool WarnWindowsSearchServiceOff { get; set; } = true;

public bool ShowFileSizeInPreviewPanel { get; set; } = true;
Expand Down Expand Up @@ -154,13 +166,15 @@ public enum ContentIndexSearchEngineOption

#endregion

internal enum ActionKeyword
public enum ActionKeyword
{
SearchActionKeyword,
PathSearchActionKeyword,
FileContentSearchActionKeyword,
IndexSearchActionKeyword,
QuickAccessActionKeyword
QuickAccessActionKeyword,
FolderSearchActionKeyword,
FileSearchActionKeyword,
}

internal string GetActionKeyword(ActionKeyword actionKeyword) => actionKeyword switch
Expand All @@ -170,6 +184,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")
};

Expand All @@ -180,6 +196,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")
};

Expand All @@ -190,6 +208,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")
};

Expand All @@ -200,7 +220,22 @@ 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")
};

public ActionKeyword? GetActiveActionKeyword(string actionKeywordStr)
{
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) return action;
}
return null;
}
}
}
Loading
Loading