Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for iterable, nested commands #6856

Merged
82 commits merged into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
c56eb8f
Add a ton of text regarding Commandline Mode vs Action Mode
zadjii-msft Apr 30, 2020
fe640ff
move this spec out of `drafts/`
zadjii-msft Apr 30, 2020
614d1b2
last little todos for review
zadjii-msft Apr 30, 2020
ead76d7
Merge remote-tracking branch 'origin/master' into dev/migrie/s/5400-C…
zadjii-msft May 7, 2020
2e56436
Move this section to 'future considerations'
zadjii-msft May 7, 2020
0886e8f
Merge remote-tracking branch 'origin/master' into dev/migrie/s/5400-C…
zadjii-msft May 20, 2020
11130a4
Add some notes about the advanced tab switcher and how that might int…
zadjii-msft May 20, 2020
f53553f
Merge remote-tracking branch 'origin/master' into dev/migrie/s/5400-C…
zadjii-msft May 28, 2020
0672812
good bot
zadjii-msft May 28, 2020
43bd483
Blind port these files from the old branch
zadjii-msft May 28, 2020
75af4ca
Hook up all the parsing once again
zadjii-msft May 28, 2020
33a9e32
Get names from the resources if provided as an object, not a string
zadjii-msft May 28, 2020
3627d8a
This should have been in the previous commit
zadjii-msft May 28, 2020
0416a94
Hook up the parsing of `ActionAndArgs`s to the command palette
zadjii-msft May 28, 2020
6905065
Merge remote-tracking branch 'origin/master' into dev/migrie/f/2046-C…
zadjii-msft May 28, 2020
02f47f4
Make the action names map public, so the ToJson in AKBSerialization c…
zadjii-msft May 28, 2020
62b9a0d
Move the action into it's own sub-object
zadjii-msft May 28, 2020
207666e
Add a ton of default commands
zadjii-msft May 29, 2020
b88be45
Make sure to scroll the selected item into view
zadjii-msft May 29, 2020
00763fd
Add duplicate pane to the default commands
zadjii-msft May 29, 2020
bc546db
Add some stability
zadjii-msft May 29, 2020
e1be26b
Do this in WinRTUtils instead of hackily doing it manually
zadjii-msft May 29, 2020
a309191
Rename private methods, fix wraparound logic
zadjii-msft May 29, 2020
9905945
Return focus to the active control when closed
zadjii-msft May 29, 2020
cf6e1f2
Try to do this with a shadow, but it crashes inexplicably
zadjii-msft May 29, 2020
fa93fdc
Well, this is neat, and works, but requires 18362 and also casts a sh…
zadjii-msft May 29, 2020
1fbe8e4
Turns out, the shadow on the menuflyout isn't my fault, it's on every…
zadjii-msft May 29, 2020
6e6979a
Extract ActionAndArgs::FromJson into it's own class, so it can be re-…
zadjii-msft Jun 4, 2020
829beda
Merge remote-tracking branch 'origin/master' into dev/migrie/f/2046-C…
zadjii-msft Jun 4, 2020
487f33e
Merge commit '6e6979abe' into dev/migrie/f/2046-Command-Palette-v2
zadjii-msft Jun 4, 2020
58be8cd
Tons of commenting
zadjii-msft Jun 4, 2020
9d411d4
Make the commands a map, so we can override on "name"
zadjii-msft Jun 4, 2020
d71d8d7
Ready for review.
zadjii-msft Jun 4, 2020
b3d8f0e
Merge remote-tracking branch 'origin/master' into dev/migrie/f/2046-C…
zadjii-msft Jun 9, 2020
67c7969
I couldn't tell you how long this took
zadjii-msft Jun 9, 2020
82f968d
If a key is bound to that action, then display the keybinding in the …
zadjii-msft Jun 9, 2020
edc8b55
Add much better key chord text to the command palette
zadjii-msft Jun 10, 2020
dd684cb
implement a weighted ordering for command palette entries
zadjii-msft Jun 10, 2020
3674c61
Add support for expanding a command into many commands for every prof…
zadjii-msft Jun 10, 2020
0b126f0
lets clean this up so it's not so horrifying.
zadjii-msft Jun 10, 2020
4420aea
Merge remote-tracking branch 'origin/master' into dev/migrie/f/comman…
zadjii-msft Jul 7, 2020
4e134e3
get it building again
zadjii-msft Jul 7, 2020
946d710
This works to auto-generate names from iterable commands without give…
zadjii-msft Jul 7, 2020
a5eba5a
add some stubs for tests in the future
zadjii-msft Jul 7, 2020
ac74de4
escape profile names for json before re-parsing
zadjii-msft Jul 8, 2020
7321639
re-use FromJson when expanding, as to not just duplicate the entire m…
zadjii-msft Jul 8, 2020
d953d8b
start working on nested actions
zadjii-msft Jul 8, 2020
dc7a816
Merge remote-tracking branch 'origin/master' into dev/migrie/f/comman…
zadjii-msft Jul 8, 2020
0c8471d
holy shit this worked on the first try
zadjii-msft Jul 8, 2020
bdd7e3b
write a test that _should_ check if an iterable command can have nest…
zadjii-msft Jul 8, 2020
93d31c2
This was a failure
zadjii-msft Jul 8, 2020
5052e64
Revert "This was a failure"
zadjii-msft Jul 8, 2020
c6a8fe9
more tests, more!
zadjii-msft Jul 9, 2020
f5659d2
this fixes `TestNestedInIterableCommand`, but `TestMixedNestedAndIter…
zadjii-msft Jul 9, 2020
077cd6e
this mysteriously fixes the mixed test, which I was pretty onfident I…
zadjii-msft Jul 9, 2020
26a1a41
man these are some great tests
zadjii-msft Jul 9, 2020
33ee177
clean up this duplicated code
zadjii-msft Jul 9, 2020
be5aee3
make sure to actually update the xaml lists as well.
zadjii-msft Jul 9, 2020
92167a5
code cleanup
zadjii-msft Jul 9, 2020
7968b2a
more tests, code cleanup
zadjii-msft Jul 9, 2020
c5208e6
finish remaining todos, also, good bot
zadjii-msft Jul 9, 2020
7cb870c
Merge remote-tracking branch 'origin/master' into dev/migrie/f/comman…
zadjii-msft Aug 3, 2020
8fd3e0d
These changes to defaults.json are out-of-date, and Command doesn't n…
zadjii-msft Aug 3, 2020
2763c90
Merge remote-tracking branch 'origin/master' into dev/migrie/f/comman…
zadjii-msft Aug 4, 2020
3d0c81c
the tiniest nit
zadjii-msft Aug 4, 2020
50bdc3d
Merge remote-tracking branch 'origin/master' into dev/migrie/f/comman…
zadjii-msft Aug 5, 2020
2a8d4c5
well, we definitely want this
zadjii-msft Aug 5, 2020
0719b42
This makes command expansion the TerminalPage's responsibility, but i…
zadjii-msft Aug 5, 2020
77e51af
expand commands every time the list of tabs changes
zadjii-msft Aug 5, 2020
1e9bf47
Merge remote-tracking branch 'origin/master' into dev/migrie/f/comman…
zadjii-msft Aug 11, 2020
b60b82b
Merge remote-tracking branch 'origin/master' into dev/migrie/f/comman…
zadjii-msft Aug 11, 2020
ec2db21
I guess we only really need to do this once
zadjii-msft Aug 11, 2020
c8c0032
Merge remote-tracking branch 'origin/dev/migrie/f/6856-let-terminalpa…
zadjii-msft Aug 11, 2020
b4eb86d
nits from PR
zadjii-msft Aug 11, 2020
1975e8c
Merge remote-tracking branch 'origin/master' into dev/migrie/f/comman…
zadjii-msft Aug 12, 2020
57abe29
Add a helper for find/replace-ing a string
zadjii-msft Aug 12, 2020
9981126
Clean up our string handling substantially
zadjii-msft Aug 12, 2020
c55d53e
minor nits, more tracing
zadjii-msft Aug 12, 2020
3269309
some cleaner lifetime management for these variables
zadjii-msft Aug 12, 2020
933dc7e
use a IMap to store subcommands, and then generate the vector when we…
zadjii-msft Aug 13, 2020
5198142
Fix the localtests for commands
zadjii-msft Aug 13, 2020
7b014d9
Dustin wanted every command to not necessarily have a subcommand map,…
zadjii-msft Aug 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,356 changes: 1,356 additions & 0 deletions src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/cascadia/TerminalApp/AppKeyBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion src/cascadia/TerminalApp/AppLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
1 change: 0 additions & 1 deletion src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
// "Generated Files" directory.

using namespace ::TerminalApp;
using namespace winrt::Microsoft::Terminal::TerminalControl;
using namespace winrt::TerminalApp;
using namespace ::Microsoft::Console;

Expand Down
294 changes: 281 additions & 13 deletions src/cascadia/TerminalApp/Command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,33 @@
#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::wstring_view ProfileName{ L"${profile.name}" };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whatever we choose, I absolutely want to make sure we can eventually give this a parser and a key/value reader so we don't need to specialize variable substitutions by the hundreds 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I could see that being a reasonable follow-up to this. I'd rather not convolute this PR more though, if possible 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mostly just mean “let’s choose a syntax that isn’t going to hamper our efforts here in the future” moreso than “do it in this PR” :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay, that makes sense.

We could maybe even drop the profile. here. You're already iterating on profiles. Maybe that's redundant?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I don’t want us to regret it later and have to shim toooooo much)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that idea actually, profile being implied. Or like Go templates, .name where . Implies the object currently in scope (it supports referencing things that aren’t in scope)


namespace winrt::TerminalApp::implementation
{
Command::Command()
{
_nestedCommandsView = winrt::single_threaded_vector<winrt::TerminalApp::Command>();
_setAction(nullptr);
}

Collections::IVector<winrt::TerminalApp::Command> 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.
Expand Down Expand Up @@ -99,35 +117,81 @@ 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)] })
{
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;
Expand Down Expand Up @@ -181,4 +245,208 @@ 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.
winrt::hstring _escapeForJson(const std::wstring_view input)
{
Json::Value inJson{ winrt::to_string(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 winrt::to_hstring(ss);
}
return winrt::to_hstring(out);
}

// Function Description:
// - A helper to replace any occurences of `keyword` with `replaceWith` in `sourceString`
winrt::hstring _replaceKeyword(const winrt::hstring& sourceString,
const std::wstring_view keyword,
const std::wstring_view replaceWith)
{
std::wstring result{ sourceString };
size_t index = 0;
while (true)
{
index = result.find(keyword, index);
if (index == std::wstring::npos)
{
break;
}
result.replace(index, keyword.size(), replaceWith);
index += replaceWith.size();
}
return winrt::hstring{ result };
}

// 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(std::unordered_map<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 (auto nameAndCmd : commands)
{
winrt::com_ptr<winrt::TerminalApp::implementation::Command> cmd;
cmd.copy_from(winrt::get_self<winrt::TerminalApp::implementation::Command>(nameAndCmd.second));

auto newCommands = _expandCommand(cmd, profiles, warnings);
if (newCommands.size() > 0)
{
commandsToRemove.push_back(nameAndCmd.first);
commandsToAdd.insert(commandsToAdd.end(), newCommands.begin(), newCommands.end());
}
}

// Second, remove all the commands that need to be removed.
for (auto& name : commandsToRemove)
{
commands.erase(name);
}

// Finally, add all the new commands.
for (auto& cmd : commandsToAdd)
{
commands.insert_or_assign(cmd.Name(), cmd);
}
}

// Method Description:
// - Initialize our ObservableVector of NestedCommands, recursively. This
// will build the vector of nested Commands that XAML can access.
// Arguments:
// - <none>
// Return Value:
// - <none>
void Command::_createView()
{
_nestedCommandsView.Clear();

// Add all the commands we've parsed to the observable vector we
// have, so we can access them in XAML.
for (auto& nameAndCommand : _subcommands)
{
auto command = nameAndCommand.second;
_nestedCommandsView.Append(command);

winrt::com_ptr<winrt::TerminalApp::implementation::Command> cmd;
cmd.copy_from(winrt::get_self<winrt::TerminalApp::implementation::Command>(command));
cmd->_createView();
}
}

// 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(winrt::com_ptr<Command> expandable,
gsl::span<const ::TerminalApp::Profile> profiles,
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings)
{
std::vector<winrt::TerminalApp::Command> newCommands;

if (!expandable->_subcommands.empty())
{
ExpandCommands(expandable->_subcommands, profiles, warnings);

expandable->_createView();
}

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 = winrt::to_hstring(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(p.GetName());
auto newJsonString = winrt::to_string(_replaceKeyword(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;
}
}
Loading