diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 06f39b738b..39694eaeb6 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -5,94 +5,34 @@ package lsp import ( - "fmt" - "go/token" - "strconv" - "strings" - - "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" ) -func diagnostics(v *source.View, uri source.URI) (map[string][]protocol.Diagnostic, error) { - pkg, err := v.GetFile(uri).GetPackage() - if err != nil { - return nil, err +func toProtocolDiagnostics(v *source.View, diagnostics []source.Diagnostic) []protocol.Diagnostic { + reports := []protocol.Diagnostic{} + for _, diag := range diagnostics { + tok := v.Config.Fset.File(diag.Range.Start) + reports = append(reports, protocol.Diagnostic{ + Message: diag.Message, + Range: toProtocolRange(tok, diag.Range), + Severity: toProtocolSeverity(diag.Severity), + Source: "LSP", + }) } - if pkg == nil { - return nil, fmt.Errorf("package for %v not found", uri) - } - 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 + return reports } -func parseErrorPos(pkgErr packages.Error) (pos token.Position) { - remainder1, first, hasLine := chop(pkgErr.Pos) - remainder2, second, hasColumn := chop(remainder1) - if hasLine && hasColumn { - pos.Filename = remainder2 - pos.Line = second - pos.Column = first - } else if hasLine { - pos.Filename = remainder1 - pos.Line = first +func toProtocolSeverity(severity source.DiagnosticSeverity) protocol.DiagnosticSeverity { + switch severity { + case source.SeverityError: + return protocol.SeverityError + case source.SeverityWarning: + return protocol.SeverityWarning + case source.SeverityHint: + return protocol.SeverityHint + case source.SeverityInformation: + return protocol.SeverityInformation } - return pos -} - -func chop(text string) (remainder string, value int, ok bool) { - i := strings.LastIndex(text, ":") - if i < 0 { - return text, 0, false - } - v, err := strconv.ParseInt(text[i+1:], 10, 64) - if err != nil { - return text, 0, false - } - return text[:i], int(v), true + return protocol.SeverityError // default } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index ee1377e30c..62716cd015 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -25,7 +25,8 @@ func TestLSP(t *testing.T) { } func testLSP(t *testing.T, exporter packagestest.Exporter) { - dir := "testdata" + const dir = "testdata" + files := packagestest.MustCopyFileTree(dir) subdirs, err := ioutil.ReadDir(dir) if err != nil { @@ -95,7 +96,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { }, }, Severity: protocol.SeverityError, - Source: "LSP: Go compiler", + Source: "LSP", Message: msg, } if _, ok := expectedDiagnostics[pos.Filename]; ok { @@ -153,11 +154,12 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { func testDiagnostics(t *testing.T, v *source.View, pkgs []*packages.Package, wants map[string][]protocol.Diagnostic) { for _, pkg := range pkgs { for _, filename := range pkg.GoFiles { - diagnostics, err := diagnostics(v, source.ToURI(filename)) + f := v.GetFile(source.ToURI(filename)) + diagnostics, err := source.Diagnostics(context.Background(), v, f) if err != nil { t.Fatal(err) } - got := diagnostics[filename] + got := toProtocolDiagnostics(v, diagnostics[filename]) sort.Slice(got, func(i int, j int) bool { return got[i].Range.Start.Line < got[j].Range.Start.Line }) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index f7cf697498..4c4270fba3 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -49,10 +49,8 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara }, DocumentFormattingProvider: true, DocumentRangeFormattingProvider: true, - CompletionProvider: protocol.CompletionOptions{ - TriggerCharacters: []string{"."}, - }, - DefinitionProvider: true, + CompletionProvider: protocol.CompletionOptions{}, + DefinitionProvider: true, }, }, nil } @@ -119,14 +117,16 @@ func (s *server) cacheAndDiagnoseFile(ctx context.Context, uri protocol.Document f := s.view.GetFile(source.URI(uri)) f.SetContent([]byte(text)) go func() { - reports, err := diagnostics(s.view, f.URI) - if err == nil { - for filename, diagnostics := range reports { - s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ - URI: protocol.DocumentURI(source.ToURI(filename)), - Diagnostics: diagnostics, - }) - } + f := s.view.GetFile(source.URI(uri)) + reports, err := source.Diagnostics(ctx, s.view, f) + if err != nil { + return // handle error? + } + for filename, diagnostics := range reports { + s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ + URI: protocol.DocumentURI(source.ToURI(filename)), + Diagnostics: toProtocolDiagnostics(s.view, diagnostics), + }) } }() } diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go new file mode 100644 index 0000000000..d1ecedb18b --- /dev/null +++ b/internal/lsp/source/diagnostics.go @@ -0,0 +1,148 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "go/token" + "strconv" + "strings" + + "golang.org/x/tools/go/packages" +) + +type Diagnostic struct { + Range Range + Severity DiagnosticSeverity + Message string +} + +type DiagnosticSeverity int + +const ( + SeverityError DiagnosticSeverity = iota + SeverityWarning + SeverityHint + SeverityInformation +) + +func Diagnostics(ctx context.Context, v *View, f *File) (map[string][]Diagnostic, error) { + pkg, err := f.GetPackage() + if err != nil { + return nil, err + } + // Prepare the reports we will send for this package. + reports := make(map[string][]Diagnostic) + for _, filename := range pkg.GoFiles { + reports[filename] = []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. + diags := typeErrors + if len(parseErrors) > 0 { + diags = parseErrors + } + for _, diag := range diags { + filename, start := v.errorPos(diag) + // TODO(rstambler): Add support for diagnostic ranges. + end := start + diagnostic := Diagnostic{ + Range: Range{ + Start: start, + End: end, + }, + Message: diag.Msg, + Severity: SeverityError, + } + if _, ok := reports[filename]; ok { + reports[filename] = append(reports[filename], diagnostic) + } + } + return reports, nil +} + +func (v *View) errorPos(pkgErr packages.Error) (string, token.Pos) { + remainder1, first, hasLine := chop(pkgErr.Pos) + remainder2, second, hasColumn := chop(remainder1) + var pos token.Position + if hasLine && hasColumn { + pos.Filename = remainder2 + pos.Line = second + pos.Column = first + } else if hasLine { + pos.Filename = remainder1 + pos.Line = first + } + f := v.GetFile(ToURI(pos.Filename)) + if f == nil { + return "", token.NoPos + } + tok, err := f.GetToken() + if err != nil { + return "", token.NoPos + } + return pos.Filename, fromTokenPosition(tok, pos) +} + +func chop(text string) (remainder string, value int, ok bool) { + i := strings.LastIndex(text, ":") + if i < 0 { + return text, 0, false + } + v, err := strconv.ParseInt(text[i+1:], 10, 64) + if err != nil { + return text, 0, false + } + return text[:i], int(v), true +} + +// fromTokenPosition converts a token.Position (1-based line and column +// number) to a token.Pos (byte offset value). +// It requires the token file the pos belongs to in order to do this. +func fromTokenPosition(f *token.File, pos token.Position) token.Pos { + line := lineStart(f, pos.Line) + return line + token.Pos(pos.Column-1) // TODO: this is wrong, bytes not characters +} + +// this functionality was borrowed from the analysisutil package +func lineStart(f *token.File, line int) token.Pos { + // Use binary search to find the start offset of this line. + // + // TODO(adonovan): eventually replace this function with the + // simpler and more efficient (*go/token.File).LineStart, added + // in go1.12. + + min := 0 // inclusive + max := f.Size() // exclusive + for { + offset := (min + max) / 2 + pos := f.Pos(offset) + posn := f.Position(pos) + if posn.Line == line { + return pos - (token.Pos(posn.Column) - 1) + } + + if min+1 >= max { + return token.NoPos + } + + if posn.Line < line { + min = offset + } else { + max = offset + } + } +}