Add support for iterable, nested commands (#6856)

## Summary of the Pull Request

This PR adds support for both _nested_ and _iterable_ commands in the Command palette.
![nested-commands-000](https://user-images.githubusercontent.com/18356694/87072916-2d991c00-c1e2-11ea-8917-a70e8b8b9803.gif)

* **Nested commands**: These are commands that include additional sub-commands. When the user selects on of these, the palette will update to only show the nested commands.
* **Iterable commands**: These are commands what allow the user to define only a single command, which is repeated once for every profile. (in the future, also repeated for color schemes, themes, etc.)

The above gif uses the following json:

```json
        {
            "name": "Split Pane...",
            "commands": [
                {
                    "iterateOn": "profiles",
                    "name": "Split with ${profile.name}...",
                    "commands": [
                        { "command": { "action": "splitPane", "profile": "${profile.name}", "split": "automatic" } },
                        { "command": { "action": "splitPane", "profile": "${profile.name}", "split": "vertical" } },
                        { "command": { "action": "splitPane", "profile": "${profile.name}", "split": "horizontal" } }
                    ]
                }
            ]
        },
```

## References

## PR Checklist
* [x] Closes #3994
* [x] I work here
* [x] Tests added/passed
* [ ] Requires documentation to be updated - Sure does, but we'll finish polishing this first.

## Detailed Description of the Pull Request / Additional comments

We've now gotta keep the original json for a command around, so that once we know what all the profiles will be, we can expand the commands that need it. 

We've also got to parse commands recursively, because they might have any number of child commands.

These together made the command parsing a _lot_ more complicated, but it feels good so far.

## Validation Steps Performed
* wrote a bunch of tests
* Played with it a bunch
This commit is contained in:
Mike Griese 2020-08-13 16:22:46 -05:00 committed by GitHub
parent d9ffca6614
commit dcc2799457
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 2139 additions and 129 deletions

View file

@ -11,6 +11,7 @@ using namespace Microsoft::Console;
using namespace TerminalApp;
using namespace winrt::TerminalApp;
using namespace winrt::Microsoft::Terminal::TerminalControl;
using namespace winrt::Windows::Foundation::Collections;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace WEX::Common;
@ -61,25 +62,25 @@ namespace TerminalAppLocalTests
const auto commands1Json = VerifyParseSucceeded(commands1String);
const auto commands2Json = VerifyParseSucceeded(commands2String);
std::unordered_map<winrt::hstring, Command> commands;
VERIFY_ARE_EQUAL(0u, commands.size());
IMap<winrt::hstring, winrt::TerminalApp::Command> commands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
VERIFY_ARE_EQUAL(0u, commands.Size());
{
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
}
VERIFY_ARE_EQUAL(1u, commands.size());
VERIFY_ARE_EQUAL(1u, commands.Size());
{
auto warnings = implementation::Command::LayerJson(commands, commands1Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
}
VERIFY_ARE_EQUAL(2u, commands.size());
VERIFY_ARE_EQUAL(2u, commands.Size());
{
auto warnings = implementation::Command::LayerJson(commands, commands2Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
}
VERIFY_ARE_EQUAL(4u, commands.size());
VERIFY_ARE_EQUAL(4u, commands.Size());
}
void CommandTests::LayerCommand()
@ -95,13 +96,13 @@ namespace TerminalAppLocalTests
const auto commands2Json = VerifyParseSucceeded(commands2String);
const auto commands3Json = VerifyParseSucceeded(commands3String);
std::unordered_map<winrt::hstring, Command> commands;
VERIFY_ARE_EQUAL(0u, commands.size());
IMap<winrt::hstring, winrt::TerminalApp::Command> commands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
VERIFY_ARE_EQUAL(0u, commands.Size());
{
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(1u, commands.size());
auto command = commands.at(L"action0");
VERIFY_ARE_EQUAL(1u, commands.Size());
auto command = commands.Lookup(L"action0");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::CopyText, command.Action().Action());
@ -111,8 +112,8 @@ namespace TerminalAppLocalTests
{
auto warnings = implementation::Command::LayerJson(commands, commands1Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(1u, commands.size());
auto command = commands.at(L"action0");
VERIFY_ARE_EQUAL(1u, commands.Size());
auto command = commands.Lookup(L"action0");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::PasteText, command.Action().Action());
@ -121,8 +122,8 @@ namespace TerminalAppLocalTests
{
auto warnings = implementation::Command::LayerJson(commands, commands2Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(1u, commands.size());
auto command = commands.at(L"action0");
VERIFY_ARE_EQUAL(1u, commands.Size());
auto command = commands.Lookup(L"action0");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, command.Action().Action());
@ -133,7 +134,7 @@ namespace TerminalAppLocalTests
// This last command should "unbind" the action.
auto warnings = implementation::Command::LayerJson(commands, commands3Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(0u, commands.size());
VERIFY_ARE_EQUAL(0u, commands.Size());
}
}
@ -153,14 +154,14 @@ namespace TerminalAppLocalTests
const auto commands0Json = VerifyParseSucceeded(commands0String);
std::unordered_map<winrt::hstring, Command> commands;
VERIFY_ARE_EQUAL(0u, commands.size());
IMap<winrt::hstring, winrt::TerminalApp::Command> commands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
VERIFY_ARE_EQUAL(0u, commands.Size());
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(5u, commands.size());
VERIFY_ARE_EQUAL(5u, commands.Size());
{
auto command = commands.at(L"command0");
auto command = commands.Lookup(L"command0");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
@ -170,7 +171,7 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle());
}
{
auto command = commands.at(L"command1");
auto command = commands.Lookup(L"command1");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
@ -180,7 +181,7 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle());
}
{
auto command = commands.at(L"command2");
auto command = commands.Lookup(L"command2");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
@ -190,7 +191,7 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle());
}
{
auto command = commands.at(L"command4");
auto command = commands.Lookup(L"command4");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
@ -200,7 +201,7 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle());
}
{
auto command = commands.at(L"command5");
auto command = commands.Lookup(L"command5");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
@ -217,17 +218,17 @@ namespace TerminalAppLocalTests
const std::string commands0String{ R"([ { "name": { "key": "DuplicateTabCommandKey"}, "command": "copy" } ])" };
const auto commands0Json = VerifyParseSucceeded(commands0String);
std::unordered_map<winrt::hstring, Command> commands;
VERIFY_ARE_EQUAL(0u, commands.size());
IMap<winrt::hstring, winrt::TerminalApp::Command> commands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
VERIFY_ARE_EQUAL(0u, commands.Size());
{
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(1u, commands.size());
VERIFY_ARE_EQUAL(1u, commands.Size());
// NOTE: We're relying on DuplicateTabCommandKey being defined as
// "Duplicate Tab" here. If that string changes in our resources,
// this test will break.
auto command = commands.at(L"Duplicate tab");
auto command = commands.Lookup(L"Duplicate tab");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::CopyText, command.Action().Action());
@ -257,18 +258,18 @@ namespace TerminalAppLocalTests
const auto commands0Json = VerifyParseSucceeded(commands0String);
std::unordered_map<winrt::hstring, Command> commands;
VERIFY_ARE_EQUAL(0u, commands.size());
IMap<winrt::hstring, winrt::TerminalApp::Command> commands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
VERIFY_ARE_EQUAL(0u, commands.Size());
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
// There are only 3 commands here: all of the `"none"`, `"auto"`,
// `"foo"`, `null`, and <no args> bindings all generate the same action,
// which will generate just a single name for all of them.
VERIFY_ARE_EQUAL(3u, commands.size());
VERIFY_ARE_EQUAL(3u, commands.Size());
{
auto command = commands.at(L"Split pane");
auto command = commands.Lookup(L"Split pane");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
@ -278,7 +279,7 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle());
}
{
auto command = commands.at(L"Split pane, direction: vertical");
auto command = commands.Lookup(L"Split pane, split: vertical");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
@ -288,7 +289,7 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle());
}
{
auto command = commands.at(L"Split pane, direction: horizontal");
auto command = commands.Lookup(L"Split pane, split: horizontal");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
@ -307,14 +308,14 @@ namespace TerminalAppLocalTests
const auto commands0Json = VerifyParseSucceeded(commands0String);
std::unordered_map<winrt::hstring, Command> commands;
VERIFY_ARE_EQUAL(0u, commands.size());
IMap<winrt::hstring, winrt::TerminalApp::Command> commands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
VERIFY_ARE_EQUAL(0u, commands.Size());
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(1u, commands.size());
VERIFY_ARE_EQUAL(1u, commands.Size());
{
auto command = commands.at(L"Split pane");
auto command = commands.Lookup(L"Split pane");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.Action());
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());

File diff suppressed because it is too large Load diff

View file

@ -53,6 +53,11 @@ namespace winrt::TerminalApp::implementation
// - The bound keychord, if this ActionAndArgs is bound to a key, otherwise nullptr.
KeyChord AppKeyBindings::GetKeyBindingForActionWithArgs(TerminalApp::ActionAndArgs const& actionAndArgs)
{
if (actionAndArgs == nullptr)
{
return { nullptr };
}
for (auto& kv : _keyShortcuts)
{
const auto action = kv.second.Action();

View file

@ -38,7 +38,8 @@ static const std::array<std::wstring_view, static_cast<uint32_t>(SettingsLoadWar
USES_RESOURCE(L"AtLeastOneKeybindingWarning"),
USES_RESOURCE(L"TooManyKeysForChord"),
USES_RESOURCE(L"MissingRequiredParameter"),
USES_RESOURCE(L"LegacyGlobalsProperty")
USES_RESOURCE(L"LegacyGlobalsProperty"),
USES_RESOURCE(L"FailedToParseCommandJson")
};
static const std::array<std::wstring_view, static_cast<uint32_t>(SettingsLoadErrors::ERRORS_SIZE)> settingsLoadErrorsLabels {
USES_RESOURCE(L"NoProfilesText"),

View file

@ -694,14 +694,11 @@ void CascadiaSettings::_ValidateNoGlobalsKey()
// - The new settings string.
std::string CascadiaSettings::_ApplyFirstRunChangesToSettingsTemplate(std::string_view settingsTemplate) const
{
// We're using replace_needle_in_haystack_inplace here, because it's more
// efficient to iteratively modify a single string in-place than it is to
// keep copying over the contents and modifying a copy (which
// replace_needle_in_haystack would do).
std::string finalSettings{ settingsTemplate };
auto replace{ [](std::string& haystack, std::string_view needle, std::string_view replacement) {
auto pos{ std::string::npos };
while ((pos = haystack.rfind(needle, pos)) != std::string::npos)
{
haystack.replace(pos, needle.size(), replacement);
}
} };
std::wstring defaultProfileGuid{ DEFAULT_WINDOWS_POWERSHELL_GUID };
if (const auto psCoreProfileGuid{ _GetProfileGuidByName(PowershellCoreProfileGenerator::GetPreferredPowershellProfileName()) })
@ -709,14 +706,22 @@ std::string CascadiaSettings::_ApplyFirstRunChangesToSettingsTemplate(std::strin
defaultProfileGuid = Utils::GuidToString(*psCoreProfileGuid);
}
replace(finalSettings, "%DEFAULT_PROFILE%", til::u16u8(defaultProfileGuid));
til::replace_needle_in_haystack_inplace(finalSettings,
"%DEFAULT_PROFILE%",
til::u16u8(defaultProfileGuid));
if (const auto appLogic{ winrt::TerminalApp::implementation::AppLogic::Current() })
{
replace(finalSettings, "%VERSION%", til::u16u8(appLogic->ApplicationVersion()));
replace(finalSettings, "%PRODUCT%", til::u16u8(appLogic->ApplicationDisplayName()));
til::replace_needle_in_haystack_inplace(finalSettings,
"%VERSION%",
til::u16u8(appLogic->ApplicationVersion()));
til::replace_needle_in_haystack_inplace(finalSettings,
"%PRODUCT%",
til::u16u8(appLogic->ApplicationDisplayName()));
}
replace(finalSettings, "%COMMAND_PROMPT_LOCALIZED_NAME%", RS_A(L"CommandPromptDisplayName"));
til::replace_needle_in_haystack_inplace(finalSettings,
"%COMMAND_PROMPT_LOCALIZED_NAME%",
RS_A(L"CommandPromptDisplayName"));
return finalSettings;
}

View file

@ -19,7 +19,6 @@
// "Generated Files" directory.
using namespace ::TerminalApp;
using namespace winrt::Microsoft::Terminal::TerminalControl;
using namespace winrt::TerminalApp;
using namespace ::Microsoft::Console;

View file

@ -11,15 +11,36 @@
#include <LibraryResources.h>
using namespace winrt::TerminalApp;
using namespace winrt::Windows::Foundation;
using namespace ::TerminalApp;
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view IconPathKey{ "iconPath" };
static constexpr std::string_view ActionKey{ "command" };
static constexpr std::string_view ArgsKey{ "args" };
static constexpr std::string_view IterateOnKey{ "iterateOn" };
static constexpr std::string_view CommandsKey{ "commands" };
static constexpr std::string_view IterateOnProfilesValue{ "profiles" };
static constexpr std::string_view ProfileName{ "${profile.name}" };
namespace winrt::TerminalApp::implementation
{
Command::Command()
{
_setAction(nullptr);
}
Collections::IMapView<winrt::hstring, TerminalApp::Command> Command::NestedCommands()
{
return _subcommands ? _subcommands.GetView() : nullptr;
}
bool Command::HasNestedCommands()
{
return _subcommands ? _subcommands.Size() > 0 : false;
}
// Function Description:
// - attempt to get the name of this command from the provided json object.
// * If the "name" property is a string, return that value.
@ -99,35 +120,83 @@ namespace winrt::TerminalApp::implementation
{
auto result = winrt::make_self<Command>();
bool nested = false;
if (const auto iterateOnJson{ json[JsonKey(IterateOnKey)] })
{
auto s = iterateOnJson.asString();
if (s == IterateOnProfilesValue)
{
result->_IterateOn = ExpandCommandType::Profiles;
}
}
// For iterable commands, we'll make another pass at parsing them once
// the json is patched. So ignore parsing sub-commands for now. Commands
// will only be marked iterable on the first pass.
if (const auto nestedCommandsJson{ json[JsonKey(CommandsKey)] })
{
// Initialize our list of subcommands.
result->_subcommands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
auto nestedWarnings = Command::LayerJson(result->_subcommands, nestedCommandsJson);
// It's possible that the nested commands have some warnings
warnings.insert(warnings.end(), nestedWarnings.begin(), nestedWarnings.end());
nested = true;
}
else if (json.isMember(JsonKey(CommandsKey)))
{
// { "name": "foo", "commands": null } will land in this case, which
// should also be used for unbinding.
return nullptr;
}
// TODO GH#6644: iconPath not implemented quite yet. Can't seem to get
// the binding quite right. Additionally, do we want it to be an image,
// or a FontIcon? I've had difficulty binding either/or.
if (const auto actionJson{ json[JsonKey(ActionKey)] })
// If we're a nested command, we can ignore the current action.
if (!nested)
{
auto actionAndArgs = ActionAndArgs::FromJson(actionJson, warnings);
if (actionAndArgs)
if (const auto actionJson{ json[JsonKey(ActionKey)] })
{
result->_setAction(*actionAndArgs);
auto actionAndArgs = ActionAndArgs::FromJson(actionJson, warnings);
if (actionAndArgs)
{
result->_setAction(*actionAndArgs);
}
else
{
// Something like
// { name: "foo", action: "unbound" }
// will _remove_ the "foo" command, by returning null here.
return nullptr;
}
// If an iterable command doesn't have a name set, we'll still just
// try and generate a fake name for the command give the string we
// currently have. It'll probably generate something like "New tab,
// profile: ${profile.name}". This string will only be temporarily
// used internally, so there's no problem.
result->_setName(_nameFromJsonOrAction(json, actionAndArgs));
}
else
{
// Something like
// { name: "foo", action: "unbound" }
// will _remove_ the "foo" command, by returning null here.
// { name: "foo", action: null } will land in this case, which
// should also be used for unbinding.
return nullptr;
}
result->_setName(_nameFromJsonOrAction(json, actionAndArgs));
}
else
{
// { name: "foo", action: null } will land in this case, which
// should also be used for unbinding.
return nullptr;
result->_setName(_nameFromJson(json));
}
// Stash the original json value in this object. If the command is
// iterable, we'll need to re-parse it later, once we know what all the
// values we can iterate on are.
result->_originalJson = json;
if (result->_Name.empty())
{
return nullptr;
@ -147,7 +216,7 @@ namespace winrt::TerminalApp::implementation
// - json: A Json::Value containing an array of serialized commands
// Return Value:
// - A vector containing any warnings detected while parsing
std::vector<::TerminalApp::SettingsLoadWarnings> Command::LayerJson(std::unordered_map<winrt::hstring, winrt::TerminalApp::Command>& commands,
std::vector<::TerminalApp::SettingsLoadWarnings> Command::LayerJson(Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command>& commands,
const Json::Value& json)
{
std::vector<::TerminalApp::SettingsLoadWarnings> warnings;
@ -162,7 +231,7 @@ namespace winrt::TerminalApp::implementation
if (result)
{
// Override commands with the same name
commands.insert_or_assign(result->Name(), *result);
commands.Insert(result->Name(), *result);
}
else
{
@ -172,7 +241,7 @@ namespace winrt::TerminalApp::implementation
const auto name = _nameFromJson(value);
if (!name.empty())
{
commands.erase(name);
commands.Remove(name);
}
}
}
@ -181,4 +250,160 @@ namespace winrt::TerminalApp::implementation
}
return warnings;
}
// Function Description:
// - Helper to escape a string as a json string. This function will also
// trim off the leading and trailing double-quotes, so the output string
// can be inserted directly into another json blob.
// Arguments:
// - input: the string to JSON escape.
// Return Value:
// - the input string escaped properly to be inserted into another json blob.
std::string _escapeForJson(const std::string& input)
{
Json::Value inJson{ input };
Json::StreamWriterBuilder builder;
builder.settings_["indentation"] = "";
std::string out{ Json::writeString(builder, inJson) };
if (out.size() >= 2)
{
// trim off the leading/trailing '"'s
auto ss{ out.substr(1, out.size() - 2) };
return ss;
}
return out;
}
// Method Description:
// - Iterate over all the provided commands, and recursively expand any
// commands with `iterateOn` set. If we successfully generated expanded
// commands for them, then we'll remove the original command, and add all
// the newly generated commands.
// - For more specific implementation details, see _expandCommand.
// Arguments:
// - commands: a map of commands to expand. Newly created commands will be
// inserted into the map to replace the expandable commands.
// - profiles: A list of all the profiles that this command should be expanded on.
// - warnings: If there were any warnings during parsing, they'll be
// appended to this vector.
// Return Value:
// - <none>
void Command::ExpandCommands(Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command>& commands,
gsl::span<const ::TerminalApp::Profile> profiles,
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings)
{
std::vector<winrt::hstring> commandsToRemove;
std::vector<winrt::TerminalApp::Command> commandsToAdd;
// First, collect up all the commands that need replacing.
for (const auto& nameAndCmd : commands)
{
auto cmd{ get_self<implementation::Command>(nameAndCmd.Value()) };
auto newCommands = _expandCommand(cmd, profiles, warnings);
if (newCommands.size() > 0)
{
commandsToRemove.push_back(nameAndCmd.Key());
commandsToAdd.insert(commandsToAdd.end(), newCommands.begin(), newCommands.end());
}
}
// Second, remove all the commands that need to be removed.
for (auto& name : commandsToRemove)
{
commands.Remove(name);
}
// Finally, add all the new commands.
for (auto& cmd : commandsToAdd)
{
commands.Insert(cmd.Name(), cmd);
}
}
// Function Description:
// - Attempts to expand the given command into many commands, if the command
// has `"iterateOn": "profiles"` set.
// - If it doesn't, this function will do
// nothing and return an empty vector.
// - If it does, we're going to attempt to build a new set of commands using
// the given command as a prototype. We'll attempt to create a new command
// for each and every profile, to replace the original command.
// * For the new commands, we'll replace any instance of "${profile.name}"
// in the original json used to create this action with the name of the
// given profile.
// - If we encounter any errors while re-parsing the json with the replaced
// name, we'll just return immediately.
// - At the end, we'll return all the new commands we've build for the given command.
// Arguments:
// - expandable: the Command to potentially turn into more commands
// - profiles: A list of all the profiles that this command should be expanded on.
// - warnings: If there were any warnings during parsing, they'll be
// appended to this vector.
// Return Value:
// - and empty vector if the command wasn't expandable, otherwise a list of
// the newly-created commands.
std::vector<winrt::TerminalApp::Command> Command::_expandCommand(Command* const expandable,
gsl::span<const ::TerminalApp::Profile> profiles,
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings)
{
std::vector<winrt::TerminalApp::Command> newCommands;
if (expandable->HasNestedCommands())
{
ExpandCommands(expandable->_subcommands, profiles, warnings);
}
if (expandable->_IterateOn == ExpandCommandType::None)
{
return newCommands;
}
std::string errs; // This string will receive any error text from failing to parse.
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
// First, get a string for the original Json::Value
auto oldJsonString = expandable->_originalJson.toStyledString();
if (expandable->_IterateOn == ExpandCommandType::Profiles)
{
for (const auto& p : profiles)
{
// For each profile, create a new command. This command will have:
// * the icon path and keychord text of the original command
// * the Name will have any instances of "${profile.name}"
// replaced with the profile's name
// * for the action, we'll take the original json, replace any
// instances of "${profile.name}" with the profile's name,
// then re-attempt to parse the action and args.
// Replace all the keywords in the original json, and try and parse that
// - Escape the profile name for JSON appropriately
auto escapedProfileName = _escapeForJson(til::u16u8(p.GetName()));
auto newJsonString = til::replace_needle_in_haystack(oldJsonString,
ProfileName,
escapedProfileName);
// - Now, re-parse the modified value.
Json::Value newJsonValue;
const auto actualDataStart = newJsonString.data();
const auto actualDataEnd = newJsonString.data() + newJsonString.size();
if (!reader->parse(actualDataStart, actualDataEnd, &newJsonValue, &errs))
{
warnings.push_back(::TerminalApp::SettingsLoadWarnings::FailedToParseCommandJson);
// If we encounter a re-parsing error, just stop processing the rest of the commands.
break;
}
// Pass the new json back though FromJson, to get the new expanded value.
if (auto newCmd{ Command::FromJson(newJsonValue, warnings) })
{
newCommands.push_back(*newCmd);
}
}
}
return newCommands;
}
}

View file

@ -1,4 +1,4 @@
/*++
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
@ -20,17 +20,39 @@ Author(s):
#include "Command.g.h"
#include "TerminalWarnings.h"
#include "Profile.h"
#include "..\inc\cppwinrt_utils.h"
// fwdecl unittest classes
namespace TerminalAppLocalTests
{
class SettingsTests;
class CommandTests;
};
namespace winrt::TerminalApp::implementation
{
enum class ExpandCommandType : uint32_t
{
None = 0,
Profiles
};
struct Command : CommandT<Command>
{
Command() = default;
Command();
static winrt::com_ptr<Command> FromJson(const Json::Value& json, std::vector<::TerminalApp::SettingsLoadWarnings>& warnings);
static std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(std::unordered_map<winrt::hstring, winrt::TerminalApp::Command>& commands,
static winrt::com_ptr<Command> FromJson(const Json::Value& json,
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings);
static void ExpandCommands(Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command>& commands,
gsl::span<const ::TerminalApp::Profile> profiles,
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings);
static std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command>& commands,
const Json::Value& json);
bool HasNestedCommands();
Windows::Foundation::Collections::IMapView<winrt::hstring, TerminalApp::Command> NestedCommands();
winrt::Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker propertyChangedRevoker;
@ -39,6 +61,18 @@ namespace winrt::TerminalApp::implementation
OBSERVABLE_GETSET_PROPERTY(winrt::TerminalApp::ActionAndArgs, Action, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, KeyChordText, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(winrt::Windows::UI::Xaml::Controls::IconSource, IconSource, _PropertyChangedHandlers, nullptr);
GETSET_PROPERTY(ExpandCommandType, IterateOn, ExpandCommandType::None);
private:
Json::Value _originalJson;
Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command> _subcommands{ nullptr };
static std::vector<winrt::TerminalApp::Command> _expandCommand(Command* const expandable,
gsl::span<const ::TerminalApp::Profile> profiles,
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings);
friend class TerminalAppLocalTests::SettingsTests;
friend class TerminalAppLocalTests::CommandTests;
};
}

View file

@ -14,5 +14,8 @@ namespace TerminalApp
String KeyChordText;
Windows.UI.Xaml.Controls.IconSource IconSource;
Boolean HasNestedCommands();
Windows.Foundation.Collections.IMapView<String, Command> NestedCommands { get; };
}
}

View file

@ -28,6 +28,8 @@ namespace winrt::TerminalApp::implementation
InitializeComponent();
_filteredActions = winrt::single_threaded_observable_vector<winrt::TerminalApp::Command>();
_nestedActionStack = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
_currentNestedCommands = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
_allCommands = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
_allTabActions = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
@ -67,6 +69,7 @@ namespace winrt::TerminalApp::implementation
else
{
_searchBox().Focus(FocusState::Programmatic);
_updateFilteredActions();
_filteredActionsView().SelectedIndex(0);
}
@ -271,6 +274,29 @@ namespace winrt::TerminalApp::implementation
_dispatchCommand(e.ClickedItem().try_as<TerminalApp::Command>());
}
// Method Description:
// - This is called when the user selects a command with subcommands. It
// will update our UI to now display the list of subcommands instead, and
// clear the search text so the user can search from the new list of
// commands.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CommandPalette::_updateUIForStackChange()
{
if (_searchBox().Text().empty())
{
// Manually call _filterTextChanged, because setting the text to the
// empty string won't update it for us (as it won't actually change value.)
_filterTextChanged(nullptr, nullptr);
}
// Changing the value of the search box will trigger _filterTextChanged,
// which will cause us to refresh the list of filterable commands.
_searchBox().Text(L"");
}
// Method Description:
// - Retrieve the list of commands that we should currently be filtering.
// * If the user has command with subcommands, this will return that command's subcommands.
@ -285,6 +311,11 @@ namespace winrt::TerminalApp::implementation
switch (_currentMode)
{
case CommandPaletteMode::ActionMode:
if (_nestedActionStack.Size() > 0)
{
return _currentNestedCommands;
}
return _allCommands;
case CommandPaletteMode::TabSwitcherMode:
return _allTabActions;
@ -306,20 +337,44 @@ namespace winrt::TerminalApp::implementation
{
if (command)
{
// Close before we dispatch so that actions that open the command
// palette like the Tab Switcher will be able to have the last laugh.
_close();
if (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);
_currentNestedCommands.Clear();
for (const auto& nameAndCommand : command.NestedCommands())
{
_currentNestedCommands.Append(nameAndCommand.Value());
}
const auto actionAndArgs = command.Action();
_dispatch.DoAction(actionAndArgs);
_updateUIForStackChange();
}
else
{
// First stash the search text length, because _close will clear this.
const auto searchTextLength = _searchBox().Text().size();
TraceLoggingWrite(
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
"CommandPaletteDispatchedAction",
TraceLoggingDescription("Event emitted when the user selects an action in the Command Palette"),
TraceLoggingUInt32(_searchBox().Text().size(), "SearchTextLength", "Number of characters in the search string"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
// An action from the root command list has depth=0
const auto nestedCommandDepth = _nestedActionStack.Size();
// Close before we dispatch so that actions that open the command
// palette like the Tab Switcher will be able to have the last laugh.
_close();
const auto actionAndArgs = command.Action();
_dispatch.DoAction(actionAndArgs);
TraceLoggingWrite(
g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider
"CommandPaletteDispatchedAction",
TraceLoggingDescription("Event emitted when the user selects an action in the Command Palette"),
TraceLoggingUInt32(searchTextLength, "SearchTextLength", "Number of characters in the search string"),
TraceLoggingUInt32(nestedCommandDepth, "NestedCommandDepth", "the depth in the tree of commands for the dispatched action"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
}
}
}
@ -707,6 +762,9 @@ namespace winrt::TerminalApp::implementation
// Clear the text box each time we close the dialog. This is consistent with VsCode.
_searchBox().Text(L"");
_nestedActionStack.Clear();
_currentNestedCommands.Clear();
}
// Method Description:

View file

@ -37,9 +37,11 @@ namespace winrt::TerminalApp::implementation
private:
friend struct CommandPaletteT<CommandPalette>; // for Xaml to bind events
Windows::Foundation::Collections::IObservableVector<TerminalApp::Command> _filteredActions{ nullptr };
Windows::Foundation::Collections::IVector<TerminalApp::Command> _allCommands{ nullptr };
Windows::Foundation::Collections::IVector<TerminalApp::Command> _currentNestedCommands{ nullptr };
Windows::Foundation::Collections::IObservableVector<TerminalApp::Command> _filteredActions{ nullptr };
Windows::Foundation::Collections::IVector<TerminalApp::Command> _nestedActionStack{ nullptr };
winrt::TerminalApp::ShortcutActionDispatch _dispatch;
Windows::Foundation::Collections::IVector<TerminalApp::Command> _commandsToFilter();
@ -53,6 +55,8 @@ namespace winrt::TerminalApp::implementation
void _keyUpHandler(Windows::Foundation::IInspectable const& sender,
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e);
void _updateUIForStackChange();
void _rootPointerPressed(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
void _backdropPointerPressed(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
@ -61,7 +65,9 @@ namespace winrt::TerminalApp::implementation
void _selectNextItem(const bool moveDown);
void _updateFilteredActions();
std::vector<winrt::TerminalApp::Command> _collectFilteredActions();
static int _getWeight(const winrt::hstring& searchText, const winrt::hstring& name);
void _close();

View file

@ -60,6 +60,7 @@ GlobalAppSettings::GlobalAppSettings() :
_WordDelimiters{ DEFAULT_WORD_DELIMITERS },
_DebugFeaturesEnabled{ debugFeaturesDefault }
{
_commands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
}
GlobalAppSettings::~GlobalAppSettings()
@ -230,7 +231,12 @@ std::vector<TerminalApp::SettingsLoadWarnings> GlobalAppSettings::GetKeybindings
return _keybindingsWarnings;
}
const std::unordered_map<winrt::hstring, winrt::TerminalApp::Command>& GlobalAppSettings::GetCommands() const noexcept
const winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command>& GlobalAppSettings::GetCommands() const noexcept
{
return _commands;
}
winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command>& GlobalAppSettings::GetCommands() noexcept
{
return _commands;
}

View file

@ -50,7 +50,8 @@ public:
std::vector<TerminalApp::SettingsLoadWarnings> GetKeybindingsWarnings() const;
const std::unordered_map<winrt::hstring, winrt::TerminalApp::Command>& GetCommands() const noexcept;
const winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command>& GetCommands() const noexcept;
winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command>& GetCommands() noexcept;
// These are implemented manually to handle the string/GUID exchange
// by higher layers in the app.
@ -89,7 +90,7 @@ private:
std::vector<::TerminalApp::SettingsLoadWarnings> _keybindingsWarnings;
std::unordered_map<std::wstring, ColorScheme> _colorSchemes;
std::unordered_map<winrt::hstring, winrt::TerminalApp::Command> _commands;
winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command> _commands;
friend class TerminalAppLocalTests::SettingsTests;
friend class TerminalAppLocalTests::ColorSchemeTests;

View file

@ -222,6 +222,10 @@
<data name="LegacyGlobalsPropertyHrefLabel" xml:space="preserve">
<value>For more info, see this web page.</value>
</data>
<data name="FailedToParseCommandJson" xml:space="preserve">
<value>Failed to expand a command with "iterateOn" set. This command will be ignored.</value>
<comment>{Locked="\"iterateOn\""} </comment>
</data>
<data name="CmdCommandArgDesc" xml:space="preserve">
<value>An optional command, with arguments, to be spawned in the new tab or pane</value>
</data>

View file

@ -51,6 +51,60 @@ namespace winrt::TerminalApp::implementation
InitializeComponent();
}
// Function Description:
// - Recursively check our commands to see if there's a keybinding for
// exactly their action. If there is, label that command with the text
// corresponding to that key chord.
// - Will recurse into nested commands as well.
// Arguments:
// - settings: The settings who's keybindings we should use to look up the key chords from
// - commands: The list of commands to label.
static void _recursiveUpdateCommandKeybindingLabels(std::shared_ptr<::TerminalApp::CascadiaSettings> settings,
Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::TerminalApp::Command> commands)
{
for (const auto& nameAndCmd : commands)
{
const auto& command = nameAndCmd.Value();
// If there's a keybinding that's bound to exactly this command,
// then get the string for that keychord and display it as a
// part of the command in the UI. Each Command's KeyChordText is
// unset by default, so we don't need to worry about clearing it
// if there isn't a key associated with it.
auto keyChord{ settings->GetKeybindings().GetKeyBindingForActionWithArgs(command.Action()) };
if (keyChord)
{
command.KeyChordText(KeyChordSerialization::ToString(keyChord));
}
if (command.HasNestedCommands())
{
_recursiveUpdateCommandKeybindingLabels(settings, command.NestedCommands());
}
}
}
static void _recursiveUpdateCommandIcons(Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::TerminalApp::Command> commands)
{
for (const auto& nameAndCmd : commands)
{
const auto& command = nameAndCmd.Value();
// Set the default IconSource to a BitmapIconSource with a null source
// (instead of just nullptr) because there's a really weird crash when swapping
// data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette).
// Swapping between nullptr IconSources and non-null IconSources causes a crash
// to occur, but swapping between IconSources with a null source and non-null IconSources
// work perfectly fine :shrug:.
winrt::Windows::UI::Xaml::Controls::BitmapIconSource icon;
icon.UriSource(nullptr);
command.IconSource(icon);
if (command.HasNestedCommands())
{
_recursiveUpdateCommandIcons(command.NestedCommands());
}
}
}
winrt::fire_and_forget TerminalPage::SetSettings(std::shared_ptr<::TerminalApp::CascadiaSettings> settings,
bool needRefreshUI)
{
@ -64,36 +118,7 @@ namespace winrt::TerminalApp::implementation
co_await winrt::resume_foreground(Dispatcher());
if (auto page{ weakThis.get() })
{
// Update the command palette when settings reload
auto commandsCollection = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
for (auto& nameAndCommand : _settings->GlobalSettings().GetCommands())
{
auto command = nameAndCommand.second;
// If there's a keybinding that's bound to exactly this command,
// then get the string for that keychord and display it as a
// part of the command in the UI. Each Command's KeyChordText is
// unset by default, so we don't need to worry about clearing it
// if there isn't a key associated with it.
auto keyChord{ _settings->GetKeybindings().GetKeyBindingForActionWithArgs(command.Action()) };
if (keyChord)
{
command.KeyChordText(KeyChordSerialization::ToString(keyChord));
}
// Set the default IconSource to a BitmapIconSource with a null source
// (instead of just nullptr) because there's a really weird crash when swapping
// data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette).
// Swapping between nullptr IconSources and non-null IconSources causes a crash
// to occur, but swapping between IconSources with a null source and non-null IconSources
// work perfectly fine :shrug:.
winrt::Windows::UI::Xaml::Controls::BitmapIconSource icon;
icon.UriSource(nullptr);
command.IconSource(icon);
commandsCollection.Append(command);
}
CommandPalette().SetCommands(commandsCollection);
_UpdateCommandsForPalette();
}
}
@ -1807,10 +1832,12 @@ namespace winrt::TerminalApp::implementation
}
// Method Description:
// - Responds to changes in the TabView's item list by changing the tabview's
// visibility. This method is also invoked when tabs are dragged / dropped as part of tab reordering
// and this method hands that case as well in concert with TabDragStarting and TabDragCompleted handlers
// that are set up in TerminalPage::Create()
// - Responds to changes in the TabView's item list by changing the
// tabview's visibility.
// - This method is also invoked when tabs are dragged / dropped as part of
// tab reordering and this method hands that case as well in concert with
// TabDragStarting and TabDragCompleted handlers that are set up in
// TerminalPage::Create()
// Arguments:
// - sender: the control that originated this event
// - eventArgs: the event's constituent arguments
@ -1828,6 +1855,10 @@ namespace winrt::TerminalApp::implementation
_rearrangeTo = eventArgs.Index();
}
}
else
{
_UpdateCommandsForPalette();
}
_UpdateTabView();
}
@ -2004,6 +2035,54 @@ namespace winrt::TerminalApp::implementation
_alwaysOnTopChangedHandlers(*this, nullptr);
}
// Method Description:
// - Takes a mapping of names->commands and expands them
// Arguments:
// - <none>
// Return Value:
// - <none>
IMap<winrt::hstring, winrt::TerminalApp::Command> TerminalPage::_ExpandCommands(IMapView<winrt::hstring, winrt::TerminalApp::Command> commandsToExpand,
gsl::span<const ::TerminalApp::Profile> profiles)
{
std::vector<::TerminalApp::SettingsLoadWarnings> warnings;
IMap<winrt::hstring, winrt::TerminalApp::Command> copyOfCommands = winrt::single_threaded_map<winrt::hstring, winrt::TerminalApp::Command>();
for (const auto& nameAndCommand : commandsToExpand)
{
copyOfCommands.Insert(nameAndCommand.Key(), nameAndCommand.Value());
}
Command::ExpandCommands(copyOfCommands,
profiles,
warnings);
return copyOfCommands;
}
// Method Description:
// - Repopulates the list of commands in the command palette with the
// current commands in the settings. Also updates the keybinding labels to
// reflect any matching keybindings.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalPage::_UpdateCommandsForPalette()
{
IMap<winrt::hstring, winrt::TerminalApp::Command> copyOfCommands = _ExpandCommands(_settings->GlobalSettings().GetCommands().GetView(),
_settings->GetProfiles());
_recursiveUpdateCommandKeybindingLabels(_settings, copyOfCommands.GetView());
_recursiveUpdateCommandIcons(copyOfCommands.GetView());
// Update the command palette when settings reload
auto commandsCollection = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
for (const auto& nameAndCommand : copyOfCommands)
{
commandsCollection.Append(nameAndCommand.Value());
}
CommandPalette().SetCommands(commandsCollection);
}
// Method Description:
// - Sets the initial actions to process on startup. We'll make a copy of
// this list, and process these actions when we're loaded.

View file

@ -16,6 +16,7 @@
namespace TerminalAppLocalTests
{
class TabTests;
class SettingsTests;
};
namespace winrt::TerminalApp::implementation
@ -134,6 +135,10 @@ namespace winrt::TerminalApp::implementation
void _UpdateTabIcon(Tab& tab);
void _UpdateTabView();
void _UpdateTabWidthMode();
void _UpdateCommandsForPalette();
static winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command> _ExpandCommands(Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::TerminalApp::Command> commandsToExpand,
gsl::span<const ::TerminalApp::Profile> profiles);
void _DuplicateTabViewItem();
void _RemoveTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem);
void _RemoveTabViewItemByIndex(uint32_t tabIndex);
@ -236,6 +241,7 @@ namespace winrt::TerminalApp::implementation
#pragma endregion
friend class TerminalAppLocalTests::TabTests;
friend class TerminalAppLocalTests::SettingsTests;
};
}

View file

@ -30,6 +30,7 @@ namespace TerminalApp
TooManyKeysForChord = 6,
MissingRequiredParameter = 7,
LegacyGlobalsProperty = 8,
FailedToParseCommandJson = 9,
WARNINGS_SIZE // IMPORTANT: This MUST be the last value in this enum. It's an unused placeholder.
};

View file

@ -35,6 +35,7 @@
#include <winrt/Windows.UI.Text.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Controls.Primitives.h>
#include <winrt/Windows.UI.Xaml.Data.h>
#include <winrt/Windows.ui.xaml.media.h>
#include <winrt/Windows.ui.xaml.input.h>
#include <winrt/Windows.UI.Xaml.Hosting.h>

View file

@ -6,9 +6,10 @@
#include "../types/inc/Viewport.hpp"
#include "../types/inc/utils.hpp"
#include "../types/inc/User32Utils.hpp"
#include "resource.h"
#include <winrt/Microsoft.Terminal.TerminalControl.h>
using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Composition;
using namespace winrt::Windows::UI::Xaml;

View file

@ -3,7 +3,6 @@
#include "pch.h"
#include <winrt/Microsoft.Terminal.TerminalControl.h>
#include <winrt/TerminalApp.h>
#include "NonClientIslandWindow.h"

View file

@ -3,7 +3,6 @@
#include "pch.h"
#include "BaseWindow.h"
#include <winrt/Microsoft.Terminal.TerminalControl.h>
#include <winrt/TerminalApp.h>
#include "../../cascadia/inc/cppwinrt_utils.h"

View file

@ -17,6 +17,7 @@
#include "til/u8u16convert.h"
#include "til/spsc.h"
#include "til/coalesce.h"
#include "til/replace.h"
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{

73
src/inc/til/replace.h Normal file
View file

@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
namespace til
{
namespace details
{
template<typename T>
struct view_type_oracle
{
};
template<>
struct view_type_oracle<std::string>
{
using type = std::string_view;
};
template<>
struct view_type_oracle<std::wstring>
{
using type = std::wstring_view;
};
}
// Method Description:
// - This is a function for finding all occurences of a given string
// `needle` in a larger string `haystack`, and replacing them with the
// string `replacement`.
// - This find/replace is done in-place, leaving `haystack` modified as a result.
// Arguments:
// - haystack: The string to find and replace in
// - needle: The string to search for
// - replacement: The string to replace `needle` with
// Return Value:
// - <none>
template<typename T>
void replace_needle_in_haystack_inplace(T& haystack,
const typename details::view_type_oracle<T>::type& needle,
const typename details::view_type_oracle<T>::type& replacement)
{
auto pos{ T::npos };
while ((pos = haystack.rfind(needle, pos)) != T::npos)
{
haystack.replace(pos, needle.size(), replacement);
}
}
// Method Description:
// - This is a function for finding all occurences of a given string
// `needle` in a larger string `haystack`, and replacing them with the
// string `replacement`.
// - This find/replace is done on a copy of `haystack`, leaving `haystack`
// unmodified, and returning a new string.
// Arguments:
// - haystack: The string to search for `needle` in.
// - needle: The string to search for
// - replacement: The string to replace `needle` with
// Return Value:
// - a copy of `haystack` with all instances of `needle` replaced with `replacement`.`
template<typename T>
T replace_needle_in_haystack(const T& haystack,
const typename details::view_type_oracle<T>::type& needle,
const typename details::view_type_oracle<T>::type& replacement)
{
std::basic_string<typename T::value_type> result{ haystack };
replace_needle_in_haystack_inplace(result, needle, replacement);
return result;
}
}

View file

@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "WexTestClass.h"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
class ReplaceTests
{
TEST_CLASS(ReplaceTests);
TEST_METHOD(ReplaceStrings);
TEST_METHOD(ReplaceStringAndViews);
TEST_METHOD(ReplaceStringsInplace);
TEST_METHOD(ReplaceStringAndViewsInplace);
TEST_METHOD(ReplaceWstrings);
TEST_METHOD(ReplaceWstringAndViews);
TEST_METHOD(ReplaceWstringsInplace);
TEST_METHOD(ReplaceWstringAndViewsInplace);
// There are explicitly no winrt::hstring tests here, because it's capital-H
// hard to get the winrt hstring header included in this project without
// pulling in all of the winrt machinery.
};
void ReplaceTests::ReplaceStrings()
{
std::string foo{ "foo" };
auto temp1 = til::replace_needle_in_haystack(foo, "f", "b");
VERIFY_ARE_EQUAL("boo", temp1);
auto temp2 = til::replace_needle_in_haystack(temp1, "o", "00");
VERIFY_ARE_EQUAL("b0000", temp2);
}
void ReplaceTests::ReplaceStringAndViews()
{
std::string foo{ "foo" };
std::string_view f{ "f" };
std::string_view b{ "b" };
std::string_view o{ "o" };
std::string_view zeroZero{ "00" };
auto temp1 = til::replace_needle_in_haystack(foo, f, b);
VERIFY_ARE_EQUAL("boo", temp1);
auto temp2 = til::replace_needle_in_haystack(temp1, o, zeroZero);
VERIFY_ARE_EQUAL("b0000", temp2);
}
void ReplaceTests::ReplaceStringsInplace()
{
std::string foo{ "foo" };
til::replace_needle_in_haystack_inplace(foo, "f", "b");
VERIFY_ARE_EQUAL("boo", foo);
til::replace_needle_in_haystack_inplace(foo, "o", "00");
VERIFY_ARE_EQUAL("b0000", foo);
}
void ReplaceTests::ReplaceStringAndViewsInplace()
{
std::string foo{ "foo" };
std::string_view f{ "f" };
std::string_view b{ "b" };
std::string_view o{ "o" };
std::string_view zeroZero{ "00" };
til::replace_needle_in_haystack_inplace(foo, f, b);
VERIFY_ARE_EQUAL("boo", foo);
til::replace_needle_in_haystack_inplace(foo, o, zeroZero);
VERIFY_ARE_EQUAL("b0000", foo);
}
void ReplaceTests::ReplaceWstrings()
{
std::wstring foo{ L"foo" };
auto temp1 = til::replace_needle_in_haystack(foo, L"f", L"b");
VERIFY_ARE_EQUAL(L"boo", temp1);
auto temp2 = til::replace_needle_in_haystack(temp1, L"o", L"00");
VERIFY_ARE_EQUAL(L"b0000", temp2);
}
void ReplaceTests::ReplaceWstringAndViews()
{
std::wstring foo{ L"foo" };
std::wstring_view f{ L"f" };
std::wstring_view b{ L"b" };
std::wstring_view o{ L"o" };
std::wstring_view zeroZero{ L"00" };
auto temp1 = til::replace_needle_in_haystack(foo, f, b);
VERIFY_ARE_EQUAL(L"boo", temp1);
auto temp2 = til::replace_needle_in_haystack(temp1, o, zeroZero);
VERIFY_ARE_EQUAL(L"b0000", temp2);
}
void ReplaceTests::ReplaceWstringsInplace()
{
std::wstring foo{ L"foo" };
til::replace_needle_in_haystack_inplace(foo, L"f", L"b");
VERIFY_ARE_EQUAL(L"boo", foo);
til::replace_needle_in_haystack_inplace(foo, L"o", L"00");
VERIFY_ARE_EQUAL(L"b0000", foo);
}
void ReplaceTests::ReplaceWstringAndViewsInplace()
{
std::wstring foo{ L"foo" };
std::wstring_view f{ L"f" };
std::wstring_view b{ L"b" };
std::wstring_view o{ L"o" };
std::wstring_view zeroZero{ L"00" };
til::replace_needle_in_haystack_inplace(foo, f, b);
VERIFY_ARE_EQUAL(L"boo", foo);
til::replace_needle_in_haystack_inplace(foo, o, zeroZero);
VERIFY_ARE_EQUAL(L"b0000", foo);
}

View file

@ -19,6 +19,7 @@
<ClCompile Include="SizeTests.cpp" />
<ClCompile Include="ColorTests.cpp" />
<ClCompile Include="CoalesceTests.cpp" />
<ClCompile Include="ReplaceTests.cpp" />
<ClCompile Include="SomeTests.cpp" />
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>