mirror of
https://github.com/microsoft/terminal.git
synced 2026-05-19 21:41:15 +00:00
PRE-MERGE #18700 Update command palette search to prioritize "longest substring" match.
This commit is contained in:
1
.github/actions/spelling/excludes.txt
vendored
1
.github/actions/spelling/excludes.txt
vendored
@@ -133,3 +133,4 @@ Resources/(?!en)
|
||||
^\Qsrc/terminal/parser/ft_fuzzwrapper/run.bat\E$
|
||||
^\Qsrc/tools/lnkd/lnkd.bat\E$
|
||||
^\Qsrc/tools/pixels/pixels.bat\E$
|
||||
^\Qsrc/cascadia/ut_app/FzfTests.cpp\E$
|
||||
|
||||
4
.github/actions/spelling/expect/expect.txt
vendored
4
.github/actions/spelling/expect/expect.txt
vendored
@@ -651,6 +651,7 @@ FONTSTRING
|
||||
FONTTYPE
|
||||
FONTWIDTH
|
||||
FONTWINDOW
|
||||
foob
|
||||
FORCEOFFFEEDBACK
|
||||
FORCEONFEEDBACK
|
||||
FRAMECHANGED
|
||||
@@ -668,9 +669,11 @@ fuzzer
|
||||
fuzzmain
|
||||
fuzzmap
|
||||
fuzzwrapper
|
||||
fuzzyfinder
|
||||
fwdecl
|
||||
fwe
|
||||
fwlink
|
||||
fzf
|
||||
gci
|
||||
gcx
|
||||
gdi
|
||||
@@ -1248,6 +1251,7 @@ onecoreuuid
|
||||
ONECOREWINDOWS
|
||||
onehalf
|
||||
oneseq
|
||||
oob
|
||||
openbash
|
||||
opencode
|
||||
opencon
|
||||
|
||||
@@ -35,7 +35,8 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing command name segmentation with no filter");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
auto segments = filteredCommand->_computeHighlightedName().Segments();
|
||||
filteredCommand->_update();
|
||||
auto segments = filteredCommand->HighlightedName().Segments();
|
||||
VERIFY_ARE_EQUAL(segments.Size(), 1u);
|
||||
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
|
||||
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
|
||||
@@ -43,8 +44,9 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing command name segmentation with empty filter");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"";
|
||||
auto segments = filteredCommand->_computeHighlightedName().Segments();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L""));
|
||||
filteredCommand->_update();
|
||||
auto segments = filteredCommand->HighlightedName().Segments();
|
||||
VERIFY_ARE_EQUAL(segments.Size(), 1u);
|
||||
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
|
||||
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
|
||||
@@ -52,8 +54,9 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing command name segmentation with filter equal to the string");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
|
||||
auto segments = filteredCommand->_computeHighlightedName().Segments();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"AAAAAABBBBBBCCC"));
|
||||
filteredCommand->_update();
|
||||
auto segments = filteredCommand->HighlightedName().Segments();
|
||||
VERIFY_ARE_EQUAL(segments.Size(), 1u);
|
||||
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
|
||||
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
|
||||
@@ -61,8 +64,9 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing command name segmentation with filter with first character matching");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"A";
|
||||
auto segments = filteredCommand->_computeHighlightedName().Segments();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"A"));
|
||||
filteredCommand->_update();
|
||||
auto segments = filteredCommand->HighlightedName().Segments();
|
||||
VERIFY_ARE_EQUAL(segments.Size(), 2u);
|
||||
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
|
||||
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
|
||||
@@ -72,8 +76,9 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing command name segmentation with filter with other case");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"a";
|
||||
auto segments = filteredCommand->_computeHighlightedName().Segments();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"a"));
|
||||
filteredCommand->_update();
|
||||
auto segments = filteredCommand->HighlightedName().Segments();
|
||||
VERIFY_ARE_EQUAL(segments.Size(), 2u);
|
||||
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
|
||||
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
|
||||
@@ -83,8 +88,9 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing command name segmentation with filter matching several characters");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"ab";
|
||||
auto segments = filteredCommand->_computeHighlightedName().Segments();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"ab"));
|
||||
filteredCommand->_update();
|
||||
auto segments = filteredCommand->HighlightedName().Segments();
|
||||
VERIFY_ARE_EQUAL(segments.Size(), 4u);
|
||||
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
|
||||
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
|
||||
@@ -98,8 +104,9 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing command name segmentation with non matching filter");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"abcd";
|
||||
auto segments = filteredCommand->_computeHighlightedName().Segments();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"abcd"));
|
||||
filteredCommand->_update();
|
||||
auto segments = filteredCommand->HighlightedName().Segments();
|
||||
VERIFY_ARE_EQUAL(segments.Size(), 1u);
|
||||
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
|
||||
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
|
||||
@@ -116,48 +123,48 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing weight of command with no filter");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
|
||||
auto weight = filteredCommand->_computeWeight();
|
||||
filteredCommand->_update();
|
||||
auto weight = filteredCommand->Weight();
|
||||
VERIFY_ARE_EQUAL(weight, 0);
|
||||
}
|
||||
{
|
||||
Log::Comment(L"Testing weight of command with empty filter");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"";
|
||||
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
|
||||
auto weight = filteredCommand->_computeWeight();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L""));
|
||||
filteredCommand->_update();
|
||||
auto weight = filteredCommand->Weight();
|
||||
VERIFY_ARE_EQUAL(weight, 0);
|
||||
}
|
||||
{
|
||||
Log::Comment(L"Testing weight of command with filter equal to the string");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
|
||||
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
|
||||
auto weight = filteredCommand->_computeWeight();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"AAAAAABBBBBBCCC"));
|
||||
filteredCommand->_update();
|
||||
auto weight = filteredCommand->Weight();
|
||||
VERIFY_ARE_EQUAL(weight, 30); // 1 point for the first char and 2 points for the 14 consequent ones + 1 point for the beginning of the word
|
||||
}
|
||||
{
|
||||
Log::Comment(L"Testing weight of command with filter with first character matching");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"A";
|
||||
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
|
||||
auto weight = filteredCommand->_computeWeight();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"A"));
|
||||
filteredCommand->_update();
|
||||
auto weight = filteredCommand->Weight();
|
||||
VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word
|
||||
}
|
||||
{
|
||||
Log::Comment(L"Testing weight of command with filter with other case");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"a";
|
||||
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
|
||||
auto weight = filteredCommand->_computeWeight();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"a"));
|
||||
filteredCommand->_update();
|
||||
auto weight = filteredCommand->Weight();
|
||||
VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word
|
||||
}
|
||||
{
|
||||
Log::Comment(L"Testing weight of command with filter matching several characters");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"ab";
|
||||
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
|
||||
auto weight = filteredCommand->_computeWeight();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"ab"));
|
||||
filteredCommand->_update();
|
||||
auto weight = filteredCommand->Weight();
|
||||
VERIFY_ARE_EQUAL(weight, 3); // 1 point for the first char match + 1 point for the beginning of the word + 1 point for the match of "b"
|
||||
}
|
||||
});
|
||||
@@ -181,14 +188,12 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing comparison of commands with empty filter");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"";
|
||||
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
|
||||
filteredCommand->_Weight = filteredCommand->_computeWeight();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L""));
|
||||
filteredCommand->_update();
|
||||
|
||||
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
|
||||
filteredCommand2->_Filter = L"";
|
||||
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
|
||||
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
|
||||
filteredCommand2->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L""));
|
||||
filteredCommand->_update();
|
||||
|
||||
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
|
||||
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
|
||||
@@ -196,14 +201,12 @@ namespace TerminalAppLocalTests
|
||||
{
|
||||
Log::Comment(L"Testing comparison of commands with different weights");
|
||||
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
|
||||
filteredCommand->_Filter = L"B";
|
||||
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
|
||||
filteredCommand->_Weight = filteredCommand->_computeWeight();
|
||||
filteredCommand->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"B"));
|
||||
filteredCommand->_update();
|
||||
|
||||
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
|
||||
filteredCommand2->_Filter = L"B";
|
||||
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
|
||||
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
|
||||
filteredCommand2->_pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"B"));
|
||||
filteredCommand->_update();
|
||||
|
||||
VERIFY_IS_TRUE(filteredCommand->Weight() < filteredCommand2->Weight()); // Second command gets more points due to the beginning of the word
|
||||
VERIFY_IS_FALSE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
|
||||
|
||||
@@ -1174,12 +1174,15 @@ namespace winrt::TerminalApp::implementation
|
||||
}
|
||||
else if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::ActionMode || _currentMode == CommandPaletteMode::CommandlineMode)
|
||||
{
|
||||
auto pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(searchText));
|
||||
|
||||
for (const auto& action : commandsToFilter)
|
||||
{
|
||||
// Update filter for all commands
|
||||
// This will modify the highlighting but will also lead to re-computation of weight (and consequently sorting).
|
||||
// Pay attention that it already updates the highlighting in the UI
|
||||
action.UpdateFilter(searchText);
|
||||
auto impl = winrt::get_self<implementation::FilteredCommand>(action);
|
||||
impl->UpdateFilter(pattern);
|
||||
|
||||
// if there is active search we skip commands with 0 weight
|
||||
if (searchText.empty() || action.Weight() > 0)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "CommandPalette.h"
|
||||
#include "HighlightedText.h"
|
||||
#include <LibraryResources.h>
|
||||
#include "fzf/fzf.h"
|
||||
|
||||
#include "FilteredCommand.g.cpp"
|
||||
|
||||
@@ -35,197 +36,103 @@ namespace winrt::TerminalApp::implementation
|
||||
void FilteredCommand::_constructFilteredCommand(const winrt::TerminalApp::PaletteItem& item)
|
||||
{
|
||||
_Item = item;
|
||||
_Filter = L"";
|
||||
_Weight = 0;
|
||||
_HighlightedName = _computeHighlightedName();
|
||||
|
||||
_update();
|
||||
|
||||
// Recompute the highlighted name if the item name changes
|
||||
_itemChangedRevoker = _Item.PropertyChanged(winrt::auto_revoke, [weakThis{ get_weak() }](auto& /*sender*/, auto& e) {
|
||||
auto filteredCommand{ weakThis.get() };
|
||||
if (filteredCommand && e.PropertyName() == L"Name")
|
||||
{
|
||||
filteredCommand->HighlightedName(filteredCommand->_computeHighlightedName());
|
||||
filteredCommand->Weight(filteredCommand->_computeWeight());
|
||||
filteredCommand->_update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void FilteredCommand::UpdateFilter(const winrt::hstring& filter)
|
||||
void FilteredCommand::UpdateFilter(std::shared_ptr<fzf::matcher::Pattern> pattern)
|
||||
{
|
||||
// If the filter was not changed we want to prevent the re-computation of matching
|
||||
// that might result in triggering a notification event
|
||||
if (filter != _Filter)
|
||||
if (pattern != _pattern)
|
||||
{
|
||||
Filter(filter);
|
||||
HighlightedName(_computeHighlightedName());
|
||||
Weight(_computeWeight());
|
||||
_pattern = pattern;
|
||||
_update();
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Looks up the filter characters within the item name.
|
||||
// Iterating through the filter and the item name it tries to associate the next filter character
|
||||
// with the first appearance of this character in the item name suffix.
|
||||
//
|
||||
// E.g., for filter="c l t s" and name="close all tabs after this", the match will be "CLose TabS after this".
|
||||
//
|
||||
// The item name is then split into segments (groupings of matched and non matched characters).
|
||||
//
|
||||
// E.g., the segments were the example above will be "CL", "ose ", "T", "ab", "S", "after this".
|
||||
//
|
||||
// The segments matching the filter characters are marked as highlighted.
|
||||
//
|
||||
// E.g., ("CL", true) ("ose ", false), ("T", true), ("ab", false), ("S", true), ("after this", false)
|
||||
//
|
||||
// TODO: we probably need to merge this logic with _getWeight computation?
|
||||
//
|
||||
// Return Value:
|
||||
// - The HighlightedText object initialized with the segments computed according to the algorithm above.
|
||||
winrt::TerminalApp::HighlightedText FilteredCommand::_computeHighlightedName()
|
||||
void FilteredCommand::_update()
|
||||
{
|
||||
const auto segments = winrt::single_threaded_observable_vector<winrt::TerminalApp::HighlightedTextSegment>();
|
||||
auto segments = winrt::single_threaded_observable_vector<winrt::TerminalApp::HighlightedTextSegment>();
|
||||
auto commandName = _Item.Name();
|
||||
auto isProcessingMatchedSegment = false;
|
||||
uint32_t nextOffsetToReport = 0;
|
||||
uint32_t currentOffset = 0;
|
||||
|
||||
for (const auto searchChar : _Filter)
|
||||
auto weight = 0;
|
||||
if (!_pattern || !_pattern->terms.empty())
|
||||
{
|
||||
const WCHAR searchCharAsString[] = { searchChar, L'\0' };
|
||||
while (true)
|
||||
{
|
||||
if (currentOffset == commandName.size())
|
||||
{
|
||||
// There are still unmatched filter characters but we finished scanning the name.
|
||||
// In this case we return the entire item name as unmatched
|
||||
auto entireNameSegment{ winrt::make<HighlightedTextSegment>(commandName, false) };
|
||||
segments.Clear();
|
||||
segments.Append(entireNameSegment);
|
||||
return winrt::make<HighlightedText>(segments);
|
||||
}
|
||||
segments.Append(winrt::TerminalApp::HighlightedTextSegment(commandName, false));
|
||||
}
|
||||
else if (auto match = fzf::matcher::Match(commandName, *_pattern.get()); !match)
|
||||
{
|
||||
segments.Append(winrt::TerminalApp::HighlightedTextSegment(commandName, false));
|
||||
}
|
||||
else
|
||||
{
|
||||
auto& matchResult = *match;
|
||||
weight = matchResult.Score;
|
||||
auto positions = matchResult.Pos;
|
||||
// positions are returned is sorted pairs by search term. E.g. sp anta {5,4,11,10,9,8}
|
||||
// sorting these in ascending order so it is easier to build the text segments
|
||||
std::ranges::sort(positions);
|
||||
// a position can be matched in multiple terms, removed duplicates to simplify segments
|
||||
positions.erase(std::unique(positions.begin(), positions.end()), positions.end());
|
||||
|
||||
// GH#9941: search should be locale-aware as well
|
||||
// We use the same comparison method as upon sorting to guarantee consistent behavior
|
||||
const WCHAR currentCharAsString[] = { commandName[currentOffset], L'\0' };
|
||||
auto isCurrentCharMatched = lstrcmpi(searchCharAsString, currentCharAsString) == 0;
|
||||
if (isProcessingMatchedSegment != isCurrentCharMatched)
|
||||
std::vector<std::pair<size_t, size_t>> runs;
|
||||
if (!positions.empty())
|
||||
{
|
||||
size_t runStart = positions[0];
|
||||
size_t runEnd = runStart;
|
||||
for (size_t i = 1; i < positions.size(); ++i)
|
||||
{
|
||||
// We reached the end of the region (matched character came after a series of unmatched or vice versa).
|
||||
// Conclude the segment and add it to the list.
|
||||
// Skip segment if it is empty (might happen when the first character of the name is matched)
|
||||
auto sizeToReport = currentOffset - nextOffsetToReport;
|
||||
if (sizeToReport > 0)
|
||||
if (positions[i] == runEnd + 1)
|
||||
{
|
||||
winrt::hstring segment{ commandName.data() + nextOffsetToReport, sizeToReport };
|
||||
auto highlightedSegment{ winrt::make<HighlightedTextSegment>(segment, isProcessingMatchedSegment) };
|
||||
segments.Append(highlightedSegment);
|
||||
nextOffsetToReport = currentOffset;
|
||||
runEnd = positions[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
runs.emplace_back(runStart, runEnd);
|
||||
runStart = positions[i];
|
||||
runEnd = runStart;
|
||||
}
|
||||
isProcessingMatchedSegment = isCurrentCharMatched;
|
||||
}
|
||||
|
||||
currentOffset++;
|
||||
|
||||
if (isCurrentCharMatched)
|
||||
{
|
||||
// We have matched this filter character, let's move to matching the next filter char
|
||||
break;
|
||||
}
|
||||
runs.emplace_back(runStart, runEnd);
|
||||
}
|
||||
}
|
||||
|
||||
// Either the filter or the item name were fully processed.
|
||||
// If we were in the middle of the matched segment - add it.
|
||||
if (isProcessingMatchedSegment)
|
||||
{
|
||||
auto sizeToReport = currentOffset - nextOffsetToReport;
|
||||
if (sizeToReport > 0)
|
||||
size_t lastPos = 0;
|
||||
for (auto [start, end] : runs)
|
||||
{
|
||||
winrt::hstring segment{ commandName.data() + nextOffsetToReport, sizeToReport };
|
||||
auto highlightedSegment{ winrt::make<HighlightedTextSegment>(segment, true) };
|
||||
segments.Append(highlightedSegment);
|
||||
nextOffsetToReport = currentOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// Now create a segment for all remaining characters.
|
||||
// We will have remaining characters as long as the filter is shorter than the item name.
|
||||
auto sizeToReport = commandName.size() - nextOffsetToReport;
|
||||
if (sizeToReport > 0)
|
||||
{
|
||||
winrt::hstring segment{ commandName.data() + nextOffsetToReport, sizeToReport };
|
||||
auto highlightedSegment{ winrt::make<HighlightedTextSegment>(segment, false) };
|
||||
segments.Append(highlightedSegment);
|
||||
}
|
||||
|
||||
return winrt::make<HighlightedText>(segments);
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
// - Calculates a "weighting" by which should be used to order a item
|
||||
// name relative to other names, given a specific search string.
|
||||
// Currently, this is based off of two factors:
|
||||
// * The weight is incremented once for each matched character of the
|
||||
// search text.
|
||||
// * If a matching character from the search text was found at the start
|
||||
// of a word in the name, then we increment the weight again.
|
||||
// * For example, for a search string "sp", we want "Split Pane" to
|
||||
// appear in the list before "Close Pane"
|
||||
// * Consecutive matches will be weighted higher than matches with
|
||||
// characters in between the search characters.
|
||||
// - This will return 0 if the item should not be shown. If all the
|
||||
// characters of search text appear in order in `name`, then this function
|
||||
// will return a positive number. There can be any number of characters
|
||||
// separating consecutive characters in searchText.
|
||||
// * For example:
|
||||
// "name": "New Tab"
|
||||
// "name": "Close Tab"
|
||||
// "name": "Close Pane"
|
||||
// "name": "[-] Split Horizontal"
|
||||
// "name": "[ | ] Split Vertical"
|
||||
// "name": "Next Tab"
|
||||
// "name": "Prev Tab"
|
||||
// "name": "Open Settings"
|
||||
// "name": "Open Media Controls"
|
||||
// * "open" should return both "**Open** Settings" and "**Open** Media Controls".
|
||||
// * "Tab" would return "New **Tab**", "Close **Tab**", "Next **Tab**" and "Prev
|
||||
// **Tab**".
|
||||
// * "P" would return "Close **P**ane", "[-] S**p**lit Horizontal", "[ | ]
|
||||
// S**p**lit Vertical", "**P**rev Tab", "O**p**en Settings" and "O**p**en Media
|
||||
// Controls".
|
||||
// * "sv" would return "[ | ] Split Vertical" (by matching the **S** in
|
||||
// "Split", then the **V** in "Vertical").
|
||||
// Arguments:
|
||||
// - searchText: the string of text to search for in `name`
|
||||
// - name: the name to check
|
||||
// Return Value:
|
||||
// - the relative weight of this match
|
||||
int FilteredCommand::_computeWeight()
|
||||
{
|
||||
auto result = 0;
|
||||
auto isNextSegmentWordBeginning = true;
|
||||
|
||||
for (const auto& segment : _HighlightedName.Segments())
|
||||
{
|
||||
const auto& segmentText = segment.TextSegment();
|
||||
const auto segmentSize = segmentText.size();
|
||||
|
||||
if (segment.IsHighlighted())
|
||||
{
|
||||
// Give extra point for each consecutive match
|
||||
result += (segmentSize <= 1) ? segmentSize : 1 + 2 * (segmentSize - 1);
|
||||
|
||||
// Give extra point if this segment is at the beginning of a word
|
||||
if (isNextSegmentWordBeginning)
|
||||
if (start > lastPos)
|
||||
{
|
||||
result++;
|
||||
hstring nonMatch{ commandName.data() + lastPos,
|
||||
static_cast<unsigned>(start - lastPos) };
|
||||
segments.Append(winrt::TerminalApp::HighlightedTextSegment(nonMatch, false));
|
||||
}
|
||||
|
||||
hstring matchSeg{ commandName.data() + start,
|
||||
static_cast<unsigned>(end - start + 1) };
|
||||
segments.Append(winrt::TerminalApp::HighlightedTextSegment(matchSeg, true));
|
||||
|
||||
lastPos = end + 1;
|
||||
}
|
||||
|
||||
isNextSegmentWordBeginning = segmentSize > 0 && segmentText[segmentSize - 1] == L' ';
|
||||
if (lastPos < commandName.size())
|
||||
{
|
||||
hstring tail{ commandName.data() + lastPos,
|
||||
static_cast<unsigned>(commandName.size() - lastPos) };
|
||||
segments.Append(winrt::TerminalApp::HighlightedTextSegment(tail, false));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
HighlightedName(winrt::make<HighlightedText>(segments));
|
||||
Weight(weight);
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "HighlightedTextControl.h"
|
||||
#include "FilteredCommand.g.h"
|
||||
#include "fzf/fzf.h"
|
||||
|
||||
// fwdecl unittest classes
|
||||
namespace TerminalAppLocalTests
|
||||
@@ -19,13 +20,12 @@ namespace winrt::TerminalApp::implementation
|
||||
FilteredCommand() = default;
|
||||
FilteredCommand(const winrt::TerminalApp::PaletteItem& item);
|
||||
|
||||
virtual void UpdateFilter(const winrt::hstring& filter);
|
||||
virtual void UpdateFilter(std::shared_ptr<fzf::matcher::Pattern> pattern);
|
||||
|
||||
static int Compare(const winrt::TerminalApp::FilteredCommand& first, const winrt::TerminalApp::FilteredCommand& second);
|
||||
|
||||
til::property_changed_event PropertyChanged;
|
||||
WINRT_OBSERVABLE_PROPERTY(winrt::TerminalApp::PaletteItem, Item, PropertyChanged.raise, nullptr);
|
||||
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, Filter, PropertyChanged.raise);
|
||||
WINRT_OBSERVABLE_PROPERTY(winrt::TerminalApp::HighlightedText, HighlightedName, PropertyChanged.raise);
|
||||
WINRT_OBSERVABLE_PROPERTY(int, Weight, PropertyChanged.raise);
|
||||
|
||||
@@ -33,8 +33,8 @@ namespace winrt::TerminalApp::implementation
|
||||
void _constructFilteredCommand(const winrt::TerminalApp::PaletteItem& item);
|
||||
|
||||
private:
|
||||
winrt::TerminalApp::HighlightedText _computeHighlightedName();
|
||||
int _computeWeight();
|
||||
std::shared_ptr<fzf::matcher::Pattern> _pattern;
|
||||
void _update();
|
||||
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _itemChangedRevoker;
|
||||
|
||||
friend class TerminalAppLocalTests::FilteredCommandTests;
|
||||
|
||||
@@ -12,10 +12,7 @@ namespace TerminalApp
|
||||
FilteredCommand(PaletteItem item);
|
||||
|
||||
PaletteItem Item { get; };
|
||||
String Filter;
|
||||
HighlightedText HighlightedName { get; };
|
||||
Int32 Weight;
|
||||
|
||||
void UpdateFilter(String filter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace winrt::TerminalApp::implementation
|
||||
void SnippetsPaneContent::_updateFilteredCommands()
|
||||
{
|
||||
const auto& queryString = _filterBox().Text();
|
||||
auto pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(queryString));
|
||||
|
||||
// DON'T replace the itemSource here. If you do, it'll un-expand all the
|
||||
// nested items the user has expanded. Instead, just update the filter.
|
||||
@@ -39,7 +40,7 @@ namespace winrt::TerminalApp::implementation
|
||||
for (const auto& t : _allTasks)
|
||||
{
|
||||
auto impl = winrt::get_self<implementation::FilteredTask>(t);
|
||||
impl->UpdateFilter(queryString);
|
||||
impl->UpdateFilter(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,13 +77,14 @@ namespace winrt::TerminalApp::implementation
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateFilter(const winrt::hstring& filter)
|
||||
void UpdateFilter(std::shared_ptr<fzf::matcher::Pattern> pattern)
|
||||
{
|
||||
_filteredCommand->UpdateFilter(filter);
|
||||
_pattern = pattern;
|
||||
_filteredCommand->UpdateFilter(pattern);
|
||||
for (const auto& c : _children)
|
||||
{
|
||||
auto impl = winrt::get_self<implementation::FilteredTask>(c);
|
||||
impl->UpdateFilter(filter);
|
||||
impl->UpdateFilter(pattern);
|
||||
}
|
||||
|
||||
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"Visibility" });
|
||||
@@ -108,6 +109,7 @@ namespace winrt::TerminalApp::implementation
|
||||
bool HasChildren() { return _children.Size() > 0; }
|
||||
winrt::Microsoft::Terminal::Settings::Model::Command Command() { return _command; }
|
||||
winrt::TerminalApp::FilteredCommand FilteredCommand() { return *_filteredCommand; }
|
||||
std::shared_ptr<fzf::matcher::Pattern> _pattern;
|
||||
|
||||
int32_t Row() { return HasChildren() ? 2 : 1; } // See the BODGY comment in the .XAML for explanation
|
||||
|
||||
@@ -117,7 +119,7 @@ namespace winrt::TerminalApp::implementation
|
||||
winrt::Windows::UI::Xaml::Visibility Visibility()
|
||||
{
|
||||
// Is there no filter, or do we match it?
|
||||
if (_filteredCommand->Filter().empty() || _filteredCommand->Weight() > 0)
|
||||
if ((!_pattern || _pattern->terms.empty() || _filteredCommand->Weight() > 0))
|
||||
{
|
||||
return winrt::Windows::UI::Xaml::Visibility::Visible;
|
||||
}
|
||||
|
||||
@@ -936,12 +936,15 @@ namespace winrt::TerminalApp::implementation
|
||||
auto commandsToFilter = _commandsToFilter();
|
||||
|
||||
{
|
||||
auto pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(searchText));
|
||||
|
||||
for (const auto& action : commandsToFilter)
|
||||
{
|
||||
// Update filter for all commands
|
||||
// This will modify the highlighting but will also lead to re-computation of weight (and consequently sorting).
|
||||
// Pay attention that it already updates the highlighting in the UI
|
||||
action.UpdateFilter(searchText);
|
||||
auto impl = winrt::get_self<implementation::FilteredCommand>(action);
|
||||
impl->UpdateFilter(pattern);
|
||||
|
||||
// if there is active search we skip commands with 0 weight
|
||||
if (searchText.empty() || action.Weight() > 0)
|
||||
|
||||
@@ -138,6 +138,8 @@
|
||||
</ClInclude>
|
||||
<ClInclude Include="FilteredCommand.h" />
|
||||
<ClInclude Include="Pane.h" />
|
||||
<ClInclude Include="fzf/fzf.h" />
|
||||
<ClInclude Include="fzf/LICENSE" />
|
||||
<ClInclude Include="ColorHelper.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="ShortcutActionDispatch.h">
|
||||
@@ -212,6 +214,7 @@
|
||||
<ClCompile Include="TabBase.cpp">
|
||||
<DependentUpon>TabBase.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="fzf/fzf.cpp" />
|
||||
<ClCompile Include="TabPaletteItem.cpp" />
|
||||
<ClCompile Include="TaskbarState.cpp">
|
||||
<DependentUpon>TaskbarState.idl</DependentUpon>
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
<ClCompile Include="HighlightedText.cpp">
|
||||
<Filter>highlightedText</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="fzf/fzf.cpp">
|
||||
<Filter>fzf</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Toast.cpp" />
|
||||
<ClCompile Include="LanguageProfileNotifier.cpp" />
|
||||
<ClCompile Include="Monarch.cpp" />
|
||||
@@ -77,6 +80,12 @@
|
||||
<ClInclude Include="HighlightedText.h">
|
||||
<Filter>highlightedText</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="fzf/fzf.h">
|
||||
<Filter>fzf</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="fzf/LICENSE">
|
||||
<Filter>fzf</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Toast.h" />
|
||||
<ClInclude Include="LanguageProfileNotifier.h" />
|
||||
<ClInclude Include="WindowsPackageManagerFactory.h" />
|
||||
@@ -176,6 +185,9 @@
|
||||
<Filter Include="highlightedText">
|
||||
<UniqueIdentifier>{e490f626-547d-4b5b-b22d-c6d33c9e3210}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="fzf">
|
||||
<UniqueIdentifier>{e4588ff4-c80a-40f7-be57-3e81f570a93d}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="App.xaml">
|
||||
|
||||
22
src/cascadia/TerminalApp/fzf/LICENSE
Normal file
22
src/cascadia/TerminalApp/fzf/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2021-2025 Simon Hauser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
490
src/cascadia/TerminalApp/fzf/fzf.cpp
Normal file
490
src/cascadia/TerminalApp/fzf/fzf.cpp
Normal file
@@ -0,0 +1,490 @@
|
||||
#include "pch.h"
|
||||
#include "fzf.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace fzf
|
||||
{
|
||||
namespace matcher
|
||||
{
|
||||
constexpr int16_t ScoreMatch = 16;
|
||||
constexpr int16_t ScoreGapStart = -3;
|
||||
constexpr int16_t ScoreGapExtension = -1;
|
||||
constexpr int16_t BoundaryBonus = ScoreMatch / 2;
|
||||
constexpr int16_t NonWordBonus = ScoreMatch / 2;
|
||||
constexpr int16_t CamelCaseBonus = BoundaryBonus + ScoreGapExtension;
|
||||
constexpr int16_t BonusConsecutive = -(ScoreGapStart + ScoreGapExtension);
|
||||
constexpr int16_t BonusFirstCharMultiplier = 2;
|
||||
|
||||
enum CharClass : uint8_t
|
||||
{
|
||||
NonWord = 0,
|
||||
CharLower = 1,
|
||||
CharUpper = 2,
|
||||
Digit = 3,
|
||||
};
|
||||
|
||||
std::wstring_view TrimStart(const std::wstring_view str)
|
||||
{
|
||||
const auto off = str.find_first_not_of(L' ');
|
||||
return str.substr(std::min(off, str.size()));
|
||||
}
|
||||
|
||||
std::wstring_view TrimSuffixSpaces(std::wstring_view input)
|
||||
{
|
||||
size_t end = input.size();
|
||||
while (end > 0 && input[end - 1] == L' ')
|
||||
{
|
||||
--end;
|
||||
}
|
||||
return input.substr(0, end);
|
||||
}
|
||||
|
||||
UChar32 FoldCase(UChar32 c) noexcept
|
||||
{
|
||||
return u_foldCase(c, U_FOLD_CASE_DEFAULT);
|
||||
}
|
||||
|
||||
int32_t IndexOfChar(const std::vector<UChar32>& input, const UChar32 searchChar, int32_t startIndex)
|
||||
{
|
||||
for (int32_t i = startIndex; i < static_cast<int32_t>(input.size()); ++i)
|
||||
{
|
||||
if (input[i] == searchChar)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t FuzzyIndexOf(const std::vector<UChar32>& input, const std::vector<UChar32>& pattern)
|
||||
{
|
||||
int32_t idx = 0;
|
||||
int32_t firstIdx = 0;
|
||||
for (int32_t pi = 0; pi < pattern.size(); ++pi)
|
||||
{
|
||||
idx = IndexOfChar(input, pattern[pi], idx);
|
||||
if (idx < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pi == 0 && idx > 0)
|
||||
{
|
||||
firstIdx = idx - 1;
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
return firstIdx;
|
||||
}
|
||||
|
||||
int16_t CalculateBonus(CharClass prevClass, CharClass currentClass)
|
||||
{
|
||||
if (prevClass == NonWord && currentClass != NonWord)
|
||||
{
|
||||
return BoundaryBonus;
|
||||
}
|
||||
if ((prevClass == CharLower && currentClass == CharUpper) ||
|
||||
(prevClass != Digit && currentClass == Digit))
|
||||
{
|
||||
return CamelCaseBonus;
|
||||
}
|
||||
if (currentClass == NonWord)
|
||||
{
|
||||
return NonWordBonus;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static constexpr auto s_charClassLut = []() {
|
||||
std::array<CharClass, U_CHAR_CATEGORY_COUNT> lut{};
|
||||
lut.fill(CharClass::NonWord);
|
||||
lut[U_UPPERCASE_LETTER] = CharUpper;
|
||||
lut[U_LOWERCASE_LETTER] = CharLower;
|
||||
lut[U_MODIFIER_LETTER] = CharLower;
|
||||
lut[U_OTHER_LETTER] = CharLower;
|
||||
lut[U_DECIMAL_DIGIT_NUMBER] = Digit;
|
||||
return lut;
|
||||
}();
|
||||
|
||||
CharClass ClassOf(UChar32 ch)
|
||||
{
|
||||
return s_charClassLut[u_charType(ch)];
|
||||
}
|
||||
|
||||
FzfResult FzfFuzzyMatchV2(const std::vector<UChar32>& text, const std::vector<UChar32>& pattern, std::vector<int32_t>* pos)
|
||||
{
|
||||
int32_t patternSize = static_cast<int32_t>(pattern.size());
|
||||
int32_t textSize = static_cast<int32_t>(text.size());
|
||||
|
||||
if (patternSize == 0)
|
||||
{
|
||||
return { 0, 0, 0 };
|
||||
}
|
||||
|
||||
std::vector<UChar32> foldedText;
|
||||
foldedText.reserve(text.size());
|
||||
for (auto cp : text)
|
||||
{
|
||||
auto foldedCp = u_foldCase(cp, U_FOLD_CASE_DEFAULT);
|
||||
foldedText.push_back(foldedCp);
|
||||
}
|
||||
|
||||
int32_t firstIndexOf = FuzzyIndexOf(foldedText, pattern);
|
||||
if (firstIndexOf < 0)
|
||||
{
|
||||
return { -1, -1, 0 };
|
||||
}
|
||||
|
||||
auto initialScores = std::vector<int16_t>(textSize);
|
||||
auto consecutiveScores = std::vector<int16_t>(textSize);
|
||||
auto firstOccurrenceOfEachChar = std::vector<int32_t>(patternSize);
|
||||
auto bonusesSpan = std::vector<int16_t>(textSize);
|
||||
|
||||
int16_t maxScore = 0;
|
||||
int32_t maxScorePos = 0;
|
||||
int32_t patternIndex = 0;
|
||||
int32_t lastIndex = 0;
|
||||
UChar32 firstPatternChar = pattern[0];
|
||||
UChar32 currentPatternChar = pattern[0];
|
||||
int16_t previousInitialScore = 0;
|
||||
CharClass previousClass = NonWord;
|
||||
bool inGap = false;
|
||||
|
||||
std::span<const UChar32> lowerText(foldedText);
|
||||
auto lowerTextSlice = lowerText.subspan(firstIndexOf);
|
||||
auto initialScoresSlice = std::span(initialScores).subspan(firstIndexOf);
|
||||
auto consecutiveScoresSlice = std::span(consecutiveScores).subspan(firstIndexOf);
|
||||
auto bonusesSlice = std::span(bonusesSpan).subspan(firstIndexOf, textSize - firstIndexOf);
|
||||
|
||||
for (int32_t i = 0; i < lowerTextSlice.size(); i++)
|
||||
{
|
||||
UChar32 currentChar = lowerTextSlice[i];
|
||||
CharClass currentClass = ClassOf(currentChar);
|
||||
int16_t bonus = CalculateBonus(previousClass, currentClass);
|
||||
bonusesSlice[i] = bonus;
|
||||
previousClass = currentClass;
|
||||
|
||||
//currentPatternChar was already folded in ParsePattern
|
||||
if (currentChar == currentPatternChar)
|
||||
{
|
||||
if (patternIndex < pattern.size())
|
||||
{
|
||||
firstOccurrenceOfEachChar[patternIndex] = firstIndexOf + static_cast<int32_t>(i);
|
||||
patternIndex++;
|
||||
if (patternIndex < patternSize)
|
||||
{
|
||||
currentPatternChar = pattern[patternIndex];
|
||||
}
|
||||
}
|
||||
lastIndex = firstIndexOf + static_cast<int32_t>(i);
|
||||
}
|
||||
if (currentChar == firstPatternChar)
|
||||
{
|
||||
int16_t score = ScoreMatch + bonus * BonusFirstCharMultiplier;
|
||||
initialScoresSlice[i] = score;
|
||||
consecutiveScoresSlice[i] = 1;
|
||||
if (patternSize == 1 && (score > maxScore))
|
||||
{
|
||||
maxScore = score;
|
||||
maxScorePos = firstIndexOf + static_cast<int32_t>(i);
|
||||
if (bonus == BoundaryBonus)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
inGap = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
initialScoresSlice[i] = inGap ? std::max<int16_t>(previousInitialScore + ScoreGapExtension, 0) : std::max<int16_t>(previousInitialScore + ScoreGapStart, 0);
|
||||
consecutiveScoresSlice[i] = 0;
|
||||
inGap = true;
|
||||
}
|
||||
previousInitialScore = initialScoresSlice[i];
|
||||
}
|
||||
|
||||
if (patternIndex != pattern.size())
|
||||
{
|
||||
return { -1, -1, 0 };
|
||||
}
|
||||
|
||||
if (pattern.size() == 1)
|
||||
{
|
||||
if (pos)
|
||||
{
|
||||
pos->push_back(maxScorePos);
|
||||
}
|
||||
int32_t end = maxScorePos + 1;
|
||||
return { maxScorePos, end, maxScore };
|
||||
}
|
||||
|
||||
int32_t firstOccurrenceOfFirstChar = firstOccurrenceOfEachChar[0];
|
||||
int32_t width = lastIndex - firstOccurrenceOfFirstChar + 1;
|
||||
int32_t rows = static_cast<int32_t>(pattern.size());
|
||||
auto consecutiveCharMatrixSize = width * patternSize;
|
||||
|
||||
std::vector<int16_t> scoreMatrix(width * rows);
|
||||
std::copy_n(
|
||||
initialScores.begin() + firstOccurrenceOfFirstChar,
|
||||
width,
|
||||
scoreMatrix.begin());
|
||||
auto scoreSpan = std::span<int16_t>(scoreMatrix);
|
||||
|
||||
std::vector<int16_t> consecutiveCharMatrix(width * rows);
|
||||
std::copy_n(
|
||||
consecutiveScores.begin() + firstOccurrenceOfFirstChar,
|
||||
width,
|
||||
consecutiveCharMatrix.begin());
|
||||
auto consecutiveCharMatrixSpan = std::span(consecutiveCharMatrix);
|
||||
|
||||
auto patternSliceStr = std::span(pattern).subspan(1);
|
||||
|
||||
for (int32_t off = 0; off < patternSize - 1; off++)
|
||||
{
|
||||
auto patternCharOffset = firstOccurrenceOfEachChar[off + 1];
|
||||
auto sliceLen = lastIndex - patternCharOffset + 1;
|
||||
currentPatternChar = patternSliceStr[off];
|
||||
patternIndex = off + 1;
|
||||
int32_t row = patternIndex * width;
|
||||
inGap = false;
|
||||
auto tmp = lowerText.subspan(patternCharOffset, sliceLen);
|
||||
std::span<const UChar32> textSlice = lowerText.subspan(patternCharOffset, sliceLen);
|
||||
std::span<int16_t> bonusSlice(bonusesSpan.begin() + patternCharOffset, textSlice.size());
|
||||
std::span<int16_t> consecutiveCharMatrixSlice = consecutiveCharMatrixSpan.subspan(row + patternCharOffset - firstOccurrenceOfFirstChar, textSlice.size());
|
||||
std::span<int16_t> consecutiveCharMatrixDiagonalSlice = consecutiveCharMatrixSpan.subspan(
|
||||
+row + patternCharOffset - firstOccurrenceOfFirstChar - 1 - width, textSlice.size());
|
||||
std::span<int16_t> scoreMatrixSlice = scoreSpan.subspan(row + patternCharOffset - firstOccurrenceOfFirstChar, textSlice.size());
|
||||
std::span<int16_t> scoreMatrixDiagonalSlice = scoreSpan.subspan(row + patternCharOffset - firstOccurrenceOfFirstChar - 1 - width, textSlice.size());
|
||||
std::span<int16_t> scoreMatrixLeftSlice = scoreSpan.subspan(row + patternCharOffset - firstOccurrenceOfFirstChar - 1, textSlice.size());
|
||||
if (!scoreMatrixLeftSlice.empty())
|
||||
{
|
||||
scoreMatrixLeftSlice[0] = 0;
|
||||
}
|
||||
for (int32_t j = 0; j < textSlice.size(); j++)
|
||||
{
|
||||
UChar32 currentChar = textSlice[j];
|
||||
int32_t column = patternCharOffset + static_cast<int32_t>(j);
|
||||
int16_t score = inGap ? scoreMatrixLeftSlice[j] + ScoreGapExtension : scoreMatrixLeftSlice[j] + ScoreGapStart;
|
||||
int16_t diagonalScore = 0;
|
||||
int16_t consecutive = 0;
|
||||
if (currentChar == currentPatternChar)
|
||||
{
|
||||
diagonalScore = scoreMatrixDiagonalSlice[j] + ScoreMatch;
|
||||
int16_t bonus = bonusSlice[j];
|
||||
consecutive = consecutiveCharMatrixDiagonalSlice[j] + 1;
|
||||
if (bonus == BoundaryBonus)
|
||||
{
|
||||
consecutive = 1;
|
||||
}
|
||||
else if (consecutive > 1)
|
||||
{
|
||||
bonus = std::max({ bonus, BonusConsecutive, (bonusesSpan[column - consecutive + 1]) });
|
||||
}
|
||||
if (diagonalScore + bonus < score)
|
||||
{
|
||||
diagonalScore += bonusSlice[j];
|
||||
consecutive = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
diagonalScore += bonus;
|
||||
}
|
||||
}
|
||||
consecutiveCharMatrixSlice[j] = consecutive;
|
||||
inGap = (diagonalScore < score);
|
||||
int16_t cellScore = std::max(int16_t{0}, std::max(diagonalScore, score));
|
||||
if (off == patternSize - 2 && cellScore > maxScore)
|
||||
{
|
||||
maxScore = cellScore;
|
||||
maxScorePos = column;
|
||||
}
|
||||
scoreMatrixSlice[j] = cellScore;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t currentColIndex = maxScorePos;
|
||||
if (pos)
|
||||
{
|
||||
patternIndex = patternSize - 1;
|
||||
bool preferCurrentMatch = true;
|
||||
while (true)
|
||||
{
|
||||
int32_t rowStartIndex = patternIndex * width;
|
||||
int colOffset = currentColIndex - firstOccurrenceOfFirstChar;
|
||||
int cellScore = scoreMatrix[rowStartIndex + colOffset];
|
||||
int diagonalCellScore = 0, leftCellScore = 0;
|
||||
|
||||
if (patternIndex > 0 && currentColIndex >= firstOccurrenceOfEachChar[patternIndex])
|
||||
{
|
||||
diagonalCellScore = scoreMatrix[rowStartIndex - width + colOffset - 1];
|
||||
}
|
||||
if (currentColIndex > firstOccurrenceOfEachChar[patternIndex])
|
||||
{
|
||||
leftCellScore = scoreMatrix[rowStartIndex + colOffset - 1];
|
||||
}
|
||||
|
||||
if (cellScore > diagonalCellScore &&
|
||||
(cellScore > leftCellScore || (cellScore == leftCellScore && preferCurrentMatch)))
|
||||
{
|
||||
pos->push_back(currentColIndex);
|
||||
if (patternIndex == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
patternIndex--;
|
||||
}
|
||||
|
||||
currentColIndex--;
|
||||
if (rowStartIndex + colOffset >= consecutiveCharMatrixSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
preferCurrentMatch = (consecutiveCharMatrix[rowStartIndex + colOffset] > 1) ||
|
||||
((rowStartIndex + width + colOffset + 1 <
|
||||
consecutiveCharMatrixSize) &&
|
||||
(consecutiveCharMatrix[rowStartIndex + width + colOffset + 1] > 0));
|
||||
}
|
||||
}
|
||||
int32_t end = maxScorePos + 1;
|
||||
return { currentColIndex, end, maxScore };
|
||||
}
|
||||
|
||||
int BonusAt(std::wstring_view input, int idx)
|
||||
{
|
||||
if (idx == 0)
|
||||
{
|
||||
return BoundaryBonus;
|
||||
}
|
||||
return CalculateBonus(ClassOf(input[idx - 1]), ClassOf(input[idx]));
|
||||
}
|
||||
|
||||
int32_t utf32Length(std::wstring_view str)
|
||||
{
|
||||
return u_countChar32(reinterpret_cast<const UChar*>(str.data()), static_cast<int32_t>(str.size()));
|
||||
}
|
||||
|
||||
static std::vector<UChar32> ConvertUtf16ToCodePoints(
|
||||
std::wstring_view text,
|
||||
bool fold,
|
||||
std::vector<int32_t>* utf16OffsetsOut = nullptr)
|
||||
{
|
||||
const UChar* data = reinterpret_cast<const UChar*>(text.data());
|
||||
int32_t dataLen = static_cast<int32_t>(text.size());
|
||||
int32_t cpCount = utf32Length(text);
|
||||
|
||||
std::vector<UChar32> out;
|
||||
out.reserve(cpCount);
|
||||
|
||||
if (utf16OffsetsOut)
|
||||
{
|
||||
utf16OffsetsOut->clear();
|
||||
utf16OffsetsOut->reserve(cpCount);
|
||||
}
|
||||
|
||||
int32_t src = 0;
|
||||
while (src < dataLen)
|
||||
{
|
||||
auto startUnit = src;
|
||||
if (utf16OffsetsOut)
|
||||
{
|
||||
utf16OffsetsOut->push_back(startUnit);
|
||||
}
|
||||
|
||||
UChar32 cp;
|
||||
U16_NEXT(data, src, dataLen, cp);
|
||||
|
||||
if (fold)
|
||||
{
|
||||
cp = u_foldCase(cp, U_FOLD_CASE_DEFAULT);
|
||||
}
|
||||
|
||||
out.push_back(cp);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Pattern ParsePattern(const std::wstring_view patternStr)
|
||||
{
|
||||
Pattern patObj;
|
||||
if (patternStr.empty())
|
||||
{
|
||||
return patObj;
|
||||
}
|
||||
|
||||
auto trimmed = TrimStart(patternStr);
|
||||
trimmed = TrimSuffixSpaces(trimmed);
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < trimmed.size())
|
||||
{
|
||||
size_t found = trimmed.find(L' ', pos);
|
||||
auto slice = (found == std::wstring_view::npos) ? trimmed.substr(pos) : trimmed.substr(pos, found - pos);
|
||||
|
||||
patObj.terms.push_back(ConvertUtf16ToCodePoints(slice, true));
|
||||
|
||||
if (found == std::wstring_view::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
pos = found + 1;
|
||||
}
|
||||
|
||||
return patObj;
|
||||
}
|
||||
|
||||
static std::vector<int32_t> MapCodepointsToUtf16(
|
||||
std::vector<int32_t> const& cpPos,
|
||||
std::vector<int32_t> const& cpMap,
|
||||
size_t dataLen)
|
||||
{
|
||||
std::vector<int32_t> utf16pos;
|
||||
utf16pos.reserve(cpPos.size() * 2);
|
||||
|
||||
for (int32_t cpIndex : cpPos)
|
||||
{
|
||||
int32_t start = cpMap[cpIndex];
|
||||
int32_t end = cpIndex + int32_t{ 1 } < static_cast<int32_t>(cpMap.size()) ? cpMap[cpIndex + 1] : static_cast<int32_t>(dataLen);
|
||||
|
||||
for (int32_t cu = end - 1; cu >= start; --cu)
|
||||
{
|
||||
utf16pos.push_back(cu);
|
||||
}
|
||||
}
|
||||
return utf16pos;
|
||||
}
|
||||
|
||||
std::optional<MatchResult> Match(std::wstring_view text, const Pattern& pattern)
|
||||
{
|
||||
if (pattern.terms.empty())
|
||||
{
|
||||
return MatchResult{};;
|
||||
}
|
||||
|
||||
int16_t totalScore = 0;
|
||||
std::vector<int32_t> pos;
|
||||
for (const auto& term : pattern.terms)
|
||||
{
|
||||
std::vector<int32_t> utf16map;
|
||||
std::vector<int32_t> codePointPos;
|
||||
auto textCodePoints = ConvertUtf16ToCodePoints(text, false, &utf16map);
|
||||
FzfResult res = FzfFuzzyMatchV2(textCodePoints, term, &codePointPos);
|
||||
if (res.Score <= 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto termUtf16Pos = MapCodepointsToUtf16(codePointPos, utf16map, text.size());
|
||||
for (auto t : termUtf16Pos)
|
||||
{
|
||||
pos.push_back(t);
|
||||
}
|
||||
totalScore += res.Score;
|
||||
}
|
||||
return MatchResult{totalScore, pos};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
32
src/cascadia/TerminalApp/fzf/fzf.h
Normal file
32
src/cascadia/TerminalApp/fzf/fzf.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <icu.h>
|
||||
|
||||
namespace fzf
|
||||
{
|
||||
namespace matcher
|
||||
{
|
||||
struct FzfResult
|
||||
{
|
||||
int32_t Start;
|
||||
int32_t End;
|
||||
int16_t Score;
|
||||
};
|
||||
|
||||
struct MatchResult
|
||||
{
|
||||
int16_t Score = 0;
|
||||
std::vector<int32_t> Pos;
|
||||
};
|
||||
|
||||
class Pattern
|
||||
{
|
||||
public:
|
||||
std::vector<std::vector<UChar32>> terms;
|
||||
};
|
||||
|
||||
Pattern ParsePattern(const std::wstring_view patternStr);
|
||||
std::optional<MatchResult> Match(std::wstring_view text, const Pattern& pattern);
|
||||
}
|
||||
}
|
||||
534
src/cascadia/ut_app/FzfTests.cpp
Normal file
534
src/cascadia/ut_app/FzfTests.cpp
Normal file
@@ -0,0 +1,534 @@
|
||||
// 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(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 AssertScoreAndPositions(std::wstring_view patternText, std::wstring_view text, int expectedScore, std::vector<int16_t> expectedPositions)
|
||||
{
|
||||
auto pattern = fzf::matcher::ParsePattern(patternText);
|
||||
auto match = fzf::matcher::Match(text, pattern);
|
||||
if (expectedScore == 0 && expectedPositions.size() == 0)
|
||||
{
|
||||
VERIFY_ARE_EQUAL(std::nullopt, match);
|
||||
return;
|
||||
}
|
||||
auto score = match->Score;
|
||||
auto positions = match->Pos;
|
||||
VERIFY_ARE_EQUAL(expectedScore, score);
|
||||
VERIFY_ARE_EQUAL(expectedPositions.size(), positions.size());
|
||||
for (auto i = 0; i < expectedPositions.size(); i++)
|
||||
{
|
||||
VERIFY_ARE_EQUAL(expectedPositions[i], positions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void FzfTests::AllPatternCharsDoNotMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"fbb",
|
||||
L"foo bar",
|
||||
0,
|
||||
{});
|
||||
}
|
||||
|
||||
void FzfTests::ConsecutiveChars()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"oba",
|
||||
L"foobar",
|
||||
ScoreMatch * 3 + BonusConsecutive * 2,
|
||||
{ 4, 3, 2 });
|
||||
}
|
||||
|
||||
void FzfTests::ConsecutiveChars_FirstCharBonus()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"foo",
|
||||
L"foobar",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
|
||||
{ 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::NonWordBonusBoundary_ConsecutiveChars()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"zshc",
|
||||
L"/man1/zshcompctl.1",
|
||||
ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + BonusFirstCharMultiplier * BonusConsecutive * 3,
|
||||
{ 9, 8, 7, 6 });
|
||||
}
|
||||
|
||||
void FzfTests::Russian_CaseMisMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"новая",
|
||||
L"Новая вкладка",
|
||||
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
|
||||
{ 4, 3, 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::Russian_CaseMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"Новая",
|
||||
L"Новая вкладка",
|
||||
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
|
||||
{ 4, 3, 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::German_CaseMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"fuß",
|
||||
L"Fußball",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
|
||||
{ 2, 1, 0 });
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
AssertScoreAndPositions(
|
||||
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
|
||||
{ 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::French_CaseMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"Éco",
|
||||
L"École",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
|
||||
{ 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::French_CaseMisMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"Éco",
|
||||
L"école",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
|
||||
{ 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::Greek_CaseMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"λόγος",
|
||||
L"λόγος",
|
||||
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
|
||||
{ 4, 3, 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::Greek_CaseMisMatch()
|
||||
{
|
||||
//I think this tests validates folding (σ, ς)
|
||||
AssertScoreAndPositions(
|
||||
L"λόγοσ",
|
||||
L"λόγος",
|
||||
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
|
||||
{ 4, 3, 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::English_CaseMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"Newer",
|
||||
L"Newer tab",
|
||||
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
|
||||
{ 4, 3, 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::English_CaseMisMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"newer",
|
||||
L"Newer tab",
|
||||
ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4,
|
||||
{ 4, 3, 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::SurrogatePair()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"N😀ewer",
|
||||
L"N😀ewer tab",
|
||||
ScoreMatch * 6 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 5,
|
||||
{ 6, 5, 4, 3, 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::SurrogatePair_ToUtf16Pos_ConsecutiveChars()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"N𠀋N😀𝄞e𐐷",
|
||||
L"N𠀋N😀𝄞e𐐷 tab",
|
||||
ScoreMatch * 7 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 6,
|
||||
{ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::SurrogatePair_ToUtf16Pos_PreferConsecutiveChars()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"𠀋😀",
|
||||
L"N𠀋😀wer 😀b𐐷 ",
|
||||
ScoreMatch * 2 + BonusConsecutive * 2,
|
||||
{4, 3, 2, 1});
|
||||
}
|
||||
|
||||
void FzfTests::SurrogatePair_ToUtf16Pos_GapAndBoundary()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"𠀋😀",
|
||||
L"N𠀋wer 😀b𐐷 ",
|
||||
ScoreMatch * 2 + ScoreGapStart + ScoreGapExtension * 3 + BonusBoundary,
|
||||
{8, 7, 2, 1});
|
||||
}
|
||||
|
||||
void FzfTests::MatchOnNonWordChars_CaseInSensitive()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"foo-b",
|
||||
L"xFoo-Bar Baz",
|
||||
ScoreMatch * 5 + BonusConsecutive * 4 + BonusBoundary,
|
||||
{5,4,3,2,1});
|
||||
}
|
||||
|
||||
void FzfTests::MatchOnNonWordCharsWithGap()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"12356",
|
||||
L"abc123 456",
|
||||
ScoreMatch * 5 + BonusCamel123 * BonusFirstCharMultiplier + BonusCamel123 * 2 + BonusConsecutive + ScoreGapStart + ScoreGapExtension,
|
||||
{ 9, 8, 5, 4, 3 });
|
||||
}
|
||||
|
||||
void FzfTests::BonusBoundaryAndFirstCharMultiplier()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"fbb",
|
||||
L"foo bar baz",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension,
|
||||
{ 8, 4, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::MatchesAreCaseInSensitive()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"FBB",
|
||||
L"foo bar baz",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension,
|
||||
{ 8, 4, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::MultipleTerms()
|
||||
{
|
||||
auto term1Score = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive);
|
||||
auto term2Score = ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive) * 3;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"sp anta",
|
||||
L"Split Pane, split: horizontal, profile: SSH: Antares",
|
||||
term1Score + term2Score,
|
||||
{1, 0, 48, 47, 46, 45 });
|
||||
}
|
||||
|
||||
void FzfTests::MultipleTerms_AllCharsMatch()
|
||||
{
|
||||
auto term1Score = ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive * 2);
|
||||
auto term2Score = term1Score;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"foo bar",
|
||||
L"foo bar",
|
||||
term1Score + term2Score,
|
||||
{2,1,0,6,5,4});
|
||||
}
|
||||
|
||||
void FzfTests::MultipleTerms_NotAllTermsMatch()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"sp anta zz",
|
||||
L"Split Pane, split: horizontal, profile: SSH: Antares",
|
||||
0,
|
||||
{});
|
||||
}
|
||||
|
||||
void FzfTests::MatchesAreCaseInSensitive_BonusBoundary()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"fbb",
|
||||
L"Foo Bar Baz",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension,
|
||||
{ 8, 4, 0 });
|
||||
}
|
||||
|
||||
void FzfTests::TraceBackWillPickTheFirstMatchIfBothHaveTheSameScore()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"bar",
|
||||
L"Foo Bar Bar",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier * 2,
|
||||
{ 6, 5, 4 });
|
||||
}
|
||||
|
||||
void FzfTests::TraceBackWillPickTheMatchWithTheHighestScore()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"bar",
|
||||
L"Foo aBar Bar",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier * 2,
|
||||
{ 11, 10, 9 });
|
||||
}
|
||||
|
||||
void FzfTests::TraceBackWillPickTheMatchWithTheHighestScore_Gaps()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"bar",
|
||||
L"Boo Author Raz Bar",
|
||||
ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2,
|
||||
{17,16,15});
|
||||
}
|
||||
|
||||
void FzfTests::TraceBackWillPickEarlierCharsWhenNoBonus()
|
||||
{
|
||||
AssertScoreAndPositions(
|
||||
L"clts",
|
||||
L"close all tabs after this",
|
||||
ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + BonusFirstCharMultiplier * BonusConsecutive + ScoreGapStart + ScoreGapExtension * 7 + BonusBoundary + ScoreGapStart + ScoreGapExtension,
|
||||
{13,10,1,0});
|
||||
}
|
||||
|
||||
void FzfTests::ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothDontHaveBonus()
|
||||
{
|
||||
auto consecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2;
|
||||
auto gapScore = (ScoreMatch * 3) + ScoreGapStart + ScoreGapStart;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oob",
|
||||
L"aoobar",
|
||||
consecutiveScore,
|
||||
{3,2,1});
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oob",
|
||||
L"aoaoabound",
|
||||
gapScore,
|
||||
{5,3,1});
|
||||
|
||||
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;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oob",
|
||||
L"oobar",
|
||||
consecutiveScore,
|
||||
{2,1,0});
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oob",
|
||||
L"oaoabound",
|
||||
gapScore,
|
||||
{4,2,0});
|
||||
|
||||
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;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oob",
|
||||
L"foobar",
|
||||
consecutiveScore,
|
||||
{3,2,1});
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oob",
|
||||
L"out-of-bound",
|
||||
gapScore,
|
||||
{7,4,0});
|
||||
|
||||
VERIFY_IS_GREATER_THAN(gapScore, consecutiveScore);
|
||||
}
|
||||
|
||||
void FzfTests::MatchWithGapCanHaveHigherScoreThanConsecutiveWhenGapHasFirstCharBonus()
|
||||
{
|
||||
auto consecutiveScore = ScoreMatch * 2 + BonusConsecutive;
|
||||
auto gapScore = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"ob",
|
||||
L"aobar",
|
||||
consecutiveScore,
|
||||
{2,1});
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"ob",
|
||||
L"oabar",
|
||||
gapScore,
|
||||
{2,0});
|
||||
|
||||
VERIFY_IS_GREATER_THAN(gapScore, consecutiveScore);
|
||||
}
|
||||
|
||||
void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_2CharPattern()
|
||||
{
|
||||
auto consecutiveScore = ScoreMatch * 2 + BonusConsecutive;
|
||||
auto gapScore = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart + ScoreGapExtension * 10;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"ob",
|
||||
L"aobar",
|
||||
consecutiveScore,
|
||||
{2,1});
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"ob",
|
||||
L"oaaaaaaaaaaabar",
|
||||
gapScore,
|
||||
{12,0});
|
||||
|
||||
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;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oba",
|
||||
L"aobar",
|
||||
consecutiveScore,
|
||||
{3,2,1});
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oba",
|
||||
L"oaaaaaaaaaaabar",
|
||||
gapScore,
|
||||
{13,12,0});
|
||||
|
||||
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;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oba",
|
||||
L"aobar",
|
||||
allConsecutiveScore,
|
||||
{3,2,1});
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"oba",
|
||||
L"oaaabzzar",
|
||||
allBoundaryWithGapScore,
|
||||
{7,4,0});
|
||||
|
||||
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;
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"obar",
|
||||
L"aobar",
|
||||
consecutiveScore,
|
||||
{4, 3,2,1});
|
||||
|
||||
AssertScoreAndPositions(
|
||||
L"obar",
|
||||
L"oabzazr",
|
||||
gapScore,
|
||||
{6, 4,2,0});
|
||||
|
||||
VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
<ClCompile Include="ColorHelperTests.cpp" />
|
||||
|
||||
<ClCompile Include="JsonUtilsTests.cpp" />
|
||||
<ClCompile Include="FzfTests.cpp" />
|
||||
|
||||
<ClCompile Include="precomp.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
|
||||
Reference in New Issue
Block a user