diff --git a/cmake/PedanticCompiler.cmake b/cmake/PedanticCompiler.cmake index ecf2d63011..983db970dd 100644 --- a/cmake/PedanticCompiler.cmake +++ b/cmake/PedanticCompiler.cmake @@ -56,7 +56,11 @@ else() endif() if(${PEDANTIC_COMPILER_WERROR}) - try_add_compile_options(-Werror) # XXX Not yet, but hopefully soon. + try_add_compile_options(-Werror) + + # Don't complain here. That's needed for bitpacking (codepoint_properties) in libunicode dependency. + try_add_compile_options(-Wno-error=c++20-extensions) + try_add_compile_options(-Wno-c++20-extensions) # Not sure how to work around these. try_add_compile_options(-Wno-error=class-memaccess) diff --git a/metainfo.xml b/metainfo.xml index aa1563156f..92a4b85a97 100644 --- a/metainfo.xml +++ b/metainfo.xml @@ -111,6 +111,7 @@
  • Removes `images.sixel_cursor_conformance` config option.
  • Adds VT sequence DECSCA, DECSEL, DECSED and DECSERA to support protected grid areas during erase operations (#29, #30, #31).
  • Improve Input Method (IME) handling, visualizing preedit-text.
  • +
  • Improve throughput performance of arbitrary complex Unicode.
  • Update Unicode data to version 15.0.0 (release). See Announcing The UnicodeĀ® Standard, Version 15.0.0.
  • Fixes cursor highlight in VI mode
  • diff --git a/scripts/install-deps.ps1 b/scripts/install-deps.ps1 index b7bdd5f032..11900ebf70 100755 --- a/scripts/install-deps.ps1 +++ b/scripts/install-deps.ps1 @@ -26,9 +26,9 @@ $ThirdParties = Macro = "" }; [ThirdParty]@{ - Folder = "libunicode-44969c6d80e44a4731b584d047ba108769926835"; - Archive = "libunicode-44969c6d80e44a4731b584d047ba108769926835.zip"; - URI = "https://github.com/contour-terminal/libunicode/archive/44969c6d80e44a4731b584d047ba108769926835.zip"; + Folder = "libunicode-be83e5052f6e7590c244faad59be16af210db90b"; + Archive = "libunicode-be83e5052f6e7590c244faad59be16af210db90b.zip"; + URI = "https://github.com/contour-terminal/libunicode/archive/be83e5052f6e7590c244faad59be16af210db90b.zip"; Macro = "libunicode" }; [ThirdParty]@{ diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh index 1db9848b73..4b15a8ca46 100755 --- a/scripts/install-deps.sh +++ b/scripts/install-deps.sh @@ -102,12 +102,20 @@ fetch_and_unpack_embeds() https://github.com/contour-terminal/termbench-pro/archive/$termbench_pro_git_sha.tar.gz \ termbench_pro - local libunicode_git_sha="44969c6d80e44a4731b584d047ba108769926835" - fetch_and_unpack \ - libunicode-$libunicode_git_sha \ - libunicode-$libunicode_git_sha.tar.gz \ - https://github.com/contour-terminal/libunicode/archive/$libunicode_git_sha.tar.gz \ - libunicode + if test x$LIBUNICODE_SRC_DIR = x; then + local libunicode_git_sha="be83e5052f6e7590c244faad59be16af210db90b" + fetch_and_unpack \ + libunicode-$libunicode_git_sha \ + libunicode-$libunicode_git_sha.tar.gz \ + https://github.com/contour-terminal/libunicode/archive/$libunicode_git_sha.tar.gz \ + libunicode + else + echo "Hard linking external libunicode source directory to: $LIBUNICODE_SRC_DIR" + MACRO="libunicode" + echo "macro(ContourThirdParties_Embed_$MACRO)" >> $SYSDEPS_CMAKE_FILE + echo " add_subdirectory($LIBUNICODE_SRC_DIR libunicode EXCLUDE_FROM_ALL)" >> $SYSDEPS_CMAKE_FILE + echo "endmacro()" >> $SYSDEPS_CMAKE_FILE + fi } fetch_and_unpack_yaml_cpp() diff --git a/src/terminal/Line.cpp b/src/terminal/Line.cpp index f25cd1a04b..35326228b0 100644 --- a/src/terminal/Line.cpp +++ b/src/terminal/Line.cpp @@ -154,10 +154,10 @@ InflatedLineBuffer inflate(TrivialLineBuffer const& input) auto columns = InflatedLineBuffer {}; columns.reserve(unbox(input.displayWidth)); - // fmt::print("Inflating {}/{}\n", input.text.size(), input.displayWidth); auto lastChar = char32_t { 0 }; auto utf8DecoderState = unicode::utf8_decoder_state {}; + auto gapPending = 0; for (char const ch: input.text.view()) { @@ -173,10 +173,16 @@ InflatedLineBuffer inflate(TrivialLineBuffer const& input) if (!lastChar || isAsciiBreakable || unicode::grapheme_segmenter::breakable(lastChar, nextChar)) { + while (gapPending > 0) + { + columns.emplace_back(Cell { input.textAttributes, input.hyperlink }); + --gapPending; + } + auto const charWidth = unicode::width(nextChar); columns.emplace_back(Cell {}); columns.back().setHyperlink(input.hyperlink); - columns.back().write( - input.textAttributes, nextChar, static_cast(unicode::width(nextChar))); + columns.back().write(input.textAttributes, nextChar, static_cast(charWidth)); + gapPending = charWidth - 1; } else { @@ -193,7 +199,15 @@ InflatedLineBuffer inflate(TrivialLineBuffer const& input) } } } + lastChar = nextChar; + } + + while (gapPending > 0) + { + columns.emplace_back(Cell { input.textAttributes, input.hyperlink }); + --gapPending; } + assert(columns.size() == unbox(input.usedColumns)); while (columns.size() < unbox(input.displayWidth)) diff --git a/src/terminal/Line_test.cpp b/src/terminal/Line_test.cpp index f495ea4cdf..0070ffca16 100644 --- a/src/terminal/Line_test.cpp +++ b/src/terminal/Line_test.cpp @@ -66,3 +66,110 @@ TEST_CASE("Line.inflate", "[Line]") CHECK(char(cell.codepoint(0)) == testText[i]); } } + +TEST_CASE("Line.inflate.Unicode", "[Line]") +{ + auto constexpr DisplayWidth = ColumnCount(10); + auto constexpr testTextUtf32 = U"0\u2705123456789ABCDEF"sv; + auto const testTextUtf8 = unicode::convert_to(testTextUtf32); + + auto pool = BufferObjectPool(32); + auto bufferObject = pool.allocateBufferObject(); + bufferObject->writeAtEnd(testTextUtf8); + + // Buffer fragment containing 9 codepoints, with one of them using display width of 2. + auto const bufferFragment = bufferObject->ref(0, 11); + + auto sgr = GraphicsAttributes {}; + sgr.foregroundColor = RGBColor(0x123456); + sgr.backgroundColor = Color::Indexed(IndexedColor::Yellow); + sgr.underlineColor = Color::Indexed(IndexedColor::Red); + sgr.flags |= CellFlags::CurlyUnderlined; + auto const trivial = + TrivialLineBuffer { DisplayWidth, sgr, sgr, HyperlinkId {}, DisplayWidth, bufferFragment }; + + auto const inflated = inflate(trivial); + + CHECK(inflated.size() == unbox(DisplayWidth)); + for (size_t i = 0, k = 0; i < inflated.size();) + { + auto const& cell = inflated[i]; + INFO(fmt::format("column {}, k {}, codepoint U+{:X}", i, k, (unsigned) cell.codepoint(0))); + REQUIRE(cell.codepointCount() == 1); + REQUIRE(cell.codepoint(0) == testTextUtf32[k]); + REQUIRE(cell.foregroundColor() == sgr.foregroundColor); + REQUIRE(cell.backgroundColor() == sgr.backgroundColor); + REQUIRE(cell.underlineColor() == sgr.underlineColor); + for (int n = 1; n < cell.width(); ++n) + { + INFO(fmt::format("column.sub: {}\n", n)); + auto const& fillCell = inflated.at(i + static_cast(n)); + REQUIRE(fillCell.codepointCount() == 0); + REQUIRE(fillCell.foregroundColor() == sgr.foregroundColor); + REQUIRE(fillCell.backgroundColor() == sgr.backgroundColor); + REQUIRE(fillCell.underlineColor() == sgr.underlineColor); + } + i += cell.width(); + k++; + } +} + +TEST_CASE("Line.inflate.Unicode.FamilyEmoji", "[Line]") +{ + // Ensure inflate() is also working for reaaally complex Unicode grapheme clusters. + + auto constexpr DisplayWidth = ColumnCount(5); + auto constexpr UsedColumnCount = ColumnCount(4); + auto constexpr testTextUtf32 = U"A\U0001F468\u200D\U0001F468\u200D\U0001F467B"sv; + auto const testTextUtf8 = unicode::convert_to(testTextUtf32); + auto const familyEmojiUtf8 = unicode::convert_to(U"\U0001F468\u200D\U0001F468\u200D\U0001F467"sv); + + auto pool = BufferObjectPool(32); + auto bufferObject = pool.allocateBufferObject(); + bufferObject->writeAtEnd(testTextUtf8); + + auto const bufferFragment = bufferObject->ref(0, testTextUtf8.size()); + + auto sgr = GraphicsAttributes {}; + sgr.foregroundColor = RGBColor(0x123456); + sgr.backgroundColor = Color::Indexed(IndexedColor::Yellow); + sgr.underlineColor = Color::Indexed(IndexedColor::Red); + sgr.flags |= CellFlags::CurlyUnderlined; + + auto fillSGR = GraphicsAttributes {}; + fillSGR.foregroundColor = RGBColor(0x123456); + fillSGR.backgroundColor = Color::Indexed(IndexedColor::Yellow); + fillSGR.underlineColor = Color::Indexed(IndexedColor::Red); + fillSGR.flags |= CellFlags::CurlyUnderlined; + + auto const trivial = + TrivialLineBuffer { DisplayWidth, sgr, fillSGR, HyperlinkId {}, UsedColumnCount, bufferFragment }; + + auto const inflated = inflate(trivial); + + CHECK(inflated.size() == unbox(DisplayWidth)); + + // Check text in 0..3 + // Check @4 is empty text. + // Check 0..3 has same SGR. + // Check @4 has fill-SGR. + + REQUIRE(inflated[0].toUtf8() == "A"); + REQUIRE(inflated[1].toUtf8() == familyEmojiUtf8); + REQUIRE(inflated[2].toUtf8() == ""); + REQUIRE(inflated[3].toUtf8() == "B"); + REQUIRE(inflated[4].toUtf8() == ""); + + for (auto const i: { 0u, 2u, 1u, 3u }) + { + auto const& cell = inflated[i]; + REQUIRE(cell.foregroundColor() == sgr.foregroundColor); + REQUIRE(cell.backgroundColor() == sgr.backgroundColor); + REQUIRE(cell.underlineColor() == sgr.underlineColor); + } + + auto const& cell = inflated[4]; + REQUIRE(cell.foregroundColor() == fillSGR.foregroundColor); + REQUIRE(cell.backgroundColor() == fillSGR.backgroundColor); + REQUIRE(cell.underlineColor() == fillSGR.underlineColor); +} diff --git a/src/terminal/Parser.cpp b/src/terminal/Parser.cpp index 17ab982c56..795d070cf4 100644 --- a/src/terminal/Parser.cpp +++ b/src/terminal/Parser.cpp @@ -354,43 +354,49 @@ void Parser::parseFragment(std::string_view co auto input = _data.data(); auto const end = _data.data() + _data.size(); - do + while (input != end) { - if (state_ == State::Ground) + if (state_ == State::Ground && eventListener_.acceptsBulkText()) { auto const chunk = std::string_view(input, static_cast(std::distance(input, end))); + auto const [cellCount, next, subStart, subEnd] = + unicode::scan_for_text(scanState_, chunk, maxCharCount); - if (auto const cellCount = unicode::scan_for_text_ascii(chunk, maxCharCount); cellCount > 0) + if (next != input) { - auto const next = input + cellCount; - auto const byteCount = static_cast(std::distance(input, next)); - precedingGraphicCharacter = static_cast(input[cellCount - 1]); - assert(byteCount <= chunk.size()); + // We do not test on cellCount>0 because the scan could contain only a ZWJ (zero width + // joiner), and that would be misleading. + + assert(subStart <= subEnd); + auto const byteCount = static_cast(std::distance(subStart, subEnd)); assert(cellCount <= maxCharCount); + assert(subEnd <= chunk.data() + chunk.size()); assert(next <= chunk.data() + chunk.size()); #if defined(LIBTERMINAL_LOG_TRACE) if (VTTraceParserLog) - VTTraceParserLog()( - "[{}] Scanned text: cap {}; available cells {}; chars {}; bytes {}; \"{}\"", - "US-ASCII", - chunk.size(), - maxCharCount, - cellCount, - byteCount, - crispy::escape(std::string_view { input, byteCount })); + VTTraceParserLog()("[Unicode] Scanned text: {}/{} cells; \"{}\"", + cellCount, + maxCharCount, + crispy::escape(std::string_view { input, byteCount })); #endif - auto const text = std::string_view { input, byteCount }; - if (utf8DecoderState_.expectedLength == 0) + auto const text = std::string_view { subStart, byteCount }; + if (scanState_.utf8.expectedLength == 0) { - maxCharCount = eventListener_.print(text, cellCount); - precedingGraphicCharacter = static_cast(text.back()); + if (!text.empty()) + { + maxCharCount = eventListener_.print(text, cellCount); + } } else { - for (char const ch: text) - printUtf8Byte(ch); + // fmt::print("Parser.text: incomplete UTF-8 sequence at end: {}/{}\n", + // scanState_.utf8.currentLength, + // scanState_.utf8.expectedLength); + + // for (char const ch: text) + // printUtf8Byte(ch); } input = next; @@ -429,13 +435,13 @@ void Parser::parseFragment(std::string_view co state_, ch, static_cast(ch))); - } while (input != end); + } } template void Parser::printUtf8Byte(char ch) { - unicode::ConvertResult const r = unicode::from_utf8(utf8DecoderState_, (uint8_t) ch); + unicode::ConvertResult const r = unicode::from_utf8(scanState_.utf8, (uint8_t) ch); if (std::holds_alternative(r)) return; @@ -443,7 +449,7 @@ void Parser::printUtf8Byte(char ch) auto const codepoint = std::holds_alternative(r) ? std::get(r).value : ReplacementCharacter; eventListener_.print(codepoint); - precedingGraphicCharacter = codepoint; + scanState_.lastCodepointHint = codepoint; } template @@ -466,7 +472,7 @@ void Parser::handle(ActionClass _actionClass, switch (_action) { - case Action::GroundStart: precedingGraphicCharacter = 0; break; + case Action::GroundStart: scanState_.lastCodepointHint = 0; break; case Action::Clear: eventListener_.clear(); break; case Action::CollectLeader: eventListener_.collectLeader(ch); break; case Action::Collect: eventListener_.collect(ch); break; diff --git a/src/terminal/Parser.h b/src/terminal/Parser.h index 5f1dd2739c..35822e7685 100644 --- a/src/terminal/Parser.h +++ b/src/terminal/Parser.h @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -554,17 +555,18 @@ class Parser [[nodiscard]] State state() const noexcept { return state_; } - char32_t precedingGraphicCharacter = 0; + [[nodiscard]] char32_t precedingGraphicCharacter() const noexcept { return scanState_.lastCodepointHint; } + + void printUtf8Byte(char ch); private: void handle(ActionClass _actionClass, Action _action, uint8_t _char); - void printUtf8Byte(char ch); // private properties // State state_ = State::Ground; EventListener& eventListener_; - unicode::utf8_decoder_state utf8DecoderState_ = {}; + unicode::scan_state scanState_ {}; }; /// @returns parsed tuple with OSC code and offset to first data parameter byte. diff --git a/src/terminal/ParserEvents.h b/src/terminal/ParserEvents.h index 83f7520833..1d4e671f82 100644 --- a/src/terminal/ParserEvents.h +++ b/src/terminal/ParserEvents.h @@ -48,6 +48,14 @@ class ParserEvents */ virtual size_t print(std::string_view _chars, size_t cellCount) = 0; + /** + * Used to indicate whether or not the print() overload may be used to process bulk text. + * + * There may be situations where it would not be efficient to process bulk text. In such situations, + * simply calling print() per codepoint is sufficient (potentially being more performant). + */ + [[nodiscard]] virtual bool acceptsBulkText() const noexcept = 0; + /** * The C0 or C1 control function should be executed, which may have any one of a variety of * effects, including changing the cursor position, suspending or resuming communications or @@ -168,6 +176,7 @@ class NullParserEvents: public ParserEvents void error(std::string_view const&) override {} void print(char32_t) override {} size_t print(std::string_view, size_t) override { return 0; } + [[nodiscard]] bool acceptsBulkText() const noexcept override { return true; } void execute(char) override {} void clear() override {} void collect(char) override {} diff --git a/src/terminal/Screen.cpp b/src/terminal/Screen.cpp index c47c92baa1..4a6b83908d 100644 --- a/src/terminal/Screen.cpp +++ b/src/terminal/Screen.cpp @@ -292,6 +292,7 @@ string_view Screen::tryEmplaceChars(string_view _chars, size_t cellCount) crlfIfWrapPending(); auto const columnsAvailable = pageSize().columns.value - _state.cursor.position.column.value; + assert(cellCount <= static_cast(columnsAvailable)); if (!_terminal.isModeEnabled(DECMode::AutoWrap) && cellCount > static_cast(columnsAvailable)) // With AutoWrap on, we can only emplace if it fits the line. @@ -301,51 +302,36 @@ string_view Screen::tryEmplaceChars(string_view _chars, size_t cellCount) { if (currentLine().empty()) { - _chars.remove_prefix(emplaceCharsIntoCurrentLine(_chars, cellCount)); - _chars = tryEmplaceContinuousChars(_chars, cellCount); - _terminal.currentPtyBuffer()->advanceHotEndUntil(_chars.data()); - return _chars; + auto const numberOfBytesEmplaced = emplaceCharsIntoCurrentLine(_chars, cellCount); + _terminal.currentPtyBuffer()->advanceHotEndUntil(_chars.data() + numberOfBytesEmplaced); + _chars.remove_prefix(numberOfBytesEmplaced); + assert(_chars.empty()); } return _chars; } - if (canResumeEmplace(_chars)) + if (isContiguousToCurrentLine(_chars)) { - auto const charsToWrite = static_cast(min(columnsAvailable, static_cast(_chars.size()))); + // We can append the chars to a pre-existing non-empty line. + assert(static_cast(cellCount) <= columnsAvailable); auto& lineBuffer = currentLine().trivialBuffer(); - lineBuffer.text.growBy(charsToWrite); + lineBuffer.text.growBy(_chars.size()); lineBuffer.usedColumns += ColumnCount::cast_from(cellCount); - advanceCursorAfterWrite(ColumnCount::cast_from(charsToWrite)); - _chars.remove_prefix(charsToWrite); - _chars = tryEmplaceContinuousChars(_chars, cellCount); - _terminal.currentPtyBuffer()->advanceHotEndUntil(_chars.data()); + advanceCursorAfterWrite(ColumnCount::cast_from(cellCount)); + _terminal.currentPtyBuffer()->advanceHotEndUntil(_chars.data() + _chars.size()); + _chars.remove_prefix(_chars.size()); return _chars; } return _chars; } -template -string_view Screen::tryEmplaceContinuousChars(string_view _chars, size_t cellCount) noexcept -{ - while (!_chars.empty()) - { - crlf(); - if (!currentLine().empty()) - break; - - _chars.remove_prefix(emplaceCharsIntoCurrentLine(_chars, cellCount)); - } - - return _chars; -} - template size_t Screen::emplaceCharsIntoCurrentLine(string_view _chars, size_t cellCount) noexcept { - auto columnsAvailable = (_state.margin.horizontal.to.value + 1) - _state.cursor.position.column.value; + [[maybe_unused]] auto columnsAvailable = + (_state.margin.horizontal.to.value + 1) - _state.cursor.position.column.value; assert(cellCount <= static_cast(columnsAvailable)); - auto const charsToWrite = static_cast(min(columnsAvailable, static_cast(_chars.size()))); Line& line = currentLine(); if (line.isTrivialBuffer() && line.empty()) @@ -357,29 +343,19 @@ size_t Screen::emplaceCharsIntoCurrentLine(string_view _chars, size_t cell line.trivialBuffer().fillAttributes, _state.cursor.hyperlink, ColumnCount::cast_from(cellCount), - crispy::BufferFragment { - _terminal.currentPtyBuffer(), - _chars.substr(0, charsToWrite), - } }); + crispy::BufferFragment { _terminal.currentPtyBuffer(), _chars } }); advanceCursorAfterWrite(ColumnCount::cast_from(cellCount)); } else { // Transforming _chars input from UTF-8 to UTF-32 even though right now it should only // be containing US-ASCII, but soon it'll be any arbitrary textual Unicode codepoints. - auto utf8DecoderState = unicode::utf8_decoder_state {}; for (char const ch: _chars) { - auto const result = unicode::from_utf8(utf8DecoderState, static_cast(ch)); - if (holds_alternative(result)) - writeText(get(result).value); - else if (holds_alternative(result)) - writeText(U'\uFFFE'); // U+FFFE (Not a Character) + _state.parser.printUtf8Byte(ch); } } - // fmt::print("emplaceCharsIntoCurrentLine ({} cols, {} bytes): \"{}\"\n", cellCount, _chars.size(), - // _chars); - return charsToWrite; + return _chars.size(); } template @@ -397,34 +373,25 @@ void Screen::advanceCursorAfterWrite(ColumnCount n) noexcept } } -template -bool Screen::canResumeEmplace(std::string_view continuationChars) const noexcept -{ - auto& line = currentLine(); - if (!line.isTrivialBuffer()) - return false; - TrivialLineBuffer const& buffer = line.trivialBuffer(); - return buffer.text.view().end() == continuationChars.begin() - && buffer.textAttributes == _state.cursor.graphicsRendition - && buffer.hyperlink == _state.cursor.hyperlink - && buffer.usedColumns == boxed_cast(cursor().position.column) - && buffer.text.owner() == _terminal.currentPtyBuffer(); -} - template void Screen::writeText(string_view _chars, size_t cellCount) { #if defined(LIBTERMINAL_LOG_TRACE) if (VTTraceSequenceLog) - VTTraceSequenceLog()("text({} bytes): \"{}\"", _chars.size(), _chars); + VTTraceSequenceLog()("text({} bytes, {} cells): \"{}\"", _chars.size(), cellCount, escape(_chars)); #endif + assert(cellCount <= static_cast(pageSize().columns.value - _state.cursor.position.column.value)); _chars = tryEmplaceChars(_chars, cellCount); if (_chars.empty()) return; + // Making use of the optimized code path for the input characters did NOT work, so we need to first + // convert UTF-8 to UTF-32 codepoints (reusing the logic in VT parser) and pass these codepoints + // to the grapheme cluster processor. + for (char const ch: _chars) - writeTextInternal(static_cast(ch)); + _state.parser.printUtf8Byte(ch); } template diff --git a/src/terminal/Screen.h b/src/terminal/Screen.h index 3190d8aecc..b843c0b34c 100644 --- a/src/terminal/Screen.h +++ b/src/terminal/Screen.h @@ -605,7 +605,7 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase } [[nodiscard]] char32_t precedingGraphicCharacter() const noexcept { - return _state.parser.precedingGraphicCharacter; + return _state.parser.precedingGraphicCharacter(); } void applyAndLog(FunctionDefinition const& function, Sequence const& seq); @@ -615,10 +615,14 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase private: void writeTextInternal(char32_t _char); + + /// Attempts to emplace the given character sequence into the current cursor position, assuming + /// that the current line is either empty or trivial and the input character sequence is contiguous. + /// + /// @returns the string view of the UTF-8 text that could not be emplaced. std::string_view tryEmplaceChars(std::string_view chars, size_t cellCount) noexcept; - std::string_view tryEmplaceContinuousChars(std::string_view chars, size_t cellCount) noexcept; size_t emplaceCharsIntoCurrentLine(std::string_view chars, size_t cellCount) noexcept; - [[nodiscard]] bool canResumeEmplace(std::string_view continuationChars) const noexcept; + [[nodiscard]] bool isContiguousToCurrentLine(std::string_view continuationChars) const noexcept; void advanceCursorAfterWrite(ColumnCount n) noexcept; void clearAllTabs(); @@ -660,4 +664,11 @@ inline void Screen::scrollUp(LineCount _n, Margin _margin) scrollUp(_n, cursor().graphicsRendition, _margin); } +template +inline bool Screen::isContiguousToCurrentLine(std::string_view continuationChars) const noexcept +{ + auto const& line = currentLine(); + return line.isTrivialBuffer() && line.trivialBuffer().text.view().end() == continuationChars.begin(); +} + } // namespace terminal diff --git a/src/terminal/Screen_test.cpp b/src/terminal/Screen_test.cpp index dc6f1eb9e4..48e4918cc2 100644 --- a/src/terminal/Screen_test.cpp +++ b/src/terminal/Screen_test.cpp @@ -430,8 +430,8 @@ TEST_CASE("AppendChar.emoji_VS15_smiley", "[screen]") mock.writeToScreen(U"\U0001F600"); REQUIRE(*screen.logicalCursorPosition().column == 2); mock.writeToScreen(U"\uFE0E"); - REQUIRE(*screen.logicalCursorPosition().column - == 2); // U+FE0E does *NOT* lower width to 1 (easier to implement) + REQUIRE(*screen.logicalCursorPosition().column == 2); + // ^^^ U+FE0E does *NOT* lower width to 1 (easier to implement) mock.writeToScreen("X"); REQUIRE(*screen.logicalCursorPosition().column == 3); logScreenText(screen); diff --git a/src/terminal/Sequencer.cpp b/src/terminal/Sequencer.cpp index 5b6eaba5b1..b82450a05c 100644 --- a/src/terminal/Sequencer.cpp +++ b/src/terminal/Sequencer.cpp @@ -150,6 +150,11 @@ void Sequencer::unhook() } } +bool Sequencer::acceptsBulkText() const noexcept +{ + return terminal_.isPrimaryScreen() && terminal_.primaryScreen().currentLine().isTrivialBuffer(); +} + void Sequencer::handleSequence() { parameterBuilder_.fixiate(); diff --git a/src/terminal/Sequencer.h b/src/terminal/Sequencer.h index f1c1eea707..1ec3fd1c81 100644 --- a/src/terminal/Sequencer.h +++ b/src/terminal/Sequencer.h @@ -168,6 +168,8 @@ class Sequencer hookedParser_ = std::move(parserExtension); } + [[nodiscard]] bool acceptsBulkText() const noexcept; + private: void handleSequence();