internal/lsp: support running `go mod tidy` as a code action

This changes adds basic support for running `go mod tidy` as a code
action when a user opens a go.mod file. When we have a command
available like `go mod tidy -check`, we will be able to return edits as
part of the codeAction. For now, we execute the command directly.

This change also required a few modifications to our handling of file
kinds so that we could distinguish between a Go file and a go.mod file.

Change-Id: I343079b8886724b67f90a314e45639545a34f21e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/196322
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/cache/builtin.go b/internal/lsp/cache/builtin.go
index a313290..990acef 100644
--- a/internal/lsp/cache/builtin.go
+++ b/internal/lsp/cache/builtin.go
@@ -40,7 +40,7 @@
 	pkg := pkgs[0]
 	files := make(map[string]*ast.File)
 	for _, filename := range pkg.GoFiles {
-		fh := view.session.GetFile(span.FileURI(filename))
+		fh := view.session.GetFile(span.FileURI(filename), source.Go)
 		ph := view.session.cache.ParseGoHandle(fh, source.ParseFull)
 		view.builtin.files = append(view.builtin.files, ph)
 		file, _, _, err := ph.Parse(ctx)
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index b20712e..764d077 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -54,8 +54,8 @@
 	err   error
 }
 
-func (c *cache) GetFile(uri span.URI) source.FileHandle {
-	underlying := c.fs.GetFile(uri)
+func (c *cache) GetFile(uri span.URI, kind source.FileKind) source.FileHandle {
+	underlying := c.fs.GetFile(uri, kind)
 	key := fileKey{
 		identity: underlying.Identity(),
 	}
@@ -96,10 +96,6 @@
 	return h.underlying.Identity()
 }
 
-func (h *fileHandle) Kind() source.FileKind {
-	return h.underlying.Kind()
-}
-
 func (h *fileHandle) Read(ctx context.Context) ([]byte, string, error) {
 	v := h.handle.Get(ctx)
 	if v == nil {
diff --git a/internal/lsp/cache/external.go b/internal/lsp/cache/external.go
index 0b84b24..315a131 100644
--- a/internal/lsp/cache/external.go
+++ b/internal/lsp/cache/external.go
@@ -27,7 +27,7 @@
 	identity source.FileIdentity
 }
 
-func (fs *nativeFileSystem) GetFile(uri span.URI) source.FileHandle {
+func (fs *nativeFileSystem) GetFile(uri span.URI, kind source.FileKind) source.FileHandle {
 	version := "DOES NOT EXIST"
 	if fi, err := os.Stat(uri.Filename()); err == nil {
 		version = fi.ModTime().String()
@@ -37,6 +37,7 @@
 		identity: source.FileIdentity{
 			URI:     uri,
 			Version: version,
+			Kind:    kind,
 		},
 	}
 }
@@ -49,17 +50,12 @@
 	return h.identity
 }
 
-func (h *nativeFileHandle) Kind() source.FileKind {
-	// TODO: How should we determine the file kind?
-	return source.Go
-}
-
 func (h *nativeFileHandle) Read(ctx context.Context) ([]byte, string, error) {
 	ctx, done := trace.StartSpan(ctx, "cache.nativeFileHandle.Read", telemetry.File.Of(h.identity.URI.Filename()))
 	defer done()
 	ioLimit <- struct{}{}
 	defer func() { <-ioLimit }()
-	//TODO: this should fail if the version is not the same as the handle
+	// TODO: this should fail if the version is not the same as the handle
 	data, err := ioutil.ReadFile(h.identity.URI.Filename())
 	if err != nil {
 		return nil, "", err
diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go
index 19bf46b..a9748d5 100644
--- a/internal/lsp/cache/file.go
+++ b/internal/lsp/cache/file.go
@@ -59,7 +59,7 @@
 	defer f.handleMu.Unlock()
 
 	if f.handle == nil {
-		f.handle = f.view.Session().GetFile(f.URI())
+		f.handle = f.view.session.GetFile(f.URI(), f.kind)
 	}
 	return f.handle
 }
diff --git a/internal/lsp/cache/gofile.go b/internal/lsp/cache/gofile.go
index 7bf6fd5..6c55ef7 100644
--- a/internal/lsp/cache/gofile.go
+++ b/internal/lsp/cache/gofile.go
@@ -101,7 +101,7 @@
 	}
 	for _, uri := range m.files {
 		// Call unlocked version of getFile since we hold the lock on the view.
-		if f, err := v.getFile(ctx, uri); err == nil && v.session.IsOpen(uri) {
+		if f, err := v.getFile(ctx, uri, source.Go); err == nil && v.session.IsOpen(uri) {
 			results[f.(*goFile)] = struct{}{}
 		}
 	}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 3480366..bb2477f 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -245,7 +245,7 @@
 		m.files = append(m.files, span.FileURI(filename))
 
 		// Call the unlocked version of getFile since we are holding the view's mutex.
-		f, err := v.getFile(ctx, span.FileURI(filename))
+		f, err := v.getFile(ctx, span.FileURI(filename), source.Go)
 		if err != nil {
 			log.Error(ctx, "no file", err, telemetry.File.Of(filename))
 			continue
diff --git a/internal/lsp/cache/modfile.go b/internal/lsp/cache/modfile.go
index 883dba1..4863e43 100644
--- a/internal/lsp/cache/modfile.go
+++ b/internal/lsp/cache/modfile.go
@@ -4,22 +4,11 @@
 
 package cache
 
-import (
-	"context"
-	"go/token"
-
-	errors "golang.org/x/xerrors"
-)
-
 // modFile holds all of the information we know about a mod file.
 type modFile struct {
 	fileBase
 }
 
-func (*modFile) GetToken(context.Context) (*token.File, error) {
-	return nil, errors.Errorf("GetToken: not implemented")
-}
-
 func (*modFile) setContent(content []byte) {}
 func (*modFile) filename() string          { return "" }
 func (*modFile) isActive() bool            { return false }
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 10c25af..e35addd 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -193,7 +193,7 @@
 }
 
 // TODO: Propagate the language ID through to the view.
-func (s *session) DidOpen(ctx context.Context, uri span.URI, _ source.FileKind, text []byte) {
+func (s *session) DidOpen(ctx context.Context, uri span.URI, kind source.FileKind, text []byte) {
 	ctx = telemetry.File.With(ctx, uri)
 
 	// Files with _ prefixes are ignored.
@@ -211,7 +211,7 @@
 
 	// Read the file on disk and compare it to the text provided.
 	// If it is the same as on disk, we can avoid sending it as an overlay to go/packages.
-	s.openOverlay(ctx, uri, text)
+	s.openOverlay(ctx, uri, kind, text)
 
 	// Mark the file as just opened so that we know to re-run packages.Load on it.
 	// We do this because we may not be aware of all of the packages the file belongs to.
@@ -254,15 +254,15 @@
 	return open
 }
 
-func (s *session) GetFile(uri span.URI) source.FileHandle {
+func (s *session) GetFile(uri span.URI, kind source.FileKind) source.FileHandle {
 	if overlay := s.readOverlay(uri); overlay != nil {
 		return overlay
 	}
 	// Fall back to the cache-level file system.
-	return s.Cache().GetFile(uri)
+	return s.cache.GetFile(uri, kind)
 }
 
-func (s *session) SetOverlay(uri span.URI, data []byte) bool {
+func (s *session) SetOverlay(uri span.URI, kind source.FileKind, data []byte) bool {
 	s.overlayMu.Lock()
 	defer func() {
 		s.overlayMu.Unlock()
@@ -280,6 +280,7 @@
 	s.overlays[uri] = &overlay{
 		session:   s,
 		uri:       uri,
+		kind:      kind,
 		data:      data,
 		hash:      hashContents(data),
 		unchanged: o == nil,
@@ -289,7 +290,7 @@
 
 // openOverlay adds the file content to the overlay.
 // It also checks if the provided content is equivalent to the file's content on disk.
-func (s *session) openOverlay(ctx context.Context, uri span.URI, data []byte) {
+func (s *session) openOverlay(ctx context.Context, uri span.URI, kind source.FileKind, data []byte) {
 	s.overlayMu.Lock()
 	defer func() {
 		s.overlayMu.Unlock()
@@ -298,11 +299,12 @@
 	s.overlays[uri] = &overlay{
 		session:   s,
 		uri:       uri,
+		kind:      kind,
 		data:      data,
 		hash:      hashContents(data),
 		unchanged: true,
 	}
-	_, hash, err := s.cache.GetFile(uri).Read(ctx)
+	_, hash, err := s.cache.GetFile(uri, kind).Read(ctx)
 	if err != nil {
 		log.Error(ctx, "failed to read", err, telemetry.File)
 		return
@@ -356,14 +358,9 @@
 	return source.FileIdentity{
 		URI:     o.uri,
 		Version: o.hash,
+		Kind:    o.kind,
 	}
 }
-
-func (o *overlay) Kind() source.FileKind {
-	// TODO: Determine the file kind using textDocument.languageId.
-	return source.Go
-}
-
 func (o *overlay) Read(ctx context.Context) ([]byte, string, error) {
 	return o.data, o.hash, nil
 }
diff --git a/internal/lsp/cache/sumfile.go b/internal/lsp/cache/sumfile.go
index 21d313c..f73171d 100644
--- a/internal/lsp/cache/sumfile.go
+++ b/internal/lsp/cache/sumfile.go
@@ -4,22 +4,11 @@
 
 package cache
 
-import (
-	"context"
-	"go/token"
-
-	errors "golang.org/x/xerrors"
-)
-
 // sumFile holds all of the information we know about a sum file.
 type sumFile struct {
 	fileBase
 }
 
-func (*sumFile) GetToken(context.Context) (*token.File, error) {
-	return nil, errors.Errorf("GetToken: not implemented")
-}
-
 func (*sumFile) setContent(content []byte) {}
 func (*sumFile) filename() string          { return "" }
 func (*sumFile) isActive() bool            { return false }
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index b16e34e..e80b6af 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -12,7 +12,6 @@
 	"go/types"
 	"os"
 	"os/exec"
-	"path/filepath"
 	"strings"
 	"sync"
 
@@ -229,7 +228,7 @@
 	// and modules included by a replace directive. Return true if
 	// any of these file versions do not match.
 	for filename, version := range v.modFileVersions {
-		if version != v.fileVersion(filename) {
+		if version != v.fileVersion(filename, source.Mod) {
 			return true
 		}
 	}
@@ -248,14 +247,14 @@
 	// and modules included by a replace directive in the resolver.
 	for _, mod := range r.ModsByModPath {
 		if (mod.Main || mod.Replace != nil) && mod.GoMod != "" {
-			v.modFileVersions[mod.GoMod] = v.fileVersion(mod.GoMod)
+			v.modFileVersions[mod.GoMod] = v.fileVersion(mod.GoMod, source.Mod)
 		}
 	}
 }
 
-func (v *view) fileVersion(filename string) string {
+func (v *view) fileVersion(filename string, kind source.FileKind) string {
 	uri := span.FileURI(filename)
-	f := v.session.GetFile(uri)
+	f := v.session.GetFile(uri, kind)
 	return f.Identity().Version
 }
 
@@ -315,7 +314,8 @@
 	v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
 
 	if !v.Ignore(uri) {
-		return v.session.SetOverlay(uri, content), nil
+		kind := source.DetectLanguage("", uri.Filename())
+		return v.session.SetOverlay(uri, kind, content), nil
 	}
 	return false, nil
 }
@@ -425,32 +425,33 @@
 	v.mu.Lock()
 	defer v.mu.Unlock()
 
-	return v.getFile(ctx, uri)
+	// TODO(rstambler): Should there be a version that provides a kind explicitly?
+	kind := source.DetectLanguage("", uri.Filename())
+	return v.getFile(ctx, uri, kind)
 }
 
 // getFile is the unlocked internal implementation of GetFile.
-func (v *view) getFile(ctx context.Context, uri span.URI) (viewFile, error) {
+func (v *view) getFile(ctx context.Context, uri span.URI, kind source.FileKind) (viewFile, error) {
 	if f, err := v.findFile(uri); err != nil {
 		return nil, err
 	} else if f != nil {
 		return f, nil
 	}
-	filename := uri.Filename()
 	var f viewFile
-	switch ext := filepath.Ext(filename); ext {
-	case ".mod":
+	switch kind {
+	case source.Mod:
 		f = &modFile{
 			fileBase: fileBase{
 				view:  v,
-				fname: filename,
+				fname: uri.Filename(),
 				kind:  source.Mod,
 			},
 		}
-	case ".sum":
+	case source.Sum:
 		f = &sumFile{
 			fileBase: fileBase{
 				view:  v,
-				fname: filename,
+				fname: uri.Filename(),
 				kind:  source.Sum,
 			},
 		}
@@ -459,7 +460,7 @@
 		f = &goFile{
 			fileBase: fileBase{
 				view:  v,
-				fname: filename,
+				fname: uri.Filename(),
 				kind:  source.Go,
 			},
 		}
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index a8c7e8c..c1889a5 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -19,24 +19,6 @@
 	errors "golang.org/x/xerrors"
 )
 
-func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
-	allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
-	for _, kinds := range s.session.Options().SupportedCodeActions {
-		for kind := range kinds {
-			allCodeActionKinds[kind] = struct{}{}
-		}
-	}
-
-	var result []protocol.CodeActionKind
-	for kind := range allCodeActionKinds {
-		result = append(result, kind)
-	}
-	sort.Slice(result, func(i, j int) bool {
-		return result[i] < result[j]
-	})
-	return result
-}
-
 func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
 	uri := span.NewURI(params.TextDocument.URI)
 	view := s.session.ViewOf(uri)
@@ -46,7 +28,7 @@
 	}
 
 	// Determine the supported actions for this file kind.
-	fileKind := f.Handle(ctx).Kind()
+	fileKind := f.Handle(ctx).Identity().Kind
 	supportedCodeActions, ok := view.Options().SupportedCodeActions[fileKind]
 	if !ok {
 		return nil, fmt.Errorf("no supported code actions for %v file kind", fileKind)
@@ -68,63 +50,95 @@
 	}
 
 	var codeActions []protocol.CodeAction
-
-	edits, editsPerFix, err := source.AllImportsFixes(ctx, view, f)
-	if err != nil {
-		return nil, err
-	}
-
-	// If the user wants to see quickfixes.
-	if wanted[protocol.QuickFix] {
-		// First, add the quick fixes reported by go/analysis.
-		gof, ok := f.(source.GoFile)
-		if !ok {
-			return nil, fmt.Errorf("%s is not a Go file", f.URI())
+	switch fileKind {
+	case source.Mod:
+		if !wanted[protocol.SourceOrganizeImports] {
+			return nil, nil
 		}
-		qf, err := quickFixes(ctx, view, gof)
-		if err != nil {
-			log.Error(ctx, "quick fixes failed", err, telemetry.File.Of(uri))
-		}
-		codeActions = append(codeActions, qf...)
-
-		// If we also have diagnostics for missing imports, we can associate them with quick fixes.
-		if findImportErrors(params.Context.Diagnostics) {
-			// Separate this into a set of codeActions per diagnostic, where
-			// each action is the addition, removal, or renaming of one import.
-			for _, importFix := range editsPerFix {
-				// Get the diagnostics this fix would affect.
-				if fixDiagnostics := importDiagnostics(importFix.Fix, params.Context.Diagnostics); len(fixDiagnostics) > 0 {
-					codeActions = append(codeActions, protocol.CodeAction{
-						Title: importFixTitle(importFix.Fix),
-						Kind:  protocol.QuickFix,
-						Edit: &protocol.WorkspaceEdit{
-							Changes: &map[string][]protocol.TextEdit{
-								string(uri): importFix.Edits,
-							},
-						},
-						Diagnostics: fixDiagnostics,
-					})
-				}
-			}
-		}
-	}
-
-	// Add the results of import organization as source.OrganizeImports.
-	if wanted[protocol.SourceOrganizeImports] {
 		codeActions = append(codeActions, protocol.CodeAction{
-			Title: "Organize Imports",
+			Title: "Tidy",
 			Kind:  protocol.SourceOrganizeImports,
-			Edit: &protocol.WorkspaceEdit{
-				Changes: &map[string][]protocol.TextEdit{
-					string(uri): edits,
+			Command: &protocol.Command{
+				Title:   "Tidy",
+				Command: "tidy",
+				Arguments: []interface{}{
+					f.URI(),
 				},
 			},
 		})
-	}
+	case source.Go:
+		gof, ok := f.(source.GoFile)
+		if !ok {
+			return nil, errors.Errorf("%s is not a Go file", f.URI())
+		}
+		edits, editsPerFix, err := source.AllImportsFixes(ctx, view, gof)
+		if err != nil {
+			return nil, err
+		}
+		if wanted[protocol.QuickFix] {
+			// First, add the quick fixes reported by go/analysis.
+			qf, err := quickFixes(ctx, view, gof)
+			if err != nil {
+				log.Error(ctx, "quick fixes failed", err, telemetry.File.Of(uri))
+			}
+			codeActions = append(codeActions, qf...)
 
+			// If we also have diagnostics for missing imports, we can associate them with quick fixes.
+			if findImportErrors(params.Context.Diagnostics) {
+				// Separate this into a set of codeActions per diagnostic, where
+				// each action is the addition, removal, or renaming of one import.
+				for _, importFix := range editsPerFix {
+					// Get the diagnostics this fix would affect.
+					if fixDiagnostics := importDiagnostics(importFix.Fix, params.Context.Diagnostics); len(fixDiagnostics) > 0 {
+						codeActions = append(codeActions, protocol.CodeAction{
+							Title: importFixTitle(importFix.Fix),
+							Kind:  protocol.QuickFix,
+							Edit: &protocol.WorkspaceEdit{
+								Changes: &map[string][]protocol.TextEdit{
+									string(uri): importFix.Edits,
+								},
+							},
+							Diagnostics: fixDiagnostics,
+						})
+					}
+				}
+			}
+		}
+		if wanted[protocol.SourceOrganizeImports] {
+			codeActions = append(codeActions, protocol.CodeAction{
+				Title: "Organize Imports",
+				Kind:  protocol.SourceOrganizeImports,
+				Edit: &protocol.WorkspaceEdit{
+					Changes: &map[string][]protocol.TextEdit{
+						string(uri): edits,
+					},
+				},
+			})
+		}
+	default:
+		// Unsupported file kind for a code action.
+		return nil, nil
+	}
 	return codeActions, nil
 }
 
+func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind {
+	allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
+	for _, kinds := range s.session.Options().SupportedCodeActions {
+		for kind := range kinds {
+			allCodeActionKinds[kind] = struct{}{}
+		}
+	}
+	var result []protocol.CodeActionKind
+	for kind := range allCodeActionKinds {
+		result = append(result, kind)
+	}
+	sort.Slice(result, func(i, j int) bool {
+		return result[i] < result[j]
+	})
+	return result
+}
+
 type protocolImportFix struct {
 	fix   *imports.ImportFix
 	edits []protocol.TextEdit
@@ -189,9 +203,7 @@
 				results = append(results, diagnostic)
 			}
 		}
-
 	}
-
 	return results
 }
 
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
new file mode 100644
index 0000000..d6c4a03
--- /dev/null
+++ b/internal/lsp/command.go
@@ -0,0 +1,34 @@
+package lsp
+
+import (
+	"context"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+	errors "golang.org/x/xerrors"
+)
+
+func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
+	switch params.Command {
+	case "tidy":
+		if len(params.Arguments) == 0 || len(params.Arguments) > 1 {
+			return nil, errors.Errorf("expected one file URI for call to `go mod tidy`, got %v", params.Arguments)
+		}
+		// Confirm that this action is being taken on a go.mod file.
+		uri := span.NewURI(params.Arguments[0].(string))
+		view := s.session.ViewOf(uri)
+		f, err := view.GetFile(ctx, uri)
+		if err != nil {
+			return nil, err
+		}
+		if _, ok := f.(source.ModFile); !ok {
+			return nil, errors.Errorf("%s is not a mod file", uri)
+		}
+		// Run go.mod tidy on the view.
+		if err := source.ModTidy(ctx, view); err != nil {
+			return nil, err
+		}
+	}
+	return nil, nil
+}
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index a531814..b10e603 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -82,12 +82,15 @@
 			DefinitionProvider:         true,
 			DocumentFormattingProvider: true,
 			DocumentSymbolProvider:     true,
-			FoldingRangeProvider:       true,
-			HoverProvider:              true,
-			DocumentHighlightProvider:  true,
-			DocumentLinkProvider:       &protocol.DocumentLinkOptions{},
-			ReferencesProvider:         true,
-			RenameProvider:             renameOpts,
+			ExecuteCommandProvider: &protocol.ExecuteCommandOptions{
+				Commands: options.SupportedCommands,
+			},
+			FoldingRangeProvider:      true,
+			HoverProvider:             true,
+			DocumentHighlightProvider: true,
+			DocumentLinkProvider:      &protocol.DocumentLinkOptions{},
+			ReferencesProvider:        true,
+			RenameProvider:            renameOpts,
 			SignatureHelpProvider: &protocol.SignatureHelpOptions{
 				TriggerCharacters: []string{"(", ","},
 			},
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index a6a1b60..5033004 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -67,7 +67,7 @@
 	options.Env = data.Config.Env
 	session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options)
 	for filename, content := range data.Config.Overlay {
-		session.SetOverlay(span.FileURI(filename), content)
+		session.SetOverlay(span.FileURI(filename), source.DetectLanguage("", filename), content)
 	}
 
 	r := &runner{
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 423f734..7b514fc 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -124,8 +124,8 @@
 	return nil, notImplemented("Symbol")
 }
 
-func (s *Server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) (interface{}, error) {
-	return nil, notImplemented("ExecuteCommand")
+func (s *Server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
+	return s.executeCommand(ctx, params)
 }
 
 // Text Synchronization
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index ad91f40..746de0e 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -9,7 +9,6 @@
 	"bytes"
 	"context"
 	"go/format"
-	"log"
 
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/imports"
@@ -150,15 +149,11 @@
 // In addition to returning the result of applying all edits,
 // it returns a list of fixes that could be applied to the file, with the
 // corresponding TextEdits that would be needed to apply that fix.
-func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
+func AllImportsFixes(ctx context.Context, view View, f GoFile) (edits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
 	ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes")
 	defer done()
 
-	gof, ok := f.(GoFile)
-	if !ok {
-		return nil, nil, errors.Errorf("no imports fixes for non-Go files: %v", err)
-	}
-	cphs, err := gof.CheckPackageHandles(ctx)
+	cphs, err := f.CheckPackageHandles(ctx)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -279,7 +274,6 @@
 func hasListErrors(errors []packages.Error) bool {
 	for _, err := range errors {
 		if err.Kind == packages.ListError {
-			log.Printf("LIST ERROR: %v", err)
 			return true
 		}
 	}
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 205ed9a..a5819a5 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -26,9 +26,14 @@
 				protocol.SourceOrganizeImports: true,
 				protocol.QuickFix:              true,
 			},
-			Mod: {},
+			Mod: {
+				protocol.SourceOrganizeImports: true,
+			},
 			Sum: {},
 		},
+		SupportedCommands: []string{
+			"tidy", // for go.mod files
+		},
 		Completion: CompletionOptions{
 			Documentation: true,
 			Deep:          true,
@@ -39,7 +44,6 @@
 )
 
 type Options struct {
-
 	// Env is the current set of environment overrides on this view.
 	Env []string
 
@@ -59,6 +63,8 @@
 
 	SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool
 
+	SupportedCommands []string
+
 	// TODO: Remove the option once we are certain there are no issues here.
 	TextDocumentSyncKind protocol.TextDocumentSyncKind
 
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index bdc7722..7e9af00 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -57,7 +57,7 @@
 		ctx:  ctx,
 	}
 	for filename, content := range data.Config.Overlay {
-		session.SetOverlay(span.FileURI(filename), content)
+		session.SetOverlay(span.FileURI(filename), source.DetectLanguage("", filename), content)
 	}
 	tests.Run(t, r, data)
 }
diff --git a/internal/lsp/source/tidy.go b/internal/lsp/source/tidy.go
new file mode 100644
index 0000000..476a32a
--- /dev/null
+++ b/internal/lsp/source/tidy.go
@@ -0,0 +1,19 @@
+package source
+
+import (
+	"context"
+)
+
+func ModTidy(ctx context.Context, view View) error {
+	cfg := view.Config(ctx)
+
+	// Running `go mod tidy` modifies the file on disk directly.
+	// Ideally, we should return modules that could possibly be removed
+	// and apply each action as an edit.
+	//
+	// TODO(rstambler): This will be possible when golang/go#27005 is resolved.
+	if _, err := invokeGo(ctx, view.Folder().Filename(), cfg.Env, "mod", "tidy"); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 2e89c1f..425e7a5 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -22,10 +22,11 @@
 type FileIdentity struct {
 	URI     span.URI
 	Version string
+	Kind    FileKind
 }
 
 func (identity FileIdentity) String() string {
-	return fmt.Sprintf("%s%s", identity.URI, identity.Version)
+	return fmt.Sprintf("%s%s%s", identity.URI, identity.Version, identity.Kind)
 }
 
 // FileHandle represents a handle to a specific version of a single file from
@@ -37,9 +38,6 @@
 	// Identity returns the FileIdentity for the file.
 	Identity() FileIdentity
 
-	// Kind returns the FileKind for the file.
-	Kind() FileKind
-
 	// Read reads the contents of a file and returns it along with its hash value.
 	// If the file is not available, returns a nil slice and an error.
 	Read(ctx context.Context) ([]byte, string, error)
@@ -48,7 +46,7 @@
 // FileSystem is the interface to something that provides file contents.
 type FileSystem interface {
 	// GetFile returns a handle for the specified file.
-	GetFile(uri span.URI) FileHandle
+	GetFile(uri span.URI, kind FileKind) FileHandle
 }
 
 // FileKind describes the kind of the file in question.
@@ -59,6 +57,7 @@
 	Go = FileKind(iota)
 	Mod
 	Sum
+	UnknownKind
 )
 
 // TokenHandle represents a handle to the *token.File for a file.
@@ -189,7 +188,7 @@
 	IsOpen(uri span.URI) bool
 
 	// Called to set the effective contents of a file from this session.
-	SetOverlay(uri span.URI, data []byte) (wasFirstChange bool)
+	SetOverlay(uri span.URI, kind FileKind, data []byte) (wasFirstChange bool)
 
 	// DidChangeOutOfBand is called when a file under the root folder
 	// changes. The file is not necessarily open in the editor.
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 9e9072a..201b142 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -96,7 +96,7 @@
 }
 
 func (s *Server) applyChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) (string, error) {
-	content, _, err := s.session.GetFile(uri).Read(ctx)
+	content, _, err := s.session.GetFile(uri, source.UnknownKind).Read(ctx)
 	if err != nil {
 		return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found (%v)", err)
 	}