terminal/src/host/ut_host/ScreenBufferTests.cpp

5583 lines
225 KiB
C++
Raw Normal View History

Improve the DECSC/DECRC implementation (#3160) The current DECSC implementation only saves the cursor position and origin mode. This PR improves that functionality with additional support for saving the SGR attributes, as well as the active character set. It also fixes the way the saved state interacts with the alt buffer (private mode 1049), triggering a save when switching to the alt buffer, and a restore when switching back, and tracking the alt buffer state independently from the main state. In order to properly save and restore the SGR attributes, we first needed to add a pair of APIs in the `ConGetSet` interface which could round-trip the attributes with full 32-bit colors (the existing methods only work with legacy attributes). I then added a struct in the `AdaptDispatch` implementation to make it easier to manage all of the parameters that needed to be saved. This includes the cursor position and origin mode that we were already tracking, and now also the SGR text attributes and the active character set (via the `TermOutput` class). Two instances of this structure are required, since changes made to the saved state in the alt buffer need to be tracked separately from changes in the main buffer. I've added a boolean property that specifies whether we're in the alt buffer or not, and use that to decide which of the two instances we're working with. I also needed to explicitly trigger a save when switching to the alt buffer, and a restore when switching back, since we weren't already doing that (the existing implementation gave the impression that the state was saved, because each buffer has its own cursor position, but that doesn't properly match the XTerm behaviour). For the state tracking itself, we've now got two additional properties - the SGR attributes, which we obtain via the new private API, and the active character set, which we get from a local `AdaptDispatch` field. I'm saving the whole `TermOutput` class for the character set, since I'm hoping that will make it automatically supports future enhancements. When restoring the cursor position, there is also now a fix to handle the relative origin mode correctly. If the margins are changed between the position being saved and restored, it's possible for the cursor to end up outside of the new margins, which would be illegal. So there is now an additional step that clamps the Y coordinate within the margin boundaries if the origin mode is relative. # Validation I've added a couple of screen buffer tests which check that the various parameters are saved and restored as expected, as well as checking that the Y coordinate is clamped appropriately when the relative origin mode is set. I've also tested manually with vttest and confirmed that the _SAVE/RESTORE CURSOR_ test (the last page of the _Test of screen features_)) is now working a lot better than it used to. Closes #148.
2019-11-05 22:35:50 +01:00
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "WexTestClass.h"
#include "..\..\inc\consoletaeftemplates.hpp"
#include "CommonState.hpp"
#include "globals.h"
#include "screenInfo.hpp"
#include "input.h"
#include "getset.h"
#include "_stream.h" // For WriteCharsLegacy
#include "..\interactivity\inc\ServiceLocator.hpp"
#include "..\..\inc\conattrs.hpp"
#include "..\..\types\inc\Viewport.hpp"
#include <sstream>
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::Interactivity;
using namespace Microsoft::Console::VirtualTerminal;
class ScreenBufferTests
{
CommonState* m_state;
TEST_CLASS(ScreenBufferTests);
TEST_CLASS_SETUP(ClassSetup)
{
m_state = new CommonState();
m_state->InitEvents();
m_state->PrepareGlobalFont();
m_state->PrepareGlobalScreenBuffer();
m_state->PrepareGlobalInputBuffer();
return true;
}
TEST_CLASS_CLEANUP(ClassCleanup)
{
m_state->CleanupGlobalScreenBuffer();
m_state->CleanupGlobalFont();
m_state->CleanupGlobalInputBuffer();
delete m_state;
return true;
}
TEST_METHOD_SETUP(MethodSetup)
{
// Set up some sane defaults
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetDefaultForegroundColor(INVALID_COLOR);
gci.SetDefaultBackgroundColor(INVALID_COLOR);
gci.SetFillAttribute(0x07); // DARK_WHITE on DARK_BLACK
m_state->PrepareNewTextBufferInfo();
auto& currentBuffer = gci.GetActiveOutputBuffer();
// Make sure a test hasn't left us in the alt buffer on accident
VERIFY_IS_FALSE(currentBuffer._IsAltBuffer());
VERIFY_SUCCEEDED(currentBuffer.SetViewportOrigin(true, { 0, 0 }, true));
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), currentBuffer.GetTextBuffer().GetCursor().GetPosition());
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
m_state->CleanupNewTextBufferInfo();
return true;
}
TEST_METHOD(SingleAlternateBufferCreationTest);
TEST_METHOD(MultipleAlternateBufferCreationTest);
TEST_METHOD(MultipleAlternateBuffersFromMainCreationTest);
TEST_METHOD(TestReverseLineFeed);
TEST_METHOD(TestAddTabStop);
TEST_METHOD(TestClearTabStops);
TEST_METHOD(TestClearTabStop);
TEST_METHOD(TestGetForwardTab);
TEST_METHOD(TestGetReverseTab);
TEST_METHOD(TestAreTabsSet);
TEST_METHOD(TestAltBufferDefaultTabStops);
TEST_METHOD(EraseAllTests);
TEST_METHOD(OutputNULTest);
TEST_METHOD(VtResize);
TEST_METHOD(VtResizeComprehensive);
TEST_METHOD(VtResizeDECCOLM);
TEST_METHOD(VtSoftResetCursorPosition);
TEST_METHOD(VtScrollMarginsNewlineColor);
TEST_METHOD(VtNewlinePastViewport);
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-11 00:14:40 +01:00
TEST_METHOD(VtNewlinePastEndOfBuffer);
TEST_METHOD(VtNewlineOutsideMargins);
TEST_METHOD(VtSetColorTable);
TEST_METHOD(ResizeTraditionalDoesntDoubleFreeAttrRows);
TEST_METHOD(ResizeCursorUnchanged);
TEST_METHOD(ResizeAltBuffer);
TEST_METHOD(ResizeAltBufferGetScreenBufferInfo);
TEST_METHOD(VtEraseAllPersistCursor);
TEST_METHOD(VtEraseAllPersistCursorFillColor);
TEST_METHOD(GetWordBoundary);
void GetWordBoundaryTrimZeros(bool on);
TEST_METHOD(GetWordBoundaryTrimZerosOn);
TEST_METHOD(GetWordBoundaryTrimZerosOff);
TEST_METHOD(TestAltBufferCursorState);
TEST_METHOD(TestAltBufferVtDispatching);
TEST_METHOD(SetDefaultsIndividuallyBothDefault);
TEST_METHOD(SetDefaultsTogether);
TEST_METHOD(ReverseResetWithDefaultBackground);
TEST_METHOD(BackspaceDefaultAttrs);
TEST_METHOD(BackspaceDefaultAttrsWriteCharsLegacy);
TEST_METHOD(BackspaceDefaultAttrsInPrompt);
TEST_METHOD(SetGlobalColorTable);
TEST_METHOD(SetColorTableThreeDigits);
TEST_METHOD(SetDefaultForegroundColor);
TEST_METHOD(SetDefaultBackgroundColor);
TEST_METHOD(DeleteCharsNearEndOfLine);
TEST_METHOD(DeleteCharsNearEndOfLineSimpleFirstCase);
TEST_METHOD(DeleteCharsNearEndOfLineSimpleSecondCase);
TEST_METHOD(DontResetColorsAboveVirtualBottom);
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
TEST_METHOD(ScrollOperations);
TEST_METHOD(InsertChars);
TEST_METHOD(DeleteChars);
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-11 00:14:40 +01:00
TEST_METHOD(EraseScrollbackTests);
TEST_METHOD(EraseTests);
TEST_METHOD(ScrollUpInMargins);
TEST_METHOD(ScrollDownInMargins);
TEST_METHOD(InsertLinesInMargins);
TEST_METHOD(DeleteLinesInMargins);
TEST_METHOD(ReverseLineFeedInMargins);
TEST_METHOD(ScrollLines256Colors);
TEST_METHOD(SetOriginMode);
Make the RIS command clear the display and scrollback correctly (#2367) When the scrollback buffer is empty, the RIS escape sequence (Reset to Initial State) will fail to clear the screen, or reset any of the state. And when there is something in the scrollback, it doesn't get cleared completely, and the screen may get filled with the wrong background color (it should use the default color, but it actually uses the previously active background color). This commit attempts to fix those issues. The initial failure is caused by the `SCREEN_INFORMATION::WriteRect` method throwing an exception when passed an empty viewport. And the reason it's passed an empty viewport is because that's what the `Viewport::Subtract` method returns when the result of the subtraction is nothing. The PR fixes the problem by making the `Viewport::Subtract` method actually return nothing in that situation. This is a change in the defined behavior that also required the associated viewport tests to be updated. However, it does seem a sensible change, since the `Subtract` method never returns empty viewports under any other circumstances. And the only place the method seems to be used is in the `ScrollRegion` implementation, where the previous behavior is guaranteed to throw an exception. The other issues are fixed simply by changing the order in which things are reset in the `AdaptDispatch::HardReset` method. The call to `SoftReset` needed to be made first, so that the SGR attributes would be reset before the screen was cleared, thus making sure that the default background color would be used. And the screen needed to be cleared before the scrollback was erased, otherwise the last view of the screen would be retained in the scrollback buffer. These changes also required existing adapter tests to be updated, but not because of a change in the expected behaviour. It's just that certain tests relied on the `SoftReset` happening later in the order, so weren't expecting it to be called if say the scrollback erase had failed. It doesn't seem like the tests were deliberately trying to verify that the SoftReset _hadn't_ been called. In addition to the updates to existing tests, this PR also add a new screen buffer test which verifies the display and scrollback are correctly cleared under the conditions that were previously failing. Fixes #2307.
2019-08-28 03:45:38 +02:00
TEST_METHOD(HardResetBuffer);
TEST_METHOD(RestoreDownAltBufferWithTerminalScrolling);
TEST_METHOD(SnapCursorWithTerminalScrolling);
TEST_METHOD(ClearAlternateBuffer);
TEST_METHOD(InitializeTabStopsInVTMode);
TEST_METHOD(TestExtendedTextAttributes);
TEST_METHOD(TestExtendedTextAttributesWithColors);
TEST_METHOD(CursorUpDownAcrossMargins);
TEST_METHOD(CursorUpDownOutsideMargins);
TEST_METHOD(CursorUpDownExactlyAtMargins);
Improve the DECSC/DECRC implementation (#3160) The current DECSC implementation only saves the cursor position and origin mode. This PR improves that functionality with additional support for saving the SGR attributes, as well as the active character set. It also fixes the way the saved state interacts with the alt buffer (private mode 1049), triggering a save when switching to the alt buffer, and a restore when switching back, and tracking the alt buffer state independently from the main state. In order to properly save and restore the SGR attributes, we first needed to add a pair of APIs in the `ConGetSet` interface which could round-trip the attributes with full 32-bit colors (the existing methods only work with legacy attributes). I then added a struct in the `AdaptDispatch` implementation to make it easier to manage all of the parameters that needed to be saved. This includes the cursor position and origin mode that we were already tracking, and now also the SGR text attributes and the active character set (via the `TermOutput` class). Two instances of this structure are required, since changes made to the saved state in the alt buffer need to be tracked separately from changes in the main buffer. I've added a boolean property that specifies whether we're in the alt buffer or not, and use that to decide which of the two instances we're working with. I also needed to explicitly trigger a save when switching to the alt buffer, and a restore when switching back, since we weren't already doing that (the existing implementation gave the impression that the state was saved, because each buffer has its own cursor position, but that doesn't properly match the XTerm behaviour). For the state tracking itself, we've now got two additional properties - the SGR attributes, which we obtain via the new private API, and the active character set, which we get from a local `AdaptDispatch` field. I'm saving the whole `TermOutput` class for the character set, since I'm hoping that will make it automatically supports future enhancements. When restoring the cursor position, there is also now a fix to handle the relative origin mode correctly. If the margins are changed between the position being saved and restored, it's possible for the cursor to end up outside of the new margins, which would be illegal. So there is now an additional step that clamps the Y coordinate within the margin boundaries if the origin mode is relative. # Validation I've added a couple of screen buffer tests which check that the various parameters are saved and restored as expected, as well as checking that the Y coordinate is clamped appropriately when the relative origin mode is set. I've also tested manually with vttest and confirmed that the _SAVE/RESTORE CURSOR_ test (the last page of the _Test of screen features_)) is now working a lot better than it used to. Closes #148.
2019-11-05 22:35:50 +01:00
TEST_METHOD(CursorSaveRestore);
Add support for the DECALN escape sequence (#3968) ## Summary of the Pull Request This adds support for the [`DECALN`](https://vt100.net/docs/vt510-rm/DECALN.html) escape sequence, which produces a kind of test pattern, originally used on VT terminals to adjust the screen alignment. It's needed to pass several of the tests in the [Vttest](https://invisible-island.net/vttest/) suite. ## PR Checklist * [x] Closes #3671 * [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 discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx ## Detailed Description of the Pull Request / Additional comments To start with, the `ActionEscDispatch` method in the `OutputStateMachineEngine` needed to be extended to check for a new intermediate type (`#`). Then when that intermediate is followed by an `8`, it dispatches to a new `ScreenAlignmentPattern` method in the `ITermDispatch` interface. The implementation of the `ScreenAlignmentPattern` itself is fairly simple. It uses the recently added `PrivateFillRegion` API to fill the screen with the character `E` using default attributes. Then in addition to that, a bunch of VT properties are reset: * The meta/extended attributes are reset (although the active colors must be left unchanged). * The origin mode is set to absolute positioning. * The scrolling margins are cleared. * The cursor position is moved to home. ## Validation Steps Performed I've added a screen buffer test that makes sure the `DECALN` sequence fills the screen with the correct character and attributes, and that the above mentioned properties are all updated appropriately. I've also tested in Vttest, and confirmed that the first two pages of the _Test of cursor movements_ are now showing the frame of E's that are expected there.
2019-12-17 19:11:52 +01:00
TEST_METHOD(ScreenAlignmentPattern);
};
void ScreenBufferTests::SingleAlternateBufferCreationTest()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
Log::Comment(L"Testing creating one alternate buffer, then returning to the main buffer.");
SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer();
VERIFY_IS_NULL(psiOriginal->_psiAlternateBuffer);
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer();
if (VERIFY_IS_TRUE(NT_SUCCESS(Status)))
{
Log::Comment(L"First alternate buffer successfully created");
SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer();
VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate);
VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer);
VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer);
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer);
psiFirstAlternate->UseMainScreenBuffer();
Log::Comment(L"successfully swapped to the main buffer");
SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer();
VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate);
VERIFY_ARE_EQUAL(psiFinal, psiOriginal);
VERIFY_IS_NULL(psiFinal->_psiMainBuffer);
VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer);
}
}
void ScreenBufferTests::MultipleAlternateBufferCreationTest()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
Log::Comment(
L"Testing creating one alternate buffer, then creating another "
L"alternate from that first alternate, before returning to the "
L"main buffer.");
SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer();
NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer();
if (VERIFY_IS_TRUE(NT_SUCCESS(Status)))
{
Log::Comment(L"First alternate buffer successfully created");
SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer();
VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate);
VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer);
VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer);
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer);
Status = psiFirstAlternate->UseAlternateScreenBuffer();
if (VERIFY_IS_TRUE(NT_SUCCESS(Status)))
{
Log::Comment(L"Second alternate buffer successfully created");
SCREEN_INFORMATION* psiSecondAlternate = &gci.GetActiveOutputBuffer();
VERIFY_ARE_NOT_EQUAL(psiOriginal, psiSecondAlternate);
VERIFY_ARE_NOT_EQUAL(psiSecondAlternate, psiFirstAlternate);
VERIFY_ARE_EQUAL(psiSecondAlternate, psiOriginal->_psiAlternateBuffer);
VERIFY_ARE_EQUAL(psiOriginal, psiSecondAlternate->_psiMainBuffer);
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
VERIFY_IS_NULL(psiSecondAlternate->_psiAlternateBuffer);
psiSecondAlternate->UseMainScreenBuffer();
Log::Comment(L"successfully swapped to the main buffer");
SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer();
VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate);
VERIFY_ARE_NOT_EQUAL(psiFinal, psiSecondAlternate);
VERIFY_ARE_EQUAL(psiFinal, psiOriginal);
VERIFY_IS_NULL(psiFinal->_psiMainBuffer);
VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer);
}
}
}
void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
Log::Comment(
L"Testing creating one alternate buffer, then creating another"
L" alternate from the main, before returning to the main buffer.");
SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer();
NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer();
if (VERIFY_IS_TRUE(NT_SUCCESS(Status)))
{
Log::Comment(L"First alternate buffer successfully created");
SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer();
VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate);
VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer);
VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer);
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer);
Status = psiOriginal->UseAlternateScreenBuffer();
if (VERIFY_IS_TRUE(NT_SUCCESS(Status)))
{
Log::Comment(L"Second alternate buffer successfully created");
SCREEN_INFORMATION* const psiSecondAlternate = &gci.GetActiveOutputBuffer();
VERIFY_ARE_NOT_EQUAL(psiOriginal, psiSecondAlternate);
VERIFY_ARE_NOT_EQUAL(psiSecondAlternate, psiFirstAlternate);
VERIFY_ARE_EQUAL(psiSecondAlternate, psiOriginal->_psiAlternateBuffer);
VERIFY_ARE_EQUAL(psiOriginal, psiSecondAlternate->_psiMainBuffer);
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
VERIFY_IS_NULL(psiSecondAlternate->_psiAlternateBuffer);
psiSecondAlternate->UseMainScreenBuffer();
Log::Comment(L"successfully swapped to the main buffer");
SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer();
VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate);
VERIFY_ARE_NOT_EQUAL(psiFinal, psiSecondAlternate);
VERIFY_ARE_EQUAL(psiFinal, psiOriginal);
VERIFY_IS_NULL(psiFinal->_psiMainBuffer);
VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer);
}
}
}
void ScreenBufferTests::TestReverseLineFeed()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
auto& stateMachine = screenInfo.GetStateMachine();
auto& cursor = screenInfo._textBuffer->GetCursor();
auto viewport = screenInfo.GetViewport();
VERIFY_ARE_EQUAL(viewport.Top(), 0);
////////////////////////////////////////////////////////////////////////
Log::Comment(L"Case 1: RI from below top of viewport");
stateMachine.ProcessString(L"foo\nfoo", 7);
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1);
VERIFY_ARE_EQUAL(viewport.Top(), 0);
VERIFY_SUCCEEDED(DoSrvPrivateReverseLineFeed(screenInfo));
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0);
viewport = screenInfo.GetViewport();
VERIFY_ARE_EQUAL(viewport.Top(), 0);
Log::Comment(NoThrowString().Format(
L"viewport={L:%d,T:%d,R:%d,B:%d}",
viewport.Left(),
viewport.Top(),
viewport.RightInclusive(),
viewport.BottomInclusive()));
////////////////////////////////////////////////////////////////////////
Log::Comment(L"Case 2: RI from top of viewport");
cursor.SetPosition({ 0, 0 });
stateMachine.ProcessString(L"123456789", 9);
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0);
VERIFY_ARE_EQUAL(screenInfo.GetViewport().Top(), 0);
VERIFY_SUCCEEDED(DoSrvPrivateReverseLineFeed(screenInfo));
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0);
viewport = screenInfo.GetViewport();
VERIFY_ARE_EQUAL(viewport.Top(), 0);
Log::Comment(NoThrowString().Format(
L"viewport={L:%d,T:%d,R:%d,B:%d}",
viewport.Left(),
viewport.Top(),
viewport.RightInclusive(),
viewport.BottomInclusive()));
auto c = screenInfo._textBuffer->GetLastNonSpaceCharacter();
VERIFY_ARE_EQUAL(c.Y, 2); // This is the coordinates of the second "foo" from before.
////////////////////////////////////////////////////////////////////////
Log::Comment(L"Case 3: RI from top of viewport, when viewport is below top of buffer");
cursor.SetPosition({ 0, 5 });
VERIFY_SUCCEEDED(screenInfo.SetViewportOrigin(true, { 0, 5 }, true));
stateMachine.ProcessString(L"ABCDEFGH");
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 8);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5);
VERIFY_ARE_EQUAL(screenInfo.GetViewport().Top(), 5);
LOG_IF_FAILED(DoSrvPrivateReverseLineFeed(screenInfo));
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 8);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5);
viewport = screenInfo.GetViewport();
VERIFY_ARE_EQUAL(viewport.Top(), 5);
Log::Comment(NoThrowString().Format(
L"viewport={L:%d,T:%d,R:%d,B:%d}",
viewport.Left(),
viewport.Top(),
viewport.RightInclusive(),
viewport.BottomInclusive()));
c = screenInfo._textBuffer->GetLastNonSpaceCharacter();
VERIFY_ARE_EQUAL(c.Y, 6);
}
void ScreenBufferTests::TestAddTabStop()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
screenInfo.ClearTabStops();
auto scopeExit = wil::scope_exit([&]() { screenInfo.ClearTabStops(); });
std::list<short> expectedStops{ 12 };
Log::Comment(L"Add tab to empty list.");
screenInfo.AddTabStop(12);
VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops);
Log::Comment(L"Add tab to head of existing list.");
screenInfo.AddTabStop(4);
expectedStops.push_front(4);
VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops);
Log::Comment(L"Add tab to tail of existing list.");
screenInfo.AddTabStop(30);
expectedStops.push_back(30);
VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops);
Log::Comment(L"Add tab to middle of existing list.");
screenInfo.AddTabStop(24);
expectedStops.push_back(24);
expectedStops.sort();
VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops);
Log::Comment(L"Add tab that duplicates an item in the existing list.");
screenInfo.AddTabStop(24);
VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops);
}
void ScreenBufferTests::TestClearTabStops()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
Log::Comment(L"Clear non-existant tab stops.");
{
screenInfo.ClearTabStops();
VERIFY_IS_TRUE(screenInfo._tabStops.empty());
}
Log::Comment(L"Clear handful of tab stops.");
{
for (auto x : { 3, 6, 13, 2, 25 })
{
screenInfo.AddTabStop(gsl::narrow<short>(x));
}
VERIFY_IS_FALSE(screenInfo._tabStops.empty());
screenInfo.ClearTabStops();
VERIFY_IS_TRUE(screenInfo._tabStops.empty());
}
}
void ScreenBufferTests::TestClearTabStop()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
Log::Comment(L"Try to clear nonexistant list.");
{
screenInfo.ClearTabStop(0);
VERIFY_IS_TRUE(screenInfo._tabStops.empty(), L"List should remain empty");
}
Log::Comment(L"Allocate 1 list item and clear it.");
{
screenInfo._tabStops.push_back(0);
screenInfo.ClearTabStop(0);
VERIFY_IS_TRUE(screenInfo._tabStops.empty());
}
Log::Comment(L"Allocate 1 list item and clear non-existant.");
{
screenInfo._tabStops.push_back(0);
Log::Comment(L"Free greater");
screenInfo.ClearTabStop(1);
VERIFY_IS_FALSE(screenInfo._tabStops.empty());
Log::Comment(L"Free less than");
screenInfo.ClearTabStop(-1);
VERIFY_IS_FALSE(screenInfo._tabStops.empty());
// clear all tab stops
screenInfo._tabStops.clear();
}
Log::Comment(L"Allocate many (5) list items and clear head.");
{
std::list<short> inputData = { 3, 5, 6, 10, 15, 17 };
screenInfo._tabStops = inputData;
screenInfo.ClearTabStop(inputData.front());
inputData.pop_front();
VERIFY_ARE_EQUAL(inputData, screenInfo._tabStops);
// clear all tab stops
screenInfo._tabStops.clear();
}
Log::Comment(L"Allocate many (5) list items and clear middle.");
{
std::list<short> inputData = { 3, 5, 6, 10, 15, 17 };
screenInfo._tabStops = inputData;
screenInfo.ClearTabStop(*std::next(inputData.begin()));
inputData.erase(std::next(inputData.begin()));
VERIFY_ARE_EQUAL(inputData, screenInfo._tabStops);
// clear all tab stops
screenInfo._tabStops.clear();
}
Log::Comment(L"Allocate many (5) list items and clear tail.");
{
std::list<short> inputData = { 3, 5, 6, 10, 15, 17 };
screenInfo._tabStops = inputData;
screenInfo.ClearTabStop(inputData.back());
inputData.pop_back();
VERIFY_ARE_EQUAL(inputData, screenInfo._tabStops);
// clear all tab stops
screenInfo._tabStops.clear();
}
Log::Comment(L"Allocate many (5) list items and clear non-existant item.");
{
std::list<short> inputData = { 3, 5, 6, 10, 15, 17 };
screenInfo._tabStops = inputData;
screenInfo.ClearTabStop(9000);
VERIFY_ARE_EQUAL(inputData, screenInfo._tabStops);
// clear all tab stops
screenInfo._tabStops.clear();
}
}
void ScreenBufferTests::TestGetForwardTab()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
std::list<short> inputData = { 3, 5, 6, 10, 15, 17 };
si._tabStops = inputData;
const COORD coordScreenBufferSize = si.GetBufferSize().Dimensions();
COORD coordCursor;
coordCursor.Y = coordScreenBufferSize.Y / 2; // in the middle of the buffer, it doesn't make a difference.
Log::Comment(L"Find next tab from before front.");
{
coordCursor.X = 0;
COORD coordCursorExpected;
coordCursorExpected = coordCursor;
coordCursorExpected.X = inputData.front();
COORD const coordCursorResult = si.GetForwardTab(coordCursor);
VERIFY_ARE_EQUAL(coordCursorExpected,
coordCursorResult,
L"Cursor advanced to first tab stop from sample list.");
}
Log::Comment(L"Find next tab from in the middle.");
{
coordCursor.X = 6;
COORD coordCursorExpected;
coordCursorExpected = coordCursor;
coordCursorExpected.X = *std::next(inputData.begin(), 3);
COORD const coordCursorResult = si.GetForwardTab(coordCursor);
VERIFY_ARE_EQUAL(coordCursorExpected,
coordCursorResult,
L"Cursor advanced to middle tab stop from sample list.");
}
Log::Comment(L"Find next tab from end.");
{
coordCursor.X = 30;
COORD coordCursorExpected;
coordCursorExpected = coordCursor;
coordCursorExpected.X = coordScreenBufferSize.X - 1;
COORD const coordCursorResult = si.GetForwardTab(coordCursor);
VERIFY_ARE_EQUAL(coordCursorExpected,
coordCursorResult,
L"Cursor advanced to end of screen buffer.");
}
Prevent the horizontal tab character wrapping at the end of a line (#3197) # Summary of the Pull Request When a horizontal tab ('\t') is output on the last column of the screen, the current implementation moves the cursor position to the start of the next line. However, the DEC STD 070 manual specifies that a horizontal tab shouldn't move past the last column of the active line (or the right margin, if we supported horizontal margins). This PR updates the forward tab implementation, to prevent it wrapping onto a new line when it reaches the end of a line. # Detailed Description of the Pull Request / Additional comments Originally the SCREEN_INFORMATION::GetForwardTab method had a condition which handled a tab at the end of the line as a special case, moving the cursor to the start of the next line. I've simply removed that condition, so an end-of-line tab is handled the same way as any other position (in this case it will just leaves the cursor where it is). While testing, though, I found that there were circumstances where you could have tab stops greater than the width of the screen, and when that happens, a tab can still end up wrapping onto the next line. To fix that I had to add an additional check to make sure the tab position was always clamped to the width of the buffer. With these fixes in place, a tab control should now never move off the active line, so I realised that the DoPrivateTabHelper function could be optimized to calculate all of the tab movements in advance, and then only make a single call to AdjustCursorPosition with the final coordinates. This change is not strictly necessary, though, so it can easily be reverted if there are any objections. Regarding backwards compatibility, note that the GetForwardTab method is only used in two places: when handling a tab character in the WriteCharsLegacy function, but this only applies in VT mode (see here). when handling the CHT escape sequence in the DoPrivateTabHelper function, and obviously an escape sequence would also only be applicable in VT mode. So this change should have no effect on legacy console applications, which wouldn't have VT mode activated. # Validation Steps Performed I've added another step to the TestGetForwardTab test which makes sure that a horizontal tab won't wrap at the end of a line. I've also confirmed that this fixes the last remaining issue in the Test of autowrap in Vttest (pages 3 and 4 of the Test of cursor movements). Although I should note that this only works in conhost.
2019-12-04 19:48:09 +01:00
Log::Comment(L"Find next tab from rightmost column.");
{
coordCursor.X = coordScreenBufferSize.X - 1;
COORD coordCursorExpected;
coordCursorExpected = coordCursor;
COORD const coordCursorResult = si.GetForwardTab(coordCursor);
VERIFY_ARE_EQUAL(coordCursorExpected,
coordCursorResult,
L"Cursor remains in rightmost column.");
}
si._tabStops.clear();
}
void ScreenBufferTests::TestGetReverseTab()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
std::list<short> inputData = { 3, 5, 6, 10, 15, 17 };
si._tabStops = inputData;
COORD coordCursor;
// in the middle of the buffer, it doesn't make a difference.
coordCursor.Y = si.GetBufferSize().Height() / 2;
Log::Comment(L"Find previous tab from before front.");
{
coordCursor.X = 1;
COORD coordCursorExpected;
coordCursorExpected = coordCursor;
coordCursorExpected.X = 0;
COORD const coordCursorResult = si.GetReverseTab(coordCursor);
VERIFY_ARE_EQUAL(coordCursorExpected,
coordCursorResult,
L"Cursor adjusted to beginning of the buffer when it started before sample list.");
}
Log::Comment(L"Find previous tab from in the middle.");
{
coordCursor.X = 6;
COORD coordCursorExpected;
coordCursorExpected = coordCursor;
coordCursorExpected.X = *std::next(inputData.begin());
COORD const coordCursorResult = si.GetReverseTab(coordCursor);
VERIFY_ARE_EQUAL(coordCursorExpected,
coordCursorResult,
L"Cursor adjusted back one tab spot from middle of sample list.");
}
Log::Comment(L"Find next tab from end.");
{
coordCursor.X = 30;
COORD coordCursorExpected;
coordCursorExpected = coordCursor;
coordCursorExpected.X = inputData.back();
COORD const coordCursorResult = si.GetReverseTab(coordCursor);
VERIFY_ARE_EQUAL(coordCursorExpected,
coordCursorResult,
L"Cursor adjusted to last item in the sample list from position beyond end.");
}
si._tabStops.clear();
}
void ScreenBufferTests::TestAreTabsSet()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
si._tabStops.clear();
VERIFY_IS_FALSE(si.AreTabsSet());
si.AddTabStop(1);
VERIFY_IS_TRUE(si.AreTabsSet());
}
void ScreenBufferTests::TestAltBufferDefaultTabStops()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to swap buffers.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer();
// Make sure we're in VT mode
WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
mainBuffer.SetDefaultVtTabStops();
VERIFY_IS_TRUE(mainBuffer.AreTabsSet());
VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer();
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
Log::Comment(NoThrowString().Format(
L"Manually enable VT mode for the alt buffer - "
L"usually the ctor will pick this up from GCI, but not in the tests."));
WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
VERIFY_IS_TRUE(altBuffer.AreTabsSet());
VERIFY_IS_TRUE(altBuffer._tabStops.size() > 3);
const COORD origin{ 0, 0 };
auto& cursor = altBuffer.GetTextBuffer().GetCursor();
cursor.SetPosition(origin);
auto& stateMachine = altBuffer.GetStateMachine();
Log::Comment(NoThrowString().Format(
L"Tab a few times - make sure the cursor is where we expect."));
stateMachine.ProcessString(L"\t");
COORD expected{ 8, 0 };
VERIFY_ARE_EQUAL(expected, cursor.GetPosition());
stateMachine.ProcessString(L"\t");
expected = { 16, 0 };
VERIFY_ARE_EQUAL(expected, cursor.GetPosition());
stateMachine.ProcessString(L"\n");
expected = { 0, 1 };
VERIFY_ARE_EQUAL(expected, cursor.GetPosition());
altBuffer.ClearTabStops();
VERIFY_IS_FALSE(altBuffer.AreTabsSet());
stateMachine.ProcessString(L"\t");
expected = { altBuffer.GetBufferSize().Width() - 1, 1 };
VERIFY_ARE_EQUAL(expected, cursor.GetPosition());
useMain.release();
altBuffer.UseMainScreenBuffer();
VERIFY_IS_TRUE(mainBuffer.AreTabsSet());
}
void ScreenBufferTests::EraseAllTests()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si._textBuffer->GetCursor();
VERIFY_ARE_EQUAL(si.GetViewport().Top(), 0);
////////////////////////////////////////////////////////////////////////
Log::Comment(L"Case 1: Erase a single line of text in the buffer\n");
stateMachine.ProcessString(L"foo", 3);
COORD originalRelativePosition = { 3, 0 };
VERIFY_ARE_EQUAL(si.GetViewport().Top(), 0);
VERIFY_ARE_EQUAL(cursor.GetPosition(), originalRelativePosition);
VERIFY_SUCCEEDED(si.VtEraseAll());
auto viewport = si._viewport;
VERIFY_ARE_EQUAL(viewport.Top(), 1);
COORD newRelativePos = originalRelativePosition;
viewport.ConvertFromOrigin(&newRelativePos);
VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos);
Log::Comment(NoThrowString().Format(
L"viewport={L:%d,T:%d,R:%d,B:%d}",
viewport.Left(),
viewport.Top(),
viewport.RightInclusive(),
viewport.BottomInclusive()));
////////////////////////////////////////////////////////////////////////
Log::Comment(L"Case 2: Erase multiple lines, below the top of the buffer\n");
stateMachine.ProcessString(L"bar\nbar\nbar", 11);
viewport = si._viewport;
originalRelativePosition = cursor.GetPosition();
viewport.ConvertToOrigin(&originalRelativePosition);
VERIFY_ARE_EQUAL(viewport.Top(), 1);
Log::Comment(NoThrowString().Format(
L"viewport={L:%d,T:%d,R:%d,B:%d}",
viewport.Left(),
viewport.Top(),
viewport.RightInclusive(),
viewport.BottomInclusive()));
VERIFY_SUCCEEDED(si.VtEraseAll());
viewport = si._viewport;
VERIFY_ARE_EQUAL(viewport.Top(), 4);
newRelativePos = originalRelativePosition;
viewport.ConvertFromOrigin(&newRelativePos);
VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos);
Log::Comment(NoThrowString().Format(
L"viewport={L:%d,T:%d,R:%d,B:%d}",
viewport.Left(),
viewport.Top(),
viewport.RightInclusive(),
viewport.BottomInclusive()));
////////////////////////////////////////////////////////////////////////
Log::Comment(L"Case 3: multiple lines at the bottom of the buffer\n");
cursor.SetPosition({ 0, 275 });
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 220 }, true));
stateMachine.ProcessString(L"bar\nbar\nbar", 11);
viewport = si._viewport;
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 277);
originalRelativePosition = cursor.GetPosition();
viewport.ConvertToOrigin(&originalRelativePosition);
Log::Comment(NoThrowString().Format(
L"viewport={L:%d,T:%d,R:%d,B:%d}",
viewport.Left(),
viewport.Top(),
viewport.RightInclusive(),
viewport.BottomInclusive()));
VERIFY_SUCCEEDED(si.VtEraseAll());
viewport = si._viewport;
auto heightFromBottom = si.GetBufferSize().Height() - (viewport.Height());
VERIFY_ARE_EQUAL(viewport.Top(), heightFromBottom);
newRelativePos = originalRelativePosition;
viewport.ConvertFromOrigin(&newRelativePos);
VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos);
Log::Comment(NoThrowString().Format(
L"viewport={L:%d,T:%d,R:%d,B:%d}",
viewport.Left(),
viewport.Top(),
viewport.RightInclusive(),
viewport.BottomInclusive()));
}
void ScreenBufferTests::OutputNULTest()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si._textBuffer->GetCursor();
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
Log::Comment(NoThrowString().Format(
L"Writing a single NUL"));
stateMachine.ProcessString(L"\0", 1);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
Log::Comment(NoThrowString().Format(
L"Writing many NULs"));
stateMachine.ProcessString(L"\0\0\0\0\0\0\0\0", 8);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
Log::Comment(NoThrowString().Format(
L"Testing a single NUL followed by real text"));
stateMachine.ProcessString(L"\0foo", 4);
VERIFY_ARE_EQUAL(3, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
stateMachine.ProcessString(L"\n");
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y);
Log::Comment(NoThrowString().Format(
L"Writing NULs in between other strings"));
stateMachine.ProcessString(L"\0foo\0bar\0", 9);
VERIFY_ARE_EQUAL(6, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y);
}
void ScreenBufferTests::VtResize()
{
// Run this test in isolation - for one reason or another, this breaks other tests.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES()
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = tbi.GetCursor();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
cursor.SetXPosition(0);
cursor.SetYPosition(0);
auto initialSbHeight = si.GetBufferSize().Height();
auto initialSbWidth = si.GetBufferSize().Width();
auto initialViewHeight = si.GetViewport().Height();
auto initialViewWidth = si.GetViewport().Width();
Log::Comment(NoThrowString().Format(
L"Write '\x1b[8;30;80t'"
L" The Screen buffer height should remain unchanged, but the width should be 80 columns"
L" The viewport should be w,h=80,30"));
std::wstring sequence = L"\x1b[8;30;80t";
stateMachine.ProcessString(&sequence[0], sequence.length());
auto newSbHeight = si.GetBufferSize().Height();
auto newSbWidth = si.GetBufferSize().Width();
auto newViewHeight = si.GetViewport().Height();
auto newViewWidth = si.GetViewport().Width();
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(80, newSbWidth);
VERIFY_ARE_EQUAL(30, newViewHeight);
VERIFY_ARE_EQUAL(80, newViewWidth);
initialSbHeight = newSbHeight;
initialSbWidth = newSbWidth;
initialViewHeight = newViewHeight;
initialViewWidth = newViewWidth;
Log::Comment(NoThrowString().Format(
L"Write '\x1b[8;40;80t'"
L" The Screen buffer height should remain unchanged, but the width should be 80 columns"
L" The viewport should be w,h=80,40"));
sequence = L"\x1b[8;40;80t";
stateMachine.ProcessString(&sequence[0], sequence.length());
newSbHeight = si.GetBufferSize().Height();
newSbWidth = si.GetBufferSize().Width();
newViewHeight = si.GetViewport().Height();
newViewWidth = si.GetViewport().Width();
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(80, newSbWidth);
VERIFY_ARE_EQUAL(40, newViewHeight);
VERIFY_ARE_EQUAL(80, newViewWidth);
initialSbHeight = newSbHeight;
initialSbWidth = newSbWidth;
initialViewHeight = newViewHeight;
initialViewWidth = newViewWidth;
Log::Comment(NoThrowString().Format(
L"Write '\x1b[8;40;90t'"
L" The Screen buffer height should remain unchanged, but the width should be 90 columns"
L" The viewport should be w,h=90,40"));
sequence = L"\x1b[8;40;90t";
stateMachine.ProcessString(&sequence[0], sequence.length());
newSbHeight = si.GetBufferSize().Height();
newSbWidth = si.GetBufferSize().Width();
newViewHeight = si.GetViewport().Height();
newViewWidth = si.GetViewport().Width();
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(90, newSbWidth);
VERIFY_ARE_EQUAL(40, newViewHeight);
VERIFY_ARE_EQUAL(90, newViewWidth);
initialSbHeight = newSbHeight;
initialSbWidth = newSbWidth;
initialViewHeight = newViewHeight;
initialViewWidth = newViewWidth;
Log::Comment(NoThrowString().Format(
L"Write '\x1b[8;12;12t'"
L" The Screen buffer height should remain unchanged, but the width should be 12 columns"
L" The viewport should be w,h=12,12"));
sequence = L"\x1b[8;12;12t";
stateMachine.ProcessString(&sequence[0], sequence.length());
newSbHeight = si.GetBufferSize().Height();
newSbWidth = si.GetBufferSize().Width();
newViewHeight = si.GetViewport().Height();
newViewWidth = si.GetViewport().Width();
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(12, newSbWidth);
VERIFY_ARE_EQUAL(12, newViewHeight);
VERIFY_ARE_EQUAL(12, newViewWidth);
initialSbHeight = newSbHeight;
initialSbWidth = newSbWidth;
initialViewHeight = newViewHeight;
initialViewWidth = newViewWidth;
Log::Comment(NoThrowString().Format(
L"Write '\x1b[8;0;0t'"
L" Nothing should change"));
sequence = L"\x1b[8;0;0t";
stateMachine.ProcessString(&sequence[0], sequence.length());
newSbHeight = si.GetBufferSize().Height();
newSbWidth = si.GetBufferSize().Width();
newViewHeight = si.GetViewport().Height();
newViewWidth = si.GetViewport().Width();
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(initialSbWidth, newSbWidth);
VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight);
VERIFY_ARE_EQUAL(initialViewWidth, newViewWidth);
}
void ScreenBufferTests::VtResizeComprehensive()
{
// Run this test in isolation - for one reason or another, this breaks other tests.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 0, 1, 10}")
TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}")
END_TEST_METHOD_PROPERTIES()
int dx, dy;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer");
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer");
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = tbi.GetCursor();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
cursor.SetXPosition(0);
cursor.SetYPosition(0);
auto initialViewHeight = si.GetViewport().Height();
auto initialViewWidth = si.GetViewport().Width();
auto expectedViewWidth = initialViewWidth + dx;
auto expectedViewHeight = initialViewHeight + dy;
std::wstringstream ss;
ss << L"\x1b[8;" << expectedViewHeight << L";" << expectedViewWidth << L"t";
Log::Comment(NoThrowString().Format(
L"Write '\\x1b[8;%d;%dt'"
L" The viewport should be w,h=%d,%d",
expectedViewHeight,
expectedViewWidth,
expectedViewWidth,
expectedViewHeight));
std::wstring sequence = ss.str();
stateMachine.ProcessString(sequence);
auto newViewHeight = si.GetViewport().Height();
auto newViewWidth = si.GetViewport().Width();
VERIFY_ARE_EQUAL(expectedViewWidth, newViewWidth);
VERIFY_ARE_EQUAL(expectedViewHeight, newViewHeight);
}
void ScreenBufferTests::VtResizeDECCOLM()
{
// Run this test in isolation - for one reason or another, this breaks other tests.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES()
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
const auto setInitialMargins = L"\x1b[5;15r";
const auto setInitialCursor = L"\x1b[10;40HABCDEF";
const auto allowDECCOLM = L"\x1b[?40h";
const auto disallowDECCOLM = L"\x1b[?40l";
const auto setDECCOLM = L"\x1b[?3h";
const auto resetDECCOLM = L"\x1b[?3l";
auto getRelativeCursorPosition = [&]() {
return si.GetTextBuffer().GetCursor().GetPosition() - si.GetViewport().Origin();
};
stateMachine.ProcessString(setInitialMargins);
stateMachine.ProcessString(setInitialCursor);
auto initialMargins = si.GetRelativeScrollMargins();
auto initialCursorPosition = getRelativeCursorPosition();
auto initialSbHeight = si.GetBufferSize().Height();
auto initialSbWidth = si.GetBufferSize().Width();
auto initialViewHeight = si.GetViewport().Height();
auto initialViewWidth = si.GetViewport().Width();
Log::Comment(L"By default, setting DECCOLM should have no effect");
stateMachine.ProcessString(setDECCOLM);
auto newSbHeight = si.GetBufferSize().Height();
auto newSbWidth = si.GetBufferSize().Width();
auto newViewHeight = si.GetViewport().Height();
auto newViewWidth = si.GetViewport().Width();
VERIFY_IS_TRUE(si.AreMarginsSet());
VERIFY_ARE_EQUAL(initialMargins, si.GetRelativeScrollMargins());
VERIFY_ARE_EQUAL(initialCursorPosition, getRelativeCursorPosition());
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight);
VERIFY_ARE_EQUAL(initialSbWidth, newSbWidth);
VERIFY_ARE_EQUAL(initialViewWidth, newViewWidth);
stateMachine.ProcessString(setInitialMargins);
stateMachine.ProcessString(setInitialCursor);
initialSbHeight = newSbHeight;
initialSbWidth = newSbWidth;
initialViewHeight = newViewHeight;
initialViewWidth = newViewWidth;
Log::Comment(
L"Once DECCOLM is allowed, setting it "
L"should change the width to 132 columns "
L"and reset the margins and cursor position");
stateMachine.ProcessString(allowDECCOLM);
stateMachine.ProcessString(setDECCOLM);
newSbHeight = si.GetBufferSize().Height();
newSbWidth = si.GetBufferSize().Width();
newViewHeight = si.GetViewport().Height();
newViewWidth = si.GetViewport().Width();
VERIFY_IS_FALSE(si.AreMarginsSet());
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), getRelativeCursorPosition());
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight);
VERIFY_ARE_EQUAL(132, newSbWidth);
VERIFY_ARE_EQUAL(132, newViewWidth);
stateMachine.ProcessString(setInitialMargins);
stateMachine.ProcessString(setInitialCursor);
initialMargins = si.GetRelativeScrollMargins();
initialCursorPosition = getRelativeCursorPosition();
initialSbHeight = newSbHeight;
initialSbWidth = newSbWidth;
initialViewHeight = newViewHeight;
initialViewWidth = newViewWidth;
Log::Comment(L"If DECCOLM is disallowed, resetting it should have no effect");
stateMachine.ProcessString(disallowDECCOLM);
stateMachine.ProcessString(resetDECCOLM);
newSbHeight = si.GetBufferSize().Height();
newSbWidth = si.GetBufferSize().Width();
newViewHeight = si.GetViewport().Height();
newViewWidth = si.GetViewport().Width();
VERIFY_IS_TRUE(si.AreMarginsSet());
VERIFY_ARE_EQUAL(initialMargins, si.GetRelativeScrollMargins());
VERIFY_ARE_EQUAL(initialCursorPosition, getRelativeCursorPosition());
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight);
VERIFY_ARE_EQUAL(initialSbWidth, newSbWidth);
VERIFY_ARE_EQUAL(initialViewWidth, newViewWidth);
stateMachine.ProcessString(setInitialMargins);
stateMachine.ProcessString(setInitialCursor);
initialSbHeight = newSbHeight;
initialSbWidth = newSbWidth;
initialViewHeight = newViewHeight;
initialViewWidth = newViewWidth;
Log::Comment(
L"Once DECCOLM is allowed again, resetting it "
L"should change the width to 80 columns "
L"and reset the margins and cursor position");
stateMachine.ProcessString(allowDECCOLM);
stateMachine.ProcessString(resetDECCOLM);
newSbHeight = si.GetBufferSize().Height();
newSbWidth = si.GetBufferSize().Width();
newViewHeight = si.GetViewport().Height();
newViewWidth = si.GetViewport().Width();
VERIFY_IS_FALSE(si.AreMarginsSet());
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), getRelativeCursorPosition());
VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight);
VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight);
VERIFY_ARE_EQUAL(80, newSbWidth);
VERIFY_ARE_EQUAL(80, newViewWidth);
}
void ScreenBufferTests::VtSoftResetCursorPosition()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
const Cursor& cursor = tbi.GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
Log::Comment(NoThrowString().Format(
L"Move the cursor to 2,2, then execute a soft reset.\n"
L"The cursor should not move."));
std::wstring seq = L"\x1b[2;2H";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition());
seq = L"\x1b[!p";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition());
Log::Comment(NoThrowString().Format(
L"Set some margins. The cursor should move home."));
seq = L"\x1b[2;10r";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
Log::Comment(NoThrowString().Format(
L"Move the cursor to 2,2, then execute a soft reset.\n"
L"The cursor should not move, even though there are margins."));
seq = L"\x1b[2;2H";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition());
seq = L"\x1b[!p";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition());
Log::Comment(
L"Set the origin mode, some margins, and move the cursor to 2,2.\n"
L"The position should be relative to the top-left of the margin area.");
stateMachine.ProcessString(L"\x1b[?6h");
stateMachine.ProcessString(L"\x1b[5;10r");
stateMachine.ProcessString(L"\x1b[2;2H");
VERIFY_ARE_EQUAL(COORD({ 1, 5 }), cursor.GetPosition());
Log::Comment(
L"Execute a soft reset, reapply the margins, and move the cursor to 2,2.\n"
L"The position should now be relative to the top-left of the screen.");
stateMachine.ProcessString(L"\x1b[!p");
stateMachine.ProcessString(L"\x1b[5;10r");
stateMachine.ProcessString(L"\x1b[2;2H");
VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition());
}
void ScreenBufferTests::VtScrollMarginsNewlineColor()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition(COORD({ 0, 0 }));
const COLORREF yellow = RGB(255, 255, 0);
const COLORREF magenta = RGB(255, 0, 255);
gci.SetDefaultForegroundColor(yellow);
gci.SetDefaultBackgroundColor(magenta);
const TextAttribute defaultAttrs = gci.GetDefaultAttributes();
si.SetAttributes(defaultAttrs);
Log::Comment(NoThrowString().Format(L"Begin by clearing the screen."));
std::wstring seq = L"\x1b[2J";
stateMachine.ProcessString(seq);
seq = L"\x1b[m";
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"Set the margins to 2, 5, then emit 10 'X\\n' strings. "
L"Each time, check that rows 0-10 have default attributes in their entire row."));
seq = L"\x1b[2;5r";
stateMachine.ProcessString(seq);
// Make sure we clear the margins to not screw up another test.
auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); });
for (int iteration = 0; iteration < 10; iteration++)
{
Log::Comment(NoThrowString().Format(
L"Iteration:%d", iteration));
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\n";
stateMachine.ProcessString(seq);
const COORD cursorPos = cursor.GetPosition();
Log::Comment(NoThrowString().Format(
L"Cursor=%s",
VerifyOutputTraits<COORD>::ToString(cursorPos).GetBuffer()));
const auto viewport = si.GetViewport();
Log::Comment(NoThrowString().Format(
L"Viewport=%s",
VerifyOutputTraits<SMALL_RECT>::ToString(viewport.ToInclusive()).GetBuffer()));
const auto viewTop = viewport.Top();
for (int y = viewTop; y < viewTop + 10; y++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
const ROW& row = tbi.GetRowByOffset(y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
for (int x = 0; x < viewport.RightInclusive(); x++)
{
const auto& attr = attrs[x];
VERIFY_ARE_EQUAL(false, attr.IsLegacy());
VERIFY_ARE_EQUAL(defaultAttrs, attr);
VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attr));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attr));
}
}
}
}
void ScreenBufferTests::VtNewlinePastViewport()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
// Make sure we're in VT mode
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition(COORD({ 0, 0 }));
std::wstring seq = L"\x1b[m";
stateMachine.ProcessString(seq);
seq = L"\x1b[2J";
stateMachine.ProcessString(seq);
const TextAttribute defaultAttrs{};
Log::Comment(NoThrowString().Format(
L"Move the cursor to the bottom of the viewport"));
const auto initialViewport = si.GetViewport();
Log::Comment(NoThrowString().Format(
L"initialViewport=%s",
VerifyOutputTraits<SMALL_RECT>::ToString(initialViewport.ToInclusive()).GetBuffer()));
cursor.SetPosition(COORD({ 0, initialViewport.BottomInclusive() }));
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-11 00:14:40 +01:00
// Set the attributes that will be used to initialize new rows.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(fillAttr);
// But note that the meta attributes are expected to be cleared.
auto expectedFillAttr = fillAttr;
expectedFillAttr.SetStandardErase();
seq = L"\n";
stateMachine.ProcessString(seq);
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-11 00:14:40 +01:00
const auto viewport = si.GetViewport();
Log::Comment(NoThrowString().Format(
L"viewport=%s",
VerifyOutputTraits<SMALL_RECT>::ToString(viewport.ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().Y);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
for (int y = viewport.Top(); y < viewport.BottomInclusive(); y++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
const ROW& row = tbi.GetRowByOffset(y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
for (int x = 0; x < viewport.RightInclusive(); x++)
{
const auto& attr = attrs[x];
VERIFY_ARE_EQUAL(defaultAttrs, attr);
}
}
const ROW& row = tbi.GetRowByOffset(viewport.BottomInclusive());
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
for (int x = 0; x < viewport.RightInclusive(); x++)
{
const auto& attr = attrs[x];
VERIFY_ARE_EQUAL(expectedFillAttr, attr);
}
}
void ScreenBufferTests::VtNewlinePastEndOfBuffer()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
// Make sure we're in VT mode
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition(COORD({ 0, 0 }));
std::wstring seq = L"\x1b[m";
stateMachine.ProcessString(seq);
seq = L"\x1b[2J";
stateMachine.ProcessString(seq);
const TextAttribute defaultAttrs{};
Log::Comment(L"Move the cursor to the bottom of the buffer");
for (auto i = 0; i < si.GetBufferSize().Height(); i++)
{
stateMachine.ProcessString(L"\n");
}
const auto initialViewport = si.GetViewport();
Log::Comment(NoThrowString().Format(
L"initialViewport=%s",
VerifyOutputTraits<SMALL_RECT>::ToString(initialViewport.ToInclusive()).GetBuffer()));
cursor.SetPosition(COORD({ 0, initialViewport.BottomInclusive() }));
// Set the attributes that will be used to initialize new rows.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(fillAttr);
// But note that the meta attributes are expected to be cleared.
auto expectedFillAttr = fillAttr;
expectedFillAttr.SetStandardErase();
seq = L"\n";
stateMachine.ProcessString(seq);
const auto viewport = si.GetViewport();
Log::Comment(NoThrowString().Format(
L"viewport=%s",
VerifyOutputTraits<SMALL_RECT>::ToString(viewport.ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().Y);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
for (int y = viewport.Top(); y < viewport.BottomInclusive(); y++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
const ROW& row = tbi.GetRowByOffset(y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
for (int x = 0; x < viewport.RightInclusive(); x++)
{
const auto& attr = attrs[x];
VERIFY_ARE_EQUAL(defaultAttrs, attr);
}
}
const ROW& row = tbi.GetRowByOffset(viewport.BottomInclusive());
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
for (int x = 0; x < viewport.RightInclusive(); x++)
{
const auto& attr = attrs[x];
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-11 00:14:40 +01:00
VERIFY_ARE_EQUAL(expectedFillAttr, attr);
}
}
void ScreenBufferTests::VtNewlineOutsideMargins()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
const auto viewportTop = si.GetViewport().Top();
const auto viewportBottom = si.GetViewport().BottomInclusive();
// Make sure the bottom margin will fit inside the viewport.
VERIFY_IS_TRUE(si.GetViewport().Height() > 5);
Log::Comment(L"LF at bottom of viewport scrolls the viewport");
cursor.SetPosition({ 0, viewportBottom });
stateMachine.ProcessString(L"\n");
VERIFY_ARE_EQUAL(COORD({ 0, viewportBottom + 1 }), cursor.GetPosition());
VERIFY_ARE_EQUAL(COORD({ 0, viewportTop + 1 }), si.GetViewport().Origin());
Log::Comment(L"Reset viewport and apply DECSTBM margins");
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, viewportTop }), true));
stateMachine.ProcessString(L"\x1b[1;5r");
// Make sure we clear the margins on exit so they can't break other tests.
auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); });
Log::Comment(L"LF no longer scrolls the viewport when below bottom margin");
cursor.SetPosition({ 0, viewportBottom });
stateMachine.ProcessString(L"\n");
VERIFY_ARE_EQUAL(COORD({ 0, viewportBottom }), cursor.GetPosition());
VERIFY_ARE_EQUAL(COORD({ 0, viewportTop }), si.GetViewport().Origin());
}
void ScreenBufferTests::VtSetColorTable()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
StateMachine& stateMachine = si.GetStateMachine();
// Start with a known value
gci.SetColorTableEntry(0, RGB(0, 0, 0));
Log::Comment(NoThrowString().Format(
L"Process some valid sequences for setting the table"));
std::wstring seq = L"\x1b]4;0;rgb:1/1/1\x7";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(1, 1, 1), gci.GetColorTableEntry(::XtermToWindowsIndex(0)));
seq = L"\x1b]4;1;rgb:1/23/1\x7";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(1, 0x23, 1), gci.GetColorTableEntry(::XtermToWindowsIndex(1)));
seq = L"\x1b]4;2;rgb:1/23/12\x7";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(1, 0x23, 0x12), gci.GetColorTableEntry(::XtermToWindowsIndex(2)));
seq = L"\x1b]4;3;rgb:12/23/12\x7";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(0x12, 0x23, 0x12), gci.GetColorTableEntry(::XtermToWindowsIndex(3)));
seq = L"\x1b]4;4;rgb:ff/a1/1b\x7";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(0xff, 0xa1, 0x1b), gci.GetColorTableEntry(::XtermToWindowsIndex(4)));
seq = L"\x1b]4;5;rgb:ff/a1/1b\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(0xff, 0xa1, 0x1b), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"Try a bunch of invalid sequences."));
Log::Comment(NoThrowString().Format(
L"First start by setting an entry to a known value to compare to."));
seq = L"\x1b]4;5;rgb:9/9/9\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: Missing the first component"));
seq = L"\x1b]4;5;rgb:/1/1\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: too many characters in a component"));
seq = L"\x1b]4;5;rgb:111/1/1\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: too many componenets"));
seq = L"\x1b]4;5;rgb:1/1/1/1\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: no second component"));
seq = L"\x1b]4;5;rgb:1//1\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: no components"));
seq = L"\x1b]4;5;rgb://\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: no third component"));
seq = L"\x1b]4;5;rgb:1/11/\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: rgbi is not a supported color space"));
seq = L"\x1b]4;5;rgbi:1/1/1\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: cmyk is not a supported color space"));
seq = L"\x1b]4;5;cmyk:1/1/1\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: no table index should do nothing"));
seq = L"\x1b]4;;rgb:1/1/1\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
Log::Comment(NoThrowString().Format(
L"invalid: need to specify a color space"));
seq = L"\x1b]4;5;1/1/1\x1b\\";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(RGB(9, 9, 9), gci.GetColorTableEntry(::XtermToWindowsIndex(5)));
}
void ScreenBufferTests::ResizeTraditionalDoesntDoubleFreeAttrRows()
{
// there is not much to verify here, this test passes if the console doesn't crash.
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
gci.SetWrapText(false);
COORD newBufferSize = si.GetBufferSize().Dimensions();
newBufferSize.Y--;
VERIFY_SUCCEEDED(si.ResizeTraditional(newBufferSize));
}
void ScreenBufferTests::ResizeCursorUnchanged()
{
// Created for MSFT:19863799. Make sure whewn we resize the buffer, the
// cursor looks the same as it did before.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:useResizeWithReflow", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 0, 1, 10}")
TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}")
END_TEST_METHOD_PROPERTIES();
bool useResizeWithReflow;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"useResizeWithReflow", useResizeWithReflow), L"Use ResizeWithReflow or not");
int dx, dy;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer");
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer");
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const auto& initialCursor = si.GetTextBuffer().GetCursor();
// Get initial cursor values
const CursorType initialType = initialCursor.GetType();
const auto initialSize = initialCursor.GetSize();
const COLORREF initialColor = initialCursor.GetColor();
// set our wrap mode accordingly - ResizeScreenBuffer will be smart enough
// to call the appropriate implementation
gci.SetWrapText(useResizeWithReflow);
COORD newBufferSize = si.GetBufferSize().Dimensions();
newBufferSize.X += static_cast<short>(dx);
newBufferSize.Y += static_cast<short>(dy);
VERIFY_SUCCEEDED(si.ResizeScreenBuffer(newBufferSize, false));
const auto& finalCursor = si.GetTextBuffer().GetCursor();
const CursorType finalType = finalCursor.GetType();
const auto finalSize = finalCursor.GetSize();
const COLORREF finalColor = finalCursor.GetColor();
VERIFY_ARE_EQUAL(initialType, finalType);
VERIFY_ARE_EQUAL(initialColor, finalColor);
VERIFY_ARE_EQUAL(initialSize, finalSize);
}
void ScreenBufferTests::ResizeAltBuffer()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Log::Comment(NoThrowString().Format(
L"Try resizing the alt buffer. Make sure the call doesn't stack overflow."));
VERIFY_IS_FALSE(si._IsAltBuffer());
const Viewport originalMainSize = Viewport(si._viewport);
Log::Comment(NoThrowString().Format(
L"Switch to alt buffer"));
std::wstring seq = L"\x1b[?1049h";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_IS_FALSE(si._IsAltBuffer());
VERIFY_IS_NOT_NULL(si._psiAlternateBuffer);
SCREEN_INFORMATION* const psiAlt = si._psiAlternateBuffer;
COORD newSize = originalMainSize.Dimensions();
newSize.X += 2;
newSize.Y += 2;
Log::Comment(NoThrowString().Format(
L"MSFT:15917333 This call shouldn't stack overflow"));
psiAlt->SetViewportSize(&newSize);
VERIFY_IS_TRUE(true);
Log::Comment(NoThrowString().Format(
L"Switch back from buffer"));
seq = L"\x1b[?1049l";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_IS_FALSE(si._IsAltBuffer());
VERIFY_IS_NULL(si._psiAlternateBuffer);
}
void ScreenBufferTests::ResizeAltBufferGetScreenBufferInfo()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 1, 10}")
TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 1, 10}")
END_TEST_METHOD_PROPERTIES();
int dx, dy;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer");
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer");
// Tests MSFT:19918103
Log::Comment(NoThrowString().Format(
L"Switch to the alt buffer, then resize the buffer. "
L"GetConsoleScreenBufferInfoEx(mainBuffer) should return the alt "
L"buffer's size, not the main buffer's size."));
auto& g = ServiceLocator::LocateGlobals();
CONSOLE_INFORMATION& gci = g.getConsoleInformation();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer().GetActiveBuffer();
StateMachine& stateMachine = mainBuffer.GetStateMachine();
VERIFY_IS_FALSE(mainBuffer._IsAltBuffer());
const Viewport originalMainSize = Viewport(mainBuffer._viewport);
Log::Comment(NoThrowString().Format(
L"Switch to alt buffer"));
std::wstring seq = L"\x1b[?1049h";
stateMachine.ProcessString(seq);
VERIFY_IS_FALSE(mainBuffer._IsAltBuffer());
VERIFY_IS_NOT_NULL(mainBuffer._psiAlternateBuffer);
auto& altBuffer = *(mainBuffer._psiAlternateBuffer);
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
COORD newBufferSize = originalMainSize.Dimensions();
newBufferSize.X += static_cast<short>(dx);
newBufferSize.Y += static_cast<short>(dy);
const Viewport originalAltSize = Viewport(altBuffer._viewport);
VERIFY_ARE_EQUAL(originalMainSize.Width(), originalAltSize.Width());
VERIFY_ARE_EQUAL(originalMainSize.Height(), originalAltSize.Height());
altBuffer.SetViewportSize(&newBufferSize);
CONSOLE_SCREEN_BUFFER_INFOEX csbiex{ 0 };
g.api.GetConsoleScreenBufferInfoExImpl(mainBuffer, csbiex);
const auto newActualMainView = mainBuffer.GetViewport();
const auto newActualAltView = altBuffer.GetViewport();
const auto newApiViewport = Viewport::FromExclusive(csbiex.srWindow);
VERIFY_ARE_NOT_EQUAL(originalAltSize.Width(), newActualAltView.Width());
VERIFY_ARE_NOT_EQUAL(originalAltSize.Height(), newActualAltView.Height());
VERIFY_ARE_NOT_EQUAL(originalMainSize.Width(), newActualAltView.Width());
VERIFY_ARE_NOT_EQUAL(originalMainSize.Height(), newActualAltView.Height());
VERIFY_ARE_EQUAL(newActualAltView.Width(), newApiViewport.Width());
VERIFY_ARE_EQUAL(newActualAltView.Height(), newApiViewport.Height());
}
void ScreenBufferTests::VtEraseAllPersistCursor()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
const Cursor& cursor = tbi.GetCursor();
Log::Comment(NoThrowString().Format(
L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
Log::Comment(NoThrowString().Format(
L"Move the cursor to 2,2, then execute a Erase All.\n"
L"The cursor should not move relative to the viewport."));
std::wstring seq = L"\x1b[2;2H";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(COORD({ 1, 1 }), cursor.GetPosition());
seq = L"\x1b[2J";
stateMachine.ProcessString(&seq[0], seq.length());
auto newViewport = si._viewport;
COORD expectedCursor = { 1, 1 };
newViewport.ConvertFromOrigin(&expectedCursor);
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition());
}
void ScreenBufferTests::VtEraseAllPersistCursorFillColor()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Log::Comment(NoThrowString().Format(
L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
Log::Comment(NoThrowString().Format(
L"Change the colors to dark_red on bright_blue, then execute a Erase All.\n"
L"The viewport should be full of dark_red on bright_blue"));
auto expectedAttr = TextAttribute(XtermToLegacy(1, 12));
std::wstring seq = L"\x1b[31;104m";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes());
seq = L"\x1b[2J";
stateMachine.ProcessString(&seq[0], seq.length());
VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes());
auto newViewport = si._viewport;
Log::Comment(NoThrowString().Format(
L"new Viewport: %s",
VerifyOutputTraits<SMALL_RECT>::ToString(newViewport.ToInclusive()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"Buffer Size: %s",
VerifyOutputTraits<SMALL_RECT>::ToString(si.GetBufferSize().ToInclusive()).GetBuffer()));
auto iter = tbi.GetCellDataAt(newViewport.Origin());
auto height = newViewport.Height();
auto width = newViewport.Width();
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
VERIFY_ARE_EQUAL(expectedAttr, iter->TextAttr());
iter++;
}
}
}
void ScreenBufferTests::GetWordBoundary()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const auto text = L"This is some test text for word boundaries.";
const auto length = wcslen(text);
// Make the buffer as big as our test text.
const COORD newBufferSize = { gsl::narrow<SHORT>(length), 10 };
VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize));
const OutputCellIterator it(text, si.GetAttributes());
si.Write(it, { 0, 0 });
// Now find some words in it.
Log::Comment(L"Find first word from its front.");
COORD expectedFirst = { 0, 0 };
COORD expectedSecond = { 4, 0 };
auto boundary = si.GetWordBoundary({ 0, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find first word from its middle.");
boundary = si.GetWordBoundary({ 1, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find first word from its end.");
boundary = si.GetWordBoundary({ 3, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find middle word from its front.");
expectedFirst = { 13, 0 };
expectedSecond = { 17, 0 };
boundary = si.GetWordBoundary({ 13, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find middle word from its middle.");
boundary = si.GetWordBoundary({ 15, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find middle word from its end.");
boundary = si.GetWordBoundary({ 16, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find end word from its front.");
expectedFirst = { 32, 0 };
expectedSecond = { 43, 0 };
boundary = si.GetWordBoundary({ 32, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find end word from its middle.");
boundary = si.GetWordBoundary({ 39, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find end word from its end.");
boundary = si.GetWordBoundary({ 43, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find a word starting from a boundary character.");
expectedFirst = { 8, 0 };
expectedSecond = { 12, 0 };
boundary = si.GetWordBoundary({ 12, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
}
void ScreenBufferTests::GetWordBoundaryTrimZeros(const bool on)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const auto text = L"000fe12 0xfe12 0Xfe12 0nfe12 0Nfe12";
const auto length = wcslen(text);
// Make the buffer as big as our test text.
const COORD newBufferSize = { gsl::narrow<SHORT>(length), 10 };
VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize));
const OutputCellIterator it(text, si.GetAttributes());
si.Write(it, { 0, 0 });
gci.SetTrimLeadingZeros(on);
COORD expectedFirst;
COORD expectedSecond;
std::pair<COORD, COORD> boundary;
Log::Comment(L"Find lead with 000");
expectedFirst = on ? COORD{ 3, 0 } : COORD{ 0, 0 };
expectedSecond = COORD{ 7, 0 };
boundary = si.GetWordBoundary({ 0, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find lead with 0x");
expectedFirst = COORD{ 8, 0 };
expectedSecond = COORD{ 14, 0 };
boundary = si.GetWordBoundary({ 8, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find lead with 0X");
expectedFirst = COORD{ 15, 0 };
expectedSecond = COORD{ 21, 0 };
boundary = si.GetWordBoundary({ 15, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find lead with 0n");
expectedFirst = COORD{ 22, 0 };
expectedSecond = COORD{ 28, 0 };
boundary = si.GetWordBoundary({ 22, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
Log::Comment(L"Find lead with 0N");
expectedFirst = on ? COORD{ 30, 0 } : COORD{ 29, 0 };
expectedSecond = COORD{ 35, 0 };
boundary = si.GetWordBoundary({ 29, 0 });
VERIFY_ARE_EQUAL(expectedFirst, boundary.first);
VERIFY_ARE_EQUAL(expectedSecond, boundary.second);
}
void ScreenBufferTests::GetWordBoundaryTrimZerosOn()
{
GetWordBoundaryTrimZeros(true);
}
void ScreenBufferTests::GetWordBoundaryTrimZerosOff()
{
GetWordBoundaryTrimZeros(false);
}
void ScreenBufferTests::TestAltBufferCursorState()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
Log::Comment(L"Creating one alternate buffer");
auto& original = gci.GetActiveOutputBuffer();
VERIFY_IS_NULL(original._psiAlternateBuffer);
VERIFY_IS_NULL(original._psiMainBuffer);
NTSTATUS Status = original.UseAlternateScreenBuffer();
if (VERIFY_IS_TRUE(NT_SUCCESS(Status)))
{
Log::Comment(L"Alternate buffer successfully created");
auto& alternate = gci.GetActiveOutputBuffer();
// Make sure that when the test is done, we switch back to the main buffer.
// Otherwise, one test could pollute another.
auto useMain = wil::scope_exit([&] { alternate.UseMainScreenBuffer(); });
const auto* pMain = &original;
const auto* pAlt = &alternate;
// Validate that the pointers were mapped appropriately to link
// alternate and main buffers
VERIFY_ARE_NOT_EQUAL(pMain, pAlt);
VERIFY_ARE_EQUAL(pAlt, original._psiAlternateBuffer);
VERIFY_ARE_EQUAL(pMain, alternate._psiMainBuffer);
VERIFY_IS_NULL(original._psiMainBuffer);
VERIFY_IS_NULL(alternate._psiAlternateBuffer);
auto& mainCursor = original.GetTextBuffer().GetCursor();
auto& altCursor = alternate.GetTextBuffer().GetCursor();
// Validate that the cursor state was copied appropriately into the
// alternate buffer
VERIFY_ARE_EQUAL(mainCursor.GetSize(), altCursor.GetSize());
VERIFY_ARE_EQUAL(mainCursor.GetColor(), altCursor.GetColor());
VERIFY_ARE_EQUAL(mainCursor.GetType(), altCursor.GetType());
}
}
void ScreenBufferTests::TestAltBufferVtDispatching()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
Log::Comment(L"Creating one alternate buffer");
auto& mainBuffer = gci.GetActiveOutputBuffer();
// Make sure we're in VT mode
WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
// Make sure we're suing the default attributes at the start of the test,
// Otherwise they could be polluted from a previous test.
mainBuffer.SetAttributes(gci.GetDefaultAttributes());
VERIFY_IS_NULL(mainBuffer._psiAlternateBuffer);
VERIFY_IS_NULL(mainBuffer._psiMainBuffer);
NTSTATUS Status = mainBuffer.UseAlternateScreenBuffer();
if (VERIFY_IS_TRUE(NT_SUCCESS(Status)))
{
Log::Comment(L"Alternate buffer successfully created");
auto& alternate = gci.GetActiveOutputBuffer();
// Make sure that when the test is done, we switch back to the main buffer.
// Otherwise, one test could pollute another.
auto useMain = wil::scope_exit([&] { alternate.UseMainScreenBuffer(); });
// Manually turn on VT mode - usually gci enables this for you.
WI_SetFlag(alternate.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
const auto* pMain = &mainBuffer;
const auto* pAlt = &alternate;
// Validate that the pointers were mapped appropriately to link
// alternate and main buffers
VERIFY_ARE_NOT_EQUAL(pMain, pAlt);
VERIFY_ARE_EQUAL(pAlt, mainBuffer._psiAlternateBuffer);
VERIFY_ARE_EQUAL(pMain, alternate._psiMainBuffer);
VERIFY_IS_NULL(mainBuffer._psiMainBuffer);
VERIFY_IS_NULL(alternate._psiAlternateBuffer);
auto& mainCursor = mainBuffer.GetTextBuffer().GetCursor();
auto& altCursor = alternate.GetTextBuffer().GetCursor();
const COORD origin = { 0, 0 };
mainCursor.SetPosition(origin);
altCursor.SetPosition(origin);
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, origin, true));
VERIFY_SUCCEEDED(alternate.SetViewportOrigin(true, origin, true));
VERIFY_ARE_EQUAL(origin, mainCursor.GetPosition());
VERIFY_ARE_EQUAL(origin, altCursor.GetPosition());
// We're going to write some data to either the main buffer or the alt
// buffer, as if we were using the API.
std::unique_ptr<WriteData> waiter;
std::wstring seq = L"\x1b[5;6H";
size_t seqCb = 2 * seq.size();
VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter));
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition());
// recall: vt coordinates are (row, column), 1-indexed
VERIFY_ARE_EQUAL(COORD({ 5, 4 }), altCursor.GetPosition());
const TextAttribute expectedDefaults = gci.GetDefaultAttributes();
TextAttribute expectedRgb = expectedDefaults;
expectedRgb.SetBackground(RGB(255, 0, 255));
VERIFY_ARE_EQUAL(expectedDefaults, mainBuffer.GetAttributes());
VERIFY_ARE_EQUAL(expectedDefaults, alternate.GetAttributes());
seq = L"\x1b[48;2;255;0;255m";
seqCb = 2 * seq.size();
VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter));
VERIFY_ARE_EQUAL(expectedDefaults, mainBuffer.GetAttributes());
VERIFY_ARE_EQUAL(expectedRgb, alternate.GetAttributes());
seq = L"X";
seqCb = 2 * seq.size();
VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter));
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition());
VERIFY_ARE_EQUAL(COORD({ 6, 4 }), altCursor.GetPosition());
// Recall we didn't print an 'X' to the main buffer, so there's no
// char to inspect the attributes of.
const ROW& altRow = alternate.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y);
const auto altAttrRow = &altRow.GetAttrRow();
const std::vector<TextAttribute> altAttrs{ altAttrRow->begin(), altAttrRow->end() };
const auto altAttrA = altAttrs[altCursor.GetPosition().X - 1];
VERIFY_ARE_EQUAL(expectedRgb, altAttrA);
}
}
void ScreenBufferTests::SetDefaultsIndividuallyBothDefault()
{
// Tests MSFT:19828103
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition({ 0, 0 });
COLORREF magenta = RGB(255, 0, 255);
COLORREF yellow = RGB(255, 255, 0);
COLORREF brightGreen = gci.GetColorTableEntry(::XtermToWindowsIndex(10));
COLORREF darkBlue = gci.GetColorTableEntry(::XtermToWindowsIndex(4));
gci.SetDefaultForegroundColor(yellow);
gci.SetDefaultBackgroundColor(magenta);
si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() });
Log::Comment(NoThrowString().Format(L"Write 6 X's:"));
Log::Comment(NoThrowString().Format(L" The first in default-fg on default-bg (yellow on magenta)"));
Log::Comment(NoThrowString().Format(L" The second with bright-green on dark-blue"));
Log::Comment(NoThrowString().Format(L" The third with default-fg on dark-blue"));
Log::Comment(NoThrowString().Format(L" The fourth in default-fg on default-bg (yellow on magenta)"));
Log::Comment(NoThrowString().Format(L" The fifth with bright-green on dark-blue"));
Log::Comment(NoThrowString().Format(L" The sixth with bright-green on default-bg"));
std::wstring seq = L"\x1b[m"; // Reset to defaults
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[92;44m"; // bright-green on dark-blue
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[39m"; // reset fg
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[49m"; // reset bg
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[92;44m"; // bright-green on dark-blue
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[49m"; // reset bg
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
// See the log comment above for description of these values.
TextAttribute expectedDefaults{};
TextAttribute expectedTwo{ FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE };
TextAttribute expectedThree{ FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE };
expectedThree.SetDefaultForeground();
// Four is the same as Defaults
// Five is the same as two
TextAttribute expectedSix{ FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE };
expectedSix.SetDefaultBackground();
COORD expectedCursor{ 6, 0 };
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition());
const ROW& row = tbi.GetRowByOffset(0);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
const auto attrC = attrs[2];
const auto attrD = attrs[3];
const auto attrE = attrs[4];
const auto attrF = attrs[5];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
LOG_ATTR(attrC);
LOG_ATTR(attrD);
LOG_ATTR(attrE);
LOG_ATTR(attrF);
VERIFY_ARE_EQUAL(false, attrA.IsLegacy());
VERIFY_ARE_EQUAL(true, attrB.IsLegacy());
VERIFY_ARE_EQUAL(false, attrC.IsLegacy());
VERIFY_ARE_EQUAL(false, attrD.IsLegacy());
VERIFY_ARE_EQUAL(true, attrE.IsLegacy());
VERIFY_ARE_EQUAL(false, attrF.IsLegacy());
VERIFY_ARE_EQUAL(expectedDefaults, attrA);
VERIFY_ARE_EQUAL(expectedTwo, attrB);
VERIFY_ARE_EQUAL(expectedThree, attrC);
VERIFY_ARE_EQUAL(expectedDefaults, attrD);
VERIFY_ARE_EQUAL(expectedTwo, attrE);
VERIFY_ARE_EQUAL(expectedSix, attrF);
VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrA));
VERIFY_ARE_EQUAL(brightGreen, gci.LookupForegroundColor(attrB));
VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrC));
VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrD));
VERIFY_ARE_EQUAL(brightGreen, gci.LookupForegroundColor(attrE));
VERIFY_ARE_EQUAL(brightGreen, gci.LookupForegroundColor(attrF));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrB));
VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrC));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrD));
VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrE));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrF));
}
void ScreenBufferTests::SetDefaultsTogether()
{
// Tests MSFT:19828103
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition({ 0, 0 });
COLORREF magenta = RGB(255, 0, 255);
COLORREF yellow = RGB(255, 255, 0);
COLORREF color250 = gci.GetColorTableEntry(250);
gci.SetDefaultForegroundColor(yellow);
gci.SetDefaultBackgroundColor(magenta);
si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() });
Log::Comment(NoThrowString().Format(L"Write 6 X's:"));
Log::Comment(NoThrowString().Format(L" The first in default-fg on default-bg (yellow on magenta)"));
Log::Comment(NoThrowString().Format(L" The second with default-fg on xterm(250)"));
Log::Comment(NoThrowString().Format(L" The third with defaults again"));
std::wstring seq = L"\x1b[m"; // Reset to defaults
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[48;5;250m"; // bright-green on dark-blue
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[39;49m"; // reset fg
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
// See the log comment above for description of these values.
TextAttribute expectedDefaults{};
TextAttribute expectedTwo{};
expectedTwo.SetBackground(color250);
COORD expectedCursor{ 3, 0 };
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition());
const ROW& row = tbi.GetRowByOffset(0);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
const auto attrC = attrs[2];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
LOG_ATTR(attrC);
VERIFY_ARE_EQUAL(false, attrA.IsLegacy());
VERIFY_ARE_EQUAL(false, attrB.IsLegacy());
VERIFY_ARE_EQUAL(false, attrC.IsLegacy());
VERIFY_ARE_EQUAL(expectedDefaults, attrA);
VERIFY_ARE_EQUAL(expectedTwo, attrB);
VERIFY_ARE_EQUAL(expectedDefaults, attrC);
VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrA));
VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrB));
VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrC));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(color250, gci.LookupBackgroundColor(attrB));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrC));
}
void ScreenBufferTests::ReverseResetWithDefaultBackground()
{
// Tests MSFT:19694089
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition({ 0, 0 });
COLORREF magenta = RGB(255, 0, 255);
gci.SetDefaultForegroundColor(INVALID_COLOR);
gci.SetDefaultBackgroundColor(magenta);
si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() });
Log::Comment(NoThrowString().Format(L"Write 3 X's:"));
Log::Comment(NoThrowString().Format(L" The first in default-attr on default color (magenta)"));
Log::Comment(NoThrowString().Format(L" The second with reversed attrs"));
Log::Comment(NoThrowString().Format(L" The third after resetting the attrs back"));
std::wstring seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[7m";
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
seq = L"\x1b[27m";
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
TextAttribute expectedDefaults{ gci.GetFillAttribute() };
expectedDefaults.SetDefaultBackground();
TextAttribute expectedReversed = expectedDefaults;
expectedReversed.Invert();
COORD expectedCursor{ 3, 0 };
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition());
const ROW& row = tbi.GetRowByOffset(0);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
const auto attrC = attrs[2];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
LOG_ATTR(attrC);
VERIFY_ARE_EQUAL(false, attrA.IsLegacy());
VERIFY_ARE_EQUAL(false, attrB.IsLegacy());
VERIFY_ARE_EQUAL(false, attrC.IsLegacy());
VERIFY_ARE_EQUAL(false, WI_IsFlagSet(attrA.GetMetaAttributes(), COMMON_LVB_REVERSE_VIDEO));
VERIFY_ARE_EQUAL(true, WI_IsFlagSet(attrB.GetMetaAttributes(), COMMON_LVB_REVERSE_VIDEO));
VERIFY_ARE_EQUAL(false, WI_IsFlagSet(attrC.GetMetaAttributes(), COMMON_LVB_REVERSE_VIDEO));
VERIFY_ARE_EQUAL(expectedDefaults, attrA);
VERIFY_ARE_EQUAL(expectedReversed, attrB);
VERIFY_ARE_EQUAL(expectedDefaults, attrC);
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(magenta, gci.LookupForegroundColor(attrB));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrC));
}
void ScreenBufferTests::BackspaceDefaultAttrs()
{
// Created for MSFT:19735050, but doesn't actually test that.
// That bug actually involves the input line, and that needs to use
// TextAttributes instead of WORDs
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition({ 0, 0 });
COLORREF magenta = RGB(255, 0, 255);
gci.SetDefaultBackgroundColor(magenta);
si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() });
Log::Comment(NoThrowString().Format(L"Write 2 X's, then backspace one."));
std::wstring seq = L"\x1b[m";
stateMachine.ProcessString(seq);
seq = L"XX";
stateMachine.ProcessString(seq);
seq = UNICODE_BACKSPACE;
stateMachine.ProcessString(seq);
TextAttribute expectedDefaults{};
expectedDefaults.SetDefaultBackground();
COORD expectedCursor{ 1, 0 };
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition());
const ROW& row = tbi.GetRowByOffset(0);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
VERIFY_ARE_EQUAL(false, attrA.IsLegacy());
VERIFY_ARE_EQUAL(false, attrB.IsLegacy());
VERIFY_ARE_EQUAL(expectedDefaults, attrA);
VERIFY_ARE_EQUAL(expectedDefaults, attrB);
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrB));
}
void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:writeSingly", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:writeCharsLegacyMode", L"{0, 1, 2, 3, 4, 5, 6, 7}")
END_TEST_METHOD_PROPERTIES();
bool writeSingly;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeSingly", writeSingly), L"Write one at a time = true, all at the same time = false");
DWORD writeCharsLegacyMode;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeCharsLegacyMode", writeCharsLegacyMode), L"");
// Created for MSFT:19735050.
// Kinda the same as above, but with WriteCharsLegacy instead.
// The variable that really breaks this scenario
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition({ 0, 0 });
COLORREF magenta = RGB(255, 0, 255);
gci.SetDefaultBackgroundColor(magenta);
si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() });
Log::Comment(NoThrowString().Format(L"Write 2 X's, then backspace one."));
std::wstring seq = L"\x1b[m";
stateMachine.ProcessString(seq);
if (writeSingly)
{
wchar_t* str = L"X";
size_t seqCb = 2;
VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr));
VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr));
str = L"\x08";
VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr));
}
else
{
wchar_t* str = L"XX\x08";
size_t seqCb = 6;
VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr));
}
TextAttribute expectedDefaults{};
expectedDefaults.SetDefaultBackground();
COORD expectedCursor{ 1, 0 };
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition());
const ROW& row = tbi.GetRowByOffset(0);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
VERIFY_ARE_EQUAL(false, attrA.IsLegacy());
VERIFY_ARE_EQUAL(false, attrB.IsLegacy());
VERIFY_ARE_EQUAL(expectedDefaults, attrA);
VERIFY_ARE_EQUAL(expectedDefaults, attrB);
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrB));
}
void ScreenBufferTests::BackspaceDefaultAttrsInPrompt()
{
// Tests MSFT:19853701 - when you edit the prompt line at a bash prompt,
// make sure that the end of the line isn't filled with default/garbage attributes.
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
const TextBuffer& tbi = si.GetTextBuffer();
StateMachine& stateMachine = si.GetStateMachine();
Cursor& cursor = si.GetTextBuffer().GetCursor();
// Make sure we're in VT mode
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
cursor.SetPosition({ 0, 0 });
COLORREF magenta = RGB(255, 0, 255);
gci.SetDefaultBackgroundColor(magenta);
si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() });
TextAttribute expectedDefaults{};
Log::Comment(NoThrowString().Format(L"Write 3 X's, move to the left, then delete-char the second."));
Log::Comment(NoThrowString().Format(L"This emulates editing the prompt line on bash"));
std::wstring seq = L"\x1b[m";
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"Clear the screen - make sure the line is filled with the current attributes."));
seq = L"\x1b[2J";
stateMachine.ProcessString(seq);
const auto viewport = si.GetViewport();
const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
Log::Comment(NoThrowString().Format(
L"Make sure the row contains what we're expecting before we start."
L"It should entirely be filled with defaults"));
const std::vector<TextAttribute> initialAttrs{ attrRow->begin(), attrRow->end() };
for (int x = 0; x <= viewport.RightInclusive(); x++)
{
const auto& attr = initialAttrs[x];
VERIFY_ARE_EQUAL(expectedDefaults, attr);
}
}
Log::Comment(NoThrowString().Format(
L"Print 'XXX', move the cursor left 2, delete a character."));
seq = L"XXX";
stateMachine.ProcessString(seq);
seq = L"\x1b[2D";
stateMachine.ProcessString(seq);
seq = L"\x1b[P";
stateMachine.ProcessString(seq);
COORD expectedCursor{ 1, 1 }; // We're expecting y=1, because the 2J above
// should have moved the viewport down a line.
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition());
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
for (int x = 0; x <= viewport.RightInclusive(); x++)
{
const auto& attr = attrs[x];
VERIFY_ARE_EQUAL(expectedDefaults, attr);
}
}
void ScreenBufferTests::SetGlobalColorTable()
{
// Created for MSFT:19723934.
// Changing the value of the color table should apply to the attributes in
// both the alt AND main buffer. While many other properties should be
// reset upon returning to the main buffer, the color table is a
// global property. This behavior is consistent with other terminals
// tested.
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to swap buffers.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer();
VERIFY_IS_FALSE(mainBuffer._IsAltBuffer());
WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
StateMachine& stateMachine = mainBuffer.GetStateMachine();
Cursor& mainCursor = mainBuffer.GetTextBuffer().GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, COORD({ 0, 0 }), true));
mainCursor.SetPosition({ 0, 0 });
const COLORREF originalRed = gci.GetColorTableEntry(4);
const COLORREF testColor = RGB(0x11, 0x22, 0x33);
VERIFY_ARE_NOT_EQUAL(originalRed, testColor);
std::wstring seq = L"\x1b[41m";
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
COORD expectedCursor{ 1, 0 };
VERIFY_ARE_EQUAL(expectedCursor, mainCursor.GetPosition());
{
const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
LOG_ATTR(attrA);
VERIFY_ARE_EQUAL(originalRed, gci.LookupBackgroundColor(attrA));
}
Log::Comment(NoThrowString().Format(L"Create an alt buffer"));
VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer();
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
Cursor& altCursor = altBuffer.GetTextBuffer().GetCursor();
altCursor.SetPosition({ 0, 0 });
Log::Comment(NoThrowString().Format(
L"Print one X in red, should be the original red color"));
seq = L"\x1b[41m";
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
VERIFY_ARE_EQUAL(expectedCursor, altCursor.GetPosition());
{
const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
LOG_ATTR(attrA);
VERIFY_ARE_EQUAL(originalRed, gci.LookupBackgroundColor(attrA));
}
Log::Comment(NoThrowString().Format(L"Change the value of red to RGB(0x11, 0x22, 0x33)"));
seq = L"\x1b]4;1;rgb:11/22/33\x07";
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"Print another X, both should be the new \"red\" color"));
seq = L"X";
stateMachine.ProcessString(seq);
VERIFY_ARE_EQUAL(COORD({ 2, 0 }), altCursor.GetPosition());
{
const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrB));
}
Log::Comment(NoThrowString().Format(L"Switch back to the main buffer"));
useMain.release();
altBuffer.UseMainScreenBuffer();
const auto& mainBufferPostSwitch = gci.GetActiveOutputBuffer();
VERIFY_ARE_EQUAL(&mainBufferPostSwitch, &mainBuffer);
Log::Comment(NoThrowString().Format(
L"Print another X, both should be the new \"red\" color"));
seq = L"X";
stateMachine.ProcessString(seq);
VERIFY_ARE_EQUAL(COORD({ 2, 0 }), mainCursor.GetPosition());
{
const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrB));
}
}
void ScreenBufferTests::SetColorTableThreeDigits()
{
// Created for MSFT:19723934.
// Changing the value of the color table above index 99 should work
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to swap buffers.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer();
VERIFY_IS_FALSE(mainBuffer._IsAltBuffer());
WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
StateMachine& stateMachine = mainBuffer.GetStateMachine();
Cursor& mainCursor = mainBuffer.GetTextBuffer().GetCursor();
Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0"));
VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, COORD({ 0, 0 }), true));
mainCursor.SetPosition({ 0, 0 });
const COLORREF originalRed = gci.GetColorTableEntry(123);
const COLORREF testColor = RGB(0x11, 0x22, 0x33);
VERIFY_ARE_NOT_EQUAL(originalRed, testColor);
std::wstring seq = L"\x1b[48;5;123m";
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
COORD expectedCursor{ 1, 0 };
VERIFY_ARE_EQUAL(expectedCursor, mainCursor.GetPosition());
{
const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
LOG_ATTR(attrA);
VERIFY_ARE_EQUAL(originalRed, gci.LookupBackgroundColor(attrA));
}
Log::Comment(NoThrowString().Format(L"Create an alt buffer"));
VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer();
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
Cursor& altCursor = altBuffer.GetTextBuffer().GetCursor();
altCursor.SetPosition({ 0, 0 });
Log::Comment(NoThrowString().Format(
L"Print one X in red, should be the original red color"));
seq = L"\x1b[48;5;123m";
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
VERIFY_ARE_EQUAL(expectedCursor, altCursor.GetPosition());
{
const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
LOG_ATTR(attrA);
VERIFY_ARE_EQUAL(originalRed, gci.LookupBackgroundColor(attrA));
}
Log::Comment(NoThrowString().Format(L"Change the value of red to RGB(0x11, 0x22, 0x33)"));
seq = L"\x1b]4;123;rgb:11/22/33\x07";
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"Print another X, it should be the new \"red\" color"));
// TODO MSFT:20105972 -
// You shouldn't need to manually update the attributes again.
seq = L"\x1b[48;5;123m";
stateMachine.ProcessString(seq);
seq = L"X";
stateMachine.ProcessString(seq);
VERIFY_ARE_EQUAL(COORD({ 2, 0 }), altCursor.GetPosition());
{
const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrB = attrs[1];
// TODO MSFT:20105972 - attrA and attrB should both be the same color now
LOG_ATTR(attrB);
VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrB));
}
}
void ScreenBufferTests::SetDefaultForegroundColor()
{
// Setting the default foreground color should work
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to swap buffers.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer();
VERIFY_IS_FALSE(mainBuffer._IsAltBuffer());
WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
StateMachine& stateMachine = mainBuffer.GetStateMachine();
COLORREF originalColor = gci.GetDefaultForegroundColor();
COLORREF newColor = gci.GetDefaultForegroundColor();
COLORREF testColor = RGB(0x33, 0x66, 0x99);
VERIFY_ARE_NOT_EQUAL(originalColor, testColor);
Log::Comment(L"Valid Hexadecimal Notation");
std::wstring seq = L"\x1b]10;rgb:33/66/99\x1b\\";
stateMachine.ProcessString(seq);
newColor = gci.GetDefaultForegroundColor();
VERIFY_ARE_EQUAL(testColor, newColor);
Log::Comment(L"Valid Hexadecimal Notation");
originalColor = newColor;
testColor = RGB(0xff, 0xff, 0xff);
seq = L"\x1b]10;rgb:ff/ff/ff\x1b\\";
stateMachine.ProcessString(seq);
newColor = gci.GetDefaultForegroundColor();
VERIFY_ARE_EQUAL(testColor, newColor);
Log::Comment(L"Invalid Decimal Notation");
originalColor = newColor;
testColor = RGB(153, 102, 51);
seq = L"\x1b]10;rgb:153/102/51\x1b\\";
stateMachine.ProcessString(seq);
newColor = gci.GetDefaultForegroundColor();
VERIFY_ARE_NOT_EQUAL(testColor, newColor);
// it will, in fact leave the color the way it was
VERIFY_ARE_EQUAL(originalColor, newColor);
Log::Comment(L"Invalid syntax");
testColor = RGB(153, 102, 51);
seq = L"\x1b]10;99/66/33\x1b\\";
stateMachine.ProcessString(seq);
newColor = gci.GetDefaultForegroundColor();
VERIFY_ARE_NOT_EQUAL(testColor, newColor);
// it will, in fact leave the color the way it was
VERIFY_ARE_EQUAL(originalColor, newColor);
}
void ScreenBufferTests::SetDefaultBackgroundColor()
{
// Setting the default Background color should work
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to swap buffers.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer();
VERIFY_IS_FALSE(mainBuffer._IsAltBuffer());
WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING));
StateMachine& stateMachine = mainBuffer.GetStateMachine();
COLORREF originalColor = gci.GetDefaultBackgroundColor();
COLORREF newColor = gci.GetDefaultBackgroundColor();
COLORREF testColor = RGB(0x33, 0x66, 0x99);
VERIFY_ARE_NOT_EQUAL(originalColor, testColor);
Log::Comment(L"Valid Hexadecimal Notation");
std::wstring seq = L"\x1b]11;rgb:33/66/99\x1b\\";
stateMachine.ProcessString(seq);
newColor = gci.GetDefaultBackgroundColor();
VERIFY_ARE_EQUAL(testColor, newColor);
Log::Comment(L"Valid Hexadecimal Notation");
originalColor = newColor;
testColor = RGB(0xff, 0xff, 0xff);
seq = L"\x1b]11;rgb:ff/ff/ff\x1b\\";
stateMachine.ProcessString(seq);
newColor = gci.GetDefaultBackgroundColor();
VERIFY_ARE_EQUAL(testColor, newColor);
Log::Comment(L"Invalid Decimal Notation");
originalColor = newColor;
testColor = RGB(153, 102, 51);
seq = L"\x1b]11;rgb:153/102/51\x1b\\";
stateMachine.ProcessString(seq);
newColor = gci.GetDefaultBackgroundColor();
VERIFY_ARE_NOT_EQUAL(testColor, newColor);
// it will, in fact leave the color the way it was
VERIFY_ARE_EQUAL(originalColor, newColor);
Log::Comment(L"Invalid Syntax");
testColor = RGB(153, 102, 51);
seq = L"\x1b]11;99/66/33\x1b\\";
stateMachine.ProcessString(seq);
newColor = gci.GetDefaultBackgroundColor();
VERIFY_ARE_NOT_EQUAL(testColor, newColor);
// it will, in fact leave the color the way it was
VERIFY_ARE_EQUAL(originalColor, newColor);
}
void ScreenBufferTests::DeleteCharsNearEndOfLine()
{
// Created for MSFT:19888564.
// There are some cases when you DCH N chars, where there are artifacts left
// from the previous contents of the row after the DCH finishes.
// If you are deleting N chars,
// and there are N+X chars left in the row after the cursor, such that X<N,
// We'll move the X chars to the left, and delete X chars both at the cursor
// pos and at cursor.X+N, but the region of characters at
// [cursor.X+X, cursor.X+N] is left untouched.
//
// Which is the case:
// `(d - 1 > v_w - 1 - c_x - d) && (v_w - 1 - c_x - d >= 0)`
// where:
// - `d`: num chars to delete
// - `v_w`: viewport.Width()
// - `c_x`: cursor.X
//
// Example: (this is tested by DeleteCharsNearEndOfLineSimpleFirstCase)
// start with the following buffer contents, and the cursor on the "D"
// [ABCDEFG ]
// ^
// When you DCH(3) here, we are trying to delete the D, E and F.
// We do that by shifting the contents of the line after the deleted
// characters to the left. HOWEVER, there are only 2 chars left to move.
// So (before the fix) the buffer end up like this:
// [ABCG F ]
// ^
// The G and " " have moved, but the F did not get overwritten.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:dx", L"{1, 2, 3, 5, 8, 13, 21, 34}")
TEST_METHOD_PROPERTY(L"Data:numCharsToDelete", L"{1, 2, 3, 5, 8, 13, 21, 34}")
END_TEST_METHOD_PROPERTIES();
int dx;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"Distance to move the cursor back into the line");
int numCharsToDelete;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"numCharsToDelete", numCharsToDelete), L"Number of characters to delete");
// let W = viewport.Width
// Print W 'X' chars
// Move to (0, W-dx)
// DCH(numCharsToDelete)
// There should be N 'X' chars, and then numSpaces spaces
// where
// numSpaces = min(dx, numCharsToDelete)
// N = W - numSpaces
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& mainBuffer = gci.GetActiveOutputBuffer();
auto& tbi = mainBuffer.GetTextBuffer();
auto& stateMachine = mainBuffer.GetStateMachine();
auto& mainCursor = tbi.GetCursor();
auto& mainView = mainBuffer.GetViewport();
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition());
VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width());
VERIFY_IS_GREATER_THAN(mainView.Width(), (dx + numCharsToDelete));
std::wstring seq = L"X";
for (int x = 0; x < mainView.Width(); x++)
{
stateMachine.ProcessString(seq);
}
VERIFY_ARE_EQUAL(COORD({ mainView.Width() - 1, 0 }), mainCursor.GetPosition());
Log::Comment(NoThrowString().Format(
L"row_i=[%s]",
tbi.GetRowByOffset(0).GetText().c_str()));
mainCursor.SetPosition({ mainView.Width() - static_cast<short>(dx), 0 });
std::wstringstream ss;
ss << L"\x1b[" << numCharsToDelete << L"P";
seq = ss.str(); // Delete N chars
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"row_f=[%s]",
tbi.GetRowByOffset(0).GetText().c_str()));
VERIFY_ARE_EQUAL(COORD({ mainView.Width() - static_cast<short>(dx), 0 }), mainCursor.GetPosition());
auto iter = tbi.GetCellDataAt({ 0, 0 });
auto expectedNumSpaces = std::min(dx, numCharsToDelete);
for (int x = 0; x < mainView.Width() - expectedNumSpaces; x++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
if (iter->Chars() != L"X")
{
Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x));
}
VERIFY_ARE_EQUAL(L"X", iter->Chars());
iter++;
}
for (int x = mainView.Width() - expectedNumSpaces; x < mainView.Width(); x++)
{
if (iter->Chars() != L"\x20")
{
Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x));
}
VERIFY_ARE_EQUAL(L"\x20", iter->Chars());
iter++;
}
}
void ScreenBufferTests::DeleteCharsNearEndOfLineSimpleFirstCase()
{
// Created for MSFT:19888564.
// This is a single case that I'm absolutely sure will repro this bug -
// DeleteCharsNearEndOfLine is the more comprehensive version of this test.
// Write a string, move the cursor into it, then delete some chars.
// There should be no artifacts left behind.
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();
const auto newBufferWidth = 8;
VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ newBufferWidth, si.GetBufferSize().Height() }, false));
auto& mainBuffer = gci.GetActiveOutputBuffer();
const COORD newViewSize{ newBufferWidth, mainBuffer.GetViewport().Height() };
mainBuffer.SetViewportSize(&newViewSize);
auto& tbi = mainBuffer.GetTextBuffer();
auto& mainView = mainBuffer.GetViewport();
auto& mainCursor = tbi.GetCursor();
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition());
VERIFY_ARE_EQUAL(newBufferWidth, mainView.Width());
VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width());
std::wstring seq = L"ABCDEFG";
stateMachine.ProcessString(seq);
VERIFY_ARE_EQUAL(COORD({ 7, 0 }), mainCursor.GetPosition());
// Place the cursor on the 'D'
mainCursor.SetPosition({ 3, 0 });
Log::Comment(NoThrowString().Format(L"before=[%s]", tbi.GetRowByOffset(0).GetText().c_str()));
// Delete 3 chars - [D, E, F]
std::wstringstream ss;
ss << L"\x1b[" << 3 << L"P";
seq = ss.str();
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(L"after =[%s]", tbi.GetRowByOffset(0).GetText().c_str()));
// Cursor shouldn't have moved
VERIFY_ARE_EQUAL(COORD({ 3, 0 }), mainCursor.GetPosition());
auto iter = tbi.GetCellDataAt({ 0, 0 });
VERIFY_ARE_EQUAL(L"A", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"B", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"C", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"G", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"\x20", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"\x20", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"\x20", iter->Chars());
iter++;
}
void ScreenBufferTests::DeleteCharsNearEndOfLineSimpleSecondCase()
{
// Created for MSFT:19888564.
// This is another single case that I'm absolutely sure will repro this bug
// DeleteCharsNearEndOfLine is the more comprehensive version of this test.
// Write a string, move the cursor into it, then delete some chars.
// There should be no artifacts left behind.
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();
const auto newBufferWidth = 8;
VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ newBufferWidth, si.GetBufferSize().Height() }, false));
auto& mainBuffer = gci.GetActiveOutputBuffer();
const COORD newViewSize{ newBufferWidth, mainBuffer.GetViewport().Height() };
mainBuffer.SetViewportSize(&newViewSize);
auto& tbi = mainBuffer.GetTextBuffer();
auto& mainView = mainBuffer.GetViewport();
auto& mainCursor = tbi.GetCursor();
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition());
VERIFY_ARE_EQUAL(newBufferWidth, mainView.Width());
VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width());
std::wstring seq = L"ABCDEFG";
stateMachine.ProcessString(seq);
VERIFY_ARE_EQUAL(COORD({ 7, 0 }), mainCursor.GetPosition());
// Place the cursor on the 'C'
mainCursor.SetPosition({ 2, 0 });
Log::Comment(NoThrowString().Format(L"before=[%s]", tbi.GetRowByOffset(0).GetText().c_str()));
// Delete 4 chars - [C, D, E, F]
std::wstringstream ss;
ss << L"\x1b[" << 4 << L"P";
seq = ss.str();
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(L"after =[%s]", tbi.GetRowByOffset(0).GetText().c_str()));
VERIFY_ARE_EQUAL(COORD({ 2, 0 }), mainCursor.GetPosition());
auto iter = tbi.GetCellDataAt({ 0, 0 });
VERIFY_ARE_EQUAL(L"A", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"B", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"G", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"\x20", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"\x20", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"\x20", iter->Chars());
iter++;
VERIFY_ARE_EQUAL(L"\x20", iter->Chars());
iter++;
}
void ScreenBufferTests::DontResetColorsAboveVirtualBottom()
{
// Created for MSFT:19989333.
// Print some colored text, then scroll the viewport up, so the colored text
// is below the visible viewport. Change the colors, then write a character.
// Both the old chars and the new char should have different colors, the
// first character should not have been reset to the new colors.
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
VERIFY_SUCCESS_NTSTATUS(si.SetViewportOrigin(true, { 0, 1 }, true));
cursor.SetPosition({ 0, si.GetViewport().BottomInclusive() });
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
const auto darkRed = gci.GetColorTableEntry(::XtermToWindowsIndex(1));
const auto darkBlue = gci.GetColorTableEntry(::XtermToWindowsIndex(4));
const auto darkBlack = gci.GetColorTableEntry(::XtermToWindowsIndex(0));
const auto darkWhite = gci.GetColorTableEntry(::XtermToWindowsIndex(7));
stateMachine.ProcessString(L"\x1b[31;44m");
stateMachine.ProcessString(L"X");
stateMachine.ProcessString(L"\x1b[m");
stateMachine.ProcessString(L"X");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(2, cursor.GetPosition().X);
{
const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
VERIFY_ARE_EQUAL(darkRed, gci.LookupForegroundColor(attrA));
VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(darkWhite, gci.LookupForegroundColor(attrB));
VERIFY_ARE_EQUAL(darkBlack, gci.LookupBackgroundColor(attrB));
}
Log::Comment(NoThrowString().Format(L"Emulate scrolling up with the mouse"));
VERIFY_SUCCESS_NTSTATUS(si.SetViewportOrigin(true, { 0, 0 }, false));
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_IS_GREATER_THAN(cursor.GetPosition().Y, si.GetViewport().BottomInclusive());
stateMachine.ProcessString(L"X");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(3, cursor.GetPosition().X);
{
const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y);
const auto attrRow = &row.GetAttrRow();
const std::vector<TextAttribute> attrs{ attrRow->begin(), attrRow->end() };
const auto attrA = attrs[0];
const auto attrB = attrs[1];
const auto attrC = attrs[1];
LOG_ATTR(attrA);
LOG_ATTR(attrB);
LOG_ATTR(attrC);
VERIFY_ARE_EQUAL(darkRed, gci.LookupForegroundColor(attrA));
VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrA));
VERIFY_ARE_EQUAL(darkWhite, gci.LookupForegroundColor(attrB));
VERIFY_ARE_EQUAL(darkBlack, gci.LookupBackgroundColor(attrB));
VERIFY_ARE_EQUAL(darkWhite, gci.LookupForegroundColor(attrC));
VERIFY_ARE_EQUAL(darkBlack, gci.LookupBackgroundColor(attrC));
}
}
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
template<class T>
void _FillLine(COORD position, T fillContent, TextAttribute fillAttr)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& row = si.GetTextBuffer().GetRowByOffset(position.Y);
row.WriteCells({ fillContent, fillAttr }, position.X, false);
}
template<class T>
void _FillLine(int line, T fillContent, TextAttribute fillAttr)
{
_FillLine({ 0, gsl::narrow<SHORT>(line) }, fillContent, fillAttr);
}
template<class T>
void _FillLines(int startLine, int endLine, T fillContent, TextAttribute fillAttr)
{
for (auto line = startLine; line < endLine; ++line)
{
_FillLine(line, fillContent, fillAttr);
}
}
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-11 00:14:40 +01:00
template<class... T>
bool _ValidateLineContains(COORD position, T&&... expectedContent)
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto actual = si.GetCellLineDataAt(position);
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-11 00:14:40 +01:00
auto expected = OutputCellIterator{ std::forward<T>(expectedContent)... };
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
while (actual && expected)
{
if (actual->Chars() != expected->Chars() || actual->TextAttr() != expected->TextAttr())
{
return false;
}
++actual;
++expected;
}
return true;
};
template<class T>
bool _ValidateLineContains(int line, T expectedContent, TextAttribute expectedAttr)
{
return _ValidateLineContains({ 0, gsl::narrow<SHORT>(line) }, expectedContent, expectedAttr);
}
template<class T>
auto _ValidateLinesContain(int startLine, int endLine, T expectedContent, TextAttribute expectedAttr)
{
for (auto line = startLine; line < endLine; ++line)
{
if (!_ValidateLineContains(line, expectedContent, expectedAttr))
{
return false;
}
}
return true;
};
void ScreenBufferTests::ScrollOperations()
{
enum ScrollType : int
{
ScrollUp,
ScrollDown,
InsertLine,
DeleteLine,
ReverseIndex
};
enum ScrollDirection : int
{
Up,
Down
};
ScrollType scrollType;
ScrollDirection scrollDirection;
int scrollMagnitude;
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:scrollType", L"{0, 1, 2, 3, 4}")
TEST_METHOD_PROPERTY(L"Data:scrollMagnitude", L"{1, 2, 5}")
END_TEST_METHOD_PROPERTIES()
VERIFY_SUCCEEDED(TestData::TryGetValue(L"scrollType", (int&)scrollType));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"scrollMagnitude", scrollMagnitude));
std::wstringstream escapeSequence;
switch (scrollType)
{
case ScrollUp:
Log::Comment(L"Testing scroll up (SU).");
escapeSequence << "\x1b[" << scrollMagnitude << "S";
scrollDirection = Up;
break;
case ScrollDown:
Log::Comment(L"Testing scroll down (SD).");
escapeSequence << "\x1b[" << scrollMagnitude << "T";
scrollDirection = Down;
break;
case InsertLine:
Log::Comment(L"Testing insert line (IL).");
escapeSequence << "\x1b[" << scrollMagnitude << "L";
scrollDirection = Down;
break;
case DeleteLine:
Log::Comment(L"Testing delete line (DL).");
escapeSequence << "\x1b[" << scrollMagnitude << "M";
scrollDirection = Up;
break;
case ReverseIndex:
Log::Comment(L"Testing reverse index (RI).");
for (auto i = 0; i < scrollMagnitude; ++i)
{
escapeSequence << "\x1bM";
}
scrollDirection = Down;
break;
default:
VERIFY_FAIL();
return;
}
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
const auto& cursor = si.GetTextBuffer().GetCursor();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
const auto bufferWidth = si.GetBufferSize().Width();
const auto bufferHeight = si.GetBufferSize().Height();
// Move the viewport down a few lines, and only cover part of the buffer width.
si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 10 }), true);
const auto viewportStart = si.GetViewport().Top();
const auto viewportEnd = si.GetViewport().BottomExclusive();
// Fill the entire buffer with Zs. Blue on Green.
const auto bufferChar = L'Z';
const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
_FillLines(0, bufferHeight, bufferChar, bufferAttr);
// Fill the viewport with a range of letters to see if they move. Red on Blue.
const auto viewportAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
auto viewportChar = L'A';
auto viewportLine = viewportStart;
while (viewportLine < viewportEnd)
{
_FillLine(viewportLine++, viewportChar++, viewportAttr);
}
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-11 00:14:40 +01:00
// Set the attributes that will be used to fill the revealed area.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(fillAttr);
// But note that the meta attributes are expected to be cleared.
auto expectedFillAttr = fillAttr;
expectedFillAttr.SetStandardErase();
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
// Place the cursor in the center.
auto cursorPos = COORD{ bufferWidth / 2, (viewportStart + viewportEnd) / 2 };
// Unless this is reverse index, which has to be be at the top of the viewport.
if (scrollType == ReverseIndex)
{
cursorPos.Y = viewportStart;
}
Log::Comment(L"Set the cursor position and perform the operation.");
VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true));
stateMachine.ProcessString(escapeSequence.str());
// The cursor shouldn't move.
auto expectedCursorPos = cursorPos;
// Unless this is an IL or DL control, which moves the cursor to the left margin.
if (scrollType == InsertLine || scrollType == DeleteLine)
{
expectedCursorPos.X = 0;
}
Log::Comment(L"Verify expected cursor position.");
VERIFY_ARE_EQUAL(expectedCursorPos, cursor.GetPosition());
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
Log::Comment(L"Field of Zs outside viewport should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, bufferChar, bufferAttr));
VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, bufferChar, bufferAttr));
// Depending on the direction of scrolling, lines are either deleted or inserted.
const auto deletedLines = scrollDirection == Up ? scrollMagnitude : 0;
const auto insertedLines = scrollDirection == Down ? scrollMagnitude : 0;
// Insert and delete operations only scroll the viewport below the cursor position.
const auto scrollStart = (scrollType == InsertLine || scrollType == DeleteLine) ? cursorPos.Y : viewportStart;
// Reset the viewport character and line number for the verification loop.
viewportChar = L'A';
viewportLine = viewportStart;
Log::Comment(L"Lines above the scrolled area should remain unchanged.");
while (viewportLine < scrollStart)
{
VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr));
}
Log::Comment(L"Scrolled area should have moved up/down by given magnitude.");
viewportChar += gsl::narrow<wchar_t>(deletedLines); // Characters dropped when deleting
viewportLine += gsl::narrow<SHORT>(insertedLines); // Lines skipped when inserting
while (viewportLine < viewportEnd - deletedLines)
{
VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr));
}
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-11 00:14:40 +01:00
Log::Comment(L"The revealed area should now be blank, with standard erase attributes.");
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
const auto revealedStart = scrollDirection == Up ? viewportEnd - deletedLines : scrollStart;
const auto revealedEnd = revealedStart + scrollMagnitude;
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-11 00:14:40 +01:00
VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', expectedFillAttr));
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
}
void ScreenBufferTests::InsertChars()
{
Remove unwanted DECSTBM clipping (#2764) The `DECSTBM` margins are meant to define the range of lines within which certain vertical scrolling operations take place. However, we were applying these margin restrictions in the `ScrollRegion` function, which is also used in a number of places that shouldn't be affected by `DECSTBM`. This includes the `ICH` and `DCH` escape sequences (which are only affected by the horizontal margins, which we don't yet support), the `ScrollConsoleScreenBuffer` API (which is public Console API, not meant to be affected by the VT terminal emulation), and the `CSI 3 J` erase scrollback extension (which isn't really scrolling as such, but uses the `ScrollRegion` function to manipulate the scrollback buffer). This commit moves the margin clipping out of the `ScrollRegion` function, so it can be applied exclusively in the places that need it. With the margin clipping removed from the `ScrollRegion` function, it now had to be applied manually in the places it was actually required. This included: * The `DoSrvPrivateReverseLineFeed` function (for the `RI` control): This was * just a matter of updating the bottom of the scroll rect to the bottom margin * (at least when the margins were actually set), since the top of the scroll * rect would always be the top of the viewport. The * `DoSrvPrivateModifyLinesImpl` function (for the `IL` and `DL` commands): * Again this was just a matter of updating the bottom of the scroll rect, since * the cursor position would always determine the top of the scroll rect. The * `AdaptDispatch::_ScrollMovement` method (for the `SU` and `SD` commands): * This required updating both the top and bottom coordinates of the scroll * rect, and also a simpler destination Y coordinate (the way the `ScrollRegion` * function worked before, the caller was expected to take the margins into * account when determining the destination). On the plus side, there was now no longer a need to override the margins when calling `ScrollRegion` in the `AdjustCursorPosition` function. In the first case, the margins had needed to be cleared (_stream.cpp 143-145), but that is now the default behaviour. In the second case, there had been a more complicated adjustment of the margins (_stream.cpp 196-209), but that code was never actually used so could be removed completely (to get to that point either _fScrollUp_ was true, so _scrollDownAtTop_ couldn't also be true, or _fScrollDown_ was true, but in that case there is a check to make sure _scrollDownAtTop_ is false). While testing, I also noticed that one of the `ScrollRegion` calls in the `AdjustCursorPosition` function was not setting the horizontal range correctly - the scrolling should always affect the full buffer width rather than just the viewport width - so I've fixed that now as well. ## Validation Steps Performed For commands like `RI`, `IL`, `DL`, etc. where we've changed the implementation but not the behaviour, there were already unit tests that could confirm that the new implementation was still producing the correct results. Where there has been a change in behaviour - namely for the `ICH` and `DCH` commands, and the `ScrollConsoleScreenBuffer` API - I've extended the existing unit tests to check that they still function correctly even when the `DECSTBM` margins are set (which would previously have caused them to fail). I've also tested manually with the test cases in issues #2543 and #2659, and confirmed that they now work as expected. Closes #2543 Closes #2659
2019-09-24 01:16:54 +02:00
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:setMargins", L"{false, true}")
END_TEST_METHOD_PROPERTIES();
bool setMargins;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins));
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
// Set the buffer width to 40, with a centered viewport of 20.
const auto bufferWidth = 40;
const auto bufferHeight = si.GetBufferSize().Height();
const auto viewportStart = 10;
const auto viewportEnd = viewportStart + 20;
VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false));
si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true);
Remove unwanted DECSTBM clipping (#2764) The `DECSTBM` margins are meant to define the range of lines within which certain vertical scrolling operations take place. However, we were applying these margin restrictions in the `ScrollRegion` function, which is also used in a number of places that shouldn't be affected by `DECSTBM`. This includes the `ICH` and `DCH` escape sequences (which are only affected by the horizontal margins, which we don't yet support), the `ScrollConsoleScreenBuffer` API (which is public Console API, not meant to be affected by the VT terminal emulation), and the `CSI 3 J` erase scrollback extension (which isn't really scrolling as such, but uses the `ScrollRegion` function to manipulate the scrollback buffer). This commit moves the margin clipping out of the `ScrollRegion` function, so it can be applied exclusively in the places that need it. With the margin clipping removed from the `ScrollRegion` function, it now had to be applied manually in the places it was actually required. This included: * The `DoSrvPrivateReverseLineFeed` function (for the `RI` control): This was * just a matter of updating the bottom of the scroll rect to the bottom margin * (at least when the margins were actually set), since the top of the scroll * rect would always be the top of the viewport. The * `DoSrvPrivateModifyLinesImpl` function (for the `IL` and `DL` commands): * Again this was just a matter of updating the bottom of the scroll rect, since * the cursor position would always determine the top of the scroll rect. The * `AdaptDispatch::_ScrollMovement` method (for the `SU` and `SD` commands): * This required updating both the top and bottom coordinates of the scroll * rect, and also a simpler destination Y coordinate (the way the `ScrollRegion` * function worked before, the caller was expected to take the margins into * account when determining the destination). On the plus side, there was now no longer a need to override the margins when calling `ScrollRegion` in the `AdjustCursorPosition` function. In the first case, the margins had needed to be cleared (_stream.cpp 143-145), but that is now the default behaviour. In the second case, there had been a more complicated adjustment of the margins (_stream.cpp 196-209), but that code was never actually used so could be removed completely (to get to that point either _fScrollUp_ was true, so _scrollDownAtTop_ couldn't also be true, or _fScrollDown_ was true, but in that case there is a check to make sure _scrollDownAtTop_ is false). While testing, I also noticed that one of the `ScrollRegion` calls in the `AdjustCursorPosition` function was not setting the horizontal range correctly - the scrolling should always affect the full buffer width rather than just the viewport width - so I've fixed that now as well. ## Validation Steps Performed For commands like `RI`, `IL`, `DL`, etc. where we've changed the implementation but not the behaviour, there were already unit tests that could confirm that the new implementation was still producing the correct results. Where there has been a change in behaviour - namely for the `ICH` and `DCH` commands, and the `ScrollConsoleScreenBuffer` API - I've extended the existing unit tests to check that they still function correctly even when the `DECSTBM` margins are set (which would previously have caused them to fail). I've also tested manually with the test cases in issues #2543 and #2659, and confirmed that they now work as expected. Closes #2543 Closes #2659
2019-09-24 01:16:54 +02:00
// Tests are run both with and without the DECSTBM margins set. This should not alter
// the results, since the ICH operation is not affected by vertical margins.
stateMachine.ProcessString(setMargins ? L"\x1b[15;20r" : L"\x1b[r");
// Make sure we clear the margins on exit so they can't break other tests.
auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); });
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
Log::Comment(
L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. "
L"Then insert 5 spaces at the cursor. Watch spaces get inserted, text slides right "
L"out of the viewport, pushing some of the Qs out of the buffer.");
const auto insertLine = SHORT{ 10 };
auto insertPos = SHORT{ 20 };
// Place the cursor in the center of the line.
VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true));
// Save the cursor position. It shouldn't move for the rest of the test.
const auto& cursor = si.GetTextBuffer().GetCursor();
auto expectedCursor = cursor.GetPosition();
// Fill the entire line with Qs. Blue on Green.
const auto bufferChar = L'Q';
const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
_FillLine(insertLine, bufferChar, bufferAttr);
// Fill the viewport range with text. Red on Blue.
const auto textChars = L"ABCDEFGHIJKLMNOPQRST";
const auto textAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
_FillLine({ viewportStart, insertLine }, textChars, textAttr);
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-11 00:14:40 +01:00
// Set the attributes that will be used to fill the revealed area.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(fillAttr);
// But note that the meta attributes are expected to be cleared.
auto expectedFillAttr = fillAttr;
expectedFillAttr.SetStandardErase();
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
// Insert 5 spaces at the cursor position.
// Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
// After: QQQQQQQQQQABCDEFGHIJ KLMNOPQRSTQQQQQ
Log::Comment(L"Inserting 5 spaces in the middle of the line.");
auto before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
stateMachine.ProcessString(L"\x1b[5@");
auto after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
Log::Comment(before.c_str(), L"Before");
Log::Comment(after.c_str(), L" After");
// Verify cursor didn't move.
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation.");
// Verify the updated structure of the line.
VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr),
L"Field of Qs left of the viewport should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJ", textAttr),
L"First half of the alphabet should remain unchanged.");
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-11 00:14:40 +01:00
VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr),
L"Spaces should be inserted with standard erase attributes at the cursor position.");
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNOPQRST", textAttr),
L"Second half of the alphabet should have moved to the right by the number of spaces inserted.");
VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd + 5, insertLine }, L"QQQQQ", bufferAttr),
L"Field of Qs right of the viewport should be moved right, half pushed outside the buffer.");
Log::Comment(
L"Test 2: Inserting at the exact end of the line. Same line structure. "
L"Move cursor to right edge of window and insert > 1 space. "
L"Only 1 should be inserted, everything else unchanged.");
// Move cursor to right edge.
insertPos = bufferWidth - 1;
VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true));
expectedCursor = cursor.GetPosition();
// Fill the entire line with Qs. Blue on Green.
_FillLine(insertLine, bufferChar, bufferAttr);
// Fill the viewport range with text. Red on Blue.
_FillLine({ viewportStart, insertLine }, textChars, textAttr);
// Insert 5 spaces at the right edge. Only 1 should be inserted.
// Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
// After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ
Log::Comment(L"Inserting 5 spaces at the right edge of the buffer.");
before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
stateMachine.ProcessString(L"\x1b[5@");
after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
Log::Comment(before.c_str(), L"Before");
Log::Comment(after.c_str(), L" After");
// Verify cursor didn't move.
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation.");
// Verify the updated structure of the line.
VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr),
L"Field of Qs left of the viewport should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr),
L"Entire viewport range should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQ", bufferAttr),
L"Field of Qs right of the viewport should remain unchanged except for the last spot.");
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-11 00:14:40 +01:00
VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr),
L"One space should be inserted with standard erase attributes at the cursor postion.");
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
Log::Comment(
L"Test 3: Inserting at the exact beginning of the line. Same line structure. "
L"Move cursor to left edge of buffer and insert > buffer width of space. "
L"The whole row should be replaced with spaces.");
// Move cursor to left edge.
VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, insertLine }, true));
expectedCursor = cursor.GetPosition();
// Fill the entire line with Qs. Blue on Green.
_FillLine(insertLine, bufferChar, bufferAttr);
// Fill the viewport range with text. Red on Blue.
_FillLine({ viewportStart, insertLine }, textChars, textAttr);
// Insert greater than the buffer width at the left edge. The entire line should be erased.
// Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
// After:
Log::Comment(L"Inserting 100 spaces at the left edge of the buffer.");
before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
stateMachine.ProcessString(L"\x1b[100@");
after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
Log::Comment(before.c_str(), L"Before");
Log::Comment(after.c_str(), L" After");
// Verify cursor didn't move.
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation.");
// Verify the updated structure of the line.
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-11 00:14:40 +01:00
VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', expectedFillAttr),
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
L"A whole line of spaces was inserted at the start, erasing the line.");
}
void ScreenBufferTests::DeleteChars()
{
Remove unwanted DECSTBM clipping (#2764) The `DECSTBM` margins are meant to define the range of lines within which certain vertical scrolling operations take place. However, we were applying these margin restrictions in the `ScrollRegion` function, which is also used in a number of places that shouldn't be affected by `DECSTBM`. This includes the `ICH` and `DCH` escape sequences (which are only affected by the horizontal margins, which we don't yet support), the `ScrollConsoleScreenBuffer` API (which is public Console API, not meant to be affected by the VT terminal emulation), and the `CSI 3 J` erase scrollback extension (which isn't really scrolling as such, but uses the `ScrollRegion` function to manipulate the scrollback buffer). This commit moves the margin clipping out of the `ScrollRegion` function, so it can be applied exclusively in the places that need it. With the margin clipping removed from the `ScrollRegion` function, it now had to be applied manually in the places it was actually required. This included: * The `DoSrvPrivateReverseLineFeed` function (for the `RI` control): This was * just a matter of updating the bottom of the scroll rect to the bottom margin * (at least when the margins were actually set), since the top of the scroll * rect would always be the top of the viewport. The * `DoSrvPrivateModifyLinesImpl` function (for the `IL` and `DL` commands): * Again this was just a matter of updating the bottom of the scroll rect, since * the cursor position would always determine the top of the scroll rect. The * `AdaptDispatch::_ScrollMovement` method (for the `SU` and `SD` commands): * This required updating both the top and bottom coordinates of the scroll * rect, and also a simpler destination Y coordinate (the way the `ScrollRegion` * function worked before, the caller was expected to take the margins into * account when determining the destination). On the plus side, there was now no longer a need to override the margins when calling `ScrollRegion` in the `AdjustCursorPosition` function. In the first case, the margins had needed to be cleared (_stream.cpp 143-145), but that is now the default behaviour. In the second case, there had been a more complicated adjustment of the margins (_stream.cpp 196-209), but that code was never actually used so could be removed completely (to get to that point either _fScrollUp_ was true, so _scrollDownAtTop_ couldn't also be true, or _fScrollDown_ was true, but in that case there is a check to make sure _scrollDownAtTop_ is false). While testing, I also noticed that one of the `ScrollRegion` calls in the `AdjustCursorPosition` function was not setting the horizontal range correctly - the scrolling should always affect the full buffer width rather than just the viewport width - so I've fixed that now as well. ## Validation Steps Performed For commands like `RI`, `IL`, `DL`, etc. where we've changed the implementation but not the behaviour, there were already unit tests that could confirm that the new implementation was still producing the correct results. Where there has been a change in behaviour - namely for the `ICH` and `DCH` commands, and the `ScrollConsoleScreenBuffer` API - I've extended the existing unit tests to check that they still function correctly even when the `DECSTBM` margins are set (which would previously have caused them to fail). I've also tested manually with the test cases in issues #2543 and #2659, and confirmed that they now work as expected. Closes #2543 Closes #2659
2019-09-24 01:16:54 +02:00
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:setMargins", L"{false, true}")
END_TEST_METHOD_PROPERTIES();
bool setMargins;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins));
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
// Set the buffer width to 40, with a centered viewport of 20.
const auto bufferWidth = 40;
const auto bufferHeight = si.GetBufferSize().Height();
const auto viewportStart = 10;
const auto viewportEnd = viewportStart + 20;
VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false));
si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true);
Remove unwanted DECSTBM clipping (#2764) The `DECSTBM` margins are meant to define the range of lines within which certain vertical scrolling operations take place. However, we were applying these margin restrictions in the `ScrollRegion` function, which is also used in a number of places that shouldn't be affected by `DECSTBM`. This includes the `ICH` and `DCH` escape sequences (which are only affected by the horizontal margins, which we don't yet support), the `ScrollConsoleScreenBuffer` API (which is public Console API, not meant to be affected by the VT terminal emulation), and the `CSI 3 J` erase scrollback extension (which isn't really scrolling as such, but uses the `ScrollRegion` function to manipulate the scrollback buffer). This commit moves the margin clipping out of the `ScrollRegion` function, so it can be applied exclusively in the places that need it. With the margin clipping removed from the `ScrollRegion` function, it now had to be applied manually in the places it was actually required. This included: * The `DoSrvPrivateReverseLineFeed` function (for the `RI` control): This was * just a matter of updating the bottom of the scroll rect to the bottom margin * (at least when the margins were actually set), since the top of the scroll * rect would always be the top of the viewport. The * `DoSrvPrivateModifyLinesImpl` function (for the `IL` and `DL` commands): * Again this was just a matter of updating the bottom of the scroll rect, since * the cursor position would always determine the top of the scroll rect. The * `AdaptDispatch::_ScrollMovement` method (for the `SU` and `SD` commands): * This required updating both the top and bottom coordinates of the scroll * rect, and also a simpler destination Y coordinate (the way the `ScrollRegion` * function worked before, the caller was expected to take the margins into * account when determining the destination). On the plus side, there was now no longer a need to override the margins when calling `ScrollRegion` in the `AdjustCursorPosition` function. In the first case, the margins had needed to be cleared (_stream.cpp 143-145), but that is now the default behaviour. In the second case, there had been a more complicated adjustment of the margins (_stream.cpp 196-209), but that code was never actually used so could be removed completely (to get to that point either _fScrollUp_ was true, so _scrollDownAtTop_ couldn't also be true, or _fScrollDown_ was true, but in that case there is a check to make sure _scrollDownAtTop_ is false). While testing, I also noticed that one of the `ScrollRegion` calls in the `AdjustCursorPosition` function was not setting the horizontal range correctly - the scrolling should always affect the full buffer width rather than just the viewport width - so I've fixed that now as well. ## Validation Steps Performed For commands like `RI`, `IL`, `DL`, etc. where we've changed the implementation but not the behaviour, there were already unit tests that could confirm that the new implementation was still producing the correct results. Where there has been a change in behaviour - namely for the `ICH` and `DCH` commands, and the `ScrollConsoleScreenBuffer` API - I've extended the existing unit tests to check that they still function correctly even when the `DECSTBM` margins are set (which would previously have caused them to fail). I've also tested manually with the test cases in issues #2543 and #2659, and confirmed that they now work as expected. Closes #2543 Closes #2659
2019-09-24 01:16:54 +02:00
// Tests are run both with and without the DECSTBM margins set. This should not alter
// the results, since the DCH operation is not affected by vertical margins.
stateMachine.ProcessString(setMargins ? L"\x1b[15;20r" : L"\x1b[r");
// Make sure we clear the margins on exit so they can't break other tests.
auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); });
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
Log::Comment(
L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. "
L"Then delete 5 characters at the cursor. Watch the rest of the line slide left, "
L"replacing the deleted characters, with spaces inserted at the end of the line.");
const auto deleteLine = SHORT{ 10 };
auto deletePos = SHORT{ 20 };
// Place the cursor in the center of the line.
VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true));
// Save the cursor position. It shouldn't move for the rest of the test.
const auto& cursor = si.GetTextBuffer().GetCursor();
auto expectedCursor = cursor.GetPosition();
// Fill the entire line with Qs. Blue on Green.
const auto bufferChar = L'Q';
const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
_FillLine(deleteLine, bufferChar, bufferAttr);
// Fill the viewport range with text. Red on Blue.
const auto textChars = L"ABCDEFGHIJKLMNOPQRST";
const auto textAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
_FillLine({ viewportStart, deleteLine }, textChars, textAttr);
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-11 00:14:40 +01:00
// Set the attributes that will be used to fill the revealed area.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(fillAttr);
// But note that the meta attributes are expected to be cleared.
auto expectedFillAttr = fillAttr;
expectedFillAttr.SetStandardErase();
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
// Delete 5 characters at the cursor position.
// Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
// After: QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ
Log::Comment(L"Deleting 5 characters in the middle of the line.");
auto before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
stateMachine.ProcessString(L"\x1b[5P");
auto after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
Log::Comment(before.c_str(), L"Before");
Log::Comment(after.c_str(), L" After");
// Verify cursor didn't move.
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation.");
// Verify the updated structure of the line.
VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
L"Field of Qs left of the viewport should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJ", textAttr),
L"First half of the alphabet should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L"PQRST", textAttr),
L"Only half of the second part of the alphabet remains.");
VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
L"Field of Qs right of the viewport should be moved left.");
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-11 00:14:40 +01:00
VERIFY_IS_TRUE(_ValidateLineContains({ bufferWidth - 5, deleteLine }, L" ", expectedFillAttr),
L"The rest of the line should be replaced with spaces with standard erase attributes.");
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
Log::Comment(
L"Test 2: Deleting at the exact end of the line. Same line structure. "
L"Move cursor to right edge of window and delete > 1 character. "
L"Only 1 should be deleted, everything else unchanged.");
// Move cursor to right edge.
deletePos = bufferWidth - 1;
VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true));
expectedCursor = cursor.GetPosition();
// Fill the entire line with Qs. Blue on Green.
_FillLine(deleteLine, bufferChar, bufferAttr);
// Fill the viewport range with text. Red on Blue.
_FillLine({ viewportStart, deleteLine }, textChars, textAttr);
// Delete 5 characters at the right edge. Only 1 should be deleted.
// Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
// After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ
Log::Comment(L"Deleting 5 characters at the right edge of the buffer.");
before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
stateMachine.ProcessString(L"\x1b[5P");
after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
Log::Comment(before.c_str(), L"Before");
Log::Comment(after.c_str(), L" After");
// Verify cursor didn't move.
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation.");
// Verify the updated structure of the line.
VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
L"Field of Qs left of the viewport should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr),
L"Entire viewport range should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQ", bufferAttr),
L"Field of Qs right of the viewport should remain unchanged except for the last spot.");
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-11 00:14:40 +01:00
VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L" ", expectedFillAttr),
L"One character should be erased with standard erase attributes at the cursor postion.");
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
Log::Comment(
L"Test 3: Deleting at the exact beginning of the line. Same line structure. "
L"Move cursor to left edge of buffer and delete > buffer width of characters. "
L"The whole row should be replaced with spaces.");
// Move cursor to left edge.
VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, deleteLine }, true));
expectedCursor = cursor.GetPosition();
// Fill the entire line with Qs. Blue on Green.
_FillLine(deleteLine, bufferChar, bufferAttr);
// Fill the viewport range with text. Red on Blue.
_FillLine({ viewportStart, deleteLine }, textChars, textAttr);
// Delete greater than the buffer width at the left edge. The entire line should be erased.
// Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
// After:
Log::Comment(L"Deleting 100 characters at the left edge of the buffer.");
before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
stateMachine.ProcessString(L"\x1b[100P");
after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
Log::Comment(before.c_str(), L"Before");
Log::Comment(after.c_str(), L" After");
// Verify cursor didn't move.
VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation.");
// Verify the updated structure of the line.
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-11 00:14:40 +01:00
VERIFY_IS_TRUE(_ValidateLineContains(deleteLine, L' ', expectedFillAttr),
Correct the boundaries of the scrolling commands (#2505) There are a number of VT escape sequences that rely on the `ScrollRegion` function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues. The `ScrollRegion` function is what ultimately handles the scrolling, but it's typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method, and it's the callers of that method that have needed correcting. One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters. Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width. On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The `ScrollRegion` function already takes care of clipping everything within the buffer boundary, so we can simply set the `Left` of the rect to `0` and the `Right` to `SHORT_MAX`. More details on individual commands: * RI (the `DoSrvPrivateReverseLineFeed` function) This now uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one. * DL and IL (the `DoSrvPrivateModifyLinesImpl` function) Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to `SHORT_MAX` to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting. * SU and SD (the `AdaptDispatch::_ScrollMovement` method) This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the `ScrollRegion` function expects inclusive rects. * ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method) This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the `ScrollRegion` function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width. I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same). This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue. ## Validation Steps Performed There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the `ScrollConsoleScreenBufferW` method used in those tests was just a mock (an incomplete reimplementation of the `ScrollRegion` function), so confirming that the mock produced the correct result told you nothing about the validity of the real code. To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width. I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality. Closes #2174
2019-09-11 03:20:46 +02:00
L"A whole line of spaces was inserted from the right, erasing the line.");
}
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-11 00:14:40 +01:00
void ScreenBufferTests::EraseScrollbackTests()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
const auto& cursor = si.GetTextBuffer().GetCursor();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
const auto bufferWidth = si.GetBufferSize().Width();
const auto bufferHeight = si.GetBufferSize().Height();
// Move the viewport down a few lines, and only cover part of the buffer width.
si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 10 }), true);
const auto viewport = si.GetViewport();
// Fill the entire buffer with Zs. Blue on Green.
const auto bufferChar = L'Z';
const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
_FillLines(0, bufferHeight, bufferChar, bufferAttr);
// Fill the viewport with a range of letters to see if they move. Red on Blue.
const auto viewportAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
auto viewportChar = L'A';
auto viewportLine = viewport.Top();
while (viewportLine < viewport.BottomExclusive())
{
_FillLine(viewportLine++, viewportChar++, viewportAttr);
}
// Set the colors to Green on Red. This should have no effect on the results.
si.SetAttributes({ FOREGROUND_GREEN | BACKGROUND_RED });
// Place the cursor in the center.
const short centerX = bufferWidth / 2;
const short centerY = (si.GetViewport().Top() + si.GetViewport().BottomExclusive()) / 2;
const auto cursorPos = COORD{ centerX, centerY };
Log::Comment(L"Set the cursor position and erase the scrollback.");
VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true));
stateMachine.ProcessString(L"\x1b[3J");
// The viewport should move to the top of the buffer, while the cursor
// maintains the same relative position.
const auto expectedOffset = COORD{ 0, -viewport.Top() };
const auto expectedViewport = Viewport::Offset(viewport, expectedOffset);
const auto expectedCursorPos = COORD{ cursorPos.X, cursorPos.Y + expectedOffset.Y };
Log::Comment(L"Verify expected viewport.");
VERIFY_ARE_EQUAL(expectedViewport, si.GetViewport());
Log::Comment(L"Verify expected cursor position.");
VERIFY_ARE_EQUAL(expectedCursorPos, cursor.GetPosition());
Log::Comment(L"Viewport contents should have moved to the new location.");
viewportChar = L'A';
viewportLine = expectedViewport.Top();
while (viewportLine < expectedViewport.BottomExclusive())
{
VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr));
}
Log::Comment(L"The rest of the buffer should be cleared with default attributes.");
VERIFY_IS_TRUE(_ValidateLinesContain(viewportLine, bufferHeight, L' ', TextAttribute{}));
}
void ScreenBufferTests::EraseTests()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:eraseType", L"{0, 1, 2}") // corresponds to options in DispatchTypes::EraseType
TEST_METHOD_PROPERTY(L"Data:eraseScreen", L"{false, true}") // corresponds to Line (false) or Screen (true)
END_TEST_METHOD_PROPERTIES()
DispatchTypes::EraseType eraseType;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseType", (int&)eraseType));
bool eraseScreen;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseScreen", eraseScreen));
std::wstringstream escapeSequence;
escapeSequence << "\x1b[";
switch (eraseType)
{
case DispatchTypes::EraseType::ToEnd:
Log::Comment(L"Erasing line from cursor to end.");
escapeSequence << "0";
break;
case DispatchTypes::EraseType::FromBeginning:
Log::Comment(L"Erasing line from beginning to cursor.");
escapeSequence << "1";
break;
case DispatchTypes::EraseType::All:
Log::Comment(L"Erasing all.");
escapeSequence << "2";
break;
default:
VERIFY_FAIL(L"Unsupported erase type.");
}
if (!eraseScreen)
{
Log::Comment(L"Erasing just one line (the cursor's line).");
escapeSequence << "K";
}
else
{
Log::Comment(L"Erasing entire display (viewport). May be bounded by the cursor.");
escapeSequence << "J";
}
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
const auto bufferWidth = si.GetBufferSize().Width();
const auto bufferHeight = si.GetBufferSize().Height();
// Move the viewport down a few lines, and only cover part of the buffer width.
si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 10 }), true);
// Fill the entire buffer with Zs. Blue on Green.
const auto bufferChar = L'Z';
const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
_FillLines(0, bufferHeight, bufferChar, bufferAttr);
// Set the attributes that will be used to fill the erased area.
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut);
fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(fillAttr);
// But note that the meta attributes are expected to be cleared.
auto expectedFillAttr = fillAttr;
expectedFillAttr.SetStandardErase();
// Place the cursor in the center.
const short centerX = bufferWidth / 2;
const short centerY = (si.GetViewport().Top() + si.GetViewport().BottomExclusive()) / 2;
Log::Comment(L"Set the cursor position and perform the operation.");
VERIFY_SUCCEEDED(si.SetCursorPosition({ centerX, centerY }, true));
stateMachine.ProcessString(escapeSequence.str());
// Get cursor position and viewport range.
const auto cursorPos = si.GetTextBuffer().GetCursor().GetPosition();
const auto viewportStart = si.GetViewport().Top();
const auto viewportEnd = si.GetViewport().BottomExclusive();
Log::Comment(L"Lines outside the viewport should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, bufferChar, bufferAttr));
VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, bufferChar, bufferAttr));
// 1. Lines before cursor line
if (eraseScreen && eraseType != DispatchTypes::EraseType::ToEnd)
{
// For eraseScreen, if we're not erasing to the end, these rows will be cleared.
Log::Comment(L"Lines before the cursor line should be erased.");
VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, cursorPos.Y, L' ', expectedFillAttr));
}
else
{
// Otherwise we'll be left with the original buffer content.
Log::Comment(L"Lines before the cursor line should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, cursorPos.Y, bufferChar, bufferAttr));
}
// 2. Cursor Line
auto prefixPos = COORD{ 0, cursorPos.Y };
auto suffixPos = cursorPos;
// When erasing from the beginning, the cursor column is included in the range.
suffixPos.X += (eraseType == DispatchTypes::EraseType::FromBeginning);
size_t prefixWidth = suffixPos.X;
size_t suffixWidth = bufferWidth - prefixWidth;
if (eraseType == DispatchTypes::EraseType::ToEnd)
{
Log::Comment(L"The start of the cursor line should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains(prefixPos, bufferChar, bufferAttr, prefixWidth));
Log::Comment(L"The end of the cursor line should be erased.");
VERIFY_IS_TRUE(_ValidateLineContains(suffixPos, L' ', expectedFillAttr, suffixWidth));
}
if (eraseType == DispatchTypes::EraseType::FromBeginning)
{
Log::Comment(L"The start of the cursor line should be erased.");
VERIFY_IS_TRUE(_ValidateLineContains(prefixPos, L' ', expectedFillAttr, prefixWidth));
Log::Comment(L"The end of the cursor line should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLineContains(suffixPos, bufferChar, bufferAttr, suffixWidth));
}
if (eraseType == DispatchTypes::EraseType::All)
{
Log::Comment(L"The entire cursor line should be erased.");
VERIFY_IS_TRUE(_ValidateLineContains(cursorPos.Y, L' ', expectedFillAttr));
}
// 3. Lines after cursor line
if (eraseScreen && eraseType != DispatchTypes::EraseType::FromBeginning)
{
// For eraseScreen, if we're not erasing from the beginning, these rows will be cleared.
Log::Comment(L"Lines after the cursor line should be erased.");
VERIFY_IS_TRUE(_ValidateLinesContain(cursorPos.Y + 1, viewportEnd, L' ', expectedFillAttr));
}
else
{
// Otherwise we'll be left with the original buffer content.
Log::Comment(L"Lines after the cursor line should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLinesContain(cursorPos.Y + 1, viewportEnd, bufferChar, bufferAttr));
}
}
void _CommonScrollingSetup()
{
// Used for testing MSFT:20204600
// Place an A on the first line, and a B on the 6th line (index 5).
// Set the scrolling region in between those lines (so scrolling won't affect them.)
// First write "1\n2\n3\n4", to put 1-4 on the lines in between the A and B.
// the viewport will look like:
// A
// 1
// 2
// 3
// 4
// B
// then write "\n5\n6\n7\n", which will cycle around the scroll region a bit.
// the viewport will look like:
// A
// 5
// 6
// 7
//
// B
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
const auto oldView = si.GetViewport();
const auto view = Viewport::FromDimensions({ 0, 0 }, { oldView.Width(), 6 });
si.SetViewport(view, true);
cursor.SetPosition({ 0, 0 });
std::wstring seq = L"A";
stateMachine.ProcessString(seq);
cursor.SetPosition({ 0, 5 });
seq = L"B";
stateMachine.ProcessString(seq);
seq = L"\x1b[2;5r";
stateMachine.ProcessString(seq);
seq = L"\x1b[2;1H";
stateMachine.ProcessString(seq);
seq = L"1\n2\n3\n4";
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(1, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"1", iter1->Chars());
VERIFY_ARE_EQUAL(L"2", iter2->Chars());
VERIFY_ARE_EQUAL(L"3", iter3->Chars());
VERIFY_ARE_EQUAL(L"4", iter4->Chars());
VERIFY_ARE_EQUAL(L"B", iter5->Chars());
}
seq = L"\n5\n6\n7\n";
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"5", iter1->Chars());
VERIFY_ARE_EQUAL(L"6", iter2->Chars());
VERIFY_ARE_EQUAL(L"7", iter3->Chars());
// Chars() will return a single space for an empty row.
VERIFY_ARE_EQUAL(L"\x20", iter4->Chars());
VERIFY_ARE_EQUAL(L"B", iter5->Chars());
}
}
void ScreenBufferTests::ScrollUpInMargins()
{
// Tests MSFT:20204600
// Do the common scrolling setup, then executes a Scroll Up, and verifies
// the rows have what we'd expect.
_CommonScrollingSetup();
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
// Execute a Scroll Up command
std::wstring seq = L"\x1b[S";
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"6", iter1->Chars());
VERIFY_ARE_EQUAL(L"7", iter2->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter3->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter4->Chars());
VERIFY_ARE_EQUAL(L"B", iter5->Chars());
}
}
void ScreenBufferTests::ScrollDownInMargins()
{
// Tests MSFT:20204600
// Do the common scrolling setup, then executes a Scroll Down, and verifies
// the rows have what we'd expect.
_CommonScrollingSetup();
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
// Execute a Scroll Down command
std::wstring seq = L"\x1b[T";
stateMachine.ProcessString(seq);
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter1->Chars());
VERIFY_ARE_EQUAL(L"5", iter2->Chars());
VERIFY_ARE_EQUAL(L"6", iter3->Chars());
VERIFY_ARE_EQUAL(L"7", iter4->Chars());
VERIFY_ARE_EQUAL(L"B", iter5->Chars());
}
}
void ScreenBufferTests::InsertLinesInMargins()
{
Log::Comment(
L"Does the common scrolling setup, then inserts two lines inside the "
L"margin boundaries, and verifies the rows have what we'd expect.");
_CommonScrollingSetup();
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
// Move to column 5 of line 3
stateMachine.ProcessString(L"\x1b[3;5H");
// Insert 2 lines
stateMachine.ProcessString(L"\x1b[2L");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
// Verify cursor moved to left margin.
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(2, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"5", iter1->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter2->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter3->Chars());
VERIFY_ARE_EQUAL(L"6", iter4->Chars());
VERIFY_ARE_EQUAL(L"B", iter5->Chars());
}
Log::Comment(
L"Does the common scrolling setup, then inserts one line with no "
L"margins set, and verifies the rows have what we'd expect.");
_CommonScrollingSetup();
// Clear the scroll margins
stateMachine.ProcessString(L"\x1b[r");
// Move to column 5 of line 2
stateMachine.ProcessString(L"\x1b[2;5H");
// Insert 1 line
stateMachine.ProcessString(L"\x1b[L");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
// Verify cursor moved to left margin.
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter1->Chars());
VERIFY_ARE_EQUAL(L"5", iter2->Chars());
VERIFY_ARE_EQUAL(L"6", iter3->Chars());
VERIFY_ARE_EQUAL(L"7", iter4->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter5->Chars());
}
}
void ScreenBufferTests::DeleteLinesInMargins()
{
Log::Comment(
L"Does the common scrolling setup, then deletes two lines inside the "
L"margin boundaries, and verifies the rows have what we'd expect.");
_CommonScrollingSetup();
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
// Move to column 5 of line 3
stateMachine.ProcessString(L"\x1b[3;5H");
// Delete 2 lines
stateMachine.ProcessString(L"\x1b[2M");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
// Verify cursor moved to left margin.
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(2, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"5", iter1->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter2->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter3->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter4->Chars());
VERIFY_ARE_EQUAL(L"B", iter5->Chars());
}
Log::Comment(
L"Does the common scrolling setup, then deletes one line with no "
L"margins set, and verifies the rows have what we'd expect.");
_CommonScrollingSetup();
// Clear the scroll margins
stateMachine.ProcessString(L"\x1b[r");
// Move to column 5 of line 2
stateMachine.ProcessString(L"\x1b[2;5H");
// Delete 1 line
stateMachine.ProcessString(L"\x1b[M");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
// Verify cursor moved to left margin.
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"6", iter1->Chars());
VERIFY_ARE_EQUAL(L"7", iter2->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter3->Chars());
VERIFY_ARE_EQUAL(L"B", iter4->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter5->Chars());
}
}
void ScreenBufferTests::ReverseLineFeedInMargins()
{
Log::Comment(
L"Does the common scrolling setup, then executes a reverse line feed "
L"below the top margin, and verifies the rows have what we'd expect.");
_CommonScrollingSetup();
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
// Move to column 5 of line 2, the top margin
stateMachine.ProcessString(L"\x1b[2;5H");
// Execute a reverse line feed (RI)
stateMachine.ProcessString(L"\x1bM");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(4, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"A", iter0->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter1->Chars());
VERIFY_ARE_EQUAL(L"5", iter2->Chars());
VERIFY_ARE_EQUAL(L"6", iter3->Chars());
VERIFY_ARE_EQUAL(L"7", iter4->Chars());
VERIFY_ARE_EQUAL(L"B", iter5->Chars());
}
Log::Comment(
L"Does the common scrolling setup, then executes a reverse line feed "
L"with the top margin at the top of the screen, and verifies the rows "
L"have what we'd expect.");
_CommonScrollingSetup();
// Set the top scroll margin to the top of the screen
stateMachine.ProcessString(L"\x1b[1;5r");
// Move to column 5 of line 1, the top of the screen
stateMachine.ProcessString(L"\x1b[1;5H");
// Execute a reverse line feed (RI)
stateMachine.ProcessString(L"\x1bM");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(4, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
{
auto iter0 = tbi.GetCellDataAt({ 0, 0 });
auto iter1 = tbi.GetCellDataAt({ 0, 1 });
auto iter2 = tbi.GetCellDataAt({ 0, 2 });
auto iter3 = tbi.GetCellDataAt({ 0, 3 });
auto iter4 = tbi.GetCellDataAt({ 0, 4 });
auto iter5 = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"\x20", iter0->Chars());
VERIFY_ARE_EQUAL(L"A", iter1->Chars());
VERIFY_ARE_EQUAL(L"5", iter2->Chars());
VERIFY_ARE_EQUAL(L"6", iter3->Chars());
VERIFY_ARE_EQUAL(L"7", iter4->Chars());
VERIFY_ARE_EQUAL(L"B", iter5->Chars());
}
}
void ScreenBufferTests::ScrollLines256Colors()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:scrollType", L"{0, 1, 2}")
TEST_METHOD_PROPERTY(L"Data:colorStyle", L"{0, 1, 2}")
END_TEST_METHOD_PROPERTIES();
// colorStyle will be used to control whether we use a color from the 16
// color table, a color from the 256 color table, or a pure RGB color.
const int Use16Color = 0;
const int Use256Color = 1;
const int UseRGBColor = 2;
// scrollType will be used to control whether we use InsertLines,
// DeleteLines, or ReverseIndex to scroll the contents of the buffer.
const int InsertLines = 0;
const int DeleteLines = 1;
const int ReverseLineFeed = 2;
int scrollType;
int colorStyle;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"scrollType", scrollType), L"controls whether to use InsertLines, DeleteLines ot ReverseLineFeed");
VERIFY_SUCCEEDED(TestData::TryGetValue(L"colorStyle", colorStyle), L"controls whether to use the 16 color table, 256 table, or RGB colors");
// This test is largely taken from repro code from
// https://github.com/microsoft/terminal/issues/832#issuecomment-507447272
Log::Comment(
L"Sets the attributes to a 256/RGB color, then scrolls some lines with"
L" IL/DL/RI. Verifies the rows are cleared with the attributes we'd expect.");
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
TextAttribute expectedAttr{ si.GetAttributes() };
std::wstring sgrSeq = L"\x1b[48;5;2m";
if (colorStyle == Use16Color)
{
expectedAttr.SetBackground(gci.GetColorTableEntry(2));
}
else if (colorStyle == Use256Color)
{
expectedAttr.SetBackground(gci.GetColorTableEntry(20));
sgrSeq = L"\x1b[48;5;20m";
}
else if (colorStyle == UseRGBColor)
{
expectedAttr.SetBackground(RGB(1, 2, 3));
sgrSeq = L"\x1b[48;2;1;2;3m";
}
// Set some scrolling margins
stateMachine.ProcessString(L"\x1b[1;3r");
// Set the BG color to the table index 2, as a 256-color sequence
stateMachine.ProcessString(sgrSeq);
VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes());
// Move to home
stateMachine.ProcessString(L"\x1b[H");
// Insert/Delete/Reverse Index 10 lines
std::wstring scrollSeq = L"";
if (scrollType == InsertLines)
{
scrollSeq = L"\x1b[10L";
}
if (scrollType == DeleteLines)
{
scrollSeq = L"\x1b[10M";
}
if (scrollType == ReverseLineFeed)
{
// This is 10 "Reverse Index" commands, which don't accept a parameter.
scrollSeq = L"\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM\x1bM";
}
stateMachine.ProcessString(scrollSeq);
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"viewport=%s", VerifyOutputTraits<SMALL_RECT>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
stateMachine.ProcessString(L"foo");
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
VERIFY_ARE_EQUAL(3, cursor.GetPosition().X);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
{
auto iter00 = tbi.GetCellDataAt({ 0, 0 });
auto iter10 = tbi.GetCellDataAt({ 1, 0 });
auto iter20 = tbi.GetCellDataAt({ 2, 0 });
auto iter30 = tbi.GetCellDataAt({ 3, 0 });
auto iter01 = tbi.GetCellDataAt({ 0, 1 });
auto iter02 = tbi.GetCellDataAt({ 0, 2 });
VERIFY_ARE_EQUAL(L"f", iter00->Chars());
VERIFY_ARE_EQUAL(L"o", iter10->Chars());
VERIFY_ARE_EQUAL(L"o", iter20->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter30->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter01->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter02->Chars());
VERIFY_ARE_EQUAL(expectedAttr, iter00->TextAttr());
VERIFY_ARE_EQUAL(expectedAttr, iter10->TextAttr());
VERIFY_ARE_EQUAL(expectedAttr, iter20->TextAttr());
VERIFY_ARE_EQUAL(expectedAttr, iter30->TextAttr());
VERIFY_ARE_EQUAL(expectedAttr, iter01->TextAttr());
VERIFY_ARE_EQUAL(expectedAttr, iter02->TextAttr());
}
}
void ScreenBufferTests::SetOriginMode()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
const auto view = Viewport::FromDimensions({ 0, 0 }, { 80, 25 });
si.SetViewport(view, true);
// Testing the default state (absolute cursor addressing)
Log::Comment(L"By default, setting a margin moves the cursor to the top-left of the screen.");
cursor.SetPosition({ 40, 12 });
stateMachine.ProcessString(L"\x1B[6;20r");
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
Log::Comment(L"Cursor addressing is relative to the top-left of the screen.");
stateMachine.ProcessString(L"\x1B[13;41H");
VERIFY_ARE_EQUAL(COORD({ 40, 12 }), cursor.GetPosition());
Log::Comment(L"The cursor can be moved below the bottom margin.");
stateMachine.ProcessString(L"\x1B[23;41H");
VERIFY_ARE_EQUAL(COORD({ 40, 22 }), cursor.GetPosition());
// Testing the effects of DECOM being set (relative cursor addressing)
Log::Comment(L"Setting DECOM moves the cursor to the top-left of the margin area.");
cursor.SetPosition({ 40, 12 });
stateMachine.ProcessString(L"\x1B[?6h");
VERIFY_ARE_EQUAL(COORD({ 0, 5 }), cursor.GetPosition());
Log::Comment(L"Setting a margin moves the cursor to the top-left of the margin area.");
cursor.SetPosition({ 40, 12 });
stateMachine.ProcessString(L"\x1B[6;20r");
VERIFY_ARE_EQUAL(COORD({ 0, 5 }), cursor.GetPosition());
Log::Comment(L"Cursor addressing is relative to the top-left of the margin area.");
stateMachine.ProcessString(L"\x1B[8;41H");
VERIFY_ARE_EQUAL(COORD({ 40, 12 }), cursor.GetPosition());
Log::Comment(L"The cursor cannot be moved below the bottom margin.");
stateMachine.ProcessString(L"\x1B[100;41H");
VERIFY_ARE_EQUAL(COORD({ 40, 19 }), cursor.GetPosition());
// Testing the effects of DECOM being reset (absolute cursor addressing)
Log::Comment(L"Resetting DECOM moves the cursor to the top-left of the screen.");
cursor.SetPosition({ 40, 12 });
stateMachine.ProcessString(L"\x1B[?6l");
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
Log::Comment(L"Setting a margin moves the cursor to the top-left of the screen.");
cursor.SetPosition({ 40, 12 });
stateMachine.ProcessString(L"\x1B[6;20r");
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
Log::Comment(L"Cursor addressing is relative to the top-left of the screen.");
stateMachine.ProcessString(L"\x1B[13;41H");
VERIFY_ARE_EQUAL(COORD({ 40, 12 }), cursor.GetPosition());
Log::Comment(L"The cursor can be moved below the bottom margin.");
stateMachine.ProcessString(L"\x1B[23;41H");
VERIFY_ARE_EQUAL(COORD({ 40, 22 }), cursor.GetPosition());
// Testing the effects of DECOM being set with no margins
Log::Comment(L"With no margins, setting DECOM moves the cursor to the top-left of the screen.");
stateMachine.ProcessString(L"\x1B[r");
cursor.SetPosition({ 40, 12 });
stateMachine.ProcessString(L"\x1B[?6h");
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
Log::Comment(L"Cursor addressing is still relative to the top-left of the screen.");
stateMachine.ProcessString(L"\x1B[13;41H");
VERIFY_ARE_EQUAL(COORD({ 40, 12 }), cursor.GetPosition());
// Reset DECOM so we don't affect future tests
stateMachine.ProcessString(L"\x1B[?6l");
}
Make the RIS command clear the display and scrollback correctly (#2367) When the scrollback buffer is empty, the RIS escape sequence (Reset to Initial State) will fail to clear the screen, or reset any of the state. And when there is something in the scrollback, it doesn't get cleared completely, and the screen may get filled with the wrong background color (it should use the default color, but it actually uses the previously active background color). This commit attempts to fix those issues. The initial failure is caused by the `SCREEN_INFORMATION::WriteRect` method throwing an exception when passed an empty viewport. And the reason it's passed an empty viewport is because that's what the `Viewport::Subtract` method returns when the result of the subtraction is nothing. The PR fixes the problem by making the `Viewport::Subtract` method actually return nothing in that situation. This is a change in the defined behavior that also required the associated viewport tests to be updated. However, it does seem a sensible change, since the `Subtract` method never returns empty viewports under any other circumstances. And the only place the method seems to be used is in the `ScrollRegion` implementation, where the previous behavior is guaranteed to throw an exception. The other issues are fixed simply by changing the order in which things are reset in the `AdaptDispatch::HardReset` method. The call to `SoftReset` needed to be made first, so that the SGR attributes would be reset before the screen was cleared, thus making sure that the default background color would be used. And the screen needed to be cleared before the scrollback was erased, otherwise the last view of the screen would be retained in the scrollback buffer. These changes also required existing adapter tests to be updated, but not because of a change in the expected behaviour. It's just that certain tests relied on the `SoftReset` happening later in the order, so weren't expecting it to be called if say the scrollback erase had failed. It doesn't seem like the tests were deliberately trying to verify that the SoftReset _hadn't_ been called. In addition to the updates to existing tests, this PR also add a new screen buffer test which verifies the display and scrollback are correctly cleared under the conditions that were previously failing. Fixes #2307.
2019-08-28 03:45:38 +02:00
void ScreenBufferTests::HardResetBuffer()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
const auto& viewport = si.GetViewport();
const auto& cursor = si.GetTextBuffer().GetCursor();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
auto isBufferClear = [&]() {
auto offset = 0;
auto width = si.GetBufferSize().Width();
for (auto iter = si.GetCellDataAt({}); iter; ++iter, ++offset)
{
if (iter->Chars() != L" " || iter->TextAttr() != TextAttribute{})
{
Log::Comment(NoThrowString().Format(
L"Buffer not clear at (X:%d, Y:%d)",
offset % width,
offset / width));
return false;
}
}
return true;
};
const auto resetToInitialState = L"\033c";
Log::Comment(L"Start with a clear buffer, viewport and cursor at 0,0");
si.SetAttributes(TextAttribute());
si.ClearTextData();
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, true));
VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, 0 }, true));
VERIFY_IS_TRUE(isBufferClear());
Log::Comment(L"Write a single line of text to the buffer");
stateMachine.ProcessString(L"Hello World!\n");
VERIFY_IS_FALSE(isBufferClear());
VERIFY_ARE_EQUAL(COORD({ 0, 1 }), cursor.GetPosition());
Log::Comment(L"After a reset, buffer should be clear, with cursor at 0,0");
stateMachine.ProcessString(resetToInitialState);
VERIFY_IS_TRUE(isBufferClear());
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
Log::Comment(L"Set the background color to red");
stateMachine.ProcessString(L"\x1b[41m");
Log::Comment(L"Write multiple pages of text to the buffer");
for (auto i = 0; i < viewport.Height() * 2; i++)
{
stateMachine.ProcessString(L"Hello World!\n");
}
VERIFY_IS_FALSE(isBufferClear());
VERIFY_IS_GREATER_THAN(viewport.Top(), viewport.Height());
VERIFY_IS_GREATER_THAN(cursor.GetPosition().Y, viewport.Height());
Log::Comment(L"After a reset, buffer should be clear, with viewport and cursor at 0,0");
stateMachine.ProcessString(resetToInitialState);
VERIFY_IS_TRUE(isBufferClear());
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), viewport.Origin());
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
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-11 00:14:40 +01:00
VERIFY_ARE_EQUAL(TextAttribute{}, si.GetAttributes());
Make the RIS command clear the display and scrollback correctly (#2367) When the scrollback buffer is empty, the RIS escape sequence (Reset to Initial State) will fail to clear the screen, or reset any of the state. And when there is something in the scrollback, it doesn't get cleared completely, and the screen may get filled with the wrong background color (it should use the default color, but it actually uses the previously active background color). This commit attempts to fix those issues. The initial failure is caused by the `SCREEN_INFORMATION::WriteRect` method throwing an exception when passed an empty viewport. And the reason it's passed an empty viewport is because that's what the `Viewport::Subtract` method returns when the result of the subtraction is nothing. The PR fixes the problem by making the `Viewport::Subtract` method actually return nothing in that situation. This is a change in the defined behavior that also required the associated viewport tests to be updated. However, it does seem a sensible change, since the `Subtract` method never returns empty viewports under any other circumstances. And the only place the method seems to be used is in the `ScrollRegion` implementation, where the previous behavior is guaranteed to throw an exception. The other issues are fixed simply by changing the order in which things are reset in the `AdaptDispatch::HardReset` method. The call to `SoftReset` needed to be made first, so that the SGR attributes would be reset before the screen was cleared, thus making sure that the default background color would be used. And the screen needed to be cleared before the scrollback was erased, otherwise the last view of the screen would be retained in the scrollback buffer. These changes also required existing adapter tests to be updated, but not because of a change in the expected behaviour. It's just that certain tests relied on the `SoftReset` happening later in the order, so weren't expecting it to be called if say the scrollback erase had failed. It doesn't seem like the tests were deliberately trying to verify that the SoftReset _hadn't_ been called. In addition to the updates to existing tests, this PR also add a new screen buffer test which verifies the display and scrollback are correctly cleared under the conditions that were previously failing. Fixes #2307.
2019-08-28 03:45:38 +02:00
}
void ScreenBufferTests::RestoreDownAltBufferWithTerminalScrolling()
{
// This is a test for microsoft/terminal#1206. Refer to that issue for more
// context
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetTerminalScrolling(true);
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
auto& siMain = gci.GetActiveOutputBuffer();
COORD const coordFontSize = siMain.GetScreenFontSize();
siMain._virtualBottom = siMain._viewport.BottomInclusive();
auto originalView = siMain._viewport;
VERIFY_IS_NULL(siMain._psiMainBuffer);
VERIFY_IS_NULL(siMain._psiAlternateBuffer);
Log::Comment(L"Create an alternate buffer");
if (VERIFY_IS_TRUE(NT_SUCCESS(siMain.UseAlternateScreenBuffer())))
{
VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer);
auto& altBuffer = *siMain._psiAlternateBuffer;
VERIFY_ARE_EQUAL(0, altBuffer._viewport.Top());
VERIFY_ARE_EQUAL(altBuffer._viewport.BottomInclusive(), altBuffer._virtualBottom);
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
const COORD originalSize = originalView.Dimensions();
const COORD doubledSize = { originalSize.X * 2, originalSize.Y * 2 };
// Create some RECTs, which are dimensions in pixels, because
// ProcessResizeWindow needs to work on rects in screen _pixel_
// dimensions, not character sizes.
RECT originalClientRect{ 0 }, maximizedClientRect{ 0 };
originalClientRect.right = originalSize.X * coordFontSize.X;
originalClientRect.bottom = originalSize.Y * coordFontSize.Y;
maximizedClientRect.right = doubledSize.X * coordFontSize.X;
maximizedClientRect.bottom = doubledSize.Y * coordFontSize.Y;
Log::Comment(NoThrowString().Format(
L"Emulate a maximize"));
// Note that just calling _InternalSetViewportSize does not hit the
// exceptional case here. There's other logic farther down the stack
// that triggers it.
altBuffer.ProcessResizeWindow(&maximizedClientRect, &originalClientRect);
VERIFY_ARE_EQUAL(0, altBuffer._viewport.Top());
VERIFY_ARE_EQUAL(altBuffer._viewport.BottomInclusive(), altBuffer._virtualBottom);
Log::Comment(NoThrowString().Format(
L"Emulate a restore down"));
altBuffer.ProcessResizeWindow(&originalClientRect, &maximizedClientRect);
// Before the bugfix, this would fail, with the top being roughly 80,
// halfway into the buffer, with the bottom being anchored to the old
// size.
VERIFY_ARE_EQUAL(0, altBuffer._viewport.Top());
VERIFY_ARE_EQUAL(altBuffer._viewport.BottomInclusive(), altBuffer._virtualBottom);
}
}
void ScreenBufferTests::SnapCursorWithTerminalScrolling()
{
// This is a test for microsoft/terminal#1222. Refer to that issue for more
// context
auto& g = ServiceLocator::LocateGlobals();
CONSOLE_INFORMATION& gci = g.getConsoleInformation();
gci.SetTerminalScrolling(true);
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
auto& si = gci.GetActiveOutputBuffer();
auto& cursor = si.GetTextBuffer().GetCursor();
const auto originalView = si._viewport;
si._virtualBottom = originalView.BottomInclusive();
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"originalView=%s", VerifyOutputTraits<SMALL_RECT>::ToString(originalView.ToInclusive()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"First set the viewport somewhere lower in the buffer, as if the text "
L"was output there. Manually move the cursor there as well, so the "
L"cursor is within that viewport."));
const COORD secondWindowOrigin{ 0, 10 };
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, secondWindowOrigin, true));
si.GetTextBuffer().GetCursor().SetPosition(secondWindowOrigin);
const auto secondView = si._viewport;
const auto secondVirtualBottom = si._virtualBottom;
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"secondView=%s", VerifyOutputTraits<SMALL_RECT>::ToString(secondView.ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(10, secondView.Top());
VERIFY_ARE_EQUAL(originalView.Height() + 10, secondView.BottomExclusive());
VERIFY_ARE_EQUAL(originalView.Height() + 10 - 1, secondVirtualBottom);
Log::Comment(NoThrowString().Format(
L"Emulate scrolling upwards with the mouse (not moving the virtual view)"));
const COORD thirdWindowOrigin{ 0, 2 };
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, thirdWindowOrigin, false));
const auto thirdView = si._viewport;
const auto thirdVirtualBottom = si._virtualBottom;
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"thirdView=%s", VerifyOutputTraits<SMALL_RECT>::ToString(thirdView.ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(2, thirdView.Top());
VERIFY_ARE_EQUAL(originalView.Height() + 2, thirdView.BottomExclusive());
VERIFY_ARE_EQUAL(secondVirtualBottom, thirdVirtualBottom);
Log::Comment(NoThrowString().Format(
L"Call SetConsoleCursorPosition to snap to the cursor"));
VERIFY_SUCCEEDED(g.api.SetConsoleCursorPositionImpl(si, secondWindowOrigin));
const auto fourthView = si._viewport;
const auto fourthVirtualBottom = si._virtualBottom;
Log::Comment(NoThrowString().Format(
L"cursor=%s", VerifyOutputTraits<COORD>::ToString(cursor.GetPosition()).GetBuffer()));
Log::Comment(NoThrowString().Format(
L"thirdView=%s", VerifyOutputTraits<SMALL_RECT>::ToString(fourthView.ToInclusive()).GetBuffer()));
VERIFY_ARE_EQUAL(10, fourthView.Top());
VERIFY_ARE_EQUAL(originalView.Height() + 10, fourthView.BottomExclusive());
VERIFY_ARE_EQUAL(secondVirtualBottom, fourthVirtualBottom);
}
void ScreenBufferTests::ClearAlternateBuffer()
{
// This is a test for microsoft/terminal#1189. Refer to that issue for more
// context
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& g = ServiceLocator::LocateGlobals();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
auto& siMain = gci.GetActiveOutputBuffer();
auto WriteText = [&](TextBuffer& tbi) {
// Write text to buffer
auto& stateMachine = siMain.GetStateMachine();
auto& cursor = tbi.GetCursor();
stateMachine.ProcessString(L"foo\nfoo");
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1);
};
auto VerifyText = [&](TextBuffer& tbi) {
// Verify written text in buffer
{
auto iter00 = tbi.GetCellDataAt({ 0, 0 });
auto iter10 = tbi.GetCellDataAt({ 1, 0 });
auto iter20 = tbi.GetCellDataAt({ 2, 0 });
auto iter30 = tbi.GetCellDataAt({ 3, 0 });
auto iter01 = tbi.GetCellDataAt({ 0, 1 });
auto iter02 = tbi.GetCellDataAt({ 1, 1 });
auto iter03 = tbi.GetCellDataAt({ 2, 1 });
VERIFY_ARE_EQUAL(L"f", iter00->Chars());
VERIFY_ARE_EQUAL(L"o", iter10->Chars());
VERIFY_ARE_EQUAL(L"o", iter20->Chars());
VERIFY_ARE_EQUAL(L"\x20", iter30->Chars());
VERIFY_ARE_EQUAL(L"f", iter01->Chars());
VERIFY_ARE_EQUAL(L"o", iter02->Chars());
VERIFY_ARE_EQUAL(L"o", iter03->Chars());
}
};
WriteText(siMain.GetTextBuffer());
VerifyText(siMain.GetTextBuffer());
Log::Comment(L"Create an alternate buffer");
if (VERIFY_IS_TRUE(NT_SUCCESS(siMain.UseAlternateScreenBuffer())))
{
VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer);
auto& altBuffer = *siMain._psiAlternateBuffer;
VERIFY_ARE_EQUAL(0, altBuffer._viewport.Top());
VERIFY_ARE_EQUAL(altBuffer._viewport.BottomInclusive(), altBuffer._virtualBottom);
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
WriteText(altBuffer.GetTextBuffer());
VerifyText(altBuffer.GetTextBuffer());
#pragma region Test ScrollConsoleScreenBufferWImpl()
// Clear text of alt buffer (same params as in CMD)
VERIFY_SUCCEEDED(g.api.ScrollConsoleScreenBufferWImpl(siMain,
{ 0, 0, 120, 9001 },
{ 0, -9001 },
std::nullopt,
L' ',
7));
// Verify text is now gone
VERIFY_ARE_EQUAL(L" ", altBuffer.GetTextBuffer().GetCellDataAt({ 0, 0 })->Chars());
#pragma endregion
#pragma region Test SetConsoleCursorPositionImpl()
// Reset cursor position as we do with CLS command (same params as in CMD)
VERIFY_SUCCEEDED(g.api.SetConsoleCursorPositionImpl(siMain, { 0 }));
// Verify state of alt buffer
auto& altBufferCursor = altBuffer.GetTextBuffer().GetCursor();
VERIFY_ARE_EQUAL(altBufferCursor.GetPosition().X, 0);
VERIFY_ARE_EQUAL(altBufferCursor.GetPosition().Y, 0);
#pragma endregion
}
// Verify state of main buffer is untouched
auto& cursor = siMain.GetTextBuffer().GetCursor();
VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3);
VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1);
VerifyText(siMain.GetTextBuffer());
}
void ScreenBufferTests::InitializeTabStopsInVTMode()
{
// This is a test for microsoft/terminal#411. Refer to that issue for more
// context.
// Run this test in isolation - Let's not pollute the VT level for other
// tests, or go blowing away other test's buffers
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES()
auto& g = ServiceLocator::LocateGlobals();
auto& gci = g.getConsoleInformation();
VERIFY_IS_FALSE(gci.GetActiveOutputBuffer().AreTabsSet());
// Enable VT mode before we construct the buffer. This emulates setting the
// VirtualTerminalLevel reg key before launching the console.
gci.SetVirtTermLevel(1);
// Clean up the old buffer, and re-create it. This new buffer will be
// created as if the VT mode was always on.
m_state->CleanupGlobalScreenBuffer();
m_state->PrepareGlobalScreenBuffer();
VERIFY_IS_TRUE(gci.GetActiveOutputBuffer().AreTabsSet());
}
void ScreenBufferTests::TestExtendedTextAttributes()
{
// This is a test for microsoft/terminal#2554. Refer to that issue for more
// context.
// We're going to set every possible combination of extended attributes via
// VT, then disable them, and make sure that they are all always represented
// internally correctly.
// Run this test for each and every possible combination of states.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:bold", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:italics", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:blink", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:invisible", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:crossedOut", L"{false, true}")
END_TEST_METHOD_PROPERTIES()
bool bold, italics, blink, invisible, crossedOut;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"bold", bold));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"crossedOut", crossedOut));
auto& g = ServiceLocator::LocateGlobals();
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = tbi.GetCursor();
ExtendedAttributes expectedAttrs{ ExtendedAttributes::Normal };
std::wstring vtSeq = L"";
// Collect up a VT sequence to set the state given the method properties
if (bold)
{
WI_SetFlag(expectedAttrs, ExtendedAttributes::Bold);
vtSeq += L"\x1b[1m";
}
if (italics)
{
WI_SetFlag(expectedAttrs, ExtendedAttributes::Italics);
vtSeq += L"\x1b[3m";
}
if (blink)
{
WI_SetFlag(expectedAttrs, ExtendedAttributes::Blinking);
vtSeq += L"\x1b[5m";
}
if (invisible)
{
WI_SetFlag(expectedAttrs, ExtendedAttributes::Invisible);
vtSeq += L"\x1b[8m";
}
if (crossedOut)
{
WI_SetFlag(expectedAttrs, ExtendedAttributes::CrossedOut);
vtSeq += L"\x1b[9m";
}
// Helper lambda to write a VT sequence, then an "X", then check that the
// attributes of the "X" match what we think they should be.
auto validate = [&](const ExtendedAttributes expectedAttrs,
const std::wstring& vtSequence) {
auto cursorPos = cursor.GetPosition();
// Convert the vtSequence to something printable. Lets not set these
// attrs on the test console
std::wstring debugString = vtSequence;
{
size_t start_pos = 0;
while ((start_pos = debugString.find(L"\x1b", start_pos)) != std::string::npos)
{
debugString.replace(start_pos, 1, L"\\x1b");
start_pos += 4;
}
}
Log::Comment(NoThrowString().Format(
L"Testing string:\"%s\"", debugString.c_str()));
Log::Comment(NoThrowString().Format(
L"Expecting attrs:0x%02x", expectedAttrs));
stateMachine.ProcessString(vtSequence);
stateMachine.ProcessString(L"X");
auto iter = tbi.GetCellDataAt(cursorPos);
auto currentExtendedAttrs = iter->TextAttr().GetExtendedAttributes();
VERIFY_ARE_EQUAL(expectedAttrs, currentExtendedAttrs);
};
// Check setting all the states collected above
validate(expectedAttrs, vtSeq);
// One-by-one, turn off each of these states with VT, then check that the
// state matched.
if (bold)
{
WI_ClearFlag(expectedAttrs, ExtendedAttributes::Bold);
vtSeq = L"\x1b[22m";
validate(expectedAttrs, vtSeq);
}
if (italics)
{
WI_ClearFlag(expectedAttrs, ExtendedAttributes::Italics);
vtSeq = L"\x1b[23m";
validate(expectedAttrs, vtSeq);
}
if (blink)
{
WI_ClearFlag(expectedAttrs, ExtendedAttributes::Blinking);
vtSeq = L"\x1b[25m";
validate(expectedAttrs, vtSeq);
}
if (invisible)
{
WI_ClearFlag(expectedAttrs, ExtendedAttributes::Invisible);
vtSeq = L"\x1b[28m";
validate(expectedAttrs, vtSeq);
}
if (crossedOut)
{
WI_ClearFlag(expectedAttrs, ExtendedAttributes::CrossedOut);
vtSeq = L"\x1b[29m";
validate(expectedAttrs, vtSeq);
}
stateMachine.ProcessString(L"\x1b[0m");
}
void ScreenBufferTests::TestExtendedTextAttributesWithColors()
{
// This is a test for microsoft/terminal#2554. Refer to that issue for more
// context.
// We're going to set every possible combination of extended attributes via
// VT, then set assorted colors, then disable extended attrs, then reset
// colors, in various ways, and make sure that they are all always
// represented internally correctly.
// Run this test for each and every possible combination of states.
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:bold", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:italics", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:blink", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:invisible", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:crossedOut", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:setForegroundType", L"{0, 1, 2, 3}")
TEST_METHOD_PROPERTY(L"Data:setBackgroundType", L"{0, 1, 2, 3}")
END_TEST_METHOD_PROPERTIES()
// colorStyle will be used to control whether we use a color from the 16
// color table, a color from the 256 color table, or a pure RGB color.
const int UseDefault = 0;
const int Use16Color = 1;
const int Use256Color = 2;
const int UseRGBColor = 3;
bool bold, italics, blink, invisible, crossedOut;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"bold", bold));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"italics", italics));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"blink", blink));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"invisible", invisible));
VERIFY_SUCCEEDED(TestData::TryGetValue(L"crossedOut", crossedOut));
int setForegroundType, setBackgroundType;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"setForegroundType", setForegroundType), L"controls whether to use the 16 color table, 256 table, or RGB colors");
VERIFY_SUCCEEDED(TestData::TryGetValue(L"setBackgroundType", setBackgroundType), L"controls whether to use the 16 color table, 256 table, or RGB colors");
auto& g = ServiceLocator::LocateGlobals();
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = tbi.GetCursor();
TextAttribute expectedAttr{ si.GetAttributes() };
ExtendedAttributes expectedExtendedAttrs{ ExtendedAttributes::Normal };
std::wstring vtSeq = L"";
// Collect up a VT sequence to set the state given the method properties
if (bold)
{
WI_SetFlag(expectedExtendedAttrs, ExtendedAttributes::Bold);
vtSeq += L"\x1b[1m";
}
if (italics)
{
WI_SetFlag(expectedExtendedAttrs, ExtendedAttributes::Italics);
vtSeq += L"\x1b[3m";
}
if (blink)
{
WI_SetFlag(expectedExtendedAttrs, ExtendedAttributes::Blinking);
vtSeq += L"\x1b[5m";
}
if (invisible)
{
WI_SetFlag(expectedExtendedAttrs, ExtendedAttributes::Invisible);
vtSeq += L"\x1b[8m";
}
if (crossedOut)
{
WI_SetFlag(expectedExtendedAttrs, ExtendedAttributes::CrossedOut);
vtSeq += L"\x1b[9m";
}
// Prepare the foreground attributes
if (setForegroundType == UseDefault)
{
expectedAttr.SetDefaultForeground();
vtSeq += L"\x1b[39m";
}
else if (setForegroundType == Use16Color)
{
expectedAttr.SetIndexedAttributes({ static_cast<BYTE>(2) }, std::nullopt);
vtSeq += L"\x1b[32m";
}
else if (setForegroundType == Use256Color)
{
expectedAttr.SetForeground(gci.GetColorTableEntry(20));
vtSeq += L"\x1b[38;5;20m";
}
else if (setForegroundType == UseRGBColor)
{
expectedAttr.SetForeground(RGB(1, 2, 3));
vtSeq += L"\x1b[38;2;1;2;3m";
}
// Prepare the background attributes
if (setBackgroundType == UseDefault)
{
expectedAttr.SetDefaultBackground();
vtSeq += L"\x1b[49m";
}
else if (setBackgroundType == Use16Color)
{
expectedAttr.SetIndexedAttributes(std::nullopt, { static_cast<BYTE>(2) });
vtSeq += L"\x1b[42m";
}
else if (setBackgroundType == Use256Color)
{
expectedAttr.SetBackground(gci.GetColorTableEntry(20));
vtSeq += L"\x1b[48;5;20m";
}
else if (setBackgroundType == UseRGBColor)
{
expectedAttr.SetBackground(RGB(1, 2, 3));
vtSeq += L"\x1b[48;2;1;2;3m";
}
expectedAttr.SetExtendedAttributes(expectedExtendedAttrs);
// Helper lambda to write a VT sequence, then an "X", then check that the
// attributes of the "X" match what we think they should be.
auto validate = [&](const TextAttribute attr,
const std::wstring& vtSequence) {
auto cursorPos = cursor.GetPosition();
// Convert the vtSequence to something printable. Lets not set these
// attrs on the test console
std::wstring debugString = vtSequence;
{
size_t start_pos = 0;
while ((start_pos = debugString.find(L"\x1b", start_pos)) != std::string::npos)
{
debugString.replace(start_pos, 1, L"\\x1b");
start_pos += 4;
}
}
Log::Comment(NoThrowString().Format(
L"Testing string:\"%s\"", debugString.c_str()));
Log::Comment(NoThrowString().Format(
L"Expecting attrs:0x%02x", VerifyOutputTraits<TextAttribute>::ToString(attr).GetBuffer()));
stateMachine.ProcessString(vtSequence);
stateMachine.ProcessString(L"X");
auto iter = tbi.GetCellDataAt(cursorPos);
const TextAttribute currentAttrs = iter->TextAttr();
VERIFY_ARE_EQUAL(attr, currentAttrs);
};
// Check setting all the states collected above
validate(expectedAttr, vtSeq);
// One-by-one, turn off each of these states with VT, then check that the
// state matched.
if (bold)
{
WI_ClearFlag(expectedExtendedAttrs, ExtendedAttributes::Bold);
expectedAttr.SetExtendedAttributes(expectedExtendedAttrs);
vtSeq = L"\x1b[22m";
validate(expectedAttr, vtSeq);
}
if (italics)
{
WI_ClearFlag(expectedExtendedAttrs, ExtendedAttributes::Italics);
expectedAttr.SetExtendedAttributes(expectedExtendedAttrs);
vtSeq = L"\x1b[23m";
validate(expectedAttr, vtSeq);
}
if (blink)
{
WI_ClearFlag(expectedExtendedAttrs, ExtendedAttributes::Blinking);
expectedAttr.SetExtendedAttributes(expectedExtendedAttrs);
vtSeq = L"\x1b[25m";
validate(expectedAttr, vtSeq);
}
if (invisible)
{
WI_ClearFlag(expectedExtendedAttrs, ExtendedAttributes::Invisible);
expectedAttr.SetExtendedAttributes(expectedExtendedAttrs);
vtSeq = L"\x1b[28m";
validate(expectedAttr, vtSeq);
}
if (crossedOut)
{
WI_ClearFlag(expectedExtendedAttrs, ExtendedAttributes::CrossedOut);
expectedAttr.SetExtendedAttributes(expectedExtendedAttrs);
vtSeq = L"\x1b[29m";
validate(expectedAttr, vtSeq);
}
stateMachine.ProcessString(L"\x1b[0m");
}
void ScreenBufferTests::CursorUpDownAcrossMargins()
{
// Test inspired by: https://github.com/microsoft/terminal/issues/2929
// echo -e "\e[6;19r\e[24H\e[99AX\e[1H\e[99BY\e[r"
// This does the following:
// * sets the top and bottom DECSTBM margins to 6 and 19
// * moves to line 24 (i.e. below the bottom margin)
// * executes the CUU sequence with a count of 99, to move up 99 lines
// * writes out X
// * moves to line 1 (i.e. above the top margin)
// * executes the CUD sequence with a count of 99, to move down 99 lines
// * writes out Y
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24);
// Set some scrolling margins
stateMachine.ProcessString(L"\x1b[6;19r");
stateMachine.ProcessString(L"\x1b[24H");
VERIFY_ARE_EQUAL(23, cursor.GetPosition().Y);
stateMachine.ProcessString(L"\x1b[99A");
VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y);
stateMachine.ProcessString(L"X");
{
auto iter = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"X", iter->Chars());
}
stateMachine.ProcessString(L"\x1b[1H");
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
stateMachine.ProcessString(L"\x1b[99B");
VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y);
stateMachine.ProcessString(L"Y");
{
auto iter = tbi.GetCellDataAt({ 0, 18 });
VERIFY_ARE_EQUAL(L"Y", iter->Chars());
}
stateMachine.ProcessString(L"\x1b[r");
}
void ScreenBufferTests::CursorUpDownOutsideMargins()
{
// Test inspired by the CursorUpDownAcrossMargins test.
// echo -e "\e[6;19r\e[24H\e[1AX\e[1H\e[1BY\e[r"
// This does the following:
// * sets the top and bottom DECSTBM margins to 6 and 19
// * moves to line 24 (i.e. below the bottom margin)
// * executes the CUU sequence with a count of 1, to move up 1 lines (still below margins)
// * writes out X
// * moves to line 1 (i.e. above the top margin)
// * executes the CUD sequence with a count of 1, to move down 1 lines (still above margins)
// * writes out Y
// This test is different becasue the end location of the vertical movement
// should not be within the margins at all. We should not clamp this
// movement to be within the margins.
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24);
// Set some scrolling margins
stateMachine.ProcessString(L"\x1b[6;19r");
stateMachine.ProcessString(L"\x1b[24H");
VERIFY_ARE_EQUAL(23, cursor.GetPosition().Y);
stateMachine.ProcessString(L"\x1b[1A");
VERIFY_ARE_EQUAL(22, cursor.GetPosition().Y);
stateMachine.ProcessString(L"X");
{
auto iter = tbi.GetCellDataAt({ 0, 22 });
VERIFY_ARE_EQUAL(L"X", iter->Chars());
}
stateMachine.ProcessString(L"\x1b[1H");
VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y);
stateMachine.ProcessString(L"\x1b[1B");
VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y);
stateMachine.ProcessString(L"Y");
{
auto iter = tbi.GetCellDataAt({ 0, 1 });
VERIFY_ARE_EQUAL(L"Y", iter->Chars());
}
stateMachine.ProcessString(L"\x1b[r");
}
void ScreenBufferTests::CursorUpDownExactlyAtMargins()
{
// Test inspired by the CursorUpDownAcrossMargins test.
// echo -e "\e[6;19r\e[19H\e[1B1\e[1A2\e[6H\e[1A3\e[1B4\e[r"
// This does the following:
// * sets the top and bottom DECSTBM margins to 6 and 19
// * moves to line 19 (i.e. on the bottom margin)
// * executes the CUD sequence with a count of 1, to move down 1 lines (still on the margin)
// * writes out 1
// * executes the CUU sequence with a count of 1, to move up 1 lines (now inside margins)
// * writes out 2
// * moves to line 6 (i.e. on the top margin)
// * executes the CUU sequence with a count of 1, to move up 1 lines (still on the margin)
// * writes out 3
// * executes the CUD sequence with a count of 1, to move down 1 lines (still above margins)
// * writes out 4
// This test is different becasue the starting location for these scroll
// operations is _exactly_ on the margins
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& tbi = si.GetTextBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24);
// Set some scrolling margins
stateMachine.ProcessString(L"\x1b[6;19r");
stateMachine.ProcessString(L"\x1b[19;1H");
VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y);
stateMachine.ProcessString(L"\x1b[1B");
VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y);
stateMachine.ProcessString(L"1");
{
auto iter = tbi.GetCellDataAt({ 0, 18 });
VERIFY_ARE_EQUAL(L"1", iter->Chars());
}
stateMachine.ProcessString(L"\x1b[1A");
VERIFY_ARE_EQUAL(17, cursor.GetPosition().Y);
stateMachine.ProcessString(L"2");
{
auto iter = tbi.GetCellDataAt({ 1, 17 });
VERIFY_ARE_EQUAL(L"2", iter->Chars());
}
stateMachine.ProcessString(L"\x1b[6;1H");
VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y);
stateMachine.ProcessString(L"\x1b[1A");
VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y);
stateMachine.ProcessString(L"3");
{
auto iter = tbi.GetCellDataAt({ 0, 5 });
VERIFY_ARE_EQUAL(L"3", iter->Chars());
}
stateMachine.ProcessString(L"\x1b[1B");
VERIFY_ARE_EQUAL(6, cursor.GetPosition().Y);
stateMachine.ProcessString(L"4");
{
auto iter = tbi.GetCellDataAt({ 1, 6 });
VERIFY_ARE_EQUAL(L"4", iter->Chars());
}
stateMachine.ProcessString(L"\x1b[r");
}
Improve the DECSC/DECRC implementation (#3160) The current DECSC implementation only saves the cursor position and origin mode. This PR improves that functionality with additional support for saving the SGR attributes, as well as the active character set. It also fixes the way the saved state interacts with the alt buffer (private mode 1049), triggering a save when switching to the alt buffer, and a restore when switching back, and tracking the alt buffer state independently from the main state. In order to properly save and restore the SGR attributes, we first needed to add a pair of APIs in the `ConGetSet` interface which could round-trip the attributes with full 32-bit colors (the existing methods only work with legacy attributes). I then added a struct in the `AdaptDispatch` implementation to make it easier to manage all of the parameters that needed to be saved. This includes the cursor position and origin mode that we were already tracking, and now also the SGR text attributes and the active character set (via the `TermOutput` class). Two instances of this structure are required, since changes made to the saved state in the alt buffer need to be tracked separately from changes in the main buffer. I've added a boolean property that specifies whether we're in the alt buffer or not, and use that to decide which of the two instances we're working with. I also needed to explicitly trigger a save when switching to the alt buffer, and a restore when switching back, since we weren't already doing that (the existing implementation gave the impression that the state was saved, because each buffer has its own cursor position, but that doesn't properly match the XTerm behaviour). For the state tracking itself, we've now got two additional properties - the SGR attributes, which we obtain via the new private API, and the active character set, which we get from a local `AdaptDispatch` field. I'm saving the whole `TermOutput` class for the character set, since I'm hoping that will make it automatically supports future enhancements. When restoring the cursor position, there is also now a fix to handle the relative origin mode correctly. If the margins are changed between the position being saved and restored, it's possible for the cursor to end up outside of the new margins, which would be illegal. So there is now an additional step that clamps the Y coordinate within the margin boundaries if the origin mode is relative. # Validation I've added a couple of screen buffer tests which check that the various parameters are saved and restored as expected, as well as checking that the Y coordinate is clamped appropriately when the relative origin mode is set. I've also tested manually with vttest and confirmed that the _SAVE/RESTORE CURSOR_ test (the last page of the _Test of screen features_)) is now working a lot better than it used to. Closes #148.
2019-11-05 22:35:50 +01:00
void ScreenBufferTests::CursorSaveRestore()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();
auto& cursor = si.GetTextBuffer().GetCursor();
const auto defaultAttrs = TextAttribute{};
const auto colorAttrs = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
const auto asciiText = L"lwkmvj";
const auto graphicText = L"┌┬┐└┴┘";
const auto selectAsciiChars = L"\x1b(B";
const auto selectGraphicsChars = L"\x1b(0";
const auto saveCursor = L"\x1b[s";
const auto restoreCursor = L"\x1b[u";
const auto setDECOM = L"\x1b[?6h";
const auto resetDECOM = L"\x1b[?6l";
Log::Comment(L"Make sure the viewport is at 0,0");
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));
Log::Comment(L"Restore after save.");
// Set the cursor position, attributes, and character set.
cursor.SetPosition(COORD{ 20, 10 });
si.SetAttributes(colorAttrs);
stateMachine.ProcessString(selectGraphicsChars);
// Save state.
stateMachine.ProcessString(saveCursor);
// Reset the cursor position, attributes, and character set.
cursor.SetPosition(COORD{ 0, 0 });
si.SetAttributes(defaultAttrs);
stateMachine.ProcessString(selectAsciiChars);
// Restore state.
stateMachine.ProcessString(restoreCursor);
// Verify initial position, colors, and graphic character set.
VERIFY_ARE_EQUAL(COORD({ 20, 10 }), cursor.GetPosition());
VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes());
stateMachine.ProcessString(asciiText);
VERIFY_IS_TRUE(_ValidateLineContains(COORD({ 20, 10 }), graphicText, colorAttrs));
Log::Comment(L"Restore again without save.");
// Reset the cursor position, attributes, and character set.
cursor.SetPosition(COORD{ 0, 0 });
si.SetAttributes(defaultAttrs);
stateMachine.ProcessString(selectAsciiChars);
// Restore state.
stateMachine.ProcessString(restoreCursor);
// Verify initial saved position, colors, and graphic character set.
VERIFY_ARE_EQUAL(COORD({ 20, 10 }), cursor.GetPosition());
VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes());
stateMachine.ProcessString(asciiText);
VERIFY_IS_TRUE(_ValidateLineContains(COORD({ 20, 10 }), graphicText, colorAttrs));
Log::Comment(L"Restore after reset.");
// Soft reset.
stateMachine.ProcessString(L"\x1b[!p");
// Set the cursor position, attributes, and character set.
cursor.SetPosition(COORD{ 20, 10 });
si.SetAttributes(colorAttrs);
stateMachine.ProcessString(selectGraphicsChars);
// Restore state.
stateMachine.ProcessString(restoreCursor);
// Verify home position, default attributes, and ascii character set.
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
VERIFY_ARE_EQUAL(defaultAttrs, si.GetAttributes());
stateMachine.ProcessString(asciiText);
VERIFY_IS_TRUE(_ValidateLineContains(COORD({ 0, 0 }), asciiText, defaultAttrs));
Log::Comment(L"Restore origin mode.");
// Set margins and origin mode to relative.
stateMachine.ProcessString(L"\x1b[10;20r");
stateMachine.ProcessString(setDECOM);
// Verify home position inside margins.
VERIFY_ARE_EQUAL(COORD({ 0, 9 }), cursor.GetPosition());
// Save state and reset origin mode to absolute.
stateMachine.ProcessString(saveCursor);
stateMachine.ProcessString(resetDECOM);
// Verify home position at origin.
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition());
// Restore state and move to home position.
stateMachine.ProcessString(restoreCursor);
stateMachine.ProcessString(L"\x1b[H");
// Verify home position inside margins, i.e. relative origin mode restored.
VERIFY_ARE_EQUAL(COORD({ 0, 9 }), cursor.GetPosition());
Log::Comment(L"Clamp inside top margin.");
// Reset margins, with absolute origin, and set cursor position.
stateMachine.ProcessString(L"\x1b[r");
stateMachine.ProcessString(setDECOM);
cursor.SetPosition(COORD{ 5, 15 });
// Save state.
stateMachine.ProcessString(saveCursor);
// Set margins and restore state.
stateMachine.ProcessString(L"\x1b[20;25r");
stateMachine.ProcessString(restoreCursor);
// Verfify Y position is clamped inside the top margin
VERIFY_ARE_EQUAL(COORD({ 5, 19 }), cursor.GetPosition());
Log::Comment(L"Clamp inside bottom margin.");
// Reset margins, with absolute origin, and set cursor position.
stateMachine.ProcessString(L"\x1b[r");
stateMachine.ProcessString(setDECOM);
cursor.SetPosition(COORD{ 5, 15 });
// Save state.
stateMachine.ProcessString(saveCursor);
// Set margins and restore state.
stateMachine.ProcessString(L"\x1b[1;10r");
stateMachine.ProcessString(restoreCursor);
// Verfify Y position is clamped inside the top margin
VERIFY_ARE_EQUAL(COORD({ 5, 9 }), cursor.GetPosition());
// Reset origin mode and margins.
stateMachine.ProcessString(resetDECOM);
stateMachine.ProcessString(L"\x1b[r");
}
Add support for the DECALN escape sequence (#3968) ## Summary of the Pull Request This adds support for the [`DECALN`](https://vt100.net/docs/vt510-rm/DECALN.html) escape sequence, which produces a kind of test pattern, originally used on VT terminals to adjust the screen alignment. It's needed to pass several of the tests in the [Vttest](https://invisible-island.net/vttest/) suite. ## PR Checklist * [x] Closes #3671 * [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 discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx ## Detailed Description of the Pull Request / Additional comments To start with, the `ActionEscDispatch` method in the `OutputStateMachineEngine` needed to be extended to check for a new intermediate type (`#`). Then when that intermediate is followed by an `8`, it dispatches to a new `ScreenAlignmentPattern` method in the `ITermDispatch` interface. The implementation of the `ScreenAlignmentPattern` itself is fairly simple. It uses the recently added `PrivateFillRegion` API to fill the screen with the character `E` using default attributes. Then in addition to that, a bunch of VT properties are reset: * The meta/extended attributes are reset (although the active colors must be left unchanged). * The origin mode is set to absolute positioning. * The scrolling margins are cleared. * The cursor position is moved to home. ## Validation Steps Performed I've added a screen buffer test that makes sure the `DECALN` sequence fills the screen with the correct character and attributes, and that the above mentioned properties are all updated appropriately. I've also tested in Vttest, and confirmed that the first two pages of the _Test of cursor movements_ are now showing the frame of E's that are expected there.
2019-12-17 19:11:52 +01:00
void ScreenBufferTests::ScreenAlignmentPattern()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
const auto& cursor = si.GetTextBuffer().GetCursor();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
Log::Comment(L"Set the initial buffer state.");
const auto bufferWidth = si.GetBufferSize().Width();
const auto bufferHeight = si.GetBufferSize().Height();
// Move the viewport down a few lines, and only cover part of the buffer width.
si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 30 }), true);
const auto viewportStart = si.GetViewport().Top();
const auto viewportEnd = si.GetViewport().BottomExclusive();
// Fill the entire buffer with Zs. Blue on Green.
const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
_FillLines(0, bufferHeight, L'Z', bufferAttr);
// Set the initial attributes.
auto initialAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
initialAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE);
si.SetAttributes(initialAttr);
// Set some margins.
stateMachine.ProcessString(L"\x1b[10;20r");
VERIFY_IS_TRUE(si.AreMarginsSet());
// Place the cursor in the center.
auto cursorPos = COORD{ bufferWidth / 2, (viewportStart + viewportEnd) / 2 };
VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true));
Log::Comment(L"Execute the DECALN escape sequence.");
stateMachine.ProcessString(L"\x1b#8");
Log::Comment(L"Lines within view should be filled with Es, with default attributes.");
auto defaultAttr = TextAttribute{};
VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, viewportEnd, L'E', defaultAttr));
Log::Comment(L"Field of Zs outside viewport should remain unchanged.");
VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, L'Z', bufferAttr));
VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, L'Z', bufferAttr));
Log::Comment(L"Margins should not be set.");
VERIFY_IS_FALSE(si.AreMarginsSet());
Log::Comment(L"Cursor position shold be moved to home.");
auto homePosition = COORD{ 0, viewportStart };
VERIFY_ARE_EQUAL(homePosition, cursor.GetPosition());
Log::Comment(L"Meta/rendition attributes should be reset.");
auto expectedAttr = initialAttr;
expectedAttr.SetMetaAttributes(0);
VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes());
}