Skip to content

Commit 86ba986

Browse files
authored
Re-implement previewing, with the new TSF (#17386)
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
1 parent 125738b commit 86ba986

File tree

13 files changed

+127
-11
lines changed

13 files changed

+127
-11
lines changed

src/cascadia/TerminalApp/ActionPreviewHandlers.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ namespace winrt::TerminalApp::implementation
5050
{
5151
case ShortcutAction::SetColorScheme:
5252
case ShortcutAction::AdjustOpacity:
53+
case ShortcutAction::SendInput:
5354
{
5455
_RunRestorePreviews();
5556
break;
@@ -140,6 +141,24 @@ namespace winrt::TerminalApp::implementation
140141
});
141142
}
142143

144+
void TerminalPage::_PreviewSendInput(const Settings::Model::SendInputArgs& args)
145+
{
146+
const auto backup = _restorePreviewFuncs.empty();
147+
148+
_ApplyToActiveControls([&](const auto& control) {
149+
const auto& str{ args.Input() };
150+
control.PreviewInput(str);
151+
152+
if (backup)
153+
{
154+
_restorePreviewFuncs.emplace_back([=]() {
155+
// On dismiss:
156+
control.PreviewInput(hstring{});
157+
});
158+
}
159+
});
160+
}
161+
143162
void TerminalPage::_PreviewAction(const Settings::Model::ActionAndArgs& args)
144163
{
145164
switch (args.Action())
@@ -150,6 +169,9 @@ namespace winrt::TerminalApp::implementation
150169
case ShortcutAction::AdjustOpacity:
151170
_PreviewAdjustOpacity(args.Args().try_as<AdjustOpacityArgs>());
152171
break;
172+
case ShortcutAction::SendInput:
173+
_PreviewSendInput(args.Args().try_as<SendInputArgs>());
174+
break;
153175
}
154176

155177
// GH#9818 Other ideas for actions that could be preview-able:

src/cascadia/TerminalApp/TerminalPage.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ namespace winrt::TerminalApp::implementation
485485
void _RunRestorePreviews();
486486
void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args);
487487
void _PreviewAdjustOpacity(const Microsoft::Terminal::Settings::Model::AdjustOpacityArgs& args);
488+
void _PreviewSendInput(const Microsoft::Terminal::Settings::Model::SendInputArgs& args);
488489

489490
winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr };
490491
std::vector<std::function<void()>> _restorePreviewFuncs{};

src/cascadia/TerminalControl/ControlCore.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,4 +2893,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
28932893
return _clickedOnMark(_contextMenuBufferPosition,
28942894
[](const ::MarkExtents& m) -> bool { return !m.HasOutput(); });
28952895
}
2896+
2897+
void ControlCore::PreviewInput(std::wstring_view input)
2898+
{
2899+
_terminal->PreviewText(input);
2900+
}
2901+
28962902
}

src/cascadia/TerminalControl/ControlCore.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
257257
bool ShouldShowSelectCommand();
258258
bool ShouldShowSelectOutput();
259259

260+
void PreviewInput(std::wstring_view input);
261+
260262
RUNTIME_SETTING(float, Opacity, _settings->Opacity());
261263
RUNTIME_SETTING(float, FocusedOpacity, FocusedAppearance().Opacity());
262264
RUNTIME_SETTING(bool, UseAcrylic, _settings->UseAcrylic());

src/cascadia/TerminalControl/Resources/en-US/Resources.resw

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,8 @@ Please either install the missing font or choose another one.</value>
316316
<value>invalid</value>
317317
<comment>This brief message is displayed when a regular expression is invalid.</comment>
318318
</data>
319+
<data name="PreviewTextAnnouncement" xml:space="preserve">
320+
<value>Suggested input: {0}</value>
321+
<comment>{Locked="{0}"} {0} will be replaced with a string of input that is suggested for the user to input</comment>
322+
</data>
319323
</root>

src/cascadia/TerminalControl/TermControl.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
731731
// - <none>
732732
void TermControl::SendInput(const winrt::hstring& wstr)
733733
{
734+
// Dismiss any previewed input.
735+
PreviewInput(hstring{});
736+
734737
// only broadcast if there's an actual listener. Saves the overhead of some object creation.
735738
if (StringSent)
736739
{
@@ -3664,6 +3667,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation
36643667
return _core.OwningHwnd();
36653668
}
36663669

3670+
void TermControl::PreviewInput(const winrt::hstring& text)
3671+
{
3672+
get_self<ControlCore>(_core)->PreviewInput(text);
3673+
3674+
if (!text.empty())
3675+
{
3676+
if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) })
3677+
{
3678+
automationPeer.RaiseNotificationEvent(
3679+
AutomationNotificationKind::ItemAdded,
3680+
AutomationNotificationProcessing::All,
3681+
winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PreviewTextAnnouncement") }, text) },
3682+
L"PreviewTextAnnouncement" /* unique name for this group of notifications */);
3683+
}
3684+
}
3685+
}
3686+
36673687
void TermControl::AddMark(const Control::ScrollMark& mark)
36683688
{
36693689
_core.AddMark(mark);

src/cascadia/TerminalControl/TermControl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
7373
Windows::Foundation::Size CharacterDimensions() const;
7474
Windows::Foundation::Size MinimumSize();
7575
float SnapDimensionToGrid(const bool widthOrHeight, const float dimension);
76+
void PreviewInput(const winrt::hstring& text);
7677

7778
Windows::Foundation::Point CursorPositionInDips();
7879

src/cascadia/TerminalControl/TermControl.idl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ namespace Microsoft.Terminal.Control
127127
CommandHistoryContext CommandHistory();
128128

129129
void AdjustOpacity(Single Opacity, Boolean relative);
130+
void PreviewInput(String text);
130131

131132
// You'd think this should just be "Opacity", but UIElement already
132133
// defines an "Opacity", which we're actually not setting at all. We're

src/cascadia/TerminalCore/Terminal.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,57 @@ til::point Terminal::GetViewportRelativeCursorPosition() const noexcept
15841584
return absoluteCursorPosition - viewport.Origin();
15851585
}
15861586

1587+
void Terminal::PreviewText(std::wstring_view input)
1588+
{
1589+
// Our suggestion text is default-on-default, in italics.
1590+
static constexpr TextAttribute previewAttrs{ CharacterAttributes::Italics, TextColor{}, TextColor{}, 0u, TextColor{} };
1591+
1592+
auto lock = LockForWriting();
1593+
if (input.empty())
1594+
{
1595+
snippetPreview.text = L"";
1596+
snippetPreview.cursorPos = 0;
1597+
snippetPreview.attributes.clear();
1598+
_activeBuffer().NotifyPaintFrame();
1599+
return;
1600+
}
1601+
1602+
// When we're previewing suggestions, they might be preceded with DEL
1603+
// characters to backspace off the old command.
1604+
//
1605+
// But also, in the case of something like pwsh, there might be MORE "ghost"
1606+
// text in the buffer _after_ the commandline.
1607+
//
1608+
// We need to trim off the leading DELs, then pad out the rest of the line
1609+
// to cover any other ghost text.
1610+
// Where do the DELs end?
1611+
const auto strBegin = input.find_first_not_of(L"\x7f");
1612+
if (strBegin != std::wstring::npos)
1613+
{
1614+
// Trim them off.
1615+
input = input.substr(strBegin);
1616+
}
1617+
// How many spaces do we need, so that the preview exactly covers the entire
1618+
// prompt, all the way to the end of the viewport?
1619+
const auto bufferWidth = _GetMutableViewport().Width();
1620+
const auto cursorX = _activeBuffer().GetCursor().GetPosition().x;
1621+
const auto expectedLenTillEnd = strBegin + (static_cast<size_t>(bufferWidth) - static_cast<size_t>(cursorX));
1622+
std::wstring preview{ input };
1623+
const auto originalSize{ preview.size() };
1624+
if (expectedLenTillEnd > originalSize)
1625+
{
1626+
// pad it out
1627+
preview.insert(originalSize, expectedLenTillEnd - originalSize, L' ');
1628+
}
1629+
snippetPreview.text = til::visualize_nonspace_control_codes(preview);
1630+
// Build our composition data
1631+
const auto len = snippetPreview.text.size();
1632+
snippetPreview.attributes.clear();
1633+
snippetPreview.attributes.emplace_back(len, previewAttrs);
1634+
snippetPreview.cursorPos = len;
1635+
_activeBuffer().NotifyPaintFrame();
1636+
}
1637+
15871638
// These functions are used by TerminalInput, which must build in conhost
15881639
// against OneCore compatible signatures. See the definitions in
15891640
// VtApiRedirection.hpp (which we cannot include cross-project.)

src/cascadia/TerminalCore/Terminal.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ class Microsoft::Terminal::Core::Terminal final :
246246
const size_t GetTaskbarProgress() const noexcept;
247247

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

250251
#pragma region TextSelection
251252
// These methods are defined in TerminalSelection.cpp

src/renderer/base/renderer.cpp

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
722722
// relative to the entire buffer.
723723
const auto view = _pData->GetViewport();
724724
const auto compositionRow = _compositionCache ? _compositionCache->absoluteOrigin.y : -1;
725+
const auto& activeComposition = _pData->GetActiveComposition();
725726

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

754755
// Retrieve the text buffer so we can read information out of it.
755756
auto& buffer = _pData->GetTextBuffer();
756-
757757
// Now walk through each row of text that we need to redraw.
758758
for (auto row = redraw.Top(); row < redraw.BottomExclusive(); row++)
759759
{
@@ -772,14 +772,14 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
772772
scratch.CopyFrom(r);
773773
rowBackup = &scratch;
774774

775-
std::wstring_view text{ _pData->activeComposition.text };
775+
std::wstring_view text{ activeComposition.text };
776776
RowWriteState state{
777777
.columnLimit = r.GetReadableColumnCount(),
778778
.columnEnd = _compositionCache->absoluteOrigin.x,
779779
};
780780

781781
size_t off = 0;
782-
for (const auto& range : _pData->activeComposition.attributes)
782+
for (const auto& range : activeComposition.attributes)
783783
{
784784
const auto len = range.len;
785785
auto attr = range.attr;
@@ -1225,7 +1225,7 @@ void Renderer::_invalidateOldComposition() const
12251225
// so that _PaintBufferOutput() actually gets a chance to draw it.
12261226
void Renderer::_prepareNewComposition()
12271227
{
1228-
if (_pData->activeComposition.text.empty())
1228+
if (_pData->GetActiveComposition().text.empty())
12291229
{
12301230
return;
12311231
}
@@ -1245,17 +1245,18 @@ void Renderer::_prepareNewComposition()
12451245

12461246
auto& buffer = _pData->GetTextBuffer();
12471247
auto& scratch = buffer.GetScratchpadRow();
1248+
const auto& activeComposition = _pData->GetActiveComposition();
12481249

1249-
std::wstring_view text{ _pData->activeComposition.text };
1250+
std::wstring_view text{ activeComposition.text };
12501251
RowWriteState state{
12511252
.columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(),
12521253
};
12531254

1254-
state.text = text.substr(0, _pData->activeComposition.cursorPos);
1255+
state.text = text.substr(0, activeComposition.cursorPos);
12551256
scratch.ReplaceText(state);
12561257
const auto cursorOffset = state.columnEnd;
12571258

1258-
state.text = text.substr(_pData->activeComposition.cursorPos);
1259+
state.text = text.substr(activeComposition.cursorPos);
12591260
state.columnBegin = state.columnEnd;
12601261
scratch.ReplaceText(state);
12611262

src/renderer/inc/IRenderData.hpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ namespace Microsoft::Console::Render
7979
// Ideally this would not be stored on an interface, however ideally IRenderData should not be an interface in the first place.
8080
// This is because we should have only 1 way how to represent render data across the codebase anyway, and it should
8181
// be by-value in a struct so that we can snapshot it and release the terminal lock as quickly as possible.
82-
Composition activeComposition;
82+
const Composition& GetActiveComposition() const noexcept
83+
{
84+
return !snippetPreview.text.empty() ? snippetPreview : tsfPreview;
85+
}
86+
87+
Composition tsfPreview;
88+
Composition snippetPreview;
8389
};
8490
}

src/tsf/Implementation.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ void Implementation::Unfocus(IDataProvider* provider)
144144
renderData->UnlockConsole();
145145
});
146146

147-
if (!renderData->activeComposition.text.empty())
147+
if (!renderData->tsfPreview.text.empty())
148148
{
149-
auto& comp = renderData->activeComposition;
149+
auto& comp = renderData->tsfPreview;
150150
comp.text.clear();
151151
comp.attributes.clear();
152152
renderer->NotifyPaintFrame();
@@ -570,7 +570,7 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec)
570570
renderData->UnlockConsole();
571571
});
572572

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

0 commit comments

Comments
 (0)