internal/lsp: refactor code actions

This change makes codeActions look at the diagnostics provided before
responding. Specifically, before running "source.organizeImports", we
check if there is a diagnostic relating to imports.

Change-Id: I5268d5e8f144c4f2e085b2a861d0abfb7614323b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/170997
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/code_action.go b/internal/lsp/code_action.go
new file mode 100644
index 0000000..e95d167
--- /dev/null
+++ b/internal/lsp/code_action.go
@@ -0,0 +1,101 @@
+// 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 lsp
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+)
+
+func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	_, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	spn, err := m.RangeSpan(params.Range)
+	if err != nil {
+		return nil, err
+	}
+	var codeActions []protocol.CodeAction
+	// Determine what code actions we should take based on the diagnostics.
+	if findImportErrors(params.Context.Diagnostics) {
+		codeAction, err := organizeImports(ctx, view, spn)
+		if err != nil {
+			return nil, err
+		}
+		if codeAction != nil {
+			codeActions = append(codeActions, *codeAction)
+		}
+	}
+	return codeActions, nil
+}
+
+// findImports determines if a given diagnostic represents an error that could
+// be fixed by organizing imports.
+// TODO(rstambler): We need a better way to check this than string matching.
+func findImportErrors(diagnostics []protocol.Diagnostic) bool {
+	for _, diagnostic := range diagnostics {
+		// "undeclared name: X" may be an unresolved import.
+		if strings.HasPrefix(diagnostic.Message, "undeclared name: ") {
+			return true
+		}
+		// "could not import: X" may be an invalid import.
+		if strings.HasPrefix(diagnostic.Message, "could not import: ") {
+			return true
+		}
+		// "X imported but not used" is an unused import.
+		if strings.HasSuffix(diagnostic.Message, " imported but not used") {
+			return true
+		}
+	}
+	return false
+}
+
+func organizeImports(ctx context.Context, v source.View, s span.Span) (*protocol.CodeAction, error) {
+	f, m, err := newColumnMap(ctx, v, s.URI())
+	if err != nil {
+		return nil, err
+	}
+	rng, err := s.Range(m.Converter)
+	if err != nil {
+		return nil, err
+	}
+	if rng.Start == rng.End {
+		// If we have a single point, assume we want the whole file.
+		tok := f.GetToken(ctx)
+		if tok == nil {
+			return nil, fmt.Errorf("no file information for %s", f.URI())
+		}
+		rng.End = tok.Pos(tok.Size())
+	}
+	edits, err := source.Imports(ctx, f, rng)
+	if err != nil {
+		return nil, err
+	}
+	protocolEdits, err := toProtocolEdits(m, edits)
+	if err != nil {
+		return nil, err
+	}
+	if len(protocolEdits) == 0 {
+		return nil, nil
+	}
+	codeAction := protocol.CodeAction{
+		Title: "Organize Imports",
+		Kind:  protocol.SourceOrganizeImports,
+		Edit: &protocol.WorkspaceEdit{
+			Changes: &map[string][]protocol.TextEdit{
+				string(s.URI()): protocolEdits,
+			},
+		},
+	}
+	return &codeAction, nil
+}
diff --git a/internal/lsp/imports.go b/internal/lsp/imports.go
deleted file mode 100644
index 3d634b2..0000000
--- a/internal/lsp/imports.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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 lsp
-
-import (
-	"context"
-	"fmt"
-
-	"golang.org/x/tools/internal/lsp/protocol"
-	"golang.org/x/tools/internal/lsp/source"
-	"golang.org/x/tools/internal/span"
-)
-
-func organizeImports(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) {
-	f, m, err := newColumnMap(ctx, v, s.URI())
-	if err != nil {
-		return nil, err
-	}
-	rng, err := s.Range(m.Converter)
-	if err != nil {
-		return nil, err
-	}
-	if rng.Start == rng.End {
-		// If we have a single point, assume we want the whole file.
-		tok := f.GetToken(ctx)
-		if tok == nil {
-			return nil, fmt.Errorf("no file information for %s", f.URI())
-		}
-		rng.End = tok.Pos(tok.Size())
-	}
-	edits, err := source.Imports(ctx, f, rng)
-	if err != nil {
-		return nil, err
-	}
-	return toProtocolEdits(m, edits)
-}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 851b94a..4e32b16 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -530,31 +530,7 @@
 }
 
 func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	_, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.RangeSpan(params.Range)
-	if err != nil {
-		return nil, err
-	}
-	edits, err := organizeImports(ctx, view, spn)
-	if err != nil {
-		return nil, err
-	}
-	return []protocol.CodeAction{
-		{
-			Title: "Organize Imports",
-			Kind:  protocol.SourceOrganizeImports,
-			Edit: &protocol.WorkspaceEdit{
-				Changes: &map[string][]protocol.TextEdit{
-					params.TextDocument.URI: edits,
-				},
-			},
-		},
-	}, nil
+	return s.codeAction(ctx, params)
 }
 
 func (s *Server) CodeLens(context.Context, *protocol.CodeLensParams) ([]protocol.CodeLens, error) {