internal/lsp: plumb suggested fixes through the LSP

Change-Id: Ia9e077e6b9cf8a817103d90481768ae99409c574
Reviewed-on: https://go-review.googlesource.com/c/tools/+/183264
Run-TryBot: Michael Matloob <matloob@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/go.mod b/go.mod
index 0984a83..f35bc2b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,4 +5,5 @@
 require (
 	golang.org/x/net v0.0.0-20190311183353-d8887717615a
 	golang.org/x/sync v0.0.0-20190423024810-112230192c58
+	golang.org/x/tools/gopls v0.1.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 4a6c301..eae6dd7 100644
--- a/go.sum
+++ b/go.sum
@@ -5,3 +5,6 @@
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190612231717-10539ce30318/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools/gopls v0.1.0 h1:e5o2xK2HU//kzIRypLBw6/8pXdWuYDd8pliYpnQuNw8=
+golang.org/x/tools/gopls v0.1.0/go.mod h1:p8Q0IUu6EEeGxqmoN/g6Et3gReLCGA7PtNRdyOxcWJE=
diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go
index ea37fd5..73d5e8a 100644
--- a/internal/lsp/cache/pkg.go
+++ b/internal/lsp/cache/pkg.go
@@ -37,7 +37,7 @@
 	analyses map[*analysis.Analyzer]*analysisEntry
 
 	diagMu      sync.Mutex
-	diagnostics []analysis.Diagnostic
+	diagnostics []source.Diagnostic
 }
 
 // packageID is a type that abstracts a package ID.
@@ -193,13 +193,13 @@
 	return nil
 }
 
-func (pkg *pkg) SetDiagnostics(diags []analysis.Diagnostic) {
+func (pkg *pkg) SetDiagnostics(diags []source.Diagnostic) {
 	pkg.diagMu.Lock()
 	defer pkg.diagMu.Unlock()
 	pkg.diagnostics = diags
 }
 
-func (pkg *pkg) GetDiagnostics() []analysis.Diagnostic {
+func (pkg *pkg) GetDiagnostics() []source.Diagnostic {
 	pkg.diagMu.Lock()
 	defer pkg.diagMu.Unlock()
 	return pkg.diagnostics
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index afff382..ea4607a 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -16,7 +16,7 @@
 func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
 	uri := span.NewURI(params.TextDocument.URI)
 	view := s.session.ViewOf(uri)
-	_, m, err := getSourceFile(ctx, view, uri)
+	gof, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -57,6 +57,25 @@
 				},
 			})
 		}
+		diags := gof.GetPackage(ctx).GetDiagnostics()
+		for _, diag := range diags {
+			pdiag, err := toProtocolDiagnostic(ctx, view, diag)
+			if err != nil {
+				return nil, err
+			}
+			for _, ca := range diag.SuggestedFixes {
+				codeActions = append(codeActions, protocol.CodeAction{
+					Title: ca.Title,
+					Kind:  protocol.QuickFix, // TODO(matloob): Be more accurate about these?
+					Edit: &protocol.WorkspaceEdit{
+						Changes: &map[string][]protocol.TextEdit{
+							string(spn.URI()): edits,
+						},
+					},
+					Diagnostics: []protocol.Diagnostic{pdiag},
+				})
+			}
+		}
 	}
 	return codeActions, nil
 }
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index faccbc6..c9bf489 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -71,27 +71,35 @@
 func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []source.Diagnostic) ([]protocol.Diagnostic, error) {
 	reports := []protocol.Diagnostic{}
 	for _, diag := range diagnostics {
-		_, m, err := getSourceFile(ctx, v, diag.Span.URI())
+		diagnostic, err := toProtocolDiagnostic(ctx, v, diag)
 		if err != nil {
 			return nil, err
 		}
-		var severity protocol.DiagnosticSeverity
-		switch diag.Severity {
-		case source.SeverityError:
-			severity = protocol.SeverityError
-		case source.SeverityWarning:
-			severity = protocol.SeverityWarning
-		}
-		rng, err := m.Range(diag.Span)
-		if err != nil {
-			return nil, err
-		}
-		reports = append(reports, protocol.Diagnostic{
-			Message:  strings.TrimSpace(diag.Message), // go list returns errors prefixed by newline
-			Range:    rng,
-			Severity: severity,
-			Source:   diag.Source,
-		})
+		reports = append(reports, diagnostic)
 	}
 	return reports, nil
 }
+
+func toProtocolDiagnostic(ctx context.Context, v source.View, diag source.Diagnostic) (protocol.Diagnostic, error) {
+	_, m, err := getSourceFile(ctx, v, diag.Span.URI())
+	if err != nil {
+		return protocol.Diagnostic{}, err
+	}
+	var severity protocol.DiagnosticSeverity
+	switch diag.Severity {
+	case source.SeverityError:
+		severity = protocol.SeverityError
+	case source.SeverityWarning:
+		severity = protocol.SeverityWarning
+	}
+	rng, err := m.Range(diag.Span)
+	if err != nil {
+		return protocol.Diagnostic{}, err
+	}
+	return protocol.Diagnostic{
+		Message:  strings.TrimSpace(diag.Message), // go list returns errors prefixed by newline
+		Range:    rng,
+		Severity: severity,
+		Source:   diag.Source,
+	}, nil
+}
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 86999d9..1137fb2 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -42,6 +42,13 @@
 	Message  string
 	Source   string
 	Severity DiagnosticSeverity
+
+	SuggestedFixes []SuggestedFixes
+}
+
+type SuggestedFixes struct {
+	Title string
+	Edits []TextEdit
 }
 
 type DiagnosticSeverity int
@@ -59,7 +66,7 @@
 	// Prepare the reports we will send for the files in this package.
 	reports := make(map[span.URI][]Diagnostic)
 	for _, filename := range pkg.GetFilenames() {
-		addReport(view, reports, span.FileURI(filename), nil)
+		clearReports(view, reports, span.FileURI(filename))
 	}
 
 	// Prepare any additional reports for the errors in this package.
@@ -67,7 +74,7 @@
 		if err.Kind != packages.ListError {
 			continue
 		}
-		addReport(view, reports, packagesErrorSpan(err).URI(), nil)
+		clearReports(view, reports, packagesErrorSpan(err).URI())
 	}
 
 	// Run diagnostics for the package that this URI belongs to.
@@ -85,7 +92,7 @@
 			continue
 		}
 		for _, filename := range pkg.GetFilenames() {
-			addReport(view, reports, span.FileURI(filename), nil)
+			clearReports(view, reports, span.FileURI(filename))
 		}
 		diagnostics(ctx, view, pkg, reports)
 	}
@@ -146,22 +153,11 @@
 func analyses(ctx context.Context, v View, pkg Package, disabledAnalyses map[string]struct{}, reports map[span.URI][]Diagnostic) error {
 	// Type checking and parsing succeeded. Run analyses.
 	if err := runAnalyses(ctx, v, pkg, disabledAnalyses, func(a *analysis.Analyzer, diag analysis.Diagnostic) error {
-		r := span.NewRange(v.Session().Cache().FileSet(), diag.Pos, diag.End)
-		s, err := r.Span()
+		diagnostic, err := toDiagnostic(a, v, diag)
 		if err != nil {
-			// The diagnostic has an invalid position, so we don't have a valid span.
 			return err
 		}
-		category := a.Name
-		if diag.Category != "" {
-			category += "." + category
-		}
-		addReport(v, reports, s.URI(), &Diagnostic{
-			Source:   category,
-			Span:     s,
-			Message:  diag.Message,
-			Severity: SeverityWarning,
-		})
+		addReport(v, reports, diagnostic.Span.URI(), diagnostic)
 		return nil
 	}); err != nil {
 		return err
@@ -169,15 +165,42 @@
 	return nil
 }
 
-func addReport(v View, reports map[span.URI][]Diagnostic, uri span.URI, diagnostic *Diagnostic) {
+func toDiagnostic(a *analysis.Analyzer, v View, diag analysis.Diagnostic) (Diagnostic, error) {
+	r := span.NewRange(v.Session().Cache().FileSet(), diag.Pos, diag.End)
+	s, err := r.Span()
+	if err != nil {
+		// The diagnostic has an invalid position, so we don't have a valid span.
+		return Diagnostic{}, err
+	}
+	category := a.Name
+	if diag.Category != "" {
+		category += "." + category
+	}
+	ca, err := getCodeActions(v.Session().Cache().FileSet(), diag)
+	if err != nil {
+		return Diagnostic{}, err
+	}
+	return Diagnostic{
+		Source:         category,
+		Span:           s,
+		Message:        diag.Message,
+		Severity:       SeverityWarning,
+		SuggestedFixes: ca,
+	}, nil
+}
+
+func clearReports(v View, reports map[span.URI][]Diagnostic, uri span.URI) {
 	if v.Ignore(uri) {
 		return
 	}
-	if diagnostic == nil {
-		reports[uri] = []Diagnostic{}
-	} else {
-		reports[uri] = append(reports[uri], *diagnostic)
+	reports[uri] = []Diagnostic{}
+}
+
+func addReport(v View, reports map[span.URI][]Diagnostic, uri span.URI, diagnostic Diagnostic) {
+	if v.Ignore(uri) {
+		return
 	}
+	reports[uri] = append(reports[uri], diagnostic)
 }
 
 func packagesErrorSpan(err packages.Error) span.Span {
@@ -294,6 +317,7 @@
 
 	// Report diagnostics and errors from root analyzers.
 	for _, r := range roots {
+		var sdiags []Diagnostic
 		for _, diag := range r.diagnostics {
 			if r.err != nil {
 				// TODO(matloob): This isn't quite right: we might return a failed prerequisites error,
@@ -303,8 +327,13 @@
 			if err := report(r.Analyzer, diag); err != nil {
 				return err
 			}
+			sdiag, err := toDiagnostic(r.Analyzer, v, diag)
+			if err != nil {
+				return err
+			}
+			sdiags = append(sdiags, sdiag)
 		}
-		pkg.SetDiagnostics(r.diagnostics)
+		pkg.SetDiagnostics(sdiags)
 	}
 	return nil
 }
diff --git a/internal/lsp/source/suggested_fix.go b/internal/lsp/source/suggested_fix.go
new file mode 100644
index 0000000..6d1f733
--- /dev/null
+++ b/internal/lsp/source/suggested_fix.go
@@ -0,0 +1,10 @@
+// +build !experimental
+
+package source
+
+import "go/token"
+import "golang.org/x/tools/go/analysis"
+
+func getCodeActions(fset *token.FileSet, diag analysis.Diagnostic) ([]SuggestedFixes, error) {
+	return nil, nil
+}
diff --git a/internal/lsp/source/suggested_fix_experimental.go b/internal/lsp/source/suggested_fix_experimental.go
new file mode 100644
index 0000000..b34f8d7
--- /dev/null
+++ b/internal/lsp/source/suggested_fix_experimental.go
@@ -0,0 +1,26 @@
+// +build experimental
+
+package source
+
+import (
+	"go/token"
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/internal/span"
+)
+
+func getCodeActions(fset *token.FileSet, diag analysis.Diagnostic) ([]CodeAction, error) {
+	var cas []CodeAction
+	for _, fix := range diag.SuggestedFixes {
+		var ca CodeAction
+		ca.Title = fix.Message
+		for _, te := range fix.TextEdits {
+			span, err := span.NewRange(fset, te.Pos, te.End).Span()
+			if err != nil {
+				return nil, err
+			}
+			ca.Edits = append(ca.Edits, TextEdit{span, string(te.NewText)})
+		}
+		cas = append(cas, ca)
+	}
+	return cas, nil
+}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 9b9a196..b2619e9 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -264,8 +264,8 @@
 	IsIllTyped() bool
 	GetActionGraph(ctx context.Context, a *analysis.Analyzer) (*Action, error)
 	GetImport(pkgPath string) Package
-	GetDiagnostics() []analysis.Diagnostic
-	SetDiagnostics(diags []analysis.Diagnostic)
+	GetDiagnostics() []Diagnostic
+	SetDiagnostics(diags []Diagnostic)
 }
 
 // TextEdit represents a change to a section of a document.