Last active
December 24, 2025 00:25
-
-
Save rikkimax/f39721fe9f9377efe6896b30ad39c860 to your computer and use it in GitHub Desktop.
Rust style diagnostic reporting code, toy that tries to get the basic algorithm right.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module diagreport.app; | |
| import diagreport.defs; | |
| import diagreport.geometry; | |
| import diagreport.renderer; | |
| import std.stdio; | |
| import std.conv; | |
| import std.range; | |
| import std.array; | |
| import std.string; | |
| void main() | |
| { | |
| const testCase = 2; | |
| string filename; | |
| string source; | |
| string[] messagesText; | |
| Help[] help; | |
| int firstLineNumber; | |
| if (testCase == 1) | |
| { | |
| filename = "src/format.rs"; | |
| firstLineNumber = 11; | |
| source = ") -> Option<String> { | |
| for ann in annotations { | |
| match (ann.range.0, ann.range.1) { | |
| (None, None) => continue, | |
| (Some(start), Some(end)) if start > end_index => continue, | |
| (Some(start), Some(end)) if start >= start_index => { | |
| let label = if let Some(ref label) = ann.label { | |
| format!(\" {}\", label) | |
| } else { | |
| String::from(\"\") | |
| }; | |
| return Some(format!( | |
| \"{}{}{}\", | |
| \" \".repeat(start - start_index), | |
| \"^\".repeat(end - start), | |
| label | |
| )); | |
| } | |
| _ => continue, | |
| } | |
| }"; | |
| messagesText = [ | |
| "cannot construct `Box<_, _>` with struct literal syntax due to private fields\nand must", | |
| "unlike in C++, Java, and C#, functions are declared in `impl` blocks\nand more here", | |
| "expected `Option<String>` because of return type", | |
| "expected enum `std::option::Option", "some help", | |
| "and more help\nfor you", "for you help", "", | |
| "conclusion\ninfo here", "header", "footer" | |
| ]; | |
| help = [ | |
| Help([ | |
| Diagnostic(11, 11, Message(2, 4, false, 5)), | |
| Diagnostic(15, 15, Message(12, 70, true, 6)) | |
| ], Message(0, 0, false, 8), Message(0, 0, true, 9)), | |
| Help([Diagnostic(20, 20, Message(20, 43, false, 7)),], Message(0, | |
| 0, false, 10), Message(0, 0, false, 11)), | |
| ]; | |
| event(filename, source, firstLineNumber, [ | |
| Diagnostic(11, 11, Message(5, 19, false, 3)), | |
| Diagnostic(12, 31, Message(4, 28), Message(4, 5, false, 4)), | |
| ], messagesText, help); | |
| } | |
| else if (testCase == 2) | |
| { | |
| filename = "scope_infer_diagnostic.d"; | |
| firstLineNumber = 1; | |
| source = "void* globalPtr; | |
| void callee(Writer)(Writer w) @trusted { | |
| globalPtr = cast(void*)&w; // escapes w | |
| w(\"x\"); | |
| } | |
| void inner(Writer)(scope Writer w) { | |
| callee(w); // scope violation: w passed to non-scope parameter | |
| } | |
| void outer() @safe { | |
| int x; | |
| inner((const(char)[] s) { x++; }); | |
| }"; | |
| messagesText = [ | |
| "`w` is not `scope`", | |
| "Cannot infer `w` as `scope` as it escapes into `globalPtr` which is a global variable", | |
| "Cannot call `inner` due to it being @system", | |
| "Failed to infer `@safe`", | |
| "Due to `callee` parameter `w` not being scope" | |
| ]; | |
| event(filename, source, firstLineNumber, [ | |
| Diagnostic(12, 15, Message(0, 0, false, 0)), | |
| Diagnostic(14, 14, Message(4, 38, false, 3)), | |
| ], messagesText, null); | |
| event(filename, source, firstLineNumber, [ | |
| Diagnostic(8, 10, Message(0, 1, false, 4)), | |
| Diagnostic(9, 9, Message(4, 14, false, 5)), | |
| ], messagesText, null); | |
| event(filename, source, firstLineNumber, [ | |
| Diagnostic(3, 6, Message(0, 0, false, 0)), | |
| Diagnostic(3, 3, Message(20, 28, false, 1)), | |
| Diagnostic(4, 4, Message(4, 30, false, 2)), | |
| ], messagesText, null); | |
| } | |
| } | |
| void event(string filename, string source, int firstLineNumber, | |
| Diagnostic[] diagnostics, string[] messagesText, Help[] help) | |
| { | |
| string[] lines = source.splitLines; | |
| Renderer renderer; | |
| renderer.filename = filename; | |
| renderer.diagnostics = diagnostics; | |
| renderer.help = help; | |
| renderer.emitRaw = (string text) => write(text); | |
| renderer.emitRawFormat = (const(char)* fmt, ...) { | |
| import core.stdc.stdarg; | |
| va_list args; | |
| va_start(args, fmt); | |
| vprintf(fmt, args); | |
| va_end(args); | |
| }; | |
| renderer.emitMargin = (string text) => write("\x1b[33m", text, "\x1b[0m"); | |
| renderer.emitHeader = () => write("\x1b[31merror\x1b[0m: "); | |
| renderer.emitHeaderMultiLinePrefix = () => write(" "); | |
| renderer.emitFooter = () => write("\x1b[34mnote:\x1b[0m "); | |
| renderer.emitFooterMultiLinePrefix = () => write(" "); | |
| renderer.emitHelp = () => write("\x1b[34mhelp:\x1b[0m "); | |
| renderer.emitHelpMultiLinePrefix = () => write(" "); | |
| renderer.emitGutter = (string text) => write("\x1b[34m", text, "\x1b[0m"); | |
| renderer.emitSquiggle = (string text) => write("\x1b[31m", text, "\x1b[0m"); | |
| renderer.getSourceCode = (int lineNumber) => lines[lineNumber - firstLineNumber]; | |
| renderer.emitMessageSingleLine = (ref Message message) { | |
| if (message.id > 0 && message.id <= messagesText.length) | |
| write(messagesText[message.id - 1]); | |
| }; | |
| renderer.emitMessageMultiLine = (scope void delegate(bool isLast) beforeTextOnLine, | |
| ref Message message) { | |
| if (message.id > 0 && message.id <= messagesText.length) | |
| { | |
| string text1 = messagesText[message.id - 1]; | |
| size_t done; | |
| foreach (text2; text1.lineSplitter!(Yes.keepTerminator)) | |
| { | |
| done += text2.length; | |
| const isLast = done == text1.length; | |
| beforeTextOnLine(isLast); | |
| write(text2); | |
| if (isLast) | |
| writeln; | |
| } | |
| } | |
| }; | |
| renderer.render(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module diagreport.defs; | |
| /** | |
| Defines the vertical state of a diagnostic on a given line for drawing purposes. | |
| This enumeration captures the vertical state machine logic for a single diagnostic span. | |
| */ | |
| enum LineClassification | |
| { | |
| /// The line is not part of the diagnostic span. | |
| Inactive, | |
| /// The line is the start of the span and continues below. Implies the previous line was Inactive. | |
| SpanStart, | |
| /// The line is between the start and end. Implies the previous line and next line are Active. | |
| SpanContinue, | |
| /// The line is the end of the span and no other active diagnostics follow it. Implies the next line is Inactive. | |
| SpanEnd, | |
| /// The span starts and ends on the same line (a one-line diagnostic). | |
| SpanStartEnd | |
| } | |
| struct Diagnostic | |
| { | |
| /// The line number where the diagnostic span begins (inclusive). | |
| int start; | |
| /// The line number where the diagnostic span ends (inclusive). | |
| int end; | |
| Message startMessage; | |
| Message endMessage; | |
| size_t id; | |
| size_t offset() | |
| { | |
| return originalOffset; | |
| } | |
| package: | |
| size_t originalOffset; | |
| int column; | |
| } | |
| struct Message | |
| { | |
| int startColumn, endColumn; | |
| bool isMultiline; | |
| size_t id; | |
| } | |
| struct Help | |
| { | |
| Diagnostic[] diagnostics; | |
| Message startMessage; | |
| Message endMessage; | |
| size_t id; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module diagreport.geometry; | |
| import diagreport.defs; | |
| import std.stdio; | |
| struct TimeLineGeometry | |
| { | |
| Diagnostic[] diagnostics; | |
| int minToSkip; | |
| int toSkipBuffer; | |
| void delegate(int line, ref Diagnostic, LineClassification classification) columnDrawHandler; | |
| void delegate(int line, bool haveStartOrEndColumnsToLeft, bool previousLineColumnIsActive) columnEmptyHandler; | |
| void delegate(int line) onLineStart; | |
| void delegate(int line) onLineEnd; | |
| void delegate(int line) onLineSource; | |
| void delegate(int startLine, int endLine) onLinesSkippedBeforeMargin; | |
| void delegate(int startLine, int endLine) onLinesSkippedAfterMargin; | |
| uint delegate(int line, int startColumn, int endColumn, ref Diagnostic diag, ref Message message) graphemesBetweenPositions; | |
| void delegate(int line, int offsetToSquiggles, int numberOfSquiggles, | |
| ref Diagnostic diag, ref Message message, bool spansMultipleLines) lineHighlight; | |
| void delegate(int line, ref Diagnostic diag, ref Message message) printSingleLine; | |
| // first line has had onLineStart and printMargin called for it automatically, | |
| // last line has had onLineEnd called for it automatically. | |
| // call printMargin and emit offsetToMessage before you write a line of text | |
| void delegate(scope void delegate() printMargin, uint offsetToMessage, int line, | |
| int offsetToSquiggles, int numberOfSquiggles, ref Diagnostic diag, ref Message message) printMultiLine; | |
| void calculate() | |
| { | |
| assignColumns; | |
| int lineNumber = diagnostics[0].start; | |
| int allowBeforeSkipTo; | |
| int skipTo; | |
| for (;;) | |
| { | |
| int lastColumnEmitted = -numberOfTokenColumns; // Tracks total columns emitted on this line (across all layers) | |
| int minimumActiveColumn = -numberOfTokenColumns; | |
| int numberOfEventsForThisLine; | |
| int nextActiveLine = int.max; | |
| if (allowBeforeSkipTo > 0) | |
| { | |
| allowBeforeSkipTo--; | |
| if (allowBeforeSkipTo == 0) | |
| { | |
| onLinesSkippedBeforeMargin(lineNumber, skipTo); | |
| processDiagnosticLineEvents(lineNumber, 0, | |
| lastColumnEmitted, true, false, false); | |
| onLinesSkippedAfterMargin(lineNumber, skipTo); | |
| this.onLineEnd(lineNumber); | |
| lineNumber = skipTo; | |
| continue; | |
| } | |
| } | |
| else | |
| { | |
| foreach (ref diag; diagnostics) | |
| { | |
| bool startEndOnlyRange; | |
| const classification = calculateLineClassification(diag, | |
| lineNumber, startEndOnlyRange); | |
| if (classification != LineClassification.Inactive) | |
| { | |
| // Active on the current line. Update min column and event count. | |
| if (diag.column < minimumActiveColumn) | |
| minimumActiveColumn = diag.column; | |
| if (classification != LineClassification.SpanContinue) | |
| numberOfEventsForThisLine++; | |
| } | |
| if (diag.end >= lineNumber) | |
| { | |
| const nextLine = diag.start >= lineNumber ? diag.start : diag.end; | |
| if (nextLine < nextActiveLine) | |
| nextActiveLine = nextLine; | |
| } | |
| } | |
| if (nextActiveLine == int.max) | |
| break; | |
| else if (nextActiveLine - lineNumber > minToSkip) | |
| { | |
| allowBeforeSkipTo = toSkipBuffer; | |
| skipTo = nextActiveLine - toSkipBuffer; | |
| } | |
| } | |
| { | |
| onLineStart(lineNumber); | |
| processDiagnosticLineEvents(lineNumber, minimumActiveColumn, | |
| lastColumnEmitted, true, false, false); | |
| onLineSource(lineNumber); | |
| // we're responsible for calling onLineEnd due to withUser is set to false. | |
| onLineEnd(lineNumber); | |
| lastColumnEmitted = -numberOfTokenColumns; | |
| } | |
| // At this point we know how many columns to ignore = minimumActiveColumn. | |
| while ((numberOfColumns == 0 || lastColumnEmitted < numberOfColumns) | |
| && numberOfEventsForThisLine > 0) | |
| { | |
| onLineStart(lineNumber); | |
| processDiagnosticLineEvents(lineNumber, minimumActiveColumn, | |
| lastColumnEmitted, false, false, true); | |
| // userMessage is responsible for onLineEnd call due to withUser is set to true. | |
| numberOfEventsForThisLine--; | |
| } | |
| lineNumber++; | |
| } | |
| } | |
| private: | |
| int numberOfColumns; | |
| int numberOfTokenColumns; | |
| void assignColumns() | |
| { | |
| { | |
| // Determine the number of diagnostics that will overlap. | |
| // This is the maximum number of columns total. | |
| foreach (diag1; diagnostics) | |
| { | |
| if (diag1.start == diag1.end) | |
| { | |
| uint overlappingDiagCount; | |
| foreach (diag2; diagnostics) | |
| { | |
| if (diag2.start != diag2.end || diag1.start != diag2.start) | |
| continue; | |
| overlappingDiagCount++; | |
| } | |
| if (overlappingDiagCount > numberOfTokenColumns) | |
| numberOfTokenColumns = overlappingDiagCount; | |
| } | |
| else | |
| { | |
| uint overlappingDiagCount; | |
| foreach (diag2; diagnostics) | |
| { | |
| if (diag2.start == diag2.end) | |
| continue; | |
| else if (diag1.end < diag2.start) | |
| break; | |
| if (diag1.start <= diag2.end && diag1.end > diag2.start) | |
| overlappingDiagCount++; | |
| } | |
| if (overlappingDiagCount > numberOfColumns) | |
| numberOfColumns = overlappingDiagCount; | |
| } | |
| } | |
| } | |
| if (numberOfTokenColumns + numberOfColumns > 0) | |
| { | |
| int[] columnReleaseLine = new int[numberOfTokenColumns + numberOfColumns]; | |
| foreach (ref diag; diagnostics) | |
| { | |
| bool assigned; | |
| if (diag.start == diag.end) | |
| { | |
| if (numberOfTokenColumns > 1) | |
| { | |
| foreach (column; 0 .. numberOfTokenColumns) | |
| { | |
| if (diag.start <= columnReleaseLine[column]) | |
| continue; | |
| diag.column = column - numberOfTokenColumns; | |
| columnReleaseLine[column] = diag.end; | |
| assigned = true; | |
| break; | |
| } | |
| } | |
| else | |
| { | |
| diag.column = -1; | |
| assigned = true; | |
| } | |
| } | |
| else if (numberOfColumns > 1) | |
| { | |
| foreach (column; numberOfTokenColumns .. numberOfColumns + 1) | |
| { | |
| if (diag.start <= columnReleaseLine[column]) | |
| continue; | |
| diag.column = column - numberOfTokenColumns; | |
| columnReleaseLine[column] = diag.end; | |
| assigned = true; | |
| break; | |
| } | |
| } | |
| else | |
| assigned = true; | |
| assert(assigned); | |
| } | |
| } | |
| // Ensure there is at least one inactive column after the lines | |
| if (numberOfColumns > 0) | |
| numberOfColumns++; | |
| } | |
| void processDiagnosticLineEvents(int lineNumber, int minimumActiveColumn, | |
| ref int lastColumnEmitted, bool noStartEndAsInactive, | |
| bool noStartEndAsContinue, bool withUser) | |
| { | |
| int emittedDiags = lastColumnEmitted; | |
| const ifCalledMoreThanOnceForLine = lastColumnEmitted > numberOfTokenColumns; | |
| scope (exit) | |
| lastColumnEmitted = emittedDiags; | |
| Diagnostic* findActiveDiagInColumn(int lineNumber, int col, | |
| out LineClassification classification, out bool startEndOnlyRange) | |
| { | |
| if (lineNumber == 0) | |
| return null; | |
| foreach (ref diag; diagnostics) | |
| { | |
| if (diag.column != col) | |
| continue; | |
| classification = calculateLineClassification(diag, lineNumber, startEndOnlyRange); | |
| if (classification != LineClassification.Inactive) | |
| return &diag; | |
| } | |
| return null; | |
| } | |
| void emptyColumn(int onLine, int columnNumber, bool haveStartOrEndColumnsToLeft) | |
| { | |
| if (columnNumber < 0) | |
| return; | |
| LineClassification classification; | |
| bool startEndOnlyRange; | |
| Diagnostic* diag = findActiveDiagInColumn(onLine, columnNumber, | |
| classification, startEndOnlyRange); | |
| const isActive = classification == LineClassification.SpanStart | |
| || classification == LineClassification.SpanContinue; | |
| columnEmptyHandler(lineNumber, haveStartOrEndColumnsToLeft, isActive); | |
| } | |
| void printMargin() | |
| { | |
| int tempMaxDiagsEmitted = emittedDiags + 1; | |
| processDiagnosticLineEvents(lineNumber, minimumActiveColumn, | |
| tempMaxDiagsEmitted, false, true, false); | |
| } | |
| void userMessage(ref Diagnostic diag, ref Message message, bool spansMultipleLines) | |
| { | |
| if (!withUser) | |
| return; | |
| const offsetToSquiggles = this.graphemesBetweenPositions(lineNumber, | |
| 0, message.startColumn, diag, message); | |
| const lengthOfSquiggles = this.graphemesBetweenPositions(lineNumber, | |
| message.startColumn, message.endColumn, diag, message); | |
| // 1. squiggles ──────^^^^ | |
| lineHighlight(lineNumber, offsetToSquiggles, lengthOfSquiggles, | |
| diag, message, spansMultipleLines); | |
| if (message.isMultiline) | |
| { | |
| this.onLineEnd(lineNumber); | |
| printMultiLine(&printMargin, offsetToSquiggles, lineNumber, | |
| offsetToSquiggles, lengthOfSquiggles, diag, message); | |
| // user is responsible for handling new lines | |
| } | |
| else | |
| { | |
| this.printSingleLine(lineNumber, diag, message); | |
| this.onLineEnd(lineNumber); | |
| } | |
| } | |
| foreach (int column; 0 .. minimumActiveColumn) | |
| { | |
| // These columns do not have any active diagnostics in them. | |
| // So we'll call the empty handler preemptively. | |
| // No point checking for if a column is active in the main loop. | |
| columnEmptyHandler(lineNumber, false, false); | |
| } | |
| for (int column = minimumActiveColumn; column < numberOfColumns; column++) | |
| { | |
| LineClassification classification; | |
| bool startEndOnlyRange; | |
| Diagnostic* currentDiag = findActiveDiagInColumn(lineNumber, | |
| column, classification, startEndOnlyRange); | |
| if (column < lastColumnEmitted) | |
| classification = LineClassification.Inactive; | |
| else if (noStartEndAsInactive) | |
| { | |
| final switch (classification) | |
| { | |
| case LineClassification.SpanStart: | |
| case LineClassification.SpanStartEnd: | |
| case LineClassification.SpanEnd: | |
| classification = LineClassification.Inactive; | |
| break; | |
| case LineClassification.SpanContinue: | |
| case LineClassification.Inactive: | |
| break; | |
| } | |
| } | |
| else if (noStartEndAsContinue) | |
| { | |
| final switch (classification) | |
| { | |
| case LineClassification.SpanStart: | |
| classification = LineClassification.SpanContinue; | |
| break; | |
| case LineClassification.SpanStartEnd: | |
| classification = LineClassification.Inactive; | |
| break; | |
| case LineClassification.SpanEnd: | |
| classification = (column < lastColumnEmitted) | |
| ? LineClassification.Inactive : LineClassification.SpanContinue; | |
| break; | |
| case LineClassification.SpanContinue: | |
| case LineClassification.Inactive: | |
| break; | |
| } | |
| } | |
| final switch (classification) | |
| { | |
| case LineClassification.SpanStart: | |
| case LineClassification.SpanEnd: | |
| case LineClassification.SpanStartEnd: | |
| // This ends this call to processDiagnosticLineEvents. | |
| // Due to seeing a start/end/startend event. | |
| // Also ensures that all columns to the right are emitted. | |
| if (column >= 0) | |
| columnDrawHandler(lineNumber, | |
| *currentDiag, classification); | |
| emittedDiags++; | |
| const haveBefore = classification != LineClassification.SpanStartEnd; | |
| foreach (column2; column + 1 .. numberOfColumns) | |
| { | |
| LineClassification classification2; | |
| bool startEndOnlyRange2; | |
| Diagnostic* currentDiag2 = findActiveDiagInColumn(lineNumber, | |
| column2, classification2, startEndOnlyRange2); | |
| if (startEndOnlyRange2) | |
| columnDrawHandler(lineNumber, *currentDiag2, classification2); | |
| else | |
| emptyColumn(lineNumber - 1, column2, haveBefore); | |
| } | |
| userMessage(*currentDiag, classification == LineClassification.SpanEnd | |
| ? currentDiag.endMessage : currentDiag.startMessage, haveBefore); | |
| return; | |
| case LineClassification.SpanContinue: | |
| columnDrawHandler(lineNumber, | |
| *currentDiag, classification); | |
| emittedDiags++; | |
| break; | |
| case LineClassification.Inactive: | |
| emptyColumn(lineNumber - (!ifCalledMoreThanOnceForLine | |
| && !noStartEndAsContinue), column, false); | |
| emittedDiags++; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| private: | |
| LineClassification calculateLineClassification(ref Diagnostic diagnostic, | |
| int lineNumber, out bool startEndOnlyRange) | |
| { | |
| if (lineNumber < diagnostic.start || lineNumber > diagnostic.end) | |
| return LineClassification.Inactive; | |
| const isStart = (lineNumber == diagnostic.start), isEnd = (lineNumber == diagnostic.end); | |
| LineClassification ret; | |
| if (isStart && isEnd) | |
| ret = LineClassification.SpanStartEnd; | |
| else if (isStart) | |
| ret = LineClassification.SpanStart; | |
| else if (isEnd) | |
| ret = LineClassification.SpanEnd; | |
| else | |
| ret = LineClassification.SpanContinue; | |
| final switch (ret) | |
| { | |
| case LineClassification.SpanStart: | |
| if (diagnostic.startMessage.startColumn == 0 | |
| && diagnostic.startMessage.endColumn == 0 && diagnostic.startMessage.id == 0) | |
| startEndOnlyRange = true; | |
| break; | |
| case LineClassification.SpanEnd: | |
| if (diagnostic.endMessage.startColumn == 0 | |
| && diagnostic.endMessage.endColumn == 0 && diagnostic.endMessage.id == 0) | |
| startEndOnlyRange = true; | |
| break; | |
| case LineClassification.SpanStartEnd: | |
| case LineClassification.SpanContinue: | |
| case LineClassification.Inactive: | |
| break; | |
| } | |
| if (startEndOnlyRange) | |
| ret = LineClassification.SpanContinue; | |
| return ret; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module diagreport.renderer; | |
| import diagreport.defs; | |
| import diagreport.geometry; | |
| struct Renderer | |
| { | |
| Config config; | |
| string filename; | |
| Message header; | |
| Diagnostic[] diagnostics; | |
| Message footer; | |
| Help[] help; | |
| void delegate(string) emitRaw; | |
| void delegate(const(char)* fmt, ...) emitRawFormat; | |
| // num | << it is the | | |
| void delegate(string) emitMargin; | |
| // error: | |
| void delegate() emitHeader; | |
| // For multiline error messages, first is emitError. | |
| void delegate() emitHeaderMultiLinePrefix; | |
| void delegate() emitFooter; | |
| void delegate() emitFooterMultiLinePrefix; | |
| void delegate() emitHelp; | |
| void delegate() emitHelpMultiLinePrefix; | |
| // The ASCII art | |
| void delegate(string text) emitGutter; | |
| void delegate(string text) emitSquiggle; | |
| string delegate(int lineNumber) getSourceCode; | |
| void delegate(ref Message message) emitMessageSingleLine; | |
| void delegate(scope void delegate(bool isLast) beforeTextOnLine, ref Message message) emitMessageMultiLine; | |
| private | |
| { | |
| int minLineNumber; | |
| string columnWithoutNumber; | |
| string columnNumberFormat; | |
| // temporary | |
| int lastLineNumber; | |
| } | |
| /// Assumption, all members of this are one grapheme in size. | |
| struct Config | |
| { | |
| string margin = "│"; | |
| string marginRight = "├"; | |
| string marginToRight = "─"; | |
| string marginUpLeft = "╮"; | |
| string marginUpRight = "╭"; | |
| string marginDownLeft = "╯"; | |
| string marginDownRight = "╰"; | |
| string skippedLines = "╌"; | |
| string gutter = "│"; | |
| string gutterUpRight = "┌"; | |
| string gutterDownRight = "└"; | |
| string gutterToLabel = "─"; | |
| string gutterLeftRightUpDown = "┼"; | |
| string gutterAsSquiggle = "┘"; | |
| string squiggle = "^"; | |
| } | |
| void render() | |
| { | |
| if (diagnostics.length == 0) | |
| return; | |
| calculate; | |
| emitHeader2; | |
| emitMainDiag; | |
| emitFooter2; | |
| emitHelp2; | |
| } | |
| private: | |
| void calculate() | |
| { | |
| import core.stdc.stdio; | |
| import std.conv : text; | |
| import std.uni; | |
| import std.algorithm : sort; | |
| int maxLineNumber; | |
| minLineNumber = int.max; | |
| foreach (i, diag; diagnostics) | |
| { | |
| diag.originalOffset = i; | |
| if (diag.start < minLineNumber) | |
| minLineNumber = diag.start; | |
| if (diag.end > maxLineNumber) | |
| maxLineNumber = diag.end; | |
| } | |
| diagnostics.sort!((a, b) => a.start < b.start || (a.start == b.start && a.end < b.end)); | |
| // Calculate the line number length, to get the required with and without number strings. | |
| { | |
| int lineNumberLength = snprintf(null, 0, "%d", maxLineNumber); | |
| char[] temp; | |
| temp.length = lineNumberLength; | |
| temp[] = ' '; | |
| columnWithoutNumber = cast(string) temp; | |
| columnNumberFormat = text("%", lineNumberLength, "d\0"); | |
| } | |
| } | |
| void emitHeader2() | |
| { | |
| bool doneFirst; | |
| void beforeTextOnLine(bool isLast) | |
| { | |
| if (doneFirst) | |
| emitHeaderMultiLinePrefix(); | |
| else | |
| emitHeader(); | |
| doneFirst = true; | |
| } | |
| if (header.id > 0) | |
| { | |
| if (!header.isMultiline) | |
| { | |
| beforeTextOnLine(false); | |
| emitMessageSingleLine(header); | |
| } | |
| else | |
| emitMessageMultiLine(&beforeTextOnLine, header); | |
| } | |
| { | |
| emitRaw(columnWithoutNumber); | |
| emitRaw(" "); | |
| emitMargin(config.marginUpRight); | |
| emitRaw(" "); | |
| emitRaw(filename); | |
| emitRawFormat("(%d)\n", minLineNumber); | |
| } | |
| } | |
| void emitMainDiag() | |
| { | |
| lastLineNumber = 0; | |
| TimeLineGeometry(diagnostics, 3, 1, &columnDrawHandler, | |
| &columnEmptyHandler, &onLineStart, &onLineEnd, &onLineSource, | |
| &onLinesSkippedBeforeMargin, &onLinesSkippedAfterMargin, | |
| &graphemesBetweenPositions, &lineHighlight, &printSingleLine, &printMultiLine) | |
| .calculate; | |
| } | |
| void emitFooter2() | |
| { | |
| bool doneFirst; | |
| bool haveSomethingAfter; | |
| foreach (ref h; help) | |
| { | |
| if (h.startMessage.id != 0 || h.endMessage.id != 0) | |
| { | |
| haveSomethingAfter = true; | |
| break; | |
| } | |
| } | |
| void beforeTextOnLine(bool isLast) | |
| { | |
| emitRaw(columnWithoutNumber); | |
| if (doneFirst) | |
| { | |
| if (isLast && haveSomethingAfter) | |
| { | |
| emitRaw(" "); | |
| emitMargin(config.marginUpRight); | |
| emitMargin(config.marginDownLeft); | |
| } | |
| else | |
| { | |
| emitRaw(" "); | |
| emitMargin(config.margin); | |
| } | |
| emitRaw(" "); | |
| emitFooterMultiLinePrefix(); | |
| } | |
| else | |
| { | |
| if (isLast) | |
| { | |
| emitRaw(" "); | |
| emitMargin(config.marginRight); | |
| emitMargin(config.marginToRight); | |
| } | |
| else | |
| { | |
| emitRaw(" "); | |
| emitMargin(config.marginDownRight); | |
| emitMargin(config.marginUpLeft); | |
| } | |
| emitRaw(" "); | |
| emitFooter(); | |
| } | |
| doneFirst = true; | |
| } | |
| if (footer.id > 0) | |
| { | |
| if (!footer.isMultiline) | |
| { | |
| beforeTextOnLine(true); | |
| emitMessageSingleLine(footer); | |
| } | |
| else | |
| emitMessageMultiLine(&beforeTextOnLine, footer); | |
| } | |
| } | |
| void emitHelp2() | |
| { | |
| bool doneFirst; | |
| void beforeTextOnLine(bool isLast) | |
| { | |
| emitRaw(columnWithoutNumber); | |
| if (doneFirst) | |
| { | |
| if (isLast && help.length > 0) | |
| { | |
| emitRaw(" "); | |
| emitMargin(config.marginUpRight); | |
| emitMargin(config.marginDownLeft); | |
| } | |
| else | |
| { | |
| emitRaw(" "); | |
| emitMargin(config.margin); | |
| } | |
| emitRaw(" "); | |
| emitHelpMultiLinePrefix(); | |
| } | |
| else | |
| { | |
| if (isLast) | |
| { | |
| emitRaw(" "); | |
| emitMargin(config.marginRight); | |
| emitMargin(config.marginToRight); | |
| } | |
| else | |
| { | |
| emitRaw(" "); | |
| emitMargin(config.marginDownRight); | |
| emitMargin(config.marginUpLeft); | |
| } | |
| emitRaw(" "); | |
| emitHelp(); | |
| } | |
| doneFirst = true; | |
| } | |
| void emitMessage(ref Message message) | |
| { | |
| if (message.id == 0) | |
| return; | |
| doneFirst = false; | |
| if (message.isMultiline) | |
| { | |
| emitMessageMultiLine(&beforeTextOnLine, message); | |
| } | |
| else | |
| { | |
| beforeTextOnLine(true); | |
| emitMessageSingleLine(message); | |
| emitRaw("\n"); | |
| } | |
| } | |
| foreach (h; help) | |
| { | |
| if (h.startMessage.id == 0 && h.endMessage.id == 0) | |
| continue; | |
| emitMessage(h.startMessage); | |
| lastLineNumber = 0; | |
| TimeLineGeometry(h.diagnostics, 3, 1, &columnDrawHandler, | |
| &columnEmptyHandler, &onLineStart, &onLineEnd, &onLineSource, | |
| &onLinesSkippedBeforeMargin, &onLinesSkippedAfterMargin, | |
| &graphemesBetweenPositions, &lineHighlight, | |
| &printSingleLine, &printMultiLine).calculate; | |
| emitMessage(h.endMessage); | |
| } | |
| } | |
| void columnDrawHandler(int line, ref Diagnostic, LineClassification classification) | |
| { | |
| string glyph; | |
| final switch (classification) | |
| { | |
| case LineClassification.SpanStart: | |
| glyph = config.gutterUpRight; | |
| break; | |
| case LineClassification.SpanContinue: | |
| glyph = config.gutter; | |
| break; | |
| case LineClassification.SpanEnd: | |
| glyph = config.gutterDownRight; | |
| break; | |
| case LineClassification.SpanStartEnd: | |
| glyph = " "; | |
| break; | |
| case LineClassification.Inactive: | |
| break; | |
| } | |
| if (glyph.length > 0) | |
| emitGutter(glyph); | |
| } | |
| void columnEmptyHandler(int line, bool haveStartOrEndColumnsToLeft, | |
| bool previousLineColumnIsActive) | |
| { | |
| string glyph; | |
| if (haveStartOrEndColumnsToLeft && previousLineColumnIsActive) | |
| glyph = config.gutterLeftRightUpDown; | |
| else if (previousLineColumnIsActive) | |
| glyph = config.gutter; | |
| else if (haveStartOrEndColumnsToLeft) | |
| glyph = config.gutterToLabel; | |
| else | |
| glyph = " "; | |
| if (glyph.length > 0) | |
| emitGutter(glyph); | |
| } | |
| void onLineStart(int line) | |
| { | |
| if (lastLineNumber == line) | |
| emitRaw(columnWithoutNumber); | |
| else | |
| emitRawFormat(columnNumberFormat.ptr, line); | |
| emitRaw(" "); | |
| emitMargin(config.gutter); | |
| emitRaw(" "); | |
| lastLineNumber = line; | |
| } | |
| void onLineEnd(int line) | |
| { | |
| emitRaw("\n"); | |
| } | |
| void onLineSource(int line) | |
| { | |
| emitRaw(getSourceCode(line)); | |
| } | |
| void onLinesSkippedAfterMargin(int startLine, int endLine) | |
| { | |
| emitRawFormat("%.*s(%d)", cast(int) filename.length, filename.ptr, endLine); | |
| } | |
| uint graphemesBetweenPositions(int line, int startColumn, int endColumn, | |
| ref Diagnostic diag, ref Message message) | |
| { | |
| import std.uni; | |
| string text = getSourceCode(line); | |
| if (text.length < startColumn || text.length < endColumn) | |
| return 0; | |
| text = text[startColumn .. endColumn]; | |
| uint count; | |
| foreach (_; text.byGrapheme) | |
| { | |
| count++; | |
| } | |
| return count; | |
| } | |
| void lineHighlight(int line, int offsetToSquiggles, int numberOfSquiggles, | |
| ref Diagnostic diag, ref Message message, bool spansMultipleLines) | |
| { | |
| string offsetToSquigglesText = spansMultipleLines ? config.gutterToLabel : " "; | |
| foreach (_; 0 .. offsetToSquiggles) | |
| { | |
| emitGutter(offsetToSquigglesText); | |
| } | |
| if (numberOfSquiggles > 1) | |
| { | |
| foreach (_; 0 .. numberOfSquiggles) | |
| emitSquiggle(config.squiggle); | |
| } | |
| else if (spansMultipleLines) | |
| emitGutter(config.gutterAsSquiggle); | |
| else | |
| emitSquiggle(config.squiggle); | |
| } | |
| void printSingleLine(int line, ref Diagnostic diag, ref Message message) | |
| { | |
| emitRaw(" "); | |
| emitMessageSingleLine(message); | |
| } | |
| void onLinesSkippedBeforeMargin(int startLine, int endLine) | |
| { | |
| emitRaw(columnWithoutNumber); | |
| emitRaw(" "); | |
| emitMargin(config.skippedLines); | |
| emitRaw(" "); | |
| } | |
| void printMultiLine(scope void delegate() printMargin, uint offsetToMessage, int line, | |
| int offsetToSquiggles, int numberOfSquiggles, ref Diagnostic diag, ref Message message) | |
| { | |
| void beforeTextOnLine(bool isLast) | |
| { | |
| onLineStart(line); | |
| printMargin(); | |
| foreach (_; 0 .. offsetToMessage) | |
| emitRaw(" "); | |
| } | |
| emitMessageMultiLine(&beforeTextOnLine, message); | |
| } | |
| } |
Author
rikkimax
commented
Dec 24, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment