terminal/src/host/_output.cpp
James Holderness 381b11521a Correct fill attributes when scrolling and erasing (#3100)
## Summary of the Pull Request

Operations that erase areas of the screen are typically meant to do so using the current color attributes, but with the rendition attributes reset (what we refer to as meta attributes). This also includes scroll operations that have to clear the area of the screen that has scrolled into view. The only exception is the _Erase Scrollback_ operation, which needs to reset the buffer with the default attributes. This PR updates all of these cases to apply the correct attributes when scrolling and erasing.

## PR Checklist
* [x] Closes #2553
* [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
* [x] Tests added/passed
* [ ] Requires documentation to be updated
* [ ] I've not really discussed this with core contributors. I'm ready to accept this work might be rejected in favor of a different grand plan. 

## Detailed Description of the Pull Request / Additional comments

My initial plan was to use a special case legacy attribute value to indicate the "standard erase attribute" which could safely be passed through the legacy APIs. But this wouldn't cover the cases that required default attributes to be used. And then with the changes in PR #2668 and #2987, it became clear that our requirements could be better achieved with a couple of new private APIs that wouldn't have to depend on legacy attribute hacks at all.

To that end, I've added the `PrivateFillRegion` and `PrivateScrollRegion` APIs to the `ConGetSet` interface. These are just thin wrappers around the existing `SCREEN_INFORMATION::Write` method and the `ScrollRegion` function respectively, but with a simple boolean parameter to choose between filling with default attributes or the standard erase attributes (i.e the current colors but with meta attributes reset).

With those new APIs in place, I could then update most scroll operations to use `PrivateScrollRegion`, and most erase operations to use `PrivateFillRegion`.

The functions affected by scrolling included:
* `DoSrvPrivateReverseLineFeed` (the RI command)
* `DoSrvPrivateModifyLinesImpl` (the IL and DL commands)
* `AdaptDispatch::_InsertDeleteHelper` (the ICH and DCH commands)
* `AdaptDispatch::_ScrollMovement` (the SU and SD commands)

The functions affected by erasing included:
* `AdaptDispatch::_EraseSingleLineHelper` (the EL command, and most ED variants)
* `AdaptDispatch::EraseCharacters` (the ECH command)

While updating these erase methods, I noticed that both of them also required boundary fixes similar to those in PR #2505 (i.e. the horizontal extent of the erase operation should apply to the full width of the buffer, and not just the current viewport width), so I've addressed that at the same time.

In addition to the changes above, there were also a few special cases, the first being the line feed handling, which required updating in a number of places to use the correct erase attributes:

* `SCREEN_INFORMATION::InitializeCursorRowAttributes` - this is used to initialise the rows that pan into view when the viewport is moved down the buffer.
* `TextBuffer::IncrementCircularBuffer` - this occurs when we scroll passed the very end of the buffer, and a recycled row now needs to be reinitialised.
* `AdjustCursorPosition` - when within margin boundaries, this relies on a couple of direct calls to `ScrollRegion` which needed to be passed the correct fill attributes.

The second special case was the full screen erase sequence (`ESC 2 J`), which is handled separately from the other ED sequences. This required updating the `SCREEN_INFORMATION::VtEraseAll` method to use the standard erase attributes, and also required changes to the horizontal extent of the filled area, since it should have been clearing the full buffer width (the same issue as the other erase operations mentioned above).

Finally, there was the `AdaptDispatch::_EraseScrollback` method, which uses both scroll and fill operations, which could now be handled by the new `PrivateScrollRegion` and `PrivateFillRegion` APIs. But in this case we needed to fill with the default attributes rather than the standard erase attributes. And again this implementation needed some changes to make sure the full width of the active area was retained after the erase, similar to the horizontal boundary issues with the other erase operations.

Once all these changes were made, there were a few areas of the code that could then be simplified quite a bit. The `FillConsoleOutputCharacterW`, `FillConsoleOutputAttribute`, and `ScrollConsoleScreenBufferW` were no longer needed in the `ConGetSet` interface, so all of that code could now be removed. The `_EraseSingleLineDistanceHelper` and `_EraseAreaHelper` methods in the `AdaptDispatch` class were also no longer required and could be removed.

Then there were the hacks to handle legacy default colors in the `FillConsoleOutputAttributeImpl` and `ScrollConsoleScreenBufferWImpl` implementations. Since those hacks were only needed for VT operations, and the VT code no longer calls those methods, there was no longer a need to retain that behaviour (in fact there are probably some edge cases where that behaviour might have been considered a bug when reached via the public console APIs). 

## Validation Steps Performed

For most of the scrolling operations there were already existing tests in place, and those could easily be extended to check that the meta attributes were correctly reset when filling the revealed lines of the scrolling region.

In the screen buffer tests, I made updates of that sort to  the `ScrollOperations` method (handling SU, SD, IL, DL, and RI), the `InsertChars` and `DeleteChars` methods (ICH and DCH), and the `VtNewlinePastViewport` method (LF). I also added a new `VtNewlinePastEndOfBuffer` test to check the case where the line feed causes the viewport to pan past the end of the buffer.

The erase operations, however, were being covered by adapter tests, and those aren't really suited for this kind of functionality (the same sort of issue came up in PR #2505). As a result I've had to reimplement those tests as screen buffer tests.

Most of the erase operations are covered by the `EraseTests` method, except the for the scrollback erase which has a dedicated `EraseScrollbackTests` method. I've also had to replace the `HardReset` adapter test, but that was already mostly covered by the `HardResetBuffer` screen buffer test, which I've now extended slightly (it could do with some more checks, but I think that can wait for a future PR when we're fixing other RIS issues).
2019-12-10 23:14:40 +00:00

316 lines
11 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "_output.h"
#include "dbcs.h"
#include "handle.h"
#include "misc.h"
#include "../buffer/out/CharRow.hpp"
#include "../interactivity/inc/ServiceLocator.hpp"
#include "../types/inc/Viewport.hpp"
#include "../types/inc/convert.hpp"
#include "../types/inc/Utf16Parser.hpp"
#include <algorithm>
#include <iterator>
#pragma hdrstop
using namespace Microsoft::Console::Types;
using Microsoft::Console::Interactivity::ServiceLocator;
// Routine Description:
// - This routine writes a screen buffer region to the screen.
// Arguments:
// - screenInfo - reference to screen buffer information.
// - srRegion - Region to write in screen buffer coordinates. Region is inclusive
// Return Value:
// - <none>
void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region)
{
DBGOUTPUT(("WriteToScreen\n"));
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
// update to screen, if we're not iconic.
if (!screenInfo.IsActiveScreenBuffer() || WI_IsFlagSet(gci.Flags, CONSOLE_IS_ICONIC))
{
return;
}
// clip region to fit within the viewport
const auto clippedRegion = screenInfo.GetViewport().Clamp(region);
if (!clippedRegion.IsValid())
{
return;
}
if (screenInfo.IsActiveScreenBuffer())
{
if (ServiceLocator::LocateGlobals().pRender != nullptr)
{
ServiceLocator::LocateGlobals().pRender->TriggerRedraw(region);
}
}
WriteConvRegionToScreen(screenInfo, region);
}
// Routine Description:
// - writes text attributes to the screen
// Arguments:
// - OutContext - the screen info to write to
// - attrs - the attrs to write to the screen
// - target - the starting coordinate in the screen
// - used - number of elements written
// Return Value:
// - S_OK, E_INVALIDARG or similar HRESULT error.
[[nodiscard]] HRESULT ApiRoutines::WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext,
const std::basic_string_view<WORD> attrs,
const COORD target,
size_t& used) noexcept
{
// Set used to 0 from the beginning in case we exit early.
used = 0;
if (attrs.empty())
{
return S_OK;
}
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
auto& screenInfo = OutContext.GetActiveBuffer();
const auto bufferSize = screenInfo.GetBufferSize();
if (!bufferSize.IsInBounds(target))
{
return E_INVALIDARG;
}
const OutputCellIterator it(attrs, true);
const auto done = screenInfo.Write(it, target);
used = done.GetCellDistance(it);
return S_OK;
}
// Routine Description:
// - writes text to the screen
// Arguments:
// - screenInfo - the screen info to write to
// - chars - the text to write to the screen
// - target - the starting coordinate in the screen
// - used - number of elements written
// Return Value:
// - S_OK, E_INVALIDARG or similar HRESULT error.
[[nodiscard]] HRESULT ApiRoutines::WriteConsoleOutputCharacterWImpl(IConsoleOutputObject& OutContext,
const std::wstring_view chars,
const COORD target,
size_t& used) noexcept
{
// Set used to 0 from the beginning in case we exit early.
used = 0;
if (chars.empty())
{
return S_OK;
}
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
auto& screenInfo = OutContext.GetActiveBuffer();
const auto bufferSize = screenInfo.GetBufferSize();
if (!bufferSize.IsInBounds(target))
{
return E_INVALIDARG;
}
try
{
OutputCellIterator it(chars);
const auto finished = screenInfo.Write(it, target);
used = finished.GetInputDistance(it);
}
CATCH_RETURN();
return S_OK;
}
// Routine Description:
// - writes text to the screen
// Arguments:
// - screenInfo - the screen info to write to
// - chars - the text to write to the screen
// - target - the starting coordinate in the screen
// - used - number of elements written
// Return Value:
// - S_OK, E_INVALIDARG or similar HRESULT error.
[[nodiscard]] HRESULT ApiRoutines::WriteConsoleOutputCharacterAImpl(IConsoleOutputObject& OutContext,
const std::string_view chars,
const COORD target,
size_t& used) noexcept
{
// Set used to 0 from the beginning in case we exit early.
used = 0;
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto codepage = gci.OutputCP;
try
{
// convert to wide chars so we can call the W version of this function
const auto wideChars = ConvertToW(codepage, chars);
size_t wideCharsWritten = 0;
RETURN_IF_FAILED(WriteConsoleOutputCharacterWImpl(OutContext, wideChars, target, wideCharsWritten));
// Create a view over the wide chars and reduce it to the amount actually written (do in two steps to enforce bounds)
std::wstring_view writtenView(wideChars);
writtenView = writtenView.substr(0, wideCharsWritten);
// Look over written wide chars to find equilalent count of ascii chars so we can properly report back
// how many elements were actually written
used = GetALengthFromW(codepage, writtenView);
}
CATCH_RETURN();
return S_OK;
}
// Routine Description:
// - fills the screen buffer with the specified text attribute
// Arguments:
// - OutContext - reference to screen buffer information.
// - attribute - the text attribute to use to fill
// - lengthToWrite - the number of elements to write
// - startingCoordinate - Screen buffer coordinate to begin writing to.
// - cellsModified - the number of elements written
// Return Value:
// - S_OK or suitable HRESULT code from failure to write (memory issues, invalid arg, etc.)
[[nodiscard]] HRESULT ApiRoutines::FillConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext,
const WORD attribute,
const size_t lengthToWrite,
const COORD startingCoordinate,
size_t& cellsModified) noexcept
{
// Set modified cells to 0 from the beginning.
cellsModified = 0;
if (lengthToWrite == 0)
{
return S_OK;
}
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
auto& screenBuffer = OutContext.GetActiveBuffer();
const auto bufferSize = screenBuffer.GetBufferSize();
if (!bufferSize.IsInBounds(startingCoordinate))
{
return S_OK;
}
try
{
TextAttribute useThisAttr(attribute);
const OutputCellIterator it(useThisAttr, lengthToWrite);
const auto done = screenBuffer.Write(it, startingCoordinate);
cellsModified = done.GetCellDistance(it);
// Notify accessibility
auto endingCoordinate = startingCoordinate;
bufferSize.MoveInBounds(cellsModified, endingCoordinate);
screenBuffer.NotifyAccessibilityEventing(startingCoordinate.X, startingCoordinate.Y, endingCoordinate.X, endingCoordinate.Y);
}
CATCH_RETURN();
return S_OK;
}
// Routine Description:
// - fills the screen buffer with the specified wchar
// Arguments:
// - OutContext - reference to screen buffer information.
// - character - wchar to fill with
// - lengthToWrite - the number of elements to write
// - startingCoordinate - Screen buffer coordinate to begin writing to.
// - cellsModified - the number of elements written
// Return Value:
// - S_OK or suitable HRESULT code from failure to write (memory issues, invalid arg, etc.)
[[nodiscard]] HRESULT ApiRoutines::FillConsoleOutputCharacterWImpl(IConsoleOutputObject& OutContext,
const wchar_t character,
const size_t lengthToWrite,
const COORD startingCoordinate,
size_t& cellsModified) noexcept
{
// Set modified cells to 0 from the beginning.
cellsModified = 0;
if (lengthToWrite == 0)
{
return S_OK;
}
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
// TODO: does this even need to be here or will it exit quickly?
auto& screenInfo = OutContext.GetActiveBuffer();
const auto bufferSize = screenInfo.GetBufferSize();
if (!bufferSize.IsInBounds(startingCoordinate))
{
return S_OK;
}
try
{
const OutputCellIterator it(character, lengthToWrite);
// when writing to the buffer, specifically unset wrap if we get to the last column.
// a fill operation should UNSET wrap in that scenario. See GH #1126 for more details.
const auto done = screenInfo.Write(it, startingCoordinate, false);
cellsModified = done.GetInputDistance(it);
// Notify accessibility
auto endingCoordinate = startingCoordinate;
bufferSize.MoveInBounds(cellsModified, endingCoordinate);
screenInfo.NotifyAccessibilityEventing(startingCoordinate.X, startingCoordinate.Y, endingCoordinate.X, endingCoordinate.Y);
}
CATCH_RETURN();
return S_OK;
}
// Routine Description:
// - fills the screen buffer with the specified char
// Arguments:
// - OutContext - reference to screen buffer information.
// - character - ascii character to fill with
// - lengthToWrite - the number of elements to write
// - startingCoordinate - Screen buffer coordinate to begin writing to.
// - cellsModified - the number of elements written
// Return Value:
// - S_OK or suitable HRESULT code from failure to write (memory issues, invalid arg, etc.)
[[nodiscard]] HRESULT ApiRoutines::FillConsoleOutputCharacterAImpl(IConsoleOutputObject& OutContext,
const char character,
const size_t lengthToWrite,
const COORD startingCoordinate,
size_t& cellsModified) noexcept
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
// convert to wide chars and call W version
const auto wchs = ConvertToW(gci.OutputCP, { &character, 1 });
LOG_HR_IF(E_UNEXPECTED, wchs.size() > 1);
return FillConsoleOutputCharacterWImpl(OutContext, wchs.at(0), lengthToWrite, startingCoordinate, cellsModified);
}