diff --git a/zcl/diagnostic_text.go b/zcl/diagnostic_text.go index 1bdd39d..7a455b7 100644 --- a/zcl/diagnostic_text.go +++ b/zcl/diagnostic_text.go @@ -6,16 +6,15 @@ import ( "errors" "fmt" "io" - "strings" wordwrap "github.com/mitchellh/go-wordwrap" ) type diagnosticTextWriter struct { - sources map[string][]byte - wr io.Writer - width uint - color bool + files map[string]*File + wr io.Writer + width uint + color bool } // NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics @@ -30,12 +29,12 @@ type diagnosticTextWriter struct { // If color is set to true, the output will include VT100 escape sequences to // color-code the severity indicators. It is suggested to turn this off if // the target writer is not a terminal. -func NewDiagnosticTextWriter(wr io.Writer, sources map[string][]byte, width uint, color bool) DiagnosticWriter { +func NewDiagnosticTextWriter(wr io.Writer, files map[string]*File, width uint, color bool) DiagnosticWriter { return &diagnosticTextWriter{ - sources: sources, - wr: wr, - width: width, - color: color, + files: files, + wr: wr, + width: width, + color: color, } } @@ -70,14 +69,14 @@ func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error { if diag.Subject != nil { - src := w.sources[diag.Subject.Filename] - if src == nil { + file := w.files[diag.Subject.Filename] + if file == nil || file.Bytes == nil { fmt.Fprintf(w.wr, " on %s line %d:\n (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line) } else { + src := file.Bytes r := bytes.NewReader(src) sc := bufio.NewScanner(r) sc.Split(bufio.ScanLines) - contextLine := "" var startLine, endLine int if diag.Context != nil { @@ -88,17 +87,18 @@ func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error { endLine = diag.Subject.End.Line } + var contextLine string + if diag.Subject != nil { + contextLine = contextString(file, diag.Subject.Start.Byte) + if contextLine != "" { + contextLine = ", in " + contextLine + } + } + li := 1 var ls string for sc.Scan() { ls = sc.Text() - if len(ls) > 0 && strings.Contains(ls, "{") && ls[0] != ' ' && ls[0] != '\t' && ls[0] != '#' && ls[0] != '/' && ls[0] != '{' { - // Keep track of the latest non-space-prefixed line we've - // seen, to use as context. This is a pretty sloppy way - // to do this, but it works well enough for most normal - // files. (In particular though, it doesn't work for JSON sources.) - contextLine = fmt.Sprintf(", in %s", strings.Replace(ls, " {", "", 1)) - } if li == startLine { break @@ -150,3 +150,14 @@ func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error { } return nil } + +func contextString(file *File, offset int) string { + type contextStringer interface { + ContextString(offset int) string + } + + if cser, ok := file.Nav.(contextStringer); ok { + return cser.ContextString(offset) + } + return "" +} diff --git a/zcl/diagnostic_text_test.go b/zcl/diagnostic_text_test.go index 6930bae..9d51731 100644 --- a/zcl/diagnostic_text_test.go +++ b/zcl/diagnostic_text_test.go @@ -31,7 +31,7 @@ func TestDiagnosticTextWriter(t *testing.T) { }, `Error: Splines not reticulated - on line 1: + on line 1, in hardcoded-context: 1: foo = 1 All splines must be pre-reticulated. @@ -56,7 +56,7 @@ All splines must be pre-reticulated. }, `Error: Unsupported attribute - on line 3: + on line 3, in hardcoded-context: 3: baz = 3 "baz" is not a supported top-level @@ -94,7 +94,7 @@ attribute. Did you mean "bam"? }, `Error: Unsupported attribute - on line 5, in block "party": + on line 5, in hardcoded-context: 4: block "party" { 5: pizza = "cheese" 6: } @@ -106,14 +106,17 @@ Did you mean "pizzetta"? }, } - sources := map[string][]byte{ - "": []byte(testDiagnosticTextWriterSource), + files := map[string]*File{ + "": &File{ + Bytes: []byte(testDiagnosticTextWriterSource), + Nav: &diagnosticTestNav{}, + }, } for i, test := range tests { t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { bwr := &bytes.Buffer{} - dwr := NewDiagnosticTextWriter(bwr, sources, 40, false) + dwr := NewDiagnosticTextWriter(bwr, files, 40, false) err := dwr.WriteDiagnostic(test.Input) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -133,3 +136,10 @@ block "party" { pizza = "cheese" } ` + +type diagnosticTestNav struct { +} + +func (tn *diagnosticTestNav) ContextString(offset int) string { + return "hardcoded-context" +} diff --git a/zcl/structure.go b/zcl/structure.go index 1df01b0..d567052 100644 --- a/zcl/structure.go +++ b/zcl/structure.go @@ -8,6 +8,11 @@ import ( type File struct { Body Body Bytes []byte + + // Nav is used to integrate with the "zcled" editor integration package, + // and with diagnostic information formatters. It is not for direct use + // by a calling application. + Nav interface{} } // Block represents a nested block within a Body. diff --git a/zcled/doc.go b/zcled/doc.go new file mode 100644 index 0000000..9e77fd1 --- /dev/null +++ b/zcled/doc.go @@ -0,0 +1,4 @@ +// Package zcled provides functionality intended to help an application +// that embeds zcl to deliver relevant information to a text editor or IDE +// for navigating around and analyzing configuration files. +package zcled diff --git a/zcled/navigation.go b/zcled/navigation.go new file mode 100644 index 0000000..7190494 --- /dev/null +++ b/zcled/navigation.go @@ -0,0 +1,20 @@ +package zcled + +import ( + "github.com/apparentlymart/go-zcl/zcl" +) + +type contextStringer interface { + ContextString(offset int) string +} + +// ContextString returns a string describing the context of the given byte +// offset, if available. An empty string is returned if no such information +// is available, or otherwise the returned string is in a form that depends +// on the language used to write the referenced file. +func ContextString(file *zcl.File, offset int) string { + if cser, ok := file.Nav.(contextStringer); ok { + return cser.ContextString(offset) + } + return "" +} diff --git a/zclparse/parser.go b/zclparse/parser.go index 06c1234..360f921 100644 --- a/zclparse/parser.go +++ b/zclparse/parser.go @@ -65,3 +65,13 @@ func (p *Parser) Sources() map[string][]byte { } return ret } + +// Files returns a map from filenames to the File objects produced from them. +// This is intended to be used, for example, to print diagnostics with +// contextual information. +// +// The returned map and all of the objects it refers to directly or indirectly +// must not be modified. +func (p *Parser) Files() map[string]*zcl.File { + return p.files +}