Skip to content

Instantly share code, notes, and snippets.

@rikkimax
Last active December 24, 2025 00:25
Show Gist options
  • Select an option

  • Save rikkimax/f39721fe9f9377efe6896b30ad39c860 to your computer and use it in GitHub Desktop.

Select an option

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.
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();
}
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;
}
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;
}
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);
}
}
@rikkimax
Copy link
Author

Capture4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment