CmdPal: Make Indexer great again - part 1 - hotfix (#44729)

## Summary of the Pull Request

This PR introduces a rough hotfix for several indexer-related issues.

- Premise: patch what we can in-place and fix the core later (reworking
`SeachEngine` and `SearchQuery` is slightly trickier). This patch also
removes some dead code for future refactor.
- Adds search cancellation to the File Search page and the indexer
fallback.
- Prevents older searches from overwriting newer model state and reduces
wasted work.
- Stops reusing the search engine; creates a new instance per search to
avoid synchronization issues.
- That `SeachEngine` and `SearchQuery` are not multi-threading friendly.
- Removes search priming to simplify the code and improve performance.
- Since `SearchQuery` cancels and re-primes on every search, priming
provides little benefit and can hide extra work (for example,
cancellation triggering re-priming).
- Fixes the indexer fallback subject line not updating when there is
more than one match.
  - It previously kept the old value, which was confusing.
- ~Shows the number of matched files in the fallback result.~
- Fetching total number of rows was reverted, performance was not stable
:(
- Optimizes the indexer fallback by reducing the number of items
processed but not used.
- Only fetches the item(s) needed for the fallback itself—no extra work
on the hot path.
- Stops reusing the fallback result when navigating to the File Search
page to show more results. This requires querying again, but it
simplifies the flow and keeps components isolated.
- Fixes the English mnemonic keyword `kind` being hardcoded in the
search page filter. Windows Search uses localized mnemonic keyword
names, so this PR replaces it with canonical keyword `System.Kind` that
is universaly recognized.
- Adds extra diagnostics to `SearchQuery` and makes logging more
precise.
- DataPackage for the IndexerListItem now defers including of storage
items - boost performance and we avoid touching the item until its
needed.
- IndexerPage with a prepopulated query will delay loading until the
items are actually enumerated (to avoid populating from fallback hot
path) and it no longer takes external SearchEngine. It just recreates
everything.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Related to: #44728
- [x] Closes: #44731
- [x] Closes: #44732
- [x] Closes: #44743
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2026-02-02 18:23:34 +01:00
committed by GitHub
parent dca532cf4b
commit 4d1f92199c
9 changed files with 614 additions and 406 deletions

View File

@@ -44,6 +44,7 @@ ALLCHILDREN
ALLINPUT ALLINPUT
Allman Allman
Allmodule Allmodule
ALLNOISE
ALLOWUNDO ALLOWUNDO
ALLVIEW ALLVIEW
ALPHATYPE ALPHATYPE

View File

@@ -2,33 +2,43 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
#nullable enable
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Indexer.Data; using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Helpers; using Microsoft.CmdPal.Ext.Indexer.Helpers;
using Microsoft.CmdPal.Ext.Indexer.Properties; using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams; using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Indexer; namespace Microsoft.CmdPal.Ext.Indexer;
internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System.IDisposable internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, IDisposable
{ {
private const string _id = "com.microsoft.cmdpal.builtin.indexer.fallback"; private const string CommandId = "com.microsoft.cmdpal.builtin.indexer.fallback";
private static readonly NoOpCommand _baseCommandWithId = new() { Id = _id };
private readonly CompositeFormat fallbackItemSearchPageTitleCompositeFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title); // Cookie to identify our queries; since we replace the SearchEngine on each search,
// this can be a constant.
private const uint HardQueryCookie = 10;
private static readonly NoOpCommand BaseCommandWithId = new() { Id = CommandId };
private readonly SearchEngine _searchEngine = new(); private readonly CompositeFormat _fallbackItemSearchPageTitleFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title);
private readonly CompositeFormat _fallbackItemSearchSubtitleMultipleResults = CompositeFormat.Parse(Resources.Indexer_Fallback_MultipleResults_Subtitle);
private readonly Lock _querySwitchLock = new();
private readonly Lock _resultLock = new();
private uint _queryCookie = 10; private CancellationTokenSource? _currentQueryCts;
private Func<string, bool>? _suppressCallback;
private Func<string, bool> _suppressCallback;
public FallbackOpenFileItem() public FallbackOpenFileItem()
: base(_baseCommandWithId, Resources.Indexer_Find_Path_fallback_display_title, _id) : base(BaseCommandWithId, Resources.Indexer_Find_Path_fallback_display_title, CommandId)
{ {
Title = string.Empty; Title = string.Empty;
Subtitle = string.Empty; Subtitle = string.Empty;
@@ -37,118 +47,209 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
public override void UpdateQuery(string query) public override void UpdateQuery(string query)
{ {
if (string.IsNullOrWhiteSpace(query)) UpdateQueryCore(query);
{ }
Command = new NoOpCommand();
Title = string.Empty;
Subtitle = string.Empty;
Icon = null;
MoreCommands = null;
DataPackage = null;
private void UpdateQueryCore(string query)
{
// Calling this will cancel any ongoing query processing. We always use a new SearchEngine
// instance per query, as SearchEngine.Query cancels/reinitializes internally.
CancellationToken cancellationToken;
lock (_querySwitchLock)
{
_currentQueryCts?.Cancel();
_currentQueryCts?.Dispose();
_currentQueryCts = new CancellationTokenSource();
cancellationToken = _currentQueryCts.Token;
}
var suppressCallback = _suppressCallback;
if (string.IsNullOrWhiteSpace(query) || (suppressCallback is not null && suppressCallback(query)))
{
ClearResultForCurrentQuery(cancellationToken);
return; return;
} }
if (_suppressCallback is not null && _suppressCallback(query)) try
{ {
Command = new NoOpCommand(); var exists = Path.Exists(query);
Title = string.Empty; if (exists)
Subtitle = string.Empty;
Icon = null;
MoreCommands = null;
DataPackage = null;
return;
}
if (Path.Exists(query))
{
// Exit 1: The query is a direct path to a file. Great! Return it.
var item = new IndexerItem(fullPath: query);
var listItemForUs = new IndexerListItem(item, IncludeBrowseCommand.AsDefault);
Command = listItemForUs.Command;
MoreCommands = listItemForUs.MoreCommands;
Subtitle = item.FileName;
Title = item.FullPath;
Icon = listItemForUs.Icon;
DataPackage = DataPackageHelper.CreateDataPackageForPath(listItemForUs, item.FullPath);
try
{ {
var stream = ThumbnailHelper.GetThumbnail(item.FullPath).Result; ProcessDirectPath(query, cancellationToken);
if (stream is not null) }
else
{
ProcessSearchQuery(query, cancellationToken);
}
}
catch (OperationCanceledException)
{
// Query was superseded by a newer one - discard silently.
}
catch
{
if (!cancellationToken.IsCancellationRequested)
{
ClearResultForCurrentQuery(cancellationToken);
}
}
}
private void ProcessDirectPath(string query, CancellationToken ct)
{
var item = new IndexerItem(fullPath: query);
var indexerListItem = new IndexerListItem(item, IncludeBrowseCommand.AsDefault);
ct.ThrowIfCancellationRequested();
UpdateResultForCurrentQuery(indexerListItem, skipIcon: true, ct);
_ = LoadIconAsync(item.FullPath, ct);
}
private void ProcessSearchQuery(string query, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
// for now the SearchEngine and SearchQuery are not thread-safe, so we create a new instance per query
// since SearchEngine will re-initialize on a new query anyway, it doesn't seem to be a big overhead for now
var searchEngine = new SearchEngine();
try
{
searchEngine.Query(query, queryCookie: HardQueryCookie);
ct.ThrowIfCancellationRequested();
// We only need to know whether there are 0, 1, or more than one result
var results = searchEngine.FetchItems(0, 2, queryCookie: HardQueryCookie, out _, noIcons: true);
var count = results.Count;
if (count == 0)
{
ClearResultForCurrentQuery(ct);
}
else if (count == 1)
{
if (results[0] is IndexerListItem indexerListItem)
{ {
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); UpdateResultForCurrentQuery(indexerListItem, skipIcon: true, ct);
Icon = new IconInfo(data, data); _ = LoadIconAsync(indexerListItem.FilePath, ct);
}
else
{
ClearResultForCurrentQuery(ct);
} }
} }
catch else
{ {
var indexerPage = new IndexerPage(query);
var set = UpdateResultForCurrentQuery(
string.Format(CultureInfo.CurrentCulture, _fallbackItemSearchPageTitleFormat, query),
string.Format(CultureInfo.CurrentCulture, _fallbackItemSearchSubtitleMultipleResults),
Icons.FileExplorerIcon,
indexerPage,
MoreCommands,
DataPackage,
skipIcon: false,
ct);
if (!set)
{
// if we failed to set the result (query was cancelled), dispose the page and search engine
indexerPage.Dispose();
}
} }
return;
} }
else finally
{ {
_queryCookie++; searchEngine?.Dispose();
}
}
try private async Task LoadIconAsync(string path, CancellationToken ct)
{
try
{
var stream = await ThumbnailHelper.GetThumbnail(path).ConfigureAwait(false);
if (stream is null || ct.IsCancellationRequested)
{ {
_searchEngine.Query(query, _queryCookie);
var results = _searchEngine.FetchItems(0, 20, _queryCookie, out var _);
if (results.Count == 0 || (results[0] is not IndexerListItem indexerListItem))
{
// Exit 2: We searched for the file, and found nothing. Oh well.
// Hide ourselves.
Title = string.Empty;
Subtitle = string.Empty;
Command = new NoOpCommand();
MoreCommands = null;
DataPackage = null;
return;
}
if (results.Count == 1)
{
// Exit 3: We searched for the file, and found exactly one thing. Awesome!
// Return it.
Title = indexerListItem.Title;
Subtitle = indexerListItem.Subtitle;
Icon = indexerListItem.Icon;
Command = indexerListItem.Command;
MoreCommands = indexerListItem.MoreCommands;
DataPackage = DataPackageHelper.CreateDataPackageForPath(indexerListItem, indexerListItem.FilePath);
return;
}
// Exit 4: We found more than one result. Make our command take
// us to the file search page, prepopulated with this search.
var indexerPage = new IndexerPage(query, _searchEngine, _queryCookie, results);
Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query);
Icon = Icons.FileExplorerIcon;
Command = indexerPage;
MoreCommands = null;
DataPackage = null;
return; return;
} }
catch
var thumbnailStream = RandomAccessStreamReference.CreateFromStream(stream);
if (ct.IsCancellationRequested)
{ {
Title = string.Empty; return;
Subtitle = string.Empty;
Icon = null;
Command = new NoOpCommand();
MoreCommands = null;
DataPackage = null;
} }
var data = new IconData(thumbnailStream);
UpdateIconForCurrentQuery(new IconInfo(data), ct);
}
catch
{
// ignore - keep default icon
UpdateIconForCurrentQuery(Icons.FileExplorerIcon, ct);
}
}
private bool ClearResultForCurrentQuery(CancellationToken ct)
{
return UpdateResultForCurrentQuery(string.Empty, string.Empty, Icons.FileExplorerIcon, BaseCommandWithId, null, null, false, ct);
}
private bool UpdateResultForCurrentQuery(IndexerListItem listItem, bool skipIcon, CancellationToken ct)
{
return UpdateResultForCurrentQuery(
listItem.Title,
listItem.Subtitle,
listItem.Icon,
listItem.Command,
listItem.MoreCommands,
DataPackageHelper.CreateDataPackageForPath(listItem, listItem.FilePath),
skipIcon,
ct);
}
private bool UpdateResultForCurrentQuery(string title, string subtitle, IIconInfo? iconInfo, ICommand? command, IContextItem[]? moreCommands, DataPackage? dataPackage, bool skipIcon, CancellationToken ct)
{
lock (_resultLock)
{
if (ct.IsCancellationRequested)
{
return false;
}
Title = title;
Subtitle = subtitle;
if (!skipIcon)
{
Icon = iconInfo!;
}
MoreCommands = moreCommands!;
DataPackage = dataPackage;
Command = command;
return true;
}
}
private void UpdateIconForCurrentQuery(IIconInfo icon, CancellationToken ct)
{
lock (_resultLock)
{
if (ct.IsCancellationRequested)
{
return;
}
Icon = icon;
} }
} }
public void Dispose() public void Dispose()
{ {
_searchEngine.Dispose(); _currentQueryCts?.Cancel();
_currentQueryCts?.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
#nullable enable
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,51 +16,83 @@ namespace Microsoft.CmdPal.Ext.Indexer.Helpers;
internal static class DataPackageHelper internal static class DataPackageHelper
{ {
public static DataPackage CreateDataPackageForPath(ICommandItem listItem, string path) public static DataPackage? CreateDataPackageForPath(ICommandItem listItem, string? path)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrWhiteSpace(path))
{ {
return null; return null;
} }
var dataPackage = new DataPackage(); // Capture now; don't rely on listItem still being valid later.
dataPackage.SetText(path); var title = listItem.Title;
_ = dataPackage.TrySetStorageItemsAsync(path); var description = listItem.Subtitle;
dataPackage.Properties.Title = listItem.Title; var capturedPath = path;
dataPackage.Properties.Description = listItem.Subtitle;
dataPackage.RequestedOperation = DataPackageOperation.Copy; var dataPackage = new DataPackage
{
RequestedOperation = DataPackageOperation.Copy,
Properties =
{
Title = title,
Description = description,
},
};
// Cheap + immediate.
dataPackage.SetText(capturedPath);
// Expensive + only computed if the consumer asks for StorageItems.
dataPackage.SetDataProvider(StandardDataFormats.StorageItems, async void (request) =>
{
var deferral = request.GetDeferral();
try
{
var items = await TryGetStorageItemAsync(capturedPath).ConfigureAwait(false);
if (items is not null)
{
request.SetData(items);
}
// If null: just don't provide StorageItems. Text still works.
}
catch
{
// Swallow: better to provide partial data (text) than fail the whole package.
}
finally
{
deferral.Complete();
}
});
return dataPackage; return dataPackage;
} }
public static async Task<bool> TrySetStorageItemsAsync(this DataPackage dataPackage, string filePath) private static async Task<IStorageItem[]?> TryGetStorageItemAsync(string filePath)
{ {
try try
{ {
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
var file = await StorageFile.GetFileFromPathAsync(filePath); var file = await StorageFile.GetFileFromPathAsync(filePath);
dataPackage.SetStorageItems([file]); return [file];
return true;
} }
if (Directory.Exists(filePath)) if (Directory.Exists(filePath))
{ {
var folder = await StorageFolder.GetFolderFromPathAsync(filePath); var folder = await StorageFolder.GetFolderFromPathAsync(filePath);
dataPackage.SetStorageItems([folder]); return [folder];
return true;
} }
// nothing there return null;
return false;
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
// Access denied skip or report, but don't crash return null;
return false;
} }
catch (Exception) catch
{ {
return false; return null;
} }
} }
} }

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
@@ -17,66 +18,25 @@ namespace Microsoft.CmdPal.Ext.Indexer.Indexer;
internal sealed partial class SearchQuery : IDisposable internal sealed partial class SearchQuery : IDisposable
{ {
private readonly Lock _lockObject = new(); // Lock object for synchronization [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Indexing Service constant")]
private readonly DBPROPIDSET dbPropIdSet; private const int QUERY_E_ALLNOISE = unchecked((int)0x80041605);
private uint reuseWhereID; private readonly Lock _lockObject = new();
private EventWaitHandle queryCompletedEvent;
private Timer queryTpTimer;
private IRowset currentRowset;
private IRowset reuseRowset;
public uint Cookie { get; set; } private IRowset _currentRowset;
public QueryState State { get; private set; } = QueryState.NotStarted;
private int? LastHResult { get; set; }
private string LastErrorMessage { get; set; }
public uint Cookie { get; private set; }
public string SearchText { get; private set; } public string SearchText { get; private set; }
public ConcurrentQueue<SearchResult> SearchResults { get; private set; } = []; public ConcurrentQueue<SearchResult> SearchResults { get; private set; } = [];
public SearchQuery()
{
dbPropIdSet = new DBPROPIDSET
{
rgPropertyIDs = Marshal.AllocCoTaskMem(sizeof(uint)), // Allocate memory for the property ID array
cPropertyIDs = 1,
guidPropertySet = new Guid("AA6EE6B0-E828-11D0-B23E-00AA0047FC01"), // DBPROPSET_MSIDXS_ROWSETEXT,
};
// Copy the property ID into the allocated memory
Marshal.WriteInt32(dbPropIdSet.rgPropertyIDs, 8); // MSIDXSPROP_WHEREID
Init();
}
private void Init()
{
// Create all the objects we will want cached
try
{
queryTpTimer = new Timer(QueryTimerCallback, this, Timeout.Infinite, Timeout.Infinite);
if (queryTpTimer is null)
{
Logger.LogError("Failed to create query timer");
return;
}
queryCompletedEvent = new EventWaitHandle(false, EventResetMode.ManualReset);
if (queryCompletedEvent is null)
{
Logger.LogError("Failed to create query completed event");
return;
}
// Execute a synchronous query on file items to prime the index and keep that handle around
PrimeIndexAndCacheWhereId();
}
catch (Exception ex)
{
Logger.LogError("Exception at SearchUXQueryHelper Init", ex);
}
}
public void WaitForQueryCompletedEvent() => queryCompletedEvent.WaitOne();
public void CancelOutstandingQueries() public void CancelOutstandingQueries()
{ {
Logger.LogDebug("Cancel query " + SearchText); Logger.LogDebug("Cancel query " + SearchText);
@@ -84,14 +44,7 @@ internal sealed partial class SearchQuery : IDisposable
// Are we currently doing work? If so, let's cancel // Are we currently doing work? If so, let's cancel
lock (_lockObject) lock (_lockObject)
{ {
if (queryTpTimer is not null) State = QueryState.Cancelled;
{
queryTpTimer.Change(Timeout.Infinite, Timeout.Infinite);
queryTpTimer.Dispose();
queryTpTimer = null;
}
Init();
} }
} }
@@ -102,40 +55,32 @@ internal sealed partial class SearchQuery : IDisposable
ExecuteSyncInternal(); ExecuteSyncInternal();
} }
public static void QueryTimerCallback(object state)
{
var pQueryHelper = (SearchQuery)state;
pQueryHelper.ExecuteSyncInternal();
}
private void ExecuteSyncInternal() private void ExecuteSyncInternal()
{ {
lock (_lockObject) lock (_lockObject)
{ {
var queryStr = QueryStringBuilder.GenerateQuery(SearchText, reuseWhereID); State = QueryState.Running;
LastHResult = null;
LastErrorMessage = null;
var queryStr = QueryStringBuilder.GenerateQuery(SearchText);
try try
{ {
// We need to generate a search query string with the search text the user entered above var result = ExecuteCommand(queryStr);
if (currentRowset is not null) _currentRowset = result.Rowset;
{ State = result.State;
// We have a previous rowset, this means the user is typing and we should store this LastHResult = result.HResult;
// recapture the where ID from this so the next ExecuteSync call will be faster LastErrorMessage = result.ErrorMessage;
reuseRowset = currentRowset;
reuseWhereID = GetReuseWhereId(reuseRowset);
}
currentRowset = ExecuteCommand(queryStr);
SearchResults.Clear(); SearchResults.Clear();
} }
catch (Exception ex) catch (Exception ex)
{ {
State = QueryState.ExecuteFailed;
LastHResult = ex.HResult;
LastErrorMessage = ex.Message;
Logger.LogError("Error executing query", ex); Logger.LogError("Error executing query", ex);
} }
finally
{
queryCompletedEvent.Set();
}
} }
} }
@@ -170,31 +115,68 @@ internal sealed partial class SearchQuery : IDisposable
public bool FetchRows(int offset, int limit) public bool FetchRows(int offset, int limit)
{ {
if (currentRowset is null) if (_currentRowset is null)
{ {
Logger.LogError("No rowset to fetch rows from"); var message = $"No rowset to fetch rows from. State={State}, HResult={LastHResult}, Error='{LastErrorMessage}'";
switch (State)
{
case QueryState.NoResults:
case QueryState.AllNoise:
Logger.LogDebug(message);
break;
case QueryState.NotStarted:
case QueryState.Cancelled:
case QueryState.Running:
Logger.LogInfo(message);
break;
default:
Logger.LogError(message);
break;
}
return false; return false;
} }
IGetRow getRow = null; IGetRow getRow;
try try
{ {
getRow = (IGetRow)currentRowset; getRow = (IGetRow)_currentRowset;
} }
catch (Exception) catch (Exception ex)
{ {
Logger.LogInfo("Reset the current rowset"); Logger.LogInfo($"Reset the current rowset. State={State}, HResult={LastHResult}, Error='{LastErrorMessage}'");
Logger.LogError("Failed to cast current rowset to IGetRow", ex);
ExecuteSyncInternal(); ExecuteSyncInternal();
getRow = (IGetRow)currentRowset;
if (_currentRowset is null)
{
var message = $"Failed to reset rowset. State={State}, HResult={LastHResult}, Error='{LastErrorMessage}'";
switch (State)
{
case QueryState.NoResults:
case QueryState.AllNoise:
Logger.LogDebug(message);
break;
default:
Logger.LogError(message);
break;
}
return false;
}
getRow = (IGetRow)_currentRowset;
} }
uint rowCountReturned;
var prghRows = IntPtr.Zero; var prghRows = IntPtr.Zero;
try try
{ {
currentRowset.GetNextRows(IntPtr.Zero, offset, limit, out rowCountReturned, out prghRows); _currentRowset.GetNextRows(IntPtr.Zero, offset, limit, out var rowCountReturned, out prghRows);
if (rowCountReturned == 0) if (rowCountReturned == 0)
{ {
@@ -215,7 +197,7 @@ internal sealed partial class SearchQuery : IDisposable
} }
} }
currentRowset.ReleaseRows(rowCountReturned, rowHandles, IntPtr.Zero, null, null); _currentRowset.ReleaseRows(rowCountReturned, rowHandles, IntPtr.Zero, null, null);
Marshal.FreeCoTaskMem(prghRows); Marshal.FreeCoTaskMem(prghRows);
prghRows = IntPtr.Zero; prghRows = IntPtr.Zero;
@@ -236,141 +218,91 @@ internal sealed partial class SearchQuery : IDisposable
} }
} }
private void PrimeIndexAndCacheWhereId() private static ExecuteCommandResult ExecuteCommand(string queryStr)
{
var queryStr = QueryStringBuilder.GeneratePrimingQuery();
var rowset = ExecuteCommand(queryStr);
if (rowset is not null)
{
reuseRowset = rowset;
reuseWhereID = GetReuseWhereId(reuseRowset);
}
}
private unsafe IRowset ExecuteCommand(string queryStr)
{ {
if (string.IsNullOrEmpty(queryStr)) if (string.IsNullOrEmpty(queryStr))
{ {
return null; return new ExecuteCommandResult(Rowset: null, State: QueryState.ExecuteFailed, HResult: null, ErrorMessage: "Query string was empty.");
} }
try try
{ {
var session = (IDBCreateSession)DataSourceManager.GetDataSource(); var dataSource = DataSourceManager.GetDataSource();
if (dataSource is null)
{
Logger.LogError("GetDataSource returned null");
return new ExecuteCommandResult(Rowset: null, State: QueryState.NullDataSource, HResult: null, ErrorMessage: "GetDataSource returned null.");
}
var session = (IDBCreateSession)dataSource;
var guid = typeof(IDBCreateCommand).GUID; var guid = typeof(IDBCreateCommand).GUID;
session.CreateSession(IntPtr.Zero, ref guid, out var ppDBSession); session.CreateSession(IntPtr.Zero, ref guid, out var ppDBSession);
if (ppDBSession is null) if (ppDBSession is null)
{ {
Logger.LogError("CreateSession failed"); Logger.LogError("CreateSession failed");
return null; return new ExecuteCommandResult(Rowset: null, State: QueryState.CreateSessionFailed, HResult: null, ErrorMessage: "CreateSession returned null session.");
} }
var createCommand = (IDBCreateCommand)ppDBSession; var createCommand = (IDBCreateCommand)ppDBSession;
guid = typeof(ICommandText).GUID; guid = typeof(ICommandText).GUID;
createCommand.CreateCommand(IntPtr.Zero, ref guid, out ICommandText commandText); createCommand.CreateCommand(IntPtr.Zero, ref guid, out var commandText);
if (commandText is null) if (commandText is null)
{ {
Logger.LogError("Failed to get ICommandText interface"); Logger.LogError("Failed to get ICommandText interface");
return null; return new ExecuteCommandResult(Rowset: null, State: QueryState.CreateCommandFailed, HResult: null, ErrorMessage: "CreateCommand returned null command.");
} }
var riid = NativeHelpers.OleDb.DbGuidDefault; var riid = NativeHelpers.OleDb.DbGuidDefault;
var irowSetRiid = typeof(IRowset).GUID; var irowSetRiid = typeof(IRowset).GUID;
commandText.SetCommandText(ref riid, queryStr); commandText.SetCommandText(ref riid, queryStr);
commandText.Execute(null, ref irowSetRiid, null, out var pcRowsAffected, out var rowsetPointer); commandText.Execute(null, ref irowSetRiid, null, out _, out var rowsetPointer);
return rowsetPointer; return rowsetPointer is null
? new ExecuteCommandResult(Rowset: null, State: QueryState.NoResults, HResult: null, ErrorMessage: null)
: new ExecuteCommandResult(Rowset: rowsetPointer, State: QueryState.Completed, HResult: null, ErrorMessage: null);
}
catch (COMException ex) when (ex.HResult == QUERY_E_ALLNOISE)
{
Logger.LogDebug($"Query returned all noise, no results. ({queryStr})");
return new ExecuteCommandResult(Rowset: null, State: QueryState.AllNoise, HResult: ex.HResult, ErrorMessage: ex.Message);
}
catch (COMException ex)
{
Logger.LogError($"Unexpected COM error for query '{queryStr}'.", ex);
return new ExecuteCommandResult(Rowset: null, State: QueryState.ExecuteFailed, HResult: ex.HResult, ErrorMessage: ex.Message);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError("Unexpected error.", ex); Logger.LogError($"Unexpected error for query '{queryStr}'.", ex);
return null; return new ExecuteCommandResult(Rowset: null, State: QueryState.ExecuteFailed, HResult: ex.HResult, ErrorMessage: ex.Message);
} }
} }
private unsafe DBPROP? GetPropset(IRowsetInfo rowsetInfo)
{
var prgPropSetsPtr = IntPtr.Zero;
try
{
ulong cPropertySets;
var res = rowsetInfo.GetProperties(1, [dbPropIdSet], out cPropertySets, out prgPropSetsPtr);
if (res != 0)
{
Logger.LogError($"Error getting properties: {res}");
return null;
}
if (cPropertySets == 0 || prgPropSetsPtr == IntPtr.Zero)
{
Logger.LogError("No property sets returned");
return null;
}
var firstPropSetPtr = (DBPROPSET*)prgPropSetsPtr.ToInt64();
var propSet = *firstPropSetPtr;
if (propSet.cProperties == 0 || propSet.rgProperties == IntPtr.Zero)
{
return null;
}
var propPtr = (DBPROP*)propSet.rgProperties.ToInt64();
return *propPtr;
}
catch (Exception ex)
{
Logger.LogError($"Exception occurred while getting properties,", ex);
return null;
}
finally
{
// Free the property sets pointer returned by GetProperties, if necessary
if (prgPropSetsPtr != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(prgPropSetsPtr);
}
}
}
private uint GetReuseWhereId(IRowset rowset)
{
var rowsetInfo = (IRowsetInfo)rowset;
if (rowsetInfo is null)
{
return 0;
}
var prop = GetPropset(rowsetInfo);
if (prop is null)
{
return 0;
}
if (prop?.vValue.VarType == VarEnum.VT_UI4)
{
var value = prop?.vValue._ulong;
return (uint)value;
}
return 0;
}
public void Dispose() public void Dispose()
{ {
CancelOutstandingQueries(); CancelOutstandingQueries();
// Free the allocated memory for rgPropertyIDs
if (dbPropIdSet.rgPropertyIDs != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(dbPropIdSet.rgPropertyIDs);
}
queryCompletedEvent?.Dispose();
} }
internal enum QueryState
{
NotStarted = 0,
Running,
Completed,
NoResults,
AllNoise,
NullDataSource,
CreateSessionFailed,
CreateCommandFailed,
ExecuteFailed,
Cancelled,
}
private readonly record struct ExecuteCommandResult(
IRowset Rowset,
QueryState State,
int? HResult,
string ErrorMessage);
} }

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using ManagedCommon; using ManagedCommon;
using ManagedCsWin32; using ManagedCsWin32;
@@ -11,20 +10,16 @@ using Microsoft.CmdPal.Ext.Indexer.Indexer.SystemSearch;
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
internal sealed partial class QueryStringBuilder internal static class QueryStringBuilder
{ {
private const string Properties = "System.ItemUrl, System.ItemNameDisplay, path, System.Search.EntryID, System.Kind, System.KindText"; private const string Properties = "System.ItemUrl, System.ItemNameDisplay, path, System.Search.EntryID, System.Kind, System.KindText";
private const string SystemIndex = "SystemIndex"; private const string SystemIndex = "SystemIndex";
private const string ScopeFileConditions = "SCOPE='file:'"; private const string ScopeFileConditions = "SCOPE='file:'";
private const string OrderConditions = "System.DateModified DESC"; private const string OrderConditions = "System.DateModified DESC";
private const string SelectQueryWithScope = "SELECT " + Properties + " FROM " + SystemIndex + " WHERE (" + ScopeFileConditions + ")";
private const string SelectQueryWithScopeAndOrderConditions = SelectQueryWithScope + " ORDER BY " + OrderConditions;
private static ISearchQueryHelper queryHelper; private static ISearchQueryHelper queryHelper;
public static string GeneratePrimingQuery() => SelectQueryWithScopeAndOrderConditions; public static string GenerateQuery(string searchText)
public static string GenerateQuery(string searchText, uint whereId)
{ {
if (queryHelper is null) if (queryHelper is null)
{ {
@@ -40,7 +35,7 @@ internal sealed partial class QueryStringBuilder
throw; throw;
} }
ISearchCatalogManager catalogManager = searchManager.GetCatalog(SystemIndex); var catalogManager = searchManager.GetCatalog(SystemIndex);
if (catalogManager is null) if (catalogManager is null)
{ {
throw new ArgumentException($"Failed to get catalog manager for {SystemIndex}"); throw new ArgumentException($"Failed to get catalog manager for {SystemIndex}");
@@ -57,7 +52,7 @@ internal sealed partial class QueryStringBuilder
queryHelper.SetQuerySorting(OrderConditions); queryHelper.SetQuerySorting(OrderConditions);
} }
queryHelper.SetQueryWhereRestrictions("AND " + ScopeFileConditions + "AND ReuseWhere(" + whereId.ToString(CultureInfo.InvariantCulture) + ")"); queryHelper.SetQueryWhereRestrictions($"AND {ScopeFileConditions}");
return queryHelper.GenerateSQLFromUserQuery(searchText); return queryHelper.GenerateSQLFromUserQuery(searchText);
} }
} }

View File

@@ -2,10 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Indexer.Indexer; using Microsoft.CmdPal.Ext.Indexer.Indexer;
using Microsoft.CmdPal.Ext.Indexer.Properties; using Microsoft.CmdPal.Ext.Indexer.Properties;
@@ -16,18 +19,25 @@ namespace Microsoft.CmdPal.Ext.Indexer;
internal sealed partial class IndexerPage : DynamicListPage, IDisposable internal sealed partial class IndexerPage : DynamicListPage, IDisposable
{ {
// Cookie to identify our queries; since we replace the SearchEngine on each search,
// this can be a constant.
private const uint HardQueryCookie = 10;
private readonly List<IListItem> _indexerListItems = []; private readonly List<IListItem> _indexerListItems = [];
private readonly SearchEngine _searchEngine; private readonly Lock _searchLock = new();
private readonly bool disposeSearchEngine = true;
private uint _queryCookie; private SearchEngine? _searchEngine;
private string initialQuery = string.Empty;
private CancellationTokenSource? _searchCts;
private string _initialQuery = string.Empty;
private bool _isEmptyQuery = true; private bool _isEmptyQuery = true;
private CommandItem _noSearchEmptyContent; private CommandItem? _noSearchEmptyContent;
private CommandItem _nothingFoundEmptyContent; private CommandItem? _nothingFoundEmptyContent;
private bool _deferredLoad;
public override ICommandItem EmptyContent => _isEmptyQuery ? _noSearchEmptyContent! : _nothingFoundEmptyContent!;
public IndexerPage() public IndexerPage()
{ {
@@ -35,8 +45,8 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
Icon = Icons.FileExplorerIcon; Icon = Icons.FileExplorerIcon;
Name = Resources.Indexer_Title; Name = Resources.Indexer_Title;
PlaceholderText = Resources.Indexer_PlaceholderText; PlaceholderText = Resources.Indexer_PlaceholderText;
_searchEngine = new(); _searchEngine = new();
_queryCookie = 10;
var filters = new SearchFilters(); var filters = new SearchFilters();
filters.PropChanged += Filters_PropChanged; filters.PropChanged += Filters_PropChanged;
@@ -45,22 +55,23 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
CreateEmptyContent(); CreateEmptyContent();
} }
public IndexerPage(string query, SearchEngine searchEngine, uint queryCookie, IList<IListItem> firstPageData) public IndexerPage(string query)
{ {
Icon = Icons.FileExplorerIcon; Icon = Icons.FileExplorerIcon;
Name = Resources.Indexer_Title; Name = Resources.Indexer_Title;
_searchEngine = searchEngine;
_queryCookie = queryCookie; _searchEngine = new();
_indexerListItems.AddRange(firstPageData);
initialQuery = query; _initialQuery = query;
SearchText = query; SearchText = query;
disposeSearchEngine = false;
var filters = new SearchFilters(); var filters = new SearchFilters();
filters.PropChanged += Filters_PropChanged; filters.PropChanged += Filters_PropChanged;
Filters = filters; Filters = filters;
CreateEmptyContent(); CreateEmptyContent();
IsLoading = true;
_deferredLoad = true;
} }
private void CreateEmptyContent() private void CreateEmptyContent()
@@ -95,8 +106,6 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
ShellHelpers.OpenInShell(command); ShellHelpers.OpenInShell(command);
} }
public override ICommandItem EmptyContent => _isEmptyQuery ? _noSearchEmptyContent : _nothingFoundEmptyContent;
private void Filters_PropChanged(object sender, IPropChangedEventArgs args) private void Filters_PropChanged(object sender, IPropChangedEventArgs args)
{ {
PerformSearch(SearchText); PerformSearch(SearchText);
@@ -104,35 +113,31 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
public override void UpdateSearchText(string oldSearch, string newSearch) public override void UpdateSearchText(string oldSearch, string newSearch)
{ {
if (oldSearch != newSearch && newSearch != initialQuery) if (oldSearch != newSearch && newSearch != _initialQuery)
{ {
PerformSearch(newSearch); PerformSearch(newSearch);
} }
} }
private void PerformSearch(string newSearch) public override IListItem[] GetItems()
{ {
var actualSearch = FullSearchString(newSearch); if (_deferredLoad)
_ = Task.Run(() =>
{ {
_isEmptyQuery = string.IsNullOrWhiteSpace(actualSearch); PerformSearch(_initialQuery);
Query(actualSearch); _deferredLoad = false;
LoadMore(); }
OnPropertyChanged(nameof(EmptyContent));
initialQuery = null;
});
}
public override IListItem[] GetItems() => [.. _indexerListItems]; return [.. _indexerListItems];
}
private string FullSearchString(string query) private string FullSearchString(string query)
{ {
switch (Filters.CurrentFilterId) switch (Filters?.CurrentFilterId)
{ {
case "folders": case "folders":
return $"{query} kind:folders"; return $"System.Kind:folders {query}";
case "files": case "files":
return $"{query} kind:NOT folders"; return $"System.Kind:NOT folders {query}";
case "all": case "all":
default: default:
return query; return query;
@@ -141,28 +146,139 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
public override void LoadMore() public override void LoadMore()
{ {
var ct = Volatile.Read(ref _searchCts)?.Token;
IsLoading = true; IsLoading = true;
var results = _searchEngine.FetchItems(_indexerListItems.Count, 20, _queryCookie, out var hasMore);
_indexerListItems.AddRange(results); var hasMore = false;
HasMoreItems = hasMore; SearchEngine? searchEngine;
IsLoading = false; int offset;
RaiseItemsChanged(_indexerListItems.Count);
lock (_searchLock)
{
searchEngine = _searchEngine;
offset = _indexerListItems.Count;
}
var results = searchEngine?.FetchItems(offset, 20, queryCookie: HardQueryCookie, out hasMore) ?? [];
if (ct?.IsCancellationRequested == true)
{
IsLoading = false;
return;
}
lock (_searchLock)
{
if (ct?.IsCancellationRequested == true)
{
IsLoading = false;
return;
}
_indexerListItems.AddRange(results);
HasMoreItems = hasMore;
IsLoading = false;
RaiseItemsChanged(_indexerListItems.Count);
}
} }
private void Query(string query) private void Query(string query)
{ {
++_queryCookie; lock (_searchLock)
_indexerListItems.Clear(); {
_indexerListItems.Clear();
_searchEngine?.Query(query, queryCookie: HardQueryCookie);
}
}
_searchEngine.Query(query, _queryCookie); private void ReplaceSearchEngine(SearchEngine newSearchEngine)
{
SearchEngine? oldEngine;
lock (_searchLock)
{
oldEngine = _searchEngine;
_searchEngine = newSearchEngine;
}
oldEngine?.Dispose();
}
private void PerformSearch(string newSearch)
{
var actualSearch = FullSearchString(newSearch);
var newCts = new CancellationTokenSource();
var oldCts = Interlocked.Exchange(ref _searchCts, newCts);
oldCts?.Cancel();
oldCts?.Dispose();
var ct = newCts.Token;
_ = Task.Run(
() =>
{
ct.ThrowIfCancellationRequested();
lock (_searchLock)
{
// If the user hasn't provided any base query text, results should be empty
// regardless of the currently selected filter.
_isEmptyQuery = string.IsNullOrWhiteSpace(newSearch);
if (_isEmptyQuery)
{
_indexerListItems.Clear();
HasMoreItems = false;
IsLoading = false;
RaiseItemsChanged(0);
OnPropertyChanged(nameof(EmptyContent));
_initialQuery = string.Empty;
return;
}
// Track the most recent query we initiated, so UpdateSearchText doesn't
// spuriously suppress a search when SearchText gets set programmatically.
_initialQuery = newSearch;
}
ct.ThrowIfCancellationRequested();
ReplaceSearchEngine(new SearchEngine());
ct.ThrowIfCancellationRequested();
Query(actualSearch);
ct.ThrowIfCancellationRequested();
LoadMore();
ct.ThrowIfCancellationRequested();
lock (_searchLock)
{
OnPropertyChanged(nameof(EmptyContent));
}
},
ct);
} }
public void Dispose() public void Dispose()
{ {
if (disposeSearchEngine) var cts = Interlocked.Exchange(ref _searchCts, null);
cts?.Cancel();
cts?.Dispose();
SearchEngine? searchEngine;
lock (_searchLock)
{ {
_searchEngine.Dispose(); searchEngine = _searchEngine;
GC.SuppressFinalize(this); _searchEngine = null;
_indexerListItems.Clear();
} }
searchEngine?.Dispose();
GC.SuppressFinalize(this);
} }
} }

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources { internal class Resources {
@@ -177,6 +177,15 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to The query matches multiple items.
/// </summary>
internal static string Indexer_Fallback_MultipleResults_Subtitle {
get {
return ResourceManager.GetString("Indexer_Fallback_MultipleResults_Subtitle", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Search for &quot;{0}&quot; in files. /// Looks up a localized string similar to Search for &quot;{0}&quot; in files.
/// </summary> /// </summary>

View File

@@ -211,4 +211,7 @@ You can try searching all files on this PC or adjust your indexing settings.</va
<data name="Indexer_Command_Peek_Failed" xml:space="preserve"> <data name="Indexer_Command_Peek_Failed" xml:space="preserve">
<value>Failed to launch Peek</value> <value>Failed to launch Peek</value>
</data> </data>
<data name="Indexer_Fallback_MultipleResults_Subtitle" xml:space="preserve">
<value>The query matches multiple items</value>
</data>
</root> </root>

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@@ -16,15 +18,20 @@ namespace Microsoft.CmdPal.Ext.Indexer;
public sealed partial class SearchEngine : IDisposable public sealed partial class SearchEngine : IDisposable
{ {
private SearchQuery _searchQuery = new(); private SearchQuery? _searchQuery = new();
public void Query(string query, uint queryCookie) public void Query(string query, uint queryCookie)
{ {
// _indexerListItems.Clear(); var searchQuery = _searchQuery;
_searchQuery.SearchResults.Clear(); if (searchQuery is null)
_searchQuery.CancelOutstandingQueries(); {
return;
}
if (query == string.Empty) searchQuery.SearchResults.Clear();
searchQuery.CancelOutstandingQueries();
if (string.IsNullOrWhiteSpace(query))
{ {
return; return;
} }
@@ -32,64 +39,74 @@ public sealed partial class SearchEngine : IDisposable
Stopwatch stopwatch = new(); Stopwatch stopwatch = new();
stopwatch.Start(); stopwatch.Start();
_searchQuery.Execute(query, queryCookie); searchQuery.Execute(query, queryCookie);
stopwatch.Stop(); stopwatch.Stop();
Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\""); Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\"");
} }
public IList<IListItem> FetchItems(int offset, int limit, uint queryCookie, out bool hasMore) public IList<IListItem> FetchItems(int offset, int limit, uint queryCookie, out bool hasMore, bool noIcons = false)
{ {
hasMore = false; hasMore = false;
var results = new List<IListItem>();
if (_searchQuery is not null) var searchQuery = _searchQuery;
if (searchQuery is null)
{ {
var cookie = _searchQuery.Cookie; return [];
if (cookie == queryCookie)
{
var index = 0;
SearchResult result;
// var hasMoreItems = _searchQuery.FetchRows(_indexerListItems.Count, limit);
var hasMoreItems = _searchQuery.FetchRows(offset, limit);
while (!_searchQuery.SearchResults.IsEmpty && _searchQuery.SearchResults.TryDequeue(out result) && ++index <= limit)
{
IconInfo icon = null;
try
{
var stream = ThumbnailHelper.GetThumbnail(result.LaunchUri).Result;
if (stream is not null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to get the icon.", ex);
}
results.Add(new IndexerListItem(new IndexerItem
{
FileName = result.ItemDisplayName,
FullPath = result.LaunchUri,
})
{
Icon = icon,
});
}
hasMore = hasMoreItems;
}
} }
var cookie = searchQuery.Cookie;
if (cookie != queryCookie)
{
return [];
}
var results = new List<IListItem>();
var index = 0;
var hasMoreItems = searchQuery.FetchRows(offset, limit);
while (!searchQuery.SearchResults.IsEmpty && searchQuery.SearchResults.TryDequeue(out var result) && ++index <= limit)
{
var indexerListItem = new IndexerListItem(new IndexerItem
{
FileName = result.ItemDisplayName,
FullPath = result.LaunchUri,
});
if (!noIcons)
{
IconInfo? icon = null;
try
{
var stream = ThumbnailHelper.GetThumbnail(result.LaunchUri).Result;
if (stream is not null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to get the icon.", ex);
}
indexerListItem.Icon = icon;
}
results.Add(indexerListItem);
}
hasMore = hasMoreItems;
return results; return results;
} }
public void Dispose() public void Dispose()
{ {
var searchQuery = _searchQuery;
_searchQuery = null; _searchQuery = null;
searchQuery?.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }