// 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 "toolchain/diagnostics/renderer.h" #include #include #include #include "common/check.h" #include "common/terminal/buffer.h" #include "common/terminal/color.h" #include "common/terminal/metrics.h" #include "common/terminal/style.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/Sequence.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/Twine.h" namespace Carbon::Diagnostics { using Terminal::AnsiColor; using Terminal::Buffer; using Terminal::Charset; using Terminal::LineEnd; using Terminal::Metrics; using Terminal::Style; // The columns from the frame to the content beside it: the frame itself, and // the stub next to it that a source row leaves blank. static constexpr int FrameToContentWidth = 2; // Points at the lines the problem is on -- every line holding a range that is // part of it -- from the margin outside the line numbers. A merged snippet can // open on some other line, so without this there is nothing saying which lines // the problem is actually on. // // Two ASCII characters rather than one of the arrows, which all read better and // none of which measure reliably: they are East Asian Ambiguous, so a font may // draw one across two columns while the width tables call it one, and a // terminal falling back to such a font pushes the whole row over or clips the // glyph. This is two columns because it is two characters, so nothing can // disagree about it, and a font with programming ligatures draws it as an arrow // anyway. static constexpr llvm::StringLiteral LineMark = "->"; // The columns the mark spends before the line numbers. No space after it: the // numbers are right-aligned in a field of their own, so a number shorter than // that field already leaves a gap, and a number filling it is the widest there // is to keep the mark off. static constexpr int MarginWidth = LineMark.size(); // The columns an anchor spends before the location it names: `╭─┤ `. static constexpr int AnchorPrefixWidth = 4; // The columns a connector spends before what it reaches: three of line, and // then either a space or, where it reaches a label, the bar framing it. static constexpr int ConnectorWidth = 4; // The columns the bar framing a label's left side spends before its text: // `┤ `. The bar runs down every row the label takes, so that what belongs to // the label is bounded rather than left to be inferred from indentation. static constexpr int LabelFrameWidth = 2; // The columns between a label's anchor and its text: the connector reaching // the bar that frames it, and the bar's own space before the words. static constexpr int LabelIndentWidth = ConnectorWidth - 1 + LabelFrameWidth; // The columns a rule bracketing a snippet reaches across, starting at the // frame. Long enough to read as a divider rather than as another branch off the // frame, which is what the connector on a context row looks like. static constexpr int SnippetRuleWidth = 13; // The columns of source a diagnostic aims to show: the width code is formatted // to, so a formatted line is shown whole and nothing wider is asked for than // that needs. A line longer than this is windowed, the way one longer than the // terminal is. static constexpr int FormattedSourceColumns = 80; // The columns a line number is given in the gutter, so that the frame runs // down one column for every diagnostic in a file under ten thousand lines // rather than stepping with each number's digits, at a cost of at most three // columns on the smallest files. A number needing more still fits -- the frame // moves right and the source is squeezed by the difference -- so the width // laid out against is the same whatever line a diagnostic lands on. static constexpr int LineNumberColumns = 4; // The most bytes one drawn string may hold. Nothing legitimate comes near it: // it exists so that pathological input -- a generated file with one enormous // line, or a message argument holding one -- degrades by truncation instead of // hitting a limit the terminal buffer CHECKs. It bounds both the bytes of a // single draw and, through the rows a wrapped string of this many bytes takes, // the height a handful of them can reach, so it stays well under the buffer's // row limit. static constexpr size_t MaxDrawnBytes = 1 << 16; // Columns between tab stops, matching what a terminal does with a tab. static constexpr int SourceTabWidth = 8; // What marks each end of a source line that windowing elided. static constexpr llvm::StringLiteral WindowMarker = "..."; // The fewest columns of source a window shows, however narrow the terminal // is. static constexpr int MinWindowColumns = 16; // The most lines shown rather than elided between two spans in one file. One // line of context is worth as much as the row saying it was skipped. static constexpr int MaxShownGap = 1; // The fewest columns a framed diagnostic is drawn in, below which the compact // form is used instead. A frame, its gutter, and a label hanging off a span // need room before any of them say anything, and past some point there isn't // enough to be worth spending on structure. Low enough that a terminal narrow // enough to hit it is one a label would route on anyway. // // Where that point is, is a guess. It is not derived from anything, and no // layout depends on the number being right -- a terminal one column either side // of it gets a rendering that works. Move it if it turns out to be wrong. static constexpr int MinFrameColumns = 60; // Returns the word naming `level`, including its separator. static auto LevelText(Level level) -> llvm::StringRef { switch (level) { case Level::Error: return "error: "; case Level::Warning: return "warning: "; } } // Returns the style the marks under the source take, which is the color of // whatever they belong to without its weight. Bold reaches the line-drawing // glyphs unevenly -- several terminals leave them alone and others only // brighten them -- so the weight says nothing reliable there while it still // says something on words. static auto MarkStyle(const Style& style) -> Style { return style.Bold(false); } // The colors one theme is drawn in: what a diagnostic of that level, or the // notes explaining one, take for their words and for the marks under the source // that belong to them. // // The second is the theme's own color, which a range alone on its line takes. // Where several share a line, the whole ramp is handed out left to right // starting at the dark end, so a color always says which theme a range belongs // to and only then which range it is. using Theme = llvm::ArrayRef; // The themes for a terminal that renders 24-bit color, where the colors are // ours to choose and are chosen together. // // Each theme is a ramp handed out left to right across the ranges on a line: // darker, the theme's own color, lighter. The three centers sit at one // lightness, so the level a diagnostic is reported at is said by hue and by the // word rather than by how loud it is, and lightness varies only inside a theme, // where a step says "a different range" rather than "a more important one". // // There are two palettes because a color picked to read against black is hard // to read against white. Which is used follows `Capabilities::background`. // // See /toolchain/docs/diagnostics_rendering.md#the-palette for where these // values came from and the distances and contrast ratios they satisfy. Changing // one means checking it against those, not against how it looks on a swatch: // these are drawn as marks a pixel tall. static constexpr Terminal::Color DarkBgErrorTheme[] = { {0xb5, 0x56, 0x00}, {0xff, 0x72, 0x61}, {0xff, 0xbc, 0xc8}}; static constexpr Terminal::Color DarkBgWarningTheme[] = { {0x51, 0x86, 0x00}, {0xac, 0xac, 0x00}, {0xff, 0xc9, 0x21}}; static constexpr Terminal::Color DarkBgNoteTheme[] = { {0x00, 0x82, 0x9a}, {0x00, 0xbc, 0xbc}, {0x00, 0xf3, 0xc6}}; static constexpr Terminal::Color LightBgErrorTheme[] = { {0x80, 0x35, 0x00}, {0xd0, 0x00, 0x00}, {0xff, 0x28, 0x6a}}; static constexpr Terminal::Color LightBgWarningTheme[] = { {0x3d, 0x59, 0x00}, {0x73, 0x73, 0x00}, {0xad, 0x8c, 0x00}}; static constexpr Terminal::Color LightBgNoteTheme[] = { {0x00, 0x59, 0x67}, {0x00, 0x7f, 0x7f}, {0x00, 0xa7, 0x8e}}; // The themes for a terminal with only the 16 named colors, which renders them // through the user's own palette. Their appearance is not ours to choose, so // these say only which entry to use and let the user's theme decide the rest -- // including which background it goes with, which is why these don't vary with // it. // // Each is a ramp of two rather than three, the plain entry and then the bright // one, because there is nowhere for a longer one to go. Downsampling the 24-bit // colors instead would be worse than stopping at two: two of them land on // `BrightBlack` and on `White`, which are not that theme or any other. Which // theme a range belongs to is worth more than telling two ranges of one theme // apart, so a ramp runs out and repeats rather than reaching outside its theme. static constexpr Terminal::Color NamedErrorTheme[] = {AnsiColor::Red, AnsiColor::BrightRed}; static constexpr Terminal::Color NamedWarningTheme[] = { AnsiColor::Yellow, AnsiColor::BrightYellow}; static constexpr Terminal::Color NamedNoteTheme[] = {AnsiColor::Cyan, AnsiColor::BrightCyan}; // Returns whether `capabilities` renders colors chosen here rather than the // user's. // // `Ansi256` is on this side of the line because rounding to its cube keeps // every theme's three colors distinct and keeps two colors of different themes // far enough apart to read as different themes -- comfortably so on a dark // background, and closer on a light one, where an error and a warning range // sharing a line is the arrangement to watch. `Ansi16` keeps neither, which is // why it has a table of its own -- and why the background makes no difference // there, since a named color is drawn from a palette the user chose to go with // their background. static auto HasChosenColors(const Terminal::Capabilities& capabilities) -> bool { return capabilities.color_mode == Terminal::ColorMode::Ansi256 || capabilities.color_mode == Terminal::ColorMode::Truecolor; } // Returns whether the chosen colors should be the ones for a light background. static auto WantsLightBackground(const Terminal::Capabilities& capabilities) -> bool { return capabilities.background == Terminal::Background::Light; } // Returns the theme a diagnostic at `level` is drawn in. static auto LevelTheme(Level level, const Terminal::Capabilities& capabilities) -> Theme { bool light = WantsLightBackground(capabilities); if (!HasChosenColors(capabilities)) { return level == Level::Error ? Theme(NamedErrorTheme) : Theme(NamedWarningTheme); } switch (level) { case Level::Error: return light ? Theme(LightBgErrorTheme) : Theme(DarkBgErrorTheme); case Level::Warning: return light ? Theme(LightBgWarningTheme) : Theme(DarkBgWarningTheme); } } // Returns the theme anything explaining a diagnostic is drawn in. static auto ExplanationTheme(const Terminal::Capabilities& capabilities) -> Theme { if (!HasChosenColors(capabilities)) { return Theme(NamedNoteTheme); } return WantsLightBackground(capabilities) ? Theme(LightBgNoteTheme) : Theme(DarkBgNoteTheme); } // The place in a theme's ramp that is the theme's own color, which its words // are drawn in and which a range takes when it is the only one of its theme on // a line. Every ramp puts it second, so a ramp of two runs up to it and a ramp // of three runs through it. static constexpr int ThemeCenter = 1; // Returns the style a theme's words are drawn in. Also the color of the // underline beneath the source that belongs to it, so that a mark and the // sentence explaining it look like one thing. static auto ThemeStyle(Theme theme) -> Style { return Style().Bold().Foreground(theme[ThemeCenter]); } // The word leading anything that explains the message rather than stating it. static constexpr llvm::StringRef ExplanationText = "note: "; // The style of the frame and the line numbers beside it. Not dim, which several // terminals ignore, leaving the frame as prominent as the code inside it. static auto FrameStyle() -> Style { return Style().Bold().Foreground(AnsiColor::BrightBlue); } // The style of the diagnostic kind, which is only present under a flag and is // never the thing being read. static auto KindStyle() -> Style { return Style().Dim(); } // The style of location information, which says how the code below it was // reached rather than anything about the code itself. Dim so that a reader who // already knows the path can skip it, and unlike the frame nothing is lost on a // terminal that ignores dim. It covers the whole row, quoted code included. static auto ContextStyle() -> Style { return Style().Dim(); } namespace { // The glyphs that vary with the character set. // // Everything else the frame is made of comes from `Terminal::Buffer`'s line // drawing, which picks its own glyphs and forms the corners and tees where // lines meet. struct Glyphs { // Marks a row that annotates source rather than being source, so that which // rows are the reader's code is visible at a glance. char32_t annotation; // Marks where lines of source were skipped. char32_t elision; static auto For(Charset charset) -> Glyphs { if (charset == Charset::Utf8) { return {.annotation = U'·', .elision = U'┆'}; } // An ASCII terminal has one broken bar to spend on both the annotation and // the elision. A row holding nothing but the bar is an elision; one with an // underline after it is not. return {.annotation = U':', .elision = U':'}; } }; } // namespace // Returns whether a terminal decoding `charset` draws `byte` as itself. static auto IsPrintable(Charset charset, char byte) -> bool { auto value = static_cast(byte); if (charset == Charset::Ascii) { // A terminal decoding some single-byte encoding renders anything outside // printable ASCII as something, and there is no telling what. return value >= 0x20 && value < 0x7f; } // Everything else goes to the buffer, which decodes UTF-8 and replaces what // has no rendering of its own. return value >= 0x20 && value != 0x7f; } namespace { // A count of bytes into a source line, as against a column on screen. // // A `Loc` gives its span as byte offsets -- `column_number` counts bytes // despite its name, and Clang's land inside multi-byte characters -- while // everything drawn is placed by column. The two count the same line and // neither follows from the other, so this keeps them from being taken for each // other in the one place both are in scope. Columns stay `int`, which is what // `Terminal::Buffer` addresses its grid with, so what this marks is the count // that is not a coordinate. // // TODO: Move this out of this file, and use it for the byte counts elsewhere // that are called columns, starting with `Loc::column_number`. class Bytes { public: Bytes() = default; explicit constexpr Bytes(int count) : count_(count) {} constexpr auto count() const -> int { return count_; } constexpr auto operator+(Bytes rhs) const -> Bytes { return Bytes(count_ + rhs.count_); } constexpr auto operator-(Bytes rhs) const -> Bytes { return Bytes(count_ - rhs.count_); } constexpr auto operator++() -> Bytes& { ++count_; return *this; } constexpr auto operator--() -> Bytes& { --count_; return *this; } friend auto operator<=>(Bytes, Bytes) = default; friend auto operator==(Bytes, Bytes) -> bool = default; private: int count_ = 0; }; } // namespace namespace Internal { // A source line prepared for drawing, and where a part's span lands in it. // // The text has tabs expanded and bytes with no rendering escaped, so that the // columns measured against it are the columns a terminal will use. struct PreparedSource { std::string text; int span_start = 0; int span_length = 1; // Returns the columns the source and its underline occupy together. The // underline can reach one column further, when a span starts past the end of // the line it names. auto Width(Metrics metrics) const -> int { return std::max(metrics.Width(text), span_start + span_length); } // The column a label's connector runs down, which layout sets to whichever // end of the span the label attaches to. Until then it is the middle, which // is what a span with no label of its own is measured from. int anchor = 0; // Where the underline stops at each end. Layout sets an end to `Center` when // another range begins or ends in the next column, so that the two marks // don't run together; an end with nothing beside it covers its whole column. LineEnd left_end = LineEnd::Edge; LineEnd right_end = LineEnd::Edge; // Whether the label's connector is drawn in place of the underline rather // than hanging off it. A mark of one column with an end pulled in has no // junction to offer, and the line down its column says the column is marked // without saying the mark carries on anywhere. bool connector_stands_in = false; // Returns whether the underline covers `column` at all. auto Covers(int column) const -> bool { return column >= span_start && column < span_start + span_length; } // Returns whether the underline runs out through the left or the right side // of `column`. An end pulled in to clear the range beside it gives up the // side it was pulled in on, which is the side a connector could not tee into. auto ReachesLeftOf(int column) const -> bool { return Covers(column) && (column > span_start || left_end == LineEnd::Edge); } auto ReachesRightOf(int column) const -> bool { return Covers(column) && (column < span_start + span_length - 1 || right_end == LineEnd::Edge); } // Returns whether any column of the underline runs out through both of its // sides, which is what a connector needs one to do to tee into it. auto Joinable() const -> bool { if (span_length == 1) { return left_end == LineEnd::Edge && right_end == LineEnd::Edge; } return span_length > 2 || left_end == LineEnd::Edge || right_end == LineEnd::Edge; } }; // What a part of a diagnostic is to the reader, which is what decides how it // is drawn. enum class Part : int8_t { // A step in the path by which the part after it was reached. LocationInfo, // Something read against the code to explain the problem rather than to // state it: a label, or the message itself once a context is stating the // problem in its place. Explanation, // The problem itself: whatever leads the diagnostic, and the source that is // directly part of it. Problem, }; // A part of a diagnostic prepared for drawing. struct PreparedPart { // What this is to the reader. Part role = Part::Explanation; // Whether this part's range came from the message's own location rather than // from a range attached to the diagnostic. Only the message has one. // // Every diagnostic marks at least one range as part of the problem. Where one // was attached, it says where the problem is better than the message's // location does, and this range gives way to it: a range containing the // location hosts the message's words, and one that doesn't leaves the message // marking nothing of its own. bool from_message_loc = false; // The word leading this part's text, including its separator, and empty for // location information, which leads with its own words instead. llvm::StringRef level_text; // The style of that word and of this part's text. Style style; // The style of the underline under this part's source, and of the connector // and bar of the label hanging off it. The rotation moves this along the // theme's ramp, which is why it is not `style`: the level word, the mark // in the margin, and the line number beside the reported line all say what // the diagnostic is, so where a range sits on a line must not recolor them. Style mark_style; // `file:line:column`, with unknown parts left off, and empty for a part // with no location at all. std::string location; // The last byte column of the range starting at `location`, or 0 for a // range of one column or of unknown extent. The compact form appends it to // the location as `-`, which is all it has to say the extent with; the // framed form says it with the underline instead. int32_t location_end_column = 0; // The part's words when they go on a row of their own: the headline, and any // part with no source to hang them against. Empty when they hang as a label. std::string text; // The part's words when they hang against the source it marks, which is any // part that has source and isn't the headline. Empty otherwise. std::string label; // ` [Kind]`, or empty when kinds aren't being included. It goes at the end // of the last row carrying the part's words, so that a test matching on it // always finds it last, and so that a part drawn on two rows doesn't get it // twice. std::string kind_suffix; // Returns the suffix for a row, which is empty unless that row is the last // one carrying this part's words. auto KindSuffixOn(bool is_label_row) const -> llvm::StringRef { return is_label_row == label.empty() ? llvm::StringRef() : kind_suffix; } // The source and its span. Only meaningful when `has_source` is set. PreparedSource source; bool has_source = false; // Where the source came from, for grouping spans that share a file and for // finding the lines between two of them. llvm::StringRef file; llvm::StringRef file_text; llvm::StringRef raw_line; int line_number = 0; }; } // namespace Internal using Internal::Part; using Internal::PreparedPart; using Internal::PreparedSource; // Returns `style` recolored for the `index`th of `count` ranges of `theme` on a // line, keeping everything about it except which color it is. // // Several ranges walk the ramp from its dark end, so that reading left to right // reads as a progression. A range with no others of its theme beside it takes // the theme's own color instead, so that it matches the words it belongs to. static auto RotatedStyle(const Style& style, Theme theme, int index, int count) -> Style { if (count == 1) { return style.Foreground(theme[ThemeCenter]); } return style.Foreground(theme[index % theme.size()]); } namespace { // How a label's connector meets the underline it hangs from. enum class Attachment : int8_t { // Tees into the underline where it hangs from. Line drawing forms the // junction the same way it forms the frame's own. Tee, // Stands in for the mark, in the one column it had, because the underline // offers no column to tee into: a mark that stops at a cell's center to // clear the range beside it would form a corner there, and a corner reads as // a mark turning rather than as one a connector leaves. Through, }; // Where a label's connector meets its underline, and the column it runs down. struct Placement { Attachment attachment; int anchor; }; // A row of the rendering, laid out before anything is measured or drawn. struct Row { enum class Kind : int8_t { // A message on a row of its own, inside the frame. Message, // The location the snippet below it comes from. Anchor, // A line of source, with its number beside the frame. Source, // The underline marking a span in the source above. Annotation, // The text hanging off that underline, framed by a bar the underline's // connector reaches. Label, // A label that wouldn't fit hanging off its span, out-dented to the column // the source starts in with its own frame reaching back to the span. RoutedLabel, // A stand-in for lines of source that were skipped. Elision, // Nothing but the frame, setting a snippet off from the anchor above it. Gap, // The rule closing off one snippet before the next begins. Separator, // How the location in the anchor below it was reached. Context, }; Kind kind; // The part this row belongs to, or null for a row that draws only frame and // for a source line shown between two spans. const PreparedPart* part = nullptr; // `Annotation` rows: every span underlined, widest first, which is the order // they are drawn in so that the narrowest mark on a column is what shows. llvm::SmallVector spans; // `Source` rows: the part whose style the margin mark and line number // take, set on every line holding a range that is part of the problem, and // null on every other line. const PreparedPart* points_here = nullptr; // `Source` rows: the number to draw, or -1 when there is none to show. int line_number = -1; // `Source` rows: the text. std::string text; }; } // namespace // Returns whether `byte` continues a UTF-8 symbol that started before it. static auto IsContinuation(char byte) -> bool { return (static_cast(byte) & 0xc0) == 0x80; } // Returns `line` with tabs expanded and unrenderable bytes escaped, along with // where the span of `span_bytes` bytes at `column` lands in the result. // // TODO: Highlight the source's syntax. `PreparedSource` holds plain text, so // this needs it to hold styled runs. static auto PrepareSource(Metrics metrics, llvm::StringRef line, Bytes span_byte, Bytes span_bytes) -> PreparedSource { Charset charset = metrics.charset(); // A file written on Windows carries the carriage return into the line, and // drawing it would put a `<0D>` on the end of every snippet row. line.consume_back("\r"); // Windowing bounds what is shown by columns, not bytes, so a line of nothing // but combining marks could otherwise carry any number of bytes through it. // This bound also keeps the byte counts below inside `int`. line = line.take_front(MaxDrawnBytes); auto size = Bytes(line.size()); // `span_byte` is one-based, as `Loc::column_number` is. A span can name a // token that isn't in the line at all, at the end of a file for example, so // it is clamped rather than trusted: a renderer must never be the reason a // compiler dies while reporting a problem. Bytes span_begin = std::clamp(span_byte - Bytes(1), Bytes(0), size); // Adding the length to the start would overflow for a length this doesn't // expect, so it is clamped to what is left of the line first. Bytes span_end = span_begin + std::min(std::max(span_bytes, Bytes(1)), size - span_begin); // An offset that names a byte inside a symbol, which Clang's do, would split // it and leave the run before it measured as loose bytes. if (charset == Charset::Utf8) { // A span starting where the line ends names no byte to walk back from, as a // token synthesized by error recovery past the end of a line does. while (span_begin > Bytes(0) && span_begin < size && IsContinuation(line[span_begin.count()])) { --span_begin; } while (span_end < size && IsContinuation(line[span_end.count()])) { ++span_end; } } PreparedSource result; int cursor = 0; int start_column = 0; int end_column = 0; Bytes run_begin; for (Bytes at;; ++at) { bool at_span = at == span_begin || at == span_end; bool rewritten = at < size && !IsPrintable(charset, line[at.count()]); if (at < size && !at_span && !rewritten) { continue; } // Flush the run of bytes drawn as themselves. Measuring the run rather than // everything so far is what keeps this linear, and is valid because a run // only ever ends on a byte that starts a symbol. llvm::StringRef run = line.substr(run_begin.count(), (at - run_begin).count()); result.text.append(run.begin(), run.end()); cursor += metrics.Width(run); run_begin = at; if (at == span_begin) { start_column = cursor; } if (at == span_end) { end_column = cursor; } if (at == size) { break; } if (!rewritten) { continue; } if (line[at.count()] == '\t') { int width = SourceTabWidth - cursor % SourceTabWidth; result.text.append(width, ' '); cursor += width; } else { // TODO: Draw an escaped byte in a style of its own, such as a background // color, so that it can't be mistaken for the same characters written in // the source. This needs the styled runs that syntax highlighting does. auto value = static_cast(line[at.count()]); result.text += '<'; result.text += llvm::hexdigit(value >> 4); result.text += llvm::hexdigit(value & 0xf); result.text += '>'; cursor += 4; } run_begin = at + Bytes(1); } result.span_start = start_column; result.span_length = std::max(end_column - start_column, 1); result.anchor = result.span_start + result.span_length / 2; return result; } // Narrows `source` to `columns` around `focus`, marking each end that was // elided. // // A source line and its underline that wrap onto different rows lose the // alignment between them, which is the whole point of a snippet, so a line too // wide for the terminal is elided rather than left to wrap. // // `focus` is the column the window is kept around, which is a span's own start // unless several spans share one source row, in which case they must all be // given the same one or their underlines land in different windows. static auto WindowSource(Metrics metrics, PreparedSource& source, int columns, int focus) -> void { // The width includes the column past the text that a span starting at the // end of the line is drawn in, so a line that exactly fills the columns is // still windowed to bring that mark inside them. int width = source.Width(metrics); if (width <= columns) { return; } // Both markers are accounted for whether or not both are needed, so that how // much source a window shows doesn't depend on where in the line it lands. auto marker_width = static_cast(WindowMarker.size()); int inner = std::max(columns - 2 * marker_width, MinWindowColumns); int begin = std::clamp(focus - inner / 4, 0, std::max(width - inner, 0)); // The text is walked rather than indexed, so no byte offset into it is ever // held next to a column. What is dropped from the front is measured from what // was really taken, which can be a column short of what was asked for when a // double-width symbol straddles the boundary; measuring from the column asked // for would let the window keep that symbol's other column and come out a // column wider than it is allowed. llvm::StringRef rest = source.text; llvm::StringRef before = metrics.TakeColumns(rest, begin); int dropped = metrics.Width(before); llvm::StringRef window = metrics.TakeColumns(rest, inner); std::string text; int shift = 0; if (!before.empty()) { text.append(WindowMarker.begin(), WindowMarker.end()); shift = marker_width; } text.append(window.begin(), window.end()); if (!rest.empty()) { text.append(WindowMarker.begin(), WindowMarker.end()); } // Both ends of the span move by what was really dropped from in front of it, // and each is clamped into the window's own columns. Clamping the ends rather // than carrying the length is what keeps a span the window clipped short -- // one windowed around another span on its row, off one edge -- from // underlining the source that took its place: what was clipped away is not // marked. Every column is drawable and none is past the last cell, so the // mark stays a mark and never reaches past the grid. int visible = metrics.Width(text); int offset = shift - dropped; int old_end = source.span_start + source.span_length; source.span_start = std::clamp(source.span_start + offset, 0, std::min(visible, columns - 1)); int span_end = std::clamp(old_end + offset, source.span_start + 1, columns); source.span_length = span_end - source.span_start; source.anchor = source.span_start + source.span_length / 2; source.text = std::move(text); } // Returns line `number` of the file `part`'s source came from, or a null // reference when there is no way to find it. A line that is genuinely empty // still comes back as an empty reference over real data. static auto LineFromFile(const PreparedPart& part, int number) -> llvm::StringRef { llvm::StringRef file = part.file_text; if (file.empty() || part.raw_line.empty() || number <= part.line_number) { return llvm::StringRef(); } // `Loc::line` is meant to be a slice of `Loc::file_text`, which lets this // walk forward from it rather than counting from the top of the file. A `Loc` // is a plain aggregate that anything can fill in, so that is checked rather // than assumed. const char* line = part.raw_line.data(); if (line < file.begin() || line >= file.end()) { return llvm::StringRef(); } size_t offset = line - file.begin(); for (auto _ : llvm::seq(part.line_number, number)) { size_t newline = file.find('\n', offset); if (newline == llvm::StringRef::npos) { return llvm::StringRef(); } offset = newline + 1; } return file.substr(offset).take_until([](char c) { return c == '\n'; }); } // Returns a part of a diagnostic prepared for drawing: `text` said at `loc`, // drawn as `role` says and named `name` where kinds are being included. // // `is_headline` says that the words lead the diagnostic, so they go on the // headline row rather than against the source they point at. static auto PreparePart(Metrics metrics, Part role, bool from_message_loc, llvm::StringRef level_text, Style style, const Loc& loc, std::string text, llvm::StringRef name, bool is_headline, bool include_kind) -> PreparedPart { // Text past any legitimate size is truncated rather than handed to the // buffer, whose own limit on a single draw is a CHECK; nothing a diagnostic // says comes near either bound. if (text.size() > MaxDrawnBytes) { text.resize(MaxDrawnBytes); } PreparedPart result = { .role = role, .from_message_loc = from_message_loc, .level_text = level_text, .style = style, .mark_style = MarkStyle(style), .location = FormatLocation(loc), .location_end_column = loc.column_number > 0 && loc.length > 1 ? loc.column_number + loc.length - 1 : 0, .file = loc.filename, .file_text = loc.file_text, .raw_line = loc.line, .line_number = loc.line_number}; // Location information says how a place was reached rather than pointing at // code itself, so it never gets a snippet. if (role != Part::LocationInfo && loc.column_number > 0) { result.has_source = true; result.source = PrepareSource(metrics, loc.line, Bytes(loc.column_number), Bytes(loc.length)); } // Words that lead the diagnostic go on its headline, and words with no source // to point at have nowhere to go but a row of their own. Everything else is // read against the code it marks. if (is_headline || !result.has_source) { result.text = std::move(text); } else { result.label = std::move(text); } // Every row is one row: text carrying a newline would otherwise be measured // as one column and drawn over the row below it. for (std::string* row : {&result.text, &result.label}) { for (char& c : *row) { if (static_cast(c) < 0x20 || c == 0x7f) { c = ' '; } } } // A label that scrubbed away to spaces has nothing to hang, and hanging it // would spend rows and a connector on drawing nothing. It becomes what a // label with no words is: a mark and nothing else. if (result.label.find_first_not_of(' ') == std::string::npos) { result.label.clear(); } if (include_kind && !name.empty()) { result.kind_suffix = (llvm::Twine(" [") + name + "]").str(); } return result; } // Returns how `part`, which is location information, is spelled: what it says // and then where, so that the words read as leading to the location. A part // missing either half is just the other. static auto ContextText(const PreparedPart& part) -> std::string { if (part.location.empty()) { return part.text; } if (part.text.empty()) { return part.location; } return part.text + ": " + part.location; } // Returns what a part drawn on a row of its own says. Location information // that never found an anchor to lead into says where it is here instead. static auto MessageRowText(const PreparedPart& part) -> std::string { return part.role == Part::LocationInfo ? ContextText(part) : part.text; } // Returns where `label`'s connector meets the underline it hangs from, and the // column it runs down from there. // // The connector tees into the middle of the underline, which reads as pointing // into the range rather than as running off an end of it, and slides left when // the words don't fit -- but only as far as it has to, and never past where the // range starts. Moving the connector is what a label gives up before it starts // wrapping. // // An end pulled in to clear the range beside it has no junction to offer // either, and a connector meeting one would form a corner, which reads as the // underline turning down rather than as a line leaving it. So the connector // steps a column away from that range, where the marks run out through both // sides again. Where no column of its range does -- a single column, or two // with a range on either side -- it takes a column instead of teeing into one, // and the underline gives that column up to it. // // Whether a column offers a junction is asked of every mark on the row rather // than of `against` alone, since a cell holds all of them together: a range // drawn over this one covers what its own end gave up, and a connector meeting // that reads as the tee it is drawn as. Asking only `against` would leave a // connector running through a column another mark had filled, crossing it. // // `against` is what the connector leaves, which is the label's own range except // for the message's: where a range drawn on the same row contains the message's // location, the words belong to that range. static auto PlaceLabel(Metrics metrics, const PreparedPart& label, const PreparedSource& against, llvm::ArrayRef spans, int content_x, int columns) -> Placement { int width = metrics.Width(label.label); int middle = against.span_start + against.span_length / 2; int anchor = middle; if (content_x + anchor + LabelIndentWidth + width > columns) { anchor = std::clamp(columns - content_x - LabelIndentWidth - width, against.span_start, anchor); } auto tees_at = [&](int column) { return against.Covers(column) && llvm::any_of(spans, [&](const PreparedPart* span) { return span->source.ReachesLeftOf(column); }) && llvm::any_of(spans, [&](const PreparedPart* span) { return span->source.ReachesRightOf(column); }); }; if (tees_at(anchor)) { return {Attachment::Tee, anchor}; } // Only an end fails to offer a junction, so the step is inward, away from the // range that pulled that end in. int step = anchor == against.span_start ? 1 : -1; if (tees_at(anchor + step)) { return {Attachment::Tee, anchor + step}; } // Nothing to tee into, so every label on this range takes the same column, // which is the one the underline is measured from rather than wherever the // words happened to have to start. return {Attachment::Through, middle}; } // Draws the mark under `span`, in source drawn from column `x`, on row `y`. static auto DrawUnderline(Buffer& buffer, int x, int y, const PreparedPart& span, const Style& style) -> void { // A connector with no column to tee into is drawn down the mark's own column // in place of it. Only a mark of one column is ever left with none, so there // is nothing of it to draw beside the line: an end gives up its half column // only where the range can still carry its label without it. if (span.source.connector_stands_in) { return; } buffer.DrawHorizontalLine(x + span.source.span_start, y, span.source.span_length, style, span.source.left_end, span.source.right_end); } // Returns the part whose range stands in for `message_part`'s own, or null when // nothing does and the message marks where it points. // // A range attached as part of the problem says where the problem is in terms // the reader can see, which the message's location -- a point, and often one // chosen for an editor to put a cursor on rather than for anyone to look at -- // does not. So a range marking the line that location names stands in for it // entirely: the message marks nothing of its own, and whatever words it has // left once a context leads the diagnostic hang from that range instead. // // It has to be that line. A range elsewhere says nothing about the code the // location names, and taking its place would point the message's words at code // they aren't about while dropping the line they are. // // The range containing the location is the one that takes it, since that is the // one drawn over the column the location names. Failing that it is the first // attached to the line, which is the one whose author put it first. // // `candidates` are the ranges drawn alongside the message's, which is what a // range has to be to take its place: one drawn under an anchor of its own is // read against the code below that one instead. static auto FindMessageHost(llvm::ArrayRef candidates, const PreparedPart& message_part) -> PreparedPart* { PreparedPart* host = nullptr; int column = message_part.source.span_start; for (PreparedPart* candidate : candidates) { if (candidate == &message_part || candidate->role != Part::Problem || candidate->file != message_part.file || candidate->line_number != message_part.line_number) { continue; } const PreparedSource& source = candidate->source; if (column >= source.span_start && column < source.span_start + source.span_length) { return candidate; } if (!host) { host = candidate; } } return host; } // Recolors the marks of `spans`, which share one line, along their themes' // ramps. Each theme counts its own way through its colors, left to right, so // they run in the order the code they mark is read. static auto RotateMarkStyles(llvm::ArrayRef spans, Theme problem_theme, Theme explanation_theme) -> void { llvm::SmallVector across(spans); llvm::stable_sort(across, [](const PreparedPart* lhs, const PreparedPart* rhs) { return lhs->source.span_start < rhs->source.span_start; }); int totals[2] = {0, 0}; for (const PreparedPart* span : across) { ++totals[span->role == Part::Problem]; } int counts[2] = {0, 0}; for (PreparedPart* span : across) { bool is_problem = span->role == Part::Problem; int& count = counts[is_problem]; span->mark_style = RotatedStyle( span->mark_style, is_problem ? problem_theme : explanation_theme, count++, totals[is_problem]); } } // Pulls in the ends of `spans` that touch another span on their line. // // Two ranges that touch have nothing between them to say where one ends and // the next begins, so an end that meets one stops at the center of its cell // rather than running out through the side. That leaves half a column of gap, // and a full one where both ends give way. // // The gap is worth having but not worth paying for. An end gives way only // where the range can still carry its label without it: two columns can spare // one end but not both, since a connector needs a column the mark runs out // through on both sides and after the left end gave way only the right one is // left. Left is considered first, so the range that begins at a boundary is // always the one that gives way there, and every boundary ends up with a gap. // One column can spare neither end, and the accepted answer there is the line // the connector leaves or a point where it has no connector. static auto SeparateTouchingEnds( llvm::ArrayRef spans, const llvm::SmallPtrSetImpl& carries_label) -> void { for (PreparedPart* span : spans) { int span_end = span->source.span_start + span->source.span_length; auto meets = [&](auto&& touches) { return llvm::any_of(spans, [&](const PreparedPart* other) { return other != span && touches(other->source); }); }; if (meets([&](const PreparedSource& other) { return other.span_start + other.span_length == span->source.span_start; })) { span->source.left_end = LineEnd::Center; } if (meets([&](const PreparedSource& other) { return other.span_start == span_end; })) { span->source.right_end = LineEnd::Center; if (carries_label.contains(span) && span->source.span_length > 1 && !span->source.Joinable()) { span->source.right_end = LineEnd::Edge; } } } } // Sets each label's anchor to where its connector runs down, asked of the // underline it hangs from against everything else marked on the line. // // `message_part` and `message_host` are as in `add_line` below: the message's // words hang from the host's range when one has taken its place. static auto PlaceLabels(Metrics metrics, llvm::ArrayRef labels, const PreparedPart* message_part, const PreparedPart* message_host, llvm::ArrayRef spans, int content_x, int columns) -> void { for (PreparedPart* label : labels) { const PreparedSource& against = label == message_part && message_host ? message_host->source : label->source; Placement placement = PlaceLabel(metrics, *label, against, spans, content_x, columns); label->source.anchor = placement.anchor; // A connector drawn in place of a mark is recorded on the range it stands // in for, not on the label that asked for it -- and on every range drawn // over exactly the same column, since a second mark there would be a half // mark beside the connector rather than a stroke through it, which is the // one thing that would leave a corner in the cell. if (placement.attachment == Attachment::Through) { for (PreparedPart* span : spans) { if (span->source.span_start == against.span_start && span->source.span_length == against.span_length) { span->source.connector_stands_in = true; } } } } } // Returns the spans on `line` that draw an underline: all of them, except // `message_part` once `message_host` has taken its place. // // Everything marking the line goes on the one row below it, so that the marks // read as marking that line rather than as a stack of rows each of which has to // be traced back up to it. Ranges that overlap are drawn widest first, so the // narrowest mark covering a column is the one that shows: a range containing // another says less about the columns they share than the range inside it does, // and a mark that is part of the problem says more than one that only explains // it. // // Ranges shouldn't overlap in the first place -- one covering another means two // parts of the diagnostic marking the same code -- but they are attached by // layers that never see each other, so this repairs what it is given rather // than checking a rule nothing is in a position to enforce. static auto SpansWidestFirst(llvm::ArrayRef line, const PreparedPart* message_part, const PreparedPart* message_host) -> llvm::SmallVector { llvm::SmallVector spans; for (PreparedPart* member : line) { if (member != message_part || !message_host) { spans.push_back(member); } } llvm::stable_sort( spans, [](const PreparedPart* lhs, const PreparedPart* rhs) { if (lhs->source.span_length != rhs->source.span_length) { return lhs->source.span_length > rhs->source.span_length; } return lhs->role < rhs->role; }); return spans; } // Returns the parts whose words hang off `spans`, in the order of `spans`. static auto LabelsHangingFrom(llvm::ArrayRef spans, PreparedPart* message_part, const PreparedPart* message_host) -> llvm::SmallVector { llvm::SmallVector labels; for (PreparedPart* span : spans) { if (!span->label.empty()) { labels.push_back(span); } // A range standing in for the part's location hangs the part's own words // from its underline alongside whatever it has to say itself. if (span == message_host && !message_part->label.empty()) { labels.push_back(message_part); } } return labels; } namespace { // Lays out the rows a diagnostic's frame holds, in the order they are drawn. // // `source_columns` bounds every source row: the lines shown between two spans // as well as the spans' own, which are windowed here so that spans sharing a // row share one view of their line. `buffer` is the one the rows will be drawn // into, and is only measured against here: how far text wraps is its answer to // give. class RowLayout { public: RowLayout(const Buffer& buffer, llvm::MutableArrayRef parts, int headline, Theme problem_theme, Theme explanation_theme, int source_columns, int content_x, int columns); // Returns the rows, in the order they are drawn. auto LayOut() && -> llvm::SmallVector; private: // Adds the rows for `parts_[index]`: its own text, the anchor naming its // location, and the snippet below that, which also shows the spans of the // parts `TakeGroup` gathers with it. auto AddBlock(size_t index) -> void; // Returns the spans shown under the anchor of `parts_[index]`: its own and // every later span in the same file, shown as one view of it in source order // with the lines between them elided. Each is marked as drawn. auto TakeGroup(size_t index) -> llvm::SmallVector; // Adds the rows showing `group`'s source below its anchor, one line of // source at a time. auto AddSnippet(llvm::ArrayRef group, PreparedPart* message_part, PreparedPart* message_host) -> void; // Adds the lines of source between `previous`'s line and line `next`, or an // elision standing in for them. auto AddLinesBetween(const PreparedPart& previous, int next) -> void; // Adds the rows drawing the spans that fall on one source line: the line // itself, the underlines beneath it, and the labels hanging off those. // // `message_part` is the range standing in for the part's own location, and // `message_host` the attached range that has taken its place, in which case // the part underlines nothing of its own and its words hang from the host. auto AddLine(llvm::ArrayRef line, PreparedPart* message_part, PreparedPart* message_host) -> void; // Adds a row for each of `labels`, below the underlines they hang off. auto AddLabelRows(llvm::ArrayRef labels) -> void; // Adds `part`'s own text on a row of its own, which the drawing wraps into // the column after the level word. // // TODO: No toolchain diagnostic should reach this: the goal is every part // attached to a location that reaches source, with one genuinely about a // file as a whole anchored at the file's start or end and drawn in a form of // its own. That restructuring starts with the emitting diagnostics. auto AddMessageRows(const PreparedPart& part) -> void; // Returns whether the last row added was part of a snippet, and so wants // closing off before the next one starts. auto InSnippet() const -> bool; const Buffer* buffer_; Metrics metrics_; llvm::MutableArrayRef parts_; int headline_; Theme problem_theme_; Theme explanation_theme_; int source_columns_; int content_x_; int columns_; // Whether a part has to open a block of its own rather than being merged // into an earlier one: it has something to say on a row of its own, or there // is location information above it that needs its anchor to lead into. llvm::SmallVector opens_block_; // Whether each part's rows have been added. llvm::SmallVector drawn_; // Location information waiting for the anchor it describes. llvm::SmallVector context_; llvm::SmallVector rows_; }; } // namespace RowLayout::RowLayout(const Buffer& buffer, llvm::MutableArrayRef parts, int headline, Theme problem_theme, Theme explanation_theme, int source_columns, int content_x, int columns) : buffer_(&buffer), metrics_(buffer.metrics()), parts_(parts), headline_(headline), problem_theme_(problem_theme), explanation_theme_(explanation_theme), source_columns_(source_columns), content_x_(content_x), columns_(columns), opens_block_(parts.size(), false), drawn_(parts.size(), false) { bool context_pending = false; for (auto [index, part] : llvm::enumerate(parts)) { if (part.role == Part::LocationInfo) { context_pending = true; continue; } opens_block_[index] = context_pending || (static_cast(index) != headline && !part.text.empty()); context_pending = false; } } auto RowLayout::LayOut() && -> llvm::SmallVector { for (size_t index : llvm::seq(parts_.size())) { if (!drawn_[index]) { AddBlock(index); } } for (const PreparedPart* reached_from : context_) { AddMessageRows(*reached_from); } return std::move(rows_); } auto RowLayout::AddBlock(size_t index) -> void { drawn_[index] = true; PreparedPart& part = parts_[index]; bool is_headline = static_cast(index) == headline_; // Location information says how the location after it was reached, so it // waits and is then drawn above that location's anchor. if (part.role == Part::LocationInfo) { context_.push_back(&part); return; } if (InSnippet()) { rows_.push_back({.kind = Row::Kind::Separator}); } if (!is_headline && !part.text.empty()) { AddMessageRows(part); } // An anchor is drawn where a snippet follows it, so a part with no source // to show gets none: its words go on a row of their own, and an anchor // above them would lead nowhere. The headline is the exception -- its // location is the diagnostic's, worth saying even when it names only a // file, as for a diagnostic about a file rather than about code in one. bool anchored = !part.location.empty() && (part.has_source || is_headline); // Without an anchor below them there is nothing for the steps to lead into, // so they say where they are on rows of their own instead. for (const PreparedPart* reached_from : context_) { if (anchored) { rows_.push_back({.kind = Row::Kind::Context, .part = reached_from}); } else { AddMessageRows(*reached_from); } } context_.clear(); llvm::SmallVector group = TakeGroup(index); if (anchored) { rows_.push_back({.kind = Row::Kind::Anchor, .part = &part}); } // The spans shown under one anchor are the ones drawn alongside each other // here, so the group is what a range has to be in to stand in for the // part's location: a range under an anchor of its own is read against // the code below that one instead. PreparedPart* message_part = nullptr; for (PreparedPart* member : group) { if (member->from_message_loc) { message_part = member; break; } } PreparedPart* message_host = message_part ? FindMessageHost(group, *message_part) : nullptr; if (message_host) { // The part marks nothing of its own once something else has taken its // place, so the line it named is not part of the snippet either -- unless // an attached range is on it too. Leaving it in shows a line of source // with nothing marking it, which reads as a line the reader is meant to // find something in. llvm::erase(group, message_part); } AddSnippet(group, message_part, message_host); } auto RowLayout::TakeGroup(size_t index) -> llvm::SmallVector { const PreparedPart& part = parts_[index]; llvm::SmallVector group; if (!part.has_source) { return group; } group.push_back(&parts_[index]); for (size_t other : llvm::seq(index + 1, parts_.size())) { if (!drawn_[other] && !opens_block_[other] && parts_[other].has_source && parts_[other].file == part.file) { drawn_[other] = true; group.push_back(&parts_[other]); } } llvm::stable_sort(group, [](const PreparedPart* lhs, const PreparedPart* rhs) { return lhs->line_number < rhs->line_number; }); return group; } auto RowLayout::AddSnippet(llvm::ArrayRef group, PreparedPart* message_part, PreparedPart* message_host) -> void { if (group.empty()) { return; } // A snippet always starts a row below its anchor, so that the code doesn't // read as running on from the file name. Where the file goes on above the // first line shown, that row says so instead. int first_line = group.front()->line_number; rows_.push_back( {.kind = first_line > 1 ? Row::Kind::Elision : Row::Kind::Gap}); // Spans on one line are drawn together, against the one row that shows it. const PreparedPart* previous = nullptr; for (size_t begin = 0; begin < group.size();) { size_t end = begin + 1; while (end < group.size() && group[end]->line_number == group[begin]->line_number) { ++end; } if (previous) { AddLinesBetween(*previous, group[begin]->line_number); } AddLine(group.slice(begin, end - begin), message_part, message_host); previous = group[end - 1]; begin = end; } } auto RowLayout::AddLinesBetween(const PreparedPart& previous, int next) -> void { if (next - previous.line_number - 1 > MaxShownGap) { rows_.push_back({.kind = Row::Kind::Elision}); return; } for (int number : llvm::seq(previous.line_number + 1, next)) { llvm::StringRef line = LineFromFile(previous, number); if (line.data() == nullptr) { // Without the file there is no way to show what was skipped, and a blank // row would claim the line is empty. rows_.push_back({.kind = Row::Kind::Elision}); continue; } PreparedSource shown = PrepareSource(metrics_, line, Bytes(1), Bytes(1)); WindowSource(metrics_, shown, source_columns_, /*focus=*/0); rows_.push_back({.kind = Row::Kind::Source, .line_number = number, .text = std::move(shown.text)}); } } auto RowLayout::AddLine(llvm::ArrayRef line, PreparedPart* message_part, PreparedPart* message_host) -> void { // Spans sharing the row are windowed around the first of them: they share // the one source row that shows their line, so windowing each around its // own span would land their underlines in different views of it. Spans // drawn on different rows never share a window, wherever in the file they // are. int focus = line.front()->source.span_start; for (PreparedPart* member : line) { WindowSource(metrics_, member->source, source_columns_, focus); } llvm::SmallVector spans = SpansWidestFirst(line, message_part, message_host); RotateMarkStyles(spans, problem_theme_, explanation_theme_); // The part's connector and bar follow the range hosting its words, so // the rotation's choice for the host is copied after it is made. if (message_host && llvm::is_contained(spans, message_host)) { message_part->mark_style = message_host->mark_style; } // The mark in the margin picks out the lines the problem is on, which is // every line holding a range that is part of it. It is drawn in what the // headline is drawn in, so that it reads as part of the same sentence. bool marks_problem = llvm::any_of(spans, [](const PreparedPart* span) { return span->role == Part::Problem; }); rows_.push_back({.kind = Row::Kind::Source, .part = line.front(), .points_here = marks_problem ? &parts_[headline_] : nullptr, .line_number = line.front()->line_number, .text = line.front()->source.text}); llvm::SmallVector labels = LabelsHangingFrom(spans, message_part, message_host); // The ranges a connector will hang from, which is what decides whether a // range can afford to give up half a column at an end. llvm::SmallPtrSet carries_label; for (PreparedPart* label : labels) { carries_label.insert(label == message_part && message_host ? message_host : label); } SeparateTouchingEnds(spans, carries_label); PlaceLabels(metrics_, labels, message_part, message_host, spans, content_x_, columns_); // Right to left, so that each label's connector reaches across the ones // already hanging rather than descending through them. llvm::stable_sort(labels, [](const PreparedPart* lhs, const PreparedPart* rhs) { return lhs->source.anchor > rhs->source.anchor; }); rows_.push_back( {.kind = Row::Kind::Annotation, .spans = {spans.begin(), spans.end()}}); AddLabelRows(labels); } auto RowLayout::AddLabelRows(llvm::ArrayRef labels) -> void { for (PreparedPart* label : labels) { // A single unbreakable word can be wider than what is left to the right // of where the label hangs, and hanging it there would push it out of the // frame. Wrapping never breaks a word, so the widest one is the least // width the label can be drawn in. Below that the label is out-dented to // the column the source starts in, where the whole width is available, // and the line reaching back to the connector is what says which range it // belongs to. bool routed = content_x_ + label->source.anchor + LabelIndentWidth + buffer_->MeasureWrapWidth(label->label) > columns_; rows_.push_back({.kind = routed ? Row::Kind::RoutedLabel : Row::Kind::Label, .part = label, .text = label->label}); } } auto RowLayout::AddMessageRows(const PreparedPart& part) -> void { rows_.push_back({.kind = Row::Kind::Message, .part = &part, .text = MessageRowText(part)}); } auto RowLayout::InSnippet() const -> bool { if (rows_.empty()) { return false; } switch (rows_.back().kind) { case Row::Kind::Source: case Row::Kind::Annotation: case Row::Kind::Label: case Row::Kind::RoutedLabel: case Row::Kind::Elision: case Row::Kind::Gap: return true; case Row::Kind::Message: case Row::Kind::Anchor: case Row::Kind::Separator: case Row::Kind::Context: return false; } } // Draws the line running from an underline at `annotation_y` down to `meet_y`, // in the column a label hangs from. // // It stops at the center of the cell the underline drew rather than redrawing // it, so where the underline runs out through both sides of that cell the two // form a junction. Where it stands in for a mark of one column instead, // stopping at the center is what keeps the mark to the lower half of its cell, // so that two of them in neighboring columns read as two marks rather than as // one line drawn down both. static auto DrawConnector(Buffer& buffer, int x, int annotation_y, int meet_y, const Style& style) -> void { buffer.DrawVerticalLine(x, annotation_y, meet_y - annotation_y + 1, style, LineEnd::Center, LineEnd::Center); } // Draws `number` in the columns before the frame, or nothing when there is no // line to name. // // `points_here` is the message whose location is on this line, which takes the // margin outside the numbers and colors the number to match what the headline // says. Everything else keeps the frame's own color, so the one line the // message is about is the one that isn't part of the frame. static auto DrawLineNumber(Buffer& buffer, int frame_x, int y, int number, const PreparedPart* points_here) -> void { if (number <= 0) { return; } std::string text = std::to_string(number); // The line the problem is on takes the headline's color and its weight, and // every other line number is left as light as the frame it belongs to, so // that the one being reported is the one that stands out rather than every // number competing with it. Style style = points_here ? points_here->style : FrameStyle().Bold(false); buffer.DrawText(frame_x - 1 - text.size(), y, text, style); if (points_here) { buffer.DrawText(0, y, LineMark, style); } } // Returns whether `text` and `suffix`, wrapped together into `columns` at // (x, y), fit on the one row they start on. // // Measuring the two together, rather than asking whether the suffix fits // after the text, is what gives a block that wrapped at all a tag naming it // rather than trailing its last word. static auto WrappedSharesRow(const Buffer& buffer, int x, int y, int columns, llvm::StringRef text, llvm::StringRef suffix) -> bool { return buffer .MeasureWrappedText(x, y, /*margin=*/x, columns, (llvm::Twine(text) + suffix).str()) .y == y; } // Returns the row after what `DrawWrapped` below draws, without drawing it. static auto MeasureWrapped(const Buffer& buffer, int x, int y, int columns, llvm::StringRef text, llvm::StringRef suffix) -> int { Buffer::DrawEnd end = buffer.MeasureWrappedText(x, y, /*margin=*/x, columns, text); if (suffix.empty() || WrappedSharesRow(buffer, x, y, columns, text, suffix)) { return end.y + 1; } return end.y + 2; } // Draws `text` wrapped into `columns` at (x, y), with `suffix` after it, and // returns the row after what it drew. // // The suffix is drawn on its own so that it keeps its own style, and placed // where wrapping would have put it: after the text where the two share a row, // and on a row of its own at the margin where they do not. static auto DrawWrapped(Buffer& buffer, int x, int y, int columns, llvm::StringRef text, llvm::StringRef suffix, Style style = Style()) -> int { if (suffix.empty()) { return buffer.DrawWrappedText(x, y, /*margin=*/x, columns, text, style).y + 1; } Buffer::DrawEnd end = buffer.DrawWrappedText(x, y, /*margin=*/x, columns, text, style); if (WrappedSharesRow(buffer, x, y, columns, text, suffix)) { buffer.DrawText(end.x, end.y, suffix, KindStyle()); return end.y + 1; } // The separating space would indent a tag that starts a row. buffer.DrawText(x, end.y + 1, suffix.ltrim(), KindStyle()); return end.y + 2; } auto Renderer::Render(Terminal::OutputBufferRef out, const Diagnostic& diagnostic) const -> void { // Widths depend only on the character set, so this measures without ever // drawing into anything. Metrics metrics(capabilities_.charset); // A context names the operation that failed, and leads the diagnostic when // there is one: it is the failure the reader is being told about, and the // message is then read against the code like anything else explaining it. const Context* leading_context = LeadingContext(diagnostic); // A `PreparedPart` is undesirably large for inline storage by // SmallVector, so we specify 0. llvm::SmallVector parts; int headline = -1; // Which colors this terminal gets, decided once: everything drawn takes one // of them. Theme problem_theme = LevelTheme(diagnostic.level, capabilities_); Theme explanation_theme = ExplanationTheme(capabilities_); // What leads the diagnostic is drawn at its level and everything else as // something explaining it, except that a primary range is part of the problem // and so keeps the level's color wherever down the frame it sits. auto add_part = [&](bool is_headline, bool is_problem, bool from_message_loc, const Loc& loc, std::string text, llvm::StringRef name) { if (is_headline) { headline = parts.size(); } parts.push_back( PreparePart(metrics, is_problem ? Part::Problem : Part::Explanation, from_message_loc, is_headline ? LevelText(diagnostic.level) : ExplanationText, ThemeStyle(is_problem ? problem_theme : explanation_theme), loc, std::move(text), name, is_headline, include_kind_)); }; // The same file reached the same way is a path drawn once. A path says how a // file was got to, so every range marked in that file has the same one, and // repeating it under each of them says nothing the first didn't. std::string last_place; auto add_location_info = [&](llvm::ArrayRef steps, const Loc& reaches) { std::string place = reaches.filename.str(); for (const LocationInfo& step : steps) { place += "\n" + step.Format() + " " + FormatLocation(step.loc); } if (place == last_place) { return; } last_place = std::move(place); for (const LocationInfo& step : steps) { parts.push_back(PreparePart( metrics, Part::LocationInfo, /*from_message_loc=*/false, /*level_text=*/"", ContextStyle(), step.loc, step.Format(), step.name, /*is_headline=*/false, include_kind_)); } }; // The contexts, then the message, then everything attached to explain it, // which is the order they are read in. for (const Context& context : diagnostic.contexts) { add_location_info(context.location_info, context.loc); // Only the leading context is the failure being reported; the rest name // operations it happened inside, which explain it. bool is_headline = &context == leading_context; add_part(is_headline, is_headline, /*from_message_loc=*/false, context.loc, context.Format(), context.name); } const Message& message = diagnostic.message; add_location_info(message.location_info, message.loc); // The message states the problem whether or not a context leads it: a // context gives it a sentence to sit under, and doesn't demote what it says // to an explanation of something else. Marking it otherwise puts an // informational connector on a range that is marked as part of the problem. bool message_leads = leading_context == nullptr; add_part(message_leads, /*is_problem=*/true, /*from_message_loc=*/true, message.loc, message.Format(), message.kind.name()); for (const Label& label : diagnostic.labels) { add_location_info(label.location_info, label.loc); add_part(/*is_headline=*/false, label.category == LabelCategory::Primary, /*from_message_loc=*/false, label.loc, label.Format(), label.name); } CARBON_CHECK(headline >= 0, "Either the context or the message leads."); // With snippets off, every diagnostic takes the compact form: nothing below // is drawn, so none of its geometry needs deciding. if (!snippets_) { RenderCompact(out, parts); return; } // Where content starts, which is what a width has to hold on top of the // source it shows. Line numbers sit to the left of the frame, so their width // is what decides which column it runs down; any line shown between two spans // has a number between theirs, so the widest is always one of these. int frame_x = MarginWidth + LineNumberColumns + 1; for (const PreparedPart& part : parts) { if (part.has_source && part.line_number > 0) { frame_x = std::max( frame_x, MarginWidth + metrics.Width(std::to_string(part.line_number)) + 1); } } int content_x = frame_x + FrameToContentWidth; // What the gutter costs when a line number fits the columns set aside for it, // which is what the target width is built on. A wider number spends the // difference out of the source rather than out of the width. int target_content_x = MarginWidth + LineNumberColumns + 1 + FrameToContentWidth; // The width every layout decision below is made against, taken from the // capabilities when they hold one. `COLUMNS` is whatever a user exported, so // that width is held to what a grid can hold before anything is laid out // against it. When there is none to take, the target is the source width // plus whatever this diagnostic spends reaching it, so the gutter is paid // for out of the total rather than out of the code. int columns = capabilities_.columns ? std::min(*capabilities_.columns, Buffer::MaxColumns) : target_content_x + FormattedSourceColumns; // What is left for source once the frame and its gutter are paid for. This // has to happen before the rows take their copy of the source they show. int source_columns = std::max(columns - content_x, 1); // The frame exists to hold source, so a diagnostic with none to show -- one // for a file that couldn't be opened, say -- gets the compact form instead of // an empty frame around a location. So does a terminal too narrow to hold a // frame, a gutter, and a label hanging off a span: below that the compact // form carries more in the columns there are. bool has_source = llvm::any_of( parts, [](const PreparedPart& part) { return part.has_source; }); if (!has_source || columns < MinFrameColumns) { RenderCompact(out, parts); return; } RenderFramed(out, parts, headline, diagnostic.level, frame_x, content_x, source_columns, columns); } // Draws the framed form: the headline, and under it one frame holding every // anchor, snippet, and label. The geometry arrives from `Render`, which chose // it; the spans are windowed in `LayOutRows`, which knows which of them share // a view of a line. auto Renderer::RenderFramed(Terminal::OutputBufferRef out, llvm::MutableArrayRef parts, int headline, Level level, int frame_x, int content_x, int source_columns, int columns) const -> void { Theme problem_theme = LevelTheme(level, capabilities_); Theme explanation_theme = ExplanationTheme(capabilities_); // Every layout decision is made against `columns`, so the buffer is built for // that rather than from the capabilities directly: where they held no width, // this is the one the layout chose. Terminal::Capabilities layout_capabilities = capabilities_; layout_capabilities.columns = columns; Buffer buffer(layout_capabilities); llvm::SmallVector rows = RowLayout(buffer, parts, headline, problem_theme, explanation_theme, source_columns, content_x, columns) .LayOut(); Glyphs glyphs = Glyphs::For(capabilities_.charset); // The headline, wrapped under itself so that a continuation of it can't read // as the start of another diagnostic. const PreparedPart& head = parts[headline]; // The separator beside an annotation runs down the frame's own column and // reads as part of it, so it takes one color for the whole diagnostic rather // than the color of whichever range happens to be on that row. That color is // the headline's, which is what the mark in the margin and the line number // beside the reported line take. const Style annotation_style = MarkStyle(head.style); Buffer::DrawEnd end = buffer.DrawText(0, 0, head.level_text, head.style); int head_x = end.x; int head_bottom = DrawWrapped(buffer, head_x, 0, columns - head_x, head.text, head.kind_suffix, Style().Bold()); // Then everything else, hanging off one vertical line down the frame's // column. Its corners and tees come out of the junctions the buffer forms // where the stubs meet it, and rows that mark themselves draw their glyph // over it. A row that wraps pushes the ones below it down, so the cursor // follows what was drawn rather than counting rows. int frame_top = head_bottom; int y = frame_top; // The row the underlines were drawn on, which is where every label below it // reaches back up to. A label can be several rows down from it, since the // labels on one underline stack. int annotation_y = -1; // A routed label's words, held back until every connector is drawn; see the // `RoutedLabel` case below. struct DeferredText { int x; int y; int columns; const Row* row; }; llvm::SmallVector deferred_text; for (const Row& row : rows) { CARBON_CHECK(annotation_y >= 0 || (row.kind != Row::Kind::Label && row.kind != Row::Kind::RoutedLabel), "A label hangs off the underline laid out above it."); int next_y = y + 1; const PreparedPart* part = row.part; // What this row draws over the frame, from `mark_from` down, or nothing to // leave the frame's own line showing through. std::optional mark; Style mark_style = FrameStyle(); int mark_from = y; switch (row.kind) { case Row::Kind::Message: { buffer.DrawHorizontalLine(frame_x, y, 2, FrameStyle(), LineEnd::Center, LineEnd::Edge); Buffer::DrawEnd end = buffer.DrawText(content_x + 1, y, part->level_text, part->style); next_y = DrawWrapped( buffer, end.x, y, columns - end.x, row.text, part->KindSuffixOn(/*is_label_row=*/false), part->role == Part::LocationInfo ? ContextStyle() : Style()); mark = glyphs.annotation; mark_from = y + 1; break; } case Row::Kind::Anchor: // `╭─┤ location`. The bracket is drawn as lines so that the character // set decides how it looks, and so that the context above can reach // down into it. buffer.DrawHorizontalLine(frame_x, y, 3, FrameStyle()); buffer.DrawVerticalLine(frame_x + 2, y, 1, FrameStyle(), LineEnd::Edge, LineEnd::Edge); buffer.DrawText(frame_x + AnchorPrefixWidth, y, part->location, Style()); break; case Row::Kind::Context: { // The connector descends into the bracket before the file name in the // anchor below, so the row reads as leading to it. Drawn downwards so // that several of these stack into one line. int bracket_x = frame_x + 2; buffer.DrawVerticalLine(bracket_x, y, 2, FrameStyle()); buffer.DrawHorizontalLine(bracket_x, y, ConnectorWidth - 1, FrameStyle(), LineEnd::Center, LineEnd::Edge); std::string text = ContextText(*part); int text_x = bracket_x + ConnectorWidth; Buffer::DrawEnd end = buffer.DrawText(text_x, y, text, ContextStyle()); buffer.DrawText(end.x, end.y, part->kind_suffix, KindStyle()); break; } case Row::Kind::Source: DrawLineNumber(buffer, frame_x, y, row.line_number, row.points_here); buffer.DrawText(content_x, y, row.text, Style()); break; case Row::Kind::Annotation: { annotation_y = y; for (const PreparedPart* span : row.spans) { DrawUnderline(buffer, content_x, y, *span, span->mark_style); } mark = glyphs.annotation; mark_style = annotation_style; break; } case Row::Kind::Label: { // The label hangs to the right of the connector, which comes in from // the left to meet the bar framing it, at its middle row -- the row // above the middle when it has an even number of them. int anchor_x = content_x + part->source.anchor; int text_x = anchor_x + LabelIndentWidth; int bar_x = text_x - LabelFrameWidth; const Style& style = part->mark_style; next_y = DrawWrapped(buffer, text_x, y, columns - text_x, row.text, part->KindSuffixOn(/*is_label_row=*/true)); // The bar is a stroke on each of the label's rows rather than one line // running between them, so every cell of it reads as a bar: the one the // connector arrives at is a tee rather than the corner an end would be, // and a label of one row is framed the same as a label of ten. for (int bar_y : llvm::seq(y, next_y)) { buffer.DrawVerticalLine(bar_x, bar_y, 1, style, LineEnd::Edge, LineEnd::Edge); } // The connector runs from the underline down past whatever labels are // already hanging to the right of this one, all of which leave the // column it descends in clear -- except a routed label, whose words // are drawn last so that they win the cell; see below. int meet_y = y + (next_y - y - 1) / 2; DrawConnector(buffer, anchor_x, annotation_y, meet_y, style); buffer.DrawHorizontalLine(anchor_x, meet_y, ConnectorWidth, style); mark = glyphs.annotation; mark_style = annotation_style; break; } case Row::Kind::RoutedLabel: { // The label is out-dented past its anchor, so there is no room to reach // it from the left and the bar framing it is what reaches back instead: // its top turns right along the row under the underline and runs to the // anchor. Meeting the label at its top rather than its middle is what // lets the bar sit directly against the text, which is where the width // this form exists to find comes from. int anchor_x = content_x + part->source.anchor; int bar_x = content_x; int text_x = bar_x + LabelFrameWidth; const Style& style = part->mark_style; DrawConnector(buffer, anchor_x, annotation_y, y, style); buffer.DrawHorizontalLine(bar_x, y, anchor_x - bar_x + 1, style); // The words reach across the full width, so a label drawn after this // one -- to its left, and so below it -- runs its connector through // their rows. The words win the cells they share: they are measured // now, so the rows below land where they will, and drawn once every // connector is down, so a connector crossing this label is interrupted // by its words rather than striking through them. next_y = MeasureWrapped(buffer, text_x, y + 1, columns - text_x, row.text, part->KindSuffixOn(/*is_label_row=*/true)); deferred_text.push_back({.x = text_x, .y = y + 1, .columns = columns - text_x, .row = &row}); buffer.DrawVerticalLine(bar_x, y, next_y - y, style, LineEnd::Center, LineEnd::Edge); mark = glyphs.annotation; mark_style = annotation_style; break; } case Row::Kind::Elision: mark = glyphs.elision; break; case Row::Kind::Gap: // The frame's own line running through is all this row is. break; case Row::Kind::Separator: // The same rule that closes the diagnostic, tee'd because the frame // carries on past it. buffer.DrawHorizontalLine(frame_x, y, SnippetRuleWidth, FrameStyle(), LineEnd::Center, LineEnd::Edge); break; } // The frame's line runs the whole height of the row, edge to edge, so that // consecutive rows carry on into each other without either reaching into // the other's cells. Only the topmost row starts at a center, which is what // leaves that cell a corner for the anchor to turn. It can't be drawn in // one pass afterwards because a row that wraps pushes the ones below it // down, so how far it reaches isn't known until every row is drawn -- and // it can't be drawn in one pass beforehand because it would cover the // glyphs. buffer.DrawVerticalLine(frame_x, y, next_y - y, FrameStyle(), y == frame_top ? LineEnd::Center : LineEnd::Edge, LineEnd::Edge); if (mark) { for (int row_y : llvm::seq(mark_from, next_y)) { buffer.DrawCodePoint(frame_x, row_y, *mark, mark_style); } } y = next_y; } // The routed labels' words, drawn over whatever connectors crossed them. for (const DeferredText& text : deferred_text) { DrawWrapped(buffer, text.x, text.y, text.columns, text.row->text, text.row->part->KindSuffixOn(/*is_label_row=*/true)); } // The closing rule turns the corner the frame's last row left for it. buffer.DrawVerticalLine(frame_x, y, 1, FrameStyle(), LineEnd::Edge, LineEnd::Center); buffer.DrawHorizontalLine(frame_x, y, SnippetRuleWidth, FrameStyle(), LineEnd::Center, LineEnd::Edge); buffer.Render(out, capabilities_.color_mode); } // Draws the row `part` takes in the compact form -- its location with the // extent of its range, level word, words, and kind -- and returns whether one // was drawn: a part that only marks a range has nothing to say on a row of its // own. // // Location information reads words first, leading to its own location, as it // does in the frame. Reading it the other way around would make the words say // the wrong thing. static auto DrawMessageRow(Buffer& buffer, int y, const PreparedPart& part) -> bool { // With no frame for a label to hang off, its words serve as the part's own. const std::string& words = part.text.empty() ? part.label : part.text; if (part.role == Part::LocationInfo) { Buffer::DrawEnd end = buffer.DrawText(0, y, ContextText(part), ContextStyle()); buffer.DrawText(end.x, end.y, part.kind_suffix, KindStyle()); return true; } if (words.empty()) { return false; } // Each run picks up where the one before it ended rather than being placed // at a width measured separately, so the two can't disagree. Buffer::DrawEnd end = {.x = 0, .y = y}; if (!part.location.empty()) { // The location carries the extent of the range as `-`, which is the // one place this form has to say it; the framed form draws it instead. std::string location = part.location; if (part.location_end_column > 0) { location += "-" + std::to_string(part.location_end_column); } end = buffer.DrawText(0, y, location + ": ", Style().Bold()); } end = buffer.DrawText(end.x, end.y, part.level_text, part.style); end = buffer.DrawText(end.x, end.y, words, Style()); buffer.DrawText(end.x, end.y, part.kind_suffix, KindStyle()); return true; } auto Renderer::RenderCompact(Terminal::OutputBufferRef out, llvm::ArrayRef parts) const -> void { // Nothing here wraps, so the width binds nothing: each row runs as wide as // the runs drawn into it. The capabilities still decide how those are // measured and colored. Buffer buffer(capabilities_); int y = 0; for (const PreparedPart& part : parts) { if (DrawMessageRow(buffer, y, part)) { ++y; } } buffer.Render(out, capabilities_.color_mode); } auto FormatLocation(const Loc& loc) -> std::string { if (loc.filename.empty()) { return ""; } // A filename is drawn on one row like anything else, so a control byte in it // -- a newline most of all -- would break the frame or split a compact line // in two. The bytes are the name of a real file, so they are replaced rather // than dropped: the location still reads, and nothing it holds can move the // cursor. std::string result; for (char c : loc.filename.take_front(MaxDrawnBytes)) { result += static_cast(c) < 0x20 || c == 0x7f ? ' ' : c; } // The parts drop off the right as they become unknown. if (loc.line_number > 0) { result += ":" + std::to_string(loc.line_number); if (loc.column_number > 0) { result += ":" + std::to_string(loc.column_number); } } return result; } auto PrintSnippet(llvm::raw_ostream& out, const Loc& loc, int indent) -> void { // Crash output is plain: it is going into a stack trace, and whatever is // writing it has bigger problems than color. Metrics metrics(Charset::Ascii); llvm::SmallString<256> bytes; if (loc.column_number <= 0) { return; } Glyphs glyphs = Glyphs::For(Charset::Ascii); std::string number = loc.line_number > 0 ? std::to_string(loc.line_number) : std::string(); int frame_x = indent + std::max(number.size(), 1) + 1; int content_x = frame_x + FrameToContentWidth; PreparedSource source = PrepareSource( metrics, loc.line, Bytes(loc.column_number), Bytes(loc.length)); WindowSource(metrics, source, FormattedSourceColumns, source.span_start); Buffer buffer(content_x + source.Width(metrics), Charset::Ascii); buffer.DrawVerticalLine(frame_x, /*y=*/0, /*length=*/2, FrameStyle()); buffer.DrawText(frame_x - 1 - number.size(), 0, number, FrameStyle()); buffer.DrawText(content_x, 0, source.text, Style()); buffer.DrawCodePoint(frame_x, 1, glyphs.annotation, FrameStyle()); buffer.DrawHorizontalLine(content_x + source.span_start, 1, source.span_length, Style(), LineEnd::Edge, LineEnd::Edge); buffer.Render(bytes, Terminal::ColorMode::NoColor); out << bytes; } } // namespace Carbon::Diagnostics