Extend and adjust the terminal API for the diagnostics rendering (#7993)

The rendering we're trying to implement based on
`toolchain/docs/diagnostics_rendering.md` needs some features the
initial terminal API didn't support:

- Heavy stroke weight on lines
- Vertical lines that cross earlier lines without joining them
- Wrapping that keeps quoted code whole

This PR adds support for these and removes the two `buffer.h`
TODOs that described them.

This PR also tries to improve the ASCII fallback quality a bit by using
`^`-lines for "heavy" weight, and using `.` and `'` when drawing tees.
Still not perfect of course, but seems reasonable given the constraints
of ASCII rendering.

Assisted-by: Claude Code
This commit is contained in:
Chandler Carruth authored and GitHub committed 2026-10-30 07:25:03 +00:00
1 parent 39e69f0b4f
commit fda41783e5
5 files changed
+561 -128

No files matched your search

+193 -77
View File
@@ -7,14 +7,13 @@
#include <algorithm>
#include <array>
#include <cstdint>
#include <string_view>
#include <utility>
#include "common/check.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/Sequence.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/Support/ConvertUTF.h"
#include "llvm/Support/Unicode.h"
namespace Carbon::Terminal {
@@ -23,59 +22,77 @@ namespace Carbon::Terminal {
// let a single column of output carry unbounded bytes.
static constexpr size_t MaxCombiningBytes = 32;
// Glyphs for every combination of line directions, indexed by the direction
// bits.
static constexpr std::array<char32_t, 16> Utf8LineGlyphs = {
U'·', // (none): a line between one center and itself, which is a point
U'╴', // left
U'╶', // right
U'─', // left, right
U'╵', // up
U'╯', // left, up
U'╰', // right, up
U'┴', // left, right, up
U'╷', // down
U'╮', // left, down
U'╭', // right, down
U'┬', // left, right, down
U'│', // up, down
U'┤', // left, up, down
U'├', // right, up, down
U'┼', // left, right, up, down
};
// The ASCII stand-ins. Each keeps the axis its line runs through, which leaves
// `+` meaning a crossing and nothing else:
// Glyphs for every combination of line directions and weights, indexed by the
// heavy bits and then the direction bits.
//
// - Running through horizontally is `-`, vertically `|`, and both ways `+`.
// - A tee keeps its through-stroke and leaves the branch to what is drawn
// beside it: the dashes either side of a `|` are what `├` and `┤` reach, and
// the line under a `-` is what a `┬` reaches. Drawing a tee as `+` reads as
// the crossing it is not.
// The first row is the all-light set, whose corners are the rounded forms; a
// corner with any heavy line is square, since Unicode has no rounded heavy
// corner. The other rows match the Unicode character names, which list the
// weight of the line in each direction. `DrawLine` never sets a heavy bit
// without its direction bit, and those entries repeat the glyph with the
// direction present.
static constexpr std::array<std::u32string_view, 16> Utf8LineGlyphs = {
U"·╴╶─╵╯╰┴╷╮╭┬│┤├┼", // All light.
U"╸╸╾╾┙┙┵┵┑┑┭┭┥┥┽┽", // Heavy left.
U"╺╼╺╼┕┶┕┶┍┮┍┮┝┾┝┾", // Heavy right.
U"━━━━┷┷┷┷┯┯┯┯┿┿┿┿", // Heavy left and right.
U"╹┚┖┸╹┚┖┸╿┦┞╀╿┦┞╀", // Heavy up.
U"┛┛┹┹┛┛┹┹┩┩╃╃┩┩╃╃", // Heavy left and up.
U"┗┺┗┺┗┺┗┺┡╄┡╄┡╄┡╄", // Heavy right and up.
U"┻┻┻┻┻┻┻┻╇╇╇╇╇╇╇╇", // Heavy left, right, and up.
U"╻┒┎┰╽┧┟╁╻┒┎┰╽┧┟╁", // Heavy down.
U"┓┓┱┱┪┪╅╅┓┓┱┱┪┪╅╅", // Heavy left and down.
U"┏┲┏┲┢╆┢╆┏┲┏┲┢╆┢╆", // Heavy right and down.
U"┳┳┳┳╈╈╈╈┳┳┳┳╈╈╈╈", // Heavy left, right, and down.
U"┃┨┠╂┃┨┠╂┃┨┠╂┃┨┠╂", // Heavy up and down.
U"┫┫╉╉┫┫╉╉┫┫╉╉┫┫╉╉", // Heavy left, up, and down.
U"┣╊┣╊┣╊┣╊┣╊┣╊┣╊┣╊", // Heavy right, up, and down.
U"╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋", // All heavy.
};
// Every row has a glyph for each set of direction bits.
static_assert(std::all_of(Utf8LineGlyphs.begin(), Utf8LineGlyphs.end(),
[](std::u32string_view glyphs) {
return glyphs.size() == 16;
}));
// The ASCII glyphs, which use `+` only for a crossing:
//
// - A line running through horizontally is `-`, vertically `|`, and both ways
// `+`.
// - A corner is `.` where its line leaves downward and `'` where it arrives
// from above, which is where those characters sit in their cells.
// - A tee where a line leaves a vertical line sideways, `├` or `┤`, is `|`,
// and the dash drawn beside it shows the line leaving. A tee where a line
// leaves a horizontal line upward or downward, `┴` or `┬`, is the corner
// glyph for that direction: the dashes either side show the horizontal line,
// and this cell must show the line leaving it, because the rest of that line
// can be rows away with only gaps between. `+` would look like a crossing.
// - A point, a line between one center and itself, is `.`.
//
// What a diagnostic draws is then still told apart: the rule closing a snippet
// from the one separating two, and the anchor opening a diagnostic from the one
// carrying it on.
// What a diagnostic draws is then still told apart: the line closing a snippet
// from the one separating two, and the line opening a diagnostic from the one
// continuing it.
//
// A cell with a heavy line leaving it is `^` instead, under the rule
// `LineWeight` describes. That covers the top cell of a heavy vertical line, so
// the line is `^` above `|`.
static constexpr std::array<char32_t, 16> AsciiLineGlyphs = {
U'.', // (none): a point
U'-', // left
U'-', // right
U'-', // left, right
U'|', // up
U'\'', // left, up
U'\'', // right, up
U'-', // left, right, up
U'|', // down
U'.', // left, down
U'.', // right, down
U'-', // left, right, down
U'|', // up, down
U'|', // left, up, down
U'|', // right, up, down
U'+', // left, right, up, down
U'.', // None: a point.
U'-', // Left.
U'-', // Right.
U'-', // Left and right.
U'|', // Up.
U'\'', // Left and up.
U'\'', // Right and up.
U'\'', // Left, right, and up.
U'|', // Down.
U'.', // Left and down.
U'.', // Right and down.
U'.', // Left, right, and down.
U'|', // Up and down.
U'|', // Left, up, and down.
U'|', // Right, up, and down.
U'+', // Left, right, up, and down.
};
// Returns the next tab stop after `x` on a line whose stops are `tab_width`
@@ -241,13 +258,8 @@ auto Buffer::PlaceCodePoint(int x, int y, char32_t code_point,
return x + width;
}
// Returns the glyphs a cell's directions are read from.
static auto LineGlyphs(Charset charset) -> const std::array<char32_t, 16>& {
return charset == Charset::Utf8 ? Utf8LineGlyphs : AsciiLineGlyphs;
}
auto Buffer::DrawLine(int x, int y, uint8_t directions, const Style& style)
-> void {
auto Buffer::DrawLine(int x, int y, uint8_t directions, LineWeight weight,
const Style& style) -> void {
CARBON_DCHECK(directions <= LineDirections,
"Direction bits {0} name no glyph.", directions);
EnsureColumn(x);
@@ -262,7 +274,22 @@ auto Buffer::DrawLine(int x, int y, uint8_t directions, const Style& style)
Cell& cell = CellAt(x, y);
cell.lines = existing | directions | LineCell;
cell.code_point = LineGlyphs(metrics_.charset())[cell.lines & LineDirections];
cell.heavy_lines &= ~directions;
if (weight == LineWeight::Heavy) {
cell.heavy_lines |= directions;
}
uint8_t cell_directions = cell.lines & LineDirections;
bool line_enters_from_above = (cell_directions & LineUp) != 0;
bool light_line_leaves_downward =
(cell_directions & LineDown) != 0 && (cell.heavy_lines & LineDown) == 0;
if (metrics_.charset() == Charset::Utf8) {
cell.code_point = Utf8LineGlyphs[cell.heavy_lines][cell_directions];
} else if (cell.heavy_lines != 0 && !line_enters_from_above &&
!light_line_leaves_downward) {
cell.code_point = U'^';
} else {
cell.code_point = AsciiLineGlyphs[cell_directions];
}
cell.style = style;
}
@@ -279,7 +306,8 @@ static auto CheckLineFits(int position, int length, int limit) -> void {
}
auto Buffer::DrawHorizontalLine(int x, int y, int length, const Style& style,
LineEnd start, LineEnd end) -> DrawEnd {
LineEnd start, LineEnd end, LineWeight weight)
-> DrawEnd {
CheckOrigin(x, y);
CheckLineFits(x, length, columns_);
for (int i : llvm::seq(length)) {
@@ -289,20 +317,76 @@ auto Buffer::DrawHorizontalLine(int x, int y, int length, const Style& style,
uint8_t directions =
(i > 0 || start == LineEnd::Edge ? LineLeft : 0) |
(i + 1 < length || end == LineEnd::Edge ? LineRight : 0);
DrawLine(x + i, y, directions, style);
DrawLine(x + i, y, directions, weight, style);
}
return {.x = x + length, .y = y};
}
auto Buffer::DrawVerticalLine(int x, int y, int length, const Style& style,
LineEnd start, LineEnd end) -> DrawEnd {
LineEnd start, LineEnd end, LineWeight weight)
-> DrawEnd {
CheckOrigin(x, y);
CheckLineFits(y, length, MaxRows);
for (int i : llvm::seq(length)) {
uint8_t directions =
(i > 0 || start == LineEnd::Edge ? LineUp : 0) |
(i + 1 < length || end == LineEnd::Edge ? LineDown : 0);
DrawLine(x, y + i, directions, style);
DrawLine(x, y + i, directions, weight, style);
}
return {.x = x, .y = y + length};
}
auto Buffer::HoldsText(int x, int y) const -> bool {
if (x < 0 || x >= width_ || y >= height()) {
return false;
}
const Cell& cell = CellAt(x, y);
if ((cell.lines & LineCell) != 0) {
return false;
}
if (cell.code_point != U' ' || cell.is_continuation) {
return true;
}
return !combining_marks_.empty() &&
combining_marks_.contains(CellIndex(x, y));
}
auto Buffer::DrawCrossingVerticalLine(int x, int y, int length,
const Style& style, LineEnd start)
-> DrawEnd {
CheckOrigin(x, y);
CheckLineFits(y, length, MaxRows);
for (int i : llvm::seq(length)) {
int cell_y = y + i;
bool is_center_start = i == 0 && start == LineEnd::Center;
uint8_t directions = is_center_start ? LineDown : (LineUp | LineDown);
if (cell_y >= height()) {
// Rows below the buffer hold nothing drawn earlier.
DrawLine(x, cell_y, directions, LineWeight::Light, style);
continue;
}
const Cell& cell = CellAt(x, cell_y);
if (is_center_start) {
// A line already leaving this cell downward is left as it is, keeping
// its weight. Otherwise this line joins the cell.
if ((cell.lines & LineDown) == 0) {
DrawLine(x, cell_y, LineDown, LineWeight::Light, style);
}
continue;
}
if ((cell.lines & LineCell) != 0) {
// An earlier line keeps its cell: a crossed line stays unbroken, and a
// line already running down this column keeps its weight.
continue;
}
// Skip text, and blank cells beside text, where a line could be mistaken
// for part of a word.
if (HoldsText(x, cell_y) || HoldsText(x - 1, cell_y) ||
HoldsText(x + 1, cell_y)) {
continue;
}
DrawLine(x, cell_y, directions, LineWeight::Light, style);
}
return {.x = x, .y = y + length};
}
@@ -377,18 +461,49 @@ auto Buffer::MeasureText(int x, int y, int margin, llvm::StringRef text) const
// Returns whether wrapped text can be broken at `c`.
//
// This is the one definition of where wrapping may introduce a break, so that
// measuring what text wraps into and drawing it wrapped agree about it.
// Carriage returns count so that a CRLF ending is whitespace rather than part
// of the word before it; what becomes of the `\r` is then up to the drawing.
static constexpr auto IsWrapBreak(char c) -> bool {
return c == ' ' || c == '\t' || c == '\r';
}
// Removes the word at the front of `text` and returns it: everything up to the
// next break or newline, except that with `backticks_are_quotes`, a pair of
// backticks on one line keeps everything between them in the word. When
// `text` starts with a break or a newline, returns an empty word and leaves
// `text` unchanged, so the caller has to step past that character itself.
//
// With `IsWrapBreak` this is the one definition of where wrapping may
// introduce a break, so that measuring what text wraps into and drawing it
// wrapped agree about it.
static auto TakeWord(llvm::StringRef& text, bool backticks_are_quotes)
-> llvm::StringRef {
size_t end = 0;
while (end < text.size()) {
char c = text[end];
if (c == '\n' || IsWrapBreak(c)) {
break;
}
if (backticks_are_quotes && c == '`') {
size_t close = text.find('`', end + 1);
if (close != llvm::StringRef::npos &&
text.slice(end + 1, close).find_first_of("\t\n\r") ==
llvm::StringRef::npos) {
end = close + 1;
continue;
}
}
++end;
}
llvm::StringRef word = text.take_front(end);
text = text.drop_front(end);
return word;
}
template <typename PlaceFn>
auto Buffer::WalkWrappedText(int x, int y, int margin, int max_width,
llvm::StringRef text, PlaceFn place) const
-> DrawEnd {
llvm::StringRef text, bool backticks_are_quotes,
PlaceFn place) const -> DrawEnd {
CheckTextSize(text);
// The block runs from the margin to `margin + max_width`, lies within the
// buffer, and holds the column the text starts in, which is every bound on
@@ -452,9 +567,7 @@ auto Buffer::WalkWrappedText(int x, int y, int margin, int max_width,
continue;
}
llvm::StringRef word =
text.take_until([](char c) { return c == '\n' || IsWrapBreak(c); });
text = text.drop_front(word.size());
llvm::StringRef word = TakeWord(text, backticks_are_quotes);
// Move a word that doesn't fit down to the next row, which minimizes the
// overhang when it doesn't fit there either. The word is drawn into the row
@@ -474,9 +587,9 @@ auto Buffer::WalkWrappedText(int x, int y, int margin, int max_width,
}
auto Buffer::DrawWrappedText(int x, int y, int margin, int max_width,
llvm::StringRef text, const Style& style)
-> DrawEnd {
return WalkWrappedText(x, y, margin, max_width, text,
llvm::StringRef text, const Style& style,
bool backticks_are_quotes) -> DrawEnd {
return WalkWrappedText(x, y, margin, max_width, text, backticks_are_quotes,
[&](int cur_x, int cur_y, char32_t code_point) {
return PlaceCodePoint(cur_x, cur_y, code_point,
style);
@@ -484,20 +597,23 @@ auto Buffer::DrawWrappedText(int x, int y, int margin, int max_width,
}
auto Buffer::MeasureWrappedText(int x, int y, int margin, int max_width,
llvm::StringRef text) const -> DrawEnd {
return WalkWrappedText(x, y, margin, max_width, text,
llvm::StringRef text,
bool backticks_are_quotes) const -> DrawEnd {
return WalkWrappedText(x, y, margin, max_width, text, backticks_are_quotes,
[&](int cur_x, int /*cur_y*/, char32_t code_point) {
return cur_x + metrics_.CodePointWidth(code_point);
});
}
auto Buffer::MeasureWrapWidth(llvm::StringRef text) const -> int {
auto Buffer::MeasureWrapWidth(llvm::StringRef text,
bool backticks_are_quotes) const -> int {
int width = 0;
while (!text.empty()) {
llvm::StringRef word =
text.take_until([](char c) { return c == '\n' || IsWrapBreak(c); });
llvm::StringRef word = TakeWord(text, backticks_are_quotes);
width = std::max(width, metrics_.Width(word));
text = text.drop_front(std::max<size_t>(word.size(), 1));
if (word.empty()) {
text = text.drop_front();
}
}
return width;
}
+71 -20
View File
@@ -47,6 +47,20 @@ enum class LineEnd : int8_t {
Edge,
};
// The stroke weight a line is drawn with.
//
// Weight is recorded for each direction a line leaves a cell in, and Unicode
// has a glyph for every mix of light and heavy lines meeting in a cell, so
// where a heavy line meets a light one each keeps its weight. ASCII has no
// weights: a cell with a heavy line leaving it is `^`, the underline `rustc`
// uses for a primary range, unless a line enters it from above or a light line
// leaves it downward. Those cells keep their light glyph, so the junction or
// corner stays visible.
enum class LineWeight : int8_t {
Light,
Heavy,
};
// A grid of styled cells staged for rendering to a terminal.
//
// Coordinates are 0-based with (0, 0) at the top left, `x` counting terminal
@@ -247,14 +261,17 @@ class Buffer {
// The block and the origin are checked as drawing checks them, so measuring
// answers only for arguments drawing would accept.
auto MeasureWrappedText(int x, int y, int margin, int max_width,
llvm::StringRef text) const -> DrawEnd;
llvm::StringRef text,
bool backticks_are_quotes = false) const -> DrawEnd;
// Returns the fewest columns `text` wraps into without overhanging them,
// which is the width of its widest word since wrapping never breaks one.
// `backticks_are_quotes` is as for `DrawWrappedText`.
//
// Wrapping into fewer columns still draws everything; the excess overhangs.
// So this is a layout preference rather than a minimum.
auto MeasureWrapWidth(llvm::StringRef text) const -> int;
auto MeasureWrapWidth(llvm::StringRef text,
bool backticks_are_quotes = false) const -> int;
// Draws `code_point` at (x, y), which must be a non-negative column and a row
// inside `MaxRows`, widening the buffer and adding rows as needed to reach
@@ -286,22 +303,42 @@ class Buffer {
// containing `-` or `+` is never redrawn as line art.
//
// A cell's style is whatever was drawn there last, so crossing lines of
// different styles do depend on order.
//
// TODO: Every line is drawn with one stroke, and a line meeting another
// joins it. Diagnostic rendering needs a heavier stroke, to mark the source
// a message is about, and a line that passes another without connecting, to
// reach a label past the marks between them. Add a weight to these calls
// and a crossing form of `DrawVerticalLine`.
// different styles depend on order. Weight is kept per direction instead:
// each direction a line leaves a cell in has the weight of the line drawn
// there last, so crossing lines keep their own weights in either order, and
// a heavy line meeting a light one forms a mixed junction.
auto DrawHorizontalLine(int x, int y, int length, const Style& style,
LineEnd start = LineEnd::Center,
LineEnd end = LineEnd::Center) -> DrawEnd;
LineEnd end = LineEnd::Center,
LineWeight weight = LineWeight::Light) -> DrawEnd;
// Draws a vertical line down `length` rows starting at (x, y), with the same
// meaning for its ends. Returns the row after it, in the column it ran down.
auto DrawVerticalLine(int x, int y, int length, const Style& style,
LineEnd start = LineEnd::Center,
LineEnd end = LineEnd::Center) -> DrawEnd;
LineEnd end = LineEnd::Center,
LineWeight weight = LineWeight::Light) -> DrawEnd;
// Draws a light vertical line down `length` rows starting at (x, y), behind
// everything drawn before it. The line runs out through the bottom of its
// last cell.
//
// The line is drawn only into blank cells with no text directly left or
// right of them. Everything drawn earlier stays as it was, so where this
// line crosses another, the other line is unbroken and this one has a gap,
// and the two don't join. A line beside text could be mistaken for part of
// the text, which is why those cells are skipped too.
//
// With `start` at `LineEnd::Center`, the first cell is the exception: the
// line leaves it downward and joins what it holds, as `DrawVerticalLine`
// would. If a line already leaves that cell downward, the cell is left
// unchanged, so that line keeps its weight. With `start` at `LineEnd::Edge`,
// the line enters its first cell from above like the rest, which continues
// a line broken off above it.
//
// Returns the row after the line, in the column it ran down.
auto DrawCrossingVerticalLine(int x, int y, int length, const Style& style,
LineEnd start = LineEnd::Center) -> DrawEnd;
// Draws the outline of a box with its top-left corner at (x, y).
//
@@ -365,6 +402,12 @@ class Buffer {
// word, and one too long for a row of its own is moved down to one and then
// overhangs it rather than being broken.
//
// With `backticks_are_quotes`, backticks pair up from the left on each line
// and no break falls between a pair, so a quoted type with spaces in it
// wraps whole or overhangs, as a URL does. A backtick with no partner later
// on its line, or with a tab or carriage return before its partner, is an
// ordinary character.
//
// Whitespace stops at the block's edge rather than running past it, so the
// spaces between two words stay on the row the first of them ended and the
// row the second wraps onto begins at the margin. Spaces the text opens with,
@@ -397,12 +440,9 @@ class Buffer {
// with such text, since which breaks a reflow may discard -- every newline,
// or only those a previous wrapping introduced -- is a question about where
// that text came from.
//
// TODO: A break can fall inside a piece of code the text quotes, splitting a
// type written with spaces in it across two rows. Take a quote character and
// keep what a pair of them encloses in one word, as a URL already is.
auto DrawWrappedText(int x, int y, int margin, int max_width,
llvm::StringRef text, const Style& style) -> DrawEnd;
llvm::StringRef text, const Style& style,
bool backticks_are_quotes = false) -> DrawEnd;
// Renders the grid, appending the bytes that draw it to `out`.
//
@@ -444,7 +484,7 @@ class Buffer {
struct Cell {
// The code point rendered here. For a cell with `lines` set, this is
// derived from those bits and the charset.
// derived from `lines`, `heavy_lines`, and the charset.
char32_t code_point = ' ';
Style style;
@@ -453,6 +493,10 @@ class Buffer {
// or zero for a cell holding text.
uint8_t lines = 0;
// The directions in `lines` that a heavy line leaves in. Always a subset
// of the directions set there.
uint8_t heavy_lines = 0;
// Whether this cell is the right half of a double-width character, and so
// renders nothing of its own.
bool is_continuation = false;
@@ -511,7 +555,8 @@ class Buffer {
PlaceFn place) const -> DrawEnd;
template <typename PlaceFn>
auto WalkWrappedText(int x, int y, int margin, int max_width,
llvm::StringRef text, PlaceFn place) const -> DrawEnd;
llvm::StringRef text, bool backticks_are_quotes,
PlaceFn place) const -> DrawEnd;
// Adds rows until row `y` exists.
auto EnsureRow(int y) -> void;
@@ -528,8 +573,14 @@ class Buffer {
// Appends `code_point` to the marks rendered with the cell before column `x`.
auto AttachCombiningMark(int x, int y, char32_t code_point) -> void;
// Adds `directions` to the lines through (x, y) and updates its glyph.
auto DrawLine(int x, int y, uint8_t directions, const Style& style) -> void;
// Adds `directions` to the lines through (x, y) at `weight`, replacing the
// weight those directions had, and updates its glyph.
auto DrawLine(int x, int y, uint8_t directions, LineWeight weight,
const Style& style) -> void;
// Returns whether the cell at (x, y) holds text. Cells outside what has been
// drawn hold nothing.
auto HoldsText(int x, int y) const -> bool;
// Returns the last column in row `y` that renders anything under `mode`, or
// -1 when the row renders nothing.
+271 -5
View File
@@ -6,7 +6,6 @@
#include <gtest/gtest.h>
#include <limits>
#include <optional>
#include <string>
@@ -171,18 +170,222 @@ TEST(BufferTest, Tees) {
"│ │ │\n"
"╰─┴─╯\n");
// The same table in ASCII, where every tee keeps its through-stroke and only
// the crossing in the middle is a `+`.
// The same table in ASCII: only the crossing in the middle is a `+`, a tee
// where a line leaves sideways is `|`, and a tee where a line leaves upward
// or downward is the corner glyph for that direction.
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"
"'---'\n");
"'-'-'\n");
}
TEST(BufferTest, HeavyLines) {
// A heavy line keeps the same geometry as a light one, drawn in the heavy
// glyphs, ends included.
Buffer heavy(4, Charset::Utf8);
heavy.DrawHorizontalLine(0, 0, 4, Style(), LineEnd::Center, LineEnd::Center,
LineWeight::Heavy);
heavy.DrawVerticalLine(0, 1, 3, Style(), LineEnd::Center, LineEnd::Center,
LineWeight::Heavy);
EXPECT_EQ(Render(heavy),
"╺━━╸\n"
"╻\n"
"┃\n"
"╹\n");
// Heavy lines join each other the way light ones do, into heavy junctions.
Buffer joined(5, Charset::Utf8);
joined.DrawHorizontalLine(0, 0, 5, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
joined.DrawVerticalLine(2, 0, 3, Style(), LineEnd::Center, LineEnd::Center,
LineWeight::Heavy);
joined.DrawHorizontalLine(2, 2, 3, Style(), LineEnd::Center, LineEnd::Edge,
LineWeight::Heavy);
EXPECT_EQ(Render(joined),
"━━┳━━\n"
" ┃\n"
" ┗━━\n");
}
TEST(BufferTest, HeavyAndLightLinesMix) {
// Weight is recorded per direction, so a heavy line crossing a light one
// draws a junction that is heavy left and right and light up and down.
Buffer cross(3, Charset::Utf8);
cross.DrawHorizontalLine(0, 1, 3, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
cross.DrawVerticalLine(1, 0, 3, Style(), LineEnd::Edge, LineEnd::Edge);
EXPECT_EQ(Render(cross),
" │\n"
"━┿━\n"
" │\n");
// A light line leaving a heavy one downward: each line keeps its own weight
// in the tee.
Buffer tee(4, Charset::Utf8);
tee.DrawHorizontalLine(0, 0, 4, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
tee.DrawVerticalLine(1, 0, 2, Style(), LineEnd::Center, LineEnd::Center);
EXPECT_EQ(Render(tee),
"━┯━━\n"
" ╵\n");
// A line's weight in a cell is that of whatever drew it last, as a cell's
// style is: a heavy line drawn over a light one leaves it heavy, and a light
// one drawn over part of a heavy one lightens the stretch it covers and
// nothing else.
Buffer overdrawn(3, Charset::Utf8);
overdrawn.DrawHorizontalLine(0, 0, 3, Style());
overdrawn.DrawHorizontalLine(0, 0, 3, Style(), LineEnd::Center,
LineEnd::Center, LineWeight::Heavy);
EXPECT_EQ(Render(overdrawn), "╺━╸\n");
Buffer lightened(4, Charset::Utf8);
lightened.DrawHorizontalLine(0, 0, 4, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
lightened.DrawHorizontalLine(2, 0, 2, Style(), LineEnd::Edge, LineEnd::Edge);
EXPECT_EQ(Render(lightened), "━━──\n");
}
TEST(BufferTest, HeavyLinesInAscii) {
// ASCII has no weights, so a heavy horizontal line becomes `^`, including
// the tee where a heavy vertical line leaves it.
Buffer underline(8, Charset::Ascii);
underline.DrawHorizontalLine(0, 0, 7, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
underline.DrawVerticalLine(3, 0, 2, Style(), LineEnd::Center, LineEnd::Center,
LineWeight::Heavy);
EXPECT_EQ(Render(underline),
"^^^^^^^\n"
" |\n");
// So does the top cell of a heavy vertical line, which no line enters from
// above, so the line is `^` above `|`.
Buffer vertical(3, Charset::Ascii);
vertical.DrawVerticalLine(1, 0, 3, Style(), LineEnd::Center, LineEnd::Center,
LineWeight::Heavy);
EXPECT_EQ(Render(vertical),
" ^\n"
" |\n"
" |\n");
// A light line leaving the heavy underline downward keeps the light tee's
// `.`, which `^` would hide, while a heavy line leaving it downward keeps the
// `^`.
Buffer tees(7, Charset::Ascii);
tees.DrawHorizontalLine(0, 0, 7, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
tees.DrawVerticalLine(2, 0, 2, Style(), LineEnd::Center, LineEnd::Center);
tees.DrawVerticalLine(5, 0, 2, Style(), LineEnd::Center, LineEnd::Center,
LineWeight::Heavy);
EXPECT_EQ(Render(tees),
"^^.^^^^\n"
" | |\n");
// A cell a line enters from above keeps its light glyph, so the crossing is
// `+`, and the heavy vertical line is drawn as a light one.
Buffer crossed(3, Charset::Ascii);
crossed.DrawHorizontalLine(0, 1, 3, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
crossed.DrawVerticalLine(1, 0, 3, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
EXPECT_EQ(Render(crossed),
" |\n"
"^+^\n"
" |\n");
}
TEST(BufferTest, CrossingLinePassesBehindEarlierLines) {
// A crossing line joins the line its first cell holds. Below that, it has a
// gap where it crosses an earlier line, which stays unbroken, and the two
// don't join.
Buffer buffer(5, Charset::Utf8);
buffer.DrawHorizontalLine(0, 0, 5, Style(), LineEnd::Edge, LineEnd::Edge);
buffer.DrawHorizontalLine(0, 2, 5, Style(), LineEnd::Edge, LineEnd::Edge);
buffer.DrawCrossingVerticalLine(2, 0, 4, Style());
EXPECT_EQ(Render(buffer),
"──┬──\n"
" │\n"
"─────\n"
" │\n");
// The same crossing in ASCII, where the tee is the corner glyph for the line
// leaving downward and the crossed line is unbroken.
Buffer ascii(5, Charset::Ascii);
ascii.DrawHorizontalLine(0, 0, 5, Style(), LineEnd::Edge, LineEnd::Edge);
ascii.DrawHorizontalLine(0, 2, 5, Style(), LineEnd::Edge, LineEnd::Edge);
ascii.DrawCrossingVerticalLine(2, 0, 4, Style());
EXPECT_EQ(Render(ascii),
"--.--\n"
" |\n"
"-----\n"
" |\n");
// Weight plays no part in it: a heavy line drawn first stays whole the
// same way.
Buffer heavy(5, Charset::Utf8);
heavy.DrawHorizontalLine(0, 0, 5, Style(), LineEnd::Edge, LineEnd::Edge);
heavy.DrawHorizontalLine(0, 1, 5, Style(), LineEnd::Edge, LineEnd::Edge,
LineWeight::Heavy);
heavy.DrawCrossingVerticalLine(2, 0, 4, Style());
EXPECT_EQ(Render(heavy),
"──┬──\n"
"━━━━━\n"
" │\n"
" │\n");
}
TEST(BufferTest, CrossingLineResumesFromAnEdge) {
// A crossing line starting at an edge enters its first cell from above, so
// it continues a line broken off above a stretch it skipped, and it still
// has a gap where it crosses an earlier line.
Buffer buffer(5, Charset::Utf8);
buffer.DrawHorizontalLine(0, 0, 5, Style(), LineEnd::Edge, LineEnd::Edge);
buffer.DrawText(0, 1, "words", Style());
buffer.DrawHorizontalLine(0, 3, 5, Style(), LineEnd::Edge, LineEnd::Edge);
buffer.DrawCrossingVerticalLine(2, 0, 1, Style());
buffer.DrawCrossingVerticalLine(2, 2, 3, Style(), LineEnd::Edge);
EXPECT_EQ(Render(buffer),
"──┬──\n"
"words\n"
" │\n"
"─────\n"
" │\n");
}
TEST(BufferTest, CrossingLineSharesAColumnWithoutRedrawing) {
// A crossing line leaves the cells of an earlier line in its column as they
// are, so that line keeps its weight.
Buffer buffer(3, Charset::Utf8);
buffer.DrawHorizontalLine(0, 0, 3, Style(), LineEnd::Edge, LineEnd::Edge);
buffer.DrawVerticalLine(1, 0, 3, Style(), LineEnd::Center, LineEnd::Center,
LineWeight::Heavy);
buffer.DrawCrossingVerticalLine(1, 0, 3, Style());
EXPECT_EQ(Render(buffer),
"─┰─\n"
" ┃\n"
" ╹\n");
}
TEST(BufferTest, CrossingLineStaysClearOfText) {
// The line skips text, and blank cells directly beside text, where a line
// could be mistaken for part of a word.
Buffer buffer(6, Charset::Utf8);
buffer.DrawHorizontalLine(0, 0, 6, Style(), LineEnd::Edge, LineEnd::Edge);
buffer.DrawText(0, 1, "ab", Style());
buffer.DrawText(0, 3, "abc", Style());
buffer.DrawCrossingVerticalLine(2, 0, 5, Style());
EXPECT_EQ(Render(buffer),
"──┬───\n"
"ab\n"
" │\n"
"abc\n"
" │\n");
}
TEST(BufferTest, ALineBetweenOneCenterAndItselfIsAPoint) {
@@ -673,6 +876,69 @@ TEST(BufferTest, TabWidthComesFromCapabilities) {
EXPECT_EQ(Render(clamped), "a b\n");
}
TEST(BufferTest, WrappedTextKeepsAQuotedRunWhole) {
// With backticks as quotes, the spaces between a pair of backticks are not
// breaks, so the quoted run wraps to the next row whole.
Buffer buffer(20, Charset::Utf8);
buffer.DrawWrappedText(0, 0, 0, 20, "type `{.a: i32}` is not", Style(),
/*backticks_are_quotes=*/true);
EXPECT_EQ(Render(buffer),
"type `{.a: i32}` is\n"
"not\n");
Buffer split(14, Charset::Utf8);
split.DrawWrappedText(0, 0, 0, 14, "type `{.a: i32}` is not", Style(),
/*backticks_are_quotes=*/true);
EXPECT_EQ(Render(split),
"type\n"
"`{.a: i32}` is\n"
"not\n");
// Otherwise, the same text breaks inside the quotes like anywhere else.
Buffer unquoted(14, Charset::Utf8);
unquoted.DrawWrappedText(0, 0, 0, 14, "type `{.a: i32}` is not", Style());
EXPECT_EQ(Render(unquoted),
"type `{.a:\n"
"i32}` is not\n");
// A backtick with no partner on its line is an ordinary character, so the
// text after it still breaks at its spaces.
Buffer stray(14, Charset::Utf8);
stray.DrawWrappedText(0, 0, 0, 14, "a `stray quote and more words", Style(),
/*backticks_are_quotes=*/true);
EXPECT_EQ(Render(stray),
"a `stray quote\n"
"and more words\n");
// Nor does a run reach across a newline the caller wrote.
Buffer lines(14, Charset::Utf8);
lines.DrawWrappedText(0, 0, 0, 14, "a `b\nc d` e f g h", Style(),
/*backticks_are_quotes=*/true);
EXPECT_EQ(Render(lines),
"a `b\n"
"c d` e f g h\n");
// A word can't hold a tab or carriage return, so a quoted run doesn't reach
// across one either.
Buffer tabbed(14, Charset::Utf8);
tabbed.DrawWrappedText(0, 0, 0, 14, "a `b\tc` d", Style(),
/*backticks_are_quotes=*/true);
EXPECT_EQ(Render(tabbed), "a `b c` d\n");
EXPECT_EQ(buffer.MeasureWrapWidth("`b\tc`", /*backticks_are_quotes=*/true),
2);
EXPECT_EQ(buffer.MeasureWrapWidth("`b\rc`", /*backticks_are_quotes=*/true),
2);
// Measuring agrees: the quoted run is the widest word.
EXPECT_EQ(buffer.MeasureWrapWidth("type `{.a: i32}` is not",
/*backticks_are_quotes=*/true),
11);
EXPECT_EQ(buffer.MeasureWrapWidth("type `{.a: i32}` is not"), 5);
EXPECT_EQ(buffer.MeasureWrappedText(0, 0, 0, 14, "type `{.a: i32}` is not",
/*backticks_are_quotes=*/true),
DrawEnd(3, 2));
}
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
+22 -22
View File
@@ -318,7 +318,7 @@ TEST(RendererTest, MergedShowsOneViewOfTheFile) {
" .-| foo.carbon:4:3\n"
" |\n"
" 1 | fn Run0() {}\n"
" : ------------\n"
" : ------.-----\n"
" : '--| declared here\n"
" :\n"
"-> 4 | Run0(1);\n"
@@ -337,7 +337,7 @@ TEST(RendererTest, MergedShowsASingleSkippedLine) {
" .-| foo.carbon:3:1\n"
" |\n"
" 1 | fn Run0() {}\n"
" : --\n"
" : -.\n"
" : '--| here\n"
" 2 |\n"
"-> 3 | fn Main() {\n"
@@ -366,7 +366,7 @@ TEST(RendererTest, NotesInAnotherFileGetTheirOwnAnchor) {
" |-| bar.carbon:9:1\n"
" :\n"
" 9 | class C {\n"
" : ---------\n"
" : ----.----\n"
" : '--| declared here\n"
" '------------\n"
""));
@@ -387,7 +387,7 @@ TEST(RendererTest, APrimaryLabelTeesIntoItsRange) {
" .-| foo.carbon:4:3\n"
" :\n"
"-> 4 | Run0(1);\n"
" : -------\n"
" : ---.---\n"
" : '--| 1 argument passed here\n"
" '------------\n"
""));
@@ -507,7 +507,7 @@ TEST(RendererTest, RepeatedSpansAreOneMark) {
" .-| foo.carbon:4:3\n"
" :\n"
"-> 4 | Run0(1);\n"
" : -------\n"
" : ---.---\n"
" : '--| wrong\n"
" '------------\n"
""));
@@ -528,7 +528,7 @@ TEST(RendererTest, ARangeCoveringOthersIsRepairedRatherThanStacked) {
" .-| foo.carbon:4:3\n"
" :\n"
"-> 4 | Run0(1);\n"
" : -------\n"
" : --.--.-\n"
" : | '--| argument\n"
" : '--| callee\n"
" '------------\n"
@@ -549,7 +549,7 @@ TEST(RendererTest, ALabelOnASingleColumnRangeDropsStraightDown) {
" .-| foo.carbon:4:8\n"
" :\n"
"-> 4 | Run0(1);\n"
" : ---- -\n"
" : --.- .\n"
" : | '--| with this\n"
" : '--| indexed here\n"
" '------------\n"
@@ -851,7 +851,7 @@ TEST(RendererTest, AsciiDrawsAdjacentRangesWithoutTheGap) {
" .-| foo.carbon:4:3\n"
" :\n"
"-> 4 | Run0(1);\n"
" : -------\n"
" : -----.-\n"
" : '--| with these\n"
" '------------\n"
""));
@@ -885,7 +885,7 @@ TEST(RendererTest, TwoSpansOnOneLineShareItsRow) {
" .-| foo.carbon:1:4\n"
" |\n"
"-> 1 | fn Run0() {}\n"
" : -- ----\n"
" : -. ----\n"
" : '--| here\n"
" '------------\n"
""));
@@ -917,7 +917,7 @@ TEST(RendererTest, LabelsOnOneLineHangRightToLeft) {
" .-| foo.carbon:3:16\n"
" :\n"
"-> 3 | return count * flag;\n"
" : ----- ----\n"
" : --.-- --.-\n"
" : | '--| right operand has type `bool`\n"
" : '--| left operand has type `i32`\n"
" '------------\n"
@@ -950,7 +950,7 @@ TEST(RendererTest, SpansOnOneLineShareOneWindow) {
"-> 1 | "
"...xxxxxxxxxxxxxxxxFIRSTxxxxxxxxxxxxxxxSECONDxxxxxxxxxxxxxxxxxxxxxxx."
"..\n"
" : ----- ------\n"
" : ----- ---.--\n"
" : '--| and here\n"
" '------------\n"
""));
@@ -981,7 +981,7 @@ TEST(RendererTest, MultipleFilesEachGetAnAnchor) {
" |-| a.carbon:3:1\n"
" :\n"
" 3 | class Shape {}\n"
" : --------------\n"
" : -------.------\n"
" : '--| previously declared\n"
" '------------\n"
""));
@@ -1016,7 +1016,7 @@ TEST(RendererTest, ContextLeadsIntoTheAnchorItReached) {
" |-| a.carbon:3:1\n"
" :\n"
" 3 | class Shape {}\n"
" : --------------\n"
" : -------.------\n"
" : '--| previously declared\n"
" '------------\n"
""));
@@ -1051,7 +1051,7 @@ TEST(RendererTest, ContextStepsStackInOrder) {
" |-| inner.h:7:6\n"
" :\n"
" 7 | void Use(int, int);\n"
" : ---\n"
" : -.-\n"
" : '--| candidate not viable\n"
" '------------\n"
""));
@@ -1288,7 +1288,7 @@ TEST(RendererTest, AnnotationsTakeTheColorOfTheirMessage) {
// color, so each mark matches the words that explain it. Neither is bold: a
// mark's weight says nothing a terminal renders reliably.
EXPECT_THAT(rendered, HasSubstr("\x1b[93m-------\n"));
EXPECT_THAT(rendered, HasSubstr("\x1b[96m------------\n"));
EXPECT_THAT(rendered, HasSubstr("\x1b[96m------.-----\n"));
// The line the problem is on takes the level's color and its weight. Every
// other line number is as light as the frame it belongs to, so the one being
// reported is the one that stands out.
@@ -1308,7 +1308,7 @@ TEST(RendererTest, TheFrameSeparatorIsFrame) {
// color on every row -- including the note's, whose underline and words are
// the note color beside it. Coloring it per row made it read as belonging to
// whichever range was there rather than to the frame.
EXPECT_THAT(rendered, HasSubstr("\x1b[93m:\x1b[0m \x1b[96m------------"));
EXPECT_THAT(rendered, HasSubstr("\x1b[93m:\x1b[0m \x1b[96m------.-----"));
EXPECT_THAT(rendered, HasSubstr("\x1b[93m:\x1b[0m \x1b[96m'--|"));
}
@@ -1505,7 +1505,7 @@ TEST(RendererTest, LineShownBetweenTwoSpansIsWindowedToo) {
" .-| foo.carbon:3:1\n"
" |\n"
" 1 | short\n"
" : ----\n"
" : --.-\n"
" : '--| and\n"
" 2 | "
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@@ -1738,7 +1738,7 @@ TEST(RendererTest, ALabelIsFramedWhenItWraps) {
" .-| foo.carbon:2:11\n"
" :\n"
"-> 2 | NeedsIt(\"definitely not the right type\");\n"
" : ------------------------------\n"
" : .-----------------------------\n"
" : '--| type `str` does not implement interface\n"
" : | `Core.ImplicitAs(i32)` at all\n"
" '------------\n"
@@ -1829,7 +1829,7 @@ TEST(RendererTest, ARoutedLabelUnderAscii) {
" .-| foo.carbon:2:11\n"
" :\n"
"-> 2 | NeedsIt(\"definitely not the right type\");\n"
" : ------------------------------\n"
" : .-----------------------------\n"
" : .---------'\n"
" : | type `str` does not implement interface\n"
" : | "
@@ -2133,7 +2133,7 @@ TEST(RendererTest, ARangeOnAnotherLineDoesNotStandInForTheLocation) {
" : --\n"
" :\n"
"-> 4 | Run0(1);\n"
" : -------\n"
" : ---.---\n"
" : '--| here\n"
" '------------\n"
""));
@@ -2345,7 +2345,7 @@ TEST(RendererTest, AMergedPairWithoutFileTextStillElides) {
" : --\n"
" :\n"
" 3 | third line\n"
" : --\n"
" : -.\n"
" : '--| here\n"
" '------------\n"
""));
@@ -2408,7 +2408,7 @@ TEST(RendererTest, AContextLeadsTheFrameAndTheMessageHangs) {
"-> 3 | fn Main() {\n"
" : ----\n"
"-> 4 | Run0(1);\n"
" : -------\n"
" : ---.---\n"
" : '--| no matching call\n"
" '------------\n"
""));
+4 -4
View File
@@ -48,7 +48,7 @@ fn H() {
// CHECK:STDERR: .-| fail_note_in_same_file.carbon:[[@LINE+10]]:3
// CHECK:STDERR: :
// CHECK:STDERR: 4 | fn G() {}
// CHECK:STDERR: : --------
// CHECK:STDERR: : ----.---
// CHECK:STDERR: : '--| calling function declared here [InCallToEntity]
// CHECK:STDERR: :
// CHECK:STDERR: -> 18 | G(1);
@@ -108,7 +108,7 @@ fn Use() {
// CHECK:STDERR: |-| declared.carbon:4:1
// CHECK:STDERR: :
// CHECK:STDERR: 4 | fn Declared();
// CHECK:STDERR: : --------------
// CHECK:STDERR: : -------.------
// CHECK:STDERR: : '--| calling function declared here [InCallToEntity]
// CHECK:STDERR: '------------
// CHECK:STDERR:
@@ -123,7 +123,7 @@ library "[[@TEST_NAME]]";
// CHECK:STDERR: .-| fail_two_spans_on_one_line.carbon:[[@LINE+7]]:27
// CHECK:STDERR: :
// CHECK:STDERR: -> 12 | fn Same() {} fn Twice() { Same(1); }
// CHECK:STDERR: : ----------- -------
// CHECK:STDERR: : -----.----- -------
// CHECK:STDERR: : '--| calling function declared here [InCallToEntity]
// CHECK:STDERR: '------------
// CHECK:STDERR:
@@ -195,7 +195,7 @@ fn CallIt() {
// CHECK:STDERR: |-| ./cpp_decl.h:3:6
// CHECK:STDERR: :
// CHECK:STDERR: 3 | void Use(Widget w, int extra);
// CHECK:STDERR: : - -------------------
// CHECK:STDERR: : . ---------.---------
// CHECK:STDERR: : | '--| requires 2 arguments, but 1 was provided
// CHECK:STDERR: : | | [CppInteropParseNoteReason]
// CHECK:STDERR: : '--| candidate function not viable [CppInteropParseNote]