Skip to content

Add support for "reflow"ing the Terminal buffer #4741

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 54 commits into from
Mar 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b5c8c85
let's first move reflowing to the text buffer
zadjii-msft Jan 13, 2020
9aec694
add a doc comment because I'm not a barbarian
zadjii-msft Jan 13, 2020
1a2654d
Try to wrap the line properly with conpty
zadjii-msft Jan 13, 2020
aae6ce6
This works, fixing the ECH at end of wrapped line bug
zadjii-msft Jan 14, 2020
416be46
This is some cleanup, almost ready for PR but I need to write tests w…
zadjii-msft Jan 14, 2020
7f341a2
Merge branch 'master' into dev/migrie/f/conpty-wrapping-003
zadjii-msft Jan 27, 2020
edeb346
I think this is all I need to support wrapped lines in the Terminal
zadjii-msft Jan 27, 2020
ce3138c
Let's pull all the test fixes into their own file
zadjii-msft Jan 28, 2020
4a7f2e4
Merge branch 'dev/migrie/b/just-conpty-test-fixes' into dev/migrie/f/…
zadjii-msft Jan 28, 2020
bfde821
A simple test for wrapped lines
zadjii-msft Jan 28, 2020
e000388
add a roundtrip test
zadjii-msft Jan 28, 2020
c040a82
I've found a bug with the line wrapping, going to go update the Paint…
zadjii-msft Jan 28, 2020
0755fd7
This is polished for PR, ready to go in after #4382
zadjii-msft Jan 28, 2020
86623f5
Add PaintCursor tracing
zadjii-msft Jan 29, 2020
7fd5d51
This actually fixes this bug - different terminals EOL wrap different…
zadjii-msft Jan 29, 2020
9b6554b
Add some comments for PR
zadjii-msft Jan 29, 2020
0a98cce
Try adding a test, but I can't get the test to repro the original beh…
zadjii-msft Jan 29, 2020
40b4966
Revert "Try adding a test, but I can't get the test to repro the orig…
zadjii-msft Jan 29, 2020
e0d251c
Merge remote-tracking branch 'origin/master' into dev/migrie/b/1245-I…
zadjii-msft Jan 29, 2020
b3de042
remove some old TODO comments
zadjii-msft Jan 30, 2020
96642de
remove other dead code for PR
zadjii-msft Jan 30, 2020
5a72af9
Merge branch 'dev/migrie/f/conpty-wrapping-003' into dev/migrie/f/ref…
zadjii-msft Jan 30, 2020
1788cb1
Merge branch 'dev/migrie/b/1245-I-actually-did-it-this-time' into dev…
zadjii-msft Jan 30, 2020
74a5283
ResizeWithReflow the Terminal buffer
zadjii-msft Jan 30, 2020
9580715
wait why does this work so well
zadjii-msft Jan 30, 2020
edea9a3
Cleanup for review - this is a _great_ fix for #3490 as well as #1465
zadjii-msft Jan 30, 2020
097b62c
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Jan 31, 2020
2e815c8
fix tests
zadjii-msft Jan 31, 2020
de9911d
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Feb 7, 2020
e653713
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Feb 11, 2020
1d87f66
Merge branch 'master' into dev/migrie/f/reflow-buffer-on-resize
zadjii-msft Feb 28, 2020
0c91a9b
@ our static analysis build: you're wrong here, but fine
zadjii-msft Feb 28, 2020
0f283b4
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Mar 2, 2020
2ef2d88
add safemath for carlos
zadjii-msft Mar 2, 2020
cc7b2d3
okay I'll actually build the SA locally
zadjii-msft Mar 2, 2020
a8913ce
Squash merge of dev/migrie/f/resize-quirk
zadjii-msft Mar 5, 2020
e0626c8
audit mode is hard sometimes
zadjii-msft Mar 5, 2020
4c368c3
delete some old dead code
zadjii-msft Mar 5, 2020
4e3d008
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Mar 6, 2020
6070095
Fix wrapping lines far too frequently
zadjii-msft Mar 6, 2020
7cca547
Some minor comments from Michael
zadjii-msft Mar 6, 2020
1044d49
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Mar 10, 2020
277f280
Enable conpty to be resized even when headed
zadjii-msft Mar 10, 2020
7a40fe3
These bugs were twofold
zadjii-msft Mar 10, 2020
a3a9464
When we increase the width of the buffer, always use the old viewport…
zadjii-msft Mar 10, 2020
4ca280c
When we change the width at all, use the top, not the bottom.
zadjii-msft Mar 10, 2020
077f678
This seems like it finally does it
zadjii-msft Mar 10, 2020
85efaa7
git merge --squash dev/migrie/f/more-reflow-experiments
zadjii-msft Mar 11, 2020
9127e1c
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
zadjii-msft Mar 12, 2020
61ccc28
fix the tests
zadjii-msft Mar 12, 2020
aff3cc5
someday I"ll actually save all the files with changes in them before …
zadjii-msft Mar 12, 2020
0532c66
fix the renderer test
zadjii-msft Mar 12, 2020
5394b30
make this a reference to an optional. These tests better pass...
zadjii-msft Mar 12, 2020
7ecd3c6
Merge remote-tracking branch 'origin/master' into dev/migrie/f/reflow…
DHowett Mar 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -573,27 +573,21 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
}

//Routine Description:
// - Retrieves the position of the last non-space character on the final line of the text buffer.
// - By default, we search the entire buffer to find the last non-space character
//Arguments:
// - <none>
//Return Value:
// - Coordinate position in screen coordinates (offset coordinates, not array index coordinates).
COORD TextBuffer::GetLastNonSpaceCharacter() const
{
return GetLastNonSpaceCharacter(GetSize());
}

//Routine Description:
// - Retrieves the position of the last non-space character in the given viewport
// - This is basically an optimized version of GetLastNonSpaceCharacter(), and can be called when
// - we know the last character is within the given viewport (so we don't need to check the entire buffer)
// - Retrieves the position of the last non-space character in the given
// viewport
// - By default, we search the entire buffer to find the last non-space
// character.
// - If we know the last character is within the given viewport (so we don't
// need to check the entire buffer), we can provide a value in viewOptional
// that we'll use to search for the last character in.
//Arguments:
// - The viewport
//Return value:
// - Coordinate position (relative to the text buffer)
COORD TextBuffer::GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const
COORD TextBuffer::GetLastNonSpaceCharacter(std::optional<const Microsoft::Console::Types::Viewport> viewOptional) const
{
const auto viewport = viewOptional.has_value() ? viewOptional.value() : GetSize();

COORD coordEndOfText = { 0 };
// Search the given viewport by starting at the bottom.
coordEndOfText.Y = viewport.BottomInclusive();
Expand Down Expand Up @@ -1872,9 +1866,18 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
// Arguments:
// - oldBuffer - the text buffer to copy the contents FROM
// - newBuffer - the text buffer to copy the contents TO
// - lastCharacterViewport - Optional. If the caller knows that the last
// nonspace character is in a particular Viewport, the caller can provide this
// parameter as an optimization, as opposed to searching the entire buffer.
// - oldViewportTop - Optional. The caller can provide a row in this parameter
// and we'll calculate the position of the _end_ of that row in the new
// buffer. The row's new value is placed back into this parameter.
// Return Value:
// - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT.
HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
TextBuffer& newBuffer,
const std::optional<Viewport> lastCharacterViewport,
std::optional<short>& oldViewportTop)
{
Cursor& oldCursor = oldBuffer.GetCursor();
Cursor& newCursor = newBuffer.GetCursor();
Expand All @@ -1886,14 +1889,14 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
// place the new cursor back on the equivalent character in
// the new buffer.
const COORD cOldCursorPos = oldCursor.GetPosition();
const COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter();
const COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport);

short const cOldRowsTotal = cOldLastChar.Y + 1;
short const cOldColsTotal = oldBuffer.GetSize().Width();
const short cOldRowsTotal = cOldLastChar.Y + 1;
const short cOldColsTotal = oldBuffer.GetSize().Width();

COORD cNewCursorPos = { 0 };
bool fFoundCursorPos = false;

bool foundOldRow = false;
HRESULT hr = S_OK;
// Loop through all the rows of the old buffer and reprint them into the new buffer
for (short iOldRow = 0; iOldRow < cOldRowsTotal; iOldRow++)
Expand Down Expand Up @@ -1953,6 +1956,19 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
}
CATCH_RETURN();
}

// If we found the old row that the caller was interested in, set the
// out value of that parameter to the cursor's current Y position (the
// new location of the _end_ of that row in the buffer).
if (oldViewportTop.has_value() && !foundOldRow)
{
if (iOldRow >= oldViewportTop.value())
{
oldViewportTop = newCursor.GetPosition().Y;
foundOldRow = true;
}
}

if (SUCCEEDED(hr))
{
// If we didn't have a full row to copy, insert a new
Expand Down
8 changes: 5 additions & 3 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ class TextBuffer final
// Scroll needs access to this to quickly rotate around the buffer.
bool IncrementCircularBuffer(const bool inVtMode = false);

COORD GetLastNonSpaceCharacter() const;
COORD GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const;
COORD GetLastNonSpaceCharacter(std::optional<const Microsoft::Console::Types::Viewport> viewOptional = std::nullopt) const;

Cursor& GetCursor() noexcept;
const Cursor& GetCursor() const noexcept;
Expand Down Expand Up @@ -162,7 +161,10 @@ class TextBuffer final
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);

static HRESULT Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer);
static HRESULT Reflow(TextBuffer& oldBuffer,
TextBuffer& newBuffer,
const std::optional<Microsoft::Console::Types::Viewport> lastCharacterViewport,
std::optional<short>& oldViewportTop);

private:
std::deque<ROW> _storage;
Expand Down
1 change: 0 additions & 1 deletion src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,6 @@ namespace winrt::TerminalApp::implementation

// Create a connection based on the values in our settings object.
const auto connection = _CreateConnectionFromSettings(profileGuid, settings);

TermControl term{ settings, connection };

// Add the new tab to the list of our tabs.
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalConnection/ConptyConnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
try
{
const COORD dimensions{ gsl::narrow_cast<SHORT>(_initialCols), gsl::narrow_cast<SHORT>(_initialRows) };
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, 0, &_inPipe, &_outPipe, &_hPC));
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK, &_inPipe, &_outPipe, &_hPC));
THROW_IF_FAILED(_LaunchAttachedClient());

_startTime = std::chrono::high_resolution_clock::now();
Expand Down
140 changes: 128 additions & 12 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,24 +173,143 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting
{
return S_FALSE;
}
const auto dx = viewportSize.X - oldDimensions.X;

const auto oldTop = _mutableViewport.Top();

const short newBufferHeight = viewportSize.Y + _scrollbackLines;
COORD bufferSize{ viewportSize.X, newBufferHeight };
RETURN_IF_FAILED(_buffer->ResizeTraditional(bufferSize));

auto proposedTop = oldTop;
const auto newView = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
const auto proposedBottom = newView.BottomExclusive();
// Save cursor's relative height versus the viewport
const short sCursorHeightInViewportBefore = _buffer->GetCursor().GetPosition().Y - _mutableViewport.Top();

// This will be used to determine where the viewport should be in the new buffer.
const short oldViewportTop = _mutableViewport.Top();
short newViewportTop = oldViewportTop;

// First allocate a new text buffer to take the place of the current one.
std::unique_ptr<TextBuffer> newTextBuffer;
try
{
newTextBuffer = std::make_unique<TextBuffer>(bufferSize,
_buffer->GetCurrentAttributes(),
0, // temporarily set size to 0 so it won't render.
_buffer->GetRenderTarget());

std::optional<short> oldViewStart{ oldViewportTop };
RETURN_IF_FAILED(TextBuffer::Reflow(*_buffer.get(),
*newTextBuffer.get(),
_mutableViewport,
oldViewStart));
newViewportTop = oldViewStart.value();
}
CATCH_RETURN();

// Conpty resizes a little oddly - if the height decreased, and there were
// blank lines at the bottom, those lines will get trimmed. If there's not
// blank lines, then the top will get "shifted down", moving the top line
// into scrollback. See GH#3490 for more details.
//
// If the final position in the buffer is on the bottom row of the new
// viewport, then we're going to need to move the top down. Otherwise, move
// the bottom up.
//
// There are also important things to consider with line wrapping.
// * If a line in scrollback wrapped that didn't previously, we'll need to
// make sure to have the new viewport down another line. This will cause
// our top to move down.
// * If a line _in the viewport_ wrapped that didn't previously, then the
// conpty buffer will also have that wrapped line, and will move the
// cursor & text down a line in response. This causes our bottom to move
// down.
//
// We're going to use a combo of both these things to calculate where the
// new viewport should be. To keep in sync with conpty, we'll need to make
// sure that any lines that entered the scrollback _stay in scrollback_. We
// do that by taking the max of
// * Where the old top line in the viewport exists in the new buffer (as
// calculated by TextBuffer::Reflow)
// * Where the bottom of the text in the new buffer is (and using that to
// calculate another proposed top location).

const COORD newCursorPos = newTextBuffer->GetCursor().GetPosition();
#pragma warning(push)
#pragma warning(disable : 26496) // cpp core checks wants this const, but it's assigned immediately below...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#pragma warning (suppress: 26496)
should also work if you want a one liner instead of push/disable/pop.

COORD newLastChar = newCursorPos;
try
{
newLastChar = newTextBuffer->GetLastNonSpaceCharacter();
}
CATCH_LOG();
#pragma warning(pop)

const auto maxRow = std::max(newLastChar.Y, newCursorPos.Y);

const short proposedTopFromLastLine = ::base::saturated_cast<short>(maxRow - viewportSize.Y + 1);
const short proposedTopFromScrollback = newViewportTop;

short proposedTop = std::max(proposedTopFromLastLine,
proposedTopFromScrollback);

// If we're using the new location of the old top line to place the
// viewport, we might need to make an adjustment to it.
//
// We're using the last cell of the line to calculate where the top line is
// in the new buffer. If that line wrapped, then all the lines below it
// shifted down in the buffer. If there's space for all those lines in the
// conpty buffer, then the originally unwrapped top line will _still_ be in
// the buffer. In that case, don't stick to the _end_ of the old top line,
// instead stick to the _start_, which is one line up.
//
// We can know if there's space in the conpty buffer by checking if the
// maxRow (the highest row we've written text to) is above the viewport from
// this proposed top position.
if (proposedTop == proposedTopFromScrollback)
{
const auto proposedViewFromTop = Viewport::FromDimensions({ 0, proposedTopFromScrollback }, viewportSize);
if (maxRow < proposedViewFromTop.BottomInclusive())
{
if (dx < 0 && proposedTop > 0)
{
try
{
auto row = newTextBuffer->GetRowByOffset(::base::saturated_cast<short>(proposedTop - 1));
if (row.GetCharRow().WasWrapForced())
{
proposedTop--;
}
}
CATCH_LOG();
}
}
}

// If the new bottom would be higher than the last row of text, then we
// definitely want to use the last row of text to determine where the
// viewport should be.
const auto proposedViewFromTop = Viewport::FromDimensions({ 0, proposedTopFromScrollback }, viewportSize);
if (maxRow > proposedViewFromTop.BottomInclusive())
{
proposedTop = proposedTopFromLastLine;
}

// Make sure the proposed viewport is within the bounds of the buffer.
// First make sure the top is >=0
proposedTop = std::max(static_cast<short>(0), proposedTop);

// If the new bottom would be below the bottom of the buffer, then slide the
// top up so that we'll still fit within the buffer.
const auto newView = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
const auto proposedBottom = newView.BottomExclusive();
if (proposedBottom > bufferSize.Y)
{
proposedTop -= (proposedBottom - bufferSize.Y);
proposedTop = ::base::saturated_cast<short>(proposedTop - (proposedBottom - bufferSize.Y));
}

_mutableViewport = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);

_buffer.swap(newTextBuffer);

_scrollOffset = 0;
_NotifyScrollEvent();

Expand Down Expand Up @@ -456,18 +575,15 @@ void Terminal::_WriteBuffer(const std::wstring_view& stringView)
// Try the character again.
i--;

// Mark the line we're currently on as wrapped
// If we write the last cell of the row here, TextBuffer::Write will
// mark this line as wrapped for us. If the next character we
// process is a newline, the Terminal::CursorLineFeed will unmark
// this line as wrapped.

// TODO: GH#780 - This should really be a _deferred_ newline. If
// the next character to come in is a newline or a cursor
// movement or anything, then we should _not_ wrap this line
// here.
//
// This is more WriteCharsLegacy2ElectricBoogaloo work. I'm
// leaving it like this for now - it'll break for lines that
// _exactly_ wrap, but we can't re-wrap lines now anyways, so it
// doesn't matter.
_buffer->GetRowByOffset(cursorPosBefore.Y).GetCharRow().SetWrapForced(true);
}

_AdjustCursorPosition(proposedCursorPosition);
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalCore/TerminalApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ bool Terminal::CursorLineFeed(const bool withReturn) noexcept
try
{
auto cursorPos = _buffer->GetCursor().GetPosition();

// since we explicitly just moved down a row, clear the wrap status on the
// row we just came from
_buffer->GetRowByOffset(cursorPos.Y).GetCharRow().SetWrapForced(false);

cursorPos.Y++;
if (withReturn)
{
Expand Down
Loading