mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
263 lines
10 KiB
C#
263 lines
10 KiB
C#
using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
|
{
|
|
public static class EverythingApi
|
|
{
|
|
private const int BufferSize = 4096;
|
|
|
|
private static readonly SemaphoreSlim _semaphore = new(1, 1);
|
|
|
|
// cached buffer to remove redundant allocations.
|
|
private static readonly StringBuilder buffer = new(BufferSize);
|
|
|
|
public enum StateCode
|
|
{
|
|
OK,
|
|
MemoryError,
|
|
IPCError,
|
|
RegisterClassExError,
|
|
CreateWindowError,
|
|
CreateThreadError,
|
|
InvalidIndexError,
|
|
InvalidCallError
|
|
}
|
|
|
|
const uint EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME = 0x00000004u;
|
|
const uint EVERYTHING_REQUEST_RUN_COUNT = 0x00000400u;
|
|
|
|
/// <summary>
|
|
/// Checks whether the sort option is Fast Sort.
|
|
/// </summary>
|
|
public static bool IsFastSortOption(EverythingSortOption sortOption)
|
|
{
|
|
var fastSortOptionEnabled = EverythingApiDllImport.Everything_IsFastSort(sortOption);
|
|
|
|
// If the Everything service is not running, then this call will incorrectly report
|
|
// the state as false. This checks for errors thrown by the api and up to the caller to handle.
|
|
CheckAndThrowExceptionOnError();
|
|
|
|
return fastSortOptionEnabled;
|
|
}
|
|
|
|
public static async ValueTask<bool> IsEverythingRunningAsync(CancellationToken token = default)
|
|
{
|
|
try
|
|
{
|
|
await _semaphore.WaitAsync(token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
_ = EverythingApiDllImport.Everything_GetMajorVersion();
|
|
var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError;
|
|
return result;
|
|
}
|
|
finally
|
|
{
|
|
_semaphore.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Searches the specified key word and reset the everything API afterwards
|
|
/// </summary>
|
|
/// <param name="option">Search Criteria</param>
|
|
/// <param name="token">when cancelled the current search will stop and exit (and would not reset)</param>
|
|
/// <returns>An IAsyncEnumerable that will enumerate all results searched by the specific query and option</returns>
|
|
public static async IAsyncEnumerable<SearchResult> SearchAsync(EverythingSearchOption option,
|
|
[EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
if (option.Offset < 0)
|
|
throw new ArgumentOutOfRangeException(nameof(option.Offset), option.Offset, "Offset must be greater than or equal to 0");
|
|
|
|
if (option.MaxCount < 0)
|
|
throw new ArgumentOutOfRangeException(nameof(option.MaxCount), option.MaxCount, "MaxCount must be greater than or equal to 0");
|
|
|
|
try
|
|
{
|
|
await _semaphore.WaitAsync(token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (token.IsCancellationRequested)
|
|
yield break;
|
|
|
|
if (option.Keyword.StartsWith("@"))
|
|
{
|
|
EverythingApiDllImport.Everything_SetRegex(true);
|
|
option.Keyword = option.Keyword[1..];
|
|
}
|
|
|
|
var builder = new StringBuilder();
|
|
builder.Append(option.Keyword);
|
|
|
|
if (!string.IsNullOrWhiteSpace(option.ParentPath))
|
|
{
|
|
builder.Append($" {(option.IsRecursive ? "" : "parent:")}\"{option.ParentPath}\"");
|
|
}
|
|
|
|
if (option.IsContentSearch)
|
|
{
|
|
builder.Append($" content:\"{option.ContentSearchKeyword}\"");
|
|
}
|
|
|
|
EverythingApiDllImport.Everything_SetSearchW(builder.ToString());
|
|
EverythingApiDllImport.Everything_SetOffset(option.Offset);
|
|
EverythingApiDllImport.Everything_SetMax(option.MaxCount);
|
|
|
|
EverythingApiDllImport.Everything_SetSort(option.SortOption);
|
|
EverythingApiDllImport.Everything_SetMatchPath(option.IsFullPathSearch);
|
|
|
|
if (option.SortOption == EverythingSortOption.RUN_COUNT_DESCENDING)
|
|
{
|
|
EverythingApiDllImport.Everything_SetRequestFlags(EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME | EVERYTHING_REQUEST_RUN_COUNT);
|
|
}
|
|
else
|
|
{
|
|
EverythingApiDllImport.Everything_SetRequestFlags(EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME);
|
|
}
|
|
|
|
if (token.IsCancellationRequested) yield break;
|
|
|
|
if (!EverythingApiDllImport.Everything_QueryW(true))
|
|
{
|
|
CheckAndThrowExceptionOnError();
|
|
yield break;
|
|
}
|
|
|
|
for (var idx = 0; idx < EverythingApiDllImport.Everything_GetNumResults(); ++idx)
|
|
{
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
EverythingApiDllImport.Everything_GetResultFullPathNameW(idx, buffer, BufferSize);
|
|
|
|
var result = new SearchResult
|
|
{
|
|
// todo the types are wrong. Everything expects uint everywhere, but we send int just above/below. how to fix? Is EverythingApiDllImport autogenerated or handmade?
|
|
FullPath = buffer.ToString(),
|
|
Type = EverythingApiDllImport.Everything_IsFolderResult(idx) ? ResultType.Folder :
|
|
EverythingApiDllImport.Everything_IsFileResult(idx) ? ResultType.File :
|
|
ResultType.Volume,
|
|
Score = Convert.ToInt32(EverythingApiDllImport.Everything_GetResultRunCount((uint)idx)),
|
|
HighlightData = EverythingHighlightStringToHighlightList(EverythingApiDllImport.Everything_GetResultHighlightedFileName((uint)idx))
|
|
};
|
|
|
|
yield return result;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EverythingApiDllImport.Everything_Reset();
|
|
_semaphore.Release();
|
|
}
|
|
}
|
|
|
|
private static void CheckAndThrowExceptionOnError()
|
|
{
|
|
switch (EverythingApiDllImport.Everything_GetLastError())
|
|
{
|
|
case StateCode.CreateThreadError:
|
|
throw new CreateThreadException();
|
|
case StateCode.CreateWindowError:
|
|
throw new CreateWindowException();
|
|
case StateCode.InvalidCallError:
|
|
throw new InvalidCallException();
|
|
case StateCode.InvalidIndexError:
|
|
throw new InvalidIndexException();
|
|
case StateCode.IPCError:
|
|
throw new IPCErrorException();
|
|
case StateCode.MemoryError:
|
|
throw new MemoryErrorException();
|
|
case StateCode.RegisterClassExError:
|
|
throw new RegisterClassExException();
|
|
case StateCode.OK:
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
public static async Task IncrementRunCounterAsync(string fileOrFolder)
|
|
{
|
|
await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
|
|
try
|
|
{
|
|
_ = EverythingApiDllImport.Everything_IncRunCountFromFileName(fileOrFolder);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
/*ignored*/
|
|
}
|
|
finally { _semaphore.Release(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert the highlighted string from Everything API to a list of highlight indexes for our Result.
|
|
/// </summary>
|
|
/// <param name="highlightString">Text inside a * quote is highlighted, two consecutive *'s is a single literal *. For example, in the highlighted text: abc*123* the 123 part is highlighted.</param>
|
|
/// <returns>A list of zero-based character indices that should be highlighted.</returns>
|
|
public static List<int> EverythingHighlightStringToHighlightList(string highlightString)
|
|
{
|
|
var highlightData = new List<int>();
|
|
|
|
if (string.IsNullOrEmpty(highlightString))
|
|
return highlightData;
|
|
|
|
var isHighlighted = false;
|
|
var actualIndex = 0; // Index in the actual string (without * markers)
|
|
var length = highlightString.Length;
|
|
|
|
for (var i = 0; i < length; i++)
|
|
{
|
|
if (highlightString[i] == '*')
|
|
{
|
|
// Check if it's a literal * (two consecutive *)
|
|
if (i + 1 < length && highlightString[i + 1] == '*')
|
|
{
|
|
// Two consecutive *'s represent a single literal *
|
|
if (isHighlighted)
|
|
{
|
|
highlightData.Add(actualIndex);
|
|
}
|
|
actualIndex++;
|
|
i++; // Skip the next *
|
|
}
|
|
else
|
|
{
|
|
isHighlighted = !isHighlighted;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Regular character
|
|
if (isHighlighted)
|
|
{
|
|
highlightData.Add(actualIndex);
|
|
}
|
|
actualIndex++;
|
|
}
|
|
}
|
|
|
|
return highlightData;
|
|
}
|
|
}
|
|
}
|