Add Suggestions UI & experimental shell completions support (#14938)

There's two parts to this PR that should be considered _separately_.
1. The Suggestions UI, a new graphical menu for displaying suggestions /
completions to the user in the context of the terminal the user is
working in.
2. The VsCode shell completions protocol. This enables the shell to
invoke this UI via a VT sequence.

These are being introduced at the same time, because they both require
one another. However, I need to absolutely emphasize:

### THE FORMAT OF THE COMPLETION PROTOCOL IS EXPERIMENTAL AND SUBJECT TO
CHANGE

This is what we've prototyped with VsCode, but we're still working on
how we want to conclusively define that protocol. However, we can also
refine the Suggestions UI independently of how the protocol is actually
implemented.

This will let us rev the Suggestions UI to support other things like
tooltips, recent commands, tasks, INDEPENDENTLY of us rev'ing the
completion protocol.

So yes, they're both here, but let's not nitpick that protocol for now. 

### Checklist

* Doesn't actually close anything
* Heavily related to #3121, but I'm not gonna say that's closed till we
settle on the protocol
* See also:
  * #1595
  * #14779
  * https://github.com/microsoft/vscode/pull/171648

### Detailed Description

#### Suggestions UI

The Suggestions UI is spec'ed over in #14864, so go read that. It's
basically a transient Command Palette, that floats by the user's cursor.
It's heavily forked from the Command Palette code, with all the business
about switching modes removed. The major bit of new code is
`SuggestionsControl::Anchor`. It also supports two "modes":
* A "palette", which is like the command palette - a list with a text
box
* A "menu", which is more like the intellisense flyout. No text box.
This is the mode that the shell completions use

#### Shell Completions Protocol

I literally cannot say this enough times - this protocol is experimental
and subject to change. Build on it at your own peril. It's disabled in
Release builds (but available in preview behind
`globals.experimental.enableShellCompletionMenu`), so that when it
ships, no one can take a dependency on it accidentally.

Right now we're just taking a blob of JSON, passing that up to the App
layer, who asks `Command` to parse it and build a list of `sendInput`
actions to populate the menu with. It's not a particularly elegant
solution, but it's good enough to prototype with.

#### How do I test this?

I've been testing this in two parts. You'll need a snippet in your
powershell profile, and a keybinding in the Terminal settings to trigger
it. The work together by binding <kbd>Ctrl+space</kbd> to _essentially_
send <kbd>F12</kbd><kbd>b</kbd>. Wacky, but it works.

```json
{ "command": { "action": "sendInput","input": "\u001b[24~b" }, "keys": "ctrl+space" },
```

```ps1
function Send-Completions2 {
  $commandLine = ""
  $cursorIndex = 0
  # TODO: Since fuzzy matching exists, should completions be provided only for character after the
  #       last space and then filter on the client side? That would let you trigger ctrl+space
  #       anywhere on a word and have full completions available
  [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$commandLine, [ref]$cursorIndex)
  $completionPrefix = $commandLine

  # Get completions
  $result = "`e]633;Completions"
  if ($completionPrefix.Length -gt 0) {
    # Get and send completions
    $completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex
    if ($null -ne $completions.CompletionMatches) {
      $result += ";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);"
      $result += $completions.CompletionMatches | ConvertTo-Json -Compress
    }
  }
  $result += "`a"

  Write-Host -NoNewLine $result
}

function Set-MappedKeyHandlers {
  # VS Code send completions request (may override Ctrl+Spacebar)
  Set-PSReadLineKeyHandler -Chord 'F12,b' -ScriptBlock {
    Send-Completions2
  }
}

# Register key handlers if PSReadLine is available
if (Get-Module -Name PSReadLine) {
  Set-MappedKeyHandlers
}

```


### TODO

* [x] `(prompt | format-hex).`<kbd>Ctrl+space</kbd> -> This always
throws an exception. Seems like the payload is always clipped to

```{"CompletionText":"Ascii","ListItemText":"Ascii","ResultType":5,"ToolTip":"string
Ascii { get```
  and that ain't JSON. Investigate on the pwsh side?
This commit is contained in:
Mike Griese 2023-08-14 05:46:42 -05:00 committed by GitHub
parent 69eff7e9fd
commit a0c88bb511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2003 additions and 3 deletions

View File

@ -95,6 +95,7 @@ slnt
Sos
ssh
stakeholders
sxn
timeline
timelines
timestamped

View File

@ -315,6 +315,7 @@ CPLINFO
cplusplus
CPPCORECHECK
cppcorecheckrules
cpprest
cpprestsdk
cppwinrt
CProc
@ -1452,6 +1453,7 @@ PPEB
ppf
ppguid
ppidl
pplx
PPROC
ppropvar
ppsi

View File

@ -74,3 +74,5 @@ Author(s):
#include "../../inc/DefaultSettings.h"
#include <cppwinrt_utils.h>
#include <til/winrt.h>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "FilteredCommand.h"
#include "SuggestionsControl.g.h"
#include "AppCommandlineArgs.h"
#include <til/hash.h>
// fwdecl unittest classes
namespace TerminalAppLocalTests
{
class TabTests;
};
namespace winrt::TerminalApp::implementation
{
struct SuggestionsControl : SuggestionsControlT<SuggestionsControl>
{
SuggestionsControl();
Windows::Foundation::Collections::IObservableVector<winrt::TerminalApp::FilteredCommand> FilteredActions();
void SetCommands(const Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command>& actions);
void SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap);
bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down);
void SelectNextItem(const bool moveDown);
void ScrollPageUp();
void ScrollPageDown();
void ScrollToTop();
void ScrollToBottom();
Windows::UI::Xaml::FrameworkElement SelectedItem();
TerminalApp::SuggestionsMode Mode() const;
void Mode(TerminalApp::SuggestionsMode mode);
void Open(TerminalApp::SuggestionsMode mode,
const Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command>& commands,
Windows::Foundation::Point anchor,
Windows::Foundation::Size space,
float characterHeight);
til::typed_event<winrt::TerminalApp::SuggestionsControl, Microsoft::Terminal::Settings::Model::Command> DispatchCommandRequested;
til::typed_event<Windows::Foundation::IInspectable, Microsoft::Terminal::Settings::Model::Command> PreviewAction;
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, NoMatchesText, _PropertyChangedHandlers);
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SearchBoxPlaceholderText, _PropertyChangedHandlers);
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, ControlName, _PropertyChangedHandlers);
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, ParentCommandName, _PropertyChangedHandlers);
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, ParsedCommandLineText, _PropertyChangedHandlers);
private:
struct winrt_object_hash
{
size_t operator()(const auto& value) const noexcept
{
return til::hash(winrt::get_abi(value));
}
};
friend struct SuggestionsControlT<SuggestionsControl>; // for Xaml to bind events
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _allCommands{ nullptr };
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _currentNestedCommands{ nullptr };
Windows::Foundation::Collections::IObservableVector<winrt::TerminalApp::FilteredCommand> _filteredActions{ nullptr };
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _nestedActionStack{ nullptr };
TerminalApp::SuggestionsMode _mode{ TerminalApp::SuggestionsMode::Palette };
TerminalApp::SuggestionsDirection _direction{ TerminalApp::SuggestionsDirection::TopDown };
bool _lastFilterTextWasEmpty{ true };
Windows::Foundation::Point _anchor;
Windows::Foundation::Size _space;
Microsoft::Terminal::Settings::Model::IActionMapView _actionMap{ nullptr };
winrt::Windows::UI::Xaml::Controls::ListView::SizeChanged_revoker _sizeChangedRevoker;
winrt::TerminalApp::PaletteItemTemplateSelector _itemTemplateSelector{ nullptr };
std::unordered_map<Windows::UI::Xaml::DataTemplate, std::unordered_set<Windows::UI::Xaml::Controls::Primitives::SelectorItem, winrt_object_hash>, winrt_object_hash> _listViewItemsCache;
Windows::UI::Xaml::DataTemplate _listItemTemplate;
void _switchToMode();
void _setDirection(TerminalApp::SuggestionsDirection direction);
void _scrollToIndex(uint32_t index);
void _updateUIForStackChange();
void _updateFilteredActions();
void _dispatchCommand(const winrt::TerminalApp::FilteredCommand& command);
void _close();
void _dismissPalette();
void _filterTextChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
void _previewKeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void _keyUpHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void _rootPointerPressed(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e);
void _lostFocusHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
void _backdropPointerPressed(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e);
void _listItemClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::ItemClickEventArgs& e);
void _listItemSelectionChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e);
void _selectedCommandChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
void _moveBackButtonClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs&);
void _updateCurrentNestedCommands(const winrt::Microsoft::Terminal::Settings::Model::Command& parentCommand);
void _choosingItemContainer(const Windows::UI::Xaml::Controls::ListViewBase& sender, const Windows::UI::Xaml::Controls::ChoosingItemContainerEventArgs& args);
void _containerContentChanging(const Windows::UI::Xaml::Controls::ListViewBase& sender, const Windows::UI::Xaml::Controls::ContainerContentChangingEventArgs& args);
std::vector<winrt::TerminalApp::FilteredCommand> _collectFilteredActions();
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _commandsToFilter();
std::wstring _getTrimmedInput();
uint32_t _getNumVisibleItems();
friend class TerminalAppLocalTests::TabTests;
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(SuggestionsControl);
}

View File

@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "TabBase.idl";
import "IDirectKeyListener.idl";
import "HighlightedTextControl.idl";
import "FilteredCommand.idl";
namespace TerminalApp
{
enum SuggestionsMode
{
Palette = 0,
Menu,
// Inline,
};
enum SuggestionsDirection {
TopDown,
BottomUp
};
[default_interface] runtimeclass SuggestionsControl : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged, IDirectKeyListener
{
SuggestionsControl();
String NoMatchesText { get; };
String SearchBoxPlaceholderText { get; };
String ControlName { get; };
String ParentCommandName { get; };
Windows.UI.Xaml.FrameworkElement SelectedItem { get; };
Windows.Foundation.Collections.IObservableVector<FilteredCommand> FilteredActions { get; };
SuggestionsMode Mode { get; set; };
void SetCommands(Windows.Foundation.Collections.IVector<Microsoft.Terminal.Settings.Model.Command> actions);
void SetActionMap(Microsoft.Terminal.Settings.Model.IActionMapView actionMap);
void SelectNextItem(Boolean moveDown);
void Open(SuggestionsMode mode, IVector<Microsoft.Terminal.Settings.Model.Command> commands, Windows.Foundation.Point anchor, Windows.Foundation.Size space, Single characterHeight);
event Windows.Foundation.TypedEventHandler<SuggestionsControl, Microsoft.Terminal.Settings.Model.Command> DispatchCommandRequested;
event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Settings.Model.Command> PreviewAction;
}
}

View File

@ -0,0 +1,214 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
-->
<UserControl x:Class="TerminalApp.SuggestionsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:SettingsModel="using:Microsoft.Terminal.Settings.Model"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Microsoft.Terminal.Settings.Model"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
AllowFocusOnInteraction="True"
AutomationProperties.Name="{x:Bind ControlName, Mode=OneWay}"
IsTabStop="True"
LostFocus="_lostFocusHandler"
PointerPressed="_rootPointerPressed"
PreviewKeyDown="_previewKeyDownHandler"
PreviewKeyUp="_keyUpHandler"
TabNavigation="Cycle"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<local:EmptyStringVisibilityConverter x:Key="ParentCommandVisibilityConverter" />
<model:IconPathConverter x:Key="IconSourceConverter" />
<DataTemplate x:Key="ListItemTemplate"
x:DataType="local:FilteredCommand">
<ListViewItem Height="32"
MinHeight="0"
Padding="16,0,12,0"
HorizontalContentAlignment="Stretch"
AutomationProperties.AcceleratorKey="{x:Bind Item.KeyChordText, Mode=OneWay}"
AutomationProperties.Name="{x:Bind Item.Name, Mode=OneWay}"
FontSize="12" />
</DataTemplate>
<DataTemplate x:Key="GeneralItemTemplate"
x:DataType="local:FilteredCommand">
<Grid HorizontalAlignment="Stretch"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<!-- icon -->
<ColumnDefinition Width="Auto" />
<!-- command label -->
<ColumnDefinition Width="*" />
<!-- key chord -->
<ColumnDefinition Width="16" />
<!-- gutter for scrollbar -->
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Width="16"
Height="16"
Content="{x:Bind Item.ResolvedIcon, Mode=OneWay}" />
<local:HighlightedTextControl Grid.Column="1"
HorizontalAlignment="Left"
Text="{x:Bind HighlightedName, Mode=OneWay}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="NestedItemTemplate"
x:DataType="local:FilteredCommand">
<Grid HorizontalAlignment="Stretch"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<!-- icon -->
<ColumnDefinition Width="Auto" />
<!-- command label -->
<ColumnDefinition Width="*" />
<!-- key chord -->
<ColumnDefinition Width="16" />
<!-- gutter for scrollbar -->
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Width="16"
Height="16"
Content="{x:Bind Item.ResolvedIcon, Mode=OneWay}" />
<local:HighlightedTextControl Grid.Column="1"
HorizontalAlignment="Left"
Text="{x:Bind HighlightedName, Mode=OneWay}" />
<FontIcon Grid.Column="2"
HorizontalAlignment="Right"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xE76C;" />
</Grid>
</DataTemplate>
<local:PaletteItemTemplateSelector x:Key="PaletteItemTemplateSelector"
GeneralItemTemplate="{StaticResource GeneralItemTemplate}"
NestedItemTemplate="{StaticResource NestedItemTemplate}" />
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="6*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="8*" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Grid x:Name="_backdrop"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="0"
Grid.ColumnSpan="3"
MaxWidth="300"
MaxHeight="300"
Margin="0"
Padding="0,8,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource FlyoutPresenterBackground}"
BorderBrush="{ThemeResource FlyoutBorderThemeBrush}"
BorderThickness="{ThemeResource FlyoutBorderThemeThickness}"
CornerRadius="{ThemeResource OverlayCornerRadius}"
PointerPressed="_backdropPointerPressed"
Shadow="{StaticResource SharedShadow}"
Translation="0,0,32">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<!-- Top-down _searchBox -->
<RowDefinition Height="Auto" />
<!-- Top-down ParentCommandName -->
<RowDefinition Height="Auto" />
<!-- Top-down UNUSED???????? -->
<RowDefinition Height="*" />
<!-- _filteredActionsView -->
<RowDefinition Height="Auto" />
<!-- bottom-up _searchBox -->
</Grid.RowDefinitions>
<TextBox x:Name="_searchBox"
Grid.Row="0"
Margin="8,0,8,8"
Padding="18,8,8,8"
IsSpellCheckEnabled="False"
PlaceholderText="{x:Bind SearchBoxPlaceholderText, Mode=OneWay}"
Text=""
TextChanged="_filterTextChanged"
Visibility="Collapsed" />
<StackPanel Grid.Row="1"
Margin="8,0,8,8"
Orientation="Horizontal"
Visibility="{x:Bind ParentCommandName, Mode=OneWay, Converter={StaticResource ParentCommandVisibilityConverter}}">
<Button x:Name="_parentCommandBackButton"
x:Uid="ParentCommandBackButton"
VerticalAlignment="Center"
Click="_moveBackButtonClicked"
ClickMode="Press">
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="11"
Glyph="&#xE76b;" />
</Button>
<TextBlock x:Name="_parentCommandText"
Padding="16,4"
VerticalAlignment="Center"
FontStyle="Italic"
Text="{x:Bind ParentCommandName, Mode=OneWay}" />
</StackPanel>
<Border x:Name="_noMatchesText"
Grid.Row="3"
Height="36"
Margin="8,0,8,8"
Visibility="Collapsed">
<TextBlock Padding="12,0"
VerticalAlignment="Center"
FontStyle="Italic"
Text="{x:Bind NoMatchesText, Mode=OneWay}" />
</Border>
<ListView x:Name="_filteredActionsView"
Grid.Row="3"
Padding="4,-2,4,6"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AllowDrop="False"
CanReorderItems="False"
ChoosingItemContainer="_choosingItemContainer"
ContainerContentChanging="_containerContentChanging"
IsItemClickEnabled="True"
ItemClick="_listItemClicked"
ItemsSource="{x:Bind FilteredActions}"
SelectionChanged="_listItemSelectionChanged"
SelectionMode="Single" />
</Grid>
</Grid>
</UserControl>

View File

@ -68,6 +68,9 @@
<Page Include="CommandPalette.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="SuggestionsControl.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<!-- ========================= Headers ======================== -->
<ItemGroup>
@ -159,6 +162,9 @@
<DependentUpon>TerminalWindow.idl</DependentUpon>
</ClInclude>
<ClInclude Include="Toast.h" />
<ClInclude Include="SuggestionsControl.h">
<DependentUpon>SuggestionsControl.xaml</DependentUpon>
</ClInclude>
</ItemGroup>
<!-- ========================= Cpp Files ======================== -->
<ItemGroup>
@ -262,6 +268,9 @@
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
<ClCompile Include="Toast.cpp" />
<ClCompile Include="SuggestionsControl.cpp">
<DependentUpon>SuggestionsControl.xaml</DependentUpon>
</ClCompile>
</ItemGroup>
<!-- ========================= idl Files ======================== -->
<ItemGroup>
@ -325,6 +334,10 @@
<DependentUpon>CommandPalette.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="SuggestionsControl.idl">
<DependentUpon>SuggestionsControl.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="FilteredCommand.idl" />
<Midl Include="EmptyStringVisibilityConverter.idl" />
</ItemGroup>

View File

@ -1466,6 +1466,11 @@ namespace winrt::TerminalApp::implementation
{
CommandPaletteElement().Visibility(Visibility::Collapsed);
}
if (_suggestionsControlIs(Visibility::Visible) &&
cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette)
{
SuggestionsElement().Visibility(Visibility::Collapsed);
}
// Let's assume the user has bound the dead key "^" to a sendInput command that sends "b".
// If the user presses the two keys "^a" it'll produce "bâ", despite us marking the key event as handled.
@ -1654,6 +1659,12 @@ namespace winrt::TerminalApp::implementation
term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler });
// Don't even register for the event if the feature is compiled off.
if constexpr (Feature_ShellCompletions::IsEnabled())
{
term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler });
}
term.ContextMenu().Opening({ this, &TerminalPage::_ContextMenuOpened });
term.SelectionContextMenu().Opening({ this, &TerminalPage::_SelectionMenuOpened });
}
@ -1825,6 +1836,37 @@ namespace winrt::TerminalApp::implementation
return p;
}
SuggestionsControl TerminalPage::LoadSuggestionsUI()
{
if (const auto p = SuggestionsElement())
{
return p;
}
return _loadSuggestionsElementSlowPath();
}
bool TerminalPage::_suggestionsControlIs(WUX::Visibility visibility)
{
const auto p = SuggestionsElement();
return p && p.Visibility() == visibility;
}
SuggestionsControl TerminalPage::_loadSuggestionsElementSlowPath()
{
const auto p = FindName(L"SuggestionsElement").as<SuggestionsControl>();
p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) {
if (SuggestionsElement().Visibility() == Visibility::Collapsed)
{
_FocusActiveControl(nullptr, nullptr);
}
});
p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested });
p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler });
return p;
}
// Method Description:
// - Warn the user that they are about to close all open windows, then
// signal that we want to close everything.
@ -2787,7 +2829,7 @@ namespace winrt::TerminalApp::implementation
// Arguments:
// - sender (not used)
// - args: the arguments specifying how to set the display status to ShowWindow for our window handle
void TerminalPage::_ShowWindowChangedHandler(const IInspectable& /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args)
void TerminalPage::_ShowWindowChangedHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args)
{
_ShowWindowChangedHandlers(*this, args);
}
@ -4649,6 +4691,79 @@ namespace winrt::TerminalApp::implementation
_updateThemeColors();
}
winrt::fire_and_forget TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender,
const CompletionsChangedEventArgs args)
{
// This will come in on a background (not-UI, not output) thread.
// This won't even get hit if the velocity flag is disabled - we gate
// registering for the event based off of
// Feature_ShellCompletions::IsEnabled back in _RegisterTerminalEvents
// User must explicitly opt-in on Preview builds
if (!_settings.GlobalSettings().EnableShellCompletionMenu())
{
co_return;
}
// Parse the json string into a collection of actions
try
{
auto commandsCollection = Command::ParsePowerShellMenuComplete(args.MenuJson(),
args.ReplacementLength());
auto weakThis{ get_weak() };
Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis, commandsCollection, sender]() {
// On the UI thread...
if (const auto& page{ weakThis.get() })
{
// Open the Suggestions UI with the commands from the control
page->_OpenSuggestions(sender.try_as<TermControl>(), commandsCollection, SuggestionsMode::Menu);
}
});
}
CATCH_LOG();
}
void TerminalPage::_OpenSuggestions(
const TermControl& sender,
IVector<Command> commandsCollection,
winrt::TerminalApp::SuggestionsMode mode)
{
// ON THE UI THREAD
assert(Dispatcher().HasThreadAccess());
if (commandsCollection == nullptr)
{
return;
}
if (commandsCollection.Size() == 0)
{
if (const auto p = SuggestionsElement())
{
p.Visibility(Visibility::Collapsed);
}
return;
}
const auto& control{ sender ? sender : _GetActiveControl() };
if (!control)
{
return;
}
const auto& sxnUi{ LoadSuggestionsUI() };
const auto characterSize{ control.CharacterDimensions() };
// This is in control-relative space. We'll need to convert it to page-relative space.
const auto cursorPos{ control.CursorPositionInDips() };
const auto controlTransform = control.TransformToVisual(this->Root());
const auto realCursorPos{ controlTransform.TransformPoint({ cursorPos.X, cursorPos.Y }) }; // == controlTransform + cursorPos
const Windows::Foundation::Size windowDimensions{ gsl::narrow_cast<float>(ActualWidth()), gsl::narrow_cast<float>(ActualHeight()) };
sxnUi.Open(mode, commandsCollection, realCursorPos, windowDimensions, characterSize.Height);
}
void TerminalPage::_ContextMenuOpened(const IInspectable& sender,
const IInspectable& /*args*/)
{

View File

@ -117,6 +117,8 @@ namespace winrt::TerminalApp::implementation
winrt::hstring ApplicationVersion();
CommandPalette LoadCommandPalette();
SuggestionsControl LoadSuggestionsUI();
winrt::fire_and_forget RequestQuit();
winrt::fire_and_forget CloseWindow(bool bypassDialog);
@ -280,6 +282,8 @@ namespace winrt::TerminalApp::implementation
__declspec(noinline) CommandPalette _loadCommandPaletteSlowPath();
bool _commandPaletteIs(winrt::Windows::UI::Xaml::Visibility visibility);
__declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath();
bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility);
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowDialogHelper(const std::wstring_view& name);
@ -481,6 +485,7 @@ namespace winrt::TerminalApp::implementation
void _RunRestorePreviews();
void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args);
void _PreviewAdjustOpacity(const Microsoft::Terminal::Settings::Model::AdjustOpacityArgs& args);
winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr };
std::vector<std::function<void()>> _restorePreviewFuncs{};
@ -513,7 +518,11 @@ namespace winrt::TerminalApp::implementation
void _updateAllTabCloseButtons(const winrt::TerminalApp::TabBase& focusedTab);
void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme);
void _ShowWindowChangedHandler(const IInspectable& sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args);
winrt::fire_and_forget _ControlCompletionsChangedHandler(const winrt::Windows::Foundation::IInspectable sender, const winrt::Microsoft::Terminal::Control::CompletionsChangedEventArgs args);
void _OpenSuggestions(const Microsoft::Terminal::Control::TermControl& sender, Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::Command> commandsCollection, winrt::TerminalApp::SuggestionsMode mode);
void _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args);
winrt::fire_and_forget _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e);

View File

@ -175,6 +175,14 @@
PreviewKeyDown="_KeyDownHandler"
Visibility="Collapsed" />
<local:SuggestionsControl x:Name="SuggestionsElement"
Grid.Row="2"
HorizontalAlignment="Left"
VerticalAlignment="Top"
x:Load="False"
PreviewKeyDown="_KeyDownHandler"
Visibility="Collapsed" />
<!--
A TeachingTip with IsLightDismissEnabled="True" will immediately
dismiss itself if the window is unfocused (In Xaml Islands). This is

View File

@ -84,3 +84,5 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider);
#include <cppwinrt_utils.h>
#include <wil/cppwinrt_helpers.h> // must go after the CoreDispatcher type is defined
#include <til/winrt.h>

View File

@ -119,6 +119,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
auto pfnPlayMidiNote = std::bind(&ControlCore::_terminalPlayMidiNote, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
_terminal->SetPlayMidiNoteCallback(pfnPlayMidiNote);
auto pfnCompletionsChanged = [=](auto&& menuJson, auto&& replaceLength) { _terminalCompletionsChanged(menuJson, replaceLength); };
_terminal->CompletionsChangedCallback(pfnCompletionsChanged);
// 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
@ -2228,6 +2231,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
}
winrt::fire_and_forget ControlCore::_terminalCompletionsChanged(std::wstring_view menuJson,
unsigned int replaceLength)
{
auto args = winrt::make_self<CompletionsChangedEventArgs>(winrt::hstring{ menuJson },
replaceLength);
co_await winrt::resume_background();
_CompletionsChangedHandlers(*this, *args);
}
void ControlCore::_selectSpan(til::point_span s)
{
const auto bufferSize{ _terminal->GetTextBuffer().GetSize() };

View File

@ -258,6 +258,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
TYPED_EVENT(ShowWindowChanged, IInspectable, Control::ShowWindowArgs);
TYPED_EVENT(UpdateSelectionMarkers, IInspectable, Control::UpdateSelectionMarkersEventArgs);
TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs);
TYPED_EVENT(CompletionsChanged, IInspectable, Control::CompletionsChangedEventArgs);
TYPED_EVENT(CloseTerminalRequested, IInspectable, IInspectable);
TYPED_EVENT(RestartTerminalRequested, IInspectable, IInspectable);
@ -347,6 +349,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _terminalPlayMidiNote(const int noteNumber,
const int velocity,
const std::chrono::microseconds duration);
winrt::fire_and_forget _terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength);
#pragma endregion
MidiAudio _midiAudio;

View File

@ -175,5 +175,8 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, Object> RestartTerminalRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> Attached;
event Windows.Foundation.TypedEventHandler<Object, CompletionsChangedEventArgs> CompletionsChanged;
};
}

View File

@ -15,6 +15,7 @@
#include "FoundResultsArgs.g.cpp"
#include "ShowWindowArgs.g.cpp"
#include "UpdateSelectionMarkersEventArgs.g.cpp"
#include "CompletionsChangedEventArgs.g.cpp"
#include "KeySentEventArgs.g.cpp"
#include "CharSentEventArgs.g.cpp"
#include "StringSentEventArgs.g.cpp"

View File

@ -15,6 +15,7 @@
#include "FoundResultsArgs.g.h"
#include "ShowWindowArgs.g.h"
#include "UpdateSelectionMarkersEventArgs.g.h"
#include "CompletionsChangedEventArgs.g.h"
#include "KeySentEventArgs.g.h"
#include "CharSentEventArgs.g.h"
#include "StringSentEventArgs.g.h"
@ -183,6 +184,19 @@ namespace winrt::Microsoft::Terminal::Control::implementation
WINRT_PROPERTY(bool, ClearMarkers, false);
};
struct CompletionsChangedEventArgs : public CompletionsChangedEventArgsT<CompletionsChangedEventArgs>
{
public:
CompletionsChangedEventArgs(const winrt::hstring menuJson, const unsigned int replaceLength) :
_MenuJson(menuJson),
_ReplacementLength(replaceLength)
{
}
WINRT_PROPERTY(winrt::hstring, MenuJson, L"");
WINRT_PROPERTY(uint32_t, ReplacementLength, 0);
};
struct KeySentEventArgs : public KeySentEventArgsT<KeySentEventArgs>
{
public:

View File

@ -90,6 +90,12 @@ namespace Microsoft.Terminal.Control
Boolean ClearMarkers { get; };
}
runtimeclass CompletionsChangedEventArgs
{
String MenuJson { get; };
UInt32 ReplacementLength { get; };
}
runtimeclass KeySentEventArgs
{
UInt16 VKey { get; };

View File

@ -104,6 +104,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_revokers.ConnectionStateChanged = _core.ConnectionStateChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleConnectionStateChanged });
_revokers.ShowWindowChanged = _core.ShowWindowChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleShowWindowChanged });
_revokers.CloseTerminalRequested = _core.CloseTerminalRequested(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleCloseTerminalRequested });
_revokers.CompletionsChanged = _core.CompletionsChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleCompletionsChanged });
_revokers.RestartTerminalRequested = _core.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleRestartTerminalRequested });
_revokers.PasteFromClipboard = _interactivity.PasteFromClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubblePasteFromClipboard });
@ -2349,7 +2350,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Method Description:
// - Get the size of a single character of this control. The size is in
// DIPs. If you need it in _pixels_, you'll need to multiply by the
// _pixels_. If you want it in DIPs, you'll need to DIVIDE by the
// current display scaling.
// Arguments:
// - <none>
@ -3471,6 +3472,34 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_core.ColorSelection(fg, bg, matchMode);
}
// Returns the text cursor's position relative to our origin, in DIPs.
Windows::Foundation::Point TermControl::CursorPositionInDips()
{
const til::point cursorPos{ _core.CursorPosition() };
// CharacterDimensions returns a font size in pixels.
const auto fontSize{ CharacterDimensions() };
// Convert text buffer cursor position to client coordinate position
// within the window. This point is in _pixels_
const Windows::Foundation::Point clientCursorPos{ cursorPos.x * fontSize.Width,
cursorPos.y * fontSize.Height };
// Get scale factor for view
const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
// Adjust to DIPs
const til::point clientCursorInDips{ til::math::rounding, clientCursorPos.X / scaleFactor, clientCursorPos.Y / scaleFactor };
// Account for the margins, which are in DIPs
auto padding{ GetPadding() };
til::point relativeToOrigin{ til::math::flooring,
clientCursorInDips.x + padding.Left,
clientCursorInDips.y + padding.Top };
return relativeToOrigin.to_winrt_point();
}
void TermControl::_contextMenuHandler(IInspectable /*sender*/,
Control::ContextMenuRequestedEventArgs args)
{

View File

@ -51,6 +51,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
Windows::Foundation::Size MinimumSize();
float SnapDimensionToGrid(const bool widthOrHeight, const float dimension);
Windows::Foundation::Point CursorPositionInDips();
void WindowVisibilityChanged(const bool showOrHide);
void ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, Core::MatchMode matchMode);
@ -172,6 +174,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
BUBBLED_FORWARDED_TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable);
BUBBLED_FORWARDED_TYPED_EVENT(ShowWindowChanged, IInspectable, Control::ShowWindowArgs);
BUBBLED_FORWARDED_TYPED_EVENT(CloseTerminalRequested, IInspectable, IInspectable);
BUBBLED_FORWARDED_TYPED_EVENT(CompletionsChanged, IInspectable, Control::CompletionsChangedEventArgs);
BUBBLED_FORWARDED_TYPED_EVENT(RestartTerminalRequested, IInspectable, IInspectable);
BUBBLED_FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs);
@ -394,7 +397,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
Control::ControlCore::ConnectionStateChanged_revoker ConnectionStateChanged;
Control::ControlCore::ShowWindowChanged_revoker ShowWindowChanged;
Control::ControlCore::CloseTerminalRequested_revoker CloseTerminalRequested;
Control::ControlCore::CompletionsChanged_revoker CompletionsChanged;
Control::ControlCore::RestartTerminalRequested_revoker RestartTerminalRequested;
// These are set up in _InitializeTerminal
Control::ControlCore::RendererWarning_revoker RendererWarning;
Control::ControlCore::SwapChainChanged_revoker SwapChainChanged;

View File

@ -53,10 +53,14 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, Object> TabColorChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> ReadOnlyChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> FocusFollowMouseRequested;
event Windows.Foundation.TypedEventHandler<Object, CompletionsChangedEventArgs> CompletionsChanged;
event Windows.Foundation.TypedEventHandler<Object, KeySentEventArgs> KeySent;
event Windows.Foundation.TypedEventHandler<Object, CharSentEventArgs> CharSent;
event Windows.Foundation.TypedEventHandler<Object, StringSentEventArgs> StringSent;
Microsoft.UI.Xaml.Controls.CommandBarFlyout ContextMenu { get; };
Microsoft.UI.Xaml.Controls.CommandBarFlyout SelectionContextMenu { get; };
@ -123,6 +127,8 @@ namespace Microsoft.Terminal.Control
void ColorSelection(SelectionColor fg, SelectionColor bg, Microsoft.Terminal.Core.MatchMode matchMode);
Windows.Foundation.Point CursorPositionInDips { get; };
void ShowContextMenu();
void Detach();

View File

@ -1259,6 +1259,11 @@ const size_t Microsoft::Terminal::Core::Terminal::GetTaskbarProgress() const noe
return _taskbarProgress;
}
void Microsoft::Terminal::Core::Terminal::CompletionsChangedCallback(std::function<void(std::wstring_view, unsigned int)> pfn) noexcept
{
_pfnCompletionsChanged.swap(pfn);
}
Scheme Terminal::GetColorScheme() const
{
Scheme s;

View File

@ -152,6 +152,9 @@ public:
bool IsVtInputEnabled() const noexcept override;
void NotifyAccessibilityChange(const til::rect& changedRect) noexcept override;
void NotifyBufferRotation(const int delta) override;
void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override;
#pragma endregion
void ClearMark();
@ -225,6 +228,7 @@ public:
void TaskbarProgressChangedCallback(std::function<void()> pfn) noexcept;
void SetShowWindowCallback(std::function<void(bool)> pfn) noexcept;
void SetPlayMidiNoteCallback(std::function<void(const int, const int, const std::chrono::microseconds)> pfn) noexcept;
void CompletionsChangedCallback(std::function<void(std::wstring_view, unsigned int)> pfn) noexcept;
void SetCursorOn(const bool isOn);
bool IsCursorBlinkingAllowed() const noexcept;
@ -322,6 +326,7 @@ private:
std::function<void()> _pfnTaskbarProgressChanged;
std::function<void(bool)> _pfnShowWindowChanged;
std::function<void(const int, const int, const std::chrono::microseconds)> _pfnPlayMidiNote;
std::function<void(std::wstring_view, unsigned int)> _pfnCompletionsChanged;
RenderSettings _renderSettings;
std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine;

View File

@ -431,6 +431,14 @@ void Terminal::NotifyAccessibilityChange(const til::rect& /*changedRect*/) noexc
// This is only needed in conhost. Terminal handles accessibility in another way.
}
void Terminal::InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength)
{
if (_pfnCompletionsChanged)
{
_pfnCompletionsChanged(menuJson, replaceLength);
}
}
void Terminal::NotifyBufferRotation(const int delta)
{
// Update our selection, so it doesn't move as the buffer is cycled

View File

@ -632,4 +632,102 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return newCommands;
}
winrt::Windows::Foundation::Collections::IVector<Model::Command> Command::ParsePowerShellMenuComplete(winrt::hstring json, int32_t replaceLength)
{
if (json.empty())
{
return nullptr;
}
auto data = winrt::to_string(json);
std::string errs;
static std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder{}.newCharReader() };
Json::Value root;
if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs))
{
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
std::vector<Model::Command> result;
const auto parseElement = [&](const auto& element) {
winrt::hstring completionText;
winrt::hstring listText;
JsonUtils::GetValueForKey(element, "CompletionText", completionText);
JsonUtils::GetValueForKey(element, "ListItemText", listText);
auto args = winrt::make_self<SendInputArgs>(
winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"),
L"",
replaceLength,
static_cast<std::wstring_view>(completionText)) });
Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args };
auto c = winrt::make_self<Command>();
c->_name = listText;
c->_ActionAndArgs = actionAndArgs;
// Try to assign a sensible icon based on the result type. These are
// roughly chosen to align with the icons in
// https://github.com/PowerShell/PowerShellEditorServices/pull/1738
// as best as possible.
if (const auto resultType{ JsonUtils::GetValueForKey<int>(element, "ResultType") })
{
// PowerShell completion result -> Segoe Fluent icon value & name
switch (resultType)
{
case 1: // History -> 0xe81c History
c->_iconPath = L"\ue81c";
break;
case 2: // Command -> 0xecaa AppIconDefault
c->_iconPath = L"\uecaa";
break;
case 3: // ProviderItem -> 0xe8e4 AlignLeft
c->_iconPath = L"\ue8e4";
break;
case 4: // ProviderContainer -> 0xe838 FolderOpen
c->_iconPath = L"\ue838";
break;
case 5: // Property -> 0xe7c1 Flag
c->_iconPath = L"\ue7c1";
break;
case 6: // Method -> 0xecaa AppIconDefault
c->_iconPath = L"\uecaa";
break;
case 7: // ParameterName -> 0xe7c1 Flag
c->_iconPath = L"\ue7c1";
break;
case 8: // ParameterValue -> 0xf000 KnowledgeArticle
c->_iconPath = L"\uf000";
break;
case 10: // Namespace -> 0xe943 Code
c->_iconPath = L"\ue943";
break;
case 13: // DynamicKeyword -> 0xe945 LightningBolt
c->_iconPath = L"\ue945";
break;
}
}
result.push_back(*c);
};
if (root.isArray())
{
// If we got a whole array of suggestions, parse each one.
for (const auto& element : root)
{
parseElement(element);
}
}
else if (root.isObject())
{
// If we instead only got a single element back, just parse the root element.
parseElement(root);
}
return winrt::single_threaded_vector<Model::Command>(std::move(result));
}
}

View File

@ -66,6 +66,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
hstring IconPath() const noexcept;
void IconPath(const hstring& val);
static Windows::Foundation::Collections::IVector<Model::Command> ParsePowerShellMenuComplete(winrt::hstring json, int32_t replaceLength);
WINRT_PROPERTY(ExpandCommandType, IterateOn, ExpandCommandType::None);
WINRT_PROPERTY(Model::ActionAndArgs, ActionAndArgs);

View File

@ -45,5 +45,7 @@ namespace Microsoft.Terminal.Settings.Model
Boolean HasNestedCommands { get; };
Windows.Foundation.Collections.IMapView<String, Command> NestedCommands { get; };
static IVector<Command> ParsePowerShellMenuComplete(String json, Int32 replaceLength);
}
}

View File

@ -98,6 +98,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_SETTING(Boolean, ShowAdminShield);
INHERITABLE_SETTING(IVector<NewTabMenuEntry>, NewTabMenu);
INHERITABLE_SETTING(Boolean, EnableColorSelection);
INHERITABLE_SETTING(Boolean, EnableShellCompletionMenu);
INHERITABLE_SETTING(Boolean, IsolatedMode);
INHERITABLE_SETTING(Boolean, AllowHeadless);
INHERITABLE_SETTING(String, SearchWebDefaultQueryUrl);

View File

@ -63,6 +63,7 @@ Author(s):
X(bool, ShowAdminShield, "showAdminShield", true) \
X(bool, TrimPaste, "trimPaste", true) \
X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \
X(bool, EnableShellCompletionMenu, "experimental.enableShellCompletionMenu", false) \
X(winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>, NewTabMenu, "newTabMenu", winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} })) \
X(bool, AllowHeadless, "compatibility.allowHeadless", false) \
X(bool, IsolatedMode, "compatibility.isolatedMode", false) \

View File

@ -163,6 +163,17 @@
</alwaysEnabledBrandingTokens>
</feature>
<feature>
<name>Feature_ShellCompletions</name>
<description>An experimental escape sequence for client applications to request the Terminal display a list of suggestions.</description>
<id>3121</id>
<stage>AlwaysDisabled</stage>
<alwaysEnabledBrandingTokens>
<brandingToken>Dev</brandingToken>
<brandingToken>Preview</brandingToken>
</alwaysEnabledBrandingTokens>
</feature>
<feature>
<name>Feature_VtChecksumReport</name>
<description>Enables the DECRQCRA checksum report, which can be used to read the screen contents</description>

View File

@ -453,3 +453,7 @@ void ConhostInternalGetSet::MarkCommandFinish(std::optional<unsigned int> /*erro
{
// Not implemented for conhost.
}
void ConhostInternalGetSet::InvokeCompletions(std::wstring_view /*menuJson*/, unsigned int /*replaceLength*/)
{
// Not implemented for conhost.
}

View File

@ -74,6 +74,8 @@ public:
void MarkOutputStart() override;
void MarkCommandFinish(std::optional<unsigned int> error) override;
void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override;
private:
Microsoft::Console::IIoProvider& _io;
};

View File

@ -137,6 +137,8 @@ public:
virtual bool DoFinalTermAction(const std::wstring_view string) = 0;
virtual bool DoVsCodeAction(const std::wstring_view string) = 0;
virtual StringHandler DownloadDRCS(const VTInt fontNumber,
const VTParameter startChar,
const DispatchTypes::DrcsEraseControl eraseControl,

View File

@ -85,5 +85,7 @@ namespace Microsoft::Console::VirtualTerminal
virtual void MarkCommandStart() = 0;
virtual void MarkOutputStart() = 0;
virtual void MarkCommandFinish(std::optional<unsigned int> error) = 0;
virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0;
};
}

View File

@ -3738,6 +3738,86 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string)
// modify the state of that mark as we go.
return false;
}
// Method Description:
// - Performs a VsCode action
// - Currently, the actions we support are:
// * Completions: An experimental protocol for passing shell completion
// information from the shell to the terminal. This sequence is still under
// active development, and subject to change.
// - Not actually used in conhost
// Arguments:
// - string: contains the parameters that define which action we do
// Return Value:
// - false in conhost, true for the SetMark action, otherwise false.
bool AdaptDispatch::DoVsCodeAction(const std::wstring_view string)
{
// This is not implemented in conhost.
if (_api.IsConsolePty())
{
// Flush the frame manually to make sure this action happens at the right time.
_renderer.TriggerFlush(false);
return false;
}
if constexpr (!Feature_ShellCompletions::IsEnabled())
{
return false;
}
const auto parts = Utils::SplitString(string, L';');
if (parts.size() < 1)
{
return false;
}
const auto action = til::at(parts, 0);
if (action == L"Completions")
{
// The structure of the message is as follows:
// `e]633;
// 0: Completions;
// 1: $($completions.ReplacementIndex);
// 2: $($completions.ReplacementLength);
// 3: $($cursorIndex);
// 4: $completions.CompletionMatches | ConvertTo-Json
unsigned int replacementIndex = 0;
unsigned int replacementLength = 0;
unsigned int cursorIndex = 0;
bool succeeded = (parts.size() >= 2) &&
(Utils::StringToUint(til::at(parts, 1), replacementIndex));
succeeded &= (parts.size() >= 3) &&
(Utils::StringToUint(til::at(parts, 2), replacementLength));
succeeded &= (parts.size() >= 4) &&
(Utils::StringToUint(til::at(parts, 3), cursorIndex));
// VsCode is using cursorIndex and replacementIndex, but we aren't currently.
if (succeeded)
{
// Get the combined lengths of parts 0-3, plus the semicolons. We
// need this so that we can just pass the remainder of the string.
const auto prefixLength = til::at(parts, 0).size() + 1 +
til::at(parts, 1).size() + 1 +
til::at(parts, 2).size() + 1 +
til::at(parts, 3).size() + 1;
if (prefixLength > string.size())
{
return true;
}
// Get the remainder of the string
const auto remainder = string.substr(prefixLength);
_api.InvokeCompletions(parts.size() < 5 ? L"" : remainder,
replacementLength);
}
// If it's poorly formatted, just eat it
return true;
}
return false;
}
// Method Description:
// - DECDLD - Downloads one or more characters of a dynamically redefinable

View File

@ -139,6 +139,8 @@ namespace Microsoft::Console::VirtualTerminal
bool DoFinalTermAction(const std::wstring_view string) override;
bool DoVsCodeAction(const std::wstring_view string) override;
StringHandler DownloadDRCS(const VTInt fontNumber,
const VTParameter startChar,
const DispatchTypes::DrcsEraseControl eraseControl,

View File

@ -130,6 +130,8 @@ public:
bool DoFinalTermAction(const std::wstring_view /*string*/) override { return false; }
bool DoVsCodeAction(const std::wstring_view /*string*/) override { return false; }
StringHandler DownloadDRCS(const VTInt /*fontNumber*/,
const VTParameter /*startChar*/,
const DispatchTypes::DrcsEraseControl /*eraseControl*/,

View File

@ -231,6 +231,12 @@ public:
{
Log::Comment(L"MarkCommandFinish MOCK called...");
}
void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override
{
Log::Comment(L"InvokeCompletions MOCK called...");
VERIFY_ARE_EQUAL(_expectedMenuJson, menuJson);
VERIFY_ARE_EQUAL(_expectedReplaceLength, replaceLength);
}
void PrepData()
{
@ -378,6 +384,9 @@ public:
bool _getConsoleOutputCPResult = false;
bool _expectedShowWindow = false;
std::wstring _expectedMenuJson{};
unsigned int _expectedReplaceLength = 0;
private:
HANDLE _hCon;
};
@ -3005,6 +3014,33 @@ public:
_testGetSet->ValidateInputEvent(expectedResponse.c_str());
}
TEST_METHOD(MenuCompletionsTests)
{
_testGetSet->PrepData();
Log::Comment(L"Not enough parameters");
VERIFY_IS_FALSE(_pDispatch->DoVsCodeAction(LR"(garbage)"));
Log::Comment(L"Not enough parameters");
VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions)"));
VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;)"));
VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;)"));
VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20)"));
VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;)"));
Log::Comment(L"No trailing semicolon");
VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;3)"));
Log::Comment(L"Normal, good case");
_testGetSet->_expectedMenuJson = LR"({ "foo": 1, "bar": 2 })";
_testGetSet->_expectedReplaceLength = 2;
VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;1;2;3;{ "foo": 1, "bar": 2 })"));
Log::Comment(L"JSON has a semicolon in it");
_testGetSet->_expectedMenuJson = LR"({ "foo": "what;ever", "bar": 2 })";
_testGetSet->_expectedReplaceLength = 20;
VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;30;{ "foo": "what;ever", "bar": 2 })"));
}
private:
TerminalInput _terminalInput;
std::unique_ptr<TestGetSet> _testGetSet;

View File

@ -868,6 +868,11 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/,
success = _dispatch->DoFinalTermAction(string);
break;
}
case OscActionCodes::VsCodeAction:
{
success = _dispatch->DoVsCodeAction(string);
break;
}
default:
// If no functions to call, overall dispatch was a failure.
success = false;

View File

@ -214,6 +214,7 @@ namespace Microsoft::Console::VirtualTerminal
ResetBackgroundColor = 111, // Not implemented
ResetCursorColor = 112,
FinalTermAction = 133,
VsCodeAction = 633,
ITerm2Action = 1337,
};