Filter file extenstions in Windows Index and Everything

This commit is contained in:
VictoriousRaptor 2026-01-02 00:29:37 +08:00
parent db9be228c1
commit 6a454f9e58
7 changed files with 149 additions and 40 deletions

View file

@ -39,8 +39,8 @@ namespace Flow.Launcher.Test.Plugins
}
[SupportedOSPlatform("windows7.0")]
[TestCase("C:\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
[TestCase("C:\\SomeFolder\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
[TestCase("C:\\", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND directory='file:C:\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
[TestCase("C:\\SomeFolder\\", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND directory='file:C:\\SomeFolder\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
{
// Given
@ -56,8 +56,7 @@ namespace Flow.Launcher.Test.Plugins
}
[SupportedOSPlatform("windows7.0")]
[TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" +
" FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" +
[TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND directory='file:C:\\SomeFolder'" +
" AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
$" ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
@ -87,8 +86,8 @@ namespace Flow.Launcher.Test.Plugins
[SupportedOSPlatform("windows7.0")]
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
$"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
[TestCase("", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
$"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033) RANK BY COERCION(ABSOLUTE, 1000)) AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
[TestCase("", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND (scope='file:') ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
@ -104,7 +103,8 @@ namespace Flow.Launcher.Test.Plugins
var resultString = queryConstructor.FilesAndFolders(userSearchString);
// Then
ClassicAssert.AreEqual(expectedString, resultString);
ClassicAssert.AreEqual(expectedString, resultString, $"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
}
[SupportedOSPlatform("windows7.0")]
@ -125,8 +125,7 @@ namespace Flow.Launcher.Test.Plugins
}
[SupportedOSPlatform("windows7.0")]
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
$"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
[TestCase("some words", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND FREETEXT('some words') AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{

View file

@ -36,6 +36,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal const string WindowsIndexingOptions = "srchadmin.dll";
internal const string ExcludedFileTypesSeparator = ",";
internal static string ExplorerIconImageFullPath
=> Directory.GetParent(Assembly.GetExecutingAssembly().Location.ToString()) + "\\" + ExplorerIconImagePath;
}

View file

@ -80,7 +80,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
if (token.IsCancellationRequested)
yield break;
var searchKeyword = BuildSearchKeywordWithTypeFilter(search, allowedResultTypes);
var searchKeyword = BuildSearchKeyword(search, allowedResultTypes);
var option = new EverythingSearchOption(searchKeyword,
Settings.SortOption,
@ -92,16 +92,35 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
yield return result;
}
private static string BuildSearchKeywordWithTypeFilter(string search, IEnumerable<ResultType> allowedResultTypes)
private string BuildSearchKeyword(string search, IEnumerable<ResultType> allowedResultTypes)
{
var filters = new List<string>();
var typeFilter = BuildTypeFilter(allowedResultTypes);
if (!string.IsNullOrEmpty(typeFilter))
filters.Add(typeFilter);
var extensionFilter = BuildExtensionExclusionFilter();
if (!string.IsNullOrEmpty(extensionFilter))
filters.Add(extensionFilter);
if (filters.Count == 0)
return search;
var combinedFilters = string.Join(" ", filters);
return string.IsNullOrEmpty(search) ? combinedFilters : $"{combinedFilters} {search}";
}
private static string BuildTypeFilter(IEnumerable<ResultType> allowedResultTypes)
{
if (allowedResultTypes == null)
return search;
return "";
var hasFile = allowedResultTypes.Contains(ResultType.File);
var hasFolder = allowedResultTypes.Contains(ResultType.Folder);
var hasVolume = allowedResultTypes.Contains(ResultType.Volume);
var filter = (hasFile, hasFolder, hasVolume) switch
return (hasFile, hasFolder, hasVolume) switch
{
(true, false, false) => "file:",
(false, true, false) => "folder:",
@ -109,10 +128,23 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
(true, true, false) => "<file:|folder:>",
(true, false, true) => "<file:|volume:>",
(false, true, true) => "<folder:|volume:>",
_ => null // No filtering needed when all allowed or unspecified
_ => "" // No filtering needed when all allowed or unspecified
};
}
return filter == null ? search : $"{filter} {search}";
private string BuildExtensionExclusionFilter()
{
// Split extensions, remove whitespace, and add dot prefix
var extensions = Settings.ExcludedFileTypeList
.Where(ext => !string.IsNullOrWhiteSpace(ext))
.Select(ext => $"!*.{ext}")
.ToArray();
if (extensions.Length == 0)
return "";
// Everything syntax: !*.ext1 !*.ext2 to exclude these extensions
return string.Join(" ", extensions);
}
public async IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearch, string contentSearch,
@ -137,7 +169,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
if (token.IsCancellationRequested)
yield break;
var option = new EverythingSearchOption(plainSearch,
// Apply excluded file types in content search
var searchKeyword = BuildSearchKeyword(plainSearch, new[] { ResultType.File });
var option = new EverythingSearchOption(searchKeyword,
Settings.SortOption,
IsContentSearch: true,
ContentSearchKeyword: contentSearch,
@ -158,7 +193,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
if (token.IsCancellationRequested)
yield break;
var option = new EverythingSearchOption(search,
// Apply excluded file types in path enumeration
var searchKeyword = BuildSearchKeyword(search, null);
var option = new EverythingSearchOption(searchKeyword,
Settings.SortOption,
ParentPath: path,
IsRecursive: recursive,

View file

@ -148,12 +148,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false))
{
// TODO exclude in quick access
if (search.Type == ResultType.File && IsExcludedFile(search))
continue;
// TODO: Optimize filtering by action keyword at the provider level to reduce unnecessary searches.
// 1. Path search and content search may not need filtering as they are specific enough.
// 2. Index search can be optimized by passing allowed result types to the provider to limit the search scope.
// 3. Quick access link filtering is already handled separately.
// 3. Filter in quick access
//
if (IsResultTypeFilteredByActionKeyword(search.Type, actions))
continue;
@ -199,6 +198,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search
];
}
/// <summary>
/// Path search logic. Don't apply filtering by file extensions as it's like ls command.
/// </summary>
/// <param name="query"></param>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="SearchException"></exception>
private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
{
var querySearch = query.Search;
@ -295,7 +301,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
private bool IsExcludedFile(SearchResult result)
{
string[] excludedFileTypes = Settings.ExcludedFileTypes.Split([','], StringSplitOptions.RemoveEmptyEntries);
// TODO may remove this function
string[] excludedFileTypes = Settings.ExcludedFileTypes.Split([Constants.ExcludedFileTypesSeparator], StringSplitOptions.RemoveEmptyEntries);
string fileExtension = Path.GetExtension(result.FullPath).TrimStart('.');
return excludedFileTypes.Contains(fileExtension, StringComparer.OrdinalIgnoreCase);

View file

@ -34,7 +34,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
baseQuery.QueryContentProperties = "System.FileName";
// Set sorting order
//baseQuery.QuerySorting = "System.ItemType DESC";
baseQuery.QuerySorting = OrderIdentifier;
return baseQuery;
}
@ -62,15 +62,15 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
///</summary>
public string Directory(ReadOnlySpan<char> path, ReadOnlySpan<char> searchString = default, bool recursive = false)
{
var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND (System.FileName LIKE '{searchString}%' OR CONTAINS(System.FileName,'\"{searchString}*\"'))";
var queryConstraint = searchString.IsWhiteSpace() ? "" : $" AND (System.FileName LIKE '{searchString}%' OR CONTAINS(System.FileName,'\"{searchString}*\"'))";
var scopeConstraint = recursive
? RecursiveDirectoryConstraint(path)
: TopLevelDirectoryConstraint(path);
var query = $"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {OrderIdentifier}";
return query;
var baseQueryHelper = CreateBaseQuery();
baseQueryHelper.QueryWhereRestrictions = $"AND {scopeConstraint}{queryConstraint}";
return baseQueryHelper.GenerateSQLFromUserQuery("*");
}
///<summary>
@ -84,17 +84,22 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
// Remove any special characters that might cause issues with the query
var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString);
// Build the type filter constraint
var typeFilterConstraint = BuildTypeFilterConstraint(allowedResultTypes);
var constraints = new List<string>
{
RestrictionsForAllFilesAndFoldersSearch
};
// Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
var baseQuery = $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch}";
// Append type filter if present
if (!string.IsNullOrEmpty(typeFilterConstraint))
baseQuery += $" AND {typeFilterConstraint}";
return $"{baseQuery} ORDER BY {OrderIdentifier}";
var typeConstraint = BuildTypeFilterConstraint(allowedResultTypes);
if (!string.IsNullOrEmpty(typeConstraint))
constraints.Add(typeConstraint);
var extensionConstraint = BuildExtensionExclusionConstraint();
if (!string.IsNullOrEmpty(extensionConstraint))
constraints.Add(extensionConstraint);
var queryHelper = CreateBaseQuery();
queryHelper.QueryWhereRestrictions = $"AND {string.Join(" AND ", constraints)}";
return queryHelper.GenerateSQLFromUserQuery(replacedSearchString);
}
/// <summary>
@ -128,6 +133,22 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return null;
}
/// <summary>
/// Build WHERE clause constraint to exclude specific file extensions.
/// </summary>
/// <param name="excludedFileTypes">Comma or semicolon separated file extensions without dots (e.g., "queryHelper,log,bak")</param>
private string BuildExtensionExclusionConstraint()
{
var extensions = Settings.ExcludedFileTypeList
.Select(ext => $"System.FileExtension NOT LIKE '.{ext}'")
.ToArray();
if (extensions.Length == 0)
return "";
return string.Join(" AND ", extensions);
}
/// <summary>
/// If one special character have white space on one side, replace it with one white space.
/// So command will not have "[special character]+*" which will cause OLEDB exception.
@ -174,10 +195,19 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
///</summary>
public string FileContent(ReadOnlySpan<char> userSearchString)
{
string query =
$"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
var constraints = new List<string>
{
RestrictionsForFileContentSearch(userSearchString),
RestrictionsForAllFilesAndFoldersSearch
};
return query;
var extensionConstraint = BuildExtensionExclusionConstraint();
if (!string.IsNullOrEmpty(extensionConstraint))
constraints.Add(extensionConstraint);
var queryHelper = CreateBaseQuery();
queryHelper.QueryWhereRestrictions = $"AND {string.Join(" AND ", constraints)}";
return queryHelper.GenerateSQLFromUserQuery("*");
}
///<summary>

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
@ -25,7 +26,37 @@ namespace Flow.Launcher.Plugin.Explorer
public string ShellPath { get; set; } = "cmd";
public string ExcludedFileTypes { get; set; } = "";
[JsonIgnore]
private string _excludedFileTypes = "";
/// <summary>
/// File extensions, without dot prefix separated by comma.
/// </summary>
public string ExcludedFileTypes
{
get => _excludedFileTypes;
set
{
if (_excludedFileTypes == value) return;
_excludedFileTypes = value;
_excludedFileTypeList = ExcludedFileTypes.Split(Constants.ExcludedFileTypesSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToArray();
}
}
[JsonIgnore]
private string[] _excludedFileTypeList = null;
[JsonIgnore]
public string[] ExcludedFileTypeList
{
get
{
if (_excludedFileTypeList == null)
{
_excludedFileTypeList = ExcludedFileTypes.Split(Constants.ExcludedFileTypesSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToArray();
}
return _excludedFileTypeList;
}
}
public bool UseLocationAsWorkingDir { get; set; } = false;

View file

@ -574,6 +574,8 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
get => Settings.ExcludedFileTypes;
set
{
if (value == Settings.ExcludedFileTypes)
return;
// remove spaces and dots from the string before saving
string sanitized = string.IsNullOrEmpty(value) ? "" : value.Replace(" ", "").Replace(".", "");
Settings.ExcludedFileTypes = sanitized;