Files
terminal/src/cascadia/ut_app/FzfTests.cpp
e82eric 74b5a0c975 Update command palette search to prioritize "longest substring" match. (#18700)
It's the fzf algorithm!

Repurposed work from #16586
- I think the fzf algo fits here where it optimizes to find the optimal
  match based on consecutive chars and word boundaries.
- There are some edge cases where a match with a small gap could get a
  higher score than a match of consecutive chars when the match with a
  gap has other bonuses (FirstChar * Boundary Bonus). This can be
  adjusted by adjusting the bonuses or removing them if needed.
- From reading the thread in #6693 it looked like you guys were leaning
  towards something like the fzf algo.
- License file is now updated in
  https://github.com/nvim-telescope/telescope-fzf-native.nvim repository
  - https://github.com/nvim-telescope/telescope-fzf-native.nvim/pull/148
  - https://github.com/junegunn/fzf/issues/4310
- Removed the following from the original implementation to minimize
  complexity and the size of the PR. (Let me know if any of these should
  be added back).
  - Query expressions "$:StartsWith ^:EndsWith |:Or !:Not etc" 
- Slab to avoid allocating the scoring matrix. This felt like overkill
  for the number of items in the command pallete.
- Fallback to V1 algorithm for very long strings. I want to say that the
  command palette won't have strings this long.
- Added the logic from GH#9941 that copies pattern and text chars to
  string for comparision with lstrcmpi
  - It does this twice now which isn't great...

Closes #6693

---------

Co-authored-by: Leonard Hecker <lhecker@microsoft.com>
2025-06-03 00:22:24 +00:00

569 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "..\TerminalApp\fzf\fzf.h"
using namespace Microsoft::Console;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace WEX::Common;
namespace TerminalAppUnitTests
{
typedef enum
{
ScoreMatch = 16,
ScoreGapStart = -3,
ScoreGapExtension = -1,
BonusBoundary = ScoreMatch / 2,
BonusNonWord = ScoreMatch / 2,
BonusCamel123 = BonusBoundary + ScoreGapExtension,
BonusConsecutive = -(ScoreGapStart + ScoreGapExtension),
BonusFirstCharMultiplier = 2,
} score_t;
class FzfTests
{
BEGIN_TEST_CLASS(FzfTests)
END_TEST_CLASS()
TEST_METHOD(AllPatternCharsDoNotMatch);
TEST_METHOD(ConsecutiveChars);
TEST_METHOD(ConsecutiveChars_FirstCharBonus);
TEST_METHOD(NonWordBonusBoundary_ConsecutiveChars);
TEST_METHOD(MatchOnNonWordChars_CaseInSensitive);
TEST_METHOD(MatchOnNonWordCharsWithGap);
TEST_METHOD(BonusForCamelCaseMatch);
TEST_METHOD(BonusBoundaryAndFirstCharMultiplier);
TEST_METHOD(MatchesAreCaseInSensitive);
TEST_METHOD(MultipleTerms);
TEST_METHOD(MultipleTerms_AllCharsMatch);
TEST_METHOD(MultipleTerms_NotAllTermsMatch);
TEST_METHOD(MatchesAreCaseInSensitive_BonusBoundary);
TEST_METHOD(TraceBackWillPickTheFirstMatchIfBothHaveTheSameScore);
TEST_METHOD(TraceBackWillPickTheMatchWithTheHighestScore);
TEST_METHOD(TraceBackWillPickTheMatchWithTheHighestScore_Gaps);
TEST_METHOD(TraceBackWillPickEarlierCharsWhenNoBonus);
TEST_METHOD(MatchWithGapCanAHaveHigherScoreThanConsecutiveWhenGapMatchHasBoundaryBonus);
TEST_METHOD(ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothHaveFirstCharBonus);
TEST_METHOD(ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothDontHaveBonus);
TEST_METHOD(MatchWithGapCanHaveHigherScoreThanConsecutiveWhenGapHasFirstCharBonus);
TEST_METHOD(MatchWithGapThatMatchesOnTheFirstCharWillNoLongerScoreHigherThanConsecutiveCharsWhenTheGapIs3_NoConsecutiveChar_4CharPattern);
TEST_METHOD(MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_2CharPattern);
TEST_METHOD(MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_3CharPattern_1ConsecutiveChar);
TEST_METHOD(MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs5_NoConsecutiveChars_3CharPattern);
TEST_METHOD(Russian_CaseMisMatch);
TEST_METHOD(Russian_CaseMatch);
TEST_METHOD(English_CaseMatch);
TEST_METHOD(English_CaseMisMatch);
TEST_METHOD(SurrogatePair);
TEST_METHOD(French_CaseMatch);
TEST_METHOD(French_CaseMisMatch);
TEST_METHOD(German_CaseMatch);
TEST_METHOD(German_CaseMisMatch_FoldResultsInMultipleCodePoints);
TEST_METHOD(Greek_CaseMisMatch);
TEST_METHOD(Greek_CaseMatch);
TEST_METHOD(SurrogatePair_ToUtf16Pos_ConsecutiveChars);
TEST_METHOD(SurrogatePair_ToUtf16Pos_PreferConsecutiveChars);
TEST_METHOD(SurrogatePair_ToUtf16Pos_GapAndBoundary);
};
void AssertScoreAndRuns(std::wstring_view patternText, std::wstring_view text, int expectedScore, const std::vector<fzf::matcher::TextRun>& expectedRuns)
{
const auto pattern = fzf::matcher::ParsePattern(patternText);
const auto match = fzf::matcher::Match(text, pattern);
if (expectedScore == 0 && expectedRuns.empty())
{
VERIFY_ARE_EQUAL(std::nullopt, match);
return;
}
VERIFY_IS_TRUE(match.has_value());
VERIFY_ARE_EQUAL(expectedScore, match->Score);
const auto& runs = match->Runs;
VERIFY_ARE_EQUAL(expectedRuns.size(), runs.size());
for (size_t i = 0; i < expectedRuns.size(); ++i)
{
VERIFY_ARE_EQUAL(expectedRuns[i].Start, runs[i].Start);
VERIFY_ARE_EQUAL(expectedRuns[i].End, runs[i].End);
}
}
void FzfTests::AllPatternCharsDoNotMatch()
{
AssertScoreAndRuns(
L"fbb",
L"foo bar",
0,
{});
}
void FzfTests::ConsecutiveChars()
{
AssertScoreAndRuns(
L"oba",
L"foobar",
ScoreMatch * 3 + BonusConsecutive * 2,
{ { 2, 4 } });
}
void FzfTests::ConsecutiveChars_FirstCharBonus()
{
AssertScoreAndRuns(
L"foo",
L"foobar",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
{ { 0, 2 } });
}
void FzfTests::NonWordBonusBoundary_ConsecutiveChars()
{
AssertScoreAndRuns(
L"zshc",
L"/man1/zshcompctl.1",
ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + BonusFirstCharMultiplier * BonusConsecutive * 3,
{ { 6, 9 } });
}
void FzfTests::Russian_CaseMisMatch()
{
AssertScoreAndRuns(
L"новая",
L"Новая вкладка",
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
{ { 0, 4 } });
}
void FzfTests::Russian_CaseMatch()
{
AssertScoreAndRuns(
L"Новая",
L"Новая вкладка",
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
{ { 0, 4 } });
}
void FzfTests::German_CaseMatch()
{
AssertScoreAndRuns(
L"fuß",
L"Fußball",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
{ { 0, 2 } });
}
void FzfTests::German_CaseMisMatch_FoldResultsInMultipleCodePoints()
{
//This doesn't currently pass, I think ucase_toFullFolding would give the number of code points that resulted from the fold.
//I wasn't sure how to reference that
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"true")
END_TEST_METHOD_PROPERTIES()
AssertScoreAndRuns(
L"fuss",
L"Fußball",
//I think ScoreMatch * 4 is correct in this case since it matches 4 codepoints pattern??? fuss
ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 3,
//Only 3 positions in the text were matched
{ { 0, 2 } });
}
void FzfTests::French_CaseMatch()
{
AssertScoreAndRuns(
L"Éco",
L"École",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
{ { 0, 2 } });
}
void FzfTests::French_CaseMisMatch()
{
AssertScoreAndRuns(
L"Éco",
L"école",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
{ { 0, 2 } });
}
void FzfTests::Greek_CaseMatch()
{
AssertScoreAndRuns(
L"λόγος",
L"λόγος",
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
{ { 0, 4 } });
}
void FzfTests::Greek_CaseMisMatch()
{
//I think this tests validates folding (σ, ς)
AssertScoreAndRuns(
L"λόγοσ",
L"λόγος",
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
{ { 0, 4 } });
}
void FzfTests::English_CaseMatch()
{
AssertScoreAndRuns(
L"Newer",
L"Newer tab",
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
{ { 0, 4 } });
}
void FzfTests::English_CaseMisMatch()
{
AssertScoreAndRuns(
L"newer",
L"Newer tab",
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
{ { 0, 4 } });
}
void FzfTests::SurrogatePair()
{
AssertScoreAndRuns(
L"N😀ewer",
L"N😀ewer tab",
ScoreMatch * 6 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 5,
{ { 0, 6 } });
}
void FzfTests::SurrogatePair_ToUtf16Pos_ConsecutiveChars()
{
AssertScoreAndRuns(
L"N𠀋N😀𝄞e𐐷",
L"N𠀋N😀𝄞e𐐷 tab",
ScoreMatch * 7 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 6,
{ { 0, 10 } });
}
void FzfTests::SurrogatePair_ToUtf16Pos_PreferConsecutiveChars()
{
AssertScoreAndRuns(
L"𠀋😀",
L"N𠀋😀wer 😀b𐐷 ",
ScoreMatch * 2 + BonusConsecutive * 2,
{ { 1, 4 } });
}
void FzfTests::SurrogatePair_ToUtf16Pos_GapAndBoundary()
{
AssertScoreAndRuns(
L"𠀋😀",
L"N𠀋wer 😀b𐐷 ",
ScoreMatch * 2 + ScoreGapStart + ScoreGapExtension * 3 + BonusBoundary,
{ { 1, 2 }, { 7, 8 } });
}
void FzfTests::MatchOnNonWordChars_CaseInSensitive()
{
AssertScoreAndRuns(
L"foo-b",
L"xFoo-Bar Baz",
(ScoreMatch + BonusCamel123 * BonusFirstCharMultiplier) +
(ScoreMatch + BonusCamel123) +
(ScoreMatch + BonusCamel123) +
(ScoreMatch + BonusBoundary) +
(ScoreMatch + BonusNonWord),
{ { 1, 5 } });
}
void FzfTests::MatchOnNonWordCharsWithGap()
{
AssertScoreAndRuns(
L"12356",
L"abc123 456",
(ScoreMatch + BonusCamel123 * BonusFirstCharMultiplier) +
(ScoreMatch + BonusCamel123) +
(ScoreMatch + BonusCamel123) +
ScoreGapStart +
ScoreGapExtension +
ScoreMatch +
ScoreMatch + BonusConsecutive,
{ { 3, 5 }, { 8, 9 } });
}
void FzfTests::BonusForCamelCaseMatch()
{
AssertScoreAndRuns(
L"def56",
L"abcDEF 456",
(ScoreMatch + BonusCamel123 * BonusFirstCharMultiplier) +
(ScoreMatch + BonusCamel123) +
(ScoreMatch + BonusCamel123) +
ScoreGapStart +
ScoreGapExtension +
ScoreMatch +
(ScoreMatch + BonusConsecutive),
{ { 3, 5 }, { 8, 9 } });
}
void FzfTests::BonusBoundaryAndFirstCharMultiplier()
{
AssertScoreAndRuns(
L"fbb",
L"foo bar baz",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension,
{ { 0, 0 }, { 4, 4 }, { 8, 8 } });
}
void FzfTests::MatchesAreCaseInSensitive()
{
AssertScoreAndRuns(
L"FBB",
L"foo bar baz",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension,
{ { 0, 0 }, { 4, 4 }, { 8, 8 } });
}
void FzfTests::MultipleTerms()
{
auto term1Score = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive);
auto term2Score = ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive) * 3;
AssertScoreAndRuns(
L"sp anta",
L"Split Pane, split: horizontal, profile: SSH: Antares",
term1Score + term2Score,
{ { 0, 1 }, { 45, 48 } });
}
void FzfTests::MultipleTerms_AllCharsMatch()
{
auto term1Score = ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive * 2);
auto term2Score = term1Score;
AssertScoreAndRuns(
L"foo bar",
L"foo bar",
term1Score + term2Score,
{ { 0, 2 }, { 4, 6 } });
}
void FzfTests::MultipleTerms_NotAllTermsMatch()
{
AssertScoreAndRuns(
L"sp anta zz",
L"Split Pane, split: horizontal, profile: SSH: Antares",
0,
{});
}
void FzfTests::MatchesAreCaseInSensitive_BonusBoundary()
{
AssertScoreAndRuns(
L"fbb",
L"Foo Bar Baz",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension,
{ { 0, 0 }, { 4, 4 }, { 8, 8 } });
}
void FzfTests::TraceBackWillPickTheFirstMatchIfBothHaveTheSameScore()
{
AssertScoreAndRuns(
L"bar",
L"Foo Bar Bar",
(ScoreMatch + BonusBoundary * BonusFirstCharMultiplier) +
(ScoreMatch + BonusBoundary) +
(ScoreMatch + BonusBoundary),
//ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier * 2,
{ { 4, 6 } });
}
void FzfTests::TraceBackWillPickTheMatchWithTheHighestScore()
{
AssertScoreAndRuns(
L"bar",
L"Foo aBar Bar",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier * 2,
{ { 9, 11 } });
}
void FzfTests::TraceBackWillPickTheMatchWithTheHighestScore_Gaps()
{
AssertScoreAndRuns(
L"bar",
L"Boo Author Raz Bar",
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
{ { 15, 17 } });
}
void FzfTests::TraceBackWillPickEarlierCharsWhenNoBonus()
{
AssertScoreAndRuns(
L"clts",
L"close all tabs after this",
ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + BonusFirstCharMultiplier * BonusConsecutive + ScoreGapStart + ScoreGapExtension * 7 + BonusBoundary + ScoreGapStart + ScoreGapExtension,
{ { 0, 1 }, { 10, 10 }, { 13, 13 } });
}
void FzfTests::ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothDontHaveBonus()
{
auto consecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2;
auto gapScore = (ScoreMatch * 3) + ScoreGapStart + ScoreGapStart;
AssertScoreAndRuns(
L"oob",
L"aoobar",
consecutiveScore,
{ { 1, 3 } });
AssertScoreAndRuns(
L"oob",
L"aoaoabound",
gapScore,
{ { 1, 1 }, { 3, 3 }, { 5, 5 } });
VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore);
}
void FzfTests::ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothHaveFirstCharBonus()
{
auto consecutiveScore = ScoreMatch * 3 + BonusFirstCharMultiplier * BonusBoundary + BonusFirstCharMultiplier * BonusConsecutive * 2;
auto gapScore = (ScoreMatch * 3) + (BonusBoundary * BonusFirstCharMultiplier) + ScoreGapStart + ScoreGapStart;
AssertScoreAndRuns(
L"oob",
L"oobar",
consecutiveScore,
{ { 0, 2 } });
AssertScoreAndRuns(
L"oob",
L"oaoabound",
gapScore,
{ { 0, 0 }, { 2, 2 }, { 4, 4 } });
VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore);
}
void FzfTests::MatchWithGapCanAHaveHigherScoreThanConsecutiveWhenGapMatchHasBoundaryBonus()
{
auto consecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2;
auto gapScore = (ScoreMatch * 3) + (BonusBoundary * BonusFirstCharMultiplier) + (BonusBoundary * 2) + ScoreGapStart + (ScoreGapExtension * 2) + ScoreGapStart + ScoreGapExtension;
AssertScoreAndRuns(
L"oob",
L"foobar",
consecutiveScore,
{ { 1, 3 } });
AssertScoreAndRuns(
L"oob",
L"out-of-bound",
gapScore,
{ { 0, 0 }, { 4, 4 }, { 7, 7 } });
VERIFY_IS_GREATER_THAN(gapScore, consecutiveScore);
}
void FzfTests::MatchWithGapCanHaveHigherScoreThanConsecutiveWhenGapHasFirstCharBonus()
{
auto consecutiveScore = ScoreMatch * 2 + BonusConsecutive;
auto gapScore = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart;
AssertScoreAndRuns(
L"ob",
L"aobar",
consecutiveScore,
{ { 1, 2 } });
AssertScoreAndRuns(
L"ob",
L"oabar",
gapScore,
{ { 0, 0 }, { 2, 2 } });
VERIFY_IS_GREATER_THAN(gapScore, consecutiveScore);
}
void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_2CharPattern()
{
auto consecutiveScore = ScoreMatch * 2 + BonusConsecutive;
auto gapScore = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart + ScoreGapExtension * 10;
AssertScoreAndRuns(
L"ob",
L"aobar",
consecutiveScore,
{ { 1, 2 } });
AssertScoreAndRuns(
L"ob",
L"oaaaaaaaaaaabar",
gapScore,
{ { 0, 0 }, { 12, 12 } });
VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore);
}
void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_3CharPattern_1ConsecutiveChar()
{
auto consecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2;
auto gapScore = ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive + ScoreGapStart + ScoreGapExtension * 10;
AssertScoreAndRuns(
L"oba",
L"aobar",
consecutiveScore,
{ { 1, 3 } });
AssertScoreAndRuns(
L"oba",
L"oaaaaaaaaaaabar",
gapScore,
{ { 0, 0 }, { 12, 13 } });
VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore);
}
void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs5_NoConsecutiveChars_3CharPattern()
{
auto allConsecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2;
auto allBoundaryWithGapScore = ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart + ScoreGapExtension + ScoreGapExtension + ScoreGapStart + ScoreGapExtension;
AssertScoreAndRuns(
L"oba",
L"aobar",
allConsecutiveScore,
{ { 1, 3 } });
AssertScoreAndRuns(
L"oba",
L"oaaabzzar",
allBoundaryWithGapScore,
{ { 0, 0 }, { 4, 4 }, { 7, 7 } });
VERIFY_IS_GREATER_THAN(allConsecutiveScore, allBoundaryWithGapScore);
}
void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerScoreHigherThanConsecutiveCharsWhenTheGapIs3_NoConsecutiveChar_4CharPattern()
{
auto consecutiveScore = ScoreMatch * 4 + BonusConsecutive * 3;
auto gapScore = ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart * 3;
AssertScoreAndRuns(
L"obar",
L"aobar",
consecutiveScore,
{ { 1, 4 } });
AssertScoreAndRuns(
L"obar",
L"oabzazr",
gapScore,
{ { 0, 0 }, { 2, 2 }, { 4, 4 }, { 6, 6 } });
VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore);
}
}