Skip to content

Commit 127df07

Browse files
authored
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
1 parent 3afe7a8 commit 127df07

File tree

11 files changed

+255
-56
lines changed

11 files changed

+255
-56
lines changed

doc/cascadia/profiles.schema.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,32 @@
8585
}
8686
]
8787
},
88+
"BuiltinSuggestionSource": {
89+
"enum": [
90+
"commandHistory",
91+
"tasks",
92+
"all"
93+
],
94+
"type": "string"
95+
},
96+
"SuggestionSource": {
97+
"default": "all",
98+
"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",
99+
"$comment": "`tasks` and `local` are sources that would be added by the Tasks feature, as a follow-up",
100+
"oneOf": [
101+
{
102+
"type": [ "string", "null", "BuiltinSuggestionSource" ]
103+
},
104+
{
105+
"type": "array",
106+
"items": { "type": "BuiltinSuggestionSource" }
107+
},
108+
{
109+
"type": "array",
110+
"items": { "type": "string" }
111+
}
112+
]
113+
},
88114
"AppearanceConfig": {
89115
"properties": {
90116
"colorScheme": {
@@ -398,6 +424,7 @@
398424
"sendInput",
399425
"setColorScheme",
400426
"setTabColor",
427+
"showSuggestions",
401428
"splitPane",
402429
"switchToTab",
403430
"tabSearch",
@@ -1767,6 +1794,30 @@
17671794
}
17681795
]
17691796
},
1797+
"ShowSuggestionsAction": {
1798+
"description": "Arguments corresponding to a Open Suggestions Action",
1799+
"allOf": [
1800+
{
1801+
"$ref": "#/$defs/ShortcutAction"
1802+
},
1803+
{
1804+
"properties": {
1805+
"action": {
1806+
"type": "string",
1807+
"const": "showSuggestions"
1808+
},
1809+
"source": {
1810+
"$ref": "#/$defs/SuggestionSource",
1811+
"description": "Which suggestion sources to filter."
1812+
},
1813+
"useCommandline": {
1814+
"default": false,
1815+
"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."
1816+
}
1817+
}
1818+
}
1819+
]
1820+
},
17701821
"ShowCloseButton": {
17711822
"enum": [
17721823
"always",
@@ -2037,6 +2088,9 @@
20372088
{
20382089
"$ref": "#/$defs/SearchWebAction"
20392090
},
2091+
{
2092+
"$ref": "#/$defs/ShowSuggestionsAction"
2093+
},
20402094
{
20412095
"type": "null"
20422096
}

src/cascadia/TerminalApp/AppActionHandlers.cpp

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,25 +1264,62 @@ namespace winrt::TerminalApp::implementation
12641264
{
12651265
if (const auto& realArgs = args.ActionArgs().try_as<SuggestionsArgs>())
12661266
{
1267-
auto source = realArgs.Source();
1268-
1269-
switch (source)
1270-
{
1271-
case SuggestionsSource::CommandHistory:
1267+
const auto source = realArgs.Source();
1268+
std::vector<Command> commandsCollection;
1269+
Control::CommandHistoryContext context{ nullptr };
1270+
winrt::hstring currentCommandline = L"";
1271+
1272+
// If the user wanted to use the current commandline to filter results,
1273+
// OR they wanted command history (or some other source that
1274+
// requires context from the control)
1275+
// then get that here.
1276+
const bool shouldGetContext = realArgs.UseCommandline() ||
1277+
WI_IsFlagSet(source, SuggestionsSource::CommandHistory);
1278+
if (shouldGetContext)
12721279
{
12731280
if (const auto& control{ _GetActiveControl() })
12741281
{
1275-
const auto context = control.CommandHistory();
1276-
const auto& currentCmd{ realArgs.UseCommandline() ? context.CurrentCommandline() : L"" };
1277-
_OpenSuggestions(control,
1278-
Command::HistoryToCommands(context.History(), currentCmd, false),
1279-
SuggestionsMode::Palette,
1280-
currentCmd);
1282+
context = control.CommandHistory();
1283+
if (context)
1284+
{
1285+
currentCommandline = context.CurrentCommandline();
1286+
}
12811287
}
1282-
args.Handled(true);
12831288
}
1284-
break;
1289+
1290+
// Aggregate all the commands from the different sources that
1291+
// the user selected.
1292+
1293+
// Tasks are all the sendInput commands the user has saved in
1294+
// their settings file. Ask the ActionMap for those.
1295+
if (WI_IsFlagSet(source, SuggestionsSource::Tasks))
1296+
{
1297+
const auto tasks = _settings.GlobalSettings().ActionMap().FilterToSendInput(currentCommandline);
1298+
for (const auto& t : tasks)
1299+
{
1300+
commandsCollection.push_back(t);
1301+
}
12851302
}
1303+
1304+
// Command History comes from the commands in the buffer,
1305+
// assuming the user has enabled shell integration. Get those
1306+
// from the active control.
1307+
if (WI_IsFlagSet(source, SuggestionsSource::CommandHistory) &&
1308+
context != nullptr)
1309+
{
1310+
const auto recentCommands = Command::HistoryToCommands(context.History(), currentCommandline, false);
1311+
for (const auto& t : recentCommands)
1312+
{
1313+
commandsCollection.push_back(t);
1314+
}
1315+
}
1316+
1317+
// Open the palette with all these commands in it.
1318+
_OpenSuggestions(_GetActiveControl(),
1319+
winrt::single_threaded_vector<Command>(std::move(commandsCollection)),
1320+
SuggestionsMode::Palette,
1321+
currentCommandline);
1322+
args.Handled(true);
12861323
}
12871324
}
12881325
}

src/cascadia/TerminalSettingsModel/ActionArgs.cpp

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -709,23 +709,42 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
709709

710710
winrt::hstring SuggestionsArgs::GenerateName() const
711711
{
712-
auto base{ RS_(L"SuggestionsCommandKey") };
713-
switch (Source())
712+
std::wstringstream ss;
713+
ss << RS_(L"SuggestionsCommandKey").c_str();
714+
715+
if (UseCommandline())
714716
{
715-
case SuggestionsSource::CommandHistory:
716-
base = RS_(L"SuggestionsCommandHistoryCommandKey");
717+
ss << L", useCommandline:true";
717718
}
718719

719-
if (UseCommandline())
720+
// All of the source values will leave a trailing ", " that we need to chop later:
721+
ss << L", source: ";
722+
const auto source = Source();
723+
if (source == SuggestionsSource::All)
720724
{
721-
return winrt::hstring{
722-
fmt::format(L"{}, useCommandline:true", std::wstring_view(base))
723-
};
725+
ss << L"all, ";
726+
}
727+
else if (source == static_cast<SuggestionsSource>(0))
728+
{
729+
ss << L"none, ";
724730
}
725731
else
726732
{
727-
return base;
733+
if (WI_IsFlagSet(source, SuggestionsSource::Tasks))
734+
{
735+
ss << L"tasks, ";
736+
}
737+
738+
if (WI_IsFlagSet(source, SuggestionsSource::CommandHistory))
739+
{
740+
ss << L"commandHistory, ";
741+
}
728742
}
743+
// Chop off the last ","
744+
auto result = ss.str();
745+
// use `resize`, to avoid duplicating the entire string. (substr doesn't create a view.)
746+
result.resize(result.size() - 2);
747+
return winrt::hstring{ result };
729748
}
730749

731750
winrt::hstring FindMatchArgs::GenerateName() const

src/cascadia/TerminalSettingsModel/ActionArgs.idl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,14 @@ namespace Microsoft.Terminal.Settings.Model
112112
ToCurrent,
113113
ToMouse,
114114
};
115+
116+
[flags]
115117
enum SuggestionsSource
116118
{
117-
Tasks,
118-
CommandHistory,
119-
DirectoryHistory,
119+
Tasks = 0x1,
120+
CommandHistory = 0x2,
121+
DirectoryHistory = 0x4,
122+
All = 0xffffffff,
120123
};
121124

122125
[default_interface] runtimeclass NewTerminalArgs {

src/cascadia/TerminalSettingsModel/ActionMap.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,4 +931,72 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
931931
{
932932
return _ExpandedCommandsCache;
933933
}
934+
935+
IVector<Model::Command> _filterToSendInput(IMapView<hstring, Model::Command> nameMap,
936+
winrt::hstring currentCommandline)
937+
{
938+
auto results = winrt::single_threaded_vector<Model::Command>();
939+
940+
const auto numBackspaces = currentCommandline.size();
941+
// Helper to clone a sendInput command into a new Command, with the
942+
// input trimmed to account for the currentCommandline
943+
auto createInputAction = [&](const Model::Command& command) -> Model::Command {
944+
winrt::com_ptr<implementation::Command> cmdImpl;
945+
cmdImpl.copy_from(winrt::get_self<implementation::Command>(command));
946+
947+
const auto inArgs{ command.ActionAndArgs().Args().try_as<Model::SendInputArgs>() };
948+
949+
auto args = winrt::make_self<SendInputArgs>(
950+
winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"),
951+
L"",
952+
numBackspaces,
953+
(std::wstring_view)(inArgs ? inArgs.Input() : L"")) });
954+
Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args };
955+
956+
auto copy = cmdImpl->Copy();
957+
copy->ActionAndArgs(actionAndArgs);
958+
959+
return *copy;
960+
};
961+
962+
// iterate over all the commands in all our actions...
963+
for (auto&& [name, command] : nameMap)
964+
{
965+
// If this is not a nested command, and it's a sendInput command...
966+
if (!command.HasNestedCommands() &&
967+
command.ActionAndArgs().Action() == ShortcutAction::SendInput)
968+
{
969+
// copy it into the results.
970+
results.Append(createInputAction(command));
971+
}
972+
// If this is nested...
973+
else if (command.HasNestedCommands())
974+
{
975+
// Look for any sendInput commands nested underneath us
976+
auto innerResults = _filterToSendInput(command.NestedCommands(), currentCommandline);
977+
978+
if (innerResults.Size() > 0)
979+
{
980+
// This command did have at least one sendInput under it
981+
982+
// Create a new Command, which is a copy of this Command,
983+
// which only has SendInputs in it
984+
winrt::com_ptr<implementation::Command> cmdImpl;
985+
cmdImpl.copy_from(winrt::get_self<implementation::Command>(command));
986+
auto copy = cmdImpl->Copy();
987+
copy->NestedCommands(innerResults.GetView());
988+
989+
results.Append(*copy);
990+
}
991+
}
992+
}
993+
994+
return results;
995+
}
996+
997+
IVector<Model::Command> ActionMap::FilterToSendInput(
998+
winrt::hstring currentCommandline)
999+
{
1000+
return _filterToSendInput(NameMap(), currentCommandline);
1001+
}
9341002
}

src/cascadia/TerminalSettingsModel/ActionMap.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
7979
void ExpandCommands(const Windows::Foundation::Collections::IVectorView<Model::Profile>& profiles,
8080
const Windows::Foundation::Collections::IMapView<winrt::hstring, Model::ColorScheme>& schemes);
8181

82+
winrt::Windows::Foundation::Collections::IVector<Model::Command> FilterToSendInput(winrt::hstring currentCommandline);
83+
8284
private:
8385
std::optional<Model::Command> _GetActionByID(const InternalActionID actionID) const;
8486
std::optional<Model::Command> _GetActionByKeyChordInternal(const Control::KeyChord& keys) const;

src/cascadia/TerminalSettingsModel/ActionMap.idl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ namespace Microsoft.Terminal.Settings.Model
2222
Windows.Foundation.Collections.IMapView<Microsoft.Terminal.Control.KeyChord, Command> GlobalHotkeys { get; };
2323

2424
IVector<Command> ExpandedCommands { get; };
25+
26+
IVector<Command> FilterToSendInput(String CurrentCommandline);
2527
};
2628

2729
[default_interface] runtimeclass ActionMap : IActionMapView

src/cascadia/TerminalSettingsModel/Command.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
6464
return _subcommands ? _subcommands.GetView() : nullptr;
6565
}
6666

67+
void Command::NestedCommands(const Windows::Foundation::Collections::IVectorView<Model::Command>& nested)
68+
{
69+
_subcommands = winrt::single_threaded_map<winrt::hstring, Model::Command>();
70+
71+
for (const auto& n : nested)
72+
{
73+
_subcommands.Insert(n.Name(), n);
74+
}
75+
}
76+
6777
// Function Description:
6878
// - reports if the current command has nested commands
6979
// - This CANNOT detect { "name": "foo", "commands": null }
@@ -752,7 +762,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
752762
// Iterate in reverse over the history, so that most recent commands are first
753763
for (auto i = history.Size(); i > 0; i--)
754764
{
755-
std::wstring_view line{ history.GetAt(i - 1) };
765+
const auto& element{ history.GetAt(i - 1) };
766+
std::wstring_view line{ element };
756767

757768
if (line.empty())
758769
{

src/cascadia/TerminalSettingsModel/Command.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
5252
bool HasNestedCommands() const;
5353
bool IsNestedCommand() const noexcept;
5454
Windows::Foundation::Collections::IMapView<winrt::hstring, Model::Command> NestedCommands() const;
55+
void NestedCommands(const Windows::Foundation::Collections::IVectorView<Model::Command>& nested);
5556

5657
bool HasName() const noexcept;
5758
hstring Name() const noexcept;

0 commit comments

Comments
 (0)