diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 094ede52006..b3c1fa7ecf3 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -558,22 +558,37 @@ bool TextBuffer::IncrementCircularBuffer() //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: // - //Return Value: // - Coordinate position in screen coordinates (offset coordinates, not array index coordinates). COORD TextBuffer::GetLastNonSpaceCharacter() const { - COORD coordEndOfText; - // Always search the whole buffer, by starting at the bottom. - coordEndOfText.Y = GetSize().BottomInclusive(); + 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) +//Arguments: +// - The viewport +//Return value: +// - Coordinate position (relative to the text buffer) +COORD TextBuffer::GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const +{ + COORD coordEndOfText = { 0 }; + // Search the given viewport by starting at the bottom. + coordEndOfText.Y = viewport.BottomInclusive(); const ROW* pCurrRow = &GetRowByOffset(coordEndOfText.Y); // The X position of the end of the valid text is the Right draw boundary (which is one beyond the final valid character) coordEndOfText.X = static_cast(pCurrRow->GetCharRow().MeasureRight()) - 1; // If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text. - bool fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > 0); // this row is empty, and we're not at the top + const auto viewportTop = viewport.Top(); + bool fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > viewportTop); // this row is empty, and we're not at the top while (fDoBackUp) { coordEndOfText.Y--; @@ -581,7 +596,7 @@ COORD TextBuffer::GetLastNonSpaceCharacter() const // We need to back up to the previous row if this line is empty, AND there are more rows coordEndOfText.X = static_cast(pCurrRow->GetCharRow().MeasureRight()) - 1; - fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > 0); + fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > viewportTop); } // don't allow negative results diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 8e5aa205158..9cfd50bba0a 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -105,6 +105,7 @@ class TextBuffer final bool IncrementCircularBuffer(); COORD GetLastNonSpaceCharacter() const; + COORD GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const; Cursor& GetCursor(); const Cursor& GetCursor() const; diff --git a/src/cascadia/TerminalCore/ITerminalApi.hpp b/src/cascadia/TerminalCore/ITerminalApi.hpp index 9e4e2a3767f..e85fb29c950 100644 --- a/src/cascadia/TerminalCore/ITerminalApi.hpp +++ b/src/cascadia/TerminalCore/ITerminalApi.hpp @@ -24,7 +24,11 @@ namespace Microsoft::Terminal::Core virtual bool SetCursorPosition(short x, short y) = 0; virtual COORD GetCursorPosition() = 0; + virtual bool DeleteCharacter(const unsigned int uiCount) = 0; + virtual bool InsertCharacter(const unsigned int uiCount) = 0; virtual bool EraseCharacters(const unsigned int numChars) = 0; + virtual bool EraseInLine(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType eraseType) = 0; + virtual bool EraseInDisplay(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType eraseType) = 0; virtual bool SetWindowTitle(std::wstring_view title) = 0; diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index b41a0a9f46c..157f0155ca9 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -66,7 +66,11 @@ class Microsoft::Terminal::Core::Terminal final : bool ReverseText(bool reversed) override; bool SetCursorPosition(short x, short y) override; COORD GetCursorPosition() override; + bool DeleteCharacter(const unsigned int uiCount) override; + bool InsertCharacter(const unsigned int uiCount) override; bool EraseCharacters(const unsigned int numChars) override; + bool EraseInLine(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType eraseType) override; + bool EraseInDisplay(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType eraseType) override; bool SetWindowTitle(std::wstring_view title) override; bool SetColorTableEntry(const size_t tableIndex, const COLORREF dwColor) override; bool SetCursorStyle(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::CursorStyle cursorStyle) override; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 27ca646b320..a0504164088 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Terminal.hpp" +#include "../src/inc/unicode.hpp" using namespace Microsoft::Terminal::Core; using namespace Microsoft::Console::Types; @@ -126,17 +127,242 @@ COORD Terminal::GetCursorPosition() return newPos; } +// Method Description: +// - deletes uiCount characters starting from the cursor's current position +// - it moves over the remaining text to 'replace' the deleted text +// - for example, if the buffer looks like this ('|' is the cursor): [abc|def] +// - calling DeleteCharacter(1) will change it to: [abc|ef], +// - i.e. the 'd' gets deleted and the 'ef' gets shifted over 1 space and **retain their previous text attributes** +// Arguments: +// - uiCount, the number of characters to delete +// Return value: +// - true if succeeded, false otherwise +bool Terminal::DeleteCharacter(const unsigned int uiCount) +{ + SHORT dist; + if (!SUCCEEDED(UIntToShort(uiCount, &dist))) + { + return false; + } + const auto cursorPos = _buffer->GetCursor().GetPosition(); + const auto copyToPos = cursorPos; + const COORD copyFromPos{ cursorPos.X + dist, cursorPos.Y }; + auto sourceWidth = _mutableViewport.RightExclusive() - copyFromPos.X; + SHORT width; + if (!SUCCEEDED(UIntToShort(sourceWidth, &width))) + { + return false; + } + + // Get a rectangle of the source + auto source = Viewport::FromDimensions(copyFromPos, width, 1); + + // Get a rectangle of the target + const auto target = Viewport::FromDimensions(copyToPos, source.Dimensions()); + const auto walkDirection = Viewport::DetermineWalkDirection(source, target); + + auto sourcePos = source.GetWalkOrigin(walkDirection); + auto targetPos = target.GetWalkOrigin(walkDirection); + + // Iterate over the source cell data and copy it over to the target + do + { + const auto data = OutputCell(*(_buffer->GetCellDataAt(sourcePos))); + _buffer->Write(OutputCellIterator({ &data, 1 }), targetPos); + } while (source.WalkInBounds(sourcePos, walkDirection) && target.WalkInBounds(targetPos, walkDirection)); + + return true; +} + +// Method Description: +// - Inserts uiCount spaces starting from the cursor's current position, moving over the existing text +// - for example, if the buffer looks like this ('|' is the cursor): [abc|def] +// - calling InsertCharacter(1) will change it to: [abc| def], +// - i.e. the 'def' gets shifted over 1 space and **retain their previous text attributes** +// Arguments: +// - uiCount, the number of spaces to insert +// Return value: +// - true if succeeded, false otherwise +bool Terminal::InsertCharacter(const unsigned int uiCount) +{ + // NOTE: the code below is _extremely_ similar to DeleteCharacter + // We will want to use this same logic and implement a helper function instead + // that does the 'move a region from here to there' operation + // TODO: Github issue #2163 + SHORT dist; + if (!SUCCEEDED(UIntToShort(uiCount, &dist))) + { + return false; + } + const auto cursorPos = _buffer->GetCursor().GetPosition(); + const auto copyFromPos = cursorPos; + const COORD copyToPos{ cursorPos.X + dist, cursorPos.Y }; + auto sourceWidth = _mutableViewport.RightExclusive() - copyFromPos.X; + SHORT width; + if (!SUCCEEDED(UIntToShort(sourceWidth, &width))) + { + return false; + } + + // Get a rectangle of the source + auto source = Viewport::FromDimensions(copyFromPos, width, 1); + const auto sourceOrigin = source.Origin(); + + // Get a rectangle of the target + const auto target = Viewport::FromDimensions(copyToPos, source.Dimensions()); + const auto walkDirection = Viewport::DetermineWalkDirection(source, target); + + auto sourcePos = source.GetWalkOrigin(walkDirection); + auto targetPos = target.GetWalkOrigin(walkDirection); + + // Iterate over the source cell data and copy it over to the target + do + { + const auto data = OutputCell(*(_buffer->GetCellDataAt(sourcePos))); + _buffer->Write(OutputCellIterator({ &data, 1 }), targetPos); + } while (source.WalkInBounds(sourcePos, walkDirection) && target.WalkInBounds(targetPos, walkDirection)); + auto eraseIter = OutputCellIterator(UNICODE_SPACE, _buffer->GetCurrentAttributes(), dist); + _buffer->Write(eraseIter, cursorPos); + + return true; +} + bool Terminal::EraseCharacters(const unsigned int numChars) { const auto absoluteCursorPos = _buffer->GetCursor().GetPosition(); const auto viewport = _GetMutableViewport(); const short distanceToRight = viewport.RightExclusive() - absoluteCursorPos.X; const short fillLimit = std::min(static_cast(numChars), distanceToRight); - auto eraseIter = OutputCellIterator(L' ', _buffer->GetCurrentAttributes(), fillLimit); + auto eraseIter = OutputCellIterator(UNICODE_SPACE, _buffer->GetCurrentAttributes(), fillLimit); _buffer->Write(eraseIter, absoluteCursorPos); return true; } +// Method description: +// - erases a line of text, either from +// 1. beginning to the cursor's position +// 2. cursor's position to end +// 3. beginning to end +// - depending on the erase type +// Arguments: +// - the erase type +// Return value: +// - true if succeeded, false otherwise +bool Terminal::EraseInLine(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType eraseType) +{ + const auto cursorPos = _buffer->GetCursor().GetPosition(); + const auto viewport = _GetMutableViewport(); + COORD startPos = { 0 }; + startPos.Y = cursorPos.Y; + // nlength determines the number of spaces we need to write + DWORD nlength = 0; + + // Determine startPos.X and nlength by the eraseType + switch (eraseType) + { + case DispatchTypes::EraseType::FromBeginning: + nlength = cursorPos.X - viewport.Left() + 1; + break; + case DispatchTypes::EraseType::ToEnd: + startPos.X = cursorPos.X; + nlength = viewport.RightInclusive() - startPos.X; + break; + case DispatchTypes::EraseType::All: + startPos.X = viewport.Left(); + nlength = viewport.RightInclusive() - startPos.X; + break; + case DispatchTypes::EraseType::Scrollback: + return false; + } + + auto eraseIter = OutputCellIterator(UNICODE_SPACE, _buffer->GetCurrentAttributes(), nlength); + _buffer->Write(eraseIter, startPos); + return true; +} + +// Method description: +// - erases text in the buffer in two ways depending on erase type +// 1. 'erases' all text visible to the user (i.e. the text in the viewport) +// 2. erases all the text in the scrollback +// Arguments: +// - the erase type +// Return Value: +// - true if succeeded, false otherwise +bool Terminal::EraseInDisplay(const DispatchTypes::EraseType eraseType) +{ + // Store the relative cursor position so we can restore it later after we move the viewport + const auto cursorPos = _buffer->GetCursor().GetPosition(); + auto relativeCursor = cursorPos; + _mutableViewport.ConvertToOrigin(&relativeCursor); + + // Initialize the new location of the viewport + // the top and bottom parameters are determined by the eraseType + SMALL_RECT newWin; + newWin.Left = _mutableViewport.Left(); + newWin.Right = _mutableViewport.RightExclusive(); + + if (eraseType == DispatchTypes::EraseType::All) + { + // In this case, we simply move the viewport down, effectively pushing whatever text was on the screen into the scrollback + // and thus 'erasing' the text visible to the user + const auto coordLastChar = _buffer->GetLastNonSpaceCharacter(_mutableViewport); + if (coordLastChar.X == 0 && coordLastChar.Y == 0) + { + // Nothing to clear, just return + return true; + } + + short sNewTop = coordLastChar.Y + 1; + + // Increment the circular buffer only if the new location of the viewport would be 'below' the buffer + const short delta = (sNewTop + _mutableViewport.Height()) - (_buffer->GetSize().Height()); + for (auto i = 0; i < delta; i++) + { + _buffer->IncrementCircularBuffer(); + sNewTop--; + } + + newWin.Top = sNewTop; + newWin.Bottom = sNewTop + _mutableViewport.Height(); + } + else if (eraseType == DispatchTypes::EraseType::Scrollback) + { + // We only want to erase the scrollback, and leave everything else on the screen as it is + // so we grab the text in the viewport and rotate it up to the top of the buffer + COORD scrollFromPos{ 0, 0 }; + _mutableViewport.ConvertFromOrigin(&scrollFromPos); + _buffer->ScrollRows(scrollFromPos.Y, _mutableViewport.Height(), -scrollFromPos.Y); + + // Since we only did a rotation, the text that was in the scrollback is now _below_ where we are going to move the viewport + // and we have to make sure we erase that text + auto eraseStart = _mutableViewport.Height(); + auto eraseEnd = _buffer->GetLastNonSpaceCharacter(_mutableViewport).Y; + auto eraseIter = OutputCellIterator(UNICODE_SPACE, _buffer->GetCurrentAttributes(), _mutableViewport.RightInclusive() * (eraseEnd - eraseStart + 1)); + for (SHORT i = eraseStart; i <= eraseEnd; i++) + { + COORD erasePos{ 0, i }; + _buffer->Write(eraseIter, erasePos); + } + + // Reset the scroll offset now because there's nothing for the user to 'scroll' to + _scrollOffset = 0; + + newWin.Top = 0; + newWin.Bottom = _mutableViewport.Height(); + } + else + { + return false; + } + + // Move the viewport, adjust the scoll bar if needed, and restore the old cursor position + _mutableViewport = Viewport::FromExclusive(newWin); + Terminal::_NotifyScrollEvent(); + SetCursorPosition(relativeCursor.X, relativeCursor.Y); + + return true; +} + bool Terminal::SetWindowTitle(std::wstring_view title) { _title = title; diff --git a/src/cascadia/TerminalCore/TerminalDispatch.cpp b/src/cascadia/TerminalCore/TerminalDispatch.cpp index bd076961584..d5b061d2913 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.cpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.cpp @@ -47,6 +47,20 @@ bool TerminalDispatch::CursorForward(const unsigned int uiDistance) return _terminalApi.SetCursorPosition(newCursorPos.X, newCursorPos.Y); } +bool TerminalDispatch::CursorBackward(const unsigned int uiDistance) +{ + const auto cursorPos = _terminalApi.GetCursorPosition(); + const COORD newCursorPos{ cursorPos.X - gsl::narrow(uiDistance), cursorPos.Y }; + return _terminalApi.SetCursorPosition(newCursorPos.X, newCursorPos.Y); +} + +bool TerminalDispatch::CursorUp(const unsigned int uiDistance) +{ + const auto cursorPos = _terminalApi.GetCursorPosition(); + const COORD newCursorPos{ cursorPos.X, cursorPos.Y + gsl::narrow(uiDistance) }; + return _terminalApi.SetCursorPosition(newCursorPos.X, newCursorPos.Y); +} + bool TerminalDispatch::EraseCharacters(const unsigned int uiNumChars) { return _terminalApi.EraseCharacters(uiNumChars); @@ -98,9 +112,45 @@ bool TerminalDispatch::SetDefaultBackground(const DWORD dwColor) } // Method Description: -// - For now, this is a hacky backspace -// - TODO: GitHub #1883 -bool TerminalDispatch::EraseInLine(const DispatchTypes::EraseType) +// - Erases characters in the buffer depending on the erase type +// Arguments: +// - eraseType: the erase type (from beginning, to end, or all) +// Return Value: +// True if handled successfully. False otherwise. +bool TerminalDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) +{ + return _terminalApi.EraseInLine(eraseType); +} + +// Method Description: +// - Deletes uiCount number of characters starting from where the cursor is currently +// Arguments: +// - uiCount, the number of characters to delete +// Return Value: +// True if handled successfully. False otherwise. +bool TerminalDispatch::DeleteCharacter(const unsigned int uiCount) +{ + return _terminalApi.DeleteCharacter(uiCount); +} + +// Method Description: +// - Adds uiCount number of spaces starting from where the cursor is currently +// Arguments: +// - uiCount, the number of spaces to add +// Return Value: +// True if handled successfully, false otherwise +bool TerminalDispatch::InsertCharacter(const unsigned int uiCount) +{ + return _terminalApi.InsertCharacter(uiCount); +} + +// Method Description: +// - Moves the viewport and erases text from the buffer depending on the eraseType +// Arguments: +// - eraseType: the desired erase type +// Return Value: +// True if handled successfully. False otherwise +bool TerminalDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) { - return _terminalApi.EraseCharacters(1); + return _terminalApi.EraseInDisplay(eraseType); } diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp index e026744f0c7..5fa9758856e 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.hpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp @@ -20,6 +20,8 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc const unsigned int uiColumn) override; // CUP bool CursorForward(const unsigned int uiDistance) override; + bool CursorBackward(const unsigned int uiDistance) override; + bool CursorUp(const unsigned int uiDistance) override; bool EraseCharacters(const unsigned int uiNumChars) override; bool SetWindowTitle(std::wstring_view title) override; @@ -29,7 +31,10 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc bool SetDefaultForeground(const DWORD dwColor) override; bool SetDefaultBackground(const DWORD dwColor) override; - bool EraseInLine(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType /* eraseType*/) override; // ED + bool EraseInLine(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType eraseType) override; // ED + bool DeleteCharacter(const unsigned int uiCount) override; + bool InsertCharacter(const unsigned int uiCount) override; + bool EraseInDisplay(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType eraseType) override; private: ::Microsoft::Terminal::Core::ITerminalApi& _terminalApi;