mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
218 lines
9.1 KiB
C#
218 lines
9.1 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text.RegularExpressions;
|
||
using Microsoft.Search.Interop;
|
||
|
||
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
||
{
|
||
public class QueryConstructor
|
||
{
|
||
private static readonly Regex _specialCharacterMatcher = new(@"[\@\@\#\#\&\&*_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled);
|
||
private static readonly Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled);
|
||
|
||
private Settings Settings { get; }
|
||
|
||
private const string SystemIndex = "SystemIndex";
|
||
|
||
public QueryConstructor(Settings settings)
|
||
{
|
||
Settings = settings;
|
||
}
|
||
|
||
public CSearchQueryHelper CreateBaseQuery()
|
||
{
|
||
var baseQuery = CreateQueryHelper();
|
||
|
||
// Set the number of results we want. Don't set this property if all results are needed.
|
||
baseQuery.QueryMaxResults = Settings.MaxResult;
|
||
|
||
// Set list of columns we want to display, getting the path presently
|
||
baseQuery.QuerySelectColumns = "System.FileName, System.ItemUrl, System.ItemType";
|
||
|
||
// Filter based on file name
|
||
baseQuery.QueryContentProperties = "System.FileName";
|
||
|
||
// Set sorting order
|
||
baseQuery.QuerySorting = OrderIdentifier;
|
||
|
||
return baseQuery;
|
||
}
|
||
|
||
internal static CSearchQueryHelper CreateQueryHelper()
|
||
{
|
||
// This uses the Microsoft.Search.Interop assembly
|
||
// Throws COMException if Windows Search service is not running/disabled, this needs to be caught
|
||
var manager = new CSearchManager();
|
||
|
||
// SystemIndex catalog is the default catalog in Windows
|
||
var catalogManager = manager.GetCatalog(SystemIndex);
|
||
|
||
// Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer
|
||
var queryHelper = catalogManager.GetQueryHelper();
|
||
|
||
return queryHelper;
|
||
}
|
||
|
||
public static string TopLevelDirectoryConstraint(ReadOnlySpan<char> path) => $"directory='file:{path}'";
|
||
public static string RecursiveDirectoryConstraint(ReadOnlySpan<char> path) => $"scope='file:{path}'";
|
||
|
||
///<summary>
|
||
/// Search will be performed on all folders and files on the first level of a specified directory.
|
||
///</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 scopeConstraint = recursive
|
||
? RecursiveDirectoryConstraint(path)
|
||
: TopLevelDirectoryConstraint(path);
|
||
|
||
var baseQueryHelper = CreateBaseQuery();
|
||
baseQueryHelper.QueryWhereRestrictions = $"AND {scopeConstraint}{queryConstraint}";
|
||
return baseQueryHelper.GenerateSQLFromUserQuery("*");
|
||
}
|
||
|
||
///<summary>
|
||
/// Search will be performed on all folders and files based on user's search keywords.
|
||
///</summary>
|
||
public string FilesAndFolders(ReadOnlySpan<char> userSearchString, IEnumerable<ResultType> allowedResultTypes = null)
|
||
{
|
||
if (userSearchString.IsWhiteSpace())
|
||
userSearchString = "*";
|
||
|
||
// Remove any special characters that might cause issues with the query
|
||
var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString);
|
||
|
||
var constraints = new List<string>
|
||
{
|
||
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>
|
||
/// 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.
|
||
/// </summary>
|
||
private static string ReplaceSpecialCharacterWithTwoSideWhiteSpace(ReadOnlySpan<char> input)
|
||
{
|
||
const string whiteSpace = " ";
|
||
|
||
var inputString = input.ToString();
|
||
|
||
// Use regex to match special characters with whitespace on one side
|
||
// and replace them with a single space
|
||
var result = _specialCharacterMatcher.Replace(inputString, match =>
|
||
{
|
||
// Check if the match has whitespace on one side
|
||
bool hasLeadingWhitespace = match.Index > 0 && char.IsWhiteSpace(inputString[match.Index - 1]);
|
||
bool hasTrailingWhitespace = match.Index + match.Length < inputString.Length && char.IsWhiteSpace(inputString[match.Index + match.Length]);
|
||
if (hasLeadingWhitespace || hasTrailingWhitespace)
|
||
{
|
||
return whiteSpace;
|
||
}
|
||
return match.Value;
|
||
});
|
||
|
||
// Remove any extra spaces that might have been introduced
|
||
return _multiWhiteSpacesMatcher.Replace(result, whiteSpace).Trim();
|
||
}
|
||
|
||
///<summary>
|
||
/// Set the required WHERE clause restriction to search for all files and folders.
|
||
///</summary>
|
||
public const string RestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
|
||
|
||
/// <summary>
|
||
/// Order identifier: System.Search.Rank DESC
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <see href="https://docs.microsoft.com/en-us/windows/win32/properties/props-system-search-rank"/>
|
||
/// </remarks>
|
||
public const string OrderIdentifier = "System.Search.Rank DESC";
|
||
|
||
///<summary>
|
||
/// Search will be performed on all indexed file contents for the specified search keywords.
|
||
///</summary>
|
||
public string FileContent(ReadOnlySpan<char> userSearchString)
|
||
{
|
||
var constraints = new List<string>
|
||
{
|
||
RestrictionsForFileContentSearch(userSearchString),
|
||
RestrictionsForAllFilesAndFoldersSearch
|
||
};
|
||
|
||
var extensionConstraint = BuildExtensionExclusionConstraint();
|
||
if (!string.IsNullOrEmpty(extensionConstraint))
|
||
constraints.Add(extensionConstraint);
|
||
|
||
var queryHelper = CreateBaseQuery();
|
||
queryHelper.QueryWhereRestrictions = $"AND {string.Join(" AND ", constraints)}";
|
||
return queryHelper.GenerateSQLFromUserQuery("*");
|
||
}
|
||
|
||
///<summary>
|
||
/// Set the required WHERE clause restriction to search within file content.
|
||
///</summary>
|
||
public static string RestrictionsForFileContentSearch(ReadOnlySpan<char> searchQuery) => $"FREETEXT('{searchQuery}')";
|
||
}
|
||
}
|