From 50951206e4f4f50832c6c9c7f6f3cf9c7076cb97 Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Sat, 17 Apr 2021 00:26:28 +0800 Subject: [PATCH] Initial Implementation for tab stops in TerminalDispatch (#9597) * [x] Supports #1883 * [X] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA * [X] Tests added/passed --- src/cascadia/TerminalCore/ITerminalApi.hpp | 2 + src/cascadia/TerminalCore/Terminal.hpp | 1 + src/cascadia/TerminalCore/TerminalApi.cpp | 5 + .../TerminalCore/TerminalDispatch.cpp | 114 +++++- .../TerminalCore/TerminalDispatch.hpp | 13 + .../TerminalBufferTests.cpp | 359 ++++++++++++++++++ 6 files changed, 492 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalCore/ITerminalApi.hpp b/src/cascadia/TerminalCore/ITerminalApi.hpp index bc1c63fd89a7..2bb6376a9ab0 100644 --- a/src/cascadia/TerminalCore/ITerminalApi.hpp +++ b/src/cascadia/TerminalCore/ITerminalApi.hpp @@ -4,6 +4,7 @@ #include "../../terminal/adapter/DispatchTypes.hpp" #include "../../buffer/out/TextAttribute.hpp" +#include "../../types/inc/Viewport.hpp" namespace Microsoft::Terminal::Core { @@ -22,6 +23,7 @@ namespace Microsoft::Terminal::Core virtual TextAttribute GetTextAttributes() const noexcept = 0; virtual void SetTextAttributes(const TextAttribute& attrs) noexcept = 0; + virtual Microsoft::Console::Types::Viewport GetBufferSize() noexcept = 0; virtual bool SetCursorPosition(short x, short y) noexcept = 0; virtual COORD GetCursorPosition() noexcept = 0; virtual bool SetCursorVisibility(const bool visible) noexcept = 0; diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 03f5be583606..f38f94f51456 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -86,6 +86,7 @@ class Microsoft::Terminal::Core::Terminal final : bool ExecuteChar(wchar_t wch) noexcept override; TextAttribute GetTextAttributes() const noexcept override; void SetTextAttributes(const TextAttribute& attrs) noexcept override; + Microsoft::Console::Types::Viewport GetBufferSize() noexcept override; bool SetCursorPosition(short x, short y) noexcept override; COORD GetCursorPosition() noexcept override; bool SetCursorVisibility(const bool visible) noexcept override; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index aaf61778dbf7..891e251ff683 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -36,6 +36,11 @@ void Terminal::SetTextAttributes(const TextAttribute& attrs) noexcept _buffer->SetCurrentAttributes(attrs); } +Viewport Terminal::GetBufferSize() noexcept +{ + return _buffer->GetSize(); +} + bool Terminal::SetCursorPosition(short x, short y) noexcept try { diff --git a/src/cascadia/TerminalCore/TerminalDispatch.cpp b/src/cascadia/TerminalCore/TerminalDispatch.cpp index a8a739795198..338c6b2eaff3 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.cpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.cpp @@ -134,6 +134,74 @@ try } CATCH_LOG_RETURN_FALSE() +bool TerminalDispatch::HorizontalTabSet() noexcept +{ + const auto width = _terminalApi.GetBufferSize().Dimensions().X; + const auto column = _terminalApi.GetCursorPosition().X; + + _InitTabStopsForWidth(width); + _tabStopColumns.at(column) = true; + return true; +} + +bool TerminalDispatch::ForwardTab(const size_t numTabs) noexcept +{ + const auto width = _terminalApi.GetBufferSize().Dimensions().X; + const auto cursorPosition = _terminalApi.GetCursorPosition(); + auto column = cursorPosition.X; + const auto row = cursorPosition.Y; + auto tabsPerformed = 0u; + _InitTabStopsForWidth(width); + while (column + 1 < width && tabsPerformed < numTabs) + { + column++; + if (til::at(_tabStopColumns, column)) + { + tabsPerformed++; + } + } + + return _terminalApi.SetCursorPosition(column, row); +} + +bool TerminalDispatch::BackwardsTab(const size_t numTabs) noexcept +{ + const auto width = _terminalApi.GetBufferSize().Dimensions().X; + const auto cursorPosition = _terminalApi.GetCursorPosition(); + auto column = cursorPosition.X; + const auto row = cursorPosition.Y; + auto tabsPerformed = 0u; + _InitTabStopsForWidth(width); + while (column > 0 && tabsPerformed < numTabs) + { + column--; + if (til::at(_tabStopColumns, column)) + { + tabsPerformed++; + } + } + + return _terminalApi.SetCursorPosition(column, row); +} + +bool TerminalDispatch::TabClear(const DispatchTypes::TabClearType clearType) noexcept +{ + bool success = false; + switch (clearType) + { + case DispatchTypes::TabClearType::ClearCurrentColumn: + success = _ClearSingleTabStop(); + break; + case DispatchTypes::TabClearType::ClearAllColumns: + success = _ClearAllTabStops(); + break; + default: + success = false; + break; + } + return success; +} + // Method Description: // - Sets a single entry of the colortable to a new value // Arguments: @@ -550,6 +618,48 @@ bool TerminalDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, return success; } +bool TerminalDispatch::_ClearSingleTabStop() noexcept +{ + const auto width = _terminalApi.GetBufferSize().Dimensions().X; + const auto column = _terminalApi.GetCursorPosition().X; + + _InitTabStopsForWidth(width); + _tabStopColumns.at(column) = false; + return true; +} + +bool TerminalDispatch::_ClearAllTabStops() noexcept +{ + _tabStopColumns.clear(); + _initDefaultTabStops = false; + return true; +} + +void TerminalDispatch::_ResetTabStops() noexcept +{ + _tabStopColumns.clear(); + _initDefaultTabStops = true; +} + +void TerminalDispatch::_InitTabStopsForWidth(const size_t width) +{ + const auto initialWidth = _tabStopColumns.size(); + if (width > initialWidth) + { + _tabStopColumns.resize(width); + if (_initDefaultTabStops) + { + for (auto column = 8u; column < _tabStopColumns.size(); column += 8) + { + if (column >= initialWidth) + { + til::at(_tabStopColumns, column) = true; + } + } + } + } +} + bool TerminalDispatch::SoftReset() noexcept { // TODO:GH#1883 much of this method is not yet implemented in the Terminal, @@ -623,8 +733,8 @@ bool TerminalDispatch::HardReset() noexcept // Cursor to 1,1 - the Soft Reset guarantees this is absolute success = CursorPosition(1, 1) && success; - // // Delete all current tab stops and reapply - // _ResetTabStops(); + // Delete all current tab stops and reapply + _ResetTabStops(); return success; } diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp index d01beae80810..7c0d14b9ce79 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.hpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp @@ -40,6 +40,11 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc bool CarriageReturn() noexcept override; bool SetWindowTitle(std::wstring_view title) noexcept override; + bool HorizontalTabSet() noexcept override; // HTS + bool ForwardTab(const size_t numTabs) noexcept override; // CHT, HT + bool BackwardsTab(const size_t numTabs) noexcept override; // CBT + bool TabClear(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TabClearType clearType) noexcept override; // TBC + bool SetColorTableEntry(const size_t tableIndex, const DWORD color) noexcept override; bool SetCursorStyle(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::CursorStyle cursorStyle) noexcept override; bool SetCursorColor(const DWORD color) noexcept override; @@ -79,9 +84,17 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc private: ::Microsoft::Terminal::Core::ITerminalApi& _terminalApi; + std::vector _tabStopColumns; + bool _initDefaultTabStops = true; + size_t _SetRgbColorsHelper(const ::Microsoft::Console::VirtualTerminal::VTParameters options, TextAttribute& attr, const bool isForeground) noexcept; bool _ModeParamsHelper(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::ModeParams param, const bool enable) noexcept; + + bool _ClearSingleTabStop() noexcept; + bool _ClearAllTabStops() noexcept; + void _ResetTabStops() noexcept; + void _InitTabStopsForWidth(const size_t width); }; diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp index 974eb813d7e5..e32c197d3fef 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp @@ -41,6 +41,16 @@ class TerminalCoreUnitTests::TerminalBufferTests final TEST_METHOD(DontSnapToOutputTest); + TEST_METHOD(TestResetClearTabStops); + + TEST_METHOD(TestAddTabStop); + + TEST_METHOD(TestClearTabStop); + + TEST_METHOD(TestGetForwardTab); + + TEST_METHOD(TestGetReverseTab); + TEST_METHOD_SETUP(MethodSetup) { // STEP 1: Set up the Terminal @@ -56,6 +66,9 @@ class TerminalCoreUnitTests::TerminalBufferTests final } private: + void _SetTabStops(std::list columns, bool replace); + std::list _GetTabStops(); + DummyRenderTarget emptyRT; std::unique_ptr term; }; @@ -233,3 +246,349 @@ void TerminalBufferTests::DontSnapToOutputTest() VERIFY_ARE_EQUAL(TerminalViewHeight, seventhView.BottomExclusive()); VERIFY_ARE_EQUAL(TerminalHistoryLength, term->_scrollOffset); } + +void TerminalBufferTests::_SetTabStops(std::list columns, bool replace) +{ + auto& termTb = *term->_buffer; + auto& termSm = *term->_stateMachine; + auto& cursor = termTb.GetCursor(); + + const auto clearTabStops = L"\033[3g"; + const auto addTabStop = L"\033H"; + + if (replace) + { + termSm.ProcessString(clearTabStops); + } + + for (auto column : columns) + { + cursor.SetXPosition(column); + termSm.ProcessString(addTabStop); + } +} + +std::list TerminalBufferTests::_GetTabStops() +{ + std::list columns; + auto& termTb = *term->_buffer; + auto& termSm = *term->_stateMachine; + const auto initialView = term->GetViewport(); + const auto lastColumn = initialView.RightInclusive(); + auto& cursor = termTb.GetCursor(); + + cursor.SetPosition({ 0, 0 }); + for (;;) + { + termSm.ProcessCharacter(L'\t'); + auto column = cursor.GetPosition().X; + if (column >= lastColumn) + { + break; + } + columns.push_back(column); + } + + return columns; +} + +void TerminalBufferTests::TestResetClearTabStops() +{ + auto& termSm = *term->_stateMachine; + const auto initialView = term->GetViewport(); + + const auto clearTabStops = L"\033[3g"; + const auto resetToInitialState = L"\033c"; + + Log::Comment(L"Default tabs every 8 columns."); + std::list expectedStops{ 8, 16, 24, 32, 40, 48, 56, 64, 72 }; + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); + + Log::Comment(L"Clear all tabs."); + termSm.ProcessString(clearTabStops); + expectedStops = {}; + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); + + Log::Comment(L"RIS resets tabs to defaults."); + termSm.ProcessString(resetToInitialState); + expectedStops = { 8, 16, 24, 32, 40, 48, 56, 64, 72 }; + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); +} + +void TerminalBufferTests::TestAddTabStop() +{ + auto& termTb = *term->_buffer; + auto& termSm = *term->_stateMachine; + auto& cursor = termTb.GetCursor(); + + const auto clearTabStops = L"\033[3g"; + const auto addTabStop = L"\033H"; + + Log::Comment(L"Clear all tabs."); + termSm.ProcessString(clearTabStops); + std::list expectedStops{}; + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); + + Log::Comment(L"Add tab to empty list."); + cursor.SetXPosition(12); + termSm.ProcessString(addTabStop); + expectedStops.push_back(12); + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); + + Log::Comment(L"Add tab to head of existing list."); + cursor.SetXPosition(4); + termSm.ProcessString(addTabStop); + expectedStops.push_front(4); + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); + + Log::Comment(L"Add tab to tail of existing list."); + cursor.SetXPosition(30); + termSm.ProcessString(addTabStop); + expectedStops.push_back(30); + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); + + Log::Comment(L"Add tab to middle of existing list."); + cursor.SetXPosition(24); + termSm.ProcessString(addTabStop); + expectedStops.push_back(24); + expectedStops.sort(); + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); + + Log::Comment(L"Add tab that duplicates an item in the existing list."); + cursor.SetXPosition(24); + termSm.ProcessString(addTabStop); + VERIFY_ARE_EQUAL(expectedStops, _GetTabStops()); +} + +void TerminalBufferTests::TestClearTabStop() +{ + auto& termTb = *term->_buffer; + auto& termSm = *term->_stateMachine; + auto& cursor = termTb.GetCursor(); + + const auto clearTabStops = L"\033[3g"; + const auto clearTabStop = L"\033[0g"; + const auto addTabStop = L"\033H"; + + Log::Comment(L"Start with all tabs cleared."); + { + termSm.ProcessString(clearTabStops); + + VERIFY_IS_TRUE(_GetTabStops().empty()); + } + + Log::Comment(L"Try to clear nonexistent list."); + { + cursor.SetXPosition(0); + termSm.ProcessString(clearTabStop); + + VERIFY_IS_TRUE(_GetTabStops().empty(), L"List should remain empty"); + } + + Log::Comment(L"Allocate 1 list item and clear it."); + { + cursor.SetXPosition(0); + termSm.ProcessString(addTabStop); + termSm.ProcessString(clearTabStop); + + VERIFY_IS_TRUE(_GetTabStops().empty()); + } + + Log::Comment(L"Allocate 1 list item and clear nonexistent."); + { + cursor.SetXPosition(1); + termSm.ProcessString(addTabStop); + + Log::Comment(L"Free greater"); + cursor.SetXPosition(2); + termSm.ProcessString(clearTabStop); + VERIFY_IS_FALSE(_GetTabStops().empty()); + + Log::Comment(L"Free less than"); + cursor.SetXPosition(0); + termSm.ProcessString(clearTabStop); + VERIFY_IS_FALSE(_GetTabStops().empty()); + + // clear all tab stops + termSm.ProcessString(clearTabStops); + } + + Log::Comment(L"Allocate many (5) list items and clear head."); + { + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + _SetTabStops(inputData, false); + cursor.SetXPosition(inputData.front()); + termSm.ProcessString(clearTabStop); + + inputData.pop_front(); + VERIFY_ARE_EQUAL(inputData, _GetTabStops()); + + // clear all tab stops + termSm.ProcessString(clearTabStops); + } + + Log::Comment(L"Allocate many (5) list items and clear middle."); + { + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + _SetTabStops(inputData, false); + cursor.SetXPosition(*std::next(inputData.begin())); + termSm.ProcessString(clearTabStop); + + inputData.erase(std::next(inputData.begin())); + VERIFY_ARE_EQUAL(inputData, _GetTabStops()); + + // clear all tab stops + termSm.ProcessString(clearTabStops); + } + + Log::Comment(L"Allocate many (5) list items and clear tail."); + { + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + _SetTabStops(inputData, false); + cursor.SetXPosition(inputData.back()); + termSm.ProcessString(clearTabStop); + + inputData.pop_back(); + VERIFY_ARE_EQUAL(inputData, _GetTabStops()); + + // clear all tab stops + termSm.ProcessString(clearTabStops); + } + + Log::Comment(L"Allocate many (5) list items and clear nonexistent item."); + { + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + _SetTabStops(inputData, false); + cursor.SetXPosition(0); + termSm.ProcessString(clearTabStop); + + VERIFY_ARE_EQUAL(inputData, _GetTabStops()); + + // clear all tab stops + termSm.ProcessString(clearTabStops); + } +} + +void TerminalBufferTests::TestGetForwardTab() +{ + auto& termTb = *term->_buffer; + auto& termSm = *term->_stateMachine; + const auto initialView = term->GetViewport(); + auto& cursor = termTb.GetCursor(); + + const auto nextForwardTab = L"\033[I"; + + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + _SetTabStops(inputData, true); + + const COORD coordScreenBufferSize = initialView.Dimensions(); + + Log::Comment(L"Find next tab from before front."); + { + cursor.SetXPosition(0); + + COORD coordCursorExpected = cursor.GetPosition(); + coordCursorExpected.X = inputData.front(); + + termSm.ProcessString(nextForwardTab); + COORD const coordCursorResult = cursor.GetPosition(); + 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."); + { + cursor.SetXPosition(6); + + COORD coordCursorExpected = cursor.GetPosition(); + coordCursorExpected.X = *std::next(inputData.begin(), 3); + + termSm.ProcessString(nextForwardTab); + COORD const coordCursorResult = cursor.GetPosition(); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor advanced to middle tab stop from sample list."); + } + + Log::Comment(L"Find next tab from end."); + { + cursor.SetXPosition(30); + + COORD coordCursorExpected = cursor.GetPosition(); + coordCursorExpected.X = coordScreenBufferSize.X - 1; + + termSm.ProcessString(nextForwardTab); + COORD const coordCursorResult = cursor.GetPosition(); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor advanced to end of screen buffer."); + } + + Log::Comment(L"Find next tab from rightmost column."); + { + cursor.SetXPosition(coordScreenBufferSize.X - 1); + + COORD coordCursorExpected = cursor.GetPosition(); + + termSm.ProcessString(nextForwardTab); + COORD const coordCursorResult = cursor.GetPosition(); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor remains in rightmost column."); + } +} + +void TerminalBufferTests::TestGetReverseTab() +{ + auto& termTb = *term->_buffer; + auto& termSm = *term->_stateMachine; + auto& cursor = termTb.GetCursor(); + + const auto nextReverseTab = L"\033[Z"; + + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + _SetTabStops(inputData, true); + + Log::Comment(L"Find previous tab from before front."); + { + cursor.SetXPosition(1); + + COORD coordCursorExpected = cursor.GetPosition(); + coordCursorExpected.X = 0; + + termSm.ProcessString(nextReverseTab); + COORD const coordCursorResult = cursor.GetPosition(); + 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."); + { + cursor.SetXPosition(6); + + COORD coordCursorExpected = cursor.GetPosition(); + coordCursorExpected.X = *std::next(inputData.begin()); + + termSm.ProcessString(nextReverseTab); + COORD const coordCursorResult = cursor.GetPosition(); + 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."); + { + cursor.SetXPosition(30); + + COORD coordCursorExpected = cursor.GetPosition(); + coordCursorExpected.X = inputData.back(); + + termSm.ProcessString(nextReverseTab); + COORD const coordCursorResult = cursor.GetPosition(); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor adjusted to last item in the sample list from position beyond end."); + } +}