mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-10-05 22:02:55 +01:00
Update the terminal library for rendering diagnostics (#7659)
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
This commit is contained in:
@@ -170,6 +170,19 @@ TEST(BufferTest, Tees) {
|
||||
"├─┼─┤\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) {
|
||||
@@ -189,10 +202,11 @@ TEST(BufferTest, ALineBetweenOneCenterAndItselfIsAPoint) {
|
||||
joined.DrawHorizontalLine(0, 0, 3, Style());
|
||||
EXPECT_EQ(Render(joined), "╶─╴\n");
|
||||
|
||||
// ASCII has one glyph for everything that isn't a plain segment.
|
||||
// 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");
|
||||
EXPECT_EQ(Render(ascii), " .\n");
|
||||
|
||||
// A line with no length draws nothing at all.
|
||||
Buffer empty(3, Charset::Utf8);
|
||||
@@ -296,13 +310,15 @@ TEST(BufferTest, Box) {
|
||||
"│ │\n"
|
||||
"╰──╯\n");
|
||||
|
||||
// ASCII can only tell horizontal and vertical apart from everything else.
|
||||
// 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"
|
||||
"+--+\n");
|
||||
"'--'\n");
|
||||
|
||||
// A box with no interior is the single line that bounds it.
|
||||
Buffer flat(4, Charset::Utf8);
|
||||
@@ -846,7 +862,8 @@ TEST(BufferTest, MeasureWrapWidth) {
|
||||
EXPECT_EQ(buffer.MeasureWrapWidth("a bb ccc"), 3);
|
||||
EXPECT_EQ(buffer.MeasureWrapWidth(" spaced out "), 6);
|
||||
|
||||
// Newlines and tabs bound a word without taking columns of their own.
|
||||
// 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.
|
||||
@@ -990,24 +1007,37 @@ TEST(BufferDeathTest, WidthMustFitTheGrid) {
|
||||
"Buffer width must be in");
|
||||
}
|
||||
|
||||
TEST(BufferDeathTest, DrawingMustStartInsideTheGrid) {
|
||||
// A caller placing something already knows the width, since it is what
|
||||
// decided the layout, so landing outside it is a bug in that layout rather
|
||||
// than something to quietly drop. Rows are checked the same way.
|
||||
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(10, 0, 'a', Style()), "is outside the");
|
||||
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(10, 0, "a", Style()), "is outside the");
|
||||
EXPECT_DEATH(buffer.MeasureText(10, 0, "a"), "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.
|
||||
|
||||
Reference in New Issue
Block a user