good lord

This commit is contained in:
Leon Liang 2021-09-22 14:12:34 -07:00
parent d493f6e551
commit b278f2faf8
8 changed files with 212 additions and 140 deletions

View file

@ -115,8 +115,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// These are special cases.
// - QuakeMode: deserializes into a GlobalSummon, so we don't need a serializer
// - Invalid: has no args
// - ExplicitlyUnbound: specifically for the "unbound" case.
{ ShortcutAction::QuakeMode, { GlobalSummonArgs::QuakeModeFromJson, nullptr } },
{ ShortcutAction::Invalid, { nullptr, nullptr } },
{ ShortcutAction::Unbound, { nullptr, nullptr } },
#define ON_ALL_ACTIONS_WITH_ARGS(action) ACTION_TO_SERIALIZERS_PAIR(action)
ALL_SHORTCUT_ACTIONS_WITH_ARGS
@ -161,6 +163,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// - The ShortcutAction corresponding to the given string, if a match exists.
static ShortcutAction GetActionFromString(const std::string_view actionString)
{
if (actionString == UnboundKey)
{
return ShortcutAction::Unbound;
}
// Try matching the command to one we have. If we can't find the
// action name in our list of names, let's just unbind that key.
const auto found = ActionAndArgs::ActionKeyNamesMap.find(actionString);
@ -306,6 +313,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return copy;
}
bool ActionAndArgs::IsUnbound() const
{
return _Action == ShortcutAction::Invalid || _Action == ShortcutAction::Unbound;
}
winrt::hstring ActionAndArgs::GenerateName() const
{
// Use a magic static to initialize this map, because we won't be able

View file

@ -26,6 +26,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
hstring GenerateName() const;
bool IsUnbound() const;
WINRT_PROPERTY(ShortcutAction, Action, ShortcutAction::Invalid);
WINRT_PROPERTY(IActionArgs, Args, nullptr);
};

View file

@ -444,6 +444,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
if (!name.empty())
{
_NestedCommands.emplace(name, cmd);
if (!cmd.ExternalID().empty())
{
_ExternalIDToSpecialCommands.emplace(cmd.ExternalID(), cmd);
}
}
return;
}
@ -452,6 +457,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
if (cmdImpl->IterateOn() != ExpandCommandType::None)
{
_IterableCommands.emplace_back(cmd);
if (!cmd.ExternalID().empty())
{
_ExternalIDToSpecialCommands.emplace(cmd.ExternalID(), cmd);
}
return;
}
@ -476,11 +487,55 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// _TryUpdateActionMap may update oldCmd and maskingCmd
Model::Command oldCmd{ nullptr };
Model::Command maskingCmd{ nullptr };
_TryUpdateActionMap(cmd, oldCmd, maskingCmd);
_TryUpdateExternalID(cmd, oldCmd, maskingCmd);
_TryUpdateName(cmd, oldCmd, maskingCmd);
_TryUpdateKeyChord(cmd, oldCmd, maskingCmd);
// _TryUpdateActionWithID may update mergedCmd
// that should be passed through the rest of the update functions.
Model::Command mergedCmd{ cmd };
if (!cmd.ExternalID().empty())
{
// Try to find a use of the ID in our layer, then in our parents.
// If found in our layer, set it to oldCmd. If found in parents,
// set it to maskingCmd.
const auto& externalPair = _ExternalIDMap.find(cmd.ExternalID());
if (externalPair != _ExternalIDMap.end())
{
oldCmd = _ActionMap.find(externalPair->second)->second;
}
else
{
// The user is referring to an action defined in another layer.
// We should make a copy of that action, apply the incoming action
// on top of it (replace action, name, etc.), then re-add the action
for (const auto& parent : _parents)
{
auto parentAction = parent->GetActionByExternalID(cmd.ExternalID());
if (parentAction && *parentAction)
{
// We've found an action in a parent with the ID, so we want to update
// that parent action with whatever we've got in our command.
// In order to update, we'll make a copy of the parent action, layer
// our command on top of it, and re-add it to the parent layer.
const auto parentActionImpl{ get_self<implementation::Command>(*parentAction) };
const auto parentActionCopy{ parentActionImpl->Copy() };
parentActionCopy->LayerCommand(cmd);
parent->DeleteAction(Hash(parentAction->ActionAndArgs()));
parent->AddAction(*parentActionCopy);
mergedCmd = *parentActionCopy;
break;
}
}
_ExternalIDMap.emplace(cmd.ExternalID(), Hash(mergedCmd.ActionAndArgs()));
}
}
_TryUpdateActionMap(mergedCmd, oldCmd, maskingCmd);
_TryUpdateExternalID(mergedCmd, oldCmd, maskingCmd);
_TryUpdateName(mergedCmd, oldCmd, maskingCmd);
_TryUpdateKeyChord(mergedCmd, oldCmd, maskingCmd);
}
// Method Description:
@ -496,35 +551,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
const auto actionID{ Hash(cmd.ActionAndArgs()) };
const auto& actionPair{ _ActionMap.find(actionID) };
if (!cmd.ExternalID().empty())
// Example:
// { "command": "copy", "keys": "ctrl+c" } --> add the action in for the first time
// { "command": "copy", "keys": "ctrl+shift+c" } --> update oldCmd
if (!oldCmd)
{
const auto& externalPair = _ExternalIDMap.find(cmd.ExternalID());
if (externalPair == _ExternalIDMap.end())
{
_ExternalIDMap.emplace(cmd.ExternalID(), actionID);
if (actionPair == _ActionMap.end())
{
// add this action in for the first time
_ActionMap.emplace(actionID, cmd);
}
else
{
// We're adding an action that already exists in our layer.
// Record it so that we update it with any new information.
oldCmd = actionPair->second;
}
}
else
{
oldCmd = _ActionMap.find(externalPair->second)->second;
}
}
else
{
// Example:
// { "command": "copy", "keys": "ctrl+c" } --> add the action in for the first time
// { "command": "copy", "keys": "ctrl+shift+c" } --> update oldCmd
if (actionPair == _ActionMap.end())
{
// add this action in for the first time
@ -553,37 +584,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
FAIL_FAST_IF(_parents.size() > 1);
for (const auto& parent : _parents)
{
if (!cmd.ExternalID().empty())
// NOTE: This only checks the layer above us, but that's ok.
// If we had to find one from a layer above that, parent->_MaskingActions
// would have found it, so we inherit it for free!
auto inheritedCmd{ parent->_GetActionByID(actionID) };
if (inheritedCmd && *inheritedCmd)
{
auto inheritedCmdWithID{ parent->GetActionByExternalID(cmd.ExternalID()) };
if (inheritedCmdWithID && *inheritedCmdWithID)
{
// We've found an action in a parent layer that has the same external
// ID as our current layer action. We'll need to overwrite the parent's info.
// Update the parent's action with the incoming action.
parent->_TryUpdateAction(Hash(inheritedCmdWithID->ActionAndArgs()), cmd);
auto newParentAction{ parent->_GetActionByID(actionID) };
if (newParentAction && *newParentAction)
{
maskingCmd = *newParentAction;
_MaskingActions.emplace(actionID, maskingCmd);
}
}
}
else
{
// NOTE: This only checks the layer above us, but that's ok.
// If we had to find one from a layer above that, parent->_MaskingActions
// would have found it, so we inherit it for free!
auto inheritedCmd{ parent->_GetActionByID(actionID) };
if (inheritedCmd && *inheritedCmd)
{
const auto& inheritedCmdImpl{ get_self<Command>(*inheritedCmd) };
maskingCmd = *inheritedCmdImpl->Copy();
_MaskingActions.emplace(actionID, maskingCmd);
}
const auto& inheritedCmdImpl{ get_self<Command>(*inheritedCmd) };
maskingCmd = *inheritedCmdImpl->Copy();
_MaskingActions.emplace(actionID, maskingCmd);
}
}
}
@ -595,59 +604,43 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
}
void ActionMap::_StashIncompleteActions(const Model::Command& cmd)
void ActionMap::_StashExternalIDActions(const Model::Command& cmd)
{
// Stash commands with external IDs so we can combine them
// and AddAction when we know there's no more declarations using the ID.
// As we continue to come across action definitions with external
// IDs defined, we'll merge the action info into one object.
// We'll always take the latest name and command, and we'll register
// all the keys that are defined to this action.
//
// Once we've finished parsing the actions array for the layer,
// we'll iterate through our resulting actions and add them
// to the action map.
//
// Example:
// {
// "id": "customaction",
// "keys": "ctrl+shift+g"
// },
// {
// "id": "customaction",
// "name": "customaction"
// },
// {
// "id": "customaction",
// "command": "copy"
// }
// { "id": "customaction", "keys": "ctrl+shift+g"},
// { "id": "customaction", "name": "customaction"},
// { "id": "customaction", "command": "copy"}, --> Results in {"id": "customaction", "command": "copy", "keys": "ctrl+shift+g", "name": "customaction"}
// { "id": "customaction", "keys": "ctrl+v"},
// { "id": "customaction", "command": "paste"} --> Results in {"id": "customaction", "command": "paste", "keys": {"ctrl+shift+g", "ctrl+v"}, "name": "customaction"}
//
if (!cmd.ExternalID().empty())
{
std::wstring externalID{ cmd.ExternalID() };
if (externalID == L"")
{
}
// Add it to incomplete, or combine it with an existing incomplete.
auto incompletePair{ _ExternalIDStaging.find(cmd.ExternalID()) };
if (incompletePair == _ExternalIDStaging.end())
auto stagingPair{ _ExternalIDStaging.find(cmd.ExternalID()) };
if (stagingPair == _ExternalIDStaging.end())
{
_ExternalIDStaging.emplace(cmd.ExternalID(), cmd);
}
else
{
const auto incompleteImpl{ get_self<Command>(incompletePair->second) };
const auto cmdImpl{ get_self<Command>(cmd) };
if (cmdImpl->HasName())
{
incompleteImpl->Name(cmdImpl->Name());
}
if (cmdImpl->Keys())
{
incompleteImpl->RegisterKey(cmdImpl->Keys());
}
if ((incompleteImpl->ActionAndArgs().Action() == ShortcutAction::Invalid) ||
(cmd.ActionAndArgs().Action() != ShortcutAction::Invalid))
{
incompleteImpl->ActionAndArgs(cmd.ActionAndArgs());
}
// Merge the incoming cmd with the one found in staging.
// We'll overwrite the staging cmd's values if the incoming cmd has those values.
const auto incompleteImpl{ get_self<Command>(stagingPair->second) };
incompleteImpl->LayerCommand(cmd);
}
}
}
void ActionMap::_AddIncompleteActions()
void ActionMap::_AddStashedActions()
{
for (const auto& [id, cmd]: _ExternalIDStaging)
{
@ -656,29 +649,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_ExternalIDStaging.clear();
}
void ActionMap::_TryUpdateAction(const InternalActionID actionID, const Model::Command& action)
{
const auto& newActionHash{ Hash(action.ActionAndArgs()) };
const auto& actionPair{ _ActionMap.find(actionID) };
if (actionPair != _ActionMap.end())
{
// ActionMap update
auto actionMapNode = _ActionMap.extract(actionID);
actionMapNode.key() = newActionHash;
_ActionMap.insert(std::move(actionMapNode));
// KeyMap update
const auto& actionImpl = get_self<implementation::Command>(*_GetActionByID(newActionHash));
for (const auto& keychord : actionImpl->KeyMappings())
{
_KeyMap.insert_or_assign(keychord, newActionHash);
}
// ExternalIDMap update
_ExternalIDMap.insert_or_assign(action.ExternalID(), newActionHash);
}
}
void ActionMap::_TryUpdateExternalID(const Model::Command& cmd, Model::Command& oldCmd, Model::Command& maskingCmd)
{
if (cmd.ExternalID().empty())
@ -975,16 +945,52 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
std::optional<Model::Command> ActionMap::GetActionByExternalID(const winrt::hstring& externalID) const
{
const auto idMapPair{ _ExternalIDMap.find(externalID) };
const auto specialMapPair{ _ExternalIDToSpecialCommands.find(externalID) };
if (idMapPair != _ExternalIDMap.end())
{
// Really there should be no scenario where an ExternalID mapping to InternalID
// mapping exists but the InternalID doesn't have an action associated to it right?
return _GetActionByID(idMapPair->second);
}
else if (specialMapPair != _ExternalIDToSpecialCommands.end())
{
return specialMapPair->second;
}
return nullptr;
}
void ActionMap::DeleteAction(const InternalActionID& id)
{
if (auto action = _GetActionByID(id))
{
// Removing from KeyMap
const auto& actionImpl = get_self<implementation::Command>(*action);
for (const auto& keychord : actionImpl->KeyMappings())
{
// Double check that the keychord of this command is mapped to itself.
auto keyMapPair = _KeyMap.find(keychord);
if (keyMapPair != _KeyMap.end())
{
if (keyMapPair->second == id)
{
_KeyMap.erase(keychord);
}
}
}
// Removing from ExternalIDMap
if (!action->ExternalID().empty())
{
_ExternalIDMap.erase(action->ExternalID());
}
// Remove from ActionMap
_ActionMap.erase(id);
// TODO: How will I remove it if it's Nested/Iterable commands?
}
}
// Method Description:
// - Rebinds a key binding to a new key chord
// Arguments:

View file

@ -73,6 +73,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value ToJson() const;
// modification
void DeleteAction(const InternalActionID& id);
bool RebindKeys(Control::KeyChord const& oldKeys, Control::KeyChord const& newKeys);
void DeleteKeyBinding(Control::KeyChord const& keys);
void RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action);
@ -92,9 +93,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _TryUpdateName(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd);
void _TryUpdateKeyChord(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd);
void _TryUpdateExternalID(const Model::Command& cmd, Model::Command& oldCmd, Model::Command& consolidatedCmd);
void _TryUpdateAction(const InternalActionID oldActionID, const Model::Command& action);
void _AddIncompleteActions();
void _StashIncompleteActions(const Model::Command& cmd);
void _AddStashedActions();
void _StashExternalIDActions(const Model::Command& cmd);
Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionsCache{ nullptr };
Windows::Foundation::Collections::IMap<hstring, Model::Command> _NameMapCache{ nullptr };
@ -106,6 +106,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
std::unordered_map<Control::KeyChord, InternalActionID, KeyChordHash, KeyChordEquality> _KeyMap;
std::unordered_map<InternalActionID, Model::Command> _ActionMap;
std::unordered_map<winrt::hstring, InternalActionID> _ExternalIDMap;
std::unordered_map<winrt::hstring, Model::Command> _ExternalIDToSpecialCommands;
std::unordered_map<winrt::hstring, Model::Command> _ExternalIDStaging;
// Masking Actions:

View file

@ -54,7 +54,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
if (!cmd->ExternalID().empty())
{
_StashIncompleteActions(*cmd);
_StashExternalIDActions(*cmd);
}
else
{
@ -62,7 +62,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
}
_AddIncompleteActions();
_AddStashedActions();
return warnings;
}

View file

@ -67,6 +67,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return _subcommands ? _subcommands.GetView() : nullptr;
}
void Command::NestedCommands(const IMapView<winrt::hstring, Model::Command>& commands)
{
_subcommands.Clear();
for (auto kv : commands)
{
const auto subCmd{ winrt::get_self<Command>(kv.Value()) };
_subcommands.Insert(kv.Key(), *subCmd->Copy());
}
}
// Function Description:
// - reports if the current command has nested commands
// - This CANNOT detect { "name": "foo", "commands": null }
@ -255,6 +265,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
auto result = winrt::make_self<Command>();
if (const auto idJson{ json[JsonKey(IdKey)] })
{
if (idJson.isString())
{
result->ExternalID(to_hstring(idJson.asCString()));
}
}
bool nested = false;
JsonUtils::GetValueForKey(json, IterateOnKey, result->_IterateOn);
@ -293,14 +311,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// If we're a nested command, we can ignore the current action.
if (!nested)
{
if (const auto idJson{ json[JsonKey(IdKey)] })
{
if (idJson.isString())
{
result->ExternalID(to_hstring(idJson.asCString()));
}
}
if (const auto actionJson{ json[JsonKey(ActionKey)] })
{
result->_ActionAndArgs = *ActionAndArgs::FromJson(actionJson, warnings);
@ -646,4 +656,38 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return newCommands;
}
void Command::LayerCommand(const Model::Command& cmd)
{
// Merge the incoming cmd with the one found in staging.
// We'll overwrite the staging cmd's values if the incoming cmd has those values.
const auto cmdImpl{ get_self<Command>(cmd) };
if (cmdImpl->HasName())
{
Name(cmdImpl->Name());
}
for (const auto& keys : cmdImpl->KeyMappings())
{
RegisterKey(keys);
}
if (!cmdImpl->IconPath().empty())
{
IconPath(cmd.IconPath());
}
// Copy over the nested commands if there are some.
// Otherwise just copy the ActionAndArgs if they're valid.
IterateOn(cmdImpl->IterateOn());
if (cmdImpl->HasNestedCommands())
{
NestedCommands(cmd.NestedCommands());
}
if (cmd.ActionAndArgs().Action() != ShortcutAction::Invalid)
{
ActionAndArgs(cmd.ActionAndArgs());
}
}
}

View file

@ -38,6 +38,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
Command();
com_ptr<Command> Copy() const;
void Merge(const com_ptr<Command>& cmd);
static winrt::com_ptr<Command> FromJson(const Json::Value& json,
std::vector<SettingsLoadWarnings>& warnings);
@ -54,6 +55,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
bool HasNestedCommands() const;
bool IsNestedCommand() const noexcept;
Windows::Foundation::Collections::IMapView<winrt::hstring, Model::Command> NestedCommands() const;
void NestedCommands(const Windows::Foundation::Collections::IMapView<winrt::hstring, Model::Command>& commands);
bool HasName() const noexcept;
hstring Name() const noexcept;
@ -68,6 +70,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
hstring IconPath() const noexcept;
void IconPath(const hstring& val);
void LayerCommand(const Model::Command& cmd);
winrt::Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker propertyChangedRevoker;
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);

View file

@ -12,7 +12,8 @@ namespace Microsoft.Terminal.Settings.Model
{
enum ShortcutAction
{
Invalid = 0, // treat Invalid as unbound actions
Invalid = 0,
Unbound = 1,
// When adding a new action, add them to AllShortcutActions.h!
#define ON_ALL_ACTIONS(action) action,
@ -26,6 +27,8 @@ namespace Microsoft.Terminal.Settings.Model
IActionArgs Args;
ShortcutAction Action;
Boolean IsUnbound();
};
[default_interface] runtimeclass Command : Windows.UI.Xaml.Data.INotifyPropertyChanged