mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Filter file extenstions in Windows Index and Everything
This commit is contained in:
parent
db9be228c1
commit
6a454f9e58
7 changed files with 149 additions and 40 deletions
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue