Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for local snippets in the CWD #17388

Merged
merged 20 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .wt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"actions":
[
{
"command": { "action": "sendInput", "input": "bx\r" },
"name": "Build project",
"description": "Build the project in the CWD"
},
{
"command": { "action": "sendInput", "input": "bz\r" },
"name": "Build solution, incremental",
"description": "Just build changes to the solution"
},
{
"command": { "action": "sendInput", "input": "bcz\r" },
"name": "Clean & build solution",
"icon": "\uE8e6",
"description": "Start over. Go get your coffee. "
},
{
"command": { "action": "sendInput", "input": "nuget push -ApiKey az -source TerminalDependencies %userprofile%\\Downloads" },
"name": "Upload package to nuget feed",
"icon": "\uE898",
"description": "Go download a .nupkg, put it in ~/Downloads, and use this to push to our private feed."
},

]
}
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
111 changes: 61 additions & 50 deletions src/cascadia/TerminalApp/AppActionHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1330,64 +1330,75 @@ namespace winrt::TerminalApp::implementation
{
if (const auto& realArgs = args.ActionArgs().try_as<SuggestionsArgs>())
{
const auto source = realArgs.Source();
std::vector<Command> commandsCollection;
Control::CommandHistoryContext context{ nullptr };
winrt::hstring currentCommandline = L"";

// 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() })
{
context = control.CommandHistory();
if (context)
{
currentCommandline = context.CurrentCommandline();
}
}
}
_doHandleSuggestions(realArgs);

args.Handled(true);
}
}
}

// Aggregate all the commands from the different sources that
// the user selected.
winrt::fire_and_forget TerminalPage::_doHandleSuggestions(SuggestionsArgs realArgs)
{
const auto source = realArgs.Source();
std::vector<Command> commandsCollection;
Control::CommandHistoryContext context{ nullptr };
winrt::hstring currentCommandline = L"";
winrt::hstring currentWorkingDirectory = L"";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: always prefer {} for hstrings. no need to have empty string literals all over the place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've previously raised a nit about how I personally think that {} is perfect for object constructions, while = is perfect for assignments, in particular of trivial types. I know you all like to write auto foo{ bar() }, so I feel quite betrayed seeing this!
(This is not a serious comment. 😄)


// 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);
}
}
// 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_IsAnyFlagSet(source, SuggestionsSource::CommandHistory);
if (const auto& control{ _GetActiveControl() })
{
currentWorkingDirectory = control.CurrentWorkingDirectory();

// 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)
if (shouldGetContext)
{
context = control.CommandHistory();
if (context)
{
const auto recentCommands = Command::HistoryToCommands(context.History(), currentCommandline, false);
for (const auto& t : recentCommands)
{
commandsCollection.push_back(t);
}
currentCommandline = context.CurrentCommandline();
}
}
}

// 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);
// 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 = co_await _settings.GlobalSettings().ActionMap().FilterToSnippets(currentCommandline, currentWorkingDirectory);
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);
}
}

co_await wil::resume_foreground(Dispatcher());
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved

// Open the palette with all these commands in it.
_OpenSuggestions(_GetActiveControl(),
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
winrt::single_threaded_vector<Command>(std::move(commandsCollection)),
SuggestionsMode::Palette,
currentCommandline);
}

void TerminalPage::_HandleColorSelection(const IInspectable& /*sender*/,
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ namespace winrt::TerminalApp::implementation
winrt::com_ptr<TerminalTab> _senderOrFocusedTab(const IInspectable& sender);

void _activePaneChanged(winrt::TerminalApp::TerminalTab tab, Windows::Foundation::IInspectable args);
winrt::fire_and_forget _doHandleSuggestions(Microsoft::Terminal::Settings::Model::SuggestionsArgs realArgs);

#pragma region ActionHandlers
// These are all defined in AppActionHandlers.cpp
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2282,6 +2282,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return *context;
}

winrt::hstring ControlCore::CurrentWorkingDirectory() const
{
return winrt::hstring{ _terminal->GetWorkingDirectory() };
}

Core::Scheme ControlCore::ColorScheme() const noexcept
{
Core::Scheme s;
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation

void ContextMenuSelectCommand();
void ContextMenuSelectOutput();

winrt::hstring CurrentWorkingDirectory() const;
#pragma endregion

#pragma region ITerminalInput
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/ICoreState.idl
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,7 @@ namespace Microsoft.Terminal.Control
void SelectOutput(Boolean goUp);
IVector<ScrollMark> ScrollMarks { get; };

String CurrentWorkingDirectory { get; };

};
}
4 changes: 4 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3558,6 +3558,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
return _core.CommandHistory();
}
winrt::hstring TermControl::CurrentWorkingDirectory() const
{
return _core.CurrentWorkingDirectory();
}

Core::Scheme TermControl::ColorScheme() const noexcept
{
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void SelectCommand(const bool goUp);
void SelectOutput(const bool goUp);

winrt::hstring CurrentWorkingDirectory() const;
#pragma endregion

void ScrollViewport(int viewTop);
Expand Down
108 changes: 96 additions & 12 deletions src/cascadia/TerminalSettingsModel/ActionMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -805,10 +805,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return _ExpandedCommandsCache;
}

IVector<Model::Command> _filterToSendInput(IMapView<hstring, Model::Command> nameMap,
winrt::hstring currentCommandline)
#pragma region Snippets
std::vector<Model::Command> _filterToSnippets(IMapView<hstring, Model::Command> nameMap,
winrt::hstring currentCommandline,
const std::vector<Model::Command>& localCommands)
{
auto results = winrt::single_threaded_vector<Model::Command>();
std::vector<Model::Command> results{};

const auto numBackspaces = currentCommandline.size();
// Helper to clone a sendInput command into a new Command, with the
Expand Down Expand Up @@ -847,21 +849,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return *copy;
};

// iterate over all the commands in all our actions...
for (auto&& [name, command] : nameMap)
{
// Helper to copy this command into a snippet-styled command, and any
// nested commands
const auto addCommand = [&](auto& command) {
// 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));
results.push_back(createInputAction(command));
}
// If this is nested...
else if (command.HasNestedCommands())
{
// Look for any sendInput commands nested underneath us
auto innerResults = _filterToSendInput(command.NestedCommands(), currentCommandline);
std::vector<Model::Command> empty{};
auto innerResults = winrt::single_threaded_vector<Model::Command>(std::move(_filterToSnippets(command.NestedCommands(), currentCommandline, empty)));
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved

if (innerResults.Size() > 0)
{
Expand All @@ -874,17 +877,98 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
auto copy = cmdImpl->Copy();
copy->NestedCommands(innerResults.GetView());

results.Append(*copy);
results.push_back(*copy);
}
}
};

// iterate over all the commands in all our actions...
for (auto&& [name, command] : nameMap)
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
{
addCommand(command);
}
// ... and all the local commands passed in here
for (const auto& command : localCommands)
{
addCommand(command);
}

return results;
}

IVector<Model::Command> ActionMap::FilterToSendInput(
winrt::hstring currentCommandline)
// Update ActionMap's cache of actions for this directory. We'll look for a
// .wt.json in this directory. If it exists, we'll read it, parse it's JSON,
// then take all the sendInput actions in it and store them in our
// _cwdLocalSnippetsCache
winrt::Windows::Foundation::IAsyncAction ActionMap::_updateLocalSnippetCache(winrt::hstring currentWorkingDirectory)
{
return _filterToSendInput(NameMap(), currentCommandline);
// Don't do I/O on the main thread
co_await winrt::resume_background();

// This returns an empty string if we fail to load the file.
auto localTasksFileContents = CascadiaSettings::ReadFile(currentWorkingDirectory + L"\\.wt.json");
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
if (!localTasksFileContents.empty())
{
auto data = winrt::to_string(localTasksFileContents);
std::string errs;
static std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
Json::Value root;
if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs))
{
// In the real settings parser, we'd throw here:
// throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
//
// That seems overly aggressive for something that we don't
// really own. Instead, just bail out.
co_return;
}

auto result = std::vector<Model::Command>();
if (auto actions{ root[JsonKey("actions")] })
{
std::vector<SettingsLoadWarnings> warnings;
for (const auto& json : actions)
{
auto parsed = Command::FromJson(json, warnings, OriginTag::Generated);
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// Skip over things that aren't snippets
if (parsed->ActionAndArgs().Action() != ShortcutAction::SendInput)
{
continue;
}

result.push_back(*parsed);
}
}

_cwdLocalSnippetsCache.insert_or_assign(currentWorkingDirectory, result);
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
}

// Now at the bottom, we've either found a file successfully parsed it,
// and updated the _cwdLocalSnippetsCache. Or we failed at some point,
// and then it doesn't really matter.
co_return;
}

winrt::Windows::Foundation::IAsyncOperation<IVector<Model::Command>> ActionMap::FilterToSnippets(
winrt::hstring currentCommandline,
winrt::hstring currentWorkingDirectory)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to assert or switch over to a thread here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think we needed to. _updateLocalSnippetCache would move to a bg thread if it needs to (we may not even need to call it). And the whole FilterToSnippets is an IAsyncOperation so the caller should know it's not necessarily returning on the same thread.

// Check if there are any cached commands in this directory.
// If there aren't, then we'll try to look for any commands in this
// dir's .wt.json
auto cachedCwdCommands = _cwdLocalSnippetsCache.find(currentWorkingDirectory);
if (cachedCwdCommands == _cwdLocalSnippetsCache.end())
{
// Here, we haven't cached this path yet
co_await _updateLocalSnippetCache(currentWorkingDirectory);
cachedCwdCommands = _cwdLocalSnippetsCache.find(currentWorkingDirectory);
}

auto cachedCommands = cachedCwdCommands != _cwdLocalSnippetsCache.end() ?
cachedCwdCommands->second :
std::vector<Model::Command>{};

co_return winrt::single_threaded_vector<Model::Command>(_filterToSnippets(NameMap(), currentCommandline, cachedCommands));
}
#pragma endregion
}
6 changes: 5 additions & 1 deletion src/cascadia/TerminalSettingsModel/ActionMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ 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);
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Foundation::Collections::IVector<Model::Command>> FilterToSnippets(winrt::hstring currentCommandline, winrt::hstring currentWorkingDirectory);

private:
Model::Command _GetActionByID(const winrt::hstring& actionID) const;
Expand All @@ -101,6 +101,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _TryUpdateActionMap(const Model::Command& cmd);
void _TryUpdateKeyChord(const Model::Command& cmd, const Control::KeyChord& keys);

winrt::Windows::Foundation::IAsyncAction _updateLocalSnippetCache(winrt::hstring currentWorkingDirectory);

Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionsCache{ nullptr };
Windows::Foundation::Collections::IMap<hstring, Model::Command> _NameMapCache{ nullptr };
Windows::Foundation::Collections::IMap<Control::KeyChord, Model::Command> _GlobalHotkeysCache{ nullptr };
Expand Down Expand Up @@ -133,6 +135,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// we can give the SUI a view of the key chords and the commands they map to
Windows::Foundation::Collections::IMap<Control::KeyChord, Model::Command> _ResolvedKeyToActionMapCache{ nullptr };

std::unordered_map<hstring, std::vector<Model::Command>> _cwdLocalSnippetsCache{};

friend class SettingsModelUnitTests::KeyBindingsTests;
friend class SettingsModelUnitTests::DeserializationTests;
friend class SettingsModelUnitTests::TerminalSettingsTests;
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalSettingsModel/ActionMap.idl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Microsoft.Terminal.Settings.Model

IVector<Command> ExpandedCommands { get; };

IVector<Command> FilterToSendInput(String CurrentCommandline);
Windows.Foundation.IAsyncOperation<IVector<Command> > FilterToSnippets(String CurrentCommandline, String CurrentWorkingDirectory);
};

[default_interface] runtimeclass ActionMap : IActionMapView
Expand Down
Loading
Loading