Bold matching text in the command palette (#7977)

* Created a ViewModel class in the Command Palette called
  FilteredCommand, aggregating the Command, the filter and the
  highlighted presentation of the command name
* This ListView of the filtered commands is bound to the vector of
  FilteredCommands
* Introduced HighlightedTextControl user control with HighlightedText
  view model
* Added this control to the ListView Item's grid
* Bound the FilteredCommand's highlighted command name to the user
  control

## Validation Steps Performed
* UT for matching algorithm
* Only manual tests
* Searching in CommandLine, SwitchTab and Nested Command modes
* Checking for bot matching an non matching filters
* Dogfooding

Closes #6646
This commit is contained in:
Don-Vito 2020-11-06 03:37:45 +02:00 committed by GitHub
parent 015675d87c
commit 1aff3bc216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 975 additions and 260 deletions

View File

@ -2825,3 +2825,7 @@ zsh
zu
zxcvbnm
zy
AAAAABBBBBBCCC
AAAAA
BBBBBCCC
abcd

View File

@ -0,0 +1,271 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "../TerminalApp/TerminalSettings.h"
#include "../TerminalApp/CommandPalette.h"
using namespace Microsoft::Console;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace WEX::Common;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::TerminalControl;
namespace TerminalAppLocalTests
{
class FilteredCommandTests
{
BEGIN_TEST_CLASS(FilteredCommandTests)
TEST_CLASS_PROPERTY(L"RunAs", L"UAP")
TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml")
END_TEST_CLASS()
TEST_METHOD(VerifyHighlighting);
TEST_METHOD(VerifyWeight);
TEST_METHOD(VerifyCompare);
};
void FilteredCommandTests::VerifyHighlighting()
{
const std::string settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
}
],
"keybindings": [
{ "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" }, "name": "AAAAAABBBBBBCCC" }
]
})" };
CascadiaSettings settings{ til::u8u16(settingsJson) };
const auto commands = settings.GlobalSettings().Commands();
VERIFY_ARE_EQUAL(1u, commands.Size());
const auto command = commands.Lookup(L"AAAAAABBBBBBCCC");
VERIFY_IS_NOT_NULL(command);
VERIFY_ARE_EQUAL(command.Name(), L"AAAAAABBBBBBCCC");
{
Log::Comment(L"Testing command name segmentation with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with empty filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter equals to the string");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter with first character matching");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"A";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 2u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter with other case");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"a";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 2u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with filter matching several characters");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"ab";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 4u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A");
VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAA");
VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(2).TextSegment(), L"B");
VERIFY_IS_TRUE(segments.GetAt(2).IsHighlighted());
VERIFY_ARE_EQUAL(segments.GetAt(3).TextSegment(), L"BBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(3).IsHighlighted());
}
{
Log::Comment(L"Testing command name segmentation with non matching filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"abcd";
auto segments = filteredCommand->_computeHighlightedName().Segments();
VERIFY_ARE_EQUAL(segments.Size(), 1u);
VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC");
VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted());
}
}
void FilteredCommandTests::VerifyWeight()
{
const std::string settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
}
],
"keybindings": [
{ "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" }, "name": "AAAAAABBBBBBCCC" }
]
})" };
CascadiaSettings settings{ til::u8u16(settingsJson) };
const auto commands = settings.GlobalSettings().Commands();
VERIFY_ARE_EQUAL(1u, commands.Size());
const auto command = commands.Lookup(L"AAAAAABBBBBBCCC");
VERIFY_IS_NOT_NULL(command);
VERIFY_ARE_EQUAL(command.Name(), L"AAAAAABBBBBBCCC");
{
Log::Comment(L"Testing weight of command with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
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>(command);
filteredCommand->_Filter = L"";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
VERIFY_ARE_EQUAL(weight, 0);
}
{
Log::Comment(L"Testing weight of command with filter equals to the string");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"AAAAAABBBBBBCCC";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
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>(command);
filteredCommand->_Filter = L"A";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
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>(command);
filteredCommand->_Filter = L"a";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
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>(command);
filteredCommand->_Filter = L"ab";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
auto weight = filteredCommand->_computeWeight();
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"
}
}
void FilteredCommandTests::VerifyCompare()
{
const std::string settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
}
],
"keybindings": [
{ "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" }, "name": "AAAAAABBBBBBCCC" },
{ "keys": ["ctrl+b"], "command": { "action": "splitPane", "split": "horizontal" }, "name": "BBBBBCCC" }
]
})" };
CascadiaSettings settings{ til::u8u16(settingsJson) };
const auto commands = settings.GlobalSettings().Commands();
VERIFY_ARE_EQUAL(2u, commands.Size());
const auto command = commands.Lookup(L"AAAAAABBBBBBCCC");
VERIFY_IS_NOT_NULL(command);
VERIFY_ARE_EQUAL(command.Name(), L"AAAAAABBBBBBCCC");
const auto command2 = commands.Lookup(L"BBBBBCCC");
VERIFY_IS_NOT_NULL(command2);
VERIFY_ARE_EQUAL(command2.Name(), L"BBBBBCCC");
{
Log::Comment(L"Testing comparison of commands with no filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command2);
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
{
Log::Comment(L"Testing comparison of commands with empty filter");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
filteredCommand->_Weight = filteredCommand->_computeWeight();
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command2);
filteredCommand2->_Filter = L"";
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight());
VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2));
}
{
Log::Comment(L"Testing comparison of commands with different weights");
const auto filteredCommand = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command);
filteredCommand->_Filter = L"B";
filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName();
filteredCommand->_Weight = filteredCommand->_computeWeight();
const auto filteredCommand2 = winrt::make_self<winrt::TerminalApp::implementation::FilteredCommand>(command2);
filteredCommand2->_Filter = L"B";
filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName();
filteredCommand2->_Weight = filteredCommand2->_computeWeight();
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));
}
}
}

View File

@ -45,6 +45,7 @@
<ClCompile Include="CommandlineTest.cpp" />
<ClCompile Include="SettingsTests.cpp" />
<ClCompile Include="TabTests.cpp" />
<ClCompile Include="FilteredCommandTests.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>

View File

@ -24,11 +24,11 @@ namespace winrt::TerminalApp::implementation
{
InitializeComponent();
_filteredActions = winrt::single_threaded_observable_vector<Command>();
_nestedActionStack = winrt::single_threaded_vector<Command>();
_currentNestedCommands = winrt::single_threaded_vector<Command>();
_allCommands = winrt::single_threaded_vector<Command>();
_tabActions = winrt::single_threaded_vector<Command>();
_filteredActions = winrt::single_threaded_observable_vector<winrt::TerminalApp::FilteredCommand>();
_nestedActionStack = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_currentNestedCommands = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_allCommands = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_tabActions = winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
_switchToMode(CommandPaletteMode::ActionMode);
@ -176,10 +176,10 @@ namespace winrt::TerminalApp::implementation
{
if (_currentMode == CommandPaletteMode::TabSwitchMode)
{
const auto& selectedCommand = _filteredActionsView().SelectedItem();
if (const auto& command = selectedCommand.try_as<Command>())
const auto selectedCommand = _filteredActionsView().SelectedItem();
if (const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>())
{
const auto& actionAndArgs = command.Action();
const auto& actionAndArgs = filteredCommand.Command().Action();
_dispatch.DoAction(actionAndArgs);
}
}
@ -267,9 +267,10 @@ namespace winrt::TerminalApp::implementation
// Action, TabSwitch or TabSearchMode Mode: Dispatch the action of the selected command.
if (_currentMode != CommandPaletteMode::CommandlineMode)
{
if (const auto selectedItem = _filteredActionsView().SelectedItem())
const auto selectedCommand = _filteredActionsView().SelectedItem();
if (const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>())
{
_dispatchCommand(selectedItem.try_as<Command>());
_dispatchCommand(filteredCommand);
}
}
// Commandline Mode: Use the input to synthesize an ExecuteCommandline action
@ -376,12 +377,10 @@ namespace winrt::TerminalApp::implementation
if (!ctrlDown && !altDown && !shiftDown)
{
if (const auto selectedItem = _filteredActionsView().SelectedItem())
const auto selectedCommand = _filteredActionsView().SelectedItem();
if (const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>())
{
if (const auto data = selectedItem.try_as<Command>())
{
_dispatchCommand(data);
}
_dispatchCommand(filteredCommand);
}
}
}
@ -425,7 +424,11 @@ namespace winrt::TerminalApp::implementation
void CommandPalette::_listItemClicked(Windows::Foundation::IInspectable const& /*sender*/,
Windows::UI::Xaml::Controls::ItemClickEventArgs const& e)
{
_dispatchCommand(e.ClickedItem().try_as<Command>());
const auto selectedCommand = e.ClickedItem();
if (const auto filteredCommand = selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>())
{
_dispatchCommand(filteredCommand);
}
}
// Method Description:
@ -468,6 +471,7 @@ namespace winrt::TerminalApp::implementation
// Changing the value of the search box will trigger _filterTextChanged,
// which will cause us to refresh the list of filterable commands.
_searchBox().Text(L"");
_searchBox().Focus(FocusState::Programmatic);
}
// Method Description:
@ -479,7 +483,7 @@ namespace winrt::TerminalApp::implementation
// - <none>
// Return Value:
// - A list of Commands to filter.
Collections::IVector<Command> CommandPalette::_commandsToFilter()
Collections::IVector<winrt::TerminalApp::FilteredCommand> CommandPalette::_commandsToFilter()
{
switch (_currentMode)
{
@ -495,7 +499,7 @@ namespace winrt::TerminalApp::implementation
case CommandPaletteMode::TabSwitchMode:
return _tabActions;
case CommandPaletteMode::CommandlineMode:
return winrt::single_threaded_vector<Command>();
return winrt::single_threaded_vector<winrt::TerminalApp::FilteredCommand>();
default:
return _allCommands;
}
@ -510,21 +514,23 @@ namespace winrt::TerminalApp::implementation
// - command: the Command to dispatch. This might be null.
// Return Value:
// - <none>
void CommandPalette::_dispatchCommand(const Command& command)
void CommandPalette::_dispatchCommand(winrt::TerminalApp::FilteredCommand const& filteredCommand)
{
if (command)
if (filteredCommand)
{
if (command.HasNestedCommands())
if (filteredCommand.Command().HasNestedCommands())
{
// If this Command had subcommands, then don't dispatch the
// action. Instead, display a new list of commands for the user
// to pick from.
_nestedActionStack.Append(command);
ParentCommandName(command.Name());
_nestedActionStack.Append(filteredCommand);
ParentCommandName(filteredCommand.Command().Name());
_currentNestedCommands.Clear();
for (const auto& nameAndCommand : command.NestedCommands())
for (const auto& nameAndCommand : filteredCommand.Command().NestedCommands())
{
_currentNestedCommands.Append(nameAndCommand.Value());
const auto action = nameAndCommand.Value();
auto nestedFilteredCommand{ winrt::make<FilteredCommand>(action) };
_currentNestedCommands.Append(nestedFilteredCommand);
}
_updateUIForStackChange();
@ -541,7 +547,7 @@ namespace winrt::TerminalApp::implementation
// palette like the Tab Switcher will be able to have the last laugh.
_close();
const auto actionAndArgs = command.Action();
const auto actionAndArgs = filteredCommand.Command().Action();
_dispatch.DoAction(actionAndArgs);
TraceLoggingWrite(
@ -688,7 +694,7 @@ namespace winrt::TerminalApp::implementation
}
}
Collections::IObservableVector<Command> CommandPalette::FilteredActions()
Collections::IObservableVector<winrt::TerminalApp::FilteredCommand> CommandPalette::FilteredActions()
{
return _filteredActions;
}
@ -700,13 +706,13 @@ namespace winrt::TerminalApp::implementation
void CommandPalette::SetCommands(Collections::IVector<Command> const& actions)
{
_allCommands = actions;
_populateFilteredActions(_allCommands, actions);
_updateFilteredActions();
}
void CommandPalette::SetTabActions(Collections::IVector<Command> const& tabs, const bool clearList)
{
_tabActions = tabs;
_populateFilteredActions(_tabActions, tabs);
// The smooth remove/add animations that happen during
// UpdateFilteredActions don't work very well with changing the tab
// order, because of the sheer amount of remove/adds. So, let's just
@ -721,6 +727,24 @@ namespace winrt::TerminalApp::implementation
_updateFilteredActions();
}
// Method Description:
// - This helper function is responsible to update a collection of filtered commands (e.g., tab switcher commands)
// with the new values
// Arguments:
// - vectorToPopulate - the vector of filtered commands to populate
// - actions - the raw commands to use
// Return Value:
// - <none>
void CommandPalette::_populateFilteredActions(Collections::IVector<winrt::TerminalApp::FilteredCommand> const& vectorToPopulate, Collections::IVector<Command> const& actions)
{
vectorToPopulate.Clear();
for (const auto& action : actions)
{
auto filteredCommand{ winrt::make<FilteredCommand>(action) };
vectorToPopulate.Append(filteredCommand);
}
}
void CommandPalette::EnableCommandPaletteMode()
{
_switchToMode(CommandPaletteMode::ActionMode);
@ -777,146 +801,42 @@ namespace winrt::TerminalApp::implementation
}
}
// This is a helper to aid in sorting commands by their `Name`s, alphabetically.
static bool _compareCommandNames(const Command& lhs, const Command& rhs)
{
std::wstring_view leftName{ lhs.Name() };
std::wstring_view rightName{ rhs.Name() };
return leftName.compare(rightName) < 0;
}
// This is a helper struct to aid in sorting Commands by a given weighting.
struct WeightedCommand
{
Command command;
int weight;
int inOrderCounter;
bool operator<(const WeightedCommand& other) const
{
if (weight == other.weight)
{
// If two commands have the same weight, then we'll sort them alphabetically.
// If they both have the same name, fall back to the order in which they were
// pushed into the heap.
if (command.Name() == other.command.Name())
{
return inOrderCounter > other.inOrderCounter;
}
else
{
return !_compareCommandNames(command, other.command);
}
}
return weight < other.weight;
}
};
// Method Description:
// - Produce a list of filtered actions to reflect the current contents of
// the input box. For more details on which commands will be displayed,
// see `_getWeight`.
// the input box.
// Arguments:
// - A collection that will receive the filtered actions
// Return Value:
// - <none>
std::vector<Command> CommandPalette::_collectFilteredActions()
std::vector<winrt::TerminalApp::FilteredCommand> CommandPalette::_collectFilteredActions()
{
std::vector<Command> actions;
std::vector<winrt::TerminalApp::FilteredCommand> actions;
winrt::hstring searchText{ _getTrimmedInput() };
const bool addAll = searchText.empty();
auto commandsToFilter = _commandsToFilter();
// If there's no filter text, then just add all the commands in order to the list.
// - TODO GH#6647:Possibly add the MRU commands first in order, followed
// by the rest of the commands.
if (addAll)
for (const auto& action : commandsToFilter)
{
// If TabSwitcherMode, just add all as is. We don't want
// them to be sorted alphabetically.
if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::TabSwitchMode)
{
for (auto action : commandsToFilter)
{
actions.push_back(action);
}
// Update filter for all commands
// This will modify the highlighting but will also lead to recomputation of weight (and consequently sorting).
// Pay attention that it already updates the highlighting in the UI
action.UpdateFilter(searchText);
return actions;
}
// Add all the commands, but make sure they're sorted alphabetically.
std::vector<Command> sortedCommands;
sortedCommands.reserve(commandsToFilter.Size());
for (auto action : commandsToFilter)
{
sortedCommands.push_back(action);
}
std::sort(sortedCommands.begin(),
sortedCommands.end(),
_compareCommandNames);
for (auto action : sortedCommands)
// if there is active search we skip commands with 0 weight
if (searchText.empty() || action.Weight() > 0)
{
actions.push_back(action);
}
return actions;
}
// Here, there was some filter text.
// Show these actions in a weighted order.
// - Matching the first character of a word, then the first char of a
// subsequent word seems better than just "the order they appear in
// the list".
// - TODO GH#6647:"Recently used commands" ordering also seems valuable.
// * This could be done by weighting the recently used commands
// higher the more recently they were used, then weighting all
// the unused commands as 1
// Use a priority queue to order commands so that "better" matches
// appear first in the list. The ordering will be determined by the
// match weight produced by _getWeight.
std::priority_queue<WeightedCommand> heap;
// TODO GH#7205: Find a better way to ensure that WCs of the same
// weight and name stay in the order in which they were pushed onto
// the PQ.
uint32_t counter = 0;
for (auto action : commandsToFilter)
{
const auto weight = CommandPalette::_getWeight(searchText, action.Name());
if (weight > 0)
{
WeightedCommand wc;
wc.command = action;
wc.weight = weight;
wc.inOrderCounter = counter++;
heap.push(wc);
}
}
// At this point, all the commands in heap are matches. We've also
// sorted commands with the same weight alphabetically.
// Remove everything in-order from the queue, and add to the list of
// filtered actions.
while (!heap.empty())
{
auto top = heap.top();
heap.pop();
actions.push_back(top.command);
}
// Add all the commands, but make sure they're sorted.
std::sort(actions.begin(), actions.end(), FilteredCommand::Compare);
return actions;
}
// Method Description:
// - Update our list of filtered actions to reflect the current contents of
// the input box. For more details on which commands will be displayed,
// see `_getWeight`.
// the input box.
// Arguments:
// - <none>
// Return Value:
@ -937,7 +857,7 @@ namespace winrt::TerminalApp::implementation
{
for (uint32_t j = i; j < _filteredActions.Size(); j++)
{
if (_filteredActions.GetAt(j) == actions[i])
if (_filteredActions.GetAt(j).Command() == actions[i].Command())
{
for (uint32_t k = i; k < j; k++)
{
@ -947,7 +867,7 @@ namespace winrt::TerminalApp::implementation
}
}
if (_filteredActions.GetAt(i) != actions[i])
if (_filteredActions.GetAt(i).Command() != actions[i].Command())
{
_filteredActions.InsertAt(i, actions[i]);
}
@ -966,92 +886,6 @@ namespace winrt::TerminalApp::implementation
}
}
// Function Description:
// - Calculates a "weighting" by which should be used to order a command
// 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 command 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 CommandPalette::_getWeight(const winrt::hstring& searchText,
const winrt::hstring& name)
{
int totalWeight = 0;
bool lastWasSpace = true;
auto it = name.cbegin();
for (auto searchChar : searchText)
{
searchChar = std::towlower(searchChar);
// Advance the iterator to the next character that we're looking
// for.
bool lastWasMatch = true;
while (true)
{
// If we are at the end of the name string, we haven't found
// it.
if (it == name.cend())
{
return false;
}
// found it
if (std::towlower(*it) == searchChar)
{
break;
}
lastWasSpace = *it == L' ';
++it;
lastWasMatch = false;
}
// Advance the iterator by one character so that we don't
// end up on the same character in the next iteration.
++it;
totalWeight += 1;
totalWeight += lastWasSpace ? 1 : 0;
totalWeight += (lastWasMatch) ? 1 : 0;
}
return totalWeight;
}
void CommandPalette::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch)
{
_dispatch = dispatch;

View File

@ -3,9 +3,16 @@
#pragma once
#include "FilteredCommand.h"
#include "CommandPalette.g.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
// fwdecl unittest classes
namespace TerminalAppLocalTests
{
class FilteredCommandTests;
};
namespace winrt::TerminalApp::implementation
{
enum class CommandPaletteMode
@ -20,7 +27,7 @@ namespace winrt::TerminalApp::implementation
{
CommandPalette();
Windows::Foundation::Collections::IObservableVector<Microsoft::Terminal::Settings::Model::Command> FilteredActions();
Windows::Foundation::Collections::IObservableVector<winrt::TerminalApp::FilteredCommand> FilteredActions();
void SetCommands(Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> const& actions);
void SetTabActions(Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> const& tabs, const bool clearList);
@ -52,13 +59,13 @@ namespace winrt::TerminalApp::implementation
private:
friend struct CommandPaletteT<CommandPalette>; // for Xaml to bind events
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _allCommands{ nullptr };
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _currentNestedCommands{ nullptr };
Windows::Foundation::Collections::IObservableVector<Microsoft::Terminal::Settings::Model::Command> _filteredActions{ nullptr };
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _nestedActionStack{ nullptr };
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _allCommands{ nullptr };
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _currentNestedCommands{ nullptr };
Windows::Foundation::Collections::IObservableVector<winrt::TerminalApp::FilteredCommand> _filteredActions{ nullptr };
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _nestedActionStack{ nullptr };
winrt::TerminalApp::ShortcutActionDispatch _dispatch;
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _commandsToFilter();
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _commandsToFilter();
bool _lastFilterTextWasEmpty{ true };
@ -85,7 +92,10 @@ namespace winrt::TerminalApp::implementation
void _updateFilteredActions();
std::vector<Microsoft::Terminal::Settings::Model::Command> _collectFilteredActions();
void _populateFilteredActions(Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> const& vectorToPopulate,
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> const& actions);
std::vector<winrt::TerminalApp::FilteredCommand> _collectFilteredActions();
static int _getWeight(const winrt::hstring& searchText, const winrt::hstring& name);
void _close();
@ -99,13 +109,13 @@ namespace winrt::TerminalApp::implementation
Microsoft::Terminal::TerminalControl::IKeyBindings _bindings;
// Tab Switcher
Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command> _tabActions{ nullptr };
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _tabActions{ nullptr };
uint32_t _switcherStartIdx;
void _anchorKeyUpHandler();
winrt::Windows::UI::Xaml::Controls::ListView::SizeChanged_revoker _sizeChangedRevoker;
void _dispatchCommand(const Microsoft::Terminal::Settings::Model::Command& command);
void _dispatchCommand(winrt::TerminalApp::FilteredCommand const& command);
void _dispatchCommandline();
void _dismissPalette();
};

View File

@ -3,6 +3,8 @@
import "IDirectKeyListener.idl";
import "ShortcutActionDispatch.idl";
import "HighlightedTextControl.idl";
import "FilteredCommand.idl";
namespace TerminalApp
{
@ -16,7 +18,7 @@ namespace TerminalApp
String ControlName { get; };
String ParentCommandName { get; };
Windows.Foundation.Collections.IObservableVector<Microsoft.Terminal.Settings.Model.Command> FilteredActions { get; };
Windows.Foundation.Collections.IObservableVector<FilteredCommand> FilteredActions { get; };
void SetCommands(Windows.Foundation.Collections.IVector<Microsoft.Terminal.Settings.Model.Command> actions);
void SetTabActions(Windows.Foundation.Collections.IVector<Microsoft.Terminal.Settings.Model.Command> tabs, Boolean clearList);

View File

@ -114,7 +114,6 @@ the MIT License. See LICENSE in the project root for license information. -->
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="6*" />
@ -240,16 +239,15 @@ the MIT License. See LICENSE in the project root for license information. -->
ItemsSource="{x:Bind FilteredActions}">
<ItemsControl.ItemTemplate >
<DataTemplate x:DataType="SettingsModel:Command">
<DataTemplate x:DataType="local:FilteredCommand">
<!-- This HorizontalContentAlignment="Stretch" is important
to make sure it takes the entire width of the line -->
<ListViewItem HorizontalContentAlignment="Stretch"
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
AutomationProperties.AcceleratorKey="{x:Bind KeyChordText, Mode=OneWay}">
AutomationProperties.Name="{x:Bind Command.Name, Mode=OneWay}"
AutomationProperties.AcceleratorKey="{x:Bind Command.KeyChordText, Mode=OneWay}">
<Grid HorizontalAlignment="Stretch" ColumnSpacing="8" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/> <!-- icon -->
<ColumnDefinition Width="Auto"/> <!-- command label -->
@ -261,20 +259,21 @@ the MIT License. See LICENSE in the project root for license information. -->
Grid.Column="0"
Width="16"
Height="16"
IconSource="{x:Bind Icon,
IconSource="{x:Bind Command.Icon,
Mode=OneWay,
Converter={StaticResource IconSourceConverter}}"/>
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
Text="{x:Bind Name, Mode=OneWay}" />
<local:HighlightedTextControl
Grid.Column="1"
HorizontalAlignment="Left"
Text="{x:Bind HighlightedName, Mode=OneWay}"/>
<!-- The block for the key chord is only visible
when there's actual text set as the label. See
CommandKeyChordVisibilityConverter for details. -->
<Border
Grid.Column="2"
Visibility="{x:Bind KeyChordText,
Visibility="{x:Bind Command.KeyChordText,
Mode=OneWay,
Converter={StaticResource CommandKeyChordVisibilityConverter}}"
Style="{ThemeResource KeyChordBorderStyle}"
@ -285,7 +284,7 @@ the MIT License. See LICENSE in the project root for license information. -->
<TextBlock
Style="{ThemeResource KeyChordTextBlockStyle}"
FontSize="12"
Text="{x:Bind KeyChordText, Mode=OneWay}" />
Text="{x:Bind Command.KeyChordText, Mode=OneWay}" />
</Border>
<!-- xE70E is ChevronUp. Rotated 90 degrees, it's _ChevronRight_ -->
@ -293,7 +292,7 @@ the MIT License. See LICENSE in the project root for license information. -->
FontFamily="Segoe MDL2 Assets"
Glyph="&#xE70E;"
HorizontalAlignment="Right"
Visibility="{x:Bind HasNestedCommands,
Visibility="{x:Bind Command.HasNestedCommands,
Mode=OneWay,
Converter={StaticResource HasNestedCommandsVisibilityConverter}}"
Grid.Column="2">

View File

@ -0,0 +1,239 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "CommandPalette.h"
#include "HighlightedText.h"
#include <LibraryResources.h>
#include "FilteredCommand.g.cpp"
using namespace winrt;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::System;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::TerminalApp::implementation
{
// This is a view model class that extends the Command model,
// by managing a highlighted text that is computed by matching search filter characters to command name
FilteredCommand::FilteredCommand(Microsoft::Terminal::Settings::Model::Command const& command) :
_Command(command),
_Filter(L""),
_Weight(0)
{
_HighlightedName = _computeHighlightedName();
// Recompute the highlighted name if the command name changes
_commandChangedRevoker = _Command.PropertyChanged(winrt::auto_revoke, [weakThis{ get_weak() }](Windows::Foundation::IInspectable const& /*sender*/, Data::PropertyChangedEventArgs const& e) {
auto filteredCommand{ weakThis.get() };
if (filteredCommand && e.PropertyName() == L"Name")
{
filteredCommand->HighlightedName(filteredCommand->_computeHighlightedName());
filteredCommand->Weight(filteredCommand->_computeWeight());
}
});
}
void FilteredCommand::UpdateFilter(winrt::hstring const& filter)
{
// 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)
{
Filter(filter);
HighlightedName(_computeHighlightedName());
Weight(_computeWeight());
}
}
// Method Description:
// - Looks up the filter characters within the command name.
// Iterating through the filter and the command name it tries to associate the next filter character
// with the first appearance of this character in the command 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 command 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()
{
const auto segments = winrt::single_threaded_observable_vector<winrt::TerminalApp::HighlightedTextSegment>();
auto commandName = _Command.Name();
bool isProcessingMatchedSegment = false;
uint32_t nextOffsetToReport = 0;
uint32_t currentOffset = 0;
for (const auto searchChar : _Filter)
{
const auto lowerCaseSearchChar = std::towlower(searchChar);
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 command name as unmatched
auto entireNameSegment{ winrt::make<HighlightedTextSegment>(commandName, false) };
segments.Clear();
segments.Append(entireNameSegment);
return winrt::make<HighlightedText>(segments);
}
auto isCurrentCharMatched = std::towlower(commandName[currentOffset]) == lowerCaseSearchChar;
if (isProcessingMatchedSegment != isCurrentCharMatched)
{
// 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)
{
winrt::hstring segment{ commandName.data() + nextOffsetToReport, sizeToReport };
auto highlightedSegment{ winrt::make<HighlightedTextSegment>(segment, isProcessingMatchedSegment) };
segments.Append(highlightedSegment);
nextOffsetToReport = currentOffset;
}
isProcessingMatchedSegment = isCurrentCharMatched;
}
currentOffset++;
if (isCurrentCharMatched)
{
// We have matched this filter character, let's move to matching the next filter char
break;
}
}
}
// Either the filter or the command 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)
{
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 command 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 command
// 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 command 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()
{
int result = 0;
bool 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)
{
result++;
}
}
isNextSegmentWordBeginning = segmentSize > 0 && segmentText[segmentSize - 1] == L' ';
}
return result;
}
// Function Description:
// - Implementation of Compare for FilteredCommand interface.
// Compares firs command with the second command, first by weight, then by name.
// In the case of a tie prefers the first command
// Arguments:
// - other: another instance of Filtered Command interface
// Return Value:
// - Returns true if the first is "bigger" (aka should appear first)
int FilteredCommand::Compare(winrt::TerminalApp::FilteredCommand const& first, winrt::TerminalApp::FilteredCommand const& second)
{
auto firstWeight{ first.Weight() };
auto secondWeight{ second.Weight() };
if (firstWeight == secondWeight)
{
std::wstring_view firstName{ first.Command().Name() };
std::wstring_view secondName{ second.Command().Name() };
return firstName.compare(secondName) < 0;
}
return firstWeight > secondWeight;
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "HighlightedTextControl.h"
#include "FilteredCommand.g.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
// fwdecl unittest classes
namespace TerminalAppLocalTests
{
class FilteredCommandTests;
};
namespace winrt::TerminalApp::implementation
{
struct FilteredCommand : FilteredCommandT<FilteredCommand>
{
FilteredCommand() = default;
FilteredCommand(Microsoft::Terminal::Settings::Model::Command const& command);
void UpdateFilter(winrt::hstring const& filter);
static int Compare(winrt::TerminalApp::FilteredCommand const& first, winrt::TerminalApp::FilteredCommand const& second);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(Microsoft::Terminal::Settings::Model::Command, Command, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Filter, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::TerminalApp::HighlightedText, HighlightedName, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(int, Weight, _PropertyChangedHandlers);
private:
winrt::TerminalApp::HighlightedText _computeHighlightedName();
int _computeWeight();
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _commandChangedRevoker;
friend class TerminalAppLocalTests::FilteredCommandTests;
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(FilteredCommand);
}

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "IDirectKeyListener.idl";
import "ShortcutActionDispatch.idl";
import "HighlightedTextControl.idl";
namespace TerminalApp
{
[default_interface] runtimeclass FilteredCommand : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
FilteredCommand();
FilteredCommand(Microsoft.Terminal.Settings.Model.Command command);
Microsoft.Terminal.Settings.Model.Command Command { get; };
String Filter;
HighlightedText HighlightedName { get; };
Int32 Weight;
void UpdateFilter(String filter);
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "HighlightedText.h"
#include "HighlightedTextSegment.g.cpp"
#include "HighlightedText.g.cpp"
using namespace winrt;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::System;
using namespace winrt::Windows::UI::Text;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::TerminalApp::implementation
{
HighlightedTextSegment::HighlightedTextSegment(winrt::hstring const& textSegment, bool isHighlighted) :
_TextSegment(textSegment),
_IsHighlighted(isHighlighted)
{
}
HighlightedText::HighlightedText(Windows::Foundation::Collections::IObservableVector<winrt::TerminalApp::HighlightedTextSegment> const& segments) :
_Segments(segments)
{
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "winrt/Microsoft.UI.Xaml.Controls.h"
#include "HighlightedTextSegment.g.h"
#include "HighlightedText.g.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
namespace winrt::TerminalApp::implementation
{
struct HighlightedTextSegment : HighlightedTextSegmentT<HighlightedTextSegment>
{
HighlightedTextSegment() = default;
HighlightedTextSegment(winrt::hstring const& text, bool isHighlighted);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, TextSegment, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(bool, IsHighlighted, _PropertyChangedHandlers);
};
struct HighlightedText : HighlightedTextT<HighlightedText>
{
HighlightedText() = default;
HighlightedText(Windows::Foundation::Collections::IObservableVector<winrt::TerminalApp::HighlightedTextSegment> const& segments);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(Windows::Foundation::Collections::IObservableVector<winrt::TerminalApp::HighlightedTextSegment>, Segments, _PropertyChangedHandlers);
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(HighlightedTextSegment);
BASIC_FACTORY(HighlightedText);
}

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace TerminalApp
{
[default_interface] runtimeclass HighlightedTextSegment : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
HighlightedTextSegment();
HighlightedTextSegment(String text, Boolean isMatched);
String TextSegment { get; };
Boolean IsHighlighted { get; };
}
[default_interface] runtimeclass HighlightedText : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
HighlightedText();
HighlightedText(Windows.Foundation.Collections.IObservableVector<HighlightedTextSegment> segments);
Windows.Foundation.Collections.IObservableVector<HighlightedTextSegment> Segments;
}
}

View File

@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "winrt/Windows.UI.Xaml.Interop.h"
#include "HighlightedTextControl.h"
#include "HighlightedTextControl.g.cpp"
using namespace winrt;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::System;
using namespace winrt::Windows::UI::Text;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::TerminalApp::implementation
{
// Our control exposes a "Text" property to be used with Data Binding
// To allow this we need to register a Dependency Property Identifier to be used by the property system
// (https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/custom-dependency-properties)
DependencyProperty HighlightedTextControl::_textProperty = DependencyProperty::Register(
L"Text",
xaml_typename<winrt::TerminalApp::HighlightedText>(),
xaml_typename<winrt::TerminalApp::HighlightedTextControl>(),
PropertyMetadata(nullptr, HighlightedTextControl::_onTextChanged));
HighlightedTextControl::HighlightedTextControl()
{
InitializeComponent();
}
// Method Description:
// - Returns the Identifier of the "Text" dependency property
DependencyProperty HighlightedTextControl::TextProperty()
{
return _textProperty;
}
// Method Description:
// - Returns the TextBlock view used to render the highlighted text
// Can be used when the Text property change is triggered by the event system to update the view
// We need to expose it rather than simply bind a data source because we update the runs in code-behind
Controls::TextBlock HighlightedTextControl::TextView()
{
return _textView();
}
winrt::TerminalApp::HighlightedText HighlightedTextControl::Text()
{
return winrt::unbox_value<winrt::TerminalApp::HighlightedText>(GetValue(_textProperty));
}
void HighlightedTextControl::Text(winrt::TerminalApp::HighlightedText const& value)
{
SetValue(_textProperty, winrt::box_value(value));
}
// Method Description:
// - This callback is triggered when the Text property is changed. Responsible for updating the view
// Arguments:
// - o - dependency object that was modified, expected to be an instance of this control
// - e - event arguments of the property changed event fired by the event system upon Text property change.
// The new value is expected to be an instance of HighlightedText
void HighlightedTextControl::_onTextChanged(DependencyObject const& o, DependencyPropertyChangedEventArgs const& e)
{
const auto control = o.try_as<winrt::TerminalApp::HighlightedTextControl>();
const auto highlightedText = e.NewValue().try_as<winrt::TerminalApp::HighlightedText>();
if (control && highlightedText)
{
// Replace all the runs on the TextBlock
// Use IsHighlighted to decide if the run should be highlighted.
// To do - export the highlighting style into XAML
const auto inlinesCollection = control.TextView().Inlines();
inlinesCollection.Clear();
for (const auto& match : highlightedText.Segments())
{
const auto matchText = match.TextSegment();
const auto fontWeight = match.IsHighlighted() ? FontWeights::Bold() : FontWeights::Normal();
Documents::Run run;
run.Text(matchText);
run.FontWeight(fontWeight);
inlinesCollection.Append(run);
}
}
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "winrt/Microsoft.UI.Xaml.Controls.h"
#include "HighlightedTextControl.g.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
namespace winrt::TerminalApp::implementation
{
struct HighlightedTextControl : HighlightedTextControlT<HighlightedTextControl>
{
HighlightedTextControl();
static Windows::UI::Xaml::DependencyProperty TextProperty();
winrt::TerminalApp::HighlightedText Text();
void Text(winrt::TerminalApp::HighlightedText const& value);
Windows::UI::Xaml::Controls::TextBlock TextView();
private:
static Windows::UI::Xaml::DependencyProperty _textProperty;
static void _onTextChanged(Windows::UI::Xaml::DependencyObject const& o, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e);
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(HighlightedTextControl);
}

View File

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "HighlightedText.idl";
namespace TerminalApp
{
[default_interface] runtimeclass HighlightedTextControl : Windows.UI.Xaml.Controls.Control
{
HighlightedTextControl();
Windows.UI.Xaml.DependencyProperty TextProperty { get; };
HighlightedText Text;
Windows.UI.Xaml.Controls.TextBlock TextView { get; };
}
}

View File

@ -0,0 +1,13 @@
<UserControl
x:Class="TerminalApp.HighlightedTextControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TerminalApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="Transparent">
<TextBlock
x:Name="_textView"/>
</UserControl>

View File

@ -21,8 +21,8 @@
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
<Import Project="..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.0.0\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.props" Condition="Exists('..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.0.0\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.props')" />
<ItemDefinitionGroup>
<ClCompile>
<!-- For CLI11: It uses dynamic_cast to cast types around, which depends
on being compiled with RTTI (/GR). -->
@ -54,6 +54,9 @@
<Page Include="TabRowControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="HighlightedTextControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="ColorPickupFlyout.xaml">
<SubType>Designer</SubType>
</Page>
@ -86,12 +89,17 @@
<ClInclude Include="TabRowControl.h">
<DependentUpon>TabRowControl.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="HighlightedTextControl.h">
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="HighlightedText.h" />
<ClInclude Include="ColorPickupFlyout.h">
<DependentUpon>ColorPickupFlyout.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="CommandPalette.h">
<DependentUpon>CommandPalette.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="FilteredCommand.h" />
<ClInclude Include="EmptyStringVisibilityConverter.h">
<DependentUpon>EmptyStringVisibilityConverter.idl</DependentUpon>
</ClInclude>
@ -146,12 +154,17 @@
<ClCompile Include="TabRowControl.cpp">
<DependentUpon>TabRowControl.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="HighlightedTextControl.cpp">
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="HighlightedText.cpp" />
<ClCompile Include="ColorPickupFlyout.cpp">
<DependentUpon>ColorPickupFlyout.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="CommandPalette.cpp">
<DependentUpon>CommandPalette.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="FilteredCommand.cpp" />
<ClCompile Include="EmptyStringVisibilityConverter.cpp">
<DependentUpon>EmptyStringVisibilityConverter.idl</DependentUpon>
</ClCompile>
@ -217,6 +230,11 @@
<DependentUpon>TabRowControl.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="HighlightedTextControl.idl">
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="HighlightedText.idl" />
<Midl Include="ColorPickupFlyout.idl">
<DependentUpon>ColorPickupFlyout.xaml</DependentUpon>
<SubType>Code</SubType>
@ -225,6 +243,7 @@
<DependentUpon>CommandPalette.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="FilteredCommand.idl" />
<Midl Include="EmptyStringVisibilityConverter.idl" />
<Midl Include="HasNestedCommandsVisibilityConverter.idl" />
<Midl Include="IconPathConverter.idl" />

View File

@ -31,6 +31,12 @@
<ClCompile Include="Tab.cpp">
<Filter>tab</Filter>
</ClCompile>
<ClCompile Include="FilteredCommand.cpp">
<Filter>commandPalette</Filter>
</ClCompile>
<ClCompile Include="HighlightedText.cpp">
<Filter>highlightedText</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Utils.h" />
@ -52,10 +58,16 @@
<ClInclude Include="TerminalSettings.h">
<Filter>settings</Filter>
</ClInclude>
<ClInclude Include="Jumplist.h" />
<ClInclude Include="Jumplist.h" />
<ClInclude Include="Tab.h">
<Filter>tab</Filter>
</ClInclude>
<ClInclude Include="FilteredCommand.h">
<Filter>commandPalette</Filter>
</ClInclude>
<ClInclude Include="HighlightedText.h">
<Filter>highlightedText</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Midl Include="AppLogic.idl">
@ -77,6 +89,12 @@
<Midl Include="TerminalTab.idl">
<Filter>tab</Filter>
</Midl>
<Midl Include="FilteredCommand.idl">
<Filter>commandPalette</Filter>
</Midl>
<Midl Include="HighlightedText.idl">
<Filter>highlightedText</Filter>
</Midl>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@ -100,6 +118,9 @@
<Page Include="CommandPalette.xaml">
<Filter>commandPalette</Filter>
</Page>
<Page Include="HighlightedTextControl.xaml">
<Filter>highlightedText</Filter>
</Page>
</ItemGroup>
<ItemGroup>
<Filter Include="app">
@ -120,10 +141,13 @@
<Filter Include="commandPalette">
<UniqueIdentifier>{2ad498e1-d8ea-4381-9464-a74c141bd7dd}</UniqueIdentifier>
</Filter>
<Filter Include="highlightedText">
<UniqueIdentifier>{e490f626-547d-4b5b-b22d-c6d33c9e3210}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Filter>app</Filter>
</ApplicationDefinition>
</ItemGroup>
</Project>
</Project>