Add support for "Tasks" in the Suggestions UI (#15664)

_targets #15027_

Adds a new suggestion source, `tasks`, that allows a user to open the
Suggestions UI with `sendInput` commands saved in their settings.
`source` becomes a flag setting, so it can be combined like so:

```json
        {
            "keys": "ctrl+shift+h", "command": { "action": "suggestions", "source": "commandHistory", "useCommandline":true },
        },
        {
            "keys": "ctrl+shift+y", "command": { "action": "suggestions", "source": "tasks", "useCommandline":false },
        },
        {
            "keys": "ctrl+shift+b", "command": { "action": "suggestions", "source": ["all"], "useCommandline":true },
        },
```

If a nested command has `sendInput` commands underneath it, this will
build a tree of commands that only include `sendInput`s as leaves (but
leave the rest of the nesting structure intact).


## References and Relevant Issues

Closes #1595

See also #13445 
As spec'd in #14864 

## Validation Steps Performed

Tested manually
This commit is contained in:
Mike Griese
2023-08-15 14:01:34 -05:00
committed by GitHub
parent 3afe7a8d11
commit 127df073fa
11 changed files with 255 additions and 56 deletions

View File

@@ -85,6 +85,32 @@
}
]
},
"BuiltinSuggestionSource": {
"enum": [
"commandHistory",
"tasks",
"all"
],
"type": "string"
},
"SuggestionSource": {
"default": "all",
"description": "Either a single suggestion source, or an array of sources to concatenate. Built-in sources include `commandHistory`, `directoryHistory`, and `tasks`. The special value `all` indicates all suggestion sources should be included",
"$comment": "`tasks` and `local` are sources that would be added by the Tasks feature, as a follow-up",
"oneOf": [
{
"type": [ "string", "null", "BuiltinSuggestionSource" ]
},
{
"type": "array",
"items": { "type": "BuiltinSuggestionSource" }
},
{
"type": "array",
"items": { "type": "string" }
}
]
},
"AppearanceConfig": {
"properties": {
"colorScheme": {
@@ -398,6 +424,7 @@
"sendInput",
"setColorScheme",
"setTabColor",
"showSuggestions",
"splitPane",
"switchToTab",
"tabSearch",
@@ -1767,6 +1794,30 @@
}
]
},
"ShowSuggestionsAction": {
"description": "Arguments corresponding to a Open Suggestions Action",
"allOf": [
{
"$ref": "#/$defs/ShortcutAction"
},
{
"properties": {
"action": {
"type": "string",
"const": "showSuggestions"
},
"source": {
"$ref": "#/$defs/SuggestionSource",
"description": "Which suggestion sources to filter."
},
"useCommandline": {
"default": false,
"description": "When set to `true`, the current commandline the user has typed will pre-populate the filter of the Suggestions UI. This requires that the user has enabled shell integration in their shell's config. When set to false, the filter will start empty."
}
}
}
]
},
"ShowCloseButton": {
"enum": [
"always",
@@ -2037,6 +2088,9 @@
{
"$ref": "#/$defs/SearchWebAction"
},
{
"$ref": "#/$defs/ShowSuggestionsAction"
},
{
"type": "null"
}

View File

@@ -1264,25 +1264,62 @@ namespace winrt::TerminalApp::implementation
{
if (const auto& realArgs = args.ActionArgs().try_as<SuggestionsArgs>())
{
auto source = realArgs.Source();
const auto source = realArgs.Source();
std::vector<Command> commandsCollection;
Control::CommandHistoryContext context{ nullptr };
winrt::hstring currentCommandline = L"";
switch (source)
{
case SuggestionsSource::CommandHistory:
// If the user wanted to use the current commandline to filter results,
// OR they wanted command history (or some other source that
// requires context from the control)
// then get that here.
const bool shouldGetContext = realArgs.UseCommandline() ||
WI_IsFlagSet(source, SuggestionsSource::CommandHistory);
if (shouldGetContext)
{
if (const auto& control{ _GetActiveControl() })
{
const auto context = control.CommandHistory();
const auto& currentCmd{ realArgs.UseCommandline() ? context.CurrentCommandline() : L"" };
_OpenSuggestions(control,
Command::HistoryToCommands(context.History(), currentCmd, false),
SuggestionsMode::Palette,
currentCmd);
context = control.CommandHistory();
if (context)
{
currentCommandline = context.CurrentCommandline();
}
}
args.Handled(true);
}
break;
// Aggregate all the commands from the different sources that
// the user selected.
// Tasks are all the sendInput commands the user has saved in
// their settings file. Ask the ActionMap for those.
if (WI_IsFlagSet(source, SuggestionsSource::Tasks))
{
const auto tasks = _settings.GlobalSettings().ActionMap().FilterToSendInput(currentCommandline);
for (const auto& t : tasks)
{
commandsCollection.push_back(t);
}
}
// Command History comes from the commands in the buffer,
// assuming the user has enabled shell integration. Get those
// from the active control.
if (WI_IsFlagSet(source, SuggestionsSource::CommandHistory) &&
context != nullptr)
{
const auto recentCommands = Command::HistoryToCommands(context.History(), currentCommandline, false);
for (const auto& t : recentCommands)
{
commandsCollection.push_back(t);
}
}
// Open the palette with all these commands in it.
_OpenSuggestions(_GetActiveControl(),
winrt::single_threaded_vector<Command>(std::move(commandsCollection)),
SuggestionsMode::Palette,
currentCommandline);
args.Handled(true);
}
}
}

View File

@@ -709,23 +709,42 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::hstring SuggestionsArgs::GenerateName() const
{
auto base{ RS_(L"SuggestionsCommandKey") };
switch (Source())
{
case SuggestionsSource::CommandHistory:
base = RS_(L"SuggestionsCommandHistoryCommandKey");
}
std::wstringstream ss;
ss << RS_(L"SuggestionsCommandKey").c_str();
if (UseCommandline())
{
return winrt::hstring{
fmt::format(L"{}, useCommandline:true", std::wstring_view(base))
};
ss << L", useCommandline:true";
}
// All of the source values will leave a trailing ", " that we need to chop later:
ss << L", source: ";
const auto source = Source();
if (source == SuggestionsSource::All)
{
ss << L"all, ";
}
else if (source == static_cast<SuggestionsSource>(0))
{
ss << L"none, ";
}
else
{
return base;
if (WI_IsFlagSet(source, SuggestionsSource::Tasks))
{
ss << L"tasks, ";
}
if (WI_IsFlagSet(source, SuggestionsSource::CommandHistory))
{
ss << L"commandHistory, ";
}
}
// Chop off the last ","
auto result = ss.str();
// use `resize`, to avoid duplicating the entire string. (substr doesn't create a view.)
result.resize(result.size() - 2);
return winrt::hstring{ result };
}
winrt::hstring FindMatchArgs::GenerateName() const

View File

@@ -112,11 +112,14 @@ namespace Microsoft.Terminal.Settings.Model
ToCurrent,
ToMouse,
};
[flags]
enum SuggestionsSource
{
Tasks,
CommandHistory,
DirectoryHistory,
Tasks = 0x1,
CommandHistory = 0x2,
DirectoryHistory = 0x4,
All = 0xffffffff,
};
[default_interface] runtimeclass NewTerminalArgs {

View File

@@ -931,4 +931,72 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
return _ExpandedCommandsCache;
}
IVector<Model::Command> _filterToSendInput(IMapView<hstring, Model::Command> nameMap,
winrt::hstring currentCommandline)
{
auto results = winrt::single_threaded_vector<Model::Command>();
const auto numBackspaces = currentCommandline.size();
// Helper to clone a sendInput command into a new Command, with the
// input trimmed to account for the currentCommandline
auto createInputAction = [&](const Model::Command& command) -> Model::Command {
winrt::com_ptr<implementation::Command> cmdImpl;
cmdImpl.copy_from(winrt::get_self<implementation::Command>(command));
const auto inArgs{ command.ActionAndArgs().Args().try_as<Model::SendInputArgs>() };
auto args = winrt::make_self<SendInputArgs>(
winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"),
L"",
numBackspaces,
(std::wstring_view)(inArgs ? inArgs.Input() : L"")) });
Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args };
auto copy = cmdImpl->Copy();
copy->ActionAndArgs(actionAndArgs);
return *copy;
};
// iterate over all the commands in all our actions...
for (auto&& [name, command] : nameMap)
{
// If this is not a nested command, and it's a sendInput command...
if (!command.HasNestedCommands() &&
command.ActionAndArgs().Action() == ShortcutAction::SendInput)
{
// copy it into the results.
results.Append(createInputAction(command));
}
// If this is nested...
else if (command.HasNestedCommands())
{
// Look for any sendInput commands nested underneath us
auto innerResults = _filterToSendInput(command.NestedCommands(), currentCommandline);
if (innerResults.Size() > 0)
{
// This command did have at least one sendInput under it
// Create a new Command, which is a copy of this Command,
// which only has SendInputs in it
winrt::com_ptr<implementation::Command> cmdImpl;
cmdImpl.copy_from(winrt::get_self<implementation::Command>(command));
auto copy = cmdImpl->Copy();
copy->NestedCommands(innerResults.GetView());
results.Append(*copy);
}
}
}
return results;
}
IVector<Model::Command> ActionMap::FilterToSendInput(
winrt::hstring currentCommandline)
{
return _filterToSendInput(NameMap(), currentCommandline);
}
}

View File

@@ -79,6 +79,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void ExpandCommands(const Windows::Foundation::Collections::IVectorView<Model::Profile>& profiles,
const Windows::Foundation::Collections::IMapView<winrt::hstring, Model::ColorScheme>& schemes);
winrt::Windows::Foundation::Collections::IVector<Model::Command> FilterToSendInput(winrt::hstring currentCommandline);
private:
std::optional<Model::Command> _GetActionByID(const InternalActionID actionID) const;
std::optional<Model::Command> _GetActionByKeyChordInternal(const Control::KeyChord& keys) const;

View File

@@ -22,6 +22,8 @@ namespace Microsoft.Terminal.Settings.Model
Windows.Foundation.Collections.IMapView<Microsoft.Terminal.Control.KeyChord, Command> GlobalHotkeys { get; };
IVector<Command> ExpandedCommands { get; };
IVector<Command> FilterToSendInput(String CurrentCommandline);
};
[default_interface] runtimeclass ActionMap : IActionMapView

View File

@@ -64,6 +64,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
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 }
@@ -752,7 +762,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// Iterate in reverse over the history, so that most recent commands are first
for (auto i = history.Size(); i > 0; i--)
{
std::wstring_view line{ history.GetAt(i - 1) };
const auto& element{ history.GetAt(i - 1) };
std::wstring_view line{ element };
if (line.empty())
{

View File

@@ -52,6 +52,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::IVectorView<Model::Command>& nested);
bool HasName() const noexcept;
hstring Name() const noexcept;

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->

View File

@@ -499,12 +499,14 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FindMatchDirecti
};
};
JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SuggestionsSource)
JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SuggestionsSource)
{
JSON_MAPPINGS(3) = {
static constexpr std::array<pair_type, 5> mappings = {
pair_type{ "none", AllClear },
pair_type{ "tasks", ValueType::Tasks },
pair_type{ "commandHistory", ValueType::CommandHistory },
pair_type{ "directoryHistory", ValueType::DirectoryHistory }
pair_type{ "directoryHistory", ValueType::DirectoryHistory },
pair_type{ "all", AllSet },
};
};