internal/lsp: add support for publishing diagnostics
Any time a file is changed, we compute diagnostics for its package and return them to the client. No caching is implemented yet, so we parse and type-check the package each time. Change-Id: I7fb2f1d8975e7ce092938d903599188cc2132512 Reviewed-on: https://go-review.googlesource.com/c/143497 Reviewed-by: Alan Donovan <adonovan@google.com> Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
parent
6fe81c0879
commit
9650c66da3
|
@ -0,0 +1,85 @@
|
||||||
|
package lsp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/token"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
|
"golang.org/x/tools/internal/lsp/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *view) diagnostics(uri protocol.DocumentURI) (map[string][]protocol.Diagnostic, error) {
|
||||||
|
pkg, err := v.typeCheck(uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reports := make(map[string][]protocol.Diagnostic)
|
||||||
|
for _, filename := range pkg.GoFiles {
|
||||||
|
reports[filename] = []protocol.Diagnostic{}
|
||||||
|
}
|
||||||
|
var parseErrors, typeErrors []packages.Error
|
||||||
|
for _, err := range pkg.Errors {
|
||||||
|
switch err.Kind {
|
||||||
|
case packages.ParseError:
|
||||||
|
parseErrors = append(parseErrors, err)
|
||||||
|
case packages.TypeError:
|
||||||
|
typeErrors = append(typeErrors, err)
|
||||||
|
default:
|
||||||
|
// ignore other types of errors
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Don't report type errors if there are parse errors.
|
||||||
|
errors := typeErrors
|
||||||
|
if len(parseErrors) > 0 {
|
||||||
|
errors = parseErrors
|
||||||
|
}
|
||||||
|
for _, err := range errors {
|
||||||
|
pos := parseErrorPos(err)
|
||||||
|
line := float64(pos.Line) - 1
|
||||||
|
col := float64(pos.Column) - 1
|
||||||
|
diagnostic := protocol.Diagnostic{
|
||||||
|
// TODO(rstambler): Add support for diagnostic ranges.
|
||||||
|
Range: protocol.Range{
|
||||||
|
Start: protocol.Position{
|
||||||
|
Line: line,
|
||||||
|
Character: col,
|
||||||
|
},
|
||||||
|
End: protocol.Position{
|
||||||
|
Line: line,
|
||||||
|
Character: col,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Severity: protocol.SeverityError,
|
||||||
|
Source: "LSP: Go compiler",
|
||||||
|
Message: err.Msg,
|
||||||
|
}
|
||||||
|
if _, ok := reports[pos.Filename]; ok {
|
||||||
|
reports[pos.Filename] = append(reports[pos.Filename], diagnostic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseErrorPos(pkgErr packages.Error) (pos token.Position) {
|
||||||
|
split := strings.Split(pkgErr.Pos, ":")
|
||||||
|
if len(split) <= 1 {
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
pos.Filename = split[0]
|
||||||
|
line, err := strconv.ParseInt(split[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
pos.Line = int(line)
|
||||||
|
if len(split) == 3 {
|
||||||
|
col, err := strconv.ParseInt(split[2], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
pos.Column = int(col)
|
||||||
|
}
|
||||||
|
return pos
|
||||||
|
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/tools/internal/lsp/protocol"
|
"golang.org/x/tools/internal/lsp/protocol"
|
||||||
)
|
)
|
||||||
|
@ -39,14 +39,20 @@ func (s *server) format(uri protocol.DocumentURI, rng *protocol.Range) ([]protoc
|
||||||
}
|
}
|
||||||
if rng == nil {
|
if rng == nil {
|
||||||
// Get the ending line and column numbers for the original file.
|
// Get the ending line and column numbers for the original file.
|
||||||
line := strings.Count(data, "\n")
|
line := bytes.Count(data, []byte("\n"))
|
||||||
col := len(data) - strings.LastIndex(data, "\n") - 1
|
col := len(data) - bytes.LastIndex(data, []byte("\n")) - 1
|
||||||
if col < 0 {
|
if col < 0 {
|
||||||
col = 0
|
col = 0
|
||||||
}
|
}
|
||||||
rng = &protocol.Range{
|
rng = &protocol.Range{
|
||||||
Start: protocol.Position{0, 0},
|
Start: protocol.Position{
|
||||||
End: protocol.Position{float64(line), float64(col)},
|
Line: 0,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: protocol.Position{
|
||||||
|
Line: float64(line),
|
||||||
|
Character: float64(col),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO(rstambler): Compute text edits instead of replacing whole file.
|
// TODO(rstambler): Compute text edits instead of replacing whole file.
|
||||||
|
@ -60,17 +66,17 @@ func (s *server) format(uri protocol.DocumentURI, rng *protocol.Range) ([]protoc
|
||||||
|
|
||||||
// positionToOffset converts a 0-based line and column number in a file
|
// positionToOffset converts a 0-based line and column number in a file
|
||||||
// to a byte offset value.
|
// to a byte offset value.
|
||||||
func positionToOffset(contents string, line, col int) (int, error) {
|
func positionToOffset(contents []byte, line, col int) (int, error) {
|
||||||
start := 0
|
start := 0
|
||||||
for i := 0; i < int(line); i++ {
|
for i := 0; i < int(line); i++ {
|
||||||
if start >= len(contents) {
|
if start >= len(contents) {
|
||||||
return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line)
|
return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line)
|
||||||
}
|
}
|
||||||
index := strings.IndexByte(contents[start:], '\n')
|
index := bytes.IndexByte(contents[start:], '\n')
|
||||||
if index == -1 {
|
if index == -1 {
|
||||||
return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line)
|
return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line)
|
||||||
}
|
}
|
||||||
start += (index + 1)
|
start += index + 1
|
||||||
}
|
}
|
||||||
offset := start + int(col)
|
offset := start + int(col)
|
||||||
return offset, nil
|
return offset, nil
|
||||||
|
|
|
@ -95,7 +95,7 @@ func (s *server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
|
func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
|
||||||
s.cacheActiveFile(params.TextDocument.URI, params.TextDocument.Text)
|
s.cacheAndDiagnoseFile(ctx, params.TextDocument.URI, params.TextDocument.Text)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,11 +105,28 @@ func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDo
|
||||||
}
|
}
|
||||||
// We expect the full content of file, i.e. a single change with no range.
|
// We expect the full content of file, i.e. a single change with no range.
|
||||||
if change := params.ContentChanges[0]; change.RangeLength == 0 {
|
if change := params.ContentChanges[0]; change.RangeLength == 0 {
|
||||||
s.cacheActiveFile(params.TextDocument.URI, change.Text)
|
s.cacheAndDiagnoseFile(ctx, params.TextDocument.URI, change.Text)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *server) cacheAndDiagnoseFile(ctx context.Context, uri protocol.DocumentURI, text string) {
|
||||||
|
s.view.activeFilesMu.Lock()
|
||||||
|
s.view.activeFiles[uri] = []byte(text)
|
||||||
|
s.view.activeFilesMu.Unlock()
|
||||||
|
go func() {
|
||||||
|
reports, err := s.diagnostics(uri)
|
||||||
|
if err == nil {
|
||||||
|
for filename, diagnostics := range reports {
|
||||||
|
s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
|
||||||
|
URI: filenameToURI(filename),
|
||||||
|
Diagnostics: diagnostics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error {
|
func (s *server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error {
|
||||||
return notImplemented("WillSave")
|
return notImplemented("WillSave")
|
||||||
}
|
}
|
||||||
|
@ -119,7 +136,8 @@ func (s *server) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocume
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) error {
|
func (s *server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) error {
|
||||||
return notImplemented("DidSave")
|
// TODO(rstambler): Should we clear the cache here?
|
||||||
|
return nil // ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
|
func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
|
||||||
|
@ -172,7 +190,7 @@ func (s *server) CodeAction(context.Context, *protocol.CodeActionParams) ([]prot
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) CodeLens(context.Context, *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
|
func (s *server) CodeLens(context.Context, *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
|
||||||
return nil, notImplemented("CodeLens")
|
return nil, nil // ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) CodeLensResolve(context.Context, *protocol.CodeLens) (*protocol.CodeLens, error) {
|
func (s *server) CodeLensResolve(context.Context, *protocol.CodeLens) (*protocol.CodeLens, error) {
|
||||||
|
@ -180,7 +198,7 @@ func (s *server) CodeLensResolve(context.Context, *protocol.CodeLens) (*protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) DocumentLink(context.Context, *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
|
func (s *server) DocumentLink(context.Context, *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
|
||||||
return nil, notImplemented("DocumentLink")
|
return nil, nil // ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) DocumentLinkResolve(context.Context, *protocol.DocumentLink) (*protocol.DocumentLink, error) {
|
func (s *server) DocumentLinkResolve(context.Context, *protocol.DocumentLink) (*protocol.DocumentLink, error) {
|
||||||
|
|
|
@ -2,35 +2,47 @@ package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"go/token"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
"golang.org/x/tools/internal/lsp/protocol"
|
"golang.org/x/tools/internal/lsp/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
type view struct {
|
type view struct {
|
||||||
activeFilesMu sync.Mutex
|
activeFilesMu sync.Mutex
|
||||||
activeFiles map[protocol.DocumentURI]string
|
activeFiles map[protocol.DocumentURI][]byte
|
||||||
|
|
||||||
|
fset *token.FileSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func newView() *view {
|
func newView() *view {
|
||||||
return &view{
|
return &view{
|
||||||
activeFiles: make(map[protocol.DocumentURI]string),
|
activeFiles: make(map[protocol.DocumentURI][]byte),
|
||||||
|
fset: token.NewFileSet(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *view) cacheActiveFile(uri protocol.DocumentURI, text string) {
|
func (v *view) overlay() map[string][]byte {
|
||||||
|
over := make(map[string][]byte)
|
||||||
|
|
||||||
v.activeFilesMu.Lock()
|
v.activeFilesMu.Lock()
|
||||||
v.activeFiles[uri] = text
|
defer v.activeFilesMu.Unlock()
|
||||||
v.activeFilesMu.Unlock()
|
|
||||||
|
for uri, content := range v.activeFiles {
|
||||||
|
over[uriToFilename(uri)] = content
|
||||||
|
}
|
||||||
|
return over
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *view) readActiveFile(uri protocol.DocumentURI) (string, error) {
|
func (v *view) readActiveFile(uri protocol.DocumentURI) ([]byte, error) {
|
||||||
v.activeFilesMu.Lock()
|
v.activeFilesMu.Lock()
|
||||||
defer v.activeFilesMu.Unlock()
|
defer v.activeFilesMu.Unlock()
|
||||||
|
|
||||||
content, ok := v.activeFiles[uri]
|
content, ok := v.activeFiles[uri]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("file not found: %s", uri)
|
return nil, fmt.Errorf("file not found: %s", uri)
|
||||||
}
|
}
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
@ -40,3 +52,27 @@ func (v *view) clearActiveFile(uri protocol.DocumentURI) {
|
||||||
delete(v.activeFiles, uri)
|
delete(v.activeFiles, uri)
|
||||||
v.activeFilesMu.Unlock()
|
v.activeFilesMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// typeCheck type-checks the package for the given package path.
|
||||||
|
func (v *view) typeCheck(uri protocol.DocumentURI) (*packages.Package, error) {
|
||||||
|
cfg := &packages.Config{
|
||||||
|
Mode: packages.LoadSyntax,
|
||||||
|
Fset: v.fset,
|
||||||
|
Overlay: v.overlay(),
|
||||||
|
Tests: true,
|
||||||
|
}
|
||||||
|
pkgs, err := packages.Load(cfg, fmt.Sprintf("file=%s", uriToFilename(uri)))
|
||||||
|
if len(pkgs) == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pkg := pkgs[0]
|
||||||
|
return pkg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uriToFilename(uri protocol.DocumentURI) string {
|
||||||
|
return strings.TrimPrefix(string(uri), "file://")
|
||||||
|
}
|
||||||
|
|
||||||
|
func filenameToURI(filename string) protocol.DocumentURI {
|
||||||
|
return protocol.DocumentURI("file://" + filename)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue