package diag
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/grafana/agent/pkg/river/token"
)
const tabWidth = 4
type PrinterConfig struct {
Color bool
ContextLinesBefore, ContextLinesAfter int
}
type Printer struct {
cfg PrinterConfig
}
func NewPrinter(cfg PrinterConfig) *Printer {
return &Printer{cfg: cfg}
}
func Fprint(w io.Writer, files map[string][]byte, diags Diagnostics) error {
p := NewPrinter(PrinterConfig{
Color: false,
ContextLinesBefore: 1,
ContextLinesAfter: 1,
})
return p.Fprint(w, files, diags)
}
func (p *Printer) Fprint(w io.Writer, files map[string][]byte, diags Diagnostics) error {
bw := bufio.NewWriter(w)
for i, diag := range diags {
p.printDiagnosticHeader(bw, diag)
if !diag.EndPos.Valid() {
diag.EndPos = diag.StartPos
}
fileContents, foundFile := files[diag.StartPos.Filename]
if foundFile && diag.StartPos.Filename == diag.EndPos.Filename {
p.printRange(bw, fileContents, diag)
}
if i+1 < len(diags) {
fmt.Fprintf(bw, "\n")
}
}
return bw.Flush()
}
func (p *Printer) printDiagnosticHeader(w io.Writer, diag Diagnostic) {
if p.cfg.Color {
switch diag.Severity {
case SeverityLevelError:
cw := color.New(color.FgRed, color.Bold)
_, _ = cw.Fprintf(w, "Error: ")
case SeverityLevelWarn:
cw := color.New(color.FgYellow, color.Bold)
_, _ = cw.Fprintf(w, "Warning: ")
}
cw := color.New(color.Bold)
_, _ = cw.Fprintf(w, "%s: %s\n", diag.StartPos, diag.Message)
return
}
switch diag.Severity {
case SeverityLevelError:
_, _ = fmt.Fprintf(w, "Error: ")
case SeverityLevelWarn:
_, _ = fmt.Fprintf(w, "Warning: ")
}
fmt.Fprintf(w, "%s: %s\n", diag.StartPos, diag.Message)
}
func (p *Printer) printRange(w io.Writer, file []byte, diag Diagnostic) {
var (
start = diag.StartPos
end = diag.EndPos
)
fmt.Fprintf(w, "\n")
var (
lines = strings.Split(string(file), "\n")
startLine = max(start.Line-p.cfg.ContextLinesBefore, 1)
endLine = min(end.Line+p.cfg.ContextLinesAfter, len(lines))
multiline = end.Line-start.Line > 0
)
prefixWidth := len(strconv.Itoa(endLine))
for lineNum := startLine; lineNum <= endLine; lineNum++ {
line := lines[lineNum-1]
printPaddedNumber(w, prefixWidth, lineNum)
fmt.Fprintf(w, " | ")
if multiline {
if inRange(lineNum, 0, start, end) {
fmt.Fprint(w, "| ")
} else {
fmt.Fprint(w, " ")
}
}
for _, ch := range line {
if ch == '\r' {
continue
}
if ch == '\t' || ch == '\v' {
printCh(w, tabWidth, ' ')
continue
}
fmt.Fprintf(w, "%c", ch)
}
fmt.Fprintf(w, "\n")
if lineNum == start.Line || (multiline && lineNum == end.Line) {
printCh(w, prefixWidth, ' ')
switch {
case multiline && lineNum == start.Line:
fmt.Fprintf(w, " | _")
case multiline && lineNum == end.Line:
fmt.Fprintf(w, " | |_")
default:
fmt.Fprintf(w, " | ")
}
p.printFocus(w, line, lineNum, diag)
fmt.Fprintf(w, "\n")
}
}
}
func (p *Printer) printFocus(w io.Writer, data string, line int, diag Diagnostic) {
for i, ch := range data {
column := i + 1
if line == diag.EndPos.Line && column > diag.EndPos.Column {
break
}
blank := byte(' ')
if diag.EndPos.Line-diag.StartPos.Line > 0 {
blank = byte('_')
}
switch {
case ch == '\t' || ch == '\v':
printCh(w, tabWidth, blank)
case inRange(line, column, diag.StartPos, diag.EndPos):
fmt.Fprintf(w, "%c", '^')
default:
fmt.Fprintf(w, "%c", blank)
}
}
}
func inRange(line, col int, start, end token.Position) bool {
if line < start.Line || line > end.Line {
return false
}
switch line {
case start.Line:
return col >= start.Column
case end.Line:
return col <= end.Column
default:
return true
}
}
func printPaddedNumber(w io.Writer, width int, num int) {
numStr := strconv.Itoa(num)
for i := 0; i < width-len(numStr); i++ {
_, _ = w.Write([]byte{' '})
}
_, _ = w.Write([]byte(numStr))
}
func printCh(w io.Writer, count int, ch byte) {
for i := 0; i < count; i++ {
_, _ = w.Write([]byte{ch})
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}