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>
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
new file mode 100644
index 0000000..1a38d73
--- /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 02e2f03..a19def2 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 @@
 	}
 	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 @@
 
 // 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 3673ce3..c246270 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -95,7 +95,7 @@
 }
 
 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 @@
 	}
 	// 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) 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) 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) 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 1011ba9..825981f 100644
--- a/internal/lsp/view.go
+++ b/internal/lsp/view.go
@@ -2,35 +2,47 @@
 
 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 @@
 	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)
+}