diff --git a/.wt.json b/.wt.json new file mode 100644 index 0000000000..44f718f42e --- /dev/null +++ b/.wt.json @@ -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." + } + ] +} diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 88794eda57..cf294fbd47 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -1437,79 +1437,89 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - const auto source = realArgs.Source(); - std::vector 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(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 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(std::move(commandsCollection)), + SuggestionsMode::Palette, + currentCommandline); + } + void TerminalPage::_HandleColorSelection(const IInspectable& /*sender*/, const ActionEventArgs& args) { diff --git a/src/cascadia/TerminalApp/SnippetsPaneContent.cpp b/src/cascadia/TerminalApp/SnippetsPaneContent.cpp index b772cddac4..b6415c6ba7 100644 --- a/src/cascadia/TerminalApp/SnippetsPaneContent.cpp +++ b/src/cascadia/TerminalApp/SnippetsPaneContent.cpp @@ -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 + const auto tasks = co_await _settings.GlobalSettings().ActionMap().FilterToSnippets(winrt::hstring{}, winrt::hstring{}); // IVector + co_await wil::resume_foreground(Dispatcher()); + _allTasks = winrt::single_threaded_observable_vector(); for (const auto& t : tasks) { diff --git a/src/cascadia/TerminalApp/SnippetsPaneContent.h b/src/cascadia/TerminalApp/SnippetsPaneContent.h index 205ab3c8e7..19f8555982 100644 --- a/src/cascadia/TerminalApp/SnippetsPaneContent.h +++ b/src/cascadia/TerminalApp/SnippetsPaneContent.h @@ -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); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index aaa98db46a..26a2447847 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -555,6 +555,7 @@ namespace winrt::TerminalApp::implementation winrt::com_ptr _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 diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 6ff980d9c3..23a4a6df26 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.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; diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 59ebdc9e83..0ab124b747 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -188,6 +188,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ContextMenuSelectCommand(); void ContextMenuSelectOutput(); + + winrt::hstring CurrentWorkingDirectory() const; #pragma endregion #pragma region ITerminalInput diff --git a/src/cascadia/TerminalControl/ICoreState.idl b/src/cascadia/TerminalControl/ICoreState.idl index a3333473c2..7bd5411c6f 100644 --- a/src/cascadia/TerminalControl/ICoreState.idl +++ b/src/cascadia/TerminalControl/ICoreState.idl @@ -62,5 +62,7 @@ namespace Microsoft.Terminal.Control void SelectOutput(Boolean goUp); IVector ScrollMarks { get; }; + String CurrentWorkingDirectory { get; }; + }; } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index e53a8ada1c..be9de020b5 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -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 suggestions) { diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 13166e18c7..e8cf2cd20f 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -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); diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 354831073d..5090733342 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -7,6 +7,7 @@ #include "Command.h" #include "AllShortcutActions.h" #include +#include #include "ActionMap.g.cpp" @@ -807,10 +808,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return _ExpandedCommandsCache; } - IVector _filterToSendInput(IMapView nameMap, - winrt::hstring currentCommandline) +#pragma region Snippets + std::vector _filterToSnippets(IMapView nameMap, + winrt::hstring currentCommandline, + const std::vector& localCommands) { - auto results = winrt::single_threaded_vector(); + std::vector 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 empty{}; + auto innerResults = winrt::single_threaded_vector(_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 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 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 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(); + if (auto actions{ root[JsonKey("snippets")] }) + { + for (const auto& json : actions) + { + result.push_back(*Command::FromSnippetJson(json)); + } + } + return result; } + + winrt::Windows::Foundation::IAsyncOperation> 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(_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(_filterToSnippets(NameMap(), + currentCommandline, + result)); + } +#pragma endregion } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index 22428ee5ed..e3ceb3f518 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -84,7 +84,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void ExpandCommands(const Windows::Foundation::Collections::IVectorView& profiles, const Windows::Foundation::Collections::IMapView& schemes); - winrt::Windows::Foundation::Collections::IVector FilterToSendInput(winrt::hstring currentCommandline); + winrt::Windows::Foundation::IAsyncOperation> 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 _updateLocalSnippetCache(winrt::hstring currentWorkingDirectory); + Windows::Foundation::Collections::IMap _AvailableActionsCache{ nullptr }; Windows::Foundation::Collections::IMap _NameMapCache{ nullptr }; Windows::Foundation::Collections::IMap _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 _ResolvedKeyToActionMapCache{ nullptr }; + til::shared_mutex>> _cwdLocalSnippetsCache{}; + friend class SettingsModelUnitTests::KeyBindingsTests; friend class SettingsModelUnitTests::DeserializationTests; friend class SettingsModelUnitTests::TerminalSettingsTests; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.idl b/src/cascadia/TerminalSettingsModel/ActionMap.idl index 1849e0680a..3c94ac5d92 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.idl +++ b/src/cascadia/TerminalSettingsModel/ActionMap.idl @@ -22,7 +22,7 @@ namespace Microsoft.Terminal.Settings.Model IVector ExpandedCommands { get; }; - IVector FilterToSendInput(String CurrentCommandline); + Windows.Foundation.IAsyncOperation > FilterToSnippets(String CurrentCommandline, String CurrentWorkingDirectory); }; [default_interface] runtimeclass ActionMap : IActionMapView diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index 21ea343199..ac66d188b9 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -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::FromSnippetJson(const Json::Value& json) + { + auto result = winrt::make_self(); + 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 parseWarnings; + std::tie(args, parseWarnings) = SendInputArgs::FromJson(json); + result->_ActionAndArgs = winrt::make(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. diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index 194ed7dc95..1bc92220ca 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -51,6 +51,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::vector& warnings, const OriginTag origin); + static winrt::com_ptr FromSnippetJson(const Json::Value& json); + static void ExpandCommands(Windows::Foundation::Collections::IMap& commands, Windows::Foundation::Collections::IVectorView profiles, Windows::Foundation::Collections::IVectorView schemes); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 9737ef4553..4b22d61cf2 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -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 mappings = { + static constexpr std::array 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 },