mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 22:02:23 +01:00
Everything drawn into a buffer was checked against `columns()`. That is
right for wrapping and for line drawing, both of which have somewhere
else to put what doesn't fit, but wrong for `DrawText`, which exists for
text that must not be broken and sometimes has to run past the width
with no other answer available. It and `DrawCodePoint` now check only
that the column is non-negative and the row is one a grid can index, and
widen the buffer as far as the text needs; `DrawWrappedText`,
`DrawHorizontalLine`, `DrawVerticalLine`, and `DrawBox` are unchanged.
That also settles what a caller does after a drawing overhangs, since
`DrawEnd` exists so that a run can continue where the last one ended,
and that continuation was itself a checked error whenever the previous
run overhung. A column computed to be negative, such as a gutter
narrower than the line number it holds, still fails.
A color picked to read against black is hard to read against white, and
nothing in `Capabilities` said which a stream was going into.
`ChooseBackground` reads `COLORFGBG`, which `rxvt` and its derivatives
set to the foreground and background palette indices, and takes anything
it doesn't answer to be dark: guessing dark costs contrast, while
guessing light puts pale text on a pale background. Asking the terminal
itself with an `OSC 11` query is the accurate answer, and needs raw
mode, a timeout, and somewhere to put the reply, so there is a TODO for
it rather than an implementation.
Every corner, tee, and crossing came out of `Charset::Ascii` as `+`,
which left six of the shapes a diagnostic draws indistinguishable: the
rule closing a frame read as the one separating two snippets, and the
anchor opening a diagnostic as the one carrying it on. Each stand-in now
keeps the axis its line runs through, which leaves `+` meaning a
crossing and nothing else. A tee keeps its through-stroke and leaves the
branch to what is drawn beside it, and a corner is `.` where its line
leaves downward and `'` where it arrives from above, which is where
those characters sit in their cells. A box is a box again:
```
+--+ .--.
| | -> | |
+--+ '--'
```
Assisted-by: Claude Code
1135 lines
42 KiB
C++
1135 lines
42 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "common/terminal/buffer.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <string>
|
|
|
|
#include "common/filesystem.h"
|
|
#include "common/terminal/metrics.h"
|
|
#include "llvm/ADT/SmallString.h"
|
|
#include "llvm/ADT/SmallVector.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
|
|
namespace Carbon::Terminal {
|
|
namespace {
|
|
|
|
// "e" followed by U+0301 COMBINING ACUTE ACCENT. Spelled out because the
|
|
// precomposed U+00E9 is a single code point and wouldn't exercise marks at all.
|
|
constexpr char32_t CombiningAcute = 0x0301;
|
|
constexpr llvm::StringLiteral AcuteE = "e\xcc\x81";
|
|
|
|
using DrawEnd = Buffer::DrawEnd;
|
|
|
|
auto Render(const Buffer& buffer, ColorMode mode = ColorMode::NoColor)
|
|
-> std::string {
|
|
llvm::SmallString<256> out;
|
|
buffer.Render(out, mode);
|
|
return std::string(out);
|
|
}
|
|
|
|
TEST(BufferTest, Empty) {
|
|
Buffer buffer(10, Charset::Ascii);
|
|
EXPECT_EQ(buffer.width(), 10);
|
|
EXPECT_EQ(buffer.height(), 0);
|
|
EXPECT_EQ(Render(buffer), "");
|
|
}
|
|
|
|
TEST(BufferTest, GrowsRowsToFitWhatIsDrawn) {
|
|
Buffer buffer(4, Charset::Ascii);
|
|
EXPECT_EQ(buffer.DrawCodePoint(0, 2, 'x', Style()).x, 1);
|
|
EXPECT_EQ(buffer.height(), 3);
|
|
EXPECT_EQ(Render(buffer), "\n\nx\n");
|
|
}
|
|
|
|
TEST(BufferTest, WidthIsTheTargetUntilSomethingOverhangs) {
|
|
// `width()` tracks `columns()` while everything stays inside it.
|
|
Buffer buffer(6, Charset::Ascii);
|
|
EXPECT_EQ(buffer.columns(), 6);
|
|
EXPECT_EQ(buffer.width(), 6);
|
|
buffer.DrawHorizontalLine(1, 0, 5, Style());
|
|
buffer.DrawVerticalLine(0, 1, 5, Style());
|
|
EXPECT_EQ(buffer.width(), 6);
|
|
EXPECT_EQ(Render(buffer),
|
|
" -----\n"
|
|
"|\n"
|
|
"|\n"
|
|
"|\n"
|
|
"|\n"
|
|
"|\n");
|
|
|
|
// Text that starts inside the width can run past it, and a word wrapping
|
|
// cannot break is the case that produces.
|
|
Buffer overhang(4, Charset::Ascii);
|
|
overhang.DrawWrappedText(0, 0, 0, 4, "ab wordthatislong", Style());
|
|
EXPECT_EQ(overhang.columns(), 4);
|
|
EXPECT_GE(overhang.width(), 14);
|
|
EXPECT_EQ(Render(overhang),
|
|
"ab\n"
|
|
"wordthatislong\n");
|
|
}
|
|
|
|
TEST(BufferTest, WidthGrowsAcrossRowsAlreadyDrawn) {
|
|
// Rows are stored back to back, so widening has to reflow the ones already
|
|
// there rather than leaving them where their old width put them.
|
|
Buffer buffer(2, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "ab", Style());
|
|
buffer.DrawText(0, 1, "cd", Style());
|
|
buffer.DrawText(0, 2, "efghij", Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"ab\n"
|
|
"cd\n"
|
|
"efghij\n");
|
|
}
|
|
|
|
TEST(BufferTest, GrowthFromEveryStartingWidth) {
|
|
// Growth runs in steps, so every starting width reaches the same place by a
|
|
// different route.
|
|
for (int start : {1, 2, 3, 5, 17}) {
|
|
Buffer buffer(start, Charset::Utf8);
|
|
for (int row = 0; row < 4; ++row) {
|
|
buffer.DrawText(0, row, "abcdefghijklmnopqrstuvwxyz", Style());
|
|
}
|
|
EXPECT_EQ(Render(buffer),
|
|
"abcdefghijklmnopqrstuvwxyz\n"
|
|
"abcdefghijklmnopqrstuvwxyz\n"
|
|
"abcdefghijklmnopqrstuvwxyz\n"
|
|
"abcdefghijklmnopqrstuvwxyz\n")
|
|
<< start;
|
|
}
|
|
}
|
|
|
|
TEST(BufferTest, LinesJoinWhereTheyMeet) {
|
|
Buffer buffer(3, Charset::Utf8);
|
|
buffer.DrawHorizontalLine(0, 1, 3, Style());
|
|
buffer.DrawVerticalLine(1, 0, 3, Style());
|
|
|
|
EXPECT_EQ(Render(buffer),
|
|
" ╷\n"
|
|
"╶┼╴\n"
|
|
" ╵\n");
|
|
}
|
|
|
|
TEST(BufferTest, LineEndsFormCorners) {
|
|
Buffer buffer(3, Charset::Utf8);
|
|
buffer.DrawHorizontalLine(1, 0, 2, Style());
|
|
buffer.DrawVerticalLine(1, 0, 3, Style());
|
|
|
|
EXPECT_EQ(Render(buffer),
|
|
" ╭╴\n"
|
|
" │\n"
|
|
" ╵\n");
|
|
}
|
|
|
|
TEST(BufferTest, LinesOnlyJoinWhereTheyOverlap) {
|
|
// A cell's glyph follows from the directions lines leave it in, so joining
|
|
// is a matter of drawing into the same cell rather than of being adjacent.
|
|
Buffer separate(3, Charset::Utf8);
|
|
separate.DrawHorizontalLine(0, 0, 3, Style());
|
|
separate.DrawVerticalLine(0, 1, 2, Style());
|
|
EXPECT_EQ(Render(separate),
|
|
"╶─╴\n"
|
|
"╷\n"
|
|
"╵\n");
|
|
|
|
Buffer overlapping(3, Charset::Utf8);
|
|
overlapping.DrawHorizontalLine(0, 0, 3, Style());
|
|
overlapping.DrawVerticalLine(0, 0, 3, Style());
|
|
EXPECT_EQ(Render(overlapping),
|
|
"╭─╴\n"
|
|
"│\n"
|
|
"╵\n");
|
|
}
|
|
|
|
TEST(BufferTest, DrawOrderDoesNotMatter) {
|
|
Buffer vertical_first(3, Charset::Utf8);
|
|
vertical_first.DrawVerticalLine(1, 0, 3, Style());
|
|
vertical_first.DrawHorizontalLine(0, 1, 3, Style());
|
|
|
|
EXPECT_EQ(Render(vertical_first),
|
|
" ╷\n"
|
|
"╶┼╴\n"
|
|
" ╵\n");
|
|
}
|
|
|
|
TEST(BufferTest, Tees) {
|
|
// Every junction shape comes out of lines overlapping, so a table of them
|
|
// exercises all four tees and the cross together.
|
|
Buffer buffer(5, Charset::Utf8);
|
|
buffer.DrawBox(0, 0, 5, 5, Style());
|
|
buffer.DrawHorizontalLine(0, 2, 5, Style());
|
|
buffer.DrawVerticalLine(2, 0, 5, Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"╭─┬─╮\n"
|
|
"│ │ │\n"
|
|
"├─┼─┤\n"
|
|
"│ │ │\n"
|
|
"╰─┴─╯\n");
|
|
|
|
// The same table in ASCII, where every tee keeps its through-stroke and only
|
|
// the crossing in the middle is a `+`.
|
|
Buffer ascii(5, Charset::Ascii);
|
|
ascii.DrawBox(0, 0, 5, 5, Style());
|
|
ascii.DrawHorizontalLine(0, 2, 5, Style());
|
|
ascii.DrawVerticalLine(2, 0, 5, Style());
|
|
EXPECT_EQ(Render(ascii),
|
|
".---.\n"
|
|
"| | |\n"
|
|
"|-+-|\n"
|
|
"| | |\n"
|
|
"'---'\n");
|
|
}
|
|
|
|
TEST(BufferTest, ALineBetweenOneCenterAndItselfIsAPoint) {
|
|
// A line of one cell between two centers has no length and no direction, so
|
|
// it is a point. It is still line art, so anything drawn through it later
|
|
// joins it rather than replacing it.
|
|
Buffer horizontal(3, Charset::Utf8);
|
|
horizontal.DrawHorizontalLine(1, 0, 1, Style());
|
|
EXPECT_EQ(Render(horizontal), " ·\n");
|
|
|
|
Buffer vertical(3, Charset::Utf8);
|
|
vertical.DrawVerticalLine(1, 0, 1, Style());
|
|
EXPECT_EQ(Render(vertical), " ·\n");
|
|
|
|
Buffer joined(3, Charset::Utf8);
|
|
joined.DrawHorizontalLine(1, 0, 1, Style());
|
|
joined.DrawHorizontalLine(0, 0, 3, Style());
|
|
EXPECT_EQ(Render(joined), "╶─╴\n");
|
|
|
|
// A point is a small mark in either character set, rather than the junction
|
|
// ASCII draws where lines really cross.
|
|
Buffer ascii(3, Charset::Ascii);
|
|
ascii.DrawVerticalLine(1, 0, 1, Style());
|
|
EXPECT_EQ(Render(ascii), " .\n");
|
|
|
|
// A line with no length draws nothing at all.
|
|
Buffer empty(3, Charset::Utf8);
|
|
empty.DrawHorizontalLine(0, 0, 0, Style());
|
|
empty.DrawVerticalLine(0, 0, 0, Style());
|
|
EXPECT_EQ(Render(empty), "");
|
|
}
|
|
|
|
TEST(BufferTest, LineEndsDecideWhatMeetsThemAtTheEnd) {
|
|
// A line ending at a center is met with a corner, because the two lines stop
|
|
// at the same point. One running out through the edge of its last cell is met
|
|
// with a tee, because it carries on past whatever arrives there.
|
|
Buffer corner(4, Charset::Utf8);
|
|
corner.DrawHorizontalLine(0, 0, 3, Style());
|
|
corner.DrawVerticalLine(2, 0, 2, Style());
|
|
EXPECT_EQ(Render(corner),
|
|
"╶─╮\n"
|
|
" ╵\n");
|
|
|
|
Buffer tee(4, Charset::Utf8);
|
|
tee.DrawHorizontalLine(0, 0, 3, Style(), LineEnd::Center, LineEnd::Edge);
|
|
tee.DrawVerticalLine(2, 0, 2, Style());
|
|
EXPECT_EQ(Render(tee),
|
|
"╶─┬\n"
|
|
" ╵\n");
|
|
|
|
// `start` decides the first cell the way `end` decides the last.
|
|
Buffer edge_start(4, Charset::Utf8);
|
|
edge_start.DrawHorizontalLine(1, 0, 2, Style(), LineEnd::Edge);
|
|
EXPECT_EQ(Render(edge_start), " ─╴\n");
|
|
}
|
|
|
|
TEST(BufferTest, AnEndAtACenterDrawsHalfALine) {
|
|
// Two half-lines laid end to end show the gap that says they do not connect,
|
|
// which ASCII cannot draw.
|
|
Buffer buffer(6, Charset::Utf8);
|
|
buffer.DrawHorizontalLine(0, 0, 4, Style());
|
|
buffer.DrawVerticalLine(0, 1, 3, Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"╶──╴\n"
|
|
"╷\n"
|
|
"│\n"
|
|
"╵\n");
|
|
|
|
// Two lines that each stop at their own center, laid end to end, are drawn
|
|
// with the gap between them that says they do not connect.
|
|
Buffer apart(6, Charset::Utf8);
|
|
apart.DrawHorizontalLine(0, 0, 2, Style());
|
|
apart.DrawHorizontalLine(2, 0, 2, Style());
|
|
EXPECT_EQ(Render(apart), "╶╴╶╴\n");
|
|
|
|
// ASCII has nothing to draw half a line with, so there the two read as one.
|
|
Buffer ascii(6, Charset::Ascii);
|
|
ascii.DrawHorizontalLine(0, 0, 2, Style());
|
|
ascii.DrawHorizontalLine(2, 0, 2, Style());
|
|
EXPECT_EQ(Render(ascii), "----\n");
|
|
}
|
|
|
|
TEST(BufferTest, AnEdgeToEdgeLineSpansWholeCells) {
|
|
// Bounding a run of columns is edge to edge: the line covers all of them
|
|
// rather than stopping halfway into the first and last.
|
|
Buffer buffer(4, Charset::Utf8);
|
|
buffer.DrawHorizontalLine(0, 0, 1, Style(), LineEnd::Edge, LineEnd::Edge);
|
|
buffer.DrawVerticalLine(0, 1, 1, Style(), LineEnd::Edge, LineEnd::Edge);
|
|
EXPECT_EQ(Render(buffer),
|
|
"─\n"
|
|
"│\n");
|
|
|
|
// Two edge-to-edge runs that meet end to end read as one line, without
|
|
// either having to reach into the other's cells.
|
|
Buffer split(6, Charset::Utf8);
|
|
split.DrawVerticalLine(0, 0, 2, Style(), LineEnd::Edge, LineEnd::Edge);
|
|
split.DrawVerticalLine(0, 2, 2, Style(), LineEnd::Edge, LineEnd::Edge);
|
|
EXPECT_EQ(Render(split),
|
|
"│\n"
|
|
"│\n"
|
|
"│\n"
|
|
"│\n");
|
|
}
|
|
|
|
TEST(BufferTest, LinesDrawOverContent) {
|
|
// A line replaces whatever text was in the cell, including both halves of a
|
|
// double-width character it lands on.
|
|
Buffer over_text(5, Charset::Utf8);
|
|
over_text.DrawText(0, 0, "abcde", Style());
|
|
over_text.DrawHorizontalLine(1, 0, 3, Style());
|
|
EXPECT_EQ(Render(over_text), "a╶─╴e\n");
|
|
|
|
Buffer over_wide(5, Charset::Utf8);
|
|
over_wide.DrawText(0, 0, "中中", Style());
|
|
over_wide.DrawVerticalLine(1, 0, 1, Style(), LineEnd::Edge, LineEnd::Edge);
|
|
EXPECT_EQ(Render(over_wide), " │中\n");
|
|
}
|
|
|
|
TEST(BufferTest, Box) {
|
|
Buffer buffer(4, Charset::Utf8);
|
|
buffer.DrawBox(0, 0, 4, 4, Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"╭──╮\n"
|
|
"│ │\n"
|
|
"│ │\n"
|
|
"╰──╯\n");
|
|
|
|
// The ASCII stand-ins keep the shape: the sides run and the corners turn,
|
|
// with the character that sits low where the line leaves downward and the one
|
|
// that sits high where it arrives from above.
|
|
Buffer ascii(4, Charset::Ascii);
|
|
ascii.DrawBox(0, 0, 4, 3, Style());
|
|
EXPECT_EQ(Render(ascii),
|
|
".--.\n"
|
|
"| |\n"
|
|
"'--'\n");
|
|
|
|
// A box with no interior is the single line that bounds it.
|
|
Buffer flat(4, Charset::Utf8);
|
|
flat.DrawBox(0, 0, 4, 1, Style());
|
|
flat.DrawBox(0, 2, 1, 2, Style());
|
|
EXPECT_EQ(Render(flat),
|
|
"╶──╴\n"
|
|
"\n"
|
|
"╷\n"
|
|
"╵\n");
|
|
|
|
Buffer degenerate(4, Charset::Utf8);
|
|
degenerate.DrawBox(0, 0, 0, 4, Style());
|
|
degenerate.DrawBox(0, 0, 4, 0, Style());
|
|
EXPECT_EQ(Render(degenerate), "");
|
|
}
|
|
|
|
TEST(BufferTest, TextIsNeverRedrawnAsLines) {
|
|
Buffer buffer(10, Charset::Utf8);
|
|
buffer.DrawText(0, 0, "a -+- b", Style());
|
|
EXPECT_EQ(Render(buffer), "a -+- b\n");
|
|
}
|
|
|
|
TEST(BufferTest, Text) {
|
|
Buffer buffer(15, Charset::Utf8);
|
|
EXPECT_EQ(buffer.DrawText(0, 0, "Hello, World!", Style()).y, 0);
|
|
EXPECT_EQ(buffer.DrawText(0, 1, "A\tB\nC", Style()).y, 2);
|
|
|
|
EXPECT_EQ(Render(buffer),
|
|
"Hello, World!\n"
|
|
"A B\n"
|
|
"C\n");
|
|
|
|
// Drawing nothing ends where it began.
|
|
EXPECT_EQ(buffer.DrawText(0, 0, "", Style()).y, 0);
|
|
}
|
|
|
|
TEST(BufferTest, TabStopsAreMeasuredFromWhereTextBegins) {
|
|
// Tab stops follow the text's own origin, not the left edge, so a source
|
|
// line quoted beside a gutter keeps the tab alignment it had in the file.
|
|
Buffer buffer(20, Charset::Utf8);
|
|
buffer.DrawText(3, 0, "A\tB", Style());
|
|
EXPECT_EQ(Render(buffer), " A B\n");
|
|
}
|
|
|
|
TEST(BufferTest, TextRowsReturnToTheMargin) {
|
|
// Text drawn as differently styled spans names one margin across all of them,
|
|
// so a newline in the middle of it returns to the text's own left edge rather
|
|
// than to wherever the span it fell in started.
|
|
Buffer buffer(20, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "| ", Style());
|
|
DrawEnd end = buffer.DrawText(2, 0, 2, "plain\nmore ", Style());
|
|
buffer.DrawText(end.x, end.y, 2, "bold\ntext", Style().Bold());
|
|
EXPECT_EQ(Render(buffer),
|
|
"| plain\n"
|
|
" more bold\n"
|
|
" text\n");
|
|
|
|
// Carriage returns return there too, and tab stops are counted from it.
|
|
Buffer positional(20, Charset::Ascii);
|
|
positional.DrawText(0, 0, "| ", Style());
|
|
positional.DrawText(2, 0, 2, "a\tb\rc", Style());
|
|
EXPECT_EQ(Render(positional), "| c b\n");
|
|
}
|
|
|
|
TEST(BufferTest, TextControlCharacters) {
|
|
Buffer buffer(10, Charset::Utf8);
|
|
// A carriage return returns to the column the text started in.
|
|
buffer.DrawText(2, 0, "abc\rx", Style());
|
|
EXPECT_EQ(Render(buffer), " xbc\n");
|
|
|
|
// Anything else with no printable rendering is replaced, so a stray byte
|
|
// can't shift the columns after it.
|
|
Buffer utf8(10, Charset::Utf8);
|
|
utf8.DrawText(0, 0,
|
|
"a\x01"
|
|
"b",
|
|
Style());
|
|
EXPECT_EQ(Render(utf8), "a�b\n");
|
|
|
|
Buffer ascii(10, Charset::Ascii);
|
|
ascii.DrawText(0, 0,
|
|
"a\x01"
|
|
"b",
|
|
Style());
|
|
EXPECT_EQ(Render(ascii), "a?b\n");
|
|
}
|
|
|
|
TEST(BufferTest, AsciiDoesNoUtf8Processing) {
|
|
// A terminal that isn't decoding UTF-8 renders each byte as some character
|
|
// of its own, so every byte has to be counted as one column. Replacing the
|
|
// bytes keeps the column count honest without guessing at an encoding.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "a中b", Style());
|
|
EXPECT_EQ(Render(buffer), "a???b\n");
|
|
EXPECT_EQ(buffer.MeasureText(0, 0, "a中b").x, 5);
|
|
|
|
// The same goes for text with combining marks, which occupy no columns only
|
|
// because a UTF-8 terminal folds them into the one before.
|
|
Buffer marks(10, Charset::Ascii);
|
|
marks.DrawText(0, 0, AcuteE, Style());
|
|
EXPECT_EQ(Render(marks), "e??\n");
|
|
|
|
// Invalid UTF-8 is not even a category here; bytes are bytes.
|
|
Buffer invalid(10, Charset::Ascii);
|
|
invalid.DrawText(0, 0, llvm::StringRef("\xc0\x80z", 3), Style());
|
|
EXPECT_EQ(Render(invalid), "??z\n");
|
|
|
|
// Every code point is one column wide, whatever it is.
|
|
EXPECT_EQ(buffer.DrawCodePoint(0, 1, U'中', Style()).x, 1);
|
|
EXPECT_EQ(buffer.DrawCodePoint(1, 1, CombiningAcute, Style()).x, 2);
|
|
EXPECT_EQ(Render(buffer),
|
|
"a???b\n"
|
|
"??\n");
|
|
}
|
|
|
|
TEST(BufferTest, TextInvalidUtf8) {
|
|
Buffer buffer(10, Charset::Utf8);
|
|
// An overlong encoding of NUL. It is rejected, and decoding resynchronizes
|
|
// one byte at a time rather than giving up on the rest of the text.
|
|
buffer.DrawText(0, 0, llvm::StringRef("\xc0\x80z", 3), Style());
|
|
EXPECT_EQ(Render(buffer), "��z\n");
|
|
|
|
Buffer surrogate(10, Charset::Utf8);
|
|
surrogate.DrawText(0, 0, llvm::StringRef("\xed\xa0\x80z", 4), Style());
|
|
EXPECT_EQ(Render(surrogate), "���z\n");
|
|
}
|
|
|
|
TEST(BufferTest, CodePointsWithNoEncoding) {
|
|
// Decoding text never yields these, but `DrawCodePoint` takes any code point,
|
|
// including the surrogates and the values past the last one that UTF-8 has no
|
|
// encoding for.
|
|
Buffer buffer(10, Charset::Utf8);
|
|
EXPECT_EQ(
|
|
buffer.DrawCodePoint(0, 0, static_cast<char32_t>(0xd800), Style()).x, 1);
|
|
EXPECT_EQ(
|
|
buffer.DrawCodePoint(1, 0, static_cast<char32_t>(0xdfff), Style()).x, 2);
|
|
EXPECT_EQ(
|
|
buffer.DrawCodePoint(2, 0, static_cast<char32_t>(0x110000), Style()).x,
|
|
3);
|
|
EXPECT_EQ(Render(buffer), "���\n");
|
|
}
|
|
|
|
TEST(BufferTest, DoubleWidthCharacters) {
|
|
Buffer buffer(6, Charset::Utf8);
|
|
buffer.DrawText(0, 0, "中A🔥", Style());
|
|
EXPECT_EQ(Render(buffer), "中A🔥\n");
|
|
EXPECT_EQ(buffer.DrawCodePoint(0, 1, U'中', Style()).x, 2);
|
|
}
|
|
|
|
TEST(BufferTest, DrawingOverADoubleWidthCharacterErasesAllOfIt) {
|
|
// Overwriting either half has to erase the whole character. Either half left
|
|
// behind misaligns what follows, because the columns the grid counts for the
|
|
// cell and the ones the terminal paints stop agreeing.
|
|
Buffer over_head(4, Charset::Utf8);
|
|
over_head.DrawCodePoint(0, 0, U'中', Style());
|
|
over_head.DrawCodePoint(0, 0, 'A', Style());
|
|
EXPECT_EQ(Render(over_head), "A\n");
|
|
|
|
Buffer over_tail(4, Charset::Utf8);
|
|
over_tail.DrawCodePoint(0, 0, U'中', Style());
|
|
over_tail.DrawCodePoint(1, 0, 'B', Style());
|
|
EXPECT_EQ(Render(over_tail), " B\n");
|
|
|
|
// The same holds when a double-width character lands on another one.
|
|
Buffer over_both(6, Charset::Utf8);
|
|
over_both.DrawCodePoint(0, 0, U'中', Style());
|
|
over_both.DrawCodePoint(2, 0, U'中', Style());
|
|
over_both.DrawCodePoint(1, 0, U'国', Style());
|
|
EXPECT_EQ(Render(over_both), " 国\n");
|
|
}
|
|
|
|
TEST(BufferTest, DoubleWidthCharacterInTheLastColumnTakesBothColumns) {
|
|
// It starts inside the width, and splitting one would leave the terminal
|
|
// rendering half a character, so it overhangs by a column rather than being
|
|
// refused.
|
|
Buffer buffer(3, Charset::Utf8);
|
|
EXPECT_EQ(buffer.DrawCodePoint(2, 0, U'中', Style()).x, 4);
|
|
buffer.DrawCodePoint(0, 0, 'a', Style());
|
|
EXPECT_EQ(Render(buffer), "a 中\n");
|
|
EXPECT_GE(buffer.width(), 4);
|
|
}
|
|
|
|
TEST(BufferTest, CombiningMarks) {
|
|
// Marks render into the column before them, so a base character and its
|
|
// marks stay in one cell and don't shift what follows.
|
|
Buffer buffer(10, Charset::Utf8);
|
|
buffer.DrawText(0, 0, AcuteE, Style());
|
|
EXPECT_EQ(Render(buffer), AcuteE.str() + "\n");
|
|
EXPECT_EQ(buffer.MeasureText(0, 0, AcuteE).x, 1);
|
|
EXPECT_EQ(buffer.DrawCodePoint(5, 0, CombiningAcute, Style()).x, 5);
|
|
|
|
// Drawing over the base takes its marks with it.
|
|
Buffer overwritten(10, Charset::Utf8);
|
|
overwritten.DrawText(0, 0, AcuteE, Style());
|
|
overwritten.DrawText(1, 0, "x", Style());
|
|
overwritten.DrawCodePoint(0, 0, 'o', Style());
|
|
EXPECT_EQ(Render(overwritten), "ox\n");
|
|
}
|
|
|
|
TEST(BufferTest, CombiningMarksOnADoubleWidthBase) {
|
|
// A mark following a double-width character arrives at the column past its
|
|
// continuation, and has to reach back to the character itself.
|
|
Buffer buffer(10, Charset::Utf8);
|
|
buffer.DrawText(0, 0, ("中" + AcuteE.drop_front(1) + "x").str(), Style());
|
|
EXPECT_EQ(Render(buffer), ("中" + AcuteE.drop_front(1) + "x\n").str());
|
|
EXPECT_EQ(buffer.MeasureText(0, 0, ("中" + AcuteE.drop_front(1)).str()).x, 2);
|
|
}
|
|
|
|
TEST(BufferTest, CombiningMarksAreCapped) {
|
|
// Marks stack without bound in adversarial text, and every one of them would
|
|
// otherwise land in a single cell's output.
|
|
std::string zalgo = "e";
|
|
for (int i = 0; i < 100; ++i) {
|
|
zalgo += AcuteE.drop_front(1);
|
|
}
|
|
|
|
Buffer buffer(10, Charset::Utf8);
|
|
buffer.DrawText(0, 0, zalgo, Style());
|
|
// The base, some bounded run of marks, and the newline.
|
|
EXPECT_LT(Render(buffer).size(), 64U);
|
|
EXPECT_GT(Render(buffer).size(), 1U);
|
|
}
|
|
|
|
TEST(BufferTest, WrappedText) {
|
|
Buffer buffer(20, Charset::Ascii);
|
|
EXPECT_EQ(buffer
|
|
.DrawWrappedText(0, 0, 0, 10,
|
|
"This is a long sentence that should be "
|
|
"wrapped.",
|
|
Style())
|
|
.y,
|
|
5);
|
|
|
|
EXPECT_EQ(Render(buffer),
|
|
"This is a\n"
|
|
"long\n"
|
|
"sentence\n"
|
|
"that\n"
|
|
"should be\n"
|
|
"wrapped.\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextKeepsWordsTooLongToFitWhole) {
|
|
// A break is a newline in the rendered text, and one inside a word stops it
|
|
// being copied out in one piece, so the word overhangs the width instead and
|
|
// the buffer grows to hold it.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
EXPECT_EQ(buffer.DrawWrappedText(0, 0, 0, 5, "abcdefghij", Style()).y, 0);
|
|
EXPECT_EQ(Render(buffer), "abcdefghij\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextStartsAnOverlongWordOnItsOwnRow) {
|
|
// A word too long for any row moves to one of its own anyway, so it overhangs
|
|
// from the margin rather than from wherever the previous word ended.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
EXPECT_EQ(buffer.DrawWrappedText(0, 0, 0, 5, "ab abcdefghij", Style()).y, 1);
|
|
EXPECT_EQ(Render(buffer),
|
|
"ab\n"
|
|
"abcdefghij\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedSpansShareOneBlock) {
|
|
// A block whose spans are styled differently is drawn one span at a time,
|
|
// each continuing where the last ended and all naming the same margin, so
|
|
// the rows after the first line up with the block rather than with wherever
|
|
// the span happened to start.
|
|
Buffer buffer(20, Charset::Ascii);
|
|
Buffer::DrawEnd end =
|
|
buffer.DrawWrappedText(2, 0, 2, 12, "plain words", Style());
|
|
end = buffer.DrawWrappedText(end.x, end.y, 2, 12, " emphasized more",
|
|
Style().Bold());
|
|
buffer.DrawWrappedText(end.x, end.y, 2, 12, " plain again", Style());
|
|
|
|
EXPECT_EQ(Render(buffer),
|
|
" plain words\n"
|
|
" emphasized\n"
|
|
" more plain\n"
|
|
" again\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextIndents) {
|
|
// Wrapping is relative to the margin rather than to where the text starts,
|
|
// which is what lets a wrapped block sit beside a gutter.
|
|
Buffer buffer(12, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "| ", Style());
|
|
buffer.DrawWrappedText(2, 0, 2, 6, "alpha beta gamma", Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"| alpha\n"
|
|
" beta\n"
|
|
" gamma\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextExpandsTabsToStops) {
|
|
// A tab advances to the next stop, and one following a newline the text wrote
|
|
// is indentation and is kept.
|
|
Buffer buffer(40, Charset::Ascii);
|
|
buffer.DrawWrappedText(0, 0, 0, 30, "a\tb\nlonger\tc", Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"a b\n"
|
|
"longer c\n");
|
|
|
|
// A tab the text wrote after a newline is indentation, and is kept.
|
|
Buffer indented(40, Charset::Ascii);
|
|
indented.DrawWrappedText(0, 0, 0, 30, "a\n\tb", Style());
|
|
EXPECT_EQ(Render(indented),
|
|
"a\n"
|
|
" b\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextTabStopsFollowTheMargin) {
|
|
// Stops are counted from the block's margin rather than from the buffer's
|
|
// left edge.
|
|
Buffer buffer(40, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "| ", Style());
|
|
buffer.DrawWrappedText(2, 0, 2, 20, "a\tb", Style());
|
|
EXPECT_EQ(Render(buffer), "| a b\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextBreaksAtTabs) {
|
|
// A tab is a break opportunity as well as a jump to a stop.
|
|
Buffer buffer(40, Charset::Ascii);
|
|
buffer.DrawWrappedText(0, 0, 0, 8, "aaaa\tbbbb", Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"aaaa\n"
|
|
"bbbb\n");
|
|
|
|
// One reaching past the block stops at its edge, so the span after it
|
|
// continues from there rather than from a stop outside the block.
|
|
Buffer clipped(40, Charset::Ascii);
|
|
EXPECT_EQ(clipped.DrawWrappedText(0, 0, 0, 4, "ab\t", Style()),
|
|
DrawEnd(4, 0));
|
|
}
|
|
|
|
TEST(BufferTest, TabWidthComesFromCapabilities) {
|
|
Capabilities capabilities;
|
|
capabilities.charset = Charset::Ascii;
|
|
capabilities.tab_width = 4;
|
|
|
|
Buffer buffer(capabilities);
|
|
buffer.DrawText(0, 0, "a\tb", Style());
|
|
buffer.DrawWrappedText(0, 1, 0, 20, "a\tb", Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"a b\n"
|
|
"a b\n");
|
|
|
|
// A terminal claiming stops a buffer can't draw to is clamped rather than
|
|
// trusted, the same as one claiming an impossible width.
|
|
capabilities.tab_width = 0;
|
|
Buffer clamped(capabilities);
|
|
clamped.DrawText(0, 0, "a\tb", Style());
|
|
EXPECT_EQ(Render(clamped), "a b\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextKeepsTheBreaksItIsGiven) {
|
|
// Wrapping only adds breaks. Text that arrives wrapped to some other width
|
|
// keeps that wrapping rather than being reflowed into this one, even where
|
|
// its lines would fit together.
|
|
Buffer buffer(40, Charset::Ascii);
|
|
EXPECT_EQ(
|
|
buffer.DrawWrappedText(0, 0, 0, 30, "already\nwrapped\nnarrow", Style())
|
|
.y,
|
|
2);
|
|
EXPECT_EQ(Render(buffer),
|
|
"already\n"
|
|
"wrapped\n"
|
|
"narrow\n");
|
|
|
|
// A break the text ends with closes its last line, and a row nothing was
|
|
// drawn into is not a row, so it doesn't also open an empty one.
|
|
Buffer trailing(40, Charset::Ascii);
|
|
trailing.DrawWrappedText(0, 0, 0, 30, "one\ntwo\n", Style());
|
|
EXPECT_EQ(Render(trailing),
|
|
"one\n"
|
|
"two\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextLineBreaks) {
|
|
// Carriage returns are dropped, so CRLF endings break exactly once.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
EXPECT_EQ(buffer.DrawWrappedText(0, 0, 0, 10, "a\r\nb", Style()).y, 1);
|
|
EXPECT_EQ(Render(buffer),
|
|
"a\n"
|
|
"b\n");
|
|
|
|
// Whitespace stops at the block's edge, so a wrapped row starts at the
|
|
// margin.
|
|
Buffer spaces(10, Charset::Ascii);
|
|
spaces.DrawWrappedText(0, 0, 0, 5, "aaaaa bbbbb", Style());
|
|
EXPECT_EQ(Render(spaces),
|
|
"aaaaa\n"
|
|
"bbbbb\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextFillsTheWidthLeftOfTheMargin) {
|
|
// A caller with nothing to divide the width between gives the block all of
|
|
// what is left of it, which is what wrapping to the terminal is.
|
|
Buffer buffer(20, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "-> ", Style());
|
|
buffer.DrawWrappedText(3, 0, 3, buffer.columns() - 3,
|
|
"several words that would otherwise fit", Style());
|
|
EXPECT_EQ(Render(buffer),
|
|
"-> several words\n"
|
|
" that would\n"
|
|
" otherwise fit\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextKeepsAMarkWithTheWhitespaceBeforeIt) {
|
|
// A combining mark renders into the column before it, which for a mark
|
|
// following whitespace is that whitespace. Left to begin the word after it,
|
|
// the mark would move to another row whenever that word wrapped and attach to
|
|
// whatever preceded it there.
|
|
Buffer buffer(10, Charset::Utf8);
|
|
std::string mark = AcuteE.drop_front(1).str();
|
|
buffer.DrawWrappedText(0, 0, 0, 5, "aaa " + mark + "bbbb", Style());
|
|
EXPECT_EQ(Render(buffer), "aaa " + mark + "\nbbbb\n");
|
|
|
|
// Whitespace stops at the block's edge, so a mark can arrive where none of it
|
|
// was drawn. It attaches to the last cell written rather than being carried
|
|
// to the row the next word wraps onto.
|
|
Buffer filled(10, Charset::Utf8);
|
|
filled.DrawWrappedText(0, 0, 0, 5, "aaaaa " + mark + "bb", Style());
|
|
EXPECT_EQ(Render(filled), "aaaaa" + mark + "\nbb\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextKeepsCharactersWiderThanTheRegion) {
|
|
// No row in a one-column region could hold a double-width character. Drawing
|
|
// it anyway overruns the region, which is what keeping the text costs here.
|
|
Buffer buffer(10, Charset::Utf8);
|
|
buffer.DrawWrappedText(0, 0, 0, 1, "中中", Style());
|
|
EXPECT_EQ(Render(buffer), "中中\n");
|
|
}
|
|
|
|
TEST(BufferTest, WrappedTextWithDoubleWidthCharacters) {
|
|
// Wrapping counts columns, not characters, so half as many double-width ones
|
|
// fit a row.
|
|
Buffer buffer(10, Charset::Utf8);
|
|
EXPECT_EQ(buffer.DrawWrappedText(0, 0, 0, 4, "中中 中中", Style()).y, 1);
|
|
EXPECT_EQ(Render(buffer),
|
|
"中中\n"
|
|
"中中\n");
|
|
}
|
|
|
|
TEST(BufferTest, TrailingBlanksAreDropped) {
|
|
// Padding out to the buffer's width would put invisible whitespace into
|
|
// every line of output, which shows up in diffs and in copied text.
|
|
Buffer buffer(40, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "hi", Style());
|
|
EXPECT_EQ(Render(buffer), "hi\n");
|
|
}
|
|
|
|
TEST(BufferTest, StyledBlanksAreKept) {
|
|
// A blank cell with a background still paints, so it isn't padding.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
buffer.DrawCodePoint(0, 0, 'x', Style());
|
|
buffer.DrawCodePoint(3, 0, ' ', Style().Background(AnsiColor::Red));
|
|
EXPECT_EQ(Render(buffer, ColorMode::Ansi16), "x \x1b[41m \x1b[0m\n");
|
|
|
|
// With color off the background paints nothing, so those cells are padding
|
|
// again and must not reach the output as trailing spaces.
|
|
EXPECT_EQ(Render(buffer, ColorMode::NoColor), "x\n");
|
|
}
|
|
|
|
TEST(BufferTest, RenderMinimizesEscapes) {
|
|
Buffer buffer(4, Charset::Ascii);
|
|
Style red_bold = Style().Bold().Foreground(Color(255, 0, 0));
|
|
buffer.DrawCodePoint(0, 0, 'A', red_bold);
|
|
// Sharing the attributes and changing only the color costs one escape.
|
|
buffer.DrawCodePoint(1, 0, 'B', red_bold.Foreground(Color(0, 0, 255)));
|
|
// Dropping bold costs a reset and a fresh start.
|
|
buffer.DrawCodePoint(2, 0, 'C', Style().Foreground(Color(0, 0, 255)));
|
|
buffer.DrawCodePoint(3, 0, 'D', Style());
|
|
|
|
EXPECT_EQ(Render(buffer, ColorMode::Truecolor),
|
|
"\x1b[1m\x1b[38;2;255;0;0m"
|
|
"A"
|
|
"\x1b[38;2;0;0;255m"
|
|
"B"
|
|
"\x1b[0m\x1b[38;2;0;0;255m"
|
|
"C"
|
|
"\x1b[0m"
|
|
"D\n");
|
|
}
|
|
|
|
TEST(BufferTest, MeasureText) {
|
|
Buffer utf8(10, Charset::Utf8);
|
|
EXPECT_EQ(utf8.MeasureText(0, 0, ""), DrawEnd(0, 0));
|
|
EXPECT_EQ(utf8.MeasureText(0, 0, "hello"), DrawEnd(5, 0));
|
|
EXPECT_EQ(utf8.MeasureText(0, 0, "中A🔥"), DrawEnd(5, 0));
|
|
EXPECT_EQ(utf8.MeasureText(0, 0, AcuteE), DrawEnd(1, 0));
|
|
EXPECT_EQ(utf8.MeasureText(0, 0, llvm::StringRef("\xc0\x80", 2)),
|
|
DrawEnd(2, 0));
|
|
|
|
// Newlines, carriage returns, and tabs are interpreted as drawing does.
|
|
EXPECT_EQ(utf8.MeasureText(0, 0, "a\nb"), DrawEnd(1, 1));
|
|
EXPECT_EQ(utf8.MeasureText(0, 0, "a\r\nb"), DrawEnd(1, 1));
|
|
EXPECT_EQ(utf8.MeasureText(0, 0, "a\tb"), DrawEnd(9, 0));
|
|
|
|
// Measuring starts from where it is told to, which is the column a newline
|
|
// returns to and the origin the tab stops count from.
|
|
EXPECT_EQ(utf8.MeasureText(3, 2, "a\tb"), DrawEnd(12, 2));
|
|
EXPECT_EQ(utf8.MeasureText(3, 2, "a\nb"), DrawEnd(4, 3));
|
|
|
|
// Every byte is a column when the terminal isn't decoding UTF-8.
|
|
Buffer ascii(10, Charset::Ascii);
|
|
EXPECT_EQ(ascii.MeasureText(0, 0, "hello"), DrawEnd(5, 0));
|
|
EXPECT_EQ(ascii.MeasureText(0, 0, "中A🔥"), DrawEnd(8, 0));
|
|
EXPECT_EQ(ascii.MeasureText(0, 0, AcuteE), DrawEnd(3, 0));
|
|
}
|
|
|
|
TEST(BufferTest, MeasuringMatchesDrawing) {
|
|
// The point of measuring is to answer what drawing would, so check the two
|
|
// against each other on the text most likely to make them disagree.
|
|
for (llvm::StringRef text :
|
|
{"hello", "a\tb\tc", "a\nb\r\nc", "中A🔥", "a b", ""}) {
|
|
Buffer buffer(10, Charset::Utf8);
|
|
EXPECT_EQ(buffer.MeasureText(2, 1, text),
|
|
buffer.DrawText(2, 1, text, Style()))
|
|
<< text;
|
|
|
|
// A margin left of where the text starts moves what the positional
|
|
// characters answer to, and moves it for both of them alike.
|
|
Buffer block(10, Charset::Utf8);
|
|
EXPECT_EQ(block.MeasureText(2, 1, 1, text),
|
|
block.DrawText(2, 1, 1, text, Style()))
|
|
<< text;
|
|
}
|
|
for (llvm::StringRef text :
|
|
{"one two three", "a\nlonger line here", "verylongunbreakableword",
|
|
"a\tb\tc", "col\tone\nrow\ttwo", "e\xcc\x81 \xcc\x81word", ""}) {
|
|
Buffer buffer(10, Charset::Utf8);
|
|
EXPECT_EQ(buffer.MeasureWrappedText(2, 1, 2, 8, text),
|
|
buffer.DrawWrappedText(2, 1, 2, 8, text, Style()))
|
|
<< text;
|
|
}
|
|
}
|
|
|
|
TEST(BufferTest, MeasureWrapWidth) {
|
|
Buffer buffer(10, Charset::Utf8);
|
|
EXPECT_EQ(buffer.MeasureWrapWidth(""), 0);
|
|
EXPECT_EQ(buffer.MeasureWrapWidth("a bb ccc"), 3);
|
|
EXPECT_EQ(buffer.MeasureWrapWidth(" spaced out "), 6);
|
|
|
|
// A word ends at a newline or a tab, neither of which takes columns of its
|
|
// own.
|
|
EXPECT_EQ(buffer.MeasureWrapWidth("a\nbb\tccc"), 3);
|
|
|
|
// A word is measured in the columns it takes, not the bytes it holds.
|
|
EXPECT_EQ(buffer.MeasureWrapWidth("中中 a"), 4);
|
|
}
|
|
|
|
TEST(BufferTest, WrapWidthIsWhatWrappingDoesNotOverhang) {
|
|
// The width answered for is a fact about wrapping, so it is checked against
|
|
// the wrapping it describes.
|
|
Buffer buffer(10, Charset::Utf8);
|
|
// Tabs don't widen the answer: one stops at the block's edge rather than
|
|
// running to a stop outside it, so it can't overhang either.
|
|
for (llvm::StringRef text :
|
|
{"some quite long words here", "some\tquite\tlong words here"}) {
|
|
int width = buffer.MeasureWrapWidth(text);
|
|
Buffer drawn(width, Charset::Utf8);
|
|
drawn.DrawWrappedText(0, 0, 0, width, text, Style());
|
|
llvm::SmallVector<llvm::StringRef> rows;
|
|
llvm::StringRef(Render(drawn)).split(rows, '\n');
|
|
for (llvm::StringRef row : rows) {
|
|
EXPECT_LE(static_cast<int>(row.size()), width) << row;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(BufferTest, BuiltFromCapabilities) {
|
|
Capabilities capabilities;
|
|
capabilities.columns = 5;
|
|
capabilities.charset = Charset::Utf8;
|
|
|
|
Buffer buffer(capabilities);
|
|
EXPECT_EQ(buffer.columns(), 5);
|
|
EXPECT_EQ(buffer.charset(), Charset::Utf8);
|
|
buffer.DrawHorizontalLine(0, 0, 5, Style());
|
|
EXPECT_EQ(Render(buffer, capabilities.color_mode), "╶───╴\n");
|
|
|
|
// A terminal that said nothing about its width gets one chosen to be safe
|
|
// wherever the output ends up, rather than no width at all.
|
|
capabilities.columns = std::nullopt;
|
|
Buffer fallback(capabilities);
|
|
EXPECT_EQ(fallback.columns(), DefaultColumns);
|
|
|
|
// One claiming a width no grid can hold gets the nearest that can be held.
|
|
capabilities.columns = Buffer::MaxColumns + 1;
|
|
Buffer clamped(capabilities);
|
|
EXPECT_EQ(clamped.columns(), Buffer::MaxColumns);
|
|
}
|
|
|
|
TEST(BufferTest, CombiningMarkPastTheWidthItStartedWith) {
|
|
// The buffer grows to hold the text, so a mark arriving past the width it
|
|
// was constructed with attaches to its base like any other.
|
|
Buffer buffer(4, Charset::Utf8);
|
|
buffer.DrawText(0, 0, ("abcde" + AcuteE.drop_front(1)).str(), Style());
|
|
EXPECT_EQ(Render(buffer), ("abcde" + AcuteE.drop_front(1) + "\n").str());
|
|
}
|
|
|
|
TEST(BufferTest, CombiningMarksSurviveGrowthAndOverdraw) {
|
|
// Marks live in a side table keyed by cell index, so widening has to move
|
|
// them with the rows and overdrawing has to take them with the cell.
|
|
for (int start : {1, 2, 3}) {
|
|
Buffer buffer(start, Charset::Utf8);
|
|
buffer.DrawText(0, 0, std::string(AcuteE) + "e" + std::string(AcuteE),
|
|
Style());
|
|
buffer.DrawText(0, 1, "xxxxxxxxxxxx", Style());
|
|
buffer.DrawCodePoint(0, 0, U'z', Style());
|
|
std::string rendered = Render(buffer);
|
|
// The mark on the overdrawn cell went with it; the later one stayed.
|
|
EXPECT_EQ(rendered.substr(0, rendered.find('\n')),
|
|
"ze" + std::string(AcuteE))
|
|
<< start;
|
|
}
|
|
}
|
|
|
|
TEST(BufferTest, CombiningMarkWithNoBase) {
|
|
// Marks render into the column before them, so one at the start of a row has
|
|
// nowhere to go rather than attaching to the end of the row above. This is
|
|
// ordinary input rather than a caller mistake -- a source file can open a
|
|
// line with a mark -- so it is dropped and drawing goes on.
|
|
Buffer buffer(4, Charset::Utf8);
|
|
buffer.DrawText(0, 0, "ab", Style());
|
|
buffer.DrawText(0, 1, AcuteE.drop_front(1).str(), Style());
|
|
EXPECT_EQ(Render(buffer), "ab\n");
|
|
|
|
// Nor does it disturb what is already drawn on the row it lands on.
|
|
Buffer after(4, Charset::Utf8);
|
|
after.DrawText(0, 0, "ab", Style());
|
|
after.DrawCodePoint(0, 0, CombiningAcute, Style());
|
|
EXPECT_EQ(Render(after), "ab\n");
|
|
}
|
|
|
|
TEST(BufferTest, OverhangStopsAtTheGridBound) {
|
|
// An overhanging word is the only thing that reaches the grid's far edge, and
|
|
// how far it reaches is a fact about the text rather than something a caller
|
|
// could have checked, so it is clipped there rather than being a mistake.
|
|
// The column still advances past it, which is what keeps measuring and
|
|
// drawing answering the same thing.
|
|
Buffer buffer(4, Charset::Ascii);
|
|
std::string word(Buffer::MaxColumns + 10, 'x');
|
|
DrawEnd past(Buffer::MaxColumns + 10, 0);
|
|
EXPECT_EQ(buffer.MeasureWrappedText(0, 0, 0, 4, word), past);
|
|
EXPECT_EQ(buffer.DrawWrappedText(0, 0, 0, 4, word, Style()), past);
|
|
EXPECT_EQ(buffer.width(), Buffer::MaxColumns);
|
|
EXPECT_EQ(buffer.height(), 1);
|
|
}
|
|
|
|
TEST(BufferTest, ACharacterStraddlingTheGridBoundIsNotDrawn) {
|
|
// Whether a character fits the far edge depends on how wide it turns out to
|
|
// be, and half of one is not something a terminal can render, so one
|
|
// straddling the bound is past it entirely.
|
|
Buffer buffer(4, Charset::Utf8);
|
|
std::string word(Buffer::MaxColumns - 1, 'x');
|
|
word += "中";
|
|
EXPECT_EQ(buffer.DrawWrappedText(0, 0, 0, 4, word, Style()),
|
|
DrawEnd(Buffer::MaxColumns + 1, 0));
|
|
EXPECT_EQ(buffer.width(), Buffer::MaxColumns);
|
|
// Only the run before it reached the output; the character itself did not.
|
|
EXPECT_EQ(Render(buffer).size(), static_cast<size_t>(Buffer::MaxColumns));
|
|
}
|
|
|
|
TEST(BufferTest, APointBoxLeavesALineAlone) {
|
|
// A point is line art with no directions, so drawing one where a line
|
|
// already runs adds nothing and leaves the line's own directions in place.
|
|
Buffer buffer(3, Charset::Utf8);
|
|
buffer.DrawHorizontalLine(0, 0, 3, Style());
|
|
buffer.DrawBox(1, 0, 1, 1, Style());
|
|
EXPECT_EQ(Render(buffer), "╶─╴\n");
|
|
|
|
// With nothing there, the point is drawn.
|
|
Buffer empty(3, Charset::Utf8);
|
|
empty.DrawBox(1, 0, 1, 1, Style());
|
|
EXPECT_EQ(Render(empty), " ·\n");
|
|
}
|
|
|
|
TEST(BufferDeathTest, WidthMustFitTheGrid) {
|
|
// A width comes from `COLUMNS` by way of `Capabilities`, which clamps it. A
|
|
// caller reaching this constructor has computed the width itself, so a value
|
|
// the grid can't hold is a mistake rather than bad input.
|
|
EXPECT_DEATH(Buffer(0, Charset::Ascii), "Buffer width must be in");
|
|
EXPECT_DEATH(Buffer(-1, Charset::Ascii), "Buffer width must be in");
|
|
EXPECT_DEATH(Buffer(Buffer::MaxColumns + 1, Charset::Ascii),
|
|
"Buffer width must be in");
|
|
}
|
|
|
|
TEST(BufferDeathTest, TextMustStartInsideTheGrid) {
|
|
// Unwrapped text is not held to the width, so only a column before the
|
|
// origin or a row no grid can index is a mistake.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
EXPECT_DEATH(buffer.DrawCodePoint(-1, 0, 'a', Style()), "is outside the");
|
|
EXPECT_DEATH(buffer.DrawCodePoint(0, -1, 'a', Style()), "is outside the");
|
|
EXPECT_DEATH(buffer.DrawCodePoint(0, Buffer::MaxRows, 'a', Style()),
|
|
"is outside the");
|
|
EXPECT_DEATH(buffer.DrawText(-1, 0, "a", Style()), "is outside the");
|
|
EXPECT_DEATH(buffer.MeasureText(0, Buffer::MaxRows, "a"), "is outside the");
|
|
|
|
// Text begins at or right of the margin its rows return to.
|
|
EXPECT_DEATH(buffer.DrawText(2, 0, 3, "a", Style()), "left of its margin");
|
|
EXPECT_DEATH(buffer.MeasureText(2, 0, -1, "a"), "left of its margin");
|
|
}
|
|
|
|
TEST(BufferTest, UnwrappedTextWidensTheBuffer) {
|
|
// `DrawText` widens the buffer rather than being held to its width, so a run
|
|
// continues from where the one before it ended however far past that is.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
Buffer::DrawEnd end =
|
|
buffer.DrawText(0, 0, "a message far longer than ten columns", Style());
|
|
EXPECT_EQ(end, DrawEnd(37, 0));
|
|
EXPECT_EQ(buffer.columns(), 10);
|
|
EXPECT_GE(buffer.width(), 37);
|
|
|
|
// Which is what lets a row be built from runs that carry different styles.
|
|
buffer.DrawText(end.x, end.y, " [tag]", Style().Bold());
|
|
EXPECT_EQ(Render(buffer), "a message far longer than ten columns [tag]\n");
|
|
}
|
|
|
|
TEST(BufferDeathTest, LinesMustFitWhatTheyAreDrawnInto) {
|
|
// Nothing about a line is unbreakable, so unlike text it has no reason to
|
|
// reach outside the width, and one that does came from a wrong extent.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
EXPECT_DEATH(buffer.DrawHorizontalLine(6, 0, 5, Style()), "runs outside the");
|
|
EXPECT_DEATH(buffer.DrawHorizontalLine(0, 0, -1, Style()),
|
|
"runs outside the");
|
|
EXPECT_DEATH(buffer.DrawVerticalLine(0, 0, Buffer::MaxRows + 1, Style()),
|
|
"runs outside the");
|
|
EXPECT_DEATH(buffer.DrawBox(0, 0, 11, 2, Style()), "runs outside the");
|
|
}
|
|
|
|
TEST(BufferDeathTest, WrappedBlocksMustFitTheWidth) {
|
|
// A block is a division of the width rather than something that can exceed
|
|
// it, and the text has to start inside the block it wraps in.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
EXPECT_DEATH(buffer.DrawWrappedText(0, 0, 0, 11, "a", Style()),
|
|
"does not fit the");
|
|
EXPECT_DEATH(buffer.DrawWrappedText(4, 0, 4, 8, "a", Style()),
|
|
"does not fit the");
|
|
EXPECT_DEATH(buffer.DrawWrappedText(0, 0, 0, 0, "a", Style()),
|
|
"does not fit the");
|
|
EXPECT_DEATH(buffer.DrawWrappedText(2, 0, 4, 6, "a", Style()),
|
|
"does not fit the");
|
|
EXPECT_DEATH(buffer.MeasureWrappedText(0, 0, 0, 11, "a"), "does not fit the");
|
|
}
|
|
|
|
TEST(BufferDeathTest, TextMustBeShortEnoughToMeasure) {
|
|
// Measuring walks the text adding widths, so a long enough run would carry
|
|
// the column past what an `int` holds. Text this long was built rather than
|
|
// read off a line.
|
|
Buffer buffer(10, Charset::Ascii);
|
|
std::string huge(Buffer::MaxTextBytes + 1, 'x');
|
|
EXPECT_DEATH(buffer.DrawText(0, 0, huge, Style()), "is past the");
|
|
EXPECT_DEATH(buffer.MeasureText(0, 0, huge), "is past the");
|
|
}
|
|
|
|
TEST(BufferTest, WriteTo) {
|
|
Buffer buffer(20, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "hello", Style().Bold());
|
|
buffer.DrawText(0, 1, "world", Style());
|
|
|
|
auto dir = Filesystem::MakeTmpDir();
|
|
ASSERT_TRUE(dir.ok()) << dir.error();
|
|
auto file = dir->OpenWriteOnly("out", Filesystem::CreationOptions::CreateNew);
|
|
ASSERT_TRUE(file.ok()) << file.error();
|
|
auto written = buffer.WriteTo(*file, ColorMode::Ansi16);
|
|
EXPECT_TRUE(written.ok()) << written.error();
|
|
(*std::move(file)).Close().Check();
|
|
|
|
// Byte for byte what `Render` produces.
|
|
auto read_back = dir->ReadFileToString("out");
|
|
ASSERT_TRUE(read_back.ok()) << read_back.error();
|
|
EXPECT_EQ(*read_back, Render(buffer, ColorMode::Ansi16));
|
|
EXPECT_EQ(*read_back, "\x1b[1mhello\n\x1b[0mworld\n");
|
|
}
|
|
|
|
TEST(BufferTest, StyleCarriesAcrossRows) {
|
|
// A style is usually still in use on the row below, so turning it off at the
|
|
// end of a row and back on at the start of the next costs a reset and a fresh
|
|
// start for nothing.
|
|
Buffer buffer(4, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "ab", Style().Bold());
|
|
buffer.DrawText(0, 1, "cd", Style().Bold());
|
|
EXPECT_EQ(Render(buffer, ColorMode::Ansi16), "\x1b[1mab\ncd\x1b[0m\n");
|
|
}
|
|
|
|
TEST(BufferTest, StyleThatPaintsBlanksStopsAtTheRowEnd) {
|
|
// A terminal fills the rest of a row with the background it is in when the
|
|
// row ends, so a style that paints where there is no glyph can't be left on
|
|
// across the newline the way an attribute that only affects a glyph can.
|
|
Buffer buffer(4, Charset::Ascii);
|
|
buffer.DrawText(0, 0, "ab", Style().Background(AnsiColor::Red));
|
|
buffer.DrawText(0, 1, "cd", Style().Background(AnsiColor::Red));
|
|
EXPECT_EQ(Render(buffer, ColorMode::Ansi16),
|
|
"\x1b[41mab\x1b[0m\n"
|
|
"\x1b[41mcd\x1b[0m\n");
|
|
}
|
|
|
|
TEST(BufferTest, RenderAppends) {
|
|
// Rendering appends, so several buffers can be gathered into one write.
|
|
Buffer first(10, Charset::Ascii);
|
|
first.DrawText(0, 0, "one", Style());
|
|
Buffer second(10, Charset::Ascii);
|
|
second.DrawText(0, 0, "two", Style());
|
|
|
|
llvm::SmallString<64> out;
|
|
first.Render(out, ColorMode::NoColor);
|
|
second.Render(out, ColorMode::NoColor);
|
|
EXPECT_EQ(std::string(out), "one\ntwo\n");
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace Carbon::Terminal
|