From 7813953b23c4fdf547997d4fc461802d1cb4d0ef Mon Sep 17 00:00:00 2001 From: James Holderness Date: Thu, 19 Jan 2023 19:59:05 +0000 Subject: [PATCH] Add support for IRM (Insert Replace Mode) (#14700) This PR add support for the ANSI Insert/Replace mode (`IRM`), which determines whether output characters are inserted at the active cursor position, moving existing content to the right, or whether they should overwrite the content that is already there. The implementation is a bit of a hack. When that mode is enabled, it first measures how many cells the string is expected to occupy, then scrolls the target line right by that amount before writing out the new text. In the longer term it might be better if this was implemented entirely in the `TextBuffer` itself, so the scrolling could take place at the same time as the content was being written. ## Validation Steps Performed I've added a very basic unit test that verifies the mode is working as expected. But I've also done a lot more manual testing, confirming edge cases like wide characters, double-width lines, and both with and without wrapping mode enabled. Closes #1947 --- src/host/ut_host/ScreenBufferTests.cpp | 77 +++++++++++++++++++ src/terminal/adapter/DispatchTypes.hpp | 1 + src/terminal/adapter/ITermDispatch.hpp | 4 +- src/terminal/adapter/adaptDispatch.cpp | 30 ++++++-- src/terminal/adapter/adaptDispatch.hpp | 5 +- src/terminal/adapter/termDispatch.hpp | 4 +- .../parser/OutputStateMachineEngine.cpp | 12 +++ .../parser/OutputStateMachineEngine.hpp | 2 + src/terminal/parser/telemetry.cpp | 2 + src/terminal/parser/telemetry.hpp | 2 + 10 files changed, 128 insertions(+), 11 deletions(-) diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 786fe399c98..706f4e3540a 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -182,6 +182,7 @@ class ScreenBufferTests TEST_METHOD(DontResetColorsAboveVirtualBottom); TEST_METHOD(ScrollOperations); + TEST_METHOD(InsertReplaceMode); TEST_METHOD(InsertChars); TEST_METHOD(DeleteChars); TEST_METHOD(ScrollingWideCharsHorizontally); @@ -3782,6 +3783,82 @@ void ScreenBufferTests::ScrollOperations() VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', expectedFillAttr)); } +void ScreenBufferTests::InsertReplaceMode() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const auto bufferHeight = si.GetBufferSize().Height(); + const auto viewport = si.GetViewport(); + const auto targetRow = viewport.Top() + 5; + const auto targetCol = til::CoordType{ 10 }; + + // 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 target row with asterisks and a range of letters at the start. Red on Blue. + const auto initialChars = L"ABCDEFGHIJKLMNOPQRST"; + const auto initialAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + _FillLine(targetRow, L'*', initialAttr); + _FillLine(targetRow, initialChars, initialAttr); + + // Set the attributes that will be used for the new content. + auto newAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + newAttr.SetCrossedOut(true); + newAttr.SetReverseVideo(true); + newAttr.SetUnderlined(true); + si.SetAttributes(newAttr); + + Log::Comment(L"Write additional content into a line of text with IRM mode enabled."); + + // Set the cursor position partway through the target row. + VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow }, true)); + // Enable Insert/Replace mode. + stateMachine.ProcessString(L"\033[4h"); + // Write out some new content. + const auto newChars = L"12345"; + stateMachine.ProcessString(newChars); + + VERIFY_IS_TRUE(_ValidateLineContains({ 0, targetRow }, L"ABCDEFGHIJ", initialAttr), + L"First half of the line should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol, targetRow }, newChars, newAttr), + L"New content should be inserted at the cursor position with active attributes."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 5, targetRow }, L"KLMNOPQRST", initialAttr), + L"Second half of the line should have moved 5 columns across."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 25, targetRow }, L'*', initialAttr), + L"With the remainder of the line filled with asterisks."); + VERIFY_IS_TRUE(_ValidateLineContains(targetRow + 1, bufferChar, bufferAttr), + L"The following line should be unaffected."); + + // Fill the target row with the initial content again. + _FillLine(targetRow, L'*', initialAttr); + _FillLine(targetRow, initialChars, initialAttr); + + Log::Comment(L"Write additional content into a line of text with IRM mode disabled."); + + // Set the cursor position partway through the target row. + VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow }, true)); + // Disable Insert/Replace mode. + stateMachine.ProcessString(L"\033[4l"); + // Write out some new content. + stateMachine.ProcessString(newChars); + + VERIFY_IS_TRUE(_ValidateLineContains({ 0, targetRow }, L"ABCDEFGHIJ", initialAttr), + L"First half of the line should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol, targetRow }, newChars, newAttr), + L"New content should be added at the cursor position with active attributes."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 5, targetRow }, L"PQRST", initialAttr), + L"Second half of the line should have been partially overwritten."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 25, targetRow }, L'*', initialAttr), + L"With the remainder of the line filled with asterisks."); + VERIFY_IS_TRUE(_ValidateLineContains(targetRow + 1, bufferChar, bufferAttr), + L"The following line should be unaffected."); +} + void ScreenBufferTests::InsertChars() { BEGIN_TEST_METHOD_PROPERTIES() diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index e621398b859..b20adf4bfc4 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -395,6 +395,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes enum ModeParams : VTInt { + IRM_InsertReplaceMode = ANSIStandardMode(4), DECCKM_CursorKeysMode = DECPrivateMode(1), DECANM_AnsiMode = DECPrivateMode(2), DECCOLM_SetNumberOfColumns = DECPrivateMode(3), diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 3129b94ca49..4898a3cd942 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -89,8 +89,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool PushGraphicsRendition(const VTParameters options) = 0; // XTPUSHSGR virtual bool PopGraphicsRendition() = 0; // XTPOPSGR - virtual bool SetMode(const DispatchTypes::ModeParams param) = 0; // DECSET - virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // DECRST + virtual bool SetMode(const DispatchTypes::ModeParams param) = 0; // SM, DECSET + virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // RM, DECRST virtual bool RequestMode(const DispatchTypes::ModeParams param) = 0; // DECRQM virtual bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) = 0; // DSR diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 40edfe1cf96..3990fbdccaa 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -102,6 +102,20 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) } const OutputCellIterator it(std::wstring_view{ stringPosition, string.cend() }, attributes); + if (_modes.test(Mode::InsertReplace)) + { + // If insert-replace mode is enabled, we first measure how many cells + // the string will occupy, and scroll the target area right by that + // amount to make space for the incoming text. + auto measureIt = it; + while (measureIt && measureIt.GetCellDistance(it) < lineWidth) + { + measureIt++; + } + const auto row = cursorPosition.y; + const auto cellCount = measureIt.GetCellDistance(it); + _ScrollRectHorizontally(textBuffer, { cursorPosition.x, row, lineWidth, row + 1 }, cellCount); + } const auto itEnd = textBuffer.WriteLine(it, cursorPosition, wrapAtEOL, lineWidth - 1); if (itEnd.GetInputDistance(it) == 0) @@ -1555,7 +1569,7 @@ bool AdaptDispatch::_PassThroughInputModes() } // Routine Description: -// - Support routine for routing private mode parameters to be set/reset as flags +// - Support routine for routing mode parameters to be set/reset as flags // Arguments: // - param - mode parameter to set/reset // - enable - True for set, false for unset. @@ -1565,6 +1579,9 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con { switch (param) { + case DispatchTypes::ModeParams::IRM_InsertReplaceMode: + _modes.set(Mode::InsertReplace, enable); + return true; case DispatchTypes::ModeParams::DECCKM_CursorKeysMode: _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, enable); return !_PassThroughInputModes(); @@ -1647,7 +1664,7 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con } // Routine Description: -// - DECSET - Enables the given DEC private mode params. +// - SM/DECSET - Enables the given mode parameter (both ANSI and private). // Arguments: // - param - mode parameter to set // Return Value: @@ -1658,7 +1675,7 @@ bool AdaptDispatch::SetMode(const DispatchTypes::ModeParams param) } // Routine Description: -// - DECRST - Disables the given DEC private mode params. +// - RM/DECRST - Disables the given mode parameter (both ANSI and private). // Arguments: // - param - mode parameter to reset // Return Value: @@ -1681,6 +1698,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) switch (param) { + case DispatchTypes::ModeParams::IRM_InsertReplaceMode: + enabled = _modes.test(Mode::InsertReplace); + break; case DispatchTypes::ModeParams::DECCKM_CursorKeysMode: enabled = _terminalInput.GetInputMode(TerminalInput::Mode::CursorKey); break; @@ -2322,7 +2342,7 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled) // we actually perform. As the appropriate functionality is added to our ANSI support, // we should update this. // X Text cursor enable DECTCEM Cursor enabled. -// Insert/replace IRM Replace mode. +// X Insert/replace IRM Replace mode. // X Origin DECOM Absolute (cursor origin at upper-left of screen.) // X Autowrap DECAWM Autowrap enabled (matches XTerm behavior). // National replacement DECNRCM Multinational set. @@ -2350,7 +2370,7 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled) bool AdaptDispatch::SoftReset() { _api.GetTextBuffer().GetCursor().SetIsVisible(true); // Cursor enabled. - _modes.reset(Mode::Origin); // Absolute cursor addressing. + _modes.reset(Mode::InsertReplace, Mode::Origin); // Replace mode; Absolute cursor addressing. _api.SetAutoWrapMode(true); // Wrap at end of line. _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, false); // Normal characters. _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, false); // Numeric characters. diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 76dbfc05f5b..1a283a1c987 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -82,8 +82,8 @@ namespace Microsoft::Console::VirtualTerminal bool ScrollDown(const VTInt distance) override; // SD bool InsertLine(const VTInt distance) override; // IL bool DeleteLine(const VTInt distance) override; // DL - bool SetMode(const DispatchTypes::ModeParams param) override; // DECSET - bool ResetMode(const DispatchTypes::ModeParams param) override; // DECRST + bool SetMode(const DispatchTypes::ModeParams param) override; // SM, DECSET + bool ResetMode(const DispatchTypes::ModeParams param) override; // RM, DECRST bool RequestMode(const DispatchTypes::ModeParams param) override; // DECRQM bool SetKeypadMode(const bool applicationMode) override; // DECKPAM, DECKPNM bool SetAnsiMode(const bool ansiMode) override; // DECANM @@ -155,6 +155,7 @@ namespace Microsoft::Console::VirtualTerminal private: enum class Mode { + InsertReplace, Origin, Column, AllowDECCOLM, diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 56ab5d1578d..0a3b7a688a7 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -82,8 +82,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool PushGraphicsRendition(const VTParameters /*options*/) override { return false; } // XTPUSHSGR bool PopGraphicsRendition() override { return false; } // XTPOPSGR - bool SetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECSET - bool ResetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRST + bool SetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // SM, DECSET + bool ResetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // RM, DECRST bool RequestMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRQM bool DeviceStatusReport(const DispatchTypes::StatusType /*statusType*/, const VTParameter /*id*/) override { return false; } // DSR diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 48625cd5af7..509a16433ad 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -512,6 +512,12 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete }); TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSEL); break; + case CsiActionCodes::SM_SetMode: + success = parameters.for_each([&](const auto mode) { + return _dispatch->SetMode(DispatchTypes::ANSIStandardMode(mode)); + }); + TermTelemetry::Instance().Log(TermTelemetry::Codes::SM); + break; case CsiActionCodes::DECSET_PrivateModeSet: success = parameters.for_each([&](const auto mode) { return _dispatch->SetMode(DispatchTypes::DECPrivateMode(mode)); @@ -519,6 +525,12 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete //TODO: MSFT:6367459 Add specific logging for each of the DECSET/DECRST codes TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSET); break; + case CsiActionCodes::RM_ResetMode: + success = parameters.for_each([&](const auto mode) { + return _dispatch->ResetMode(DispatchTypes::ANSIStandardMode(mode)); + }); + TermTelemetry::Instance().Log(TermTelemetry::Codes::RM); + break; case CsiActionCodes::DECRST_PrivateModeReset: success = parameters.for_each([&](const auto mode) { return _dispatch->ResetMode(DispatchTypes::DECPrivateMode(mode)); diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index af48cf37922..83088e9a160 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -130,7 +130,9 @@ namespace Microsoft::Console::VirtualTerminal VPR_VerticalPositionRelative = VTID("e"), HVP_HorizontalVerticalPosition = VTID("f"), TBC_TabClear = VTID("g"), + SM_SetMode = VTID("h"), DECSET_PrivateModeSet = VTID("?h"), + RM_ResetMode = VTID("l"), DECRST_PrivateModeReset = VTID("?l"), SGR_SetGraphicsRendition = VTID("m"), DSR_DeviceStatusReport = VTID("n"), diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp index 83318b1719d..56775c27999 100644 --- a/src/terminal/parser/telemetry.cpp +++ b/src/terminal/parser/telemetry.cpp @@ -221,7 +221,9 @@ void TermTelemetry::WriteFinalTraceLog() const TraceLoggingUInt32(_uiTimesUsed[SGR], "SGR"), TraceLoggingUInt32(_uiTimesUsed[DECSC], "DECSC"), TraceLoggingUInt32(_uiTimesUsed[DECRC], "DECRC"), + TraceLoggingUInt32(_uiTimesUsed[SM], "SM"), TraceLoggingUInt32(_uiTimesUsed[DECSET], "DECSET"), + TraceLoggingUInt32(_uiTimesUsed[RM], "RM"), TraceLoggingUInt32(_uiTimesUsed[DECRST], "DECRST"), TraceLoggingUInt32(_uiTimesUsed[DECKPAM], "DECKPAM"), TraceLoggingUInt32(_uiTimesUsed[DECKPNM], "DECKPNM"), diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp index 01eee817e0a..6de52d060e2 100644 --- a/src/terminal/parser/telemetry.hpp +++ b/src/terminal/parser/telemetry.hpp @@ -48,7 +48,9 @@ namespace Microsoft::Console::VirtualTerminal SGR, DECSC, DECRC, + SM, DECSET, + RM, DECRST, DECKPAM, DECKPNM,