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

[Spec] Settings Model - Actions #9428

Merged
9 commits merged into from
May 5, 2021
Merged

[Spec] Settings Model - Actions #9428

9 commits merged into from
May 5, 2021

Conversation

carlos-zamora
Copy link
Member

@carlos-zamora carlos-zamora commented Mar 9, 2021

This spec covers the settings model work required to create the Actions page in the settings UI (designed in #9427).

Overall, the idea is to promote Command to include the actual KeyChord, then introduce an ActionMap that handles all of the responsibilities of KeyMapping and more (as well as general action management).

Markdown view

@carlos-zamora carlos-zamora added Issue-Docs It's a documentation issue that really should be on MicrosoftDocs/Console-Docs Area-Settings Issues related to settings and customizability, for console or terminal Product-Terminal The new Windows Terminal. labels Mar 9, 2021
@carlos-zamora carlos-zamora added this to the Terminal v1.8 milestone Mar 9, 2021
@zadjii-msft zadjii-msft self-assigned this Mar 16, 2021
Copy link
Member

@zadjii-msft zadjii-msft left a comment

Choose a reason for hiding this comment

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

feels silly to block over this if we're about to have a discussion about this in 6 hours


`GetActionByName` and `GetActionByKeyChord` will directly query the internal maps.
`GetKeyBindingForAction` will iterate through the `_KeyMap` to find a match (similar to how it is now).

Copy link
Member

Choose a reason for hiding this comment

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

Should we mention layering here too? Because the layering more applies to the container of objects than it does the individual Actions. Like, when you have

{ "keys": "ctrl+a", "command": "copy"}
{ "keys": "ctrl+a", "command": "paste"}

then we'll need to first set the KeyChordText of the copy action to "ctrl+a", but then when we go to replace copy in the keymap with paste, we'll need to clear that copy's KeyChordText when we replace it with paste

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a layering section for the defaults.json and settings.json layering.

In an event like you described above where both occur in the same layer, my plan is to keep "copy" in the action list, but actually have "paste" be bound to ctrl+a. Serialization-wise, we'll only output:

{ "command": "copy"}
{ "keys": "ctrl+a", "command": "paste"}

I think this is what our current actions model does, because we just iterate over all of our actions and add them to the command palette independently of their key chord.


Removing a name or a key chord from an `Action` propagates the change to the `ActionMap`.

Removing the action itself is more complicated...
Copy link
Member

Choose a reason for hiding this comment

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

Wait could you elaborate more on how we get into the "remove an action" state? Like, removing the keys from an action, I get why we'd do that. I get why we'd set the name of an action to null. But why are we replacing an action with Unbound? Like, what's the user story here?

Copy link
Member Author

Choose a reason for hiding this comment

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

We talked a bit about this in the spec meeting, and I've updated the spec to kinda cover this a bit better. Let me know if it's still not clear though :)

@carlos-zamora
Copy link
Member Author

Notes from Spec Meeting (3/16)

  1. Don't break up ActionAndArgs
  2. Command (aka Action)
    • Don't make it IInheritable. Move this responsibility to ActionMap
    • It's observable today. We're begrudgingly keeping it for now, but we shouldn't rely on it. We'll remove it one day.
    • Action grouping is not worth it. Just handle that in the ActionViewModel or separate out the actions manually in SUI.
    • Source tracking should be done in the ActionMap instead.
  3. ActionMap
    • Try to remove GetActionByName (or maybe just return ActionAndArgs?). We shouldn't rely more on getting actions by name.
    • Don't expose IMap Commands and IMap Keybindings. Just have everybody use ActionMap
    • Replace map<unsigned int, Action> with vector<Action>. Fill removed actions with null, if we really need it.
    • Don't listen for changes on Action.Name/Keys. It's expensive. Instead, just have ActionMap do that work when we need it.
  4. Layering
    • ActionMap needs a parent ActionMap to disambiguate between settings.json and defaults.json

Spec needs some examples of layering.

@carlos-zamora carlos-zamora requested a review from a team March 17, 2021 18:59
@github-actions

This comment has been minimized.

Copy link
Member

@zadjii-msft zadjii-msft left a comment

Choose a reason for hiding this comment

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

Curious to see how https://github.com/microsoft/terminal/pull/9428/files#r598999437 is resolved, but I think this is otherwise fine?

(there's no other approvals ATM, so I'm gonna ✔️ this so GitHub remembers my place, and because I don't have blocking concerns anymore)

- this should update `_NameMap`, `_KeyMap`, and `_ActionList` appropriately
- if the newly added name/key chord conflicts with a pre-existing one,
redirect `_NameMap` or `_KeyMap` to the newly added `Command` instead,
and update the conflicting one.
Copy link
Member

Choose a reason for hiding this comment

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

So to be clear - "update the conflicting one" means something like "If a subsequent command has the same Keys as an existing one, we'll remove the Keys from the existing one, then update the _KeyMap to point at the subsequent command (instead of the original)"

For Names, we don't really need to remove the Name from the old command in the _NameMap, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

I removed _NameMap from the spec so resolving.

Copy link
Member

Choose a reason for hiding this comment

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

NameMap is still in the spec in the public interface for ActionMap..?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, we still need a NameMap exposed to the projected type for the Command Palette. There's no (easy) way around that because the command palette needs to expand the iterable commands over in TermApp. So the process looks something like this:

  • TermApp requests ActionMap::NameMap
  • we generate the NameMap (as described in the spec)
  • TermApp expands any iterable commands
  • TermApp stores this expanded NameMap and uses it whenever the Command Palette requests it

Internally (to ActionMap), we won't be updating a _NameMap as we add more actions. It's just not worth it because we only want each action to appear once (and the _ActionMap already keeps track of that).

@carlos-zamora

This comment has been minimized.

@github-actions

This comment has been minimized.

@carlos-zamora carlos-zamora removed their assignment Apr 27, 2021

Since `NameMap` is generated upon request, we will need to pass a `std::set<InternalActionID>` as we generate the `NameMap` across each layer. This will ensure that each `Command` is only added to the `NameMap` once. We will start from the current layer and move up the inheritance tree to ensure that the current layer takes priority.

Nested commands will be saved to their own map since they do not have an `InternalActionID`. To ensure layering is accomplished correctly, we will need to start from the top-most parent and update `NameMap`. As we go down the inheritance tree, any conflicts are resolved by prioritizing the current layer (the child). This ensures that the current layer takes priority.
Copy link
Member

Choose a reason for hiding this comment

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

do nested commands have a special hash function that ensures that they will not collide with a hash generated from an Action+ActionAndArgs

Copy link
Member

Choose a reason for hiding this comment

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

or do all nested commands hash to the same thing

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a section in the spec. The gist of it is...

  • No, nested commands can only be identified by their given name. Thus, we cannot add them to the internal _ActionMap. Instead, we add them to their own map<hstring, Command> _NestedCommands.
  • Handling collisions:
    • as actions get added to the ActionMap, add nested commands to their own _NestedCommands map.
    • If a newly added Command conflicts with a name in _NestedCommands, resolve in favor of the new Command by removing the conflicting nested command from _NestedCommands.

@ghost ghost added Needs-Author-Feedback The original author of the issue/PR needs to come back and respond to something and removed Needs-Author-Feedback The original author of the issue/PR needs to come back and respond to something labels Apr 28, 2021
@github-actions

This comment has been minimized.

Nested commands will be saved to their own map since they do not have an `InternalActionID`.
- During `ActionMap`'s population, we must ensure to resolve any conflicts immediately. This means that any new `Command`s that generate a name conflicting with a nested command will take priority (and we'll remove the nested comand from its own map). Conversely, if a new nested command conflicts with an existing standard `Command`, we can ignore it because our generation of `NameMap` will handle it.
- When populating `NameMap`, we must first add all of the standard `Command`s. To ensure layering is accomplished correctly, we will need to start from the top-most parent and update `NameMap`. As we go down the inheritance tree, any conflicts are resolved by prioritizing the current layer (the child). This ensures that the current layer takes priority.
- After adding all of the standard `Command`s to the `NameMap`, we can then register all of the nested commands. Since nested commands have no identifier other than a name, we cannot use the `InternalActionID` heuristic. However, as mentioned earlier, we are updating our internal nested command map as we add new actions. So when we are generating the name map, we can assume that all of these nested commands now have priority. Thus, we simply add all of these nested commands to the name map. Any conflicts are resolved in favor of th nested command.
Copy link
Member

Choose a reason for hiding this comment

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

I'm still worried about this.

Consider that you have the following nested/repeated commands:

Split Pane...
   Windows PowerShell
New Tab...
   Windows PowerShell

those have the same name. They are very much different commands. We're not keying anything off their names, right? Well, except the Name Map which will immediately and horribly explode when you do that.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think there seems to be some confusion from my word choice. When I say "nested commands", I always mean the root. So in your example, there are 2 nested commands:

  • "split pane..."
  • "new tab..."

Part 1: AddAction the nested command

AddAction receives these commands and does the following:

  • is it a nested command?
    • Yes! Insert (or assign) "" to _NestedCommands
  • Done!

Part 2: AddAction a conflicting command

Later, let's say we get a new Command x who's name is "Split Pane...". We have two paths here:

  1. x is a nested command:
    • update _NestedCommands such that "Split Pane..." maps to x
  2. x is a standard command:
    • remove Split Pane... from _NestedCommands
    • register x with _ActionMap

NOTE: We don't care what is inside of these nested commands. We just care about the root.

Part 3: NameMap generation with our parents

Now we get to part 3 of our story: dealing with parents. We don't really care about our parents until we have to generate the NameMap.

  1. add all of the nested actions
    • Due to part 2, we can assume that we have a disjoint set of names between standard and nested actions. This means that all names in _NestedCommands are new to those from the standard actions.
    • If we start from our parents' nested actions, then work our way down to the current layer, we can create a NameMap filled with just nested commands. We don't know if any of them will persist, but that's ok, because then we move on to step 2...
  2. add all of the standard/consolidated actions
    • ANY collisions are just straight-up assigned to these actions. The only collisions we can expect are the ones from our ancestors' nested commands intersecting with our standard actions. If that happens, we have the right to prioritize our standard actions.

@zadjii-msft
Copy link
Member

I'm gonna go out on a limb and say that because this spec isn't for any sort of external-facing feature, we might be able to roll with just two signoffs. As always, the code is truth, so this is more an explanation of the thought process behind #9621 than anything else.

@github-actions

This comment has been minimized.

@carlos-zamora carlos-zamora added the AutoMerge Marked for automatic merge by the bot when requirements are met label May 5, 2021
@ghost
Copy link

ghost commented May 5, 2021

Hello @carlos-zamora!

Because this pull request has the AutoMerge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@ghost ghost merged commit 5713cd2 into main May 5, 2021
@ghost ghost deleted the dev/cazamor/spec/tsm-actions branch May 5, 2021 04:49
carlos-zamora added a commit that referenced this pull request May 5, 2021
This entirely removes `KeyMapping` from 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::FromJson` to simply iterate over each JSON action item, deserialize it, and add it to our `ActionMap`.

Internally, our `ActionMap` operates as discussed in #9428 by maintaining a `_KeyMap` that points to an action ID, and using that action ID to retrieve the `Command` from the `_ActionMap`. Adding actions to the `ActionMap` automatically accounts for name/key-chord collisions. A `NameMap` can be constructed when requested; this is for the Command Palette.

Querying the `ActionMap` is 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 as `ShortcutAction::Invalid` commands. However, we return `nullptr` when 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 expose `NameMap` as a consolidation of `ActionMap`'s `NameMap` with its parents. The same can be said for exposing key chords in nested commands.

## Validation Steps Performed

All local tests pass.
ghost pushed a commit that referenced this pull request May 18, 2021
## 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
![Actions Page Demo](https://user-images.githubusercontent.com/11050425/116034988-131d1b80-a619-11eb-8df2-c7e57c6fad86.gif)
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Settings Issues related to settings and customizability, for console or terminal AutoMerge Marked for automatic merge by the bot when requirements are met Issue-Docs It's a documentation issue that really should be on MicrosoftDocs/Console-Docs Product-Terminal The new Windows Terminal.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants