mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 03:07:56 +01:00
CmdPal: adjust frecency weighting (#42242)
In #41959 we changed the string matcher's weighting. It ended up giving us lower scores than before. That meant that the weighting from recent commands was far too heavy, and it was polluting the results. Basically any command that you'd run would be like, 30 characters of weight heavier than anything you haven't. This increases the weight of all string matches by 10x. That means something like `Command Prompt` will get a string matched weight of `100` instead of `10`. This balances better with the weighting from frecency (where the MRU command gets +35, then `+min(5*uses,35)`, for up to 70 points of weight) It also adds a bunch of tests here, to try and catch this again in the future. Closes #42158
This commit is contained in:
@@ -825,6 +825,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -2995,6 +2997,14 @@ Global
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.Build.0 = Debug|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -3323,6 +3333,7 @@ Global
|
||||
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
|
||||
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
@@ -160,7 +160,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
List<Scored<IListItem>> limitedApps = new List<Scored<IListItem>>();
|
||||
var limitedApps = new List<Scored<IListItem>>();
|
||||
|
||||
// Fuzzy matching can produce a lot of results, so we want to limit the
|
||||
// number of apps we show at once if it's a large set.
|
||||
@@ -273,9 +273,9 @@ public partial class MainListPage : DynamicListPage,
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<IListItem> newFilteredItems = Enumerable.Empty<IListItem>();
|
||||
IEnumerable<IListItem> newFallbacks = Enumerable.Empty<IListItem>();
|
||||
IEnumerable<IListItem> newApps = Enumerable.Empty<IListItem>();
|
||||
var newFilteredItems = Enumerable.Empty<IListItem>();
|
||||
var newFallbacks = Enumerable.Empty<IListItem>();
|
||||
var newApps = Enumerable.Empty<IListItem>();
|
||||
|
||||
if (_filteredItems is not null)
|
||||
{
|
||||
@@ -339,8 +339,11 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands!;
|
||||
Func<string, IListItem, int> scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); };
|
||||
|
||||
// Produce a list of everything that matches the current filter.
|
||||
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem)];
|
||||
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, scoreItem)];
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -358,7 +361,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Produce a list of filtered apps with the appropriate limit
|
||||
if (newApps.Any())
|
||||
{
|
||||
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, ScoreTopLevelItem);
|
||||
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, scoreItem);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -425,7 +428,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
|
||||
// fact that we want fallback handlers down-weighted, so that they don't
|
||||
// _always_ show up first.
|
||||
private int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem)
|
||||
internal static int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem, IRecentCommandsManager history)
|
||||
{
|
||||
var title = topLevelOrAppItem.Title;
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
@@ -501,10 +504,9 @@ public partial class MainListPage : DynamicListPage,
|
||||
// here we add the recent command weight boost
|
||||
//
|
||||
// Otherwise something like `x` will still match everything you've run before
|
||||
var finalScore = matchSomething;
|
||||
var finalScore = matchSomething * 10;
|
||||
if (matchSomething > 0)
|
||||
{
|
||||
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands;
|
||||
var recentWeightBoost = history.GetCommandHistoryWeight(id);
|
||||
finalScore += recentWeightBoost;
|
||||
}
|
||||
@@ -521,7 +523,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
AppStateModel.SaveState(state);
|
||||
}
|
||||
|
||||
private string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
{
|
||||
if (topLevelOrAppItem is TopLevelViewModel topLevel)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.CmdPal.UI.ViewModels.UnitTests")]
|
||||
@@ -7,7 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class RecentCommandsManager : ObservableObject
|
||||
public partial class RecentCommandsManager : ObservableObject, IRecentCommandsManager
|
||||
{
|
||||
[JsonInclude]
|
||||
internal List<HistoryItem> History { get; set; } = [];
|
||||
@@ -80,3 +80,10 @@ public partial class RecentCommandsManager : ObservableObject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRecentCommandsManager
|
||||
{
|
||||
int GetCommandHistoryWeight(string commandId);
|
||||
|
||||
void AddHistoryItem(string commandId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>Microsoft.CmdPal.UI.ViewModels.UnitTests</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="WyHash" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,444 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Windows.Foundation;
|
||||
using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
private static RecentCommandsManager CreateHistory(IList<string>? commandIds = null)
|
||||
{
|
||||
var history = new RecentCommandsManager();
|
||||
if (commandIds != null)
|
||||
{
|
||||
foreach (var item in commandIds)
|
||||
{
|
||||
history.AddHistoryItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateBasicHistoryService()
|
||||
{
|
||||
var commonCommands = new List<string>
|
||||
{
|
||||
"com.microsoft.cmdpal.shell",
|
||||
"com.microsoft.cmdpal.windowwalker",
|
||||
"Visual Studio 2022 Preview_6533433915015224980",
|
||||
"com.microsoft.cmdpal.reload",
|
||||
"com.microsoft.cmdpal.shell",
|
||||
};
|
||||
|
||||
return CreateHistory(commonCommands);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryFunctionality()
|
||||
{
|
||||
// Setup
|
||||
var history = CreateHistory();
|
||||
|
||||
// Act
|
||||
history.AddHistoryItem("com.microsoft.cmdpal.shell");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell") > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryWeighting()
|
||||
{
|
||||
// Setup
|
||||
var history = CreateBasicHistoryService();
|
||||
|
||||
// Act
|
||||
var shellWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell");
|
||||
var windowWalkerWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.windowwalker");
|
||||
var vsWeight = history.GetCommandHistoryWeight("Visual Studio 2022 Preview_6533433915015224980");
|
||||
var reloadWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.reload");
|
||||
var nonExistentWeight = history.GetCommandHistoryWeight("non.existent.command");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(shellWeight > windowWalkerWeight, "Shell should be weighted higher than Window Walker, more uses");
|
||||
Assert.IsTrue(vsWeight > windowWalkerWeight, "Visual Studio should be weighted higher than Window Walker, because recency");
|
||||
Assert.AreEqual(reloadWeight, vsWeight, "both reload and VS were used in the last three commands, same weight");
|
||||
Assert.IsTrue(shellWeight > vsWeight, "VS and run were both used in the last 3, but shell has 2 more frequency");
|
||||
Assert.AreEqual(0, nonExistentWeight, "Nonexistent command should have zero weight");
|
||||
}
|
||||
|
||||
private sealed partial record ListItemMock(
|
||||
string Title,
|
||||
string? Subtitle = "",
|
||||
string? GivenId = "",
|
||||
string? ProviderId = "") : IListItem
|
||||
{
|
||||
public string Id => string.IsNullOrEmpty(GivenId) ? GenerateId() : GivenId;
|
||||
|
||||
public IDetails Details => throw new System.NotImplementedException();
|
||||
|
||||
public string Section => throw new System.NotImplementedException();
|
||||
|
||||
public ITag[] Tags => throw new System.NotImplementedException();
|
||||
|
||||
public string TextToSuggest => throw new System.NotImplementedException();
|
||||
|
||||
public ICommand Command => new NoOpCommand() { Id = Id };
|
||||
|
||||
public IIconInfo Icon => throw new System.NotImplementedException();
|
||||
|
||||
public IContextItem[] MoreCommands => throw new System.NotImplementedException();
|
||||
|
||||
#pragma warning disable CS0067
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
|
||||
#pragma warning restore CS0067
|
||||
|
||||
private string GenerateId()
|
||||
{
|
||||
// Use WyHash64 to generate stable ID hashes.
|
||||
// manually seeding with 0, so that the hash is stable across launches
|
||||
var result = WyHash64.ComputeHash64(ProviderId + Title + Subtitle, seed: 0);
|
||||
return $"{ProviderId}{result}";
|
||||
}
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateHistory(IList<ListItemMock> items)
|
||||
{
|
||||
var history = new RecentCommandsManager();
|
||||
foreach (var item in items)
|
||||
{
|
||||
history.AddHistoryItem(item.Id);
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateMocksWork()
|
||||
{
|
||||
// Setup
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", "idA", "providerA"),
|
||||
new("Command B", "Subtitle B", GivenId: "idB"),
|
||||
new("Command C", "Subtitle C", ProviderId: "providerC"),
|
||||
new("Command A", "Subtitle A", "idA", "providerA"), // Duplicate to test incrementing uses
|
||||
};
|
||||
|
||||
// Act
|
||||
var history = CreateHistory(items);
|
||||
|
||||
// Assert
|
||||
foreach (var item in items)
|
||||
{
|
||||
var weight = history.GetCommandHistoryWeight(item.Id);
|
||||
Assert.IsTrue(weight > 0, $"Item {item.Title} should have a weight greater than zero.");
|
||||
}
|
||||
|
||||
// Check that the duplicate item has a higher weight due to increased uses
|
||||
var weightA = history.GetCommandHistoryWeight("idA");
|
||||
var weightB = history.GetCommandHistoryWeight("idB");
|
||||
var weightC = history.GetCommandHistoryWeight(items[2].Id); // providerC generated ID
|
||||
Assert.IsTrue(weightA > weightB, "Item A should have a higher weight than Item B due to more uses.");
|
||||
Assert.IsTrue(weightA > weightC, "Item A should have a higher weight than Item C due to more uses.");
|
||||
Assert.AreEqual(weightC, weightB, "Item C and Item B were used in the last 3 commands");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryBuckets()
|
||||
{
|
||||
// Setup
|
||||
// (these will be checked in reverse order, so that A is the most recent)
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", GivenId: "idA"), // #0 -> bucket 0
|
||||
new("Command B", "Subtitle B", GivenId: "idB"), // #1 -> bucket 0
|
||||
new("Command C", "Subtitle C", GivenId: "idC"), // #2 -> bucket 0
|
||||
new("Command D", "Subtitle D", GivenId: "idD"), // #3 -> bucket 1
|
||||
new("Command E", "Subtitle E", GivenId: "idE"), // #4 -> bucket 1
|
||||
new("Command F", "Subtitle F", GivenId: "idF"), // #5 -> bucket 1
|
||||
new("Command G", "Subtitle G", GivenId: "idG"), // #6 -> bucket 1
|
||||
new("Command H", "Subtitle H", GivenId: "idH"), // #7 -> bucket 1
|
||||
new("Command I", "Subtitle I", GivenId: "idI"), // #8 -> bucket 1
|
||||
new("Command J", "Subtitle J", GivenId: "idJ"), // #9 -> bucket 1
|
||||
new("Command K", "Subtitle K", GivenId: "idK"), // #10 -> bucket 1
|
||||
new("Command L", "Subtitle L", GivenId: "idL"), // #11 -> bucket 2
|
||||
new("Command M", "Subtitle M", GivenId: "idM"), // #12 -> bucket 2
|
||||
new("Command N", "Subtitle N", GivenId: "idN"), // #13 -> bucket 2
|
||||
new("Command O", "Subtitle O", GivenId: "idO"), // #14 -> bucket 2
|
||||
};
|
||||
|
||||
for (var i = items.Count; i <= 50; i++)
|
||||
{
|
||||
items.Add(new ListItemMock($"Command #{i}", GivenId: $"id{i}"));
|
||||
}
|
||||
|
||||
// Act
|
||||
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
|
||||
|
||||
// Assert
|
||||
// First three items should be in the top bucket
|
||||
var weightA = history.GetCommandHistoryWeight("idA");
|
||||
var weightB = history.GetCommandHistoryWeight("idB");
|
||||
var weightC = history.GetCommandHistoryWeight("idC");
|
||||
|
||||
Assert.AreEqual(weightA, weightB, "Items A and B were used in the last 3 commands");
|
||||
Assert.AreEqual(weightB, weightC, "Items B and C were used in the last 3 commands");
|
||||
|
||||
// Next eight items (3-10 inclusive) should be in the second bucket
|
||||
var weightD = history.GetCommandHistoryWeight("idD");
|
||||
var weightE = history.GetCommandHistoryWeight("idE");
|
||||
var weightF = history.GetCommandHistoryWeight("idF");
|
||||
var weightG = history.GetCommandHistoryWeight("idG");
|
||||
var weightH = history.GetCommandHistoryWeight("idH");
|
||||
var weightI = history.GetCommandHistoryWeight("idI");
|
||||
var weightJ = history.GetCommandHistoryWeight("idJ");
|
||||
var weightK = history.GetCommandHistoryWeight("idK");
|
||||
|
||||
Assert.AreEqual(weightD, weightE, "Items D and E were used in the last 10 commands");
|
||||
Assert.AreEqual(weightE, weightF, "Items E and F were used in the last 10 commands");
|
||||
Assert.AreEqual(weightF, weightG, "Items F and G were used in the last 10 commands");
|
||||
Assert.AreEqual(weightG, weightH, "Items G and H were used in the last 10 commands");
|
||||
Assert.AreEqual(weightH, weightI, "Items H and I were used in the last 10 commands");
|
||||
Assert.AreEqual(weightI, weightJ, "Items I and J were used in the last 10 commands");
|
||||
Assert.AreEqual(weightJ, weightK, "Items J and K were used in the last 10 commands");
|
||||
|
||||
// Items up to the 15th should be in the third bucket
|
||||
var weightL = history.GetCommandHistoryWeight("idL");
|
||||
var weightM = history.GetCommandHistoryWeight("idM");
|
||||
var weightN = history.GetCommandHistoryWeight("idN");
|
||||
var weightO = history.GetCommandHistoryWeight("idO");
|
||||
var weight15 = history.GetCommandHistoryWeight("id15");
|
||||
Assert.AreEqual(weightL, weightM, "Items L and M were used in the last 15 commands");
|
||||
Assert.AreEqual(weightM, weightN, "Items M and N were used in the last 15 commands");
|
||||
Assert.AreEqual(weightN, weightO, "Items N and O were used in the last 15 commands");
|
||||
Assert.AreEqual(weightO, weight15, "Items O and 15 were used in the last 15 commands");
|
||||
|
||||
// Items after that should be in the lowest buckets
|
||||
var weight0 = history.GetCommandHistoryWeight(items[0].Id);
|
||||
var weight3 = history.GetCommandHistoryWeight(items[3].Id);
|
||||
var weight11 = history.GetCommandHistoryWeight(items[11].Id);
|
||||
var weight16 = history.GetCommandHistoryWeight("id16");
|
||||
var weight20 = history.GetCommandHistoryWeight("id20");
|
||||
var weight30 = history.GetCommandHistoryWeight("id30");
|
||||
var weight40 = history.GetCommandHistoryWeight("id40");
|
||||
var weight49 = history.GetCommandHistoryWeight("id49");
|
||||
|
||||
Assert.IsTrue(weight0 > weight3);
|
||||
Assert.IsTrue(weight3 > weight11);
|
||||
Assert.IsTrue(weight11 > weight16);
|
||||
|
||||
Assert.AreEqual(weight16, weight20);
|
||||
Assert.AreEqual(weight20, weight30);
|
||||
Assert.IsTrue(weight30 > weight40);
|
||||
Assert.AreEqual(weight40, weight49);
|
||||
|
||||
// The 50th item has fallen out of the list now
|
||||
var weight50 = history.GetCommandHistoryWeight("id50");
|
||||
Assert.AreEqual(0, weight50, "Item 50 should have fallen out of the history list");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateSimpleScoring()
|
||||
{
|
||||
// Setup
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", GivenId: "idA"), // #0 -> bucket 0
|
||||
new("Command B", "Subtitle B", GivenId: "idB"), // #1 -> bucket 0
|
||||
new("Command C", "Subtitle C", GivenId: "idC"), // #2 -> bucket 0
|
||||
};
|
||||
|
||||
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
|
||||
|
||||
var scoreA = MainListPage.ScoreTopLevelItem("C", items[0], history);
|
||||
var scoreB = MainListPage.ScoreTopLevelItem("C", items[1], history);
|
||||
var scoreC = MainListPage.ScoreTopLevelItem("C", items[2], history);
|
||||
|
||||
// Assert
|
||||
// All of these equally match the query, and they're all in the same bucket,
|
||||
// so they should all have the same score.
|
||||
Assert.AreEqual(scoreA, scoreB, "Items A and B should have the same score");
|
||||
Assert.AreEqual(scoreB, scoreC, "Items B and C should have the same score");
|
||||
}
|
||||
|
||||
private static List<ListItemMock> CreateMockHistoryItems()
|
||||
{
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Visual Studio 2022"), // #0 -> bucket 0
|
||||
new("Visual Studio Code"), // #1 -> bucket 0
|
||||
new("Explore Mastodon", GivenId: "social.mastodon.explore"), // #2 -> bucket 0
|
||||
new("Run commands", Subtitle: "Executes commands (e.g. ping, cmd)", GivenId: "com.microsoft.cmdpal.run"), // #3 -> bucket 1
|
||||
new("Windows Settings"), // #4 -> bucket 1
|
||||
new("Command Prompt"), // #5 -> bucket 1
|
||||
new("Terminal Canary"), // #6 -> bucket 1
|
||||
};
|
||||
return items;
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateMockHistoryService(List<ListItemMock>? items = null)
|
||||
{
|
||||
var history = CreateHistory((items ?? CreateMockHistoryItems()).Reverse<ListItemMock>().ToList());
|
||||
return history;
|
||||
}
|
||||
|
||||
private sealed record ScoredItem(ListItemMock Item, int Score)
|
||||
{
|
||||
public string Title => Item.Title;
|
||||
|
||||
public override string ToString() => $"[{Score}]{Title}";
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> TieScoresToMatches(List<ListItemMock> items, List<int> scores)
|
||||
{
|
||||
if (items.Count != scores.Count)
|
||||
{
|
||||
throw new ArgumentException("Items and scores must have the same number of elements");
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
yield return new ScoredItem(items[i], scores[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> GetMatches(IEnumerable<ScoredItem> scoredItems)
|
||||
{
|
||||
var matches = scoredItems
|
||||
.Where(x => x.Score > 0)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.ToList();
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> GetMatches(List<ListItemMock> items, List<int> scores)
|
||||
{
|
||||
return GetMatches(TieScoresToMatches(items, scores));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateScoredWeightingSimple()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
var unweightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, emptyHistory)).ToList();
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
|
||||
Assert.AreEqual(unweightedScores.Count, weightedScores.Count, "Both score lists should have the same number of items");
|
||||
for (var i = 0; i < unweightedScores.Count; i++)
|
||||
{
|
||||
var unweighted = unweightedScores[i];
|
||||
var weighted = weightedScores[i];
|
||||
var item = items[i];
|
||||
if (item.Title.Contains('C', System.StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
Assert.IsTrue(unweighted >= 0, $"Item {item.Title} didn't match the query, so should have a weighted score of zero");
|
||||
Assert.IsTrue(weighted > unweighted, $"Item {item.Title} should have a higher weighted ({weighted}) score than unweighted ({unweighted})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreEqual(unweighted, 0, $"Item {item.Title} didn't match the query, so should have a weighted score of zero");
|
||||
Assert.AreEqual(unweighted, weighted);
|
||||
}
|
||||
}
|
||||
|
||||
var unweightedMatches = GetMatches(items, unweightedScores).ToList();
|
||||
Assert.AreEqual(4, unweightedMatches.Count);
|
||||
Assert.AreEqual("Command Prompt", unweightedMatches[0].Title, "Command Prompt should be the top match");
|
||||
Assert.AreEqual("Visual Studio Code", unweightedMatches[1].Title, "Visual Studio Code should be the second match");
|
||||
Assert.AreEqual("Terminal Canary", unweightedMatches[2].Title);
|
||||
Assert.AreEqual("Run commands", unweightedMatches[3].Title);
|
||||
|
||||
// Even after weighting for 1 use, Command Prompt should still be the top match.
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
Assert.AreEqual(4, weightedMatches.Count);
|
||||
Assert.AreEqual("Command Prompt", weightedMatches[0].Title);
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title);
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[2].Title);
|
||||
Assert.AreEqual("Run commands", weightedMatches[3].Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateTitlesAreMoreImportantThanHistory()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
|
||||
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
||||
|
||||
// Terminal is in bucket 1, VS Code is in bucket 0, but Terminal matches
|
||||
// the title better
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[0].Title, "Terminal should be the top match, title match");
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title, "VsCode does fuzzy match, but is less relevant than Terminal");
|
||||
Assert.AreEqual("Run commands", weightedMatches[2].Title, "run only matches on the subtitle");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateTitlesAreMoreImportantThanUsage()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
// Add extra uses of VS Code to try and push it above Terminal
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
history.AddHistoryItem(items[1].Id);
|
||||
}
|
||||
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
|
||||
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
||||
|
||||
// Terminal is in bucket 1, VS Code is in bucket 0, but Terminal matches
|
||||
// the title better
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[0].Title, "Terminal should be the top match, title match");
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title, "VsCode does fuzzy match, but is less relevant than Terminal");
|
||||
Assert.AreEqual("Run commands", weightedMatches[2].Title, "run only matches on the subtitle");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateUsageEventuallyHelps()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
// We're gonna run this test and keep adding more uses of VS Code till
|
||||
// it breaks past Command Prompt
|
||||
var vsCodeId = items[1].Id;
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
history.AddHistoryItem(vsCodeId);
|
||||
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
Assert.AreEqual(4, weightedMatches.Count);
|
||||
|
||||
var expectedCmdIndex = i < 5 ? 0 : 1;
|
||||
var expectedCodeIndex = i < 5 ? 1 : 0;
|
||||
Assert.AreEqual("Command Prompt", weightedMatches[expectedCmdIndex].Title);
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[expectedCodeIndex].Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user