Skip to content

Commit

Permalink
midpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
PankajBhojwani committed Apr 23, 2024
1 parent ca3eb87 commit 85933e2
Show file tree
Hide file tree
Showing 13 changed files with 443 additions and 20 deletions.
170 changes: 170 additions & 0 deletions src/cascadia/TerminalSettingsModel/ActionMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return std::nullopt;
}

std::optional<Model::Command> ActionMap::_GetActionByID2(const winrt::hstring actionID) const
{
// todo: check this
// Check current layer
const auto actionMapPair{ _ActionMap2.find(actionID) };
if (actionMapPair != _ActionMap2.end())
{
auto& cmd{ actionMapPair->second };

// ActionMap should never point to nullptr
FAIL_FAST_IF_NULL(cmd);

return !cmd.HasNestedCommands() && cmd.ActionAndArgs().Action() == ShortcutAction::Invalid ?
nullptr : // explicitly unbound
cmd;
}

// todo: we should check parents here right

// We don't have an answer
return std::nullopt;
}

static void RegisterShortcutAction(ShortcutAction shortcutAction, std::unordered_map<hstring, Model::ActionAndArgs>& list, std::unordered_set<InternalActionID>& visited)
{
const auto actionAndArgs{ make_self<ActionAndArgs>(shortcutAction) };
Expand Down Expand Up @@ -170,6 +193,38 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
}

void ActionMap::_PopulateAvailableActionsWithStandardCommands2(std::unordered_map<hstring, Model::ActionAndArgs>& availableActions, std::unordered_set<winrt::hstring>& visitedActionIDs) const
{
// todo: check this
// Update AvailableActions and visitedActionIDs with our current layer
for (const auto& [actionID, cmd] : _ActionMap2)
{
if (cmd.ActionAndArgs().Action() != ShortcutAction::Invalid)
{
// Only populate AvailableActions with actions that haven't been visited already.
if (visitedActionIDs.find(actionID) == visitedActionIDs.end())
{
const auto& name{ cmd.Name() };
if (!name.empty())
{
// Update AvailableActions.
const auto actionAndArgsImpl{ get_self<ActionAndArgs>(cmd.ActionAndArgs()) };
availableActions.insert_or_assign(name, *actionAndArgsImpl->Copy());
}

// Record that we already handled adding this action to the NameMap.
visitedActionIDs.insert(actionID);
}
}
}

// Update NameMap and visitedActionIDs with our parents
for (const auto& parent : _parents)
{
parent->_PopulateAvailableActionsWithStandardCommands2(availableActions, visitedActionIDs);
}
}

// Method Description:
// - Retrieves a map of command names to the commands themselves
// - These commands should not be modified directly because they may result in
Expand Down Expand Up @@ -285,6 +340,31 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return cumulativeActions;
}

// Method Description:
// - Provides an accumulated list of actions that are exposed. The accumulated list includes actions added in this layer, followed by actions added by our parents.
std::vector<Model::Command> ActionMap::_GetCumulativeActions2() const noexcept
{
// check this
// First, add actions from our current layer
std::vector<Model::Command> cumulativeActions2;
cumulativeActions2.reserve(_ActionMap2.size());

// masking actions have priority. Actions here are constructed from consolidating an inherited action with changes we've found when populating this layer.
std::transform(_ActionMap2.begin(), _ActionMap2.end(), std::back_inserter(cumulativeActions2), [](std::pair<winrt::hstring, Model::Command> actionPair) {
return actionPair.second;
});

// Now, add the accumulated actions from our parents
for (const auto& parent : _parents)
{
const auto parentActions{ parent->_GetCumulativeActions() };
cumulativeActions2.reserve(cumulativeActions2.size() + parentActions.size());
cumulativeActions2.insert(cumulativeActions2.end(), parentActions.begin(), parentActions.end());
}

return cumulativeActions2;
}

IMapView<Control::KeyChord, Model::Command> ActionMap::GlobalHotkeys()
{
if (!_GlobalHotkeysCache)
Expand Down Expand Up @@ -469,12 +549,18 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// to keep track of what key mappings are still valid.

// _TryUpdateActionMap may update oldCmd and maskingCmd

Model::Command oldCmd{ nullptr };
Model::Command maskingCmd{ nullptr };
_TryUpdateActionMap(cmd, oldCmd, maskingCmd);

_TryUpdateName(cmd, oldCmd, maskingCmd);
_TryUpdateKeyChord(cmd, oldCmd, maskingCmd);

_TryUpdateActionMap2(cmd);
// I don't think we need a _TryUpdateName with the new implementation?
// we might still need it for legacy case...
_TryUpdateKeyChord2(cmd);
}

// Method Description:
Expand Down Expand Up @@ -538,6 +624,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
}

void ActionMap::_TryUpdateActionMap2(const Model::Command& cmd)
{
// todo: check this
// Example:
// { "command": "copy", "id": "User.MyAction" } --> add the action in for the first time
// { "command": "paste", "id": "User.MyAction" } --> overwrite the "User.MyAction" command

// only add to the _ActionMap if there is an ID and the shortcut action is valid
// (if the shortcut action is invalid, then this is for unbinding and _TryUpdateKeyChord will handle that)
if (auto cmdID = cmd.ID(); !cmdID.empty() && cmd.ActionAndArgs().Action() != ShortcutAction::Invalid)
{
// any existing command with the same id in this layer will get overwritten
_ActionMap2.insert_or_assign(cmdID, cmd);
}
}

// Method Description:
// - Update our internal state with the name of the newly registered action
// Arguments:
Expand Down Expand Up @@ -686,6 +788,59 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
}

void ActionMap::_TryUpdateKeyChord2(const Model::Command& cmd)
{
// Example (this is a legacy case, where the keys are provided in the same block as the command):
// { "command": "copy", "keys": "ctrl+c" } --> we are registering a new key chord
// { "name": "foo", "command": "copy" } --> no change to keys, exit early
const auto keys{ cmd.Keys() };
if (!keys)
{
// the user is not trying to update the keys.
return;
}

// Handle collisions
const auto oldKeyPair{ _KeyMap2.find(keys) };
if (oldKeyPair != _KeyMap2.end())
{
// Collision: The key chord was already in use.
//
// Example:
// { "command": "copy", "keys": "ctrl+c" } --> register "ctrl+c" (different branch)
// { "command": "paste", "keys": "ctrl+c" } --> Collision! (this branch)
//
// Remove the old one. (unbind "copy" in the example above)

// if oldKeyPair->second is empty, that means this keychord was unbound and is now being rebound
// no collision logic needed - we will simply reassign it in the _KeyMap
if (!oldKeyPair->second.empty())
{
const auto actionPair{ _ActionMap2.find(oldKeyPair->second) };
const auto conflictingCmd{ actionPair->second };
const auto conflictingCmdImpl{ get_self<implementation::Command>(conflictingCmd) };
conflictingCmdImpl->EraseKey(keys);
}
}

// Assign the new action in the _KeyMap
// However, there's a strange edge case here - since we're parsing a legacy or modern block,
// the user might have { "command": null, "id": "someID", "keys": "ctrl+c" }
// i.e. they provided an ID for a null command
// in this case, we do _not_ want to use the id they provided, we want to use an empty id
// (empty id in the _KeyMap indicates the the keychord was explicitly unbound)
if (cmd.ActionAndArgs().Action() == ShortcutAction::Invalid)
{
_KeyMap2.insert_or_assign(keys, L"");
}
else
{
_KeyMap2.insert_or_assign(keys, cmd.ID());
}

cmd.RegisterKey(keys);
}

// Method Description:
// - Determines whether the given key chord is explicitly unbound
// Arguments:
Expand Down Expand Up @@ -747,6 +902,21 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return std::nullopt;
}

// Method Description:
// - Retrieves the assigned command with the given key chord.
// - Can return nullopt to differentiate explicit unbinding vs lack of binding.
// Arguments:
// - keys: the key chord of the command to search for
// Return Value:
// - the command with the given key chord
// - nullptr if the key chord is explicitly unbound
// - nullopt if it was not bound in this layer
std::optional<Model::Command> ActionMap::_GetActionByKeyChordInternal2(const Control::KeyChord& /*keys*/) const
{
// todo: complete this
return std::nullopt;
}

// Method Description:
// - Retrieves the key chord for the provided action
// Arguments:
Expand Down
27 changes: 11 additions & 16 deletions src/cascadia/TerminalSettingsModel/ActionMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Author(s):
#include "ActionMap.g.h"
#include "IInheritable.h"
#include "Command.h"
#include "KeysMap.h"

// fwdecl unittest classes
namespace SettingsModelUnitTests
Expand All @@ -31,22 +32,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
using InternalActionID = size_t;

struct KeyChordHash
{
inline std::size_t operator()(const Control::KeyChord& key) const
{
return static_cast<size_t>(key.Hash());
}
};

struct KeyChordEquality
{
inline bool operator()(const Control::KeyChord& lhs, const Control::KeyChord& rhs) const
{
return lhs.Equals(rhs);
}
};

struct ActionMap : ActionMapT<ActionMap>, IInheritable<ActionMap>
{
// views
Expand Down Expand Up @@ -84,14 +69,18 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation

private:
std::optional<Model::Command> _GetActionByID(const InternalActionID actionID) const;
std::optional<Model::Command> _GetActionByID2(const winrt::hstring actionID) const;
std::optional<Model::Command> _GetActionByKeyChordInternal(const Control::KeyChord& keys) const;
std::optional<Model::Command> _GetActionByKeyChordInternal2(const Control::KeyChord& keys) const;

void _RefreshKeyBindingCaches();
void _PopulateAvailableActionsWithStandardCommands(std::unordered_map<hstring, Model::ActionAndArgs>& availableActions, std::unordered_set<InternalActionID>& visitedActionIDs) const;
void _PopulateAvailableActionsWithStandardCommands2(std::unordered_map<hstring, Model::ActionAndArgs>& availableActions, std::unordered_set<winrt::hstring>& visitedActionIDs) const;
void _PopulateNameMapWithSpecialCommands(std::unordered_map<hstring, Model::Command>& nameMap) const;
void _PopulateNameMapWithStandardCommands(std::unordered_map<hstring, Model::Command>& nameMap) const;
void _PopulateKeyBindingMapWithStandardCommands(std::unordered_map<Control::KeyChord, Model::Command, KeyChordHash, KeyChordEquality>& keyBindingsMap, std::unordered_set<Control::KeyChord, KeyChordHash, KeyChordEquality>& unboundKeys) const;
std::vector<Model::Command> _GetCumulativeActions() const noexcept;
std::vector<Model::Command> _GetCumulativeActions2() const noexcept;

void _TryUpdateActionMap(const Model::Command& cmd, Model::Command& oldCmd, Model::Command& consolidatedCmd);
void _TryUpdateName(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd);
Expand Down Expand Up @@ -124,6 +113,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// than is necessary to be serialized.
std::unordered_map<InternalActionID, Model::Command> _MaskingActions;

void _AddKeyBindingHelper(const Json::Value& json, std::vector<SettingsLoadWarnings>& warnings);
void _TryUpdateActionMap2(const Model::Command& cmd);
void _TryUpdateKeyChord2(const Model::Command& cmd);
std::unordered_map<Control::KeyChord, winrt::hstring, KeyChordHash, KeyChordEquality> _KeyMap2;
std::unordered_map<winrt::hstring, Model::Command> _ActionMap2;

friend class SettingsModelUnitTests::KeyBindingsTests;
friend class SettingsModelUnitTests::DeserializationTests;
friend class SettingsModelUnitTests::TerminalSettingsTests;
Expand Down
76 changes: 73 additions & 3 deletions src/cascadia/TerminalSettingsModel/ActionMapSerialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// - json: an array of Json::Value's to deserialize into our ActionMap.
// Return value:
// - a list of warnings encountered while deserializing the json
// todo: update this description
std::vector<SettingsLoadWarnings> ActionMap::LayerJson(const Json::Value& json, const OriginTag origin, const bool withKeybindings)
{
// It's possible that the user provided keybindings have some warnings in
Expand All @@ -43,14 +44,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// settings phase, so we'll collect them now.
std::vector<SettingsLoadWarnings> warnings;

for (const auto& cmdJson : json)
for (const auto& jsonBlock : json)
{
if (!cmdJson.isObject())
if (!jsonBlock.isObject())
{
continue;
}

AddAction(*Command::FromJson(cmdJson, warnings, origin, withKeybindings));
// the json block may be 1 of 3 things:
// - the legacy style command block, that has the action, args and keys in it
// - the modern style command block, that has the action, args and an ID
// - the modern style keys block, that has the keys and an ID

// if the block contains a "command" field, it is either a legacy or modern style command block
// and we can call Command::FromJson on it (Command::FromJson can handle parsing both legacy or modern)

// if there is no "command" field, then it is a modern style keys block
// todo: use the CommandsKey / ActionKey static string view in Command.cpp somehow
if (jsonBlock.isMember(JsonKey("commands")) || jsonBlock.isMember(JsonKey("command")))
{
AddAction(*Command::FromJson(jsonBlock, warnings, origin, withKeybindings));
}
else
{
_AddKeyBindingHelper(jsonBlock, warnings);
}
// todo: need to have a flag for fixups applied during load
}

return warnings;
Expand All @@ -77,6 +96,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
};

// Serialize all standard Command objects in the current layer
// todo: change to _NewActionMap
for (const auto& [_, cmd] : _ActionMap)
{
toJson(cmd);
Expand All @@ -96,4 +116,54 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation

return actionList;
}

void ActionMap::_AddKeyBindingHelper(const Json::Value& json, std::vector<SettingsLoadWarnings>& warnings)
{
// There should always be a "keys" field
// - If there is also an "id" field - we add the pair to our _KeyMap
// - If there is no "id" field - this is an explicit unbinding, still add it to the _KeyMap,
// when this key chord is queried for we will know it is an explicit unbinding
// todo: use the KeysKey and IDKey static strings from Command.cpp
const auto keysJson{ json[JsonKey("keys")] };
if (keysJson.isArray() && keysJson.size() > 1)
{
warnings.push_back(SettingsLoadWarnings::TooManyKeysForChord);
}
else
{
Control::KeyChord keys{ nullptr };
winrt::hstring idJson;
if (JsonUtils::GetValueForKey(json, "keys", keys))
{
// even if the "id" field doesn't exist in the json, idJson will be an empty string which is fine
JsonUtils::GetValueForKey(json, "id", idJson);

// any existing keybinding with the same keychord in this layer will get overwritten
_KeyMap2.insert_or_assign(keys, idJson);

// if there is an id, make sure the command registers these keys
if (!idJson.empty())
{
const auto& cmd{ _GetActionByID2(idJson) };
if (cmd && *cmd)
{
cmd->RegisterKey(keys);
}
else
{
// check for the same ID among our parents
for (const auto& parent : _parents)
{
const auto& inheritedCmd{ parent->_GetActionByID2(idJson) };
if (inheritedCmd && *inheritedCmd)
{
inheritedCmd->RegisterKey(keys);
}
}
}
}
}
}
return;
}
}
Loading

0 comments on commit 85933e2

Please sign in to comment.