mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
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:
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -44,6 +44,7 @@ ALLCHILDREN
|
|||||||
ALLINPUT
|
ALLINPUT
|
||||||
Allman
|
Allman
|
||||||
Allmodule
|
Allmodule
|
||||||
|
ALLNOISE
|
||||||
ALLOWUNDO
|
ALLOWUNDO
|
||||||
ALLVIEW
|
ALLVIEW
|
||||||
ALPHATYPE
|
ALPHATYPE
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 "{0}" in files.
|
/// Looks up a localized string similar to Search for "{0}" in files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user