Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,16 @@ internal static int ScoreTopLevelItem(

// Alias matching: exact match is overwhelming priority, substring match adds a small boost
var aliasBoost = isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0);
var totalMatch = matchScore + aliasBoost;

// Title matching: exact title match ranks just below alias, prefix match gets moderate boost.
// Prefix boost requires query length >= 3 so single-character queries don't overwhelm history.
var titleMatchBoost = string.Equals(title, query.Original, StringComparison.OrdinalIgnoreCase)
? 9000
: query.Original.Length >= 3 && title.StartsWith(query.Original, StringComparison.OrdinalIgnoreCase)
? 100
: 0;

var totalMatch = matchScore + aliasBoost + titleMatchBoost;

// Apply scaling and history boost only if we matched something real
var finalScore = totalMatch * 10;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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.Common.Text;
using Microsoft.CmdPal.UI.ViewModels.MainPage;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Windows.Foundation;

namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;

[TestClass]
public partial class MainListPageScoringTests
{
private readonly IPrecomputedFuzzyMatcher _matcher = new PrecomputedFuzzyMatcher();
private readonly IRecentCommandsManager _history;

public MainListPageScoringTests()
{
var historyMock = new Mock<IRecentCommandsManager>();
historyMock.Setup(h => h.GetCommandHistoryWeight(It.IsAny<string>())).Returns(0);
_history = historyMock.Object;
}

private int Score(string query, string title)
{
var fuzzyQuery = _matcher.PrecomputeQuery(query);
var item = new MockListItem { Title = title };
return MainListPage.ScoreTopLevelItem(in fuzzyQuery, item, _history, _matcher);
}

[TestMethod]
public void ExactTitleMatch_ScoresHigherThan_SubstringMatch()
{
var exactScore = Score("Terminal", "Terminal");
var substringScore = Score("Terminal", "Windows Terminal");

Assert.IsTrue(
exactScore > substringScore,
$"Exact match score ({exactScore}) should be higher than substring match ({substringScore})");
}

[TestMethod]
public void ExactTitleMatch_CaseInsensitive()
{
var lowerScore = Score("terminal", "Terminal");
var upperScore = Score("TERMINAL", "Terminal");

Assert.IsTrue(lowerScore > 0, "Case-insensitive exact match should score > 0");
Assert.IsTrue(upperScore > 0, "Case-insensitive exact match should score > 0");

var substringScore = Score("terminal", "Windows Terminal");
Assert.IsTrue(
lowerScore > substringScore,
$"Case-insensitive exact match ({lowerScore}) should beat substring ({substringScore})");
}

[TestMethod]
public void PrefixMatch_ScoresHigherThan_SubstringMatch()
{
// Prefix boost requires query length >= 3
var prefixScore = Score("Term", "Terminal");
var substringScore = Score("Term", "Windows Terminal");

Assert.IsTrue(
prefixScore > substringScore,
$"Prefix match score ({prefixScore}) should be higher than substring match ({substringScore})");
}

[TestMethod]
public void AliasExactMatch_StillWinsOverTitleExactMatch()
{
// Alias exact match gives 9001, title exact match gives 9000
// This test verifies the hierarchy is preserved
var titleExactScore = Score("Terminal", "Terminal");

// The title exact match boost is 9000, scaled by 10 = 90000+
// Alias boost is 9001, scaled by 10 = 90010+
// As long as title exact < alias exact, hierarchy is correct
Assert.IsTrue(titleExactScore > 0, "Title exact match should produce positive score");
}

[TestMethod]
public void ExactMatch_WithMultipleCandidates_RanksCorrectly()
{
var exactScore = Score("Terminal", "Terminal");
var prefixScore = Score("Terminal", "Terminal Emulator");
var containsScore = Score("Terminal", "Windows Terminal");

Assert.IsTrue(
exactScore > prefixScore,
$"Exact match ({exactScore}) should beat prefix match ({prefixScore})");
Assert.IsTrue(
prefixScore >= containsScore,
$"Prefix match ({prefixScore}) should beat or tie substring match ({containsScore})");
}

private sealed partial class MockListItem : IListItem
{
public string Title { get; set; } = string.Empty;

public string Subtitle { get; set; } = string.Empty;

public ICommand Command => new NoOpCommand();

public IDetails? Details => null;

public IIconInfo? Icon => null;

public string Section => string.Empty;

public ITag[] Tags => [];

public string TextToSuggest => string.Empty;

public IContextItem[] MoreCommands => [];

#pragma warning disable CS0067
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
#pragma warning restore CS0067
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -442,20 +442,31 @@ public void ValidateUsageEventuallyHelps()
var q = fuzzyMatcher.PrecomputeQuery("C");

// We're gonna run this test and keep adding more uses of VS Code till
// it breaks past Command Prompt
// it breaks past Command Prompt. The exact crossover iteration depends on
// fuzzy score margins that can vary across platforms, so we only assert
// that the overtake eventually happens within a reasonable number of uses.
var vsCodeId = items[1].Id;
for (var i = 0; i < 10; i++)
var vsCodeOvertook = false;
for (var i = 0; i < 20; i++)
{
history = history.WithHistoryItem(vsCodeId);

var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).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);
var cmdIndex = weightedMatches.FindIndex(m => m.Title == "Command Prompt");
var codeIndex = weightedMatches.FindIndex(m => m.Title == "Visual Studio Code");
Assert.IsTrue(cmdIndex >= 0, "Command Prompt should always appear in results");
Assert.IsTrue(codeIndex >= 0, "Visual Studio Code should always appear in results");

if (codeIndex < cmdIndex)
{
vsCodeOvertook = true;
break;
}
}

Assert.IsTrue(vsCodeOvertook, "After enough usage, VS Code should eventually overtake Command Prompt for query 'C'");
}
}
Loading