mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 17:56:44 +02:00
[cmdpal] Support search any file in fallback command (#38455)
## Summary of the Pull Request 1. Add new setting to control this behaviour 2. Support always on and only when file exist in the fallback command 3. Change the condition of updateFallbackCommand in toplevelvm demo: https://github.com/user-attachments/assets/19e4ced3-30ad-44f4-8f3a-93620f46bb3d ## PR Checklist - [x] **Closes:** #38370 --------- Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com> Co-authored-by: Mike Griese <migrie@microsoft.com>
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -10,8 +13,14 @@ using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
internal sealed partial class FallbackOpenFileItem : FallbackCommandItem
|
||||
internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System.IDisposable
|
||||
{
|
||||
private readonly CompositeFormat fallbackItemSearchPageTitleCompositeFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title);
|
||||
|
||||
private readonly SearchEngine _searchEngine = new();
|
||||
|
||||
private uint _queryCookie = 10;
|
||||
|
||||
public FallbackOpenFileItem()
|
||||
: base(new NoOpCommand(), Resources.Indexer_Find_Path_fallback_display_title)
|
||||
{
|
||||
@@ -21,8 +30,20 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
Command = new NoOpCommand();
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Icon = null;
|
||||
MoreCommands = 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, FileName = Path.GetFileName(query) };
|
||||
var listItemForUs = new IndexerListItem(item, IncludeBrowseCommand.AsDefault);
|
||||
Command = listItemForUs.Command;
|
||||
@@ -43,12 +64,65 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Command = new NoOpCommand();
|
||||
_queryCookie++;
|
||||
|
||||
try
|
||||
{
|
||||
_searchEngine.Query(query, _queryCookie);
|
||||
var results = _searchEngine.FetchItems(0, 20, _queryCookie, out var _);
|
||||
|
||||
if (results.Count == 0 || ((results[0] as IndexerListItem) == null))
|
||||
{
|
||||
// Exit 2: We searched for the file, and found nothing. Oh well.
|
||||
// Hide ourselves.
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Command = new NoOpCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.Count == 1)
|
||||
{
|
||||
// Exit 3: We searched for the file, and found exactly one thing. Awesome!
|
||||
// Return it.
|
||||
Title = results[0].Title;
|
||||
Subtitle = results[0].Subtitle;
|
||||
Icon = results[0].Icon;
|
||||
Command = results[0].Command;
|
||||
MoreCommands = results[0].MoreCommands;
|
||||
|
||||
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.FileExplorer;
|
||||
Subtitle = Resources.Indexer_Subtitle;
|
||||
Command = indexerPage;
|
||||
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Icon = null;
|
||||
Command = new NoOpCommand();
|
||||
MoreCommands = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_searchEngine.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,22 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Indexer;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
internal sealed partial class IndexerPage : DynamicListPage, IDisposable
|
||||
{
|
||||
private readonly List<IListItem> _indexerListItems = [];
|
||||
private readonly SearchEngine _searchEngine;
|
||||
private readonly bool disposeSearchEngine = true;
|
||||
|
||||
private SearchQuery _searchQuery = new();
|
||||
private uint _queryCookie;
|
||||
|
||||
private uint _queryCookie = 10;
|
||||
private string initialQuery = string.Empty;
|
||||
|
||||
public IndexerPage()
|
||||
{
|
||||
@@ -30,16 +27,31 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
|
||||
Icon = Icons.FileExplorer;
|
||||
Name = Resources.Indexer_Title;
|
||||
PlaceholderText = Resources.Indexer_PlaceholderText;
|
||||
_searchEngine = new();
|
||||
_queryCookie = 10;
|
||||
}
|
||||
|
||||
public IndexerPage(string query, SearchEngine searchEngine, uint queryCookie, IList<IListItem> firstPageData)
|
||||
{
|
||||
Icon = Icons.FileExplorer;
|
||||
Name = Resources.Indexer_Title;
|
||||
_searchEngine = searchEngine;
|
||||
_queryCookie = queryCookie;
|
||||
_indexerListItems.AddRange(firstPageData);
|
||||
initialQuery = query;
|
||||
SearchText = query;
|
||||
disposeSearchEngine = false;
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
if (oldSearch != newSearch)
|
||||
if (oldSearch != newSearch && newSearch != initialQuery)
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
Query(newSearch);
|
||||
LoadMore();
|
||||
initialQuery = string.Empty;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,7 +61,9 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
|
||||
public override void LoadMore()
|
||||
{
|
||||
IsLoading = true;
|
||||
FetchItems(20);
|
||||
var results = _searchEngine.FetchItems(_indexerListItems.Count, 20, _queryCookie, out var hasMore);
|
||||
_indexerListItems.AddRange(results);
|
||||
HasMoreItems = hasMore;
|
||||
IsLoading = false;
|
||||
RaiseItemsChanged(_indexerListItems.Count);
|
||||
}
|
||||
@@ -58,70 +72,16 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
|
||||
{
|
||||
++_queryCookie;
|
||||
_indexerListItems.Clear();
|
||||
_searchQuery.SearchResults.Clear();
|
||||
_searchQuery.CancelOutstandingQueries();
|
||||
|
||||
if (query == string.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
|
||||
_searchQuery.Execute(query, _queryCookie);
|
||||
|
||||
stopwatch.Stop();
|
||||
Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\"");
|
||||
}
|
||||
|
||||
private void FetchItems(int limit)
|
||||
{
|
||||
if (_searchQuery != null)
|
||||
{
|
||||
var cookie = _searchQuery.Cookie;
|
||||
if (cookie == _queryCookie)
|
||||
{
|
||||
var index = 0;
|
||||
SearchResult result;
|
||||
|
||||
var hasMoreItems = _searchQuery.FetchRows(_indexerListItems.Count, 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 != null)
|
||||
{
|
||||
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
|
||||
icon = new IconInfo(data, data);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get the icon.", ex);
|
||||
}
|
||||
|
||||
_indexerListItems.Add(new IndexerListItem(new IndexerItem
|
||||
{
|
||||
FileName = result.ItemDisplayName,
|
||||
FullPath = result.LaunchUri,
|
||||
})
|
||||
{
|
||||
Icon = icon,
|
||||
});
|
||||
}
|
||||
|
||||
HasMoreItems = hasMoreItems;
|
||||
}
|
||||
}
|
||||
_searchEngine.Query(query, _queryCookie);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_searchQuery = null;
|
||||
GC.SuppressFinalize(this);
|
||||
if (disposeSearchEngine)
|
||||
{
|
||||
_searchEngine.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,15 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Search for "{0}" in files.
|
||||
/// </summary>
|
||||
internal static string Indexer_fallback_searchPage_title {
|
||||
get {
|
||||
return ResourceManager.GetString("Indexer_fallback_searchPage_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This file doesn't exist.
|
||||
/// </summary>
|
||||
@@ -168,6 +177,42 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Always on.
|
||||
/// </summary>
|
||||
internal static string Indexer_Settings_FallbackCommand_AlwaysOn {
|
||||
get {
|
||||
return ResourceManager.GetString("Indexer_Settings_FallbackCommand_AlwaysOn", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Only when file path exist.
|
||||
/// </summary>
|
||||
internal static string Indexer_Settings_FallbackCommand_FilePathExist {
|
||||
get {
|
||||
return ResourceManager.GetString("Indexer_Settings_FallbackCommand_FilePathExist", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Shows file search results on the top-level search results.
|
||||
/// </summary>
|
||||
internal static string Indexer_Settings_FallbackCommand_Mode {
|
||||
get {
|
||||
return ResourceManager.GetString("Indexer_Settings_FallbackCommand_Mode", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Always off.
|
||||
/// </summary>
|
||||
internal static string Indexer_Settings_FallbackCommand_Off {
|
||||
get {
|
||||
return ResourceManager.GetString("Indexer_Settings_FallbackCommand_Off", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Search files on this device.
|
||||
/// </summary>
|
||||
|
||||
@@ -156,10 +156,25 @@
|
||||
<data name="Indexer_PlaceholderText" xml:space="preserve">
|
||||
<value>Search for files and folders...</value>
|
||||
</data>
|
||||
<data name="Indexer_Settings_FallbackCommand_AlwaysOn" xml:space="preserve">
|
||||
<value>Always on</value>
|
||||
</data>
|
||||
<data name="Indexer_Settings_FallbackCommand_Mode" xml:space="preserve">
|
||||
<value>Shows file search results on the top-level search results</value>
|
||||
</data>
|
||||
<data name="Indexer_Settings_FallbackCommand_Off" xml:space="preserve">
|
||||
<value>Always off</value>
|
||||
</data>
|
||||
<data name="Indexer_Settings_FallbackCommand_FilePathExist" xml:space="preserve">
|
||||
<value>Only when file path exist</value>
|
||||
</data>
|
||||
<data name="Indexer_Subtitle" xml:space="preserve">
|
||||
<value>Search files on this device</value>
|
||||
</data>
|
||||
<data name="Indexer_Title" xml:space="preserve">
|
||||
<value>Search files</value>
|
||||
</data>
|
||||
<data name="Indexer_fallback_searchPage_title" xml:space="preserve">
|
||||
<value>Search for "{0}" in files</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Indexer;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
public sealed partial class SearchEngine : IDisposable
|
||||
{
|
||||
private SearchQuery _searchQuery = new();
|
||||
|
||||
public void Query(string query, uint queryCookie)
|
||||
{
|
||||
// _indexerListItems.Clear();
|
||||
_searchQuery.SearchResults.Clear();
|
||||
_searchQuery.CancelOutstandingQueries();
|
||||
|
||||
if (query == string.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
|
||||
_searchQuery.Execute(query, queryCookie);
|
||||
|
||||
stopwatch.Stop();
|
||||
Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\"");
|
||||
}
|
||||
|
||||
public IList<IListItem> FetchItems(int offset, int limit, uint queryCookie, out bool hasMore)
|
||||
{
|
||||
hasMore = false;
|
||||
var results = new List<IListItem>();
|
||||
if (_searchQuery != null)
|
||||
{
|
||||
var cookie = _searchQuery.Cookie;
|
||||
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 != 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;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_searchQuery = null;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user