Mechanism for introspection of source code for editors

The new "Nav" member on a zcl.File is an opaque object that can be
populated by parsers with an object that supports certain interfaces
that are not part of the main API but are useful for integration with
editors and other tooling.

As a first example of this, we replace the hack for getting context in
the diagnostic package with a new ContextString interface, which can
then be optionally implemented by a given parser to return a contextual
string native to the source language.
This commit is contained in:
Martin Atkins 2017-05-19 18:56:39 -07:00
parent 1168f36be5
commit a940c30903
6 changed files with 86 additions and 26 deletions

View File

@ -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 ""
}

View File

@ -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"
}

View File

@ -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.

4
zcled/doc.go Normal file
View File

@ -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

20
zcled/navigation.go Normal file
View File

@ -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 ""
}

View File

@ -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
}