Skip to content

Commit

Permalink
Re-implement previewing, with the new TSF (#17386)
Browse files Browse the repository at this point in the history
This adds support for previewing snippets, again. This time, with the
new TSF implementation. Leonard pointed me in the right direction with
this - he's the one who suggested to have a second `Composition` just
for previews like this.

Then we do some tricky magic to make it work when we're using
commandlines from shell integration, or you've got the ghost text from
powershell, etc. Then we visualize the control codes, just so they
aren't just U+FFFE diamonds.

Closes #12861
  • Loading branch information
zadjii-msft authored Jun 11, 2024
1 parent 125738b commit 86ba986
Show file tree
Hide file tree
Showing 13 changed files with 127 additions and 11 deletions.
22 changes: 22 additions & 0 deletions src/cascadia/TerminalApp/ActionPreviewHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ namespace winrt::TerminalApp::implementation
{
case ShortcutAction::SetColorScheme:
case ShortcutAction::AdjustOpacity:
case ShortcutAction::SendInput:
{
_RunRestorePreviews();
break;
Expand Down Expand Up @@ -140,6 +141,24 @@ namespace winrt::TerminalApp::implementation
});
}

void TerminalPage::_PreviewSendInput(const Settings::Model::SendInputArgs& args)
{
const auto backup = _restorePreviewFuncs.empty();

_ApplyToActiveControls([&](const auto& control) {
const auto& str{ args.Input() };
control.PreviewInput(str);

if (backup)
{
_restorePreviewFuncs.emplace_back([=]() {
// On dismiss:
control.PreviewInput(hstring{});
});
}
});
}

void TerminalPage::_PreviewAction(const Settings::Model::ActionAndArgs& args)
{
switch (args.Action())
Expand All @@ -150,6 +169,9 @@ namespace winrt::TerminalApp::implementation
case ShortcutAction::AdjustOpacity:
_PreviewAdjustOpacity(args.Args().try_as<AdjustOpacityArgs>());
break;
case ShortcutAction::SendInput:
_PreviewSendInput(args.Args().try_as<SendInputArgs>());
break;
}

// GH#9818 Other ideas for actions that could be preview-able:
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -485,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);
void _PreviewSendInput(const Microsoft::Terminal::Settings::Model::SendInputArgs& args);

winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr };
std::vector<std::function<void()>> _restorePreviewFuncs{};
Expand Down
6 changes: 6 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2893,4 +2893,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return _clickedOnMark(_contextMenuBufferPosition,
[](const ::MarkExtents& m) -> bool { return !m.HasOutput(); });
}

void ControlCore::PreviewInput(std::wstring_view input)
{
_terminal->PreviewText(input);
}

}
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
bool ShouldShowSelectCommand();
bool ShouldShowSelectOutput();

void PreviewInput(std::wstring_view input);

RUNTIME_SETTING(float, Opacity, _settings->Opacity());
RUNTIME_SETTING(float, FocusedOpacity, FocusedAppearance().Opacity());
RUNTIME_SETTING(bool, UseAcrylic, _settings->UseAcrylic());
Expand Down
4 changes: 4 additions & 0 deletions src/cascadia/TerminalControl/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,8 @@ 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="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>
</data>
</root>
20 changes: 20 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - <none>
void TermControl::SendInput(const winrt::hstring& wstr)
{
// Dismiss any previewed input.
PreviewInput(hstring{});

// only broadcast if there's an actual listener. Saves the overhead of some object creation.
if (StringSent)
{
Expand Down Expand Up @@ -3664,6 +3667,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return _core.OwningHwnd();
}

void TermControl::PreviewInput(const winrt::hstring& text)
{
get_self<ControlCore>(_core)->PreviewInput(text);

if (!text.empty())
{
if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) })
{
automationPeer.RaiseNotificationEvent(
AutomationNotificationKind::ItemAdded,
AutomationNotificationProcessing::All,
winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PreviewTextAnnouncement") }, text) },
L"PreviewTextAnnouncement" /* unique name for this group of notifications */);
}
}
}

void TermControl::AddMark(const Control::ScrollMark& mark)
{
_core.AddMark(mark);
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
Windows::Foundation::Size CharacterDimensions() const;
Windows::Foundation::Size MinimumSize();
float SnapDimensionToGrid(const bool widthOrHeight, const float dimension);
void PreviewInput(const winrt::hstring& text);

Windows::Foundation::Point CursorPositionInDips();

Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/TermControl.idl
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ namespace Microsoft.Terminal.Control
CommandHistoryContext CommandHistory();

void AdjustOpacity(Single Opacity, Boolean relative);
void PreviewInput(String text);

// You'd think this should just be "Opacity", but UIElement already
// defines an "Opacity", which we're actually not setting at all. We're
Expand Down
51 changes: 51 additions & 0 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,57 @@ til::point Terminal::GetViewportRelativeCursorPosition() const noexcept
return absoluteCursorPosition - viewport.Origin();
}

void Terminal::PreviewText(std::wstring_view input)
{
// Our suggestion text is default-on-default, in italics.
static constexpr TextAttribute previewAttrs{ CharacterAttributes::Italics, TextColor{}, TextColor{}, 0u, TextColor{} };

auto lock = LockForWriting();
if (input.empty())
{
snippetPreview.text = L"";
snippetPreview.cursorPos = 0;
snippetPreview.attributes.clear();
_activeBuffer().NotifyPaintFrame();
return;
}

// When we're previewing suggestions, they might be preceded with DEL
// characters to backspace off the old command.
//
// But also, in the case of something like pwsh, there might be MORE "ghost"
// text in the buffer _after_ the commandline.
//
// We need to trim off the leading DELs, then pad out the rest of the line
// to cover any other ghost text.
// Where do the DELs end?
const auto strBegin = input.find_first_not_of(L"\x7f");
if (strBegin != std::wstring::npos)
{
// Trim them off.
input = input.substr(strBegin);
}
// How many spaces do we need, so that the preview exactly covers the entire
// prompt, all the way to the end of the viewport?
const auto bufferWidth = _GetMutableViewport().Width();
const auto cursorX = _activeBuffer().GetCursor().GetPosition().x;
const auto expectedLenTillEnd = strBegin + (static_cast<size_t>(bufferWidth) - static_cast<size_t>(cursorX));
std::wstring preview{ input };
const auto originalSize{ preview.size() };
if (expectedLenTillEnd > originalSize)
{
// pad it out
preview.insert(originalSize, expectedLenTillEnd - originalSize, L' ');
}
snippetPreview.text = til::visualize_nonspace_control_codes(preview);
// Build our composition data
const auto len = snippetPreview.text.size();
snippetPreview.attributes.clear();
snippetPreview.attributes.emplace_back(len, previewAttrs);
snippetPreview.cursorPos = len;
_activeBuffer().NotifyPaintFrame();
}

// These functions are used by TerminalInput, which must build in conhost
// against OneCore compatible signatures. See the definitions in
// VtApiRedirection.hpp (which we cannot include cross-project.)
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ class Microsoft::Terminal::Core::Terminal final :
const size_t GetTaskbarProgress() const noexcept;

void ColorSelection(const TextAttribute& attr, winrt::Microsoft::Terminal::Core::MatchMode matchMode);
void PreviewText(std::wstring_view input);

#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
Expand Down
15 changes: 8 additions & 7 deletions src/renderer/base/renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
// relative to the entire buffer.
const auto view = _pData->GetViewport();
const auto compositionRow = _compositionCache ? _compositionCache->absoluteOrigin.y : -1;
const auto& activeComposition = _pData->GetActiveComposition();

// This is effectively the number of cells on the visible screen that need to be redrawn.
// The origin is always 0, 0 because it represents the screen itself, not the underlying buffer.
Expand Down Expand Up @@ -753,7 +754,6 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)

// Retrieve the text buffer so we can read information out of it.
auto& buffer = _pData->GetTextBuffer();

// Now walk through each row of text that we need to redraw.
for (auto row = redraw.Top(); row < redraw.BottomExclusive(); row++)
{
Expand All @@ -772,14 +772,14 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
scratch.CopyFrom(r);
rowBackup = &scratch;

std::wstring_view text{ _pData->activeComposition.text };
std::wstring_view text{ activeComposition.text };
RowWriteState state{
.columnLimit = r.GetReadableColumnCount(),
.columnEnd = _compositionCache->absoluteOrigin.x,
};

size_t off = 0;
for (const auto& range : _pData->activeComposition.attributes)
for (const auto& range : activeComposition.attributes)
{
const auto len = range.len;
auto attr = range.attr;
Expand Down Expand Up @@ -1225,7 +1225,7 @@ void Renderer::_invalidateOldComposition() const
// so that _PaintBufferOutput() actually gets a chance to draw it.
void Renderer::_prepareNewComposition()
{
if (_pData->activeComposition.text.empty())
if (_pData->GetActiveComposition().text.empty())
{
return;
}
Expand All @@ -1245,17 +1245,18 @@ void Renderer::_prepareNewComposition()

auto& buffer = _pData->GetTextBuffer();
auto& scratch = buffer.GetScratchpadRow();
const auto& activeComposition = _pData->GetActiveComposition();

std::wstring_view text{ _pData->activeComposition.text };
std::wstring_view text{ activeComposition.text };
RowWriteState state{
.columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(),
};

state.text = text.substr(0, _pData->activeComposition.cursorPos);
state.text = text.substr(0, activeComposition.cursorPos);
scratch.ReplaceText(state);
const auto cursorOffset = state.columnEnd;

state.text = text.substr(_pData->activeComposition.cursorPos);
state.text = text.substr(activeComposition.cursorPos);
state.columnBegin = state.columnEnd;
scratch.ReplaceText(state);

Expand Down
8 changes: 7 additions & 1 deletion src/renderer/inc/IRenderData.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ namespace Microsoft::Console::Render
// Ideally this would not be stored on an interface, however ideally IRenderData should not be an interface in the first place.
// This is because we should have only 1 way how to represent render data across the codebase anyway, and it should
// be by-value in a struct so that we can snapshot it and release the terminal lock as quickly as possible.
Composition activeComposition;
const Composition& GetActiveComposition() const noexcept
{
return !snippetPreview.text.empty() ? snippetPreview : tsfPreview;
}

Composition tsfPreview;
Composition snippetPreview;
};
}
6 changes: 3 additions & 3 deletions src/tsf/Implementation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ void Implementation::Unfocus(IDataProvider* provider)
renderData->UnlockConsole();
});

if (!renderData->activeComposition.text.empty())
if (!renderData->tsfPreview.text.empty())
{
auto& comp = renderData->activeComposition;
auto& comp = renderData->tsfPreview;
comp.text.clear();
comp.attributes.clear();
renderer->NotifyPaintFrame();
Expand Down Expand Up @@ -570,7 +570,7 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec)
renderData->UnlockConsole();
});

auto& comp = renderData->activeComposition;
auto& comp = renderData->tsfPreview;
comp.text = std::move(activeComposition);
comp.attributes = std::move(activeCompositionRanges);
// The code block above that calculates the `cursorPos` will clamp it to a positive number.
Expand Down

0 comments on commit 86ba986

Please sign in to comment.