From 9650c66da3ff428b355c9b29c8a5ca02bd026f3d Mon Sep 17 00:00:00 2001 From: Rebecca Stambler Date: Fri, 19 Oct 2018 16:03:29 -0400 Subject: [PATCH] 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 Run-TryBot: Rebecca Stambler TryBot-Result: Gobot Gobot --- internal/lsp/diagnostics.go | 85 +++++++++++++++++++++++++++++++++++++ internal/lsp/format.go | 22 ++++++---- internal/lsp/server.go | 28 +++++++++--- internal/lsp/view.go | 50 +++++++++++++++++++--- 4 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 internal/lsp/diagnostics.go diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go new file mode 100644 index 00000000..1a38d734 --- /dev/null +++ b/internal/lsp/diagnostics.go @@ -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 + +} diff --git a/internal/lsp/format.go b/internal/lsp/format.go index 02e2f038..a19def2a 100644 --- a/internal/lsp/format.go +++ b/internal/lsp/format.go @@ -1,9 +1,9 @@ package lsp import ( + "bytes" "fmt" "go/format" - "strings" "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 { // Get the ending line and column numbers for the original file. - line := strings.Count(data, "\n") - col := len(data) - strings.LastIndex(data, "\n") - 1 + line := bytes.Count(data, []byte("\n")) + col := len(data) - bytes.LastIndex(data, []byte("\n")) - 1 if col < 0 { col = 0 } rng = &protocol.Range{ - Start: protocol.Position{0, 0}, - End: protocol.Position{float64(line), float64(col)}, + Start: protocol.Position{ + Line: 0, + Character: 0, + }, + End: protocol.Position{ + Line: float64(line), + Character: float64(col), + }, } } // 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 // 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 for i := 0; i < int(line); i++ { if start >= len(contents) { 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 { return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line) } - start += (index + 1) + start += index + 1 } offset := start + int(col) return offset, nil diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 3673ce3e..c2462707 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -95,7 +95,7 @@ func (s *server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) } 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 } @@ -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. 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 } +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 { return notImplemented("WillSave") } @@ -119,7 +136,8 @@ func (s *server) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocume } 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 { @@ -172,7 +190,7 @@ func (s *server) CodeAction(context.Context, *protocol.CodeActionParams) ([]prot } 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) { @@ -180,7 +198,7 @@ func (s *server) CodeLensResolve(context.Context, *protocol.CodeLens) (*protocol } 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) { diff --git a/internal/lsp/view.go b/internal/lsp/view.go index 1011ba98..825981fc 100644 --- a/internal/lsp/view.go +++ b/internal/lsp/view.go @@ -2,35 +2,47 @@ package lsp import ( "fmt" + "go/token" + "strings" "sync" + "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/lsp/protocol" ) type view struct { activeFilesMu sync.Mutex - activeFiles map[protocol.DocumentURI]string + activeFiles map[protocol.DocumentURI][]byte + + fset *token.FileSet } func newView() *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.activeFiles[uri] = text - v.activeFilesMu.Unlock() + defer 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() defer v.activeFilesMu.Unlock() content, ok := v.activeFiles[uri] if !ok { - return "", fmt.Errorf("file not found: %s", uri) + return nil, fmt.Errorf("file not found: %s", uri) } return content, nil } @@ -40,3 +52,27 @@ func (v *view) clearActiveFile(uri protocol.DocumentURI) { delete(v.activeFiles, uri) 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) +}