Skip to content

Commit 74b5a0c

Browse files
e82ericlhecker
andauthored
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 - nvim-telescope/telescope-fzf-native.nvim#148 - junegunn/fzf#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>
1 parent 37d5aec commit 74b5a0c

20 files changed

+1232
-266
lines changed

.github/actions/spelling/excludes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,4 @@ Resources/(?!en)
133133
^\Qsrc/terminal/parser/ft_fuzzwrapper/run.bat\E$
134134
^\Qsrc/tools/lnkd/lnkd.bat\E$
135135
^\Qsrc/tools/pixels/pixels.bat\E$
136+
^\Qsrc/cascadia/ut_app/FzfTests.cpp\E$

.github/actions/spelling/expect/expect.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@ FONTSTRING
651651
FONTTYPE
652652
FONTWIDTH
653653
FONTWINDOW
654+
foob
654655
FORCEOFFFEEDBACK
655656
FORCEONFEEDBACK
656657
FRAMECHANGED
@@ -668,9 +669,11 @@ fuzzer
668669
fuzzmain
669670
fuzzmap
670671
fuzzwrapper
672+
fuzzyfinder
671673
fwdecl
672674
fwe
673675
fwlink
676+
fzf
674677
gci
675678
gcx
676679
gdi
@@ -1248,6 +1251,7 @@ onecoreuuid
12481251
ONECOREWINDOWS
12491252
onehalf
12501253
oneseq
1254+
oob
12511255
openbash
12521256
opencode
12531257
opencon

NOTICE.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ specific language governing permissions and limitations under the License.
285285
**Source**: [https://github.com/commonmark/cmark](https://github.com/commonmark/cmark)
286286

287287
### License
288+
289+
```
288290
Copyright (c) 2014, John MacFarlane
289291
290292
All rights reserved.
@@ -455,6 +457,36 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
455457
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
456458
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
457459
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
460+
```
461+
462+
## fzf
463+
464+
### License
465+
466+
```
467+
The MIT License (MIT)
468+
469+
Copyright (c) 2013-2024 Junegunn Choi
470+
Copyright (c) 2021-2025 Simon Hauser
471+
472+
Permission is hereby granted, free of charge, to any person obtaining a copy
473+
of this software and associated documentation files (the "Software"), to deal
474+
in the Software without restriction, including without limitation the rights
475+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
476+
copies of the Software, and to permit persons to whom the Software is
477+
furnished to do so, subject to the following conditions:
478+
479+
The above copyright notice and this permission notice shall be included in
480+
all copies or substantial portions of the Software.
481+
482+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
483+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
484+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
485+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
486+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
487+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
488+
THE SOFTWARE.
489+
```
458490

459491
# Microsoft Open Source
460492

src/cascadia/LocalTests_TerminalApp/FilteredCommandTests.cpp

Lines changed: 58 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -32,37 +32,35 @@ namespace TerminalAppLocalTests
3232
{
3333
auto result = RunOnUIThread([]() {
3434
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"AAAAAABBBBBBCCC") };
35+
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
36+
3537
{
3638
Log::Comment(L"Testing command name segmentation with no filter");
37-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
38-
auto segments = filteredCommand->_computeHighlightedName().Segments();
39+
auto segments = filteredCommand->HighlightedName().Segments();
3940
VERIFY_ARE_EQUAL(segments.Size(), 1u);
4041
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
4142
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
4243
}
4344
{
4445
Log::Comment(L"Testing command name segmentation with empty filter");
45-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
46-
filteredCommand->_Filter = L"";
47-
auto segments = filteredCommand->_computeHighlightedName().Segments();
46+
filteredCommand->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"")));
47+
auto segments = filteredCommand->HighlightedName().Segments();
4848
VERIFY_ARE_EQUAL(segments.Size(), 1u);
4949
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
5050
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
5151
}
5252
{
5353
Log::Comment(L"Testing command name segmentation with filter equal to the string");
54-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
55-
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
56-
auto segments = filteredCommand->_computeHighlightedName().Segments();
54+
filteredCommand->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"AAAAAABBBBBBCCC")));
55+
auto segments = filteredCommand->HighlightedName().Segments();
5756
VERIFY_ARE_EQUAL(segments.Size(), 1u);
5857
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
5958
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
6059
}
6160
{
6261
Log::Comment(L"Testing command name segmentation with filter with first character matching");
63-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
64-
filteredCommand->_Filter = L"A";
65-
auto segments = filteredCommand->_computeHighlightedName().Segments();
62+
filteredCommand->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"A")));
63+
auto segments = filteredCommand->HighlightedName().Segments();
6664
VERIFY_ARE_EQUAL(segments.Size(), 2u);
6765
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
6866
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
@@ -71,9 +69,8 @@ namespace TerminalAppLocalTests
7169
}
7270
{
7371
Log::Comment(L"Testing command name segmentation with filter with other case");
74-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
75-
filteredCommand->_Filter = L"a";
76-
auto segments = filteredCommand->_computeHighlightedName().Segments();
72+
filteredCommand->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"a")));
73+
auto segments = filteredCommand->HighlightedName().Segments();
7774
VERIFY_ARE_EQUAL(segments.Size(), 2u);
7875
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
7976
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
@@ -82,24 +79,20 @@ namespace TerminalAppLocalTests
8279
}
8380
{
8481
Log::Comment(L"Testing command name segmentation with filter matching several characters");
85-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
86-
filteredCommand->_Filter = L"ab";
87-
auto segments = filteredCommand->_computeHighlightedName().Segments();
88-
VERIFY_ARE_EQUAL(segments.Size(), 4u);
89-
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
90-
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
91-
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAA");
92-
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
93-
VERIFY_ARE_EQUAL(segments.GetAt(2).TextSegment(), L"B");
94-
VERIFY_IS_TRUE(segments.GetAt(2).IsHighlighted());
95-
VERIFY_ARE_EQUAL(segments.GetAt(3).TextSegment(), L"BBBBBCCC");
96-
VERIFY_IS_FALSE(segments.GetAt(3).IsHighlighted());
82+
filteredCommand->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"ab")));
83+
auto segments = filteredCommand->HighlightedName().Segments();
84+
VERIFY_ARE_EQUAL(segments.Size(), 3u);
85+
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAA");
86+
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
87+
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AB");
88+
VERIFY_IS_TRUE(segments.GetAt(1).IsHighlighted());
89+
VERIFY_ARE_EQUAL(segments.GetAt(2).TextSegment(), L"BBBBBCCC");
90+
VERIFY_IS_FALSE(segments.GetAt(2).IsHighlighted());
9791
}
9892
{
9993
Log::Comment(L"Testing command name segmentation with non matching filter");
100-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
101-
filteredCommand->_Filter = L"abcd";
102-
auto segments = filteredCommand->_computeHighlightedName().Segments();
94+
filteredCommand->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"abcd")));
95+
auto segments = filteredCommand->HighlightedName().Segments();
10396
VERIFY_ARE_EQUAL(segments.Size(), 1u);
10497
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
10598
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
@@ -113,53 +106,37 @@ namespace TerminalAppLocalTests
113106
{
114107
auto result = RunOnUIThread([]() {
115108
const auto paletteItem{ winrt::make<winrt::TerminalApp::implementation::CommandLinePaletteItem>(L"AAAAAABBBBBBCCC") };
116-
{
117-
Log::Comment(L"Testing weight of command with no filter");
118-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
119-
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
120-
auto weight = filteredCommand->_computeWeight();
121-
VERIFY_ARE_EQUAL(weight, 0);
122-
}
123-
{
124-
Log::Comment(L"Testing weight of command with empty filter");
125-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
126-
filteredCommand->_Filter = L"";
127-
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
128-
auto weight = filteredCommand->_computeWeight();
129-
VERIFY_ARE_EQUAL(weight, 0);
130-
}
131-
{
132-
Log::Comment(L"Testing weight of command with filter equal to the string");
133-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
134-
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
135-
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
136-
auto weight = filteredCommand->_computeWeight();
137-
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
138-
}
139-
{
140-
Log::Comment(L"Testing weight of command with filter with first character matching");
141-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
142-
filteredCommand->_Filter = L"A";
143-
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
144-
auto weight = filteredCommand->_computeWeight();
145-
VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word
146-
}
147-
{
148-
Log::Comment(L"Testing weight of command with filter with other case");
149-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
150-
filteredCommand->_Filter = L"a";
151-
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
152-
auto weight = filteredCommand->_computeWeight();
153-
VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word
154-
}
155-
{
156-
Log::Comment(L"Testing weight of command with filter matching several characters");
157-
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
158-
filteredCommand->_Filter = L"ab";
159-
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
160-
auto weight = filteredCommand->_computeWeight();
161-
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"
162-
}
109+
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
110+
111+
const auto weigh = [&](const wchar_t* str) {
112+
std::shared_ptr<fzf::matcher::Pattern> pattern;
113+
if (str)
114+
{
115+
pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(str));
116+
}
117+
filteredCommand->UpdateFilter(std::move(pattern));
118+
return filteredCommand->Weight();
119+
};
120+
121+
const auto null = weigh(nullptr);
122+
const auto empty = weigh(L"");
123+
const auto full = weigh(L"AAAAAABBBBBBCCC");
124+
const auto firstChar = weigh(L"A");
125+
const auto otherCase = weigh(L"a");
126+
const auto severalChars = weigh(L"ab");
127+
128+
VERIFY_ARE_EQUAL(null, 0);
129+
VERIFY_ARE_EQUAL(empty, 0);
130+
VERIFY_IS_GREATER_THAN(full, 100);
131+
132+
VERIFY_IS_GREATER_THAN(firstChar, 0);
133+
VERIFY_IS_LESS_THAN(firstChar, full);
134+
135+
VERIFY_IS_GREATER_THAN(otherCase, 0);
136+
VERIFY_IS_LESS_THAN(otherCase, full);
137+
138+
VERIFY_IS_GREATER_THAN(severalChars, otherCase);
139+
VERIFY_IS_LESS_THAN(severalChars, full);
163140
});
164141

165142
VERIFY_SUCCEEDED(result);
@@ -181,31 +158,23 @@ namespace TerminalAppLocalTests
181158
{
182159
Log::Comment(L"Testing comparison of commands with empty filter");
183160
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
184-
filteredCommand->_Filter = L"";
185-
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
186-
filteredCommand->_Weight = filteredCommand->_computeWeight();
161+
filteredCommand->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"")));
187162

188163
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
189-
filteredCommand2->_Filter = L"";
190-
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
191-
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
164+
filteredCommand2->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"")));
192165

193166
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
194167
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
195168
}
196169
{
197170
Log::Comment(L"Testing comparison of commands with different weights");
198171
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem);
199-
filteredCommand->_Filter = L"B";
200-
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
201-
filteredCommand->_Weight = filteredCommand->_computeWeight();
172+
filteredCommand->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"B")));
202173

203174
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(paletteItem2);
204-
filteredCommand2->_Filter = L"B";
205-
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
206-
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
175+
filteredCommand2->UpdateFilter(std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(L"B")));
207176

208-
VERIFY_IS_TRUE(filteredCommand->Weight() < filteredCommand2->Weight()); // Second command gets more points due to the beginning of the word
177+
VERIFY_IS_LESS_THAN(filteredCommand->Weight(), filteredCommand2->Weight()); // Second command gets more points due to the beginning of the word
209178
VERIFY_IS_FALSE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
210179
}
211180
});

src/cascadia/TerminalApp/CommandPalette.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1174,12 +1174,15 @@ namespace winrt::TerminalApp::implementation
11741174
}
11751175
else if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::ActionMode || _currentMode == CommandPaletteMode::CommandlineMode)
11761176
{
1177+
auto pattern = std::make_shared<fzf::matcher::Pattern>(fzf::matcher::ParsePattern(searchText));
1178+
11771179
for (const auto& action : commandsToFilter)
11781180
{
11791181
// Update filter for all commands
11801182
// This will modify the highlighting but will also lead to re-computation of weight (and consequently sorting).
11811183
// Pay attention that it already updates the highlighting in the UI
1182-
action.UpdateFilter(searchText);
1184+
auto impl = winrt::get_self<implementation::FilteredCommand>(action);
1185+
impl->UpdateFilter(pattern);
11831186

11841187
// if there is active search we skip commands with 0 weight
11851188
if (searchText.empty() || action.Weight() > 0)

0 commit comments

Comments
 (0)