internal/lsp: move diagnostics logic to source directory

Change-Id: I6bea7a76501e852bbf381eb5dbc79217e1ad10ac
Reviewed-on: https://go-review.googlesource.com/c/148889
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 06f39b7..39694ea 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 ee1377e..62716cd 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -25,7 +25,8 @@
 }
 
 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 @@
 					},
 				},
 				Severity: protocol.SeverityError,
-				Source:   "LSP: Go compiler",
+				Source:   "LSP",
 				Message:  msg,
 			}
 			if _, ok := expectedDiagnostics[pos.Filename]; ok {
@@ -153,11 +154,12 @@
 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 f7cf697..4c4270f 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -49,10 +49,8 @@
 			},
 			DocumentFormattingProvider:      true,
 			DocumentRangeFormattingProvider: true,
-			CompletionProvider: protocol.CompletionOptions{
-				TriggerCharacters: []string{"."},
-			},
-			DefinitionProvider: true,
+			CompletionProvider:              protocol.CompletionOptions{},
+			DefinitionProvider:              true,
 		},
 	}, nil
 }
@@ -119,14 +117,16 @@
 	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 0000000..d1ecedb
--- /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
+		}
+	}
+}