Introduce ActionMap to Terminal Settings Model#9621
Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
As described in the PR Description, I can't run the local tests for the settings model, for some reason. Here's the error I get:
3>Microsoft.Terminal.Settings.Model.Lib.lib(init.obj) : error LNK2005: DllMain already defined in MSVCRTD.lib(dll_dllmain_stub.obj)
Anybody got any ideas?
EDIT: sometimes I can get it to work and sometimes I can't. I managed to verify that most of the local tests passed. Made one more change afterwards that removed an unnecessary warning during deserialization. I suspect that was the cause of a lot of test failures so I think this is good to go (just need to verify that once the tests work again.)
EDIT: tests are fixed
| // If this throws, the app will catch it and use the default settings | ||
| resultPtr->_ValidateSettings(); | ||
|
|
||
| // GH 3855 - Gathering Data on custom profiles to inform better defaults |
There was a problem hiding this comment.
Covered this with Dustin. This was a quick-n-easy way to gather data on the key bindings. Since we removed KeyMapping, we don't actually have a way to serialize our key bindings (yet).
Makes more sense to just outright remove this, then we leverage the settings model to gather data better later (if desired).
54f17b8 to
b71b595
Compare
This comment has been minimized.
This comment has been minimized.
b71b595 to
b9c2ac0
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
2 similar comments
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
70cd021 to
c9b8294
Compare
This comment has been minimized.
This comment has been minimized.
zadjii-msft
left a comment
There was a problem hiding this comment.
Alright, so I'm at 42/52. Probably 30 of those were fully just renaming variables, but I am making progress on this. I'll keep at it.
Maybe for the Action().Action() I've got three equally bad ideas:
command.Action().Type()command.ActionAndArgs().Action()command.Action().ShortcutAction()
zadjii-msft
left a comment
There was a problem hiding this comment.
Okay a bunch of stuff still needs to be done here, but I've read it all! Almost easier to just read it all like it's entirely new code, rather than with the diff. Bunch of comments below.
|
|
||
| hstring Command::Name() const noexcept | ||
| { | ||
| if (_name.has_value()) |
There was a problem hiding this comment.
okay so in the past hasn't there been some weirdness with optional<string>s? Don't we usually use string.empty() to indicate that a string property doesn't have a value set, rather than an optional<string>? Is there a reason we're doing the optional here?
There was a problem hiding this comment.
Yes! So this goes back to the problem of IReference<hstring> cannot be exposed in the IDL.
The current architecture is...
- construct a
Commandfor each of the commands in the JSON - register that
Commandin ourActionMap
a. If it exists, update the existing command
b. If it's new, just add it in like normal
The problem is, not all commands in the JSON have a "name". And if the "name" is missing, we want to interpret that as "just use the name we were already using". Like so...
// Scenario 1
// "foo" invokes the "copy" command across 3 key bindings ctrl+a/b/c
{ "name": "foo", "command": "copy", "keys": "ctrl+a" },
{ "command": "copy", "keys": "ctrl+b" },
{ "command": "copy", "keys": "ctrl+c" },
// Scenario 2
// the "paste" command has 3 key bindings ctrl+x/y/z
// the "paste" command is _NOT_ in the command palette
{ "name": "bar", "command": "paste", "keys": "ctrl+x" },
{ "command": "paste", "keys": "ctrl+y" },
{ "name": null, "command": "paste", "keys": "ctrl+z" },Storing _name as an optional<string> allows us to distinguish the two scenarios above.
TL;DR: we need to distinguish between an "inherited" name and no name again
There was a problem hiding this comment.
The same thing can be said for "icon".
| // Add NestedCommands to NameMap _after_ we handle our parents. | ||
| // This allows us to override whatever our parents tell us. | ||
| for (const auto& [name, cmd] : _NestedCommands) | ||
| { | ||
| nameMap.insert_or_assign(name, cmd); | ||
| } |
There was a problem hiding this comment.
This seems weird - why isn't this above the parents check with a similar if (visitedActionIDs.find() == end()) check?
There was a problem hiding this comment.
Yeah, this one's trippy. Nested commands are special in that we have no way to check for equality. Thus, we can't add them to visitedActionIDs. Instead, we do the following:
- In
AddAction, remove/update_NestedCommandsif there is a collision (i.e.copyaction was explicitly set to have the same name as a nested command) - In
_PopulateNameMap...
a. add nested commands last so that we override any collisions from our parents
Using this method, we make sure that we do not modify our parents. This lets you see the state of our ActionMap after loading a specific settings file as opposed to the whole thing.
| { | ||
| // This command has a name, check if it's new. | ||
| const auto newName{ cmd.Name() }; | ||
| if (newName != oldCmd.Name()) |
There was a problem hiding this comment.
So to be sure - this is comparing newName (which definitely is a given name, because HasName will be false for a generated name) with the current name for this command - which might be a generated name (if oldCmd doesn't have a name explicitly set)?
If I do something like
{ "command": { "action": "newTab" } },
{ "command": { "action": "newTab" }, "name": "foo" },
{ "command": { "action": "newTab" }, "name": "bar" },
will I end up with one command in the palette, or 3?
There was a problem hiding this comment.
One: "bar". ActionMap will store the command as if it was:
{ "command": { "action": "newTab" }, "name": "bar" },This will be super useful when I start working on serialization because we won't need to write any of the older names that got overwritten.
This comment has been minimized.
This comment has been minimized.
Fixed! I'm a bit annoyed by the fix but it works and it's not bad, tbh. Basically, I split up
An important assumption is that in the current layer, there is no overlap in names between the names of By taking a two-step approach to populating the name map, we are able to handle collisions across multiple layers. |
This comment has been minimized.
This comment has been minimized.
Fixed! Had to move While fixing this, I ran into a problem with a deserialization test. This PR introduces new behavior for actions, but I think this is the correct behavior. It is as follows: { "name": "foo", "command": "copy" },
{ "name": "bar", "command": "copy" }
I believe this is the desired behavior because every command should only have one name. Using this behavior, we can fix #7441. In fact, #7441 needs this. Consider the following: { "name": "foo", "command": "copy" },
{ "name": "bar", "command": "copy" },
{ "name": null, "command": "copy" },
The value of a null name comes from being able to find the names that map to the command, and remove them from the name map. Thus, we can leverage internal action IDs (aka hashing) to accomplish this. |
41135af to
efe5bd6
Compare
DHowett
left a comment
There was a problem hiding this comment.
OKAY i reviewed ActionMap.cpp
| { | ||
| typedef size_t InternalActionID; | ||
|
|
||
| struct KeyChordHash |
There was a problem hiding this comment.
qq: why is this its own struct and not an std::hash specialization?
There was a problem hiding this comment.
This was stolen from KeyMapping. We use it when we declare a key map:
std::unordered_map<Control::KeyChord, InternalActionID, KeyChordHash, KeyChordEquality> _KeyMap;I think we ran into a problem before with CppWinRT already using std::hash specializations, so idk if that would work (at least right now).
| } | ||
| }; | ||
|
|
||
| struct KeyChordEquality |
There was a problem hiding this comment.
qq: why is this its own struct and not an operator== in the global namespace (or anywhere we can use ADL -- so, inside this namespace is fine)?
There was a problem hiding this comment.
Same as above, this was stolen from KeyMapping and is used in the same declarations for key map.
This one makes more sense to just have as a global operator though, so I'll do that.
There was a problem hiding this comment.
Ugh, I tried just that, but then all the key bindings were broken. It seems like these unordered_maps need KeyChordEquality.
There was a problem hiding this comment.
the unordered map should be using std::equals or std::is_equal which will use operator== from the type unless it's overridden in the template args.
Like, this should work: (writing code in godbolt)
There was a problem hiding this comment.
okay, this works. it actually requires the operator to be in the leaf namespace. i could have sworn it should work in the parent namespace.
#include <unordered_map>
namespace A::B::C {
struct Thing {
int a, b;
};
// it apparently has to go here
inline bool operator==(const C::Thing& lhs, const C::Thing& rhs) {
return lhs.a==rhs.a && lhs.b==rhs.b;
}
}
namespace A::B {
struct ThingHasher {
size_t operator()(const C::Thing& lhs) const { return 7; }
};
// okay, i thought you could put it here and because A::B::C is inside A::B it would find it.
}
int fleh() {
std::unordered_map<A::B::C::Thing, int, A::B::ThingHasher> augh;
augh[A::B::C::Thing{1,2}] = 3;
augh[A::B::C::Thing{3,4}] = 7;
return augh.find(A::B::C::Thing{1,2}) == augh.end();
}There was a problem hiding this comment.
Ah, ADL only uses the leafmost namespace for a lot of things. So yes: if you introduce an operator== into the MS.Terminal.Control namespace in your header file it will get used automatically
There was a problem hiding this comment.
broken = not recognized. Like, "ctrl+shift+space" wouldn't be detected.
There was a problem hiding this comment.
llllet's just leave it. we can fix it in post.
DHowett
left a comment
There was a problem hiding this comment.
okay I think i understand the bulk of it
DHowett
left a comment
There was a problem hiding this comment.
I'm comfortable with this. Thanks so much for all the comments!
## Summary of the Pull Request This PR lays the foundation for a new Actions page in the Settings UI as designed in #6900. The Actions page now leverages the `ActionMap` to display all of the key bindings and allow the user to modify the associated key chord or delete the key binding entirely. ## References #9621 - ActionMap #9926 - ActionMap serialization #9428 - ActionMap Spec #6900 - Actions page #9427 - Actions page design doc ## Detailed Description of the Pull Request / Additional comments ### Settings Model Changes - `Command::Copy()` now copies the `ActionAndArgs` - `ActionMap::RebindKeys()` handles changing the key chord of a key binding. If a conflict occurs, the conflicting key chord is overwritten. - `ActionMap::DeleteKeyBinding()` "deletes" a key binding by binding "unbound" to the given key chord. - `ActionMap::KeyBindings()` presents another view (similar to `NameMap`) of the `ActionMap`. It specifically presents a map of key chords to commands. It is generated similar to how `NameMap` is generated. ### Editor Changes - `Actions.xaml` is mainly split into two parts: - `ListView` (as before) holds the list of key bindings. We _could_ explore the idea of an items repeater, but the `ListView` seems to provide some niceties with regards to navigating the list via the key board (though none are selectable). - `DataTemplate` is used to represent each key binding inside the `ListView`. This is tricky because it is bound to a `KeyBindingViewModel` which must provide _all_ context necessary to modify the UI and the settings model. We cannot use names to target UI elements inside this template, so we must make the view model smart and force updates to the UI via changes in the view model. - `KeyBindingViewModel` is a view model object that controls the UI and the settings model. There are a number of TODOs in Actions.cpp will be long-term follow-ups and would be nice to have. This includes... - a binary search by name on `Actions::KeyBindingList` - presenting an error when the provided key chord is invalid. ## Demo 
## Summary of the Pull Request This PR builds on the `ActionMap` PR (#6900) by leveraging `ActionMap` to serialize actions. From the top down, the process is now as follows: - `CascadiaSettings`: remove the hack of copying whatever we had for actions before. - `GlobalAppSettings`: serialize the `ActionMap` to `"actions": []` - `ActionMap`: iterate over the internal `_ActionMap` (list of actions) and serialize each `Command` - `Command`: **THIS IS WHERE THE MAGIC HAPPENS!** For _each_ key mapping, serialize an action. Only the first one needs to include the name and icon. - `ActionAndArgs`: Find the relevant `IActionArgs` parser and serialize the `ActionAndArgs`. - `ActionArgs`: **ANNOYING CHANGE** Serialize any args that are set. We _need_ each setting to be saved as a `std::optional`. As with inheritance, this allows us to distinguish an explicit setting to the default value (sometimes `null`) vs an implicit "give me the default value". This allows us to serialize only the relevant details of each action, rather than writing _all_ of the args. ## References - #8100: Inheritance/Layering for lists - This tracks layering and better serialization for color schemes _and_ actions. This PR resolves half of that issue. The next step is to apply the concepts used in this PR (and #9621) to solve the similar problem for color schemes. - #6900: Actions page ## Validation Steps Performed Tests added!
|
🎉 Handy links: |
Summary of the Pull Request
This entirely removes
KeyMappingfrom the settings model, and builds on the work done in #9543 to consolidate all actions (key bindings and commands) into a unified data structure (ActionMap).References
#9428 - Spec
#6900 - Actions page
Closes #7441
Detailed Description of the Pull Request / Additional comments
The important thing here is to remember that we're shifting our philosophy of how to interact/represent actions. Prior to this, the actions arrays in the JSON would be deserialized twice: once for key bindings, and again for commands. By thinking of every entry in the relevant JSON as a
Command, we can remove a lot of the context switching between working with a key binding vs a command palette item.#9543 allows us to make that shift. Given the work in that PR, we can now deserialize all of the relevant information from each JSON action item. This allows us to simplify
ActionMap::FromJsonto simply iterate over each JSON action item, deserialize it, and add it to ourActionMap.Internally, our
ActionMapoperates as discussed in #9428 by maintaining a_KeyMapthat points to an action ID, and using that action ID to retrieve theCommandfrom the_ActionMap. Adding actions to theActionMapautomatically accounts for name/key-chord collisions. ANameMapcan be constructed when requested; this is for the Command Palette.Querying the
ActionMapis fairly straightforward. Helper functions were needed to be able to distinguish an explicit unbinding vs the command not being found in the current layer. Internally, we store explicitly unbound names/key-chords asShortcutAction::Invalidcommands. However, we returnnullptrwhen a query points to an unbound command. This is done to hide this complexity away from any caller.The command palette still needs special handling for nested and iterable commands. Thankfully, the expansion of iterable commands is performed on an
IMapView, so we can just exposeNameMapas a consolidation ofActionMap'sNameMapwith its parents. The same can be said for exposing key chords in nested commands.Validation Steps Performed
All local tests pass.