mirror of
https://github.com/sbrow/thor.git
synced 2026-10-25 10:11:55 -04:00
refactor: Moved diagnostic code into its own package.
This commit is contained in:
+37
-242
@@ -1,61 +1,6 @@
|
||||
package mustache
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import "core:terminal/ansi"
|
||||
import "core:unicode/utf8"
|
||||
|
||||
// line_col returns the 1-indexed line and column for a byte offset in source.
|
||||
// Newlines ('\n') separate lines; '\r' is treated as part of '\r\n'. Column is
|
||||
// counted in bytes from the start of the line.
|
||||
line_col :: proc(source: string, pos_in: int) -> (line: int, col: int) {
|
||||
pos := pos_in
|
||||
if pos < 0 {
|
||||
return 1, 1
|
||||
}
|
||||
if pos > len(source) {
|
||||
pos = len(source)
|
||||
}
|
||||
line = 1
|
||||
col = 1
|
||||
for i := 0; i < pos; i += 1 {
|
||||
if source[i] == '\n' {
|
||||
line += 1
|
||||
col = 1
|
||||
} else {
|
||||
col += 1
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// line_text returns the Nth (1-indexed) line of source, without the trailing
|
||||
// newline. Returns "" if line is out of range.
|
||||
line_text :: proc(source: string, line: int) -> string {
|
||||
if line < 1 {
|
||||
return ""
|
||||
}
|
||||
current := 1
|
||||
start := 0
|
||||
for i := 0; i < len(source); i += 1 {
|
||||
if current == line {
|
||||
end := i
|
||||
for end < len(source) && source[end] != '\n' {
|
||||
end += 1
|
||||
}
|
||||
return source[start:end]
|
||||
}
|
||||
if source[i] == '\n' {
|
||||
current += 1
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if current == line {
|
||||
return source[start:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
import diags "../diagnostics"
|
||||
|
||||
// context_extent returns the byte offset of the start of the line containing
|
||||
// pos, plus the byte offsets of the start and end of the mustache tag at pos.
|
||||
@@ -115,68 +60,11 @@ context_extent :: proc(
|
||||
return line_start, pos, pos + 1
|
||||
}
|
||||
|
||||
// should_colorize returns true if stderr is a TTY and color output is wanted.
|
||||
should_colorize :: proc() -> bool {
|
||||
return os.is_tty(os.stderr)
|
||||
}
|
||||
|
||||
// count_lines returns the number of '\n'-separated lines in source.
|
||||
// A trailing newline does not add an extra line.
|
||||
count_lines :: proc(source: string) -> int {
|
||||
if len(source) == 0 {
|
||||
return 1
|
||||
}
|
||||
n := 1
|
||||
for c in source {
|
||||
if c == '\n' {
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
// Drop phantom last line if source ends with '\n'.
|
||||
if len(source) > 0 && source[len(source) - 1] == '\n' {
|
||||
n -= 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// digit_count returns the number of decimal digits in n (min 1).
|
||||
digit_count :: proc(n: int) -> int {
|
||||
if n <= 0 {
|
||||
return 1
|
||||
}
|
||||
c := 0
|
||||
x := n
|
||||
for x > 0 {
|
||||
c += 1
|
||||
x /= 10
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// display_width returns the number of terminal cells `s` occupies.
|
||||
// For ASCII this is byte length; for UTF-8 we count runes (combining
|
||||
// marks and wide CJK chars are still approximate).
|
||||
display_width :: proc(s: string) -> int {
|
||||
return utf8.rune_count_in_string(s)
|
||||
}
|
||||
|
||||
// format_error produces a rust-style multi-line diagnostic string.
|
||||
//
|
||||
// <msg>
|
||||
// --> <path>:<line>:<col>
|
||||
// |
|
||||
// N | <source line N-2>
|
||||
// N | <source line N-1>
|
||||
// N | <source line N — the error line>
|
||||
// | ^^^^^^^^^^^ <hint>
|
||||
// N | <source line N+1>
|
||||
// N | <source line N+2>
|
||||
// |
|
||||
//
|
||||
// `context_before`/`context_after` lines of context are shown around the
|
||||
// error line. Line numbers are right-aligned to the width of the largest
|
||||
// line number shown.
|
||||
format_error :: proc(
|
||||
// format_tag_error is a mustache-aware wrapper around diags.format_error.
|
||||
// It uses context_extent to compute the caret underline for `{{ }}` tags,
|
||||
// falling back to a single-rune underline when pos is not inside a tag.
|
||||
// When `span > 0`, the caret extends from pos to pos + span.
|
||||
format_tag_error :: proc(
|
||||
path: string,
|
||||
source: string,
|
||||
pos: int,
|
||||
@@ -187,135 +75,25 @@ format_error :: proc(
|
||||
colorize: bool = false,
|
||||
span: int = 0,
|
||||
) -> string {
|
||||
line, col := line_col(source, pos)
|
||||
total_lines := count_lines(source)
|
||||
|
||||
start_line := line - context_before
|
||||
if start_line < 1 {
|
||||
start_line = 1
|
||||
}
|
||||
end_line := line + context_after
|
||||
if end_line > total_lines {
|
||||
end_line = total_lines
|
||||
}
|
||||
|
||||
// Width of the line-number column (right-align).
|
||||
width := digit_count(end_line)
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
|
||||
sb := strings.builder_make(context.temp_allocator)
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
color := colorize
|
||||
red, faint, reset := "", "", ""
|
||||
if color {
|
||||
red = ansi.CSI + ansi.FG_RED + ansi.SGR
|
||||
faint = ansi.CSI + ansi.FAINT + ansi.SGR
|
||||
reset = ansi.CSI + ansi.RESET + ansi.SGR
|
||||
}
|
||||
|
||||
// Header line: message.
|
||||
strings.write_string(&sb, msg)
|
||||
strings.write_byte(&sb, '\n')
|
||||
|
||||
// Location line: " --> path:line:col" (width spaces + arrow).
|
||||
strings.write_string(&sb, faint)
|
||||
for _ in 0 ..< width {
|
||||
strings.write_byte(&sb, ' ')
|
||||
}
|
||||
strings.write_string(&sb, "--> ")
|
||||
strings.write_string(&sb, reset)
|
||||
fmt.sbprintf(&sb, "%s:%d:%d\n", path, line, col)
|
||||
|
||||
// Top gutter line.
|
||||
write_gutter(&sb, width, faint, reset)
|
||||
|
||||
// Caret extent for the error line.
|
||||
line_start, token_start, token_end := context_extent(source, pos)
|
||||
caret_start := pos
|
||||
caret_end := pos + 1
|
||||
if span > 0 {
|
||||
token_start = pos
|
||||
token_end = pos + span
|
||||
caret_end = pos + span
|
||||
} else {
|
||||
_, ts, te := context_extent(source, pos)
|
||||
caret_start = ts
|
||||
caret_end = te
|
||||
}
|
||||
caret_start_col := token_start - line_start + 1
|
||||
caret_end_col := token_end - line_start + 1
|
||||
if caret_end_col <= caret_start_col {
|
||||
caret_end_col = caret_start_col + 1
|
||||
}
|
||||
|
||||
// Context lines.
|
||||
for n in start_line ..= end_line {
|
||||
// Line number (right-aligned, faint).
|
||||
num_str := fmt.tprintf("%d", n)
|
||||
strings.write_string(&sb, faint)
|
||||
for _ in 0 ..< width - len(num_str) {
|
||||
strings.write_byte(&sb, ' ')
|
||||
}
|
||||
strings.write_string(&sb, num_str)
|
||||
strings.write_string(&sb, " | ")
|
||||
strings.write_string(&sb, reset)
|
||||
|
||||
strings.write_string(&sb, line_text(source, n))
|
||||
strings.write_byte(&sb, '\n')
|
||||
|
||||
// After the error line, emit the caret row.
|
||||
if n == line {
|
||||
strings.write_string(&sb, faint)
|
||||
for _ in 0 ..< width + 1 {
|
||||
strings.write_byte(&sb, ' ')
|
||||
}
|
||||
strings.write_string(&sb, "| ")
|
||||
strings.write_string(&sb, reset)
|
||||
|
||||
for _ in 1 ..< caret_start_col {
|
||||
strings.write_byte(&sb, ' ')
|
||||
}
|
||||
if color {
|
||||
strings.write_string(&sb, red)
|
||||
}
|
||||
for _ in 0 ..< caret_end_col - caret_start_col {
|
||||
strings.write_byte(&sb, '^')
|
||||
}
|
||||
if color {
|
||||
strings.write_string(&sb, reset)
|
||||
}
|
||||
if hint != "" {
|
||||
strings.write_byte(&sb, ' ')
|
||||
if color {
|
||||
strings.write_string(&sb, faint)
|
||||
}
|
||||
strings.write_string(&sb, hint)
|
||||
if color {
|
||||
strings.write_string(&sb, reset)
|
||||
}
|
||||
}
|
||||
strings.write_byte(&sb, '\n')
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing gutter line for visual closure.
|
||||
write_gutter(&sb, width, faint, reset)
|
||||
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
// write_gutter emits a faint pipe-only gutter line: `<width+1 spaces> |`.
|
||||
write_gutter :: proc(sb: ^strings.Builder, width: int, faint: string, reset: string) {
|
||||
strings.write_string(sb, faint)
|
||||
for _ in 0 ..< width + 1 {
|
||||
strings.write_byte(sb, ' ')
|
||||
}
|
||||
strings.write_string(sb, "|")
|
||||
strings.write_string(sb, reset)
|
||||
strings.write_byte(sb, '\n')
|
||||
return diags.format_error(
|
||||
path, source, pos, msg, hint,
|
||||
context_before, context_after,
|
||||
colorize,
|
||||
caret_start, caret_end,
|
||||
)
|
||||
}
|
||||
|
||||
// format_render_error produces a diagnostic for an Error value using the
|
||||
// template's path and source for context. Returns "" for nil errors.
|
||||
// If the error carries its own source/path (from tag_error), those are used
|
||||
// instead of the passed-in template — this ensures errors inside partials
|
||||
// point at the correct file.
|
||||
format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false) -> string {
|
||||
if err == nil {
|
||||
return ""
|
||||
@@ -326,5 +104,22 @@ format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false)
|
||||
if path == "" {
|
||||
path = "<input>"
|
||||
}
|
||||
return format_error(path, source, b.pos, b.msg, hint = b.hint, colorize = colorize, span = b.span)
|
||||
|
||||
caret_start := b.pos
|
||||
caret_end := b.pos + 1
|
||||
if b.span > 0 {
|
||||
caret_end = b.pos + b.span
|
||||
} else {
|
||||
_, ts, te := context_extent(source, b.pos)
|
||||
caret_start = ts
|
||||
caret_end = te
|
||||
}
|
||||
|
||||
return diags.format_error(
|
||||
path, source, b.pos, b.msg,
|
||||
hint = b.hint,
|
||||
colorize = colorize,
|
||||
caret_start = caret_start,
|
||||
caret_end = caret_end,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user