mirror of
https://github.com/microsoft/terminal.git
synced 2026-02-04 03:05:08 -06:00
866 lines
34 KiB
C++
866 lines
34 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "Command.h"
|
|
#include "Command.g.cpp"
|
|
|
|
#include <til/replace.h>
|
|
#include <ScopedResourceLoader.h>
|
|
|
|
#include "KeyChordSerialization.h"
|
|
|
|
using namespace winrt::Microsoft::Terminal::Settings::Model;
|
|
using namespace winrt::Windows::Foundation::Collections;
|
|
using namespace ::Microsoft::Terminal::Settings::Model;
|
|
|
|
namespace winrt
|
|
{
|
|
namespace MUX = Microsoft::UI::Xaml;
|
|
namespace WUX = Windows::UI::Xaml;
|
|
}
|
|
|
|
static constexpr std::string_view ProfileNameToken{ "${profile.name}" };
|
|
static constexpr std::string_view ProfileIconToken{ "${profile.icon}" };
|
|
static constexpr std::string_view SchemeNameToken{ "${scheme.name}" };
|
|
|
|
template<>
|
|
struct Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<winrt::Microsoft::Terminal::Settings::Model::implementation::Command::CommandNameOrResource>
|
|
{
|
|
winrt::Microsoft::Terminal::Settings::Model::implementation::Command::CommandNameOrResource FromJson(const Json::Value& json)
|
|
{
|
|
if (json.isObject())
|
|
{
|
|
if (const auto resourceKey{ JsonUtils::GetValueForKey<std::optional<std::wstring>>(json, "key") })
|
|
{
|
|
if (HasLibraryResourceWithName(*resourceKey))
|
|
{
|
|
return { .resource = *resourceKey };
|
|
}
|
|
}
|
|
}
|
|
else if (json.isString())
|
|
{
|
|
return { .name = JsonUtils::GetValue<std::wstring>(json) };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
bool CanConvert(const Json::Value& json)
|
|
{
|
|
if (json.isObject())
|
|
{
|
|
if (const auto resourceKey{ JsonUtils::GetValueForKey<std::optional<std::wstring>>(json, "key") })
|
|
{
|
|
return HasLibraryResourceWithName(*resourceKey);
|
|
}
|
|
}
|
|
return json.isString() || json.isNull();
|
|
}
|
|
|
|
Json::Value ToJson(const winrt::Microsoft::Terminal::Settings::Model::implementation::Command::CommandNameOrResource& val)
|
|
{
|
|
if (!val.resource.empty())
|
|
{
|
|
Json::Value json{ Json::objectValue };
|
|
SetValueForKey(json, "key", val.resource);
|
|
return json;
|
|
}
|
|
else if (!val.name.empty())
|
|
{
|
|
return til::u16u8(val.name);
|
|
}
|
|
return Json::Value::nullSingleton();
|
|
}
|
|
|
|
std::string TypeDescription() const
|
|
{
|
|
return "string or valid resource";
|
|
}
|
|
};
|
|
|
|
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|
{
|
|
Command::Command() = default;
|
|
|
|
Model::Command Command::NewUserCommand()
|
|
{
|
|
auto newCmd{ winrt::make_self<Command>() };
|
|
newCmd->_Origin = OriginTag::User;
|
|
return *newCmd;
|
|
}
|
|
|
|
Model::Command Command::CopyAsUserCommand(const Model::Command& originalCmd)
|
|
{
|
|
auto command{ winrt::get_self<Command>(originalCmd) };
|
|
auto copy{ command->Copy() };
|
|
copy->_Origin = OriginTag::User;
|
|
return *copy;
|
|
}
|
|
|
|
com_ptr<Command> Command::Copy() const
|
|
{
|
|
auto command{ winrt::make_self<Command>() };
|
|
command->_name = _name;
|
|
command->_Origin = _Origin;
|
|
command->_ID = _ID;
|
|
command->_ActionAndArgs = *get_self<implementation::ActionAndArgs>(_ActionAndArgs)->Copy();
|
|
command->_icon = _icon;
|
|
command->_IterateOn = _IterateOn;
|
|
command->_Description = _Description;
|
|
|
|
command->_originalJson = _originalJson;
|
|
command->_nestedCommand = _nestedCommand;
|
|
if (HasNestedCommands())
|
|
{
|
|
command->_subcommands = winrt::single_threaded_map<winrt::hstring, Model::Command>();
|
|
for (auto kv : NestedCommands())
|
|
{
|
|
const auto subCmd{ winrt::get_self<Command>(kv.Value()) };
|
|
command->_subcommands.Insert(kv.Key(), *subCmd->Copy());
|
|
}
|
|
}
|
|
return command;
|
|
}
|
|
|
|
IMapView<winrt::hstring, Model::Command> Command::NestedCommands() const
|
|
{
|
|
return _subcommands ? _subcommands.GetView() : nullptr;
|
|
}
|
|
|
|
void Command::NestedCommands(const Windows::Foundation::Collections::IVectorView<Model::Command>& nested)
|
|
{
|
|
_subcommands = winrt::single_threaded_map<winrt::hstring, Model::Command>();
|
|
|
|
for (const auto& n : nested)
|
|
{
|
|
_subcommands.Insert(n.Name(), n);
|
|
}
|
|
}
|
|
|
|
// Function Description:
|
|
// - reports if the current command has nested commands
|
|
// - This CANNOT detect { "name": "foo", "commands": null }
|
|
bool Command::HasNestedCommands() const
|
|
{
|
|
return _subcommands ? _subcommands.Size() > 0 : false;
|
|
}
|
|
|
|
// Function Description:
|
|
// - reports if the current command IS a nested command
|
|
// - This CAN be used to detect cases like { "name": "foo", "commands": null }
|
|
bool Command::IsNestedCommand() const noexcept
|
|
{
|
|
return _nestedCommand;
|
|
}
|
|
|
|
bool Command::HasName() const noexcept
|
|
{
|
|
return _name.has_value();
|
|
}
|
|
|
|
hstring Command::Name() const noexcept
|
|
{
|
|
if (_name.has_value())
|
|
{
|
|
if (!_name->resource.empty())
|
|
{
|
|
// Resource key overrides name
|
|
return GetLibraryResourceString(_name->resource);
|
|
}
|
|
|
|
// name was explicitly set, return that value.
|
|
return hstring{ _name->name };
|
|
}
|
|
else if (_ActionAndArgs)
|
|
{
|
|
// generate a name from our action
|
|
return get_self<implementation::ActionAndArgs>(_ActionAndArgs)->GenerateName();
|
|
}
|
|
else
|
|
{
|
|
// we have no name
|
|
return {};
|
|
}
|
|
}
|
|
|
|
hstring Command::LanguageNeutralName() const noexcept
|
|
{
|
|
if (_name.has_value())
|
|
{
|
|
// User-specified names **only matter** if they are resource keys (and therefore have a language-neutral version)
|
|
if (!_name->resource.empty())
|
|
{
|
|
// Resource key overrides name
|
|
return EnglishOnlyResourceLoader().GetLocalizedString(_name->resource);
|
|
}
|
|
// ...if there was no resource, then, return nothing.
|
|
}
|
|
else if (_ActionAndArgs)
|
|
{
|
|
// generate a name from our action
|
|
return get_self<implementation::ActionAndArgs>(_ActionAndArgs)->GenerateName(EnglishOnlyResourceLoader().ResourceContext());
|
|
}
|
|
|
|
// we have no neutral name
|
|
return {};
|
|
}
|
|
|
|
hstring Command::ID() const noexcept
|
|
{
|
|
return hstring{ _ID };
|
|
}
|
|
|
|
void Command::ID(const hstring& ID) noexcept
|
|
{
|
|
_ID = ID;
|
|
}
|
|
|
|
void Command::GenerateID()
|
|
{
|
|
if (_ActionAndArgs)
|
|
{
|
|
auto actionAndArgsImpl{ winrt::get_self<implementation::ActionAndArgs>(_ActionAndArgs) };
|
|
if (const auto generatedID = actionAndArgsImpl->GenerateID(); !generatedID.empty())
|
|
{
|
|
_IDWasGenerated = true;
|
|
ID(generatedID);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Command::IDWasGenerated()
|
|
{
|
|
return _IDWasGenerated;
|
|
}
|
|
|
|
void Command::Name(const hstring& value)
|
|
{
|
|
if (!_name.has_value() || _name->name != value)
|
|
{
|
|
if (value.empty())
|
|
{
|
|
_name.reset();
|
|
}
|
|
else
|
|
{
|
|
_name = CommandNameOrResource{ .name = std::wstring{ value } };
|
|
}
|
|
}
|
|
}
|
|
|
|
IMediaResource Command::Icon() const noexcept
|
|
{
|
|
return (_icon && *_icon) ? *_icon : MediaResource::Empty();
|
|
}
|
|
|
|
void Command::Icon(const IMediaResource& val)
|
|
{
|
|
_icon = val;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Deserialize a Command from the `json` object. The json object should
|
|
// contain a "name" and "action", and optionally an "icon".
|
|
// * "name": string|object - the name of the command to display in the
|
|
// command palette. If this is an object, look for the "key" property,
|
|
// and try to load the string from our resources instead.
|
|
// * "action": string|object - A ShortcutAction, either as a name or as an
|
|
// ActionAndArgs serialization. See ActionAndArgs::FromJson for details.
|
|
// If this is null, we'll remove this command from the list of commands.
|
|
// Arguments:
|
|
// - json: the Json::Value to deserialize into a Command
|
|
// - warnings: If there were any warnings during parsing, they'll be
|
|
// appended to this vector.
|
|
// Return Value:
|
|
// - the newly constructed Command object.
|
|
winrt::com_ptr<Command> Command::FromJson(const Json::Value& json,
|
|
std::vector<SettingsLoadWarnings>& warnings,
|
|
const OriginTag origin)
|
|
{
|
|
auto result = winrt::make_self<Command>();
|
|
result->_Origin = origin;
|
|
JsonUtils::GetValueForKey(json, IDKey, result->_ID);
|
|
|
|
auto nested = false;
|
|
JsonUtils::GetValueForKey(json, IterateOnKey, result->_IterateOn);
|
|
JsonUtils::GetValueForKey(json, DescriptionKey, result->_Description);
|
|
|
|
// 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)] })
|
|
{
|
|
// Initialize our list of subcommands.
|
|
result->_subcommands = winrt::single_threaded_map<winrt::hstring, Model::Command>();
|
|
result->_nestedCommand = true;
|
|
auto nestedWarnings = Command::LayerJson(result->_subcommands, nestedCommandsJson, origin);
|
|
// It's possible that the nested commands have some warnings
|
|
warnings.insert(warnings.end(), nestedWarnings.begin(), nestedWarnings.end());
|
|
|
|
if (result->_subcommands.Size() == 0)
|
|
{
|
|
warnings.push_back(SettingsLoadWarnings::FailedToParseSubCommands);
|
|
result->_ActionAndArgs = make<implementation::ActionAndArgs>();
|
|
}
|
|
|
|
nested = true;
|
|
}
|
|
else if (json.isMember(JsonKey(CommandsKey)))
|
|
{
|
|
// { "name": "foo", "commands": null } will land in this case, which
|
|
// should also be used for unbinding.
|
|
|
|
// create an "invalid" ActionAndArgs
|
|
result->_ActionAndArgs = make<implementation::ActionAndArgs>();
|
|
result->_nestedCommand = true;
|
|
}
|
|
|
|
JsonUtils::GetValueForKey(json, IconKey, result->_icon);
|
|
|
|
// If we're a nested command, we can ignore the current action.
|
|
if (!nested)
|
|
{
|
|
if (const auto actionJson{ json[JsonKey(ActionKey)] })
|
|
{
|
|
result->_ActionAndArgs = *ActionAndArgs::FromJson(actionJson, warnings);
|
|
}
|
|
else
|
|
{
|
|
// { name: "foo", action: null } will land in this case, which
|
|
// should also be used for unbinding.
|
|
|
|
// create an "invalid" ActionAndArgs
|
|
result->_ActionAndArgs = make<implementation::ActionAndArgs>();
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
JsonUtils::GetValueForKey(json, NameKey, result->_name);
|
|
|
|
// 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;
|
|
|
|
return result;
|
|
}
|
|
|
|
// This is substantially simpler than the normal FromJson. We just want to take something that looks like:
|
|
// {
|
|
// "input": "bx",
|
|
// "name": "Build project",
|
|
// "description": "Build the project in the CWD"
|
|
// },
|
|
//
|
|
// and turn it into a sendInput action. No need to figure out what kind of
|
|
// action parser, or deal with nesting, or iterable commands or anything.
|
|
winrt::com_ptr<Command> Command::FromSnippetJson(const Json::Value& json)
|
|
{
|
|
auto result = winrt::make_self<Command>();
|
|
result->_Origin = OriginTag::Generated;
|
|
|
|
JsonUtils::GetValueForKey(json, IDKey, result->_ID);
|
|
JsonUtils::GetValueForKey(json, DescriptionKey, result->_Description);
|
|
JsonUtils::GetValueForKey(json, IconKey, result->_icon);
|
|
JsonUtils::GetValueForKey(json, NameKey, result->_name);
|
|
|
|
const auto action{ ShortcutAction::SendInput };
|
|
IActionArgs args{ nullptr };
|
|
std::vector<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings> parseWarnings;
|
|
std::tie(args, parseWarnings) = SendInputArgs::FromJson(json);
|
|
result->_ActionAndArgs = winrt::make<implementation::ActionAndArgs>(action, args);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Function Description:
|
|
// - Attempt to parse all the json objects in `json` into new Command
|
|
// objects, and add them to the map of commands.
|
|
// - If any parsed command has
|
|
// the same Name as an existing command in commands, the new one will
|
|
// layer on top of the existing one.
|
|
// Arguments:
|
|
// - commands: a map of Name->Command which new commands should be layered upon.
|
|
// - json: A Json::Value containing an array of serialized commands
|
|
// Return Value:
|
|
// - A vector containing any warnings detected while parsing
|
|
std::vector<SettingsLoadWarnings> Command::LayerJson(IMap<winrt::hstring, Model::Command>& commands,
|
|
const Json::Value& json,
|
|
const OriginTag origin)
|
|
{
|
|
std::vector<SettingsLoadWarnings> warnings;
|
|
|
|
for (const auto& value : json)
|
|
{
|
|
if (value.isObject())
|
|
{
|
|
try
|
|
{
|
|
const auto result = Command::FromJson(value, warnings, origin);
|
|
if (result->ActionAndArgs().Action() == ShortcutAction::Invalid && !result->HasNestedCommands())
|
|
{
|
|
// If there wasn't a parsed command, then try to get the
|
|
// name that *was* parsed. If that name currently
|
|
// exists in our list of commands, we should remove it.
|
|
const auto name = result->Name();
|
|
if (!name.empty())
|
|
{
|
|
commands.Remove(name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Override commands with the same name
|
|
commands.Insert(result->Name(), *result);
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
}
|
|
return warnings;
|
|
}
|
|
|
|
// Function Description:
|
|
// - Serialize the Command into a json value
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - a serialized command
|
|
Json::Value Command::ToJson() const
|
|
{
|
|
Json::Value cmdJson{ Json::ValueType::objectValue };
|
|
|
|
if (_nestedCommand || _IterateOn != ExpandCommandType::None)
|
|
{
|
|
// handle special commands
|
|
// For these, we can trust _originalJson to be correct.
|
|
// In fact, we _need_ to use it here because we don't actually deserialize `iterateOn`
|
|
// until we expand the command.
|
|
cmdJson = _originalJson;
|
|
}
|
|
else
|
|
{
|
|
JsonUtils::SetValueForKey(cmdJson, IconKey, _icon);
|
|
JsonUtils::SetValueForKey(cmdJson, NameKey, _name);
|
|
if (!_Description.empty())
|
|
{
|
|
JsonUtils::SetValueForKey(cmdJson, DescriptionKey, _Description);
|
|
}
|
|
if (!_ID.empty())
|
|
{
|
|
JsonUtils::SetValueForKey(cmdJson, IDKey, _ID);
|
|
}
|
|
|
|
if (_ActionAndArgs)
|
|
{
|
|
cmdJson[JsonKey(ActionKey)] = ActionAndArgs::ToJson(_ActionAndArgs);
|
|
}
|
|
}
|
|
|
|
return cmdJson;
|
|
}
|
|
|
|
// 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.
|
|
std::string _escapeForJson(const std::string& input)
|
|
{
|
|
Json::Value inJson{ input };
|
|
Json::StreamWriterBuilder builder;
|
|
builder.settings_["indentation"] = "";
|
|
auto out{ Json::writeString(builder, inJson) };
|
|
if (out.size() >= 2)
|
|
{
|
|
// trim off the leading/trailing '"'s
|
|
auto ss{ out.substr(1, out.size() - 2) };
|
|
return ss;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// 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(IMap<winrt::hstring, Model::Command>& commands,
|
|
IVectorView<Model::Profile> profiles,
|
|
IVectorView<Model::ColorScheme> schemes)
|
|
{
|
|
std::vector<winrt::hstring> commandsToRemove;
|
|
std::vector<Model::Command> commandsToAdd;
|
|
|
|
// First, collect up all the commands that need replacing.
|
|
for (const auto& [name, command] : commands)
|
|
{
|
|
auto cmd{ get_self<implementation::Command>(command) };
|
|
|
|
auto newCommands = _expandCommand(cmd, profiles, schemes);
|
|
if (newCommands.size() > 0)
|
|
{
|
|
commandsToRemove.push_back(name);
|
|
commandsToAdd.insert(commandsToAdd.end(), newCommands.begin(), newCommands.end());
|
|
}
|
|
}
|
|
|
|
// Second, remove all the commands that need to be removed.
|
|
for (auto& name : commandsToRemove)
|
|
{
|
|
commands.Remove(name);
|
|
}
|
|
|
|
// Finally, add all the new commands.
|
|
for (auto& cmd : commandsToAdd)
|
|
{
|
|
commands.Insert(cmd.Name(), cmd);
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
// Return Value:
|
|
// - and empty vector if the command wasn't expandable; otherwise, a list of
|
|
// the newly-created commands.
|
|
std::vector<Model::Command> Command::_expandCommand(Command* const expandable,
|
|
IVectorView<Model::Profile> profiles,
|
|
IVectorView<Model::ColorScheme> schemes)
|
|
{
|
|
std::vector<Model::Command> newCommands;
|
|
|
|
if (expandable->HasNestedCommands())
|
|
{
|
|
ExpandCommands(expandable->_subcommands, profiles, schemes);
|
|
}
|
|
|
|
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{}.newCharReader() };
|
|
|
|
// First, get a string for the original Json::Value
|
|
auto oldJsonString = expandable->_originalJson.toStyledString();
|
|
|
|
auto reParseJson = [&](const auto& newJsonString) -> bool {
|
|
// - 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))
|
|
{
|
|
// If we encounter a re-parsing error, just stop processing the rest of the commands.
|
|
return false;
|
|
}
|
|
|
|
// Pass the new json back though FromJson, to get the new expanded value.
|
|
// FromJson requires that we pass in a vector to hang on to the
|
|
// warnings, but ultimately, we don't care about warnings during
|
|
// expansion.
|
|
std::vector<SettingsLoadWarnings> unused;
|
|
if (auto newCmd{ Command::FromJson(newJsonValue, unused, expandable->_Origin) })
|
|
{
|
|
newCommands.push_back(*newCmd);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
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(til::u16u8(p.Name()));
|
|
// Use the fully resolved icon here so it doesn't have to go around the resolver again
|
|
auto escapedProfileIcon = _escapeForJson(til::u16u8(p.Icon().Resolved()));
|
|
auto newJsonString = til::replace_needle_in_haystack(oldJsonString,
|
|
ProfileNameToken,
|
|
escapedProfileName);
|
|
til::replace_needle_in_haystack_inplace(newJsonString,
|
|
ProfileIconToken,
|
|
escapedProfileIcon);
|
|
|
|
// If we encounter a re-parsing error, just stop processing the rest of the commands.
|
|
if (!reParseJson(newJsonString))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (expandable->_IterateOn == ExpandCommandType::ColorSchemes)
|
|
{
|
|
for (const auto& s : schemes)
|
|
{
|
|
// For each scheme, create a new command. We'll take the
|
|
// original json, replace any instances of "${scheme.name}" with
|
|
// the scheme's name, then re-attempt to parse the action and
|
|
// args.
|
|
|
|
// - Escape the profile name for JSON appropriately
|
|
auto escapedSchemeName = _escapeForJson(til::u16u8(s.Name()));
|
|
auto newJsonString = til::replace_needle_in_haystack(oldJsonString,
|
|
SchemeNameToken,
|
|
escapedSchemeName);
|
|
|
|
// If we encounter a re-parsing error, just stop processing the rest of the commands.
|
|
if (!reParseJson(newJsonString))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return newCommands;
|
|
}
|
|
|
|
void Command::ResolveMediaResourcesWithBasePath(const winrt::hstring& basePath, const Model::MediaResourceResolver& resolver)
|
|
{
|
|
if (_icon && *_icon)
|
|
{
|
|
ResolveIconMediaResource(_Origin, basePath, *_icon, resolver);
|
|
}
|
|
}
|
|
|
|
winrt::Windows::Foundation::Collections::IVector<Model::Command> Command::ParsePowerShellMenuComplete(winrt::hstring json, int32_t replaceLength)
|
|
{
|
|
if (json.empty())
|
|
{
|
|
return nullptr;
|
|
}
|
|
auto data = winrt::to_string(json);
|
|
|
|
std::string errs;
|
|
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder{}.newCharReader() };
|
|
Json::Value root;
|
|
if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs))
|
|
{
|
|
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
|
|
}
|
|
|
|
std::vector<Model::Command> result;
|
|
|
|
const auto parseElement = [&](const auto& element) {
|
|
std::wstring completionText;
|
|
std::wstring listText;
|
|
std::wstring tooltipText;
|
|
JsonUtils::GetValueForKey(element, "CompletionText", completionText);
|
|
JsonUtils::GetValueForKey(element, "ListItemText", listText);
|
|
JsonUtils::GetValueForKey(element, "ToolTip", tooltipText);
|
|
|
|
auto args = winrt::make_self<SendInputArgs>(til::hstring_format(FMT_COMPILE(L"{:\x7f^{}}{}"), L"", replaceLength, completionText));
|
|
|
|
Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args };
|
|
|
|
auto c = winrt::make_self<Command>();
|
|
c->_name = CommandNameOrResource{ .name = listText };
|
|
c->_Description = tooltipText;
|
|
c->_ActionAndArgs = actionAndArgs;
|
|
// Try to assign a sensible icon based on the result type. These are
|
|
// roughly chosen to align with the icons in
|
|
// https://github.com/PowerShell/PowerShellEditorServices/pull/1738
|
|
// as best as possible.
|
|
if (const auto resultType{ JsonUtils::GetValueForKey<int>(element, "ResultType") })
|
|
{
|
|
// PowerShell completion result -> Segoe Fluent icon value & name
|
|
switch (resultType)
|
|
{
|
|
case 1: // History -> 0xe81c History
|
|
c->_icon = MediaResource::FromString(L"\ue81c");
|
|
break;
|
|
case 2: // Command -> 0xecaa AppIconDefault
|
|
c->_icon = MediaResource::FromString(L"\uecaa");
|
|
break;
|
|
case 3: // ProviderItem -> 0xe8e4 AlignLeft
|
|
c->_icon = MediaResource::FromString(L"\ue8e4");
|
|
break;
|
|
case 4: // ProviderContainer -> 0xe838 FolderOpen
|
|
c->_icon = MediaResource::FromString(L"\ue838");
|
|
break;
|
|
case 5: // Property -> 0xe7c1 Flag
|
|
c->_icon = MediaResource::FromString(L"\ue7c1");
|
|
break;
|
|
case 6: // Method -> 0xecaa AppIconDefault
|
|
c->_icon = MediaResource::FromString(L"\uecaa");
|
|
break;
|
|
case 7: // ParameterName -> 0xe7c1 Flag
|
|
c->_icon = MediaResource::FromString(L"\ue7c1");
|
|
break;
|
|
case 8: // ParameterValue -> 0xf000 KnowledgeArticle
|
|
c->_icon = MediaResource::FromString(L"\uf000");
|
|
break;
|
|
case 10: // Namespace -> 0xe943 Code
|
|
c->_icon = MediaResource::FromString(L"\ue943");
|
|
break;
|
|
case 13: // DynamicKeyword -> 0xe945 LightningBolt
|
|
c->_icon = MediaResource::FromString(L"\ue945");
|
|
break;
|
|
}
|
|
}
|
|
|
|
result.push_back(*c);
|
|
};
|
|
|
|
if (root.isArray())
|
|
{
|
|
// If we got a whole array of suggestions, parse each one.
|
|
for (const auto& element : root)
|
|
{
|
|
parseElement(element);
|
|
}
|
|
}
|
|
else if (root.isObject())
|
|
{
|
|
// If we instead only got a single element back, just parse the root element.
|
|
parseElement(root);
|
|
}
|
|
|
|
return winrt::single_threaded_vector<Model::Command>(std::move(result));
|
|
}
|
|
|
|
// Method description:
|
|
// * Convert the list of recent commands into a list of sendInput actions to
|
|
// send those commands.
|
|
// * We'll give each command a "history" icon.
|
|
// * If directories is true, we'll prepend "cd " to each command, so that
|
|
// the command will be run as a directory change instead.
|
|
IVector<Model::Command> Command::HistoryToCommands(IVector<winrt::hstring> history,
|
|
winrt::hstring currentCommandline,
|
|
bool directories,
|
|
winrt::hstring iconPath)
|
|
{
|
|
std::wstring cdText = directories ? L"cd " : L"";
|
|
auto result = std::vector<Model::Command>();
|
|
|
|
// Use this map to discard duplicates.
|
|
std::unordered_map<std::wstring_view, bool> foundCommands{};
|
|
|
|
auto backspaces = std::wstring(currentCommandline.size(), L'\x7f');
|
|
|
|
// Iterate in reverse over the history, so that most recent commands are first
|
|
for (auto i = history.Size(); i > 0; i--)
|
|
{
|
|
const auto& element{ history.GetAt(i - 1) };
|
|
std::wstring_view line{ element };
|
|
|
|
if (line.empty())
|
|
{
|
|
continue;
|
|
}
|
|
if (foundCommands.contains(line))
|
|
{
|
|
continue;
|
|
}
|
|
auto args = winrt::make_self<SendInputArgs>(til::hstring_format(FMT_COMPILE(L"{}{}{}"), cdText, backspaces, line));
|
|
|
|
Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args };
|
|
|
|
auto command = winrt::make_self<Command>();
|
|
command->_ActionAndArgs = actionAndArgs;
|
|
command->_name = CommandNameOrResource{ .name = std::wstring{ line } };
|
|
command->_icon = MediaResource::FromString(iconPath);
|
|
result.push_back(*command);
|
|
foundCommands[line] = true;
|
|
}
|
|
|
|
return winrt::single_threaded_vector<Model::Command>(std::move(result));
|
|
}
|
|
|
|
void Command::LogSettingChanges(std::set<std::string>& changes)
|
|
{
|
|
if (_IterateOn != ExpandCommandType::None)
|
|
{
|
|
switch (_IterateOn)
|
|
{
|
|
case ExpandCommandType::Profiles:
|
|
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), IterateOnKey, "profiles"));
|
|
break;
|
|
case ExpandCommandType::ColorSchemes:
|
|
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), IterateOnKey, "schemes"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!_Description.empty())
|
|
{
|
|
changes.emplace(DescriptionKey);
|
|
}
|
|
|
|
if (IsNestedCommand())
|
|
{
|
|
changes.emplace(CommandsKey);
|
|
}
|
|
else
|
|
{
|
|
const auto json{ ActionAndArgs::ToJson(ActionAndArgs()) };
|
|
if (json.isString())
|
|
{
|
|
// covers actions w/out args
|
|
// - "command": "unbound" --> "unbound"
|
|
// - "command": "copy" --> "copy"
|
|
changes.emplace(json.asString());
|
|
}
|
|
else
|
|
{
|
|
// covers actions w/ args
|
|
// - "command": { "action": "copy", "singleLine": true } --> "copy.singleLine"
|
|
// - "command": { "action": "copy", "singleLine": true, "dismissSelection": true } --> "copy.singleLine", "copy.dismissSelection"
|
|
|
|
const std::string shortcutActionName{ json[JsonKey("action")].asString() };
|
|
|
|
auto members = json.getMemberNames();
|
|
members.erase(std::remove_if(members.begin(), members.end(), [](const auto& member) { return member == "action"; }), members.end());
|
|
|
|
for (const auto& actionArg : members)
|
|
{
|
|
changes.emplace(fmt::format(FMT_COMPILE("{}.{}"), shortcutActionName, actionArg));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|