diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index 722cb0f8e4..d8d328b105 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -85,6 +85,10 @@ namespace TerminalAppLocalTests TEST_METHOD(TestIterateCommands); TEST_METHOD(TestIterateAutogenNamedCommands); TEST_METHOD(TestIterateOnBadJson); + TEST_METHOD(TestNestedCommands); + TEST_METHOD(TestNestedInIterableCommand); + TEST_METHOD(TestIterableInNestedCommand); + TEST_METHOD(TestNestedCommandWithoutName); TEST_CLASS_SETUP(ClassSetup) { @@ -2927,4 +2931,50 @@ namespace TerminalAppLocalTests } } + void SettingsTests::TestNestedCommands() + { + // This test + Log::Result(WEX::Logging::TestResults::Skipped); + } + void SettingsTests::TestNestedInIterableCommand() + { + // This test checks a iterable command that includes a nested command. + // The commands should look like: + // + // + // └─ New Pane... + // ├─ Profile 1... + // | ├─ Split Automatically + // | ├─ Split Vertically + // | └─ Split Horizontally + // ├─ Profile 2... + // | ├─ Split Automatically + // | ├─ Split Vertically + // | └─ Split Horizontally + // └─ Profile 3... + // ├─ Split Automatically + // ├─ Split Vertically + // └─ Split Horizontally + Log::Result(WEX::Logging::TestResults::Skipped); + } + void SettingsTests::TestIterableInNestedCommand() + { + // This test checks a nested command that includes an iterable command. + // The commands should look like: + // + // + // └─ New Tab With Profile... + // ├─ Profile 1 + // ├─ Profile 2 + // └─ Profile 3 + Log::Result(WEX::Logging::TestResults::Skipped); + } + void SettingsTests::TestNestedCommandWithoutName() + { + // This test tests a nested command without a name specified. This type + // of command should just be ignored, since we can't auto-generate names + // for nested commands, they _must_ have names specified. + Log::Result(WEX::Logging::TestResults::Skipped); + } + } diff --git a/src/cascadia/TerminalApp/Command.cpp b/src/cascadia/TerminalApp/Command.cpp index 5e95de0460..a6888fa069 100644 --- a/src/cascadia/TerminalApp/Command.cpp +++ b/src/cascadia/TerminalApp/Command.cpp @@ -11,13 +11,14 @@ using namespace winrt::Microsoft::Terminal::Settings; using namespace winrt::TerminalApp; +using namespace winrt::Windows::Foundation; 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" }; @@ -25,6 +26,16 @@ static constexpr std::wstring_view ProfileName{ L"${profile.name}" }; namespace winrt::TerminalApp::implementation { + Command::Command() + { + _nestedCommandsView = winrt::single_threaded_observable_vector(); + } + + Collections::IObservableVector Command::NestedCommands() + { + return _nestedCommandsView; + } + // 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. @@ -108,51 +119,95 @@ namespace winrt::TerminalApp::implementation // Return Value: // - the newly constructed Command object. winrt::com_ptr Command::FromJson(const Json::Value& json, - std::vector<::TerminalApp::SettingsLoadWarnings>& warnings) + std::vector<::TerminalApp::SettingsLoadWarnings>& warnings, + const bool postExpansion) { auto result = winrt::make_self(); - if (const auto iterateOnJson{ json[JsonKey(IterateOnKey)] }) + bool iterable = false; + bool nested = false; + if (!postExpansion) { - auto s = iterateOnJson.asString(); - if (s == IterateOnProfilesValue) + if (const auto iterateOnJson{ json[JsonKey(IterateOnKey)] }) { - result->_IterateOn = ExpandCommandType::Profiles; + auto s = iterateOnJson.asString(); + if (s == IterateOnProfilesValue) + { + result->_IterateOn = ExpandCommandType::Profiles; + iterable = true; + } } } + // 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 (!iterable) + { + if (const auto nestedCommandsJson{ json[JsonKey(CommandsKey)] }) + { + 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()); + + // Add all the commands we've parsed to the observable vector we + // have, so we can access them in XAML. + for (auto& nameAndCommand : result->_subcommands) + { + auto command = nameAndCommand.second; + result->_nestedCommandsView.Append(command); + } + nested = true; + } + + // TODO: else if (hasKey(CommandKey) ) + // { + // // { 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 command. + 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; } - - // 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 { - // { 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 @@ -332,7 +387,7 @@ namespace winrt::TerminalApp::implementation } // Pass the new json back though FromJson, to get the new expanded value. - if (auto newCmd{ Command::FromJson(newJsonValue, warnings) }) + if (auto newCmd{ Command::FromJson(newJsonValue, warnings, true) }) { newCommands.push_back(*newCmd); } diff --git a/src/cascadia/TerminalApp/Command.h b/src/cascadia/TerminalApp/Command.h index 1469d1536b..3e27a044a7 100644 --- a/src/cascadia/TerminalApp/Command.h +++ b/src/cascadia/TerminalApp/Command.h @@ -33,9 +33,11 @@ namespace winrt::TerminalApp::implementation struct Command : CommandT { - Command() = default; + Command(); - static winrt::com_ptr FromJson(const Json::Value& json, std::vector<::TerminalApp::SettingsLoadWarnings>& warnings); + static winrt::com_ptr FromJson(const Json::Value& json, + std::vector<::TerminalApp::SettingsLoadWarnings>& warnings, + const bool postExpansion = false); static std::vector ExpandCommand(winrt::com_ptr expandable, const std::vector<::TerminalApp::Profile>& profiles, @@ -44,6 +46,8 @@ namespace winrt::TerminalApp::implementation static std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(std::unordered_map& commands, const Json::Value& json); + Windows::Foundation::Collections::IObservableVector NestedCommands(); + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Name, _PropertyChangedHandlers); OBSERVABLE_GETSET_PROPERTY(winrt::TerminalApp::ActionAndArgs, Action, _PropertyChangedHandlers); @@ -52,6 +56,8 @@ namespace winrt::TerminalApp::implementation private: Json::Value _originalJson; + std::unordered_map _subcommands; + Windows::Foundation::Collections::IObservableVector _nestedCommandsView{ nullptr }; }; } diff --git a/src/cascadia/TerminalApp/Command.idl b/src/cascadia/TerminalApp/Command.idl index 80e1a2b476..3dba75026a 100644 --- a/src/cascadia/TerminalApp/Command.idl +++ b/src/cascadia/TerminalApp/Command.idl @@ -12,5 +12,7 @@ namespace TerminalApp String Name; ActionAndArgs Action; String KeyChordText; + + Windows.Foundation.Collections.IObservableVector NestedCommands { get; }; } } diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp index 7a1470ff75..378c6315f0 100644 --- a/src/cascadia/TerminalApp/CommandPalette.cpp +++ b/src/cascadia/TerminalApp/CommandPalette.cpp @@ -22,6 +22,7 @@ namespace winrt::TerminalApp::implementation _filteredActions = winrt::single_threaded_observable_vector(); _allActions = winrt::single_threaded_vector(); + _nestedActionStack = winrt::single_threaded_vector(); if (CommandPaletteShadow()) { diff --git a/src/cascadia/TerminalApp/CommandPalette.h b/src/cascadia/TerminalApp/CommandPalette.h index b4cb92762d..31be388855 100644 --- a/src/cascadia/TerminalApp/CommandPalette.h +++ b/src/cascadia/TerminalApp/CommandPalette.h @@ -22,6 +22,7 @@ namespace winrt::TerminalApp::implementation Windows::Foundation::Collections::IObservableVector _filteredActions{ nullptr }; Windows::Foundation::Collections::IVector _allActions{ nullptr }; + Windows::Foundation::Collections::IVector _nestedActionStack{ nullptr }; winrt::TerminalApp::ShortcutActionDispatch _dispatch; void _filterTextChanged(Windows::Foundation::IInspectable const& sender, diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index c445779e9e..11a6de41d3 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -105,6 +105,9 @@ namespace winrt::TerminalApp::implementation // 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()) }; + + // TODO: Make this recursive, because commands might have + // subcommands that also have keybindings if (keyChord) { command.KeyChordText(KeyChordSerialization::ToString(keyChord));