Skip to content
Draft
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
15 changes: 7 additions & 8 deletions Flow.Launcher.Test/Plugins/ExplorerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework;
using NUnit.Framework.Legacy;

Check warning on line 8 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NUnit` is not a recognized word. (unrecognized-spelling)
using System;
using System.IO;
using System.Runtime.Versioning;
Expand Down Expand Up @@ -39,8 +39,8 @@
}

[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
Expand All @@ -56,9 +56,8 @@
}

[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*\"'))" +

Check warning on line 60 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`sln` is not a recognized word. (unrecognized-spelling)

Check warning on line 60 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`sln` is not a recognized word. (unrecognized-spelling)
$" ORDER BY {QueryConstructor.OrderIdentifier}")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
string folderPath, string userSearchString, string expectedString)
Expand All @@ -85,9 +84,9 @@
}

[SupportedOSPlatform("windows7.0")]
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +

Check warning on line 87 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`sln` is not a recognized word. (unrecognized-spelling)
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +

Check warning on line 88 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`sln` is not a recognized word. (unrecognized-spelling)
$"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}")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
Expand All @@ -104,7 +103,8 @@
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")]
Expand All @@ -125,8 +125,7 @@
}

[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}")]

Check warning on line 128 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`FREETEXT` is not a recognized word. (unrecognized-spelling)
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
Expand Down Expand Up @@ -239,8 +238,8 @@
}

[SupportedOSPlatform("windows7.0")]
[TestCase("c:\\somefolder\\>somefile", "*somefile*")]

Check warning on line 241 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`somefile` is not a recognized word. (unrecognized-spelling)
[TestCase("c:\\somefolder\\somefile", "somefile*")]

Check warning on line 242 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`somefolder` is not a recognized word. (unrecognized-spelling)

Check warning on line 242 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`somefile` is not a recognized word. (unrecognized-spelling)

Check warning on line 242 in Flow.Launcher.Test/Plugins/ExplorerTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`somefile` is not a recognized word. (unrecognized-spelling)
[TestCase("c:\\somefolder\\", "*")]
public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSearchCriteriaShouldUseCriteriaString(string path, string expectedString)
{
Expand Down
2 changes: 2 additions & 0 deletions Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ internal static class Constants

internal const string WindowsIndexingOptions = "srchadmin.dll";

internal const string ExcludedFileTypesSeparator = ",";

internal static string ExplorerIconImageFullPath
=> Directory.GetParent(Assembly.GetExecutingAssembly().Location.ToString()) + "\\" + ExplorerIconImagePath;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -72,14 +73,16 @@ private async ValueTask<bool> ClickToInstallEverythingAsync(ActionContext _)
}
}

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);

if (token.IsCancellationRequested)
yield break;

var option = new EverythingSearchOption(search,
var searchKeyword = BuildSearchKeyword(search, allowedResultTypes);

var option = new EverythingSearchOption(searchKeyword,
Settings.SortOption,
MaxCount: Settings.MaxResult,
IsFullPathSearch: Settings.EverythingSearchFullPath,
Expand All @@ -89,6 +92,61 @@ public async IAsyncEnumerable<SearchResult> SearchAsync(string search, [Enumerat
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,
[EnumeratorCancellation] CancellationToken token)
{
Expand All @@ -111,7 +169,10 @@ public async IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearc
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,
Expand All @@ -132,7 +193,10 @@ public async IAsyncEnumerable<SearchResult> EnumerateAsync(string path, string s
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@

namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
{
/// <summary>
/// Provides functionality for searching indexed items.
/// </summary>
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);
}
}
31 changes: 28 additions & 3 deletions Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ when activeActionKeywords.ContainsKey(ActionKeyword.QuickAccessActionKeyword):

case false
when CanUseIndexSearchByActionKeywords(activeActionKeywords):
searchResults = Settings.IndexProvider.SearchAsync(query.Search, token);
searchResults = Settings.IndexProvider.SearchAsync(query.Search, token, GetAllowedResultTypeByUsedActionKeyword(activeActionKeywords));
engineName = Enum.GetName(Settings.IndexSearchEngine);
break;
default:
Expand All @@ -148,9 +148,12 @@ when CanUseIndexSearchByActionKeywords(activeActionKeywords):
{
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.
// 3. Filter in quick access
//
if (IsResultTypeFilteredByActionKeyword(search.Type, actions))
continue;

Expand Down Expand Up @@ -195,6 +198,13 @@ private List<Result> EverythingContentSearchResult(Query query)
];
}

/// <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;
Expand Down Expand Up @@ -291,7 +301,8 @@ private bool UseWindowsIndexForDirectorySearch(string locationPath)

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);
Expand Down Expand Up @@ -329,6 +340,20 @@ private bool IsResultTypeFilteredByActionKeyword(ResultType type, List<ActionKey
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)
{
var keysToUseIndexSearch = new[]
Expand Down
Loading
Loading