mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
Still a WIP, but here's the deets so far: ## No more throwing canceled tokens Throwing exceptions is expensive and since we essentially cancel tokens anytime someone is typing beyond the debounce, we could be throwing exceptions a ton during search. Since we don't care about those past executions, now they just `return`. ## Reduced number of apps returned in search While users can specify how many apps (no limit, 1, 5), if they specify no limit, we hard limit it at 10. For a few reasons, fuzzy search gets _really_ fuzzy sometimes and gives answers that users would think is just plain wrong and they make the response list longer than it needs to be. ## Fuzzy search: still fuzzy, but faster Replaced `StringMatcher` class with `FuzzyStringMatcher`. `FuzzyStringMatcher` is a C# port by @zadjii-msft of the Rust port by @lhecker for [microsoft/edit](https://github.com/microsoft/edit), which I believe originally came from [VS Code](https://github.com/microsoft/vscode). It's a whole fuzzy rabbit hole. But it's faster than the `StringMatcher` class it replaced. ## Fallbacks, you need to fall back "In the beginning, fallbacks were created. This had made many people very angry and has been widely regarded as a bad move." Hitchhiker's Guide to the Galaxy jokes aside, fallbacks are one cause of slower search results. A few modifications have been made to get them out of the way without reverting their ability to do things dynamically. 1. Fallbacks are no longer scored and will always* appear at the bottom of the search results 2. In updating their search text, we now use a cancellation token to stop processing previous searches when a new keypress is recorded. ## * But Calculator & Run are special So, remember when I said that all fallbacks will not be ranked and always display at the bottom of the results? Surprise, some will be ranked and displayed based on that score. Specifically, Calculator and Run are fallbacks that are whitelisted from the restrictions mentioned above. They will continue to act as they do today. We do have the ability to add future fallbacks to that whitelist as well. --- ## Current preview Updated: 2025-09-24 https://github.com/user-attachments/assets/c74c9a8e-e438-4101-840b-1408d2acaefd --- Closes #39763 Closes #39239 Closes #39948 Closes #38594 Closes #40330
176 lines
6.1 KiB
C#
176 lines
6.1 KiB
C#
// 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.CommandPalette.Extensions.Toolkit;
|
|
|
|
public partial class ListHelpers
|
|
{
|
|
// Generate a score for a list item.
|
|
public static int ScoreListItem(string query, ICommandItem listItem)
|
|
{
|
|
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(listItem.Title))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var nameMatchScore = FuzzyStringMatcher.ScoreFuzzy(query, listItem.Title);
|
|
|
|
// var locNameMatch = StringMatcher.FuzzySearch(query, NameLocalized);
|
|
var descriptionMatchScore = FuzzyStringMatcher.ScoreFuzzy(query, listItem.Subtitle);
|
|
|
|
// var executableNameMatch = StringMatcher.FuzzySearch(query, ExePath);
|
|
// var locExecutableNameMatch = StringMatcher.FuzzySearch(query, ExecutableNameLocalized);
|
|
// var lnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableName);
|
|
// var locLnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableNameLocalized);
|
|
// var score = new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, executableNameMatch.Score }.Max();
|
|
return new[] { nameMatchScore, (descriptionMatchScore - 4) / 2, 0 }.Max();
|
|
}
|
|
|
|
public static IEnumerable<IListItem> FilterList(IEnumerable<IListItem> items, string query)
|
|
{
|
|
var scores = items
|
|
.Select(li => new ScoredListItem() { ListItem = li, Score = ScoreListItem(query, li) })
|
|
.Where(score => score.Score > 0)
|
|
.OrderByDescending(score => score.Score);
|
|
return scores
|
|
.Select(score => score.ListItem);
|
|
}
|
|
|
|
public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
|
|
{
|
|
return FilterListWithScores<T>(items, query, scoreFunction)
|
|
.Select(score => score.Item);
|
|
}
|
|
|
|
public static IEnumerable<Scored<T>> FilterListWithScores<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
|
|
{
|
|
var scores = items
|
|
.Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) })
|
|
.Where(score => score.Score > 0)
|
|
.OrderByDescending(score => score.Score);
|
|
return scores;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modifies the contents of `original` in-place, to match those of
|
|
/// `newContents`. The canonical use being:
|
|
/// ```cs
|
|
/// ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(ItemsToFilter, TextToFilterOn));
|
|
/// ```
|
|
/// </summary>
|
|
/// <typeparam name="T">Any type that can be compared for equality</typeparam>
|
|
/// <param name="original">Collection to modify</param>
|
|
/// <param name="newContents">The enumerable which `original` should match</param>
|
|
public static void InPlaceUpdateList<T>(IList<T> original, IEnumerable<T> newContents)
|
|
where T : class
|
|
{
|
|
InPlaceUpdateList(original, newContents, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modifies the contents of `original` in-place, to match those of
|
|
/// `newContents`. The canonical use being:
|
|
/// ```cs
|
|
/// ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(ItemsToFilter, TextToFilterOn));
|
|
/// ```
|
|
/// </summary>
|
|
/// <typeparam name="T">Any type that can be compared for equality</typeparam>
|
|
/// <param name="original">Collection to modify</param>
|
|
/// <param name="newContents">The enumerable which `original` should match</param>
|
|
/// <param name="removedItems">List of items that were removed from the original collection</param>
|
|
public static void InPlaceUpdateList<T>(IList<T> original, IEnumerable<T> newContents, out List<T> removedItems)
|
|
where T : class
|
|
{
|
|
removedItems = [];
|
|
|
|
// we're not changing newContents - stash this so we don't re-evaluate it every time
|
|
var numberOfNew = newContents.Count();
|
|
|
|
// Short circuit - new contents should just be empty
|
|
if (numberOfNew == 0)
|
|
{
|
|
removedItems.AddRange(original);
|
|
original.Clear();
|
|
return;
|
|
}
|
|
|
|
var i = 0;
|
|
foreach (var newItem in newContents)
|
|
{
|
|
if (i >= original.Count)
|
|
{
|
|
break;
|
|
}
|
|
|
|
for (var j = i; j < original.Count; j++)
|
|
{
|
|
var og_2 = original[j];
|
|
var areEqual_2 = og_2?.Equals(newItem) ?? false;
|
|
if (areEqual_2)
|
|
{
|
|
for (var k = i; k < j; k++)
|
|
{
|
|
// This item from the original list was not in the new list. Remove it.
|
|
removedItems.Add(original[i]);
|
|
original.RemoveAt(i);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
var og = original[i];
|
|
var areEqual = og?.Equals(newItem) ?? false;
|
|
|
|
// Is this new item already in the list?
|
|
if (areEqual)
|
|
{
|
|
// It is already in the list
|
|
}
|
|
else
|
|
{
|
|
// it isn't. Add it.
|
|
original.Insert(i, newItem);
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
// Remove any extra trailing items from the destination
|
|
while (original.Count > numberOfNew)
|
|
{
|
|
// RemoveAtEnd
|
|
removedItems.Add(original[original.Count - 1]);
|
|
original.RemoveAt(original.Count - 1);
|
|
}
|
|
|
|
// Add any extra trailing items from the source
|
|
if (original.Count < numberOfNew)
|
|
{
|
|
var remaining = newContents.Skip(original.Count);
|
|
foreach (var item in remaining)
|
|
{
|
|
original.Add(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct ScoredListItem
|
|
{
|
|
public int Score;
|
|
public IListItem ListItem;
|
|
}
|
|
|
|
public struct Scored<T>
|
|
{
|
|
public int Score;
|
|
public T Item;
|
|
}
|