Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions PowerToys.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Indexer.UnitTests/Microsoft.CmdPal.Ext.Indexer.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Registry.UnitTests/Microsoft.CmdPal.Ext.Registry.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
Expand Down
3 changes: 2 additions & 1 deletion src/modules/cmdpal/CommandPalette.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Calc.UnitTests\\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Indexer.UnitTests\\Microsoft.CmdPal.Ext.Indexer.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Registry.UnitTests\\Microsoft.CmdPal.Ext.Registry.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Shell.UnitTests\\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj",
Expand Down Expand Up @@ -54,4 +55,4 @@
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.CmdPal.Ext.Indexer.UnitTests;

[TestClass]
public class ImplicitWildcardQueryBuilderTests
{
[DataTestMethod]
[DataRow("term", null, "((CONTAINS(System.ItemNameDisplay, '\"term\"') OR CONTAINS(System.ItemNameDisplay, '\"term*\"')) OR System.FileName LIKE '%term%')", "System.FileName LIKE '%term%'")]
[DataRow("term Kind:Folder", "Kind:Folder", "((CONTAINS(System.ItemNameDisplay, '\"term\"') OR CONTAINS(System.ItemNameDisplay, '\"term*\"')) OR System.FileName LIKE '%term%')", "System.FileName LIKE '%term%'")]
[DataRow("System.Kind:folders term", "System.Kind:folders", "((CONTAINS(System.ItemNameDisplay, '\"term\"') OR CONTAINS(System.ItemNameDisplay, '\"term*\"')) OR System.FileName LIKE '%term%')", "System.FileName LIKE '%term%'")]
[DataRow("System.Kind:NOT folders term", "System.Kind:NOT folders", "((CONTAINS(System.ItemNameDisplay, '\"term\"') OR CONTAINS(System.ItemNameDisplay, '\"term*\"')) OR System.FileName LIKE '%term%')", "System.FileName LIKE '%term%'")]
[DataRow("\"two words\"", null, "((CONTAINS(System.ItemNameDisplay, '\"two words\"') OR CONTAINS(System.ItemNameDisplay, '\"two words*\"') OR CONTAINS(System.ItemNameDisplay, '\"two\" AND \"words\"') OR CONTAINS(System.ItemNameDisplay, '\"two*\" AND \"words*\"')) OR System.FileName LIKE '%two words%')", "System.FileName LIKE '%two words%'")]
[DataRow("foo bar", null, "((CONTAINS(System.ItemNameDisplay, '\"foo bar\"') OR CONTAINS(System.ItemNameDisplay, '\"foo bar*\"') OR CONTAINS(System.ItemNameDisplay, '\"foo\" AND \"bar\"') OR CONTAINS(System.ItemNameDisplay, '\"foo*\" AND \"bar*\"')) OR (System.FileName LIKE '%foo%' AND System.FileName LIKE '%bar%'))", "(System.FileName LIKE '%foo%' AND System.FileName LIKE '%bar%')")]
[DataRow("foo-bar", null, "((CONTAINS(System.ItemNameDisplay, '\"foo bar\"') OR CONTAINS(System.ItemNameDisplay, '\"foo bar*\"') OR CONTAINS(System.ItemNameDisplay, '\"foo\" AND \"bar\"') OR CONTAINS(System.ItemNameDisplay, '\"foo*\" AND \"bar*\"')) OR System.FileName LIKE '%foo-bar%')", "System.FileName LIKE '%foo-bar%'")]
[DataRow("foo & bar", null, "((CONTAINS(System.ItemNameDisplay, '\"foo bar\"') OR CONTAINS(System.ItemNameDisplay, '\"foo bar*\"') OR CONTAINS(System.ItemNameDisplay, '\"foo\" AND \"bar\"') OR CONTAINS(System.ItemNameDisplay, '\"foo*\" AND \"bar*\"')) OR (System.FileName LIKE '%foo%' AND System.FileName LIKE '%bar%'))", "(System.FileName LIKE '%foo%' AND System.FileName LIKE '%bar%')")]
[DataRow("tonträger", null, "((CONTAINS(System.ItemNameDisplay, '\"tonträger\"') OR CONTAINS(System.ItemNameDisplay, '\"tonträger*\"')) OR System.FileName LIKE '%tonträger%')", "System.FileName LIKE '%tonträger%'")]
[DataRow("O'Hara", null, "((CONTAINS(System.ItemNameDisplay, '\"Hara\"') OR CONTAINS(System.ItemNameDisplay, '\"Hara*\"')) OR System.FileName LIKE '%O''Hara%')", "System.FileName LIKE '%O''Hara%'")]
[DataRow("AT&T", null, "System.FileName LIKE '%AT&T%'", null)]
[DataRow("file_100%", null, "((CONTAINS(System.ItemNameDisplay, '\"file 100\"') OR CONTAINS(System.ItemNameDisplay, '\"file 100*\"') OR CONTAINS(System.ItemNameDisplay, '\"file\" AND \"100\"') OR CONTAINS(System.ItemNameDisplay, '\"file*\" AND \"100*\"')) OR System.FileName LIKE '%file[_]100[%]%')", "System.FileName LIKE '%file[_]100[%]%'")]
public void BuildExpandedQuery_BuildsExpectedRestrictions(string query, string expectedStructuredSearchText, string expectedPrimaryClause, string expectedFallbackClause)
{
var expandedQuery = ImplicitWildcardQueryBuilder.BuildExpandedQuery(query);

Assert.AreEqual(expectedStructuredSearchText, expandedQuery.StructuredSearchText);
Assert.AreEqual(expectedPrimaryClause, expandedQuery.PrimaryRestriction);
Assert.AreEqual(expectedFallbackClause, expandedQuery.FallbackRestriction);
}

[TestMethod]
public void BuildExpandedQuery_PreservesBracketWrappedTermAsLiteralOnly()
{
var expandedQuery = ImplicitWildcardQueryBuilder.BuildExpandedQuery("[red]");

Assert.AreEqual("System.FileName LIKE '%[[]red[]]%'", expandedQuery.PrimaryRestriction);
Assert.IsFalse(expandedQuery.HasFallbackRestriction);
}

[TestMethod]
public void BuildExpandedQuery_TreatsSinglePercentAsLiteralCharacter()
{
var expandedQuery = ImplicitWildcardQueryBuilder.BuildExpandedQuery("%");

Assert.AreEqual("System.FileName LIKE '%[%]%'", expandedQuery.PrimaryRestriction);
Assert.IsFalse(expandedQuery.HasFallbackRestriction);
}

[TestMethod]
public void BuildExpandedQuery_TreatsSingleUnderscoreAsLiteralCharacter()
{
var expandedQuery = ImplicitWildcardQueryBuilder.BuildExpandedQuery("_");

Assert.AreEqual("System.FileName LIKE '%[_]%'", expandedQuery.PrimaryRestriction);
Assert.IsFalse(expandedQuery.HasFallbackRestriction);
}

[DataTestMethod]
[DataRow("kind:folder")]
[DataRow("name:term")]
[DataRow("name: term")]
[DataRow("name:\"two words\"")]
[DataRow("*term*")]
[DataRow("C:\\Users")]
[DataRow("System.Kind:folders")]
[DataRow("kind:folder AND term")]
public void BuildExpandedQuery_DoesNotBroadenStructuredOrExplicitQueries(string query)
{
var expandedQuery = ImplicitWildcardQueryBuilder.BuildExpandedQuery(query);

Assert.IsFalse(expandedQuery.HasPrimaryRestriction);
Assert.IsFalse(expandedQuery.HasFallbackRestriction);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />

<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.Indexer.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CmdPal.Ext.Indexer.Indexer;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.CmdPal.Ext.Indexer.UnitTests;

[TestClass]
public class SearchNoticeInfoBuilderTests
{
[DataTestMethod]
[DataRow((int)SearchQuery.QueryState.NullDataSource)]
[DataRow((int)SearchQuery.QueryState.CreateSessionFailed)]
[DataRow((int)SearchQuery.QueryState.CreateCommandFailed)]
public void FromQueryStatus_ReturnsUnavailableNotice_ForInfrastructureFailures(int stateValue)
{
var state = (SearchQuery.QueryState)stateValue;
var notice = SearchNoticeInfoBuilder.FromQueryStatus(new SearchQuery.SearchExecutionStatus(state, null, "failure"));

Assert.IsNotNull(notice);
Assert.AreEqual(Resources.Indexer_SearchUnavailableMessage, notice.Value.Title);
Assert.AreEqual(Resources.Indexer_SearchUnavailableMessageTip, notice.Value.Subtitle);
}

[TestMethod]
public void FromQueryStatus_ReturnsUnavailableNotice_ForRpcFailures()
{
var notice = SearchNoticeInfoBuilder.FromQueryStatus(
new SearchQuery.SearchExecutionStatus(
SearchQuery.QueryState.ExecuteFailed,
unchecked((int)0x800706BA),
"RPC server unavailable"));

Assert.IsNotNull(notice);
Assert.AreEqual(Resources.Indexer_SearchUnavailableMessage, notice.Value.Title);
}

[TestMethod]
public void FromQueryStatus_ReturnsGenericFailureNotice_ForUnexpectedFailures()
{
var notice = SearchNoticeInfoBuilder.FromQueryStatus(
new SearchQuery.SearchExecutionStatus(
SearchQuery.QueryState.ExecuteFailed,
unchecked((int)0x80004005),
"unexpected"));

Assert.IsNotNull(notice);
Assert.AreEqual(Resources.Indexer_SearchFailedMessage, notice.Value.Title);
Assert.AreEqual(Resources.Indexer_SearchFailedMessageTip, notice.Value.Subtitle);
}

[DataTestMethod]
[DataRow((int)SearchQuery.QueryState.Completed)]
[DataRow((int)SearchQuery.QueryState.NoResults)]
[DataRow((int)SearchQuery.QueryState.AllNoise)]
public void FromQueryStatus_ReturnsNull_ForNonFailureStates(int stateValue)
{
var state = (SearchQuery.QueryState)stateValue;
var notice = SearchNoticeInfoBuilder.FromQueryStatus(new SearchQuery.SearchExecutionStatus(state, null, null));

Assert.IsNull(notice);
}

[TestMethod]
public void FromCatalogStatus_ReturnsIndexingNotice_WhenItemsArePending()
{
var notice = SearchNoticeInfoBuilder.FromCatalogStatus(new SearchCatalogStatus(42, null));

Assert.IsNotNull(notice);
Assert.AreEqual(Resources.Indexer_SearchIndexingMessage, notice.Value.Title);
StringAssert.Contains(notice.Value.Subtitle, "42");
}

[TestMethod]
public void FromCatalogStatus_ReturnsNull_WhenStatusReadFails()
{
var notice = SearchNoticeInfoBuilder.FromCatalogStatus(new SearchCatalogStatus(0, unchecked((int)0x800706BA)));

Assert.IsNull(notice);
}

[TestMethod]
public void FromCatalogStatus_ReturnsNull_WhenIndexingIsIdle()
{
var notice = SearchNoticeInfoBuilder.FromCatalogStatus(new SearchCatalogStatus(0, null));

Assert.IsNull(notice);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Helpers;
using Microsoft.CmdPal.Ext.Indexer.Indexer;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
Expand Down Expand Up @@ -120,12 +121,19 @@ private void ProcessSearchQuery(string query, CancellationToken ct)
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 results = searchEngine.FetchItems(0, 2, queryCookie: HardQueryCookie, out _, out var notice, noIcons: true);
var count = results.Count;

if (count == 0)
{
ClearResultForCurrentQuery(ct);
if (notice is { } searchNotice)
{
UpdateSearchNoticeForCurrentQuery(query, searchNotice, ct);
}
else
{
ClearResultForCurrentQuery(ct);
}
}
else if (count == 1)
{
Expand Down Expand Up @@ -233,6 +241,29 @@ private bool UpdateResultForCurrentQuery(string title, string subtitle, IIconInf
}
}

private bool UpdateSearchNoticeForCurrentQuery(string query, SearchNoticeInfo notice, CancellationToken ct)
{
var indexerPage = new IndexerPage(query);
var set = UpdateResultForCurrentQuery(
notice.Title,
notice.Subtitle,
Icons.FileExplorerIcon,
indexerPage,
[
new CommandContextItem(new OpenUrlCommand("ms-settings:search") { Name = Resources.Indexer_Command_OpenIndexerSettings! }),
],
null,
skipIcon: false,
ct);

if (!set)
{
indexerPage.Dispose();
}

return set;
}

private void UpdateIconForCurrentQuery(IIconInfo icon, CancellationToken ct)
{
lock (_resultLock)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,26 @@ public static IDBInitialize GetDataSource()

private static bool InitializeDataSource()
{
var riid = typeof(IDBInitialize).GUID;

try
{
_dataSource = ComHelper.CreateComInstance<IDBInitialize>(ref Unsafe.AsRef(in CLSID.CollatorDataSource), CLSCTX.InProcServer);
}
catch (Exception e)
catch (Exception ex)
{
Logger.LogError($"Failed to create datasource. ex: {e.Message}");
Logger.LogError("Failed to create datasource.", ex);
return false;
}

_dataSource.Initialize();
try
{
_dataSource.Initialize();
}
catch (Exception ex)
{
Logger.LogError("Failed to initialize datasource.", ex);
_dataSource = null;
return false;
}

return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Ext.Indexer.Indexer;

internal readonly record struct SearchCatalogStatus(uint PendingItemsCount, int? HResult)
{
public bool IsAvailable => HResult is null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using ManagedCommon;
using ManagedCsWin32;
using Microsoft.CmdPal.Ext.Indexer.Indexer.SystemSearch;

namespace Microsoft.CmdPal.Ext.Indexer.Indexer;

internal static class SearchCatalogStatusReader
{
private const string SystemIndex = "SystemIndex";
private static readonly Lock FailureLoggingLock = new();
private static int? _lastLoggedFailureHResult;

internal static SearchCatalogStatus GetStatus()
{
try
{
var catalogManager = CreateCatalogManager();
var pendingItemsCount = catalogManager.NumberOfItemsToIndex();
ResetFailureLoggingState();
return new SearchCatalogStatus(pendingItemsCount, null);
}
catch (Exception ex)
{
LogFailure(ex);
return new SearchCatalogStatus(0, ex.HResult);
}
}

private static ISearchCatalogManager CreateCatalogManager()
{
var searchManager = ComHelper.CreateComInstance<ISearchManager>(ref Unsafe.AsRef(in CLSID.SearchManager), CLSCTX.LocalServer);
var catalogManager = searchManager.GetCatalog(SystemIndex);
return catalogManager ?? throw new ArgumentException($"Failed to get catalog manager for {SystemIndex}");
}

private static void LogFailure(Exception ex)
{
var shouldLogWarning = false;

lock (FailureLoggingLock)
{
if (_lastLoggedFailureHResult != ex.HResult)
{
_lastLoggedFailureHResult = ex.HResult;
shouldLogWarning = true;
}
}

var message = $"Failed to read Windows Search catalog status. HResult=0x{ex.HResult:X8}, Message={ex.Message}";
if (shouldLogWarning)
{
Logger.LogWarning(message);
}
else
{
Logger.LogDebug(message);
}
}

private static void ResetFailureLoggingState()
{
lock (FailureLoggingLock)
{
_lastLoggedFailureHResult = null;
}
}
}
Loading
Loading