mirror of
https://github.com/microsoft/terminal.git
synced 2026-07-08 18:16:28 +00:00
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:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
-->
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user