Add support for local snippets in the CWD (#17388)

This PR adds the ability to load snippets from the CWD into the
suggestions UI.

If shell integration is disabled, then we only ever think the CWD for a
pane is it's `startingDirectory`. So, in the default case, users can
still stick snippets into the root of their git repos, and have the
Terminal load them automatically (for profiles starting in the root of
their repo).
If it's enabled though, we'll always try to load snippets from the CWD
of the shell.

* We cache the actions into a separate map of CWD -> actions. This lets
us read the file only the first time we see a dir.
* We clear that cache on settings reload
* We only load `sendInput` actions from the `.wt.json`

As spec'd in #17329
This commit is contained in:
Mike Griese 2024-07-25 20:39:26 -05:00 коммит произвёл GitHub
Родитель 7851c96812
Коммит 21fa303a3d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
16 изменённых файлов: 259 добавлений и 86 удалений

28
.wt.json Normal file
Просмотреть файл

@ -0,0 +1,28 @@
{
"$version": "1.0.0",
"snippets":
[
{
"input": "bx\r",
"name": "Build project",
"description": "Build the project in the CWD"
},
{
"input": "bz\r",
"name": "Build solution, incremental",
"description": "Just build changes to the solution"
},
{
"input": "bcz\r",
"name": "Clean & build solution",
"icon": "\uE8e6",
"description": "Start over. Go get your coffee. "
},
{
"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."
}
]
}

Просмотреть файл

@ -1437,79 +1437,89 @@ 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_IsAnyFlagSet(source, SuggestionsSource::CommandHistory | SuggestionsSource::QuickFixes);
if (shouldGetContext)
{
if (const auto& control{ _GetActiveControl() })
{
context = control.CommandHistory();
if (context)
{
currentCommandline = context.CurrentCommandline();
}
}
}
// Aggregate all the commands from the different sources that
// the user selected. This is the order presented to the user
if (WI_IsFlagSet(source, SuggestionsSource::QuickFixes) &&
context != nullptr &&
context.QuickFixes() != nullptr)
{
// \ue74c --> OEM icon
const auto recentCommands = Command::HistoryToCommands(context.QuickFixes(), hstring{ L"" }, false, hstring{ L"\ue74c" });
for (const auto& t : recentCommands)
{
commandsCollection.push_back(t);
}
}
// 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)
{
// \ue81c --> History icon
const auto recentCommands = Command::HistoryToCommands(context.History(), currentCommandline, false, hstring{ L"\ue81c" });
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);
_doHandleSuggestions(realArgs);
args.Handled(true);
}
}
}
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"";
// 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 | SuggestionsSource::QuickFixes);
if (const auto& control{ _GetActiveControl() })
{
currentWorkingDirectory = control.CurrentWorkingDirectory();
if (shouldGetContext)
{
context = control.CommandHistory();
if (context)
{
currentCommandline = context.CurrentCommandline();
}
}
}
// Aggregate all the commands from the different sources that
// the user selected.
if (WI_IsFlagSet(source, SuggestionsSource::QuickFixes) &&
context != nullptr &&
context.QuickFixes() != nullptr)
{
// \ue74c --> OEM icon
const auto recentCommands = Command::HistoryToCommands(context.QuickFixes(), hstring{ L"" }, false, hstring{ L"\ue74c" });
for (const auto& t : recentCommands)
{
commandsCollection.push_back(t);
}
}
// 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);
// ----- we may be on a background thread here -----
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, hstring{ L"\ue81c" });
for (const auto& t : recentCommands)
{
commandsCollection.push_back(t);
}
}
co_await wil::resume_foreground(Dispatcher());
// Open the palette with all these commands in it.
_OpenSuggestions(_GetActiveControl(),
winrt::single_threaded_vector<Command>(std::move(commandsCollection)),
SuggestionsMode::Palette,
currentCommandline);
}
void TerminalPage::_HandleColorSelection(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{

Просмотреть файл

@ -42,7 +42,7 @@ namespace winrt::TerminalApp::implementation
}
}
void SnippetsPaneContent::UpdateSettings(const CascadiaSettings& settings)
winrt::fire_and_forget SnippetsPaneContent::UpdateSettings(const CascadiaSettings& settings)
{
_settings = settings;
@ -51,7 +51,9 @@ namespace winrt::TerminalApp::implementation
// has typed, then relies on the suggestions UI to _also_ filter with that
// string.
const auto tasks = _settings.GlobalSettings().ActionMap().FilterToSendInput(winrt::hstring{}); // IVector<Model::Command>
const auto tasks = co_await _settings.GlobalSettings().ActionMap().FilterToSnippets(winrt::hstring{}, winrt::hstring{}); // IVector<Model::Command>
co_await wil::resume_foreground(Dispatcher());
_allTasks = winrt::single_threaded_observable_vector<TerminalApp::FilteredTask>();
for (const auto& t : tasks)
{

Просмотреть файл

@ -17,7 +17,7 @@ namespace winrt::TerminalApp::implementation
winrt::Windows::UI::Xaml::FrameworkElement GetRoot();
void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings& settings);
winrt::fire_and_forget UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings& settings);
winrt::Windows::Foundation::Size MinimumSize();
void Focus(winrt::Windows::UI::Xaml::FocusState reason = winrt::Windows::UI::Xaml::FocusState::Programmatic);

Просмотреть файл

@ -555,6 +555,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

Просмотреть файл

@ -2318,6 +2318,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return *context;
}
winrt::hstring ControlCore::CurrentWorkingDirectory() const
{
return winrt::hstring{ _terminal->GetWorkingDirectory() };
}
bool ControlCore::QuickFixesAvailable() const noexcept
{
return _cachedQuickFixes && _cachedQuickFixes.Size() > 0;

Просмотреть файл

@ -188,6 +188,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void ContextMenuSelectCommand();
void ContextMenuSelectOutput();
winrt::hstring CurrentWorkingDirectory() const;
#pragma endregion
#pragma region ITerminalInput

Просмотреть файл

@ -62,5 +62,7 @@ namespace Microsoft.Terminal.Control
void SelectOutput(Boolean goUp);
IVector<ScrollMark> ScrollMarks { get; };
String CurrentWorkingDirectory { get; };
};
}

Просмотреть файл

@ -3639,6 +3639,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
return _core.CommandHistory();
}
winrt::hstring TermControl::CurrentWorkingDirectory() const
{
return _core.CurrentWorkingDirectory();
}
void TermControl::UpdateWinGetSuggestions(Windows::Foundation::Collections::IVector<hstring> suggestions)
{

Просмотреть файл

@ -116,6 +116,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);

Просмотреть файл

@ -7,6 +7,7 @@
#include "Command.h"
#include "AllShortcutActions.h"
#include <LibraryResources.h>
#include <til/io.h>
#include "ActionMap.g.cpp"
@ -807,10 +808,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
@ -849,21 +852,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>(_filterToSnippets(command.NestedCommands(), currentCommandline, empty));
if (innerResults.Size() > 0)
{
@ -876,9 +880,20 @@ 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&& [_, command] : nameMap)
{
addCommand(command);
}
// ... and all the local commands passed in here
for (const auto& command : localCommands)
{
addCommand(command);
}
return results;
@ -900,9 +915,77 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
AddAction(*cmd, keys);
}
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
std::vector<Model::Command> ActionMap::_updateLocalSnippetCache(winrt::hstring currentWorkingDirectory)
{
return _filterToSendInput(NameMap(), currentCommandline);
// This returns an empty string if we fail to load the file.
std::filesystem::path localSnippetsPath{ std::wstring_view{ currentWorkingDirectory + L"\\.wt.json" } };
const auto localTasksFileContents = til::io::read_file_as_utf8_string_if_exists(localSnippetsPath);
if (!localTasksFileContents.has_value() || localTasksFileContents->empty())
{
return {};
}
const auto& data = *localTasksFileContents;
Json::Value root;
std::string errs;
const std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder{}.newCharReader() };
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.
return {};
}
auto result = std::vector<Model::Command>();
if (auto actions{ root[JsonKey("snippets")] })
{
for (const auto& json : actions)
{
result.push_back(*Command::FromSnippetJson(json));
}
}
return result;
}
winrt::Windows::Foundation::IAsyncOperation<IVector<Model::Command>> ActionMap::FilterToSnippets(
winrt::hstring currentCommandline,
winrt::hstring currentWorkingDirectory)
{
{
// Check if there are any cached commands in this directory.
const auto& cache{ _cwdLocalSnippetsCache.lock_shared() };
const auto cacheIterator = cache->find(currentWorkingDirectory);
if (cacheIterator != cache->end())
{
// We found something in the cache! return it.
co_return winrt::single_threaded_vector<Model::Command>(_filterToSnippets(NameMap(),
currentCommandline,
cacheIterator->second));
}
} // release the lock on the cache
// Don't do I/O on the main thread
co_await winrt::resume_background();
auto result = _updateLocalSnippetCache(currentWorkingDirectory);
if (!result.empty())
{
// We found something! Add it to the cache
auto cache{ _cwdLocalSnippetsCache.lock() };
cache->insert_or_assign(currentWorkingDirectory, result);
}
co_return winrt::single_threaded_vector<Model::Command>(_filterToSnippets(NameMap(),
currentCommandline,
result));
}
#pragma endregion
}

Просмотреть файл

@ -84,7 +84,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;
@ -102,6 +102,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _TryUpdateActionMap(const Model::Command& cmd);
void _TryUpdateKeyChord(const Model::Command& cmd, const Control::KeyChord& keys);
std::vector<Model::Command> _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 };
@ -134,6 +136,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 };
til::shared_mutex<std::unordered_map<hstring, std::vector<Model::Command>>> _cwdLocalSnippetsCache{};
friend class SettingsModelUnitTests::KeyBindingsTests;
friend class SettingsModelUnitTests::DeserializationTests;
friend class SettingsModelUnitTests::TerminalSettingsTests;

Просмотреть файл

@ -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

Просмотреть файл

@ -286,6 +286,34 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
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->_iconPath);
result->_name = _nameFromJson(json);
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.

Просмотреть файл

@ -51,6 +51,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
std::vector<SettingsLoadWarnings>& warnings,
const OriginTag origin);
static winrt::com_ptr<Command> FromSnippetJson(const Json::Value& json);
static void ExpandCommands(Windows::Foundation::Collections::IMap<winrt::hstring, Model::Command>& commands,
Windows::Foundation::Collections::IVectorView<Model::Profile> profiles,
Windows::Foundation::Collections::IVectorView<Model::ColorScheme> schemes);

Просмотреть файл

@ -501,9 +501,10 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FindMatchDirecti
JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SuggestionsSource)
{
static constexpr std::array<pair_type, 6> mappings = {
static constexpr std::array<pair_type, 7> mappings = {
pair_type{ "none", AllClear },
pair_type{ "tasks", ValueType::Tasks },
pair_type{ "snippets", ValueType::Tasks },
pair_type{ "commandHistory", ValueType::CommandHistory },
pair_type{ "directoryHistory", ValueType::DirectoryHistory },
pair_type{ "quickFix", ValueType::QuickFixes },