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:
Chandler Carruth
2026-08-21 22:19:12 +00:00
committed by GitHub
parent c41033c315
commit 40aa4419c0
6 changed files with 254 additions and 59 deletions
+43 -13
View File
@@ -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.