mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge 7efaaa6d09 into 10fcbb9990
This commit is contained in:
commit
672e233f7c
9 changed files with 243 additions and 33 deletions
|
|
@ -39,8 +39,8 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows7.0")]
|
[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:\\", $"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 directory='file:C:\\SomeFolder\\' 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)
|
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
|
|
@ -56,8 +56,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows7.0")]
|
[SupportedOSPlatform("windows7.0")]
|
||||||
[TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" +
|
[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'" +
|
||||||
" FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" +
|
|
||||||
" AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
|
" AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
|
||||||
$" ORDER BY {QueryConstructor.OrderIdentifier}")]
|
$" ORDER BY {QueryConstructor.OrderIdentifier}")]
|
||||||
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
|
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
|
||||||
|
|
@ -87,7 +86,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
[SupportedOSPlatform("windows7.0")]
|
[SupportedOSPlatform("windows7.0")]
|
||||||
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
|
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
|
||||||
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
|
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
|
||||||
$"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) 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}")]
|
[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(
|
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
|
||||||
string userSearchString, string expectedString)
|
string userSearchString, string expectedString)
|
||||||
|
|
@ -104,7 +103,8 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var resultString = queryConstructor.FilesAndFolders(userSearchString);
|
var resultString = queryConstructor.FilesAndFolders(userSearchString);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
ClassicAssert.AreEqual(expectedString, resultString);
|
ClassicAssert.AreEqual(expectedString, resultString, $"Expected string: {expectedString}{Environment.NewLine} " +
|
||||||
|
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows7.0")]
|
[SupportedOSPlatform("windows7.0")]
|
||||||
|
|
@ -125,8 +125,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows7.0")]
|
[SupportedOSPlatform("windows7.0")]
|
||||||
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
|
[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}")]
|
||||||
$"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
|
|
||||||
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
|
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
|
||||||
string userSearchString, string expectedString)
|
string userSearchString, string expectedString)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
||||||
|
|
||||||
internal const string WindowsIndexingOptions = "srchadmin.dll";
|
internal const string WindowsIndexingOptions = "srchadmin.dll";
|
||||||
|
|
||||||
|
internal const string ExcludedFileTypesSeparator = ",";
|
||||||
|
|
||||||
internal static string ExplorerIconImageFullPath
|
internal static string ExplorerIconImageFullPath
|
||||||
=> Directory.GetParent(Assembly.GetExecutingAssembly().Location.ToString()) + "\\" + ExplorerIconImagePath;
|
=> Directory.GetParent(Assembly.GetExecutingAssembly().Location.ToString()) + "\\" + ExplorerIconImagePath;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -72,14 +73,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<SearchResult> SearchAsync(string search, [EnumeratorCancellation] CancellationToken token)
|
public async IAsyncEnumerable<SearchResult> SearchAsync(string search, [EnumeratorCancellation] CancellationToken token, IEnumerable<ResultType> allowedResultTypes = null)
|
||||||
{
|
{
|
||||||
await ThrowIfEverythingNotAvailableAsync(token);
|
await ThrowIfEverythingNotAvailableAsync(token);
|
||||||
|
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
var option = new EverythingSearchOption(search,
|
var searchKeyword = BuildSearchKeyword(search, allowedResultTypes);
|
||||||
|
|
||||||
|
var option = new EverythingSearchOption(searchKeyword,
|
||||||
Settings.SortOption,
|
Settings.SortOption,
|
||||||
MaxCount: Settings.MaxResult,
|
MaxCount: Settings.MaxResult,
|
||||||
IsFullPathSearch: Settings.EverythingSearchFullPath,
|
IsFullPathSearch: Settings.EverythingSearchFullPath,
|
||||||
|
|
@ -89,6 +92,61 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
||||||
yield return result;
|
yield return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 "";
|
||||||
|
|
||||||
|
var hasFile = allowedResultTypes.Contains(ResultType.File);
|
||||||
|
var hasFolder = allowedResultTypes.Contains(ResultType.Folder);
|
||||||
|
var hasVolume = allowedResultTypes.Contains(ResultType.Volume);
|
||||||
|
|
||||||
|
return (hasFile, hasFolder, hasVolume) switch
|
||||||
|
{
|
||||||
|
(true, false, false) => "file:",
|
||||||
|
(false, true, false) => "folder:",
|
||||||
|
(false, false, true) => "volume:",
|
||||||
|
(true, true, false) => "<file:|folder:>",
|
||||||
|
(true, false, true) => "<file:|volume:>",
|
||||||
|
(false, true, true) => "<folder:|volume:>",
|
||||||
|
_ => "" // No filtering needed when all allowed or unspecified
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
public async IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearch, string contentSearch,
|
||||||
[EnumeratorCancellation] CancellationToken token)
|
[EnumeratorCancellation] CancellationToken token)
|
||||||
{
|
{
|
||||||
|
|
@ -111,7 +169,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
yield break;
|
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,
|
Settings.SortOption,
|
||||||
IsContentSearch: true,
|
IsContentSearch: true,
|
||||||
ContentSearchKeyword: contentSearch,
|
ContentSearchKeyword: contentSearch,
|
||||||
|
|
@ -132,7 +193,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
yield break;
|
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,
|
Settings.SortOption,
|
||||||
ParentPath: path,
|
ParentPath: path,
|
||||||
IsRecursive: recursive,
|
IsRecursive: recursive,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,18 @@ using System.Threading;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
|
namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides functionality for searching indexed items.
|
||||||
|
/// </summary>
|
||||||
public interface IIndexProvider
|
public interface IIndexProvider
|
||||||
{
|
{
|
||||||
public IAsyncEnumerable<SearchResult> SearchAsync(string search, CancellationToken token);
|
/// <summary>
|
||||||
|
/// Asynchronously searches for items matching the specified search criteria.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="search">The search query string.</param>
|
||||||
|
/// <param name="token">The cancellation token to cancel the search operation.</param>
|
||||||
|
/// <param name="allowedResultTypes">Optional collection of result types to filter the search results. If null, all result types are included.</param>
|
||||||
|
/// <returns>An asynchronous enumerable of <see cref="SearchResult"/> objects matching the search criteria.</returns>
|
||||||
|
public IAsyncEnumerable<SearchResult> SearchAsync(string search, CancellationToken token, IEnumerable<ResultType> allowedResultTypes = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
||||||
|
|
||||||
case false
|
case false
|
||||||
when CanUseIndexSearchByActionKeywords(activeActionKeywords):
|
when CanUseIndexSearchByActionKeywords(activeActionKeywords):
|
||||||
searchResults = Settings.IndexProvider.SearchAsync(query.Search, token);
|
searchResults = Settings.IndexProvider.SearchAsync(query.Search, token, GetAllowedResultTypeByUsedActionKeyword(activeActionKeywords));
|
||||||
engineName = Enum.GetName(Settings.IndexSearchEngine);
|
engineName = Enum.GetName(Settings.IndexSearchEngine);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
@ -148,9 +148,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
||||||
{
|
{
|
||||||
await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false))
|
await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
// TODO exclude in quick access
|
||||||
if (search.Type == ResultType.File && IsExcludedFile(search))
|
if (search.Type == ResultType.File && IsExcludedFile(search))
|
||||||
continue;
|
continue;
|
||||||
|
// TODO: Optimize filtering by action keyword at the provider level to reduce unnecessary searches.
|
||||||
|
// 3. Filter in quick access
|
||||||
|
//
|
||||||
if (IsResultTypeFilteredByActionKeyword(search.Type, actions))
|
if (IsResultTypeFilteredByActionKeyword(search.Type, actions))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -195,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)
|
private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var querySearch = query.Search;
|
var querySearch = query.Search;
|
||||||
|
|
@ -291,7 +301,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
||||||
|
|
||||||
private bool IsExcludedFile(SearchResult result)
|
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('.');
|
string fileExtension = Path.GetExtension(result.FullPath).TrimStart('.');
|
||||||
|
|
||||||
return excludedFileTypes.Contains(fileExtension, StringComparer.OrdinalIgnoreCase);
|
return excludedFileTypes.Contains(fileExtension, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
@ -329,6 +340,20 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<ResultType> GetAllowedResultTypeByUsedActionKeyword(Dictionary<ActionKeyword, string> activeActionKeywords)
|
||||||
|
{
|
||||||
|
List<ActionKeyword> activeKeywords = activeActionKeywords.Keys.ToList();
|
||||||
|
var allowedResultTypes = new List<ResultType>();
|
||||||
|
foreach (var actionKeyword in activeKeywords)
|
||||||
|
{
|
||||||
|
if (_allowedTypesByActionKeyword.TryGetValue(actionKeyword, out var resultTypes))
|
||||||
|
{
|
||||||
|
allowedResultTypes.AddRange(resultTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowedResultTypes.Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private bool CanUseIndexSearchByActionKeywords(Dictionary<ActionKeyword, string> actions)
|
private bool CanUseIndexSearchByActionKeywords(Dictionary<ActionKeyword, string> actions)
|
||||||
{
|
{
|
||||||
var keysToUseIndexSearch = new[]
|
var keysToUseIndexSearch = new[]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Search.Interop;
|
using Microsoft.Search.Interop;
|
||||||
|
|
||||||
|
|
@ -32,7 +34,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
||||||
baseQuery.QueryContentProperties = "System.FileName";
|
baseQuery.QueryContentProperties = "System.FileName";
|
||||||
|
|
||||||
// Set sorting order
|
// Set sorting order
|
||||||
//baseQuery.QuerySorting = "System.ItemType DESC";
|
baseQuery.QuerySorting = OrderIdentifier;
|
||||||
|
|
||||||
return baseQuery;
|
return baseQuery;
|
||||||
}
|
}
|
||||||
|
|
@ -60,21 +62,21 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
||||||
///</summary>
|
///</summary>
|
||||||
public string Directory(ReadOnlySpan<char> path, ReadOnlySpan<char> searchString = default, bool recursive = false)
|
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
|
var scopeConstraint = recursive
|
||||||
? RecursiveDirectoryConstraint(path)
|
? RecursiveDirectoryConstraint(path)
|
||||||
: TopLevelDirectoryConstraint(path);
|
: TopLevelDirectoryConstraint(path);
|
||||||
|
|
||||||
var query = $"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {OrderIdentifier}";
|
var baseQueryHelper = CreateBaseQuery();
|
||||||
|
baseQueryHelper.QueryWhereRestrictions = $"AND {scopeConstraint}{queryConstraint}";
|
||||||
return query;
|
return baseQueryHelper.GenerateSQLFromUserQuery("*");
|
||||||
}
|
}
|
||||||
|
|
||||||
///<summary>
|
///<summary>
|
||||||
/// Search will be performed on all folders and files based on user's search keywords.
|
/// Search will be performed on all folders and files based on user's search keywords.
|
||||||
///</summary>
|
///</summary>
|
||||||
public string FilesAndFolders(ReadOnlySpan<char> userSearchString)
|
public string FilesAndFolders(ReadOnlySpan<char> userSearchString, IEnumerable<ResultType> allowedResultTypes = null)
|
||||||
{
|
{
|
||||||
if (userSearchString.IsWhiteSpace())
|
if (userSearchString.IsWhiteSpace())
|
||||||
userSearchString = "*";
|
userSearchString = "*";
|
||||||
|
|
@ -82,8 +84,69 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
||||||
// Remove any special characters that might cause issues with the query
|
// Remove any special characters that might cause issues with the query
|
||||||
var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString);
|
var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString);
|
||||||
|
|
||||||
// Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
|
var constraints = new List<string>
|
||||||
return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
|
{
|
||||||
|
RestrictionsForAllFilesAndFoldersSearch
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
/// Build WHERE clause constraint to filter by result types (File, Folder, Volume).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// System.ItemType values:
|
||||||
|
/// - ".directory" for folders
|
||||||
|
/// - Specific file extensions (e.g., ".txt", ".pdf") for files
|
||||||
|
/// - Drive volumes are identified by their path structure
|
||||||
|
/// </remarks>
|
||||||
|
private static string BuildTypeFilterConstraint(IEnumerable<ResultType> allowedResultTypes)
|
||||||
|
{
|
||||||
|
if (allowedResultTypes == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var typesList = allowedResultTypes as IList<ResultType> ?? allowedResultTypes.ToList();
|
||||||
|
var hasFile = typesList.Contains(ResultType.File);
|
||||||
|
var hasFolder = typesList.Contains(ResultType.Folder) || typesList.Contains(ResultType.Volume); // Folder and volume are merged in Folder action keyword so treat them as same
|
||||||
|
|
||||||
|
// No filtering needed if empty or all types are allowed
|
||||||
|
if (hasFile && hasFolder || !hasFile && !hasFolder)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (hasFolder)
|
||||||
|
return "System.ItemType = '.directory'";
|
||||||
|
|
||||||
|
if (hasFile)
|
||||||
|
return "System.ItemType <> '.directory'";
|
||||||
|
|
||||||
|
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>
|
/// <summary>
|
||||||
|
|
@ -132,10 +195,19 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
||||||
///</summary>
|
///</summary>
|
||||||
public string FileContent(ReadOnlySpan<char> userSearchString)
|
public string FileContent(ReadOnlySpan<char> userSearchString)
|
||||||
{
|
{
|
||||||
string query =
|
var constraints = new List<string>
|
||||||
$"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
|
{
|
||||||
|
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>
|
///<summary>
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
||||||
|
|
||||||
private IAsyncEnumerable<SearchResult> WindowsIndexFilesAndFoldersSearchAsync(
|
private IAsyncEnumerable<SearchResult> WindowsIndexFilesAndFoldersSearchAsync(
|
||||||
ReadOnlySpan<char> querySearchString,
|
ReadOnlySpan<char> querySearchString,
|
||||||
|
IEnumerable<ResultType> allowedResultTypes = null,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return WindowsIndex.WindowsIndexSearchAsync(
|
return WindowsIndex.WindowsIndexSearchAsync(
|
||||||
QueryConstructor.CreateQueryHelper().ConnectionString,
|
QueryConstructor.CreateQueryHelper().ConnectionString,
|
||||||
QueryConstructor.FilesAndFolders(querySearchString),
|
QueryConstructor.FilesAndFolders(querySearchString, allowedResultTypes),
|
||||||
token);
|
token);
|
||||||
}
|
}
|
||||||
catch (COMException)
|
catch (COMException)
|
||||||
|
|
@ -83,9 +84,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAsyncEnumerable<SearchResult> SearchAsync(string search, CancellationToken token)
|
public IAsyncEnumerable<SearchResult> SearchAsync(string search, CancellationToken token, IEnumerable<ResultType> allowedResultTypes = null)
|
||||||
{
|
{
|
||||||
return WindowsIndexFilesAndFoldersSearchAsync(search, token: token);
|
return WindowsIndexFilesAndFoldersSearchAsync(search, allowedResultTypes, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearch, string contentSearch, CancellationToken token)
|
public IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearch, string contentSearch, CancellationToken token)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Flow.Launcher.Plugin.Explorer.Search;
|
using Flow.Launcher.Plugin.Explorer.Search;
|
||||||
using Flow.Launcher.Plugin.Explorer.Search.Everything;
|
using Flow.Launcher.Plugin.Explorer.Search.Everything;
|
||||||
|
|
@ -25,7 +26,37 @@ namespace Flow.Launcher.Plugin.Explorer
|
||||||
|
|
||||||
public string ShellPath { get; set; } = "cmd";
|
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;
|
public bool UseLocationAsWorkingDir { get; set; } = false;
|
||||||
|
|
||||||
|
|
@ -82,12 +113,16 @@ namespace Flow.Launcher.Plugin.Explorer
|
||||||
|
|
||||||
public string PreviewPanelTimeFormat { get; set; } = "HH:mm";
|
public string PreviewPanelTimeFormat { get; set; } = "HH:mm";
|
||||||
|
|
||||||
private EverythingSearchManager _everythingManagerInstance;
|
|
||||||
private WindowsIndexSearchManager _windowsIndexSearchManager;
|
|
||||||
|
|
||||||
#region SearchEngine
|
#region SearchEngine
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
private EverythingSearchManager _everythingManagerInstance;
|
||||||
|
[JsonIgnore]
|
||||||
|
private WindowsIndexSearchManager _windowsIndexSearchManager;
|
||||||
|
[JsonIgnore]
|
||||||
private EverythingSearchManager EverythingManagerInstance => _everythingManagerInstance ??= new EverythingSearchManager(this);
|
private EverythingSearchManager EverythingManagerInstance => _everythingManagerInstance ??= new EverythingSearchManager(this);
|
||||||
|
[JsonIgnore]
|
||||||
private WindowsIndexSearchManager WindowsIndexSearchManager => _windowsIndexSearchManager ??= new WindowsIndexSearchManager(this);
|
private WindowsIndexSearchManager WindowsIndexSearchManager => _windowsIndexSearchManager ??= new WindowsIndexSearchManager(this);
|
||||||
|
|
||||||
public IndexSearchEngineOption IndexSearchEngine { get; set; } = IndexSearchEngineOption.WindowsIndex;
|
public IndexSearchEngineOption IndexSearchEngine { get; set; } = IndexSearchEngineOption.WindowsIndex;
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,8 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
|
||||||
get => Settings.ExcludedFileTypes;
|
get => Settings.ExcludedFileTypes;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (value == Settings.ExcludedFileTypes)
|
||||||
|
return;
|
||||||
// remove spaces and dots from the string before saving
|
// remove spaces and dots from the string before saving
|
||||||
string sanitized = string.IsNullOrEmpty(value) ? "" : value.Replace(" ", "").Replace(".", "");
|
string sanitized = string.IsNullOrEmpty(value) ? "" : value.Replace(" ", "").Replace(".", "");
|
||||||
Settings.ExcludedFileTypes = sanitized;
|
Settings.ExcludedFileTypes = sanitized;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue