Skip to content

Commit

Permalink
Add Quick Fix UI and support for custom CommandNotFound OSC (#16848)
Browse files Browse the repository at this point in the history
### `OSC 9001; CmdNotFound; <missingCmd>`
Adds support for custom OSC "command not found" sequence `OSC 9001;
CmdNotFound; <missingCmd>`. Upon receiving the "CmdNotFound" variant
with the missing command payload, we send the missing command up to the
Quick Fix menu and add it in as `winget install <missingCmd>`.

### Quick Fix UI
The Quick Fix UI is a new UI surface that lives in the gutter (left
padding) of your terminal. The button appears if quick fixes are
available. When clicked, a list of suggestions appears in a flyout. If
there is not enough space in the gutter, the button will be presented in
a collapsed version that expands to a normal size upon hovering over it.

The Quick Fix UI was implemented similar to the context menu. The UI
itself lives in TermControl, but it can be populated by other layers
(i.e. TermApp layer).

Quick Fix suggestions are also automatically loaded into the Suggestions
UI.

If a quick fix is available and a screen reader is attached, we dispatch
an announcement that quick fixes are available to notify the user that
that's the case.

Spec: #17005
#16599

### Follow-ups
- #17377: Add a key binding for quick fix
- #17378: Use winget to search for packages using `missingCmd`

---------

Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
  • Loading branch information
3 people authored Jun 28, 2024
1 parent 024837c commit 6589957
Show file tree
Hide file tree
Showing 34 changed files with 516 additions and 42 deletions.
17 changes: 15 additions & 2 deletions src/cascadia/TerminalApp/AppActionHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,7 @@ namespace winrt::TerminalApp::implementation
// requires context from the control)
// then get that here.
const bool shouldGetContext = realArgs.UseCommandline() ||
WI_IsFlagSet(source, SuggestionsSource::CommandHistory);
WI_IsAnyFlagSet(source, SuggestionsSource::CommandHistory | SuggestionsSource::QuickFixes);
if (shouldGetContext)
{
if (const auto& control{ _GetActiveControl() })
Expand Down Expand Up @@ -1373,7 +1373,20 @@ namespace winrt::TerminalApp::implementation
if (WI_IsFlagSet(source, SuggestionsSource::CommandHistory) &&
context != nullptr)
{
const auto recentCommands = Command::HistoryToCommands(context.History(), currentCommandline, false);
// \ue81c --> History icon
const auto recentCommands = Command::HistoryToCommands(context.History(), currentCommandline, false, hstring{ L"\ue81c" });
for (const auto& t : recentCommands)
{
commandsCollection.push_back(t);
}
}

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);
Expand Down
79 changes: 79 additions & 0 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,8 @@ namespace winrt::TerminalApp::implementation

term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler });

term.SearchMissingCommand({ get_weak(), &TerminalPage::_SearchMissingCommandHandler });

// Don't even register for the event if the feature is compiled off.
if constexpr (Feature_ShellCompletions::IsEnabled())
{
Expand All @@ -1753,6 +1755,12 @@ namespace winrt::TerminalApp::implementation
page->_PopulateContextMenu(weakTerm.get(), sender.try_as<MUX::Controls::CommandBarFlyout>(), true);
}
});
term.QuickFixMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) {
if (const auto& page{ weak.get() })
{
page->_PopulateQuickFixMenu(weakTerm.get(), sender.try_as<Controls::MenuFlyout>());
}
});
}

// Method Description:
Expand Down Expand Up @@ -2985,6 +2993,30 @@ namespace winrt::TerminalApp::implementation
ShowWindowChanged.raise(*this, args);
}

winrt::fire_and_forget TerminalPage::_SearchMissingCommandHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::SearchMissingCommandEventArgs args)
{
assert(!Dispatcher().HasThreadAccess());

if (!Feature_QuickFix::IsEnabled())
{
co_return;
}

std::vector<hstring> suggestions;
suggestions.reserve(1);
suggestions.emplace_back(fmt::format(FMT_COMPILE(L"winget install {}"), args.MissingCommand()));

co_await wil::resume_foreground(Dispatcher());

auto term = _GetActiveControl();
if (!term)
{
co_return;
}
term.UpdateWinGetSuggestions(single_threaded_vector<hstring>(std::move(suggestions)));
term.RefreshQuickFixMenu();
}

// Method Description:
// - Paste text from the Windows Clipboard to the focused terminal
void TerminalPage::_PasteText()
Expand Down Expand Up @@ -4905,6 +4937,53 @@ namespace winrt::TerminalApp::implementation
makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } });
}

void TerminalPage::_PopulateQuickFixMenu(const TermControl& control,
const Controls::MenuFlyout& menu)
{
if (!control || !menu)
{
return;
}

// Helper lambda for dispatching a SendInput ActionAndArgs onto the
// ShortcutActionDispatch. Used below to wire up each menu entry to the
// respective action. Then clear the quick fix menu.
auto weak = get_weak();
auto makeCallback = [weak](const hstring& suggestion) {
return [weak, suggestion](auto&&, auto&&) {
if (auto page{ weak.get() })
{
const auto actionAndArgs = ActionAndArgs{ ShortcutAction::SendInput, SendInputArgs{ hstring{ L"\u0003" } + suggestion } };
page->_actionDispatch->DoAction(actionAndArgs);
if (auto ctrl = page->_GetActiveControl())
{
ctrl.ClearQuickFix();
}
}
};
};

// Wire up each item to the action that should be performed. By actually
// connecting these to actions, we ensure the implementation is
// consistent. This also leaves room for customizing this menu with
// actions in the future.

menu.Items().Clear();
const auto quickFixes = control.CommandHistory().QuickFixes();
for (const auto& qf : quickFixes)
{
MenuFlyoutItem item{};

auto iconElement = UI::IconPathConverter::IconWUX(L"\ue74c");
Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw);
item.Icon(iconElement);

item.Text(qf);
item.Click(makeCallback(qf));
menu.Items().Append(item);
}
}

// Handler for our WindowProperties's PropertyChanged event. We'll use this
// to pop the "Identify Window" toast when the user renames our window.
winrt::fire_and_forget TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/,
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalApp/TerminalPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ namespace winrt::TerminalApp::implementation
void _OpenSuggestions(const Microsoft::Terminal::Control::TermControl& sender, Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::Command> commandsCollection, winrt::TerminalApp::SuggestionsMode mode, winrt::hstring filterText);

void _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args);
winrt::fire_and_forget _SearchMissingCommandHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::SearchMissingCommandEventArgs args);

winrt::fire_and_forget _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);

Expand All @@ -539,6 +540,7 @@ namespace winrt::TerminalApp::implementation
void _sendDraggedTabToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional<til::point> dragPoint);

void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection);
void _PopulateQuickFixMenu(const Microsoft::Terminal::Control::TermControl& control, const Windows::UI::Xaml::Controls::MenuFlyout& sender);
winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex);

winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender);
Expand Down
28 changes: 28 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
auto pfnCompletionsChanged = [=](auto&& menuJson, auto&& replaceLength) { _terminalCompletionsChanged(menuJson, replaceLength); };
_terminal->CompletionsChangedCallback(pfnCompletionsChanged);

auto pfnSearchMissingCommand = [this](auto&& PH1) { _terminalSearchMissingCommand(std::forward<decltype(PH1)>(PH1)); };
_terminal->SetSearchMissingCommandCallback(pfnSearchMissingCommand);

auto pfnClearQuickFix = [this] { ClearQuickFix(); };
_terminal->SetClearQuickFixCallback(pfnClearQuickFix);

// MSFT 33353327: Initialize the renderer in the ctor instead of Initialize().
// We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go.
// If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach
Expand Down Expand Up @@ -1627,6 +1633,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_midiAudio.PlayNote(reinterpret_cast<HWND>(_owningHwnd), noteNumber, velocity, std::chrono::duration_cast<std::chrono::milliseconds>(duration));
}

void ControlCore::_terminalSearchMissingCommand(std::wstring_view missingCommand)
{
SearchMissingCommand.raise(*this, make<implementation::SearchMissingCommandEventArgs>(hstring{ missingCommand }));
}

void ControlCore::ClearQuickFix()
{
_cachedQuickFixes = nullptr;
RefreshQuickFixUI.raise(*this, nullptr);
}

bool ControlCore::HasSelection() const
{
const auto lock = _terminal->LockForReading();
Expand Down Expand Up @@ -2297,9 +2314,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation

auto context = winrt::make_self<CommandHistoryContext>(std::move(commands));
context->CurrentCommandline(trimmedCurrentCommand);
context->QuickFixes(_cachedQuickFixes);
return *context;
}

bool ControlCore::QuickFixesAvailable() const noexcept
{
return _cachedQuickFixes && _cachedQuickFixes.Size() > 0;
}

void ControlCore::UpdateQuickFixes(const Windows::Foundation::Collections::IVector<hstring>& quickFixes)
{
_cachedQuickFixes = quickFixes;
}

Core::Scheme ControlCore::ColorScheme() const noexcept
{
Core::Scheme s;
Expand Down
13 changes: 12 additions & 1 deletion src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
til::property<Windows::Foundation::Collections::IVector<winrt::hstring>> History;
til::property<winrt::hstring> CurrentCommandline;
til::property<Windows::Foundation::Collections::IVector<winrt::hstring>> QuickFixes;

CommandHistoryContext(std::vector<winrt::hstring>&& history)
CommandHistoryContext(std::vector<winrt::hstring>&& history) :
QuickFixes(winrt::single_threaded_vector<winrt::hstring>())
{
History(winrt::single_threaded_vector<winrt::hstring>(std::move(history)));
}
Expand Down Expand Up @@ -153,6 +155,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void PersistToPath(const wchar_t* path) const;
void RestoreFromPath(const wchar_t* path) const;

void ClearQuickFix();

#pragma region ICoreState
const size_t TaskbarState() const noexcept;
const size_t TaskbarProgress() const noexcept;
Expand Down Expand Up @@ -241,6 +245,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation

hstring ReadEntireBuffer() const;
Control::CommandHistoryContext CommandHistory() const;
bool QuickFixesAvailable() const noexcept;
void UpdateQuickFixes(const Windows::Foundation::Collections::IVector<hstring>& quickFixes);

void AdjustOpacity(const float opacity, const bool relative);

Expand Down Expand Up @@ -285,6 +291,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
til::typed_event<IInspectable, Control::UpdateSelectionMarkersEventArgs> UpdateSelectionMarkers;
til::typed_event<IInspectable, Control::OpenHyperlinkEventArgs> OpenHyperlink;
til::typed_event<IInspectable, Control::CompletionsChangedEventArgs> CompletionsChanged;
til::typed_event<IInspectable, Control::SearchMissingCommandEventArgs> SearchMissingCommand;
til::typed_event<> RefreshQuickFixUI;

til::typed_event<> CloseTerminalRequested;
til::typed_event<> RestartTerminalRequested;
Expand Down Expand Up @@ -354,6 +362,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation

til::point _contextMenuBufferPosition{ 0, 0 };

Windows::Foundation::Collections::IVector<hstring> _cachedQuickFixes{ nullptr };

void _setupDispatcherAndCallbacks();

bool _setFontSizeUnderLock(float fontSize);
Expand All @@ -377,6 +387,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _terminalPlayMidiNote(const int noteNumber,
const int velocity,
const std::chrono::microseconds duration);
void _terminalSearchMissingCommand(std::wstring_view missingCommand);

winrt::fire_and_forget _terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength);

Expand Down
6 changes: 6 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.idl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ namespace Microsoft.Terminal.Control
{
IVector<String> History { get; };
String CurrentCommandline { get; };
IVector<String> QuickFixes { get; };
};

[default_interface] runtimeclass ControlCore : ICoreState
Expand Down Expand Up @@ -155,6 +156,7 @@ namespace Microsoft.Terminal.Control

String ReadEntireBuffer();
CommandHistoryContext CommandHistory();
Boolean QuickFixesAvailable { get; };

void AdjustOpacity(Single Opacity, Boolean relative);
void WindowVisibilityChanged(Boolean showOrHide);
Expand All @@ -166,6 +168,8 @@ namespace Microsoft.Terminal.Control
Boolean ShouldShowSelectCommand();
Boolean ShouldShowSelectOutput();

void ClearQuickFix();

// These events are called from some background thread
event Windows.Foundation.TypedEventHandler<Object, TitleChangedEventArgs> TitleChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> WarningBell;
Expand All @@ -174,6 +178,8 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, Object> TaskbarProgressChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> RendererEnteredErrorState;
event Windows.Foundation.TypedEventHandler<Object, ShowWindowArgs> ShowWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, SearchMissingCommandEventArgs> SearchMissingCommand;
event Windows.Foundation.TypedEventHandler<Object, Object> RefreshQuickFixUI;

// These events are always called from the UI thread (bugs aside)
event Windows.Foundation.TypedEventHandler<Object, FontSizeChangedArgs> FontSizeChanged;
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/EventArgs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
#include "KeySentEventArgs.g.cpp"
#include "CharSentEventArgs.g.cpp"
#include "StringSentEventArgs.g.cpp"
#include "SearchMissingCommandEventArgs.g.cpp"
10 changes: 10 additions & 0 deletions src/cascadia/TerminalControl/EventArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "KeySentEventArgs.g.h"
#include "CharSentEventArgs.g.h"
#include "StringSentEventArgs.g.h"
#include "SearchMissingCommandEventArgs.g.h"

namespace winrt::Microsoft::Terminal::Control::implementation
{
Expand Down Expand Up @@ -211,6 +212,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation

WINRT_PROPERTY(winrt::hstring, Text);
};

struct SearchMissingCommandEventArgs : public SearchMissingCommandEventArgsT<SearchMissingCommandEventArgs>
{
public:
SearchMissingCommandEventArgs(const winrt::hstring& missingCommand) :
MissingCommand(missingCommand) {}

til::property<winrt::hstring> MissingCommand;
};
}

namespace winrt::Microsoft::Terminal::Control::factory_implementation
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalControl/EventArgs.idl
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,9 @@ namespace Microsoft.Terminal.Control
{
String Text { get; };
}

runtimeclass SearchMissingCommandEventArgs
{
String MissingCommand { get; };
}
}
13 changes: 13 additions & 0 deletions src/cascadia/TerminalControl/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ Please either install the missing font or choose another one.</value>
<value>Select output</value>
<comment>The tooltip for a button for selecting all of a command's output</comment>
</data>
<data name="QuickFixButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Quick fix</value>
</data>
<data name="QuickFixButton.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Quick fix</value>
</data>
<data name="SessionRestoreMessage" xml:space="preserve">
<value>Restored</value>
<comment>"Restored" as in "This content was restored"</comment>
Expand All @@ -317,6 +323,13 @@ Please either install the missing font or choose another one.</value>
<value>invalid</value>
<comment>This brief message is displayed when a regular expression is invalid.</comment>
</data>
<data name="QuickFixAvailable" xml:space="preserve">
<value>Quick fix available</value>
<comment>"Quick fix" is referencing the same feature as "QuickFixButton.ToolTipService.ToolTip".</comment>
</data>
<data name="QuickFixMenu.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Quick fix</value>
</data>
<data name="PreviewTextAnnouncement" xml:space="preserve">
<value>Suggested input: {0}</value>
<comment>{Locked="{0}"} {0} will be replaced with a string of input that is suggested for the user to input</comment>
Expand Down
Loading

0 comments on commit 6589957

Please sign in to comment.