gopls: move file-related types to a 'file' package

There is no reason for the source package to define 'FileHandle', a
fundamental type that does not pertain specifically to Go source.

This type is blocking other refactoring, so move it and related types
to a new 'file' package.

Change-Id: Ic5cd9c0f77d842b4316ae6d7f7086f3b747a2452
Reviewed-on: https://go-review.googlesource.com/c/tools/+/543441
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/file/file.go b/gopls/internal/file/file.go
new file mode 100644
index 0000000..5a9d0ac
--- /dev/null
+++ b/gopls/internal/file/file.go
@@ -0,0 +1,62 @@
+// Copyright 2023 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.
+
+// The file package defines types used for working with LSP files.
+package file
+
+import (
+	"context"
+	"fmt"
+
+	"golang.org/x/tools/gopls/internal/lsp/protocol"
+)
+
+// An Identity identifies the name and contents of a file.
+//
+// TODO(rfindley): Identity may not carry its weight. Consider instead just
+// exposing Handle.Hash, and using an ad-hoc key type where necessary.
+// Or perhaps if mod/work parsing is moved outside of the memoize cache,
+// a notion of Identity simply isn't needed.
+type Identity struct {
+	URI  protocol.DocumentURI
+	Hash Hash // digest of file contents
+}
+
+func (id Identity) String() string {
+	return fmt.Sprintf("%s%s", id.URI, id.Hash)
+}
+
+// A FileHandle represents the URI, content, hash, and optional
+// version of a file tracked by the LSP session.
+//
+// File content may be provided by the file system (for Saved files)
+// or from an overlay, for open files with unsaved edits.
+// A FileHandle may record an attempt to read a non-existent file,
+// in which case Content returns an error.
+type Handle interface {
+	// URI is the URI for this file handle.
+	URI() protocol.DocumentURI
+	// Identity returns an Identity for the file, even if there was an error
+	// reading it.
+	Identity() Identity
+	// SameContentsOnDisk reports whether the file has the same content on disk:
+	// it is false for files open on an editor with unsaved edits.
+	SameContentsOnDisk() bool
+	// Version returns the file version, as defined by the LSP client.
+	// For on-disk file handles, Version returns 0.
+	Version() int32
+	// Content returns the contents of a file.
+	// If the file is not available, returns a nil slice and an error.
+	Content() ([]byte, error)
+}
+
+// A Source maps URIs to Handles.
+type Source interface {
+	// ReadFile returns the Handle for a given URI, either by reading the content
+	// of the file or by obtaining it from a cache.
+	//
+	// Invariant: ReadFile must only return an error in the case of context
+	// cancellation. If ctx.Err() is nil, the resulting error must also be nil.
+	ReadFile(ctx context.Context, uri protocol.DocumentURI) (Handle, error)
+}
diff --git a/gopls/internal/file/hash.go b/gopls/internal/file/hash.go
new file mode 100644
index 0000000..8c19236
--- /dev/null
+++ b/gopls/internal/file/hash.go
@@ -0,0 +1,40 @@
+// Copyright 2023 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 file
+
+import (
+	"crypto/sha256"
+	"fmt"
+)
+
+// A Hash is a cryptographic digest of the contents of a file.
+// (Although at 32B it is larger than a 16B string header, it is smaller
+// and has better locality than the string header + 64B of hex digits.)
+type Hash [sha256.Size]byte
+
+// HashOf returns the hash of some data.
+func HashOf(data []byte) Hash {
+	return Hash(sha256.Sum256(data))
+}
+
+// Hashf returns the hash of a printf-formatted string.
+func Hashf(format string, args ...interface{}) Hash {
+	// Although this looks alloc-heavy, it is faster than using
+	// Fprintf on sha256.New() because the allocations don't escape.
+	return HashOf([]byte(fmt.Sprintf(format, args...)))
+}
+
+// String returns the digest as a string of hex digits.
+func (h Hash) String() string {
+	return fmt.Sprintf("%64x", [sha256.Size]byte(h))
+}
+
+// XORWith updates *h to *h XOR h2.
+func (h *Hash) XORWith(h2 Hash) {
+	// Small enough that we don't need crypto/subtle.XORBytes.
+	for i := range h {
+		h[i] ^= h2[i]
+	}
+}
diff --git a/gopls/internal/file/kind.go b/gopls/internal/file/kind.go
new file mode 100644
index 0000000..e1902e7
--- /dev/null
+++ b/gopls/internal/file/kind.go
@@ -0,0 +1,64 @@
+// Copyright 2023 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 file
+
+import "fmt"
+
+// Kind describes the kind of the file in question.
+// It can be one of Go,mod, Sum, or Tmpl.
+type Kind int
+
+const (
+	// UnknownKind is a file type we don't know about.
+	UnknownKind = Kind(iota)
+
+	// Go is a Go source file.
+	Go
+	// Mod is a go.mod file.
+	Mod
+	// Sum is a go.sum file.
+	Sum
+	// Tmpl is a template file.
+	Tmpl
+	// Work is a go.work file.
+	Work
+)
+
+func (k Kind) String() string {
+	switch k {
+	case Go:
+		return "go"
+	case Mod:
+		return "go.mod"
+	case Sum:
+		return "go.sum"
+	case Tmpl:
+		return "tmpl"
+	case Work:
+		return "go.work"
+	default:
+		return fmt.Sprintf("internal error: unknown file kind %d", k)
+	}
+}
+
+// KindForLang returns the file kind associated with the given language ID
+// (from protocol.TextDocumentItem.LanguageID), or UnknownKind if the language
+// ID is not recognized.
+func KindForLang(langID string) Kind {
+	switch langID {
+	case "go":
+		return Go
+	case "go.mod":
+		return Mod
+	case "go.sum":
+		return Sum
+	case "tmpl", "gotmpl":
+		return Tmpl
+	case "go.work":
+		return Work
+	default:
+		return UnknownKind
+	}
+}
diff --git a/gopls/internal/file/modification.go b/gopls/internal/file/modification.go
new file mode 100644
index 0000000..97313dd
--- /dev/null
+++ b/gopls/internal/file/modification.go
@@ -0,0 +1,57 @@
+// Copyright 2023 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 file
+
+import "golang.org/x/tools/gopls/internal/lsp/protocol"
+
+// Modification represents a modification to a file.
+type Modification struct {
+	URI    protocol.DocumentURI
+	Action Action
+
+	// OnDisk is true if a watched file is changed on disk.
+	// If true, Version will be -1 and Text will be nil.
+	OnDisk bool
+
+	// Version will be -1 and Text will be nil when they are not supplied,
+	// specifically on textDocument/didClose and for on-disk changes.
+	Version int32
+	Text    []byte
+
+	// LanguageID is only sent from the language client on textDocument/didOpen.
+	LanguageID string
+}
+
+// An Action is a type of file state change.
+type Action int
+
+const (
+	UnknownAction = Action(iota)
+	Open
+	Change
+	Close
+	Save
+	Create
+	Delete
+)
+
+func (a Action) String() string {
+	switch a {
+	case Open:
+		return "Open"
+	case Change:
+		return "Change"
+	case Close:
+		return "Close"
+	case Save:
+		return "Save"
+	case Create:
+		return "Create"
+	case Delete:
+		return "Delete"
+	default:
+		return "Unknown"
+	}
+}
diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go
index a937b08..684b912 100644
--- a/gopls/internal/lsp/cache/analysis.go
+++ b/gopls/internal/lsp/cache/analysis.go
@@ -32,6 +32,7 @@
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/filecache"
 	"golang.org/x/tools/gopls/internal/lsp/frob"
 	"golang.org/x/tools/gopls/internal/lsp/progress"
@@ -154,7 +155,7 @@
 //   Steps:
 //   - define a narrow driver.Snapshot interface with only these methods:
 //        Metadata(PackageID) source.Metadata
-//        ReadFile(Context, URI) (source.FileHandle, error)
+//        ReadFile(Context, URI) (file.Handle, error)
 //        View() *View // for Options
 //   - share cache.{goVersionRx,parseGoImpl}
 
@@ -289,7 +290,7 @@
 			// Load the contents of each compiled Go file through
 			// the snapshot's cache. (These are all cache hits as
 			// files are pre-loaded following packages.Load)
-			an.files = make([]source.FileHandle, len(m.CompiledGoFiles))
+			an.files = make([]file.Handle, len(m.CompiledGoFiles))
 			for i, uri := range m.CompiledGoFiles {
 				fh, err := snapshot.ReadFile(ctx, uri)
 				if err != nil {
@@ -503,7 +504,7 @@
 type analysisNode struct {
 	fset            *token.FileSet              // file set shared by entire batch (DAG)
 	m               *source.Metadata            // metadata for this package
-	files           []source.FileHandle         // contents of CompiledGoFiles
+	files           []file.Handle               // contents of CompiledGoFiles
 	analyzers       []*analysis.Analyzer        // set of analyzers to run
 	preds           []*analysisNode             // graph edges:
 	succs           map[PackageID]*analysisNode //   (preds -> self -> succs)
@@ -578,10 +579,10 @@
 // analyzeSummary is a gob-serializable summary of successfully
 // applying a list of analyzers to a package.
 type analyzeSummary struct {
-	Export         []byte      // encoded types of package
-	DeepExportHash source.Hash // hash of reflexive transitive closure of export data
-	Compiles       bool        // transitively free of list/parse/type errors
-	Actions        actionMap   // maps analyzer stablename to analysis results (*actionSummary)
+	Export         []byte    // encoded types of package
+	DeepExportHash file.Hash // hash of reflexive transitive closure of export data
+	Compiles       bool      // transitively free of list/parse/type errors
+	Actions        actionMap // maps analyzer stablename to analysis results (*actionSummary)
 }
 
 // actionMap defines a stable Gob encoding for a map.
@@ -626,8 +627,8 @@
 // actionSummary is a gob-serializable summary of one possibly failed analysis action.
 // If Err is non-empty, the other fields are undefined.
 type actionSummary struct {
-	Facts       []byte      // the encoded facts.Set
-	FactsHash   source.Hash // hash(Facts)
+	Facts       []byte    // the encoded facts.Set
+	FactsHash   file.Hash // hash(Facts)
 	Diagnostics []gobDiagnostic
 	Err         string // "" => success
 }
@@ -735,7 +736,7 @@
 	// file names and contents
 	fmt.Fprintf(hasher, "files: %d\n", len(an.files))
 	for _, fh := range an.files {
-		fmt.Fprintln(hasher, fh.FileIdentity())
+		fmt.Fprintln(hasher, fh.Identity())
 	}
 
 	// vdeps, in PackageID order
@@ -1103,8 +1104,8 @@
 	types          *types.Package
 	compiles       bool // package is transitively free of list/parse/type errors
 	factsDecoder   *facts.Decoder
-	export         []byte      // encoding of types.Package
-	deepExportHash source.Hash // reflexive transitive hash of export data
+	export         []byte    // encoding of types.Package
+	deepExportHash file.Hash // reflexive transitive hash of export data
 	typesInfo      *types.Info
 	typeErrors     []types.Error
 	typesSizes     types.Sizes
@@ -1368,7 +1369,7 @@
 	return result, &actionSummary{
 		Diagnostics: diagnostics,
 		Facts:       factsdata,
-		FactsHash:   source.HashOf(factsdata),
+		FactsHash:   file.HashOf(factsdata),
 	}, nil
 }
 
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index 7ab16ba..ab9a0d2 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -23,6 +23,7 @@
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/immutable"
 	"golang.org/x/tools/gopls/internal/lsp/filecache"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -264,7 +265,7 @@
 	// common case nothing has changed.
 	unchanged := lastImportGraph != nil && len(handles) == len(lastImportGraph.depKeys)
 	var ids []PackageID
-	depKeys := make(map[PackageID]source.Hash)
+	depKeys := make(map[PackageID]file.Hash)
 	for id, ph := range handles {
 		ids = append(ids, id)
 		depKeys[id] = ph.key
@@ -300,9 +301,9 @@
 // An importGraph holds selected results of a type-checking pass, to be re-used
 // by subsequent snapshots.
 type importGraph struct {
-	fset    *token.FileSet            // fileset used for type checking imports
-	depKeys map[PackageID]source.Hash // hash of direct dependencies for this graph
-	imports map[PackageID]pkgOrErr    // results of type checking
+	fset    *token.FileSet          // fileset used for type checking imports
+	depKeys map[PackageID]file.Hash // hash of direct dependencies for this graph
+	imports map[PackageID]pkgOrErr  // results of type checking
 }
 
 // Package visiting functions used by forEachPackage; see the documentation of
@@ -786,7 +787,7 @@
 	// dependencies.
 	localInputs typeCheckInputs
 	// localKey is a hash of localInputs.
-	localKey source.Hash
+	localKey file.Hash
 	// refs is the result of syntactic dependency analysis produced by the
 	// typerefs package.
 	refs map[string][]typerefs.Symbol
@@ -800,12 +801,12 @@
 	// depKeys records the key of each dependency that was used to calculate the
 	// key above. If the handle becomes invalid, we must re-check that each still
 	// matches.
-	depKeys map[PackageID]source.Hash
+	depKeys map[PackageID]file.Hash
 	// key is the hashed key for the package.
 	//
 	// It includes the all bits of the transitive closure of
 	// dependencies's sources.
-	key source.Hash
+	key file.Hash
 }
 
 // clone returns a copy of the receiver with the validated bit set to the
@@ -1154,7 +1155,7 @@
 	}
 
 	// Deps have changed, so we must re-evaluate the key.
-	n.ph.depKeys = make(map[PackageID]source.Hash)
+	n.ph.depKeys = make(map[PackageID]file.Hash)
 
 	// See the typerefs package: the reachable set of packages is defined to be
 	// the set of packages containing syntax that is reachable through the
@@ -1209,7 +1210,7 @@
 
 // typerefs returns typerefs for the package described by m and cgfs, after
 // either computing it or loading it from the file cache.
-func (s *snapshot) typerefs(ctx context.Context, m *source.Metadata, cgfs []source.FileHandle) (map[string][]typerefs.Symbol, error) {
+func (s *snapshot) typerefs(ctx context.Context, m *source.Metadata, cgfs []file.Handle) (map[string][]typerefs.Symbol, error) {
 	imports := make(map[ImportPath]*source.Metadata)
 	for impPath, id := range m.DepsByImpPath {
 		if id != "" {
@@ -1233,7 +1234,7 @@
 
 // typerefData retrieves encoded typeref data from the filecache, or computes it on
 // a cache miss.
-func (s *snapshot) typerefData(ctx context.Context, id PackageID, imports map[ImportPath]*source.Metadata, cgfs []source.FileHandle) ([]byte, error) {
+func (s *snapshot) typerefData(ctx context.Context, id PackageID, imports map[ImportPath]*source.Metadata, cgfs []file.Handle) ([]byte, error) {
 	key := typerefsKey(id, imports, cgfs)
 	if data, err := filecache.Get(typerefsKind, key); err == nil {
 		return data, nil
@@ -1259,7 +1260,7 @@
 
 // typerefsKey produces a key for the reference information produced by the
 // typerefs package.
-func typerefsKey(id PackageID, imports map[ImportPath]*source.Metadata, compiledGoFiles []source.FileHandle) source.Hash {
+func typerefsKey(id PackageID, imports map[ImportPath]*source.Metadata, compiledGoFiles []file.Handle) file.Hash {
 	hasher := sha256.New()
 
 	fmt.Fprintf(hasher, "typerefs: %s\n", id)
@@ -1278,7 +1279,7 @@
 
 	fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(compiledGoFiles))
 	for _, fh := range compiledGoFiles {
-		fmt.Fprintln(hasher, fh.FileIdentity())
+		fmt.Fprintln(hasher, fh.Identity())
 	}
 
 	var hash [sha256.Size]byte
@@ -1297,7 +1298,7 @@
 	// Used for type checking:
 	pkgPath                  PackagePath
 	name                     PackageName
-	goFiles, compiledGoFiles []source.FileHandle
+	goFiles, compiledGoFiles []file.Handle
 	sizes                    types.Sizes
 	depsByImpPath            map[ImportPath]PackageID
 	goVersion                string // packages.Module.GoVersion, e.g. "1.18"
@@ -1351,8 +1352,8 @@
 
 // readFiles reads the content of each file URL from the source
 // (e.g. snapshot or cache).
-func readFiles(ctx context.Context, fs source.FileSource, uris []protocol.DocumentURI) (_ []source.FileHandle, err error) {
-	fhs := make([]source.FileHandle, len(uris))
+func readFiles(ctx context.Context, fs file.Source, uris []protocol.DocumentURI) (_ []file.Handle, err error) {
+	fhs := make([]file.Handle, len(uris))
 	for i, uri := range uris {
 		fhs[i], err = fs.ReadFile(ctx, uri)
 		if err != nil {
@@ -1364,7 +1365,7 @@
 
 // localPackageKey returns a key for local inputs into type-checking, excluding
 // dependency information: files, metadata, and configuration.
-func localPackageKey(inputs typeCheckInputs) source.Hash {
+func localPackageKey(inputs typeCheckInputs) file.Hash {
 	hasher := sha256.New()
 
 	// In principle, a key must be the hash of an
@@ -1390,11 +1391,11 @@
 	// file names and contents
 	fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(inputs.compiledGoFiles))
 	for _, fh := range inputs.compiledGoFiles {
-		fmt.Fprintln(hasher, fh.FileIdentity())
+		fmt.Fprintln(hasher, fh.Identity())
 	}
 	fmt.Fprintf(hasher, "goFiles: %d\n", len(inputs.goFiles))
 	for _, fh := range inputs.goFiles {
-		fmt.Fprintln(hasher, fh.FileIdentity())
+		fmt.Fprintln(hasher, fh.Identity())
 	}
 
 	// types sizes
@@ -1636,7 +1637,7 @@
 // of pkg, or to 'requires' declarations in the package's go.mod file.
 //
 // TODO(rfindley): move this to load.go
-func depsErrors(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs source.FileSource, workspacePackages immutable.Map[PackageID, PackagePath]) ([]*source.Diagnostic, error) {
+func depsErrors(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs file.Source, workspacePackages immutable.Map[PackageID, PackagePath]) ([]*source.Diagnostic, error) {
 	// Select packages that can't be found, and were imported in non-workspace packages.
 	// Workspace packages already show their own errors.
 	var relevantErrors []*packagesinternal.PackageError
diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go
index 75d8a78..fef123b 100644
--- a/gopls/internal/lsp/cache/errors.go
+++ b/gopls/internal/lsp/cache/errors.go
@@ -23,6 +23,7 @@
 
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
@@ -35,7 +36,7 @@
 // diagnostic, using the provided metadata and filesource.
 //
 // The slice of diagnostics may be empty.
-func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) ([]*source.Diagnostic, error) {
+func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, m *source.Metadata, fs file.Source) ([]*source.Diagnostic, error) {
 	if diag, err := parseGoListImportCycleError(ctx, e, m, fs); err != nil {
 		return nil, err
 	} else if diag != nil {
@@ -510,7 +511,7 @@
 // an import cycle, returning a diagnostic if successful.
 //
 // If the error is not detected as an import cycle error, it returns nil, nil.
-func parseGoListImportCycleError(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) (*source.Diagnostic, error) {
+func parseGoListImportCycleError(ctx context.Context, e packages.Error, m *source.Metadata, fs file.Source) (*source.Diagnostic, error) {
 	re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`)
 	matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg))
 	if len(matches) < 3 {
@@ -559,7 +560,7 @@
 // It returns an error if the file could not be read.
 //
 // TODO(rfindley): eliminate this helper.
-func parseGoURI(ctx context.Context, fs source.FileSource, uri protocol.DocumentURI, mode parser.Mode) (*source.ParsedGoFile, error) {
+func parseGoURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI, mode parser.Mode) (*source.ParsedGoFile, error) {
 	fh, err := fs.ReadFile(ctx, uri)
 	if err != nil {
 		return nil, err
@@ -571,7 +572,7 @@
 // source fs.
 //
 // It returns an error if the file could not be read.
-func parseModURI(ctx context.Context, fs source.FileSource, uri protocol.DocumentURI) (*source.ParsedModule, error) {
+func parseModURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI) (*source.ParsedModule, error) {
 	fh, err := fs.ReadFile(ctx, uri)
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/lsp/cache/filemap.go
index dbdb249..f3525fe 100644
--- a/gopls/internal/lsp/cache/filemap.go
+++ b/gopls/internal/lsp/cache/filemap.go
@@ -7,8 +7,8 @@
 import (
 	"path/filepath"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
-	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/persistent"
 )
 
@@ -16,14 +16,14 @@
 // It keeps track of overlays as well as directories containing any observed
 // file.
 type fileMap struct {
-	files    *persistent.Map[protocol.DocumentURI, source.FileHandle]
+	files    *persistent.Map[protocol.DocumentURI, file.Handle]
 	overlays *persistent.Map[protocol.DocumentURI, *Overlay] // the subset of files that are overlays
 	dirs     *persistent.Set[string]                         // all dirs containing files; if nil, dirs have not been initialized
 }
 
 func newFileMap() *fileMap {
 	return &fileMap{
-		files:    new(persistent.Map[protocol.DocumentURI, source.FileHandle]),
+		files:    new(persistent.Map[protocol.DocumentURI, file.Handle]),
 		overlays: new(persistent.Map[protocol.DocumentURI, *Overlay]),
 		dirs:     new(persistent.Set[string]),
 	}
@@ -31,7 +31,7 @@
 
 // Clone creates a copy of the fileMap, incorporating the changes specified by
 // the changes map.
-func (m *fileMap) Clone(changes map[protocol.DocumentURI]source.FileHandle) *fileMap {
+func (m *fileMap) Clone(changes map[protocol.DocumentURI]file.Handle) *fileMap {
 	m2 := &fileMap{
 		files:    m.files.Clone(),
 		overlays: m.overlays.Clone(),
@@ -73,18 +73,18 @@
 
 // Get returns the file handle mapped by the given key, or (nil, false) if the
 // key is not present.
-func (m *fileMap) Get(key protocol.DocumentURI) (source.FileHandle, bool) {
+func (m *fileMap) Get(key protocol.DocumentURI) (file.Handle, bool) {
 	return m.files.Get(key)
 }
 
 // Range calls f for each (uri, fh) in the map.
-func (m *fileMap) Range(f func(uri protocol.DocumentURI, fh source.FileHandle)) {
+func (m *fileMap) Range(f func(uri protocol.DocumentURI, fh file.Handle)) {
 	m.files.Range(f)
 }
 
 // Set stores the given file handle for key, updating overlays and directories
 // accordingly.
-func (m *fileMap) Set(key protocol.DocumentURI, fh source.FileHandle) {
+func (m *fileMap) Set(key protocol.DocumentURI, fh file.Handle) {
 	m.files.Set(key, fh, nil)
 
 	// update overlays
@@ -143,7 +143,7 @@
 func (m *fileMap) Dirs() *persistent.Set[string] {
 	if m.dirs == nil {
 		m.dirs = new(persistent.Set[string])
-		m.files.Range(func(u protocol.DocumentURI, _ source.FileHandle) {
+		m.files.Range(func(u protocol.DocumentURI, _ file.Handle) {
 			m.addDirs(u)
 		})
 	}
diff --git a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/filemap_test.go
index 24b9495..d829d24 100644
--- a/gopls/internal/lsp/cache/filemap_test.go
+++ b/gopls/internal/lsp/cache/filemap_test.go
@@ -10,8 +10,8 @@
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
-	"golang.org/x/tools/gopls/internal/lsp/source"
 )
 
 func TestFileMap(t *testing.T) {
@@ -70,7 +70,7 @@
 				uri := protocol.URIFromPath(filepath.FromSlash(op.path))
 				switch op.op {
 				case set:
-					var fh source.FileHandle
+					var fh file.Handle
 					if op.overlay {
 						fh = &Overlay{uri: uri}
 					} else {
@@ -83,7 +83,7 @@
 			}
 
 			var gotFiles []string
-			m.Range(func(uri protocol.DocumentURI, _ source.FileHandle) {
+			m.Range(func(uri protocol.DocumentURI, _ file.Handle) {
 				gotFiles = append(gotFiles, normalize(uri.Path()))
 			})
 			sort.Strings(gotFiles)
diff --git a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/fs_memoized.go
index d5a0b7a..3ca0473 100644
--- a/gopls/internal/lsp/cache/fs_memoized.go
+++ b/gopls/internal/lsp/cache/fs_memoized.go
@@ -10,8 +10,8 @@
 	"sync"
 	"time"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
-	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/event/tag"
 	"golang.org/x/tools/internal/robustio"
@@ -33,14 +33,14 @@
 	uri     protocol.DocumentURI
 	modTime time.Time
 	content []byte
-	hash    source.Hash
+	hash    file.Hash
 	err     error
 }
 
 func (h *DiskFile) URI() protocol.DocumentURI { return h.uri }
 
-func (h *DiskFile) FileIdentity() source.FileIdentity {
-	return source.FileIdentity{
+func (h *DiskFile) Identity() file.Identity {
+	return file.Identity{
 		URI:  h.uri,
 		Hash: h.hash,
 	}
@@ -51,7 +51,7 @@
 func (h *DiskFile) Content() ([]byte, error) { return h.content, h.err }
 
 // ReadFile stats and (maybe) reads the file, updates the cache, and returns it.
-func (fs *memoizedFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (source.FileHandle, error) {
+func (fs *memoizedFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) {
 	id, mtime, err := robustio.GetFileID(uri.Path())
 	if err != nil {
 		// file does not exist
@@ -161,7 +161,7 @@
 		modTime: mtime,
 		uri:     uri,
 		content: content,
-		hash:    source.HashOf(content),
+		hash:    file.HashOf(content),
 		err:     err,
 	}, nil
 }
diff --git a/gopls/internal/lsp/cache/fs_overlay.go b/gopls/internal/lsp/cache/fs_overlay.go
index 5fc8f4d..8abfbc9 100644
--- a/gopls/internal/lsp/cache/fs_overlay.go
+++ b/gopls/internal/lsp/cache/fs_overlay.go
@@ -8,20 +8,20 @@
 	"context"
 	"sync"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
-	"golang.org/x/tools/gopls/internal/lsp/source"
 )
 
-// An overlayFS is a source.FileSource that keeps track of overlays on top of a
+// An overlayFS is a file.Source that keeps track of overlays on top of a
 // delegate FileSource.
 type overlayFS struct {
-	delegate source.FileSource
+	delegate file.Source
 
 	mu       sync.Mutex
 	overlays map[protocol.DocumentURI]*Overlay
 }
 
-func newOverlayFS(delegate source.FileSource) *overlayFS {
+func newOverlayFS(delegate file.Source) *overlayFS {
 	return &overlayFS{
 		delegate: delegate,
 		overlays: make(map[protocol.DocumentURI]*Overlay),
@@ -39,7 +39,7 @@
 	return overlays
 }
 
-func (fs *overlayFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (source.FileHandle, error) {
+func (fs *overlayFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) {
 	fs.mu.Lock()
 	overlay, ok := fs.overlays[uri]
 	fs.mu.Unlock()
@@ -50,13 +50,13 @@
 }
 
 // An Overlay is a file open in the editor. It may have unsaved edits.
-// It implements the source.FileHandle interface.
+// It implements the file.Handle interface.
 type Overlay struct {
 	uri     protocol.DocumentURI
 	content []byte
-	hash    source.Hash
+	hash    file.Hash
 	version int32
-	kind    source.FileKind
+	kind    file.Kind
 
 	// saved is true if a file matches the state on disk,
 	// and therefore does not need to be part of the overlay sent to go/packages.
@@ -65,8 +65,8 @@
 
 func (o *Overlay) URI() protocol.DocumentURI { return o.uri }
 
-func (o *Overlay) FileIdentity() source.FileIdentity {
-	return source.FileIdentity{
+func (o *Overlay) Identity() file.Identity {
+	return file.Identity{
 		URI:  o.uri,
 		Hash: o.hash,
 	}
@@ -75,4 +75,4 @@
 func (o *Overlay) Content() ([]byte, error) { return o.content, nil }
 func (o *Overlay) Version() int32           { return o.version }
 func (o *Overlay) SameContentsOnDisk() bool { return o.saved }
-func (o *Overlay) Kind() source.FileKind    { return o.kind }
+func (o *Overlay) Kind() file.Kind          { return o.kind }
diff --git a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go
index 3ed513d..2bd7477 100644
--- a/gopls/internal/lsp/cache/imports.go
+++ b/gopls/internal/lsp/cache/imports.go
@@ -12,6 +12,7 @@
 	"sync"
 	"time"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/event/keys"
@@ -26,7 +27,7 @@
 	processEnv             *imports.ProcessEnv
 	cacheRefreshDuration   time.Duration
 	cacheRefreshTimer      *time.Timer
-	cachedModFileHash      source.Hash
+	cachedModFileHash      file.Hash
 	cachedBuildFlags       []string
 	cachedDirectoryFilters []string
 }
@@ -43,13 +44,13 @@
 	// the mod file shouldn't be changing while people are autocompleting.
 	//
 	// TODO(rfindley): consider instead hashing on-disk modfiles here.
-	var modFileHash source.Hash
+	var modFileHash file.Hash
 	for m := range snapshot.workspaceModFiles {
 		fh, err := snapshot.ReadFile(ctx, m)
 		if err != nil {
 			return err
 		}
-		modFileHash.XORWith(fh.FileIdentity().Hash)
+		modFileHash.XORWith(fh.Identity().Hash)
 	}
 
 	// view.goEnv is immutable -- changes make a new view. Options can change.
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 00538bb..538f498 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -17,6 +17,7 @@
 
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/immutable"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -67,7 +68,7 @@
 				panic(fmt.Sprintf("internal error: load called with multiple scopes when a file scope is present (file: %s)", uri))
 			}
 			fh := s.FindFile(uri)
-			if fh == nil || s.FileKind(fh) != source.Go {
+			if fh == nil || s.FileKind(fh) != file.Go {
 				// Don't try to load a file that doesn't exist, or isn't a go file.
 				continue
 			}
@@ -360,14 +361,14 @@
 		// Place the diagnostics on the package or module declarations.
 		var rng protocol.Range
 		switch s.FileKind(fh) {
-		case source.Go:
+		case file.Go:
 			if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil {
 				// Check that we have a valid `package foo` range to use for positioning the error.
 				if pgf.File.Package.IsValid() && pgf.File.Name != nil && pgf.File.Name.End().IsValid() {
 					rng, _ = pgf.PosRange(pgf.File.Package, pgf.File.Name.End())
 				}
 			}
-		case source.Mod:
+		case file.Mod:
 			if pmf, err := s.ParseMod(ctx, fh); err == nil {
 				if mod := pmf.File.Module; mod != nil && mod.Syntax != nil {
 					rng, _ = pmf.Mapper.OffsetRange(mod.Syntax.Start.Byte, mod.Syntax.End.Byte)
@@ -557,7 +558,7 @@
 // computeLoadDiagnostics computes and sets m.Diagnostics for the given metadata m.
 //
 // It should only be called during metadata construction in snapshot.load.
-func computeLoadDiagnostics(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs source.FileSource, workspacePackages immutable.Map[PackageID, PackagePath]) {
+func computeLoadDiagnostics(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs file.Source, workspacePackages immutable.Map[PackageID, PackagePath]) {
 	for _, packagesErr := range m.Errors {
 		// Filter out parse errors from go list. We'll get them when we
 		// actually parse, and buggy overlay support may generate spurious
diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go
index 8f8ebc7..36206ed 100644
--- a/gopls/internal/lsp/cache/mod.go
+++ b/gopls/internal/lsp/cache/mod.go
@@ -14,6 +14,7 @@
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/module"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -24,14 +25,14 @@
 )
 
 // ParseMod parses a go.mod file, using a cache. It may return partial results and an error.
-func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) {
+func (s *snapshot) ParseMod(ctx context.Context, fh file.Handle) (*source.ParsedModule, error) {
 	uri := fh.URI()
 
 	s.mu.Lock()
 	entry, hit := s.parseModHandles.Get(uri)
 	s.mu.Unlock()
 
-	type parseModKey source.FileIdentity
+	type parseModKey file.Identity
 	type parseModResult struct {
 		parsed *source.ParsedModule
 		err    error
@@ -39,7 +40,7 @@
 
 	// cache miss?
 	if !hit {
-		promise, release := s.store.Promise(parseModKey(fh.FileIdentity()), func(ctx context.Context, _ interface{}) interface{} {
+		promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} {
 			parsed, err := parseModImpl(ctx, fh)
 			return parseModResult{parsed, err}
 		})
@@ -61,7 +62,7 @@
 
 // parseModImpl parses the go.mod file whose name and contents are in fh.
 // It may return partial results and an error.
-func parseModImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) {
+func parseModImpl(ctx context.Context, fh file.Handle) (*source.ParsedModule, error) {
 	_, done := event.Start(ctx, "cache.ParseMod", tag.URI.Of(fh.URI()))
 	defer done()
 
@@ -102,14 +103,14 @@
 
 // ParseWork parses a go.work file, using a cache. It may return partial results and an error.
 // TODO(adonovan): move to new work.go file.
-func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) {
+func (s *snapshot) ParseWork(ctx context.Context, fh file.Handle) (*source.ParsedWorkFile, error) {
 	uri := fh.URI()
 
 	s.mu.Lock()
 	entry, hit := s.parseWorkHandles.Get(uri)
 	s.mu.Unlock()
 
-	type parseWorkKey source.FileIdentity
+	type parseWorkKey file.Identity
 	type parseWorkResult struct {
 		parsed *source.ParsedWorkFile
 		err    error
@@ -117,7 +118,7 @@
 
 	// cache miss?
 	if !hit {
-		handle, release := s.store.Promise(parseWorkKey(fh.FileIdentity()), func(ctx context.Context, _ interface{}) interface{} {
+		handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} {
 			parsed, err := parseWorkImpl(ctx, fh)
 			return parseWorkResult{parsed, err}
 		})
@@ -138,7 +139,7 @@
 }
 
 // parseWorkImpl parses a go.work file. It may return partial results and an error.
-func parseWorkImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) {
+func parseWorkImpl(ctx context.Context, fh file.Handle) (*source.ParsedWorkFile, error) {
 	_, done := event.Start(ctx, "cache.ParseWork", tag.URI.Of(fh.URI()))
 	defer done()
 
@@ -187,7 +188,7 @@
 	// TODO(rfindley): but that's not right. Changes to sum files should
 	// invalidate content, even if it's nonexistent content.
 	sumURI := protocol.URIFromPath(sumFilename(modURI))
-	var sumFH source.FileHandle = s.FindFile(sumURI)
+	var sumFH file.Handle = s.FindFile(sumURI)
 	if sumFH == nil {
 		var err error
 		sumFH, err = s.view.fs.ReadFile(ctx, sumURI)
@@ -209,10 +210,10 @@
 // ModWhy returns the "go mod why" result for each module named in a
 // require statement in the go.mod file.
 // TODO(adonovan): move to new mod_why.go file.
-func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) {
+func (s *snapshot) ModWhy(ctx context.Context, fh file.Handle) (map[string]string, error) {
 	uri := fh.URI()
 
-	if s.FileKind(fh) != source.Mod {
+	if s.FileKind(fh) != file.Mod {
 		return nil, fmt.Errorf("%s is not a go.mod file", uri)
 	}
 
@@ -248,7 +249,7 @@
 }
 
 // modWhyImpl returns the result of "go mod why -m" on the specified go.mod file.
-func modWhyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle) (map[string]string, error) {
+func modWhyImpl(ctx context.Context, snapshot *snapshot, fh file.Handle) (map[string]string, error) {
 	ctx, done := event.Start(ctx, "cache.ModWhy", tag.URI.Of(fh.URI()))
 	defer done()
 
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
index ffbee78..896fd05 100644
--- a/gopls/internal/lsp/cache/mod_tidy.go
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -15,6 +15,7 @@
 	"strings"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -483,7 +484,7 @@
 // CompiledGoFiles, after cgo processing.)
 //
 // TODO(rfindley): this should key off source.ImportPath.
-func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) (map[string]bool, error) {
+func parseImports(ctx context.Context, s *snapshot, files []file.Handle) (map[string]bool, error) {
 	pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), source.ParseHeader, false, files...)
 	if err != nil { // e.g. context cancellation
 		return nil, err
diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go
index 74de9c3..5135581 100644
--- a/gopls/internal/lsp/cache/parse.go
+++ b/gopls/internal/lsp/cache/parse.go
@@ -16,6 +16,7 @@
 	"reflect"
 
 	goplsastutil "golang.org/x/tools/gopls/internal/astutil"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -25,8 +26,8 @@
 )
 
 // ParseGo parses the file whose contents are provided by fh, using a cache.
-// The resulting tree may have beeen fixed up.
-func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode parser.Mode) (*source.ParsedGoFile, error) {
+// The resulting tree may have been fixed up.
+func (s *snapshot) ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode) (*source.ParsedGoFile, error) {
 	pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh)
 	if err != nil {
 		return nil, err
@@ -35,7 +36,7 @@
 }
 
 // parseGoImpl parses the Go source file whose content is provided by fh.
-func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode parser.Mode, purgeFuncBodies bool) (*source.ParsedGoFile, error) {
+func parseGoImpl(ctx context.Context, fset *token.FileSet, fh file.Handle, mode parser.Mode, purgeFuncBodies bool) (*source.ParsedGoFile, error) {
 	ext := filepath.Ext(fh.URI().Path())
 	if ext != ".go" && ext != "" { // files generated by cgo have no extension
 		return nil, fmt.Errorf("cannot parse non-Go file %s", fh.URI())
diff --git a/gopls/internal/lsp/cache/parse_cache.go b/gopls/internal/lsp/cache/parse_cache.go
index 6afc7a5..669de65 100644
--- a/gopls/internal/lsp/cache/parse_cache.go
+++ b/gopls/internal/lsp/cache/parse_cache.go
@@ -17,6 +17,7 @@
 	"time"
 
 	"golang.org/x/sync/errgroup"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/memoize"
@@ -133,7 +134,7 @@
 
 type parseCacheEntry struct {
 	key      parseKey
-	hash     source.Hash
+	hash     file.Hash
 	promise  *memoize.Promise // memoize.Promise[*source.ParsedGoFile]
 	atime    uint64           // clock time of last access, for use in LRU sorting
 	walltime time.Time        // actual time of last access, for use in time-based eviction; too coarse for LRU on some systems
@@ -146,7 +147,7 @@
 // The resulting slice has an entry for every given file handle, though some
 // entries may be nil if there was an error reading the file (in which case the
 // resulting error will be non-nil).
-func (c *parseCache) startParse(mode parser.Mode, purgeFuncBodies bool, fhs ...source.FileHandle) ([]*memoize.Promise, error) {
+func (c *parseCache) startParse(mode parser.Mode, purgeFuncBodies bool, fhs ...file.Handle) ([]*memoize.Promise, error) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
@@ -180,7 +181,7 @@
 		}
 
 		if e, ok := c.m[key]; ok {
-			if e.hash == fh.FileIdentity().Hash { // cache hit
+			if e.hash == fh.Identity().Hash { // cache hit
 				e.atime = c.clock
 				e.walltime = walltime
 				heap.Fix(&c.lru, e.lruIndex)
@@ -236,7 +237,7 @@
 		// add new entry; entries are gc'ed asynchronously
 		e := &parseCacheEntry{
 			key:      key,
-			hash:     fh.FileIdentity().Hash,
+			hash:     fh.Identity().Hash,
 			promise:  promise,
 			atime:    c.clock,
 			walltime: walltime,
@@ -316,7 +317,7 @@
 //
 // If parseFiles returns an error, it still returns a slice,
 // but with a nil entry for each file that could not be parsed.
-func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode parser.Mode, purgeFuncBodies bool, fhs ...source.FileHandle) ([]*source.ParsedGoFile, error) {
+func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode parser.Mode, purgeFuncBodies bool, fhs ...file.Handle) ([]*source.ParsedGoFile, error) {
 	pgfs := make([]*source.ParsedGoFile, len(fhs))
 
 	// Temporary fall-back for 32-bit systems, where reservedForParsing is too
diff --git a/gopls/internal/lsp/cache/parse_cache_test.go b/gopls/internal/lsp/cache/parse_cache_test.go
index 178a1b5..5693972 100644
--- a/gopls/internal/lsp/cache/parse_cache_test.go
+++ b/gopls/internal/lsp/cache/parse_cache_test.go
@@ -12,6 +12,7 @@
 	"testing"
 	"time"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 )
@@ -47,7 +48,7 @@
 
 	// Fill up the cache with other files, but don't evict the file above.
 	cache.gcOnce()
-	files := []source.FileHandle{fh}
+	files := []file.Handle{fh}
 	files = append(files, dummyFileHandles(parseCacheMinFiles-1)...)
 
 	pgfs3, err := cache.parseFiles(ctx, fset, source.ParseFull, false, files...)
@@ -114,7 +115,7 @@
 	parsePadding = 0
 
 	danglingSelector := []byte("package p\nfunc _() {\n\tx.\n}")
-	files := []source.FileHandle{makeFakeFileHandle("file:///bad", danglingSelector)}
+	files := []file.Handle{makeFakeFileHandle("file:///bad", danglingSelector)}
 
 	// Parsing should succeed even though we overflow the padding.
 	cache := newParseCache(0)
@@ -192,8 +193,8 @@
 	}
 }
 
-func dummyFileHandles(n int) []source.FileHandle {
-	var fhs []source.FileHandle
+func dummyFileHandles(n int) []file.Handle {
+	var fhs []file.Handle
 	for i := 0; i < n; i++ {
 		uri := protocol.DocumentURI(fmt.Sprintf("file:///_%d", i))
 		src := []byte(fmt.Sprintf("package p\nvar _ = %d", i))
@@ -206,15 +207,15 @@
 	return fakeFileHandle{
 		uri:  uri,
 		data: src,
-		hash: source.HashOf(src),
+		hash: file.HashOf(src),
 	}
 }
 
 type fakeFileHandle struct {
-	source.FileHandle
+	file.Handle
 	uri  protocol.DocumentURI
 	data []byte
-	hash source.Hash
+	hash file.Hash
 }
 
 func (h fakeFileHandle) URI() protocol.DocumentURI {
@@ -225,8 +226,8 @@
 	return h.data, nil
 }
 
-func (h fakeFileHandle) FileIdentity() source.FileIdentity {
-	return source.FileIdentity{
+func (h fakeFileHandle) Identity() file.Identity {
+	return file.Identity{
 		URI:  h.uri,
 		Hash: h.hash,
 	}
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index 3e6f3fc..59561bd 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -15,6 +15,7 @@
 	"sync/atomic"
 
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/lsp/source/typerefs"
@@ -388,7 +389,7 @@
 // TODO(rfindley): what happens if this function fails? It must leave us in a
 // broken state, which we should surface to the user, probably as a request to
 // restart gopls.
-func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]protocol.DocumentURI, func(), error) {
+func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modification) (map[source.Snapshot][]protocol.DocumentURI, func(), error) {
 	s.viewMu.Lock()
 	defer s.viewMu.Unlock()
 
@@ -417,8 +418,8 @@
 		if isGoMod(c.URI) || isGoWork(c.URI) {
 			// Change, InvalidateMetadata, and UnknownFileAction actions do not cause
 			// us to re-evaluate views.
-			redoViews := (c.Action != source.Change &&
-				c.Action != source.UnknownFileAction)
+			redoViews := (c.Action != file.Change &&
+				c.Action != file.UnknownAction)
 
 			if redoViews {
 				checkViews = true
@@ -455,7 +456,7 @@
 	}
 
 	// Collect information about views affected by these changes.
-	views := make(map[*View]map[protocol.DocumentURI]source.FileHandle)
+	views := make(map[*View]map[protocol.DocumentURI]file.Handle)
 	affectedViews := map[protocol.DocumentURI][]*View{}
 	for _, c := range changes {
 		// Build the list of affected views.
@@ -488,7 +489,7 @@
 			// Make sure that the file is added to the view's seenFiles set.
 			view.markKnown(c.URI)
 			if _, ok := views[view]; !ok {
-				views[view] = make(map[protocol.DocumentURI]source.FileHandle)
+				views[view] = make(map[protocol.DocumentURI]file.Handle)
 			}
 			views[view][c.URI] = fh
 		}
@@ -534,7 +535,7 @@
 // ExpandModificationsToDirectories returns the set of changes with the
 // directory changes removed and expanded to include all of the files in
 // the directory.
-func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification {
+func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []file.Modification) []file.Modification {
 	var snapshots []*snapshot
 	s.viewMu.Lock()
 	for _, v := range s.views {
@@ -552,7 +553,7 @@
 	//
 	// There may be other files in the directory, but if we haven't read them yet
 	// we don't need to invalidate them.
-	var result []source.FileModification
+	var result []file.Modification
 	for _, c := range changes {
 		expanded := make(map[protocol.DocumentURI]bool)
 		for _, snapshot := range snapshots {
@@ -564,7 +565,7 @@
 			result = append(result, c)
 		} else {
 			for uri := range expanded {
-				result = append(result, source.FileModification{
+				result = append(result, file.Modification{
 					URI:        uri,
 					Action:     c.Action,
 					LanguageID: "",
@@ -579,7 +580,7 @@
 
 // Precondition: caller holds s.viewMu lock.
 // TODO(rfindley): move this to fs_overlay.go.
-func (fs *overlayFS) updateOverlays(ctx context.Context, changes []source.FileModification) error {
+func (fs *overlayFS) updateOverlays(ctx context.Context, changes []file.Modification) error {
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
 
@@ -594,10 +595,10 @@
 		}
 
 		// Determine the file kind on open, otherwise, assume it has been cached.
-		var kind source.FileKind
+		var kind file.Kind
 		switch c.Action {
-		case source.Open:
-			kind = source.FileKindForLang(c.LanguageID)
+		case file.Open:
+			kind = file.KindForLang(c.LanguageID)
 		default:
 			if !ok {
 				return fmt.Errorf("updateOverlays: modifying unopened overlay %v", c.URI)
@@ -606,7 +607,7 @@
 		}
 
 		// Closing a file just deletes its overlay.
-		if c.Action == source.Close {
+		if c.Action == file.Close {
 			delete(fs.overlays, c.URI)
 			continue
 		}
@@ -615,7 +616,7 @@
 		// overlay. Saves and on-disk file changes don't come with the file's
 		// content.
 		text := c.Text
-		if text == nil && (c.Action == source.Save || c.OnDisk) {
+		if text == nil && (c.Action == file.Save || c.OnDisk) {
 			if !ok {
 				return fmt.Errorf("no known content for overlay for %s", c.Action)
 			}
@@ -623,15 +624,15 @@
 		}
 		// On-disk changes don't come with versions.
 		version := c.Version
-		if c.OnDisk || c.Action == source.Save {
+		if c.OnDisk || c.Action == file.Save {
 			version = o.version
 		}
-		hash := source.HashOf(text)
+		hash := file.HashOf(text)
 		var sameContentOnDisk bool
 		switch c.Action {
-		case source.Delete:
+		case file.Delete:
 			// Do nothing. sameContentOnDisk should be false.
-		case source.Save:
+		case file.Save:
 			// Make sure the version and content (if present) is the same.
 			if false && o.version != version { // Client no longer sends the version
 				return fmt.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version)
@@ -643,7 +644,7 @@
 		default:
 			fh := mustReadFile(ctx, fs.delegate, c.URI)
 			_, readErr := fh.Content()
-			sameContentOnDisk = (readErr == nil && fh.FileIdentity().Hash == hash)
+			sameContentOnDisk = (readErr == nil && fh.Identity().Hash == hash)
 		}
 		o = &Overlay{
 			uri:     c.URI,
@@ -663,7 +664,7 @@
 	return nil
 }
 
-func mustReadFile(ctx context.Context, fs source.FileSource, uri protocol.DocumentURI) source.FileHandle {
+func mustReadFile(ctx context.Context, fs file.Source, uri protocol.DocumentURI) file.Handle {
 	ctx = xcontext.Detach(ctx)
 	fh, err := fs.ReadFile(ctx, uri)
 	if err != nil {
@@ -680,11 +681,11 @@
 	err error
 }
 
-func (b brokenFile) URI() protocol.DocumentURI         { return b.uri }
-func (b brokenFile) FileIdentity() source.FileIdentity { return source.FileIdentity{URI: b.uri} }
-func (b brokenFile) SameContentsOnDisk() bool          { return false }
-func (b brokenFile) Version() int32                    { return 0 }
-func (b brokenFile) Content() ([]byte, error)          { return nil, b.err }
+func (b brokenFile) URI() protocol.DocumentURI { return b.uri }
+func (b brokenFile) Identity() file.Identity   { return file.Identity{URI: b.uri} }
+func (b brokenFile) SameContentsOnDisk() bool  { return false }
+func (b brokenFile) Version() int32            { return 0 }
+func (b brokenFile) Content() ([]byte, error)  { return nil, b.err }
 
 // FileWatchingGlobPatterns returns a new set of glob patterns to
 // watch every directory known by the view. For views within a module,
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index f09ee49..976efdc 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -30,6 +30,7 @@
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/go/types/objectpath"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/immutable"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/filecache"
@@ -275,12 +276,12 @@
 	return s.view
 }
 
-func (s *snapshot) FileKind(fh source.FileHandle) source.FileKind {
+func (s *snapshot) FileKind(fh file.Handle) file.Kind {
 	// The kind of an unsaved buffer comes from the
 	// TextDocumentItem.LanguageID field in the didChange event,
 	// not from the file name. They may differ.
 	if o, ok := fh.(*Overlay); ok {
-		if o.kind != source.UnknownKind {
+		if o.kind != file.UnknownKind {
 			return o.kind
 		}
 	}
@@ -288,22 +289,22 @@
 	fext := filepath.Ext(fh.URI().Path())
 	switch fext {
 	case ".go":
-		return source.Go
+		return file.Go
 	case ".mod":
-		return source.Mod
+		return file.Mod
 	case ".sum":
-		return source.Sum
+		return file.Sum
 	case ".work":
-		return source.Work
+		return file.Work
 	}
 	exts := s.Options().TemplateExtensions
 	for _, ext := range exts {
 		if fext == ext || fext == "."+ext {
-			return source.Tmpl
+			return file.Tmpl
 		}
 	}
 	// and now what? This should never happen, but it does for cgo before go1.15
-	return source.Go
+	return file.Go
 }
 
 func (s *snapshot) Options() *source.Options {
@@ -327,13 +328,13 @@
 	return gowork
 }
 
-func (s *snapshot) Templates() map[protocol.DocumentURI]source.FileHandle {
+func (s *snapshot) Templates() map[protocol.DocumentURI]file.Handle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	tmpls := map[protocol.DocumentURI]source.FileHandle{}
-	s.files.Range(func(k protocol.DocumentURI, fh source.FileHandle) {
-		if s.FileKind(fh) == source.Tmpl {
+	tmpls := map[protocol.DocumentURI]file.Handle{}
+	s.files.Range(func(k protocol.DocumentURI, fh file.Handle) {
+		if s.FileKind(fh) == file.Tmpl {
 			tmpls[k] = fh
 		}
 	})
@@ -1047,7 +1048,7 @@
 		return nil
 	}
 	var files []protocol.DocumentURI
-	s.files.Range(func(uri protocol.DocumentURI, _ source.FileHandle) {
+	s.files.Range(func(uri protocol.DocumentURI, _ file.Handle) {
 		if source.InDir(dir, uri.Path()) {
 			files = append(files, uri)
 		}
@@ -1170,7 +1171,7 @@
 // containing uri, or a parent of that directory.
 //
 // The given uri must be a file, not a directory.
-func nearestModFile(ctx context.Context, uri protocol.DocumentURI, fs source.FileSource) (protocol.DocumentURI, error) {
+func nearestModFile(ctx context.Context, uri protocol.DocumentURI, fs file.Source) (protocol.DocumentURI, error) {
 	dir := filepath.Dir(uri.Path())
 	mod, err := findRootPattern(ctx, dir, "go.mod", fs)
 	if err != nil {
@@ -1216,7 +1217,7 @@
 	}
 }
 
-func (s *snapshot) FindFile(uri protocol.DocumentURI) source.FileHandle {
+func (s *snapshot) FindFile(uri protocol.DocumentURI) file.Handle {
 	s.view.markKnown(uri)
 
 	s.mu.Lock()
@@ -1231,7 +1232,7 @@
 //
 // ReadFile succeeds even if the file does not exist. A non-nil error return
 // indicates some type of internal error, for example if ctx is cancelled.
-func (s *snapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (source.FileHandle, error) {
+func (s *snapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
@@ -1241,7 +1242,7 @@
 // preloadFiles delegates to the view FileSource to read the requested uris in
 // parallel, without holding the snapshot lock.
 func (s *snapshot) preloadFiles(ctx context.Context, uris []protocol.DocumentURI) {
-	files := make([]source.FileHandle, len(uris))
+	files := make([]file.Handle, len(uris))
 	var wg sync.WaitGroup
 	iolimit := make(chan struct{}, 20) // I/O concurrency limiting semaphore
 	for i, uri := range uris {
@@ -1274,11 +1275,11 @@
 	}
 }
 
-// A lockedSnapshot implements the source.FileSource interface while holding
+// A lockedSnapshot implements the file.Source interface while holding
 // the lock for the wrapped snapshot.
 type lockedSnapshot struct{ wrapped *snapshot }
 
-func (s lockedSnapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (source.FileHandle, error) {
+func (s lockedSnapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) {
 	s.wrapped.view.markKnown(uri)
 	if fh, ok := s.wrapped.files.Get(uri); ok {
 		return fh, nil
@@ -1524,7 +1525,7 @@
 	var files []*Overlay
 	for _, o := range open {
 		uri := o.URI()
-		if s.IsBuiltin(uri) || s.FileKind(o) != source.Go {
+		if s.IsBuiltin(uri) || s.FileKind(o) != file.Go {
 			continue
 		}
 		if len(meta.ids[uri]) == 0 {
@@ -1625,7 +1626,7 @@
 searchOverlays:
 	for _, o := range s.overlays() {
 		uri := o.URI()
-		if s.IsBuiltin(uri) || s.FileKind(o) != source.Go {
+		if s.IsBuiltin(uri) || s.FileKind(o) != file.Go {
 			continue
 		}
 		md, err := s.MetadataForFile(ctx, uri)
@@ -1889,7 +1890,7 @@
 	// they exist. Importantly, we don't call ReadFile here: consider the case
 	// where a file is added on disk; we don't want to read the newly added file
 	// into the old snapshot, as that will break our change detection below.
-	oldFiles := make(map[protocol.DocumentURI]source.FileHandle)
+	oldFiles := make(map[protocol.DocumentURI]file.Handle)
 	for uri := range changedFiles {
 		if fh, ok := s.files.Get(uri); ok {
 			oldFiles[uri] = fh
@@ -1900,14 +1901,14 @@
 	// the old file wasn't saved, or the on-disk contents changed.
 	//
 	// oldFH may be nil.
-	changedOnDisk := func(oldFH, newFH source.FileHandle) bool {
+	changedOnDisk := func(oldFH, newFH file.Handle) bool {
 		if !newFH.SameContentsOnDisk() {
 			return false
 		}
 		if oe, ne := (oldFH != nil && fileExists(oldFH)), fileExists(newFH); !oe || !ne {
 			return oe != ne
 		}
-		return !oldFH.SameContentsOnDisk() || oldFH.FileIdentity() != newFH.FileIdentity()
+		return !oldFH.SameContentsOnDisk() || oldFH.Identity() != newFH.Identity()
 	}
 
 	if workURI, _ := s.view.GOWORK(); workURI != "" {
@@ -2281,7 +2282,7 @@
 // accomplishes this by checking to see if the original and current FileHandles
 // are both overlays, and if the current FileHandle is saved while the original
 // FileHandle was not saved.
-func fileWasSaved(originalFH, currentFH source.FileHandle) bool {
+func fileWasSaved(originalFH, currentFH file.Handle) bool {
 	c, ok := currentFH.(*Overlay)
 	if !ok || c == nil {
 		return true
@@ -2307,14 +2308,14 @@
 //     changed).
 //   - importDeleted means that an import has been deleted, or we can't
 //     determine if an import was deleted due to errors.
-func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH source.FileHandle) (invalidate, pkgFileChanged, importDeleted bool) {
+func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH file.Handle) (invalidate, pkgFileChanged, importDeleted bool) {
 	if oe, ne := oldFH != nil && fileExists(oldFH), fileExists(newFH); !oe || !ne { // existential changes
 		changed := oe != ne
 		return changed, changed, !ne // we don't know if an import was deleted
 	}
 
 	// If the file hasn't changed, there's no need to reload.
-	if oldFH.FileIdentity() == newFH.FileIdentity() {
+	if oldFH.Identity() == newFH.Identity() {
 		return false, false, false
 	}
 
diff --git a/gopls/internal/lsp/cache/symbols.go b/gopls/internal/lsp/cache/symbols.go
index b16c001..605a186 100644
--- a/gopls/internal/lsp/cache/symbols.go
+++ b/gopls/internal/lsp/cache/symbols.go
@@ -12,6 +12,7 @@
 	"strings"
 
 	"golang.org/x/tools/gopls/internal/astutil"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 )
@@ -34,8 +35,8 @@
 		if err != nil {
 			return nil, err
 		}
-		type symbolHandleKey source.Hash
-		key := symbolHandleKey(fh.FileIdentity().Hash)
+		type symbolHandleKey file.Hash
+		key := symbolHandleKey(fh.Identity().Hash)
 		promise, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} {
 			symbols, err := symbolizeImpl(ctx, arg.(*snapshot), fh)
 			return symbolizeResult{symbols, err}
@@ -58,7 +59,7 @@
 }
 
 // symbolizeImpl reads and parses a file and extracts symbols from it.
-func symbolizeImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) {
+func symbolizeImpl(ctx context.Context, snapshot *snapshot, fh file.Handle) ([]source.Symbol, error) {
 	pgfs, err := snapshot.view.parseCache.parseFiles(ctx, token.NewFileSet(), source.ParseFull, false, fh)
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index 0c379a1..1023edc 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -23,6 +23,7 @@
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/semver"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/vulncheck"
@@ -350,8 +351,8 @@
 // of the given go.mod file. On success, it is the caller's
 // responsibility to call the cleanup function when the file is no
 // longer needed.
-func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI protocol.DocumentURI, cleanup func(), err error) {
-	filenameHash := source.Hashf("%s", modFh.URI().Path())
+func tempModFile(modFh file.Handle, gosum []byte) (tmpURI protocol.DocumentURI, cleanup func(), err error) {
+	filenameHash := file.Hashf("%s", modFh.URI().Path())
 	tmpMod, err := os.CreateTemp("", fmt.Sprintf("go.%s.*.mod", filenameHash))
 	if err != nil {
 		return "", nil, err
@@ -557,7 +558,7 @@
 	}
 }
 
-func (v *View) relevantChange(c source.FileModification) bool {
+func (v *View) relevantChange(c file.Modification) bool {
 	// If the file is known to the view, the change is relevant.
 	if v.knownFile(c.URI) {
 		return true
@@ -888,7 +889,7 @@
 	return v.snapshot, v.snapshot.Acquire()
 }
 
-func getViewDefinition(ctx context.Context, runner *gocommand.Runner, fs source.FileSource, folder *Folder) (*viewDefinition, error) {
+func getViewDefinition(ctx context.Context, runner *gocommand.Runner, fs file.Source, folder *Folder) (*viewDefinition, error) {
 	if err := checkPathCase(folder.Dir.Path()); err != nil {
 		return nil, fmt.Errorf("invalid workspace folder path: %w; check that the casing of the configured workspace folder path agrees with the casing reported by the operating system", err)
 	}
@@ -953,7 +954,7 @@
 //  1. if there is a go.mod file in a parent directory, return it
 //  2. else, if there is exactly one nested module, return it
 //  3. else, return ""
-func findWorkspaceModFile(ctx context.Context, folderURI protocol.DocumentURI, fs source.FileSource, excludePath func(string) bool) (protocol.DocumentURI, error) {
+func findWorkspaceModFile(ctx context.Context, folderURI protocol.DocumentURI, fs file.Source, excludePath func(string) bool) (protocol.DocumentURI, error) {
 	folder := folderURI.Path()
 	match, err := findRootPattern(ctx, folder, "go.mod", fs)
 	if err != nil {
@@ -992,7 +993,7 @@
 //
 // The resulting string is either the file path of a matching file with the
 // given basename, or "" if none was found.
-func findRootPattern(ctx context.Context, dir, basename string, fs source.FileSource) (string, error) {
+func findRootPattern(ctx context.Context, dir, basename string, fs file.Source) (string, error) {
 	for dir != "" {
 		target := filepath.Join(dir, basename)
 		fh, err := fs.ReadFile(ctx, protocol.URIFromPath(target))
diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go
index 3787ad1..468fd8e 100644
--- a/gopls/internal/lsp/cache/workspace.go
+++ b/gopls/internal/lsp/cache/workspace.go
@@ -13,8 +13,8 @@
 	"strings"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
-	"golang.org/x/tools/gopls/internal/lsp/source"
 )
 
 // TODO(rfindley): now that experimentalWorkspaceModule is gone, this file can
@@ -22,7 +22,7 @@
 
 // computeWorkspaceModFiles computes the set of workspace mod files based on the
 // value of go.mod, go.work, and GO111MODULE.
-func computeWorkspaceModFiles(ctx context.Context, gomod, gowork protocol.DocumentURI, go111module go111module, fs source.FileSource) (map[protocol.DocumentURI]struct{}, error) {
+func computeWorkspaceModFiles(ctx context.Context, gomod, gowork protocol.DocumentURI, go111module go111module, fs file.Source) (map[protocol.DocumentURI]struct{}, error) {
 	if go111module == off {
 		return nil, nil
 	}
@@ -70,7 +70,7 @@
 
 // fileExists reports whether the file has a Content (which may be empty).
 // An overlay exists even if it is not reflected in the file system.
-func fileExists(fh source.FileHandle) bool {
+func fileExists(fh file.Handle) bool {
 	_, err := fh.Content()
 	return err == nil
 }
diff --git a/gopls/internal/lsp/call_hierarchy.go b/gopls/internal/lsp/call_hierarchy.go
index d414b08..3a88520 100644
--- a/gopls/internal/lsp/call_hierarchy.go
+++ b/gopls/internal/lsp/call_hierarchy.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
@@ -16,7 +17,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.prepareCallHierarchy")
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
@@ -29,7 +30,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.incomingCalls")
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.Item.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.Item.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
@@ -42,7 +43,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.outgoingCalls")
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.Item.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.Item.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go
index 8dfe98f..5efb7a4 100644
--- a/gopls/internal/lsp/code_action.go
+++ b/gopls/internal/lsp/code_action.go
@@ -13,6 +13,7 @@
 
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods"
@@ -29,7 +30,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.codeAction")
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
@@ -76,7 +77,7 @@
 	}
 
 	switch kind {
-	case source.Mod:
+	case file.Mod:
 		var actions []protocol.CodeAction
 
 		fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, want)
@@ -108,7 +109,7 @@
 
 		return actions, nil
 
-	case source.Go:
+	case file.Go:
 		diagnostics := params.Context.Diagnostics
 
 		// Don't suggest fixes for generated files, since they are generally
@@ -418,7 +419,7 @@
 	return actions, nil
 }
 
-func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, fh source.FileHandle, rng protocol.Range) (_ []protocol.CodeAction, rerr error) {
+func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, fh file.Handle, rng protocol.Range) (_ []protocol.CodeAction, rerr error) {
 	// golang/go#61693: code actions were refactored to run outside of the
 	// analysis framework, but as a result they lost their panic recovery.
 	//
@@ -574,7 +575,7 @@
 }
 
 // refactorInline returns inline actions available at the specified range.
-func refactorInline(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, fh source.FileHandle, rng protocol.Range) ([]protocol.CodeAction, error) {
+func refactorInline(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, fh file.Handle, rng protocol.Range) ([]protocol.CodeAction, error) {
 	var commands []protocol.Command
 
 	// If range is within call expression, offer inline action.
@@ -602,7 +603,7 @@
 	return actions, nil
 }
 
-func documentChanges(fh source.FileHandle, edits []protocol.TextEdit) []protocol.DocumentChanges {
+func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges {
 	return []protocol.DocumentChanges{
 		{
 			TextDocumentEdit: &protocol.TextDocumentEdit{
diff --git a/gopls/internal/lsp/code_lens.go b/gopls/internal/lsp/code_lens.go
index c3fd618..6d8378c 100644
--- a/gopls/internal/lsp/code_lens.go
+++ b/gopls/internal/lsp/code_lens.go
@@ -9,6 +9,7 @@
 	"fmt"
 	"sort"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/mod"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -21,16 +22,16 @@
 	ctx, done := event.Start(ctx, "lsp.Server.codeLens", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
 	}
 	var lenses map[command.Command]source.LensFunc
 	switch snapshot.FileKind(fh) {
-	case source.Mod:
+	case file.Mod:
 		lenses = mod.LensFuncs()
-	case source.Go:
+	case file.Go:
 		lenses = source.LensFuncs()
 	default:
 		// Unsupported file kind for a code lens.
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index d9ff59d..53fccee 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -22,6 +22,7 @@
 	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/cache"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/debug"
@@ -92,7 +93,7 @@
 // for details.
 type commandDeps struct {
 	snapshot source.Snapshot    // present if cfg.forURI was set
-	fh       source.FileHandle  // present if cfg.forURI was set
+	fh       file.Handle        // present if cfg.forURI was set
 	work     *progress.WorkDone // present cfg.progress was set
 }
 
@@ -124,7 +125,7 @@
 	if cfg.forURI != "" {
 		var ok bool
 		var release func()
-		deps.snapshot, deps.fh, ok, release, err = c.s.beginFileRequest(ctx, cfg.forURI, source.UnknownKind)
+		deps.snapshot, deps.fh, ok, release, err = c.s.beginFileRequest(ctx, cfg.forURI, file.UnknownKind)
 		defer release()
 		if !ok {
 			if err != nil {
@@ -316,7 +317,7 @@
 		progress: "Updating go.sum",
 	}, func(ctx context.Context, deps commandDeps) error {
 		for _, uri := range args.URIs {
-			snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind)
+			snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, file.UnknownKind)
 			defer release()
 			if !ok {
 				return err
@@ -338,7 +339,7 @@
 		progress:    "Running go mod tidy",
 	}, func(ctx context.Context, deps commandDeps) error {
 		for _, uri := range args.URIs {
-			snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind)
+			snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, file.UnknownKind)
 			defer release()
 			if !ok {
 				return err
@@ -382,7 +383,7 @@
 		requireSave: true, // if go.mod isn't saved it could cause a problem
 		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
-		snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, args.URI, source.UnknownKind)
+		snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, args.URI, file.UnknownKind)
 		defer release()
 		if !ok {
 			return err
diff --git a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go
index 558a67a..2c121a9 100644
--- a/gopls/internal/lsp/completion.go
+++ b/gopls/internal/lsp/completion.go
@@ -9,6 +9,7 @@
 	"fmt"
 	"strings"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/lsp/source/completion"
@@ -28,7 +29,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.completion", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
@@ -36,17 +37,17 @@
 	var candidates []completion.CompletionItem
 	var surrounding *completion.Selection
 	switch snapshot.FileKind(fh) {
-	case source.Go:
+	case file.Go:
 		candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context)
-	case source.Mod:
+	case file.Mod:
 		candidates, surrounding = nil, nil
-	case source.Work:
+	case file.Work:
 		cl, err := work.Completion(ctx, snapshot, fh, params.Position)
 		if err != nil {
 			break
 		}
 		return cl, nil
-	case source.Tmpl:
+	case file.Tmpl:
 		var cl *protocol.CompletionList
 		cl, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context)
 		if err != nil {
diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go
index f57b833..9609e0c 100644
--- a/gopls/internal/lsp/debug/serve.go
+++ b/gopls/internal/lsp/debug/serve.go
@@ -322,7 +322,7 @@
 	}
 	for _, o := range s.Overlays() {
 		// TODO(adonovan): understand and document this comparison.
-		if o.FileIdentity().Hash.String() == identifier {
+		if o.Identity().Hash.String() == identifier {
 			return o
 		}
 	}
@@ -836,7 +836,7 @@
 {{$session := .}}
 <ul>{{range .Overlays}}
 <li>
-<a href="/file/{{$session.ID}}/{{.FileIdentity.Hash}}">{{.FileIdentity.URI}}</a>
+<a href="/file/{{$session.ID}}/{{.Identity.Hash}}">{{.Identity.URI}}</a>
 </li>{{end}}</ul>
 {{end}}
 `))
@@ -850,11 +850,11 @@
 `))
 
 var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
-{{define "title"}}Overlay {{.FileIdentity.Hash}}{{end}}
+{{define "title"}}Overlay {{.Identity.Hash}}{{end}}
 {{define "body"}}
 {{with .}}
 	URI: <b>{{.URI}}</b><br>
-	Identifier: <b>{{.FileIdentity.Hash}}</b><br>
+	Identifier: <b>{{.Identity.Hash}}</b><br>
 	Version: <b>{{.Version}}</b><br>
 	Kind: <b>{{.Kind}}</b><br>
 {{end}}
diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go
index 5eb5062..bdbf45a 100644
--- a/gopls/internal/lsp/definition.go
+++ b/gopls/internal/lsp/definition.go
@@ -8,6 +8,7 @@
 	"context"
 	"fmt"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/lsp/template"
@@ -26,15 +27,15 @@
 	defer done()
 
 	// TODO(rfindley): definition requests should be multiplexed across all views.
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
 	}
 	switch kind := snapshot.FileKind(fh); kind {
-	case source.Tmpl:
+	case file.Tmpl:
 		return template.Definition(snapshot, fh, params.Position)
-	case source.Go:
+	case file.Go:
 		return source.Definition(ctx, snapshot, fh, params.Position)
 	default:
 		return nil, fmt.Errorf("can't find definitions for file type %s", kind)
@@ -46,13 +47,13 @@
 	defer done()
 
 	// TODO(rfindley): type definition requests should be multiplexed across all views.
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
 	}
 	switch kind := snapshot.FileKind(fh); kind {
-	case source.Go:
+	case file.Go:
 		return source.TypeDefinition(ctx, snapshot, fh, params.Position)
 	default:
 		return nil, fmt.Errorf("can't find type definitions for file type %s", kind)
diff --git a/gopls/internal/lsp/folding_range.go b/gopls/internal/lsp/folding_range.go
index 302463f..db0ec0b 100644
--- a/gopls/internal/lsp/folding_range.go
+++ b/gopls/internal/lsp/folding_range.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
@@ -17,7 +18,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.foldingRange", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
diff --git a/gopls/internal/lsp/format.go b/gopls/internal/lsp/format.go
index 5fd37ca..efb24fd 100644
--- a/gopls/internal/lsp/format.go
+++ b/gopls/internal/lsp/format.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/mod"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -19,17 +20,17 @@
 	ctx, done := event.Start(ctx, "lsp.Server.formatting", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
 	}
 	switch snapshot.FileKind(fh) {
-	case source.Mod:
+	case file.Mod:
 		return mod.Format(ctx, snapshot, fh)
-	case source.Go:
+	case file.Go:
 		return source.Format(ctx, snapshot, fh)
-	case source.Work:
+	case file.Work:
 		return work.Format(ctx, snapshot, fh)
 	}
 	return nil, nil
diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go
index 4ba8fe5..ad9be90 100644
--- a/gopls/internal/lsp/general.go
+++ b/gopls/internal/lsp/general.go
@@ -18,6 +18,7 @@
 	"sync"
 
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/goversion"
 	"golang.org/x/tools/gopls/internal/lsp/debug"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -548,7 +549,7 @@
 // We don't want to return errors for benign conditions like wrong file type,
 // so callers should do if !ok { return err } rather than if err != nil.
 // The returned cleanup function is non-nil even in case of false/error result.
-func (s *server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.FileHandle, bool, func(), error) {
+func (s *server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind file.Kind) (source.Snapshot, file.Handle, bool, func(), error) {
 	uri := pURI
 	if !uri.IsFile() {
 		// Not a file URI. Stop processing the request, but don't return an error.
@@ -567,7 +568,7 @@
 		release()
 		return nil, nil, false, func() {}, err
 	}
-	if expectKind != source.UnknownKind && snapshot.FileKind(fh) != expectKind {
+	if expectKind != file.UnknownKind && snapshot.FileKind(fh) != expectKind {
 		// Wrong kind of file. Nothing to do.
 		release()
 		return nil, nil, false, func() {}, nil
diff --git a/gopls/internal/lsp/highlight.go b/gopls/internal/lsp/highlight.go
index d058578..2a00f3b 100644
--- a/gopls/internal/lsp/highlight.go
+++ b/gopls/internal/lsp/highlight.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/lsp/template"
@@ -18,13 +19,13 @@
 	ctx, done := event.Start(ctx, "lsp.Server.documentHighlight", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
 	}
 
-	if snapshot.FileKind(fh) == source.Tmpl {
+	if snapshot.FileKind(fh) == file.Tmpl {
 		return template.Highlight(ctx, snapshot, fh, params.Position)
 	}
 
diff --git a/gopls/internal/lsp/hover.go b/gopls/internal/lsp/hover.go
index 72811d7..598fa63 100644
--- a/gopls/internal/lsp/hover.go
+++ b/gopls/internal/lsp/hover.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/mod"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -26,19 +27,19 @@
 	ctx, done := event.Start(ctx, "lsp.Server.hover", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
 	}
 	switch snapshot.FileKind(fh) {
-	case source.Mod:
+	case file.Mod:
 		return mod.Hover(ctx, snapshot, fh, params.Position)
-	case source.Go:
+	case file.Go:
 		return source.Hover(ctx, snapshot, fh, params.Position)
-	case source.Tmpl:
+	case file.Tmpl:
 		return template.Hover(ctx, snapshot, fh, params.Position)
-	case source.Work:
+	case file.Work:
 		return work.Hover(ctx, snapshot, fh, params.Position)
 	}
 	return nil, nil
diff --git a/gopls/internal/lsp/implementation.go b/gopls/internal/lsp/implementation.go
index f3e9bbe..4771f79 100644
--- a/gopls/internal/lsp/implementation.go
+++ b/gopls/internal/lsp/implementation.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/telemetry"
@@ -23,7 +24,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.implementation", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
diff --git a/gopls/internal/lsp/inlay_hint.go b/gopls/internal/lsp/inlay_hint.go
index 2496b88..3c7ba14 100644
--- a/gopls/internal/lsp/inlay_hint.go
+++ b/gopls/internal/lsp/inlay_hint.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/mod"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -18,15 +19,15 @@
 	ctx, done := event.Start(ctx, "lsp.Server.inlayHint", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
 	}
 	switch snapshot.FileKind(fh) {
-	case source.Mod:
+	case file.Mod:
 		return mod.InlayHint(ctx, snapshot, fh, params.Range)
-	case source.Go:
+	case file.Go:
 		return source.InlayHint(ctx, snapshot, fh, params.Range)
 	}
 	return nil, nil
diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go
index 095c66a..cbc11e9 100644
--- a/gopls/internal/lsp/link.go
+++ b/gopls/internal/lsp/link.go
@@ -16,6 +16,7 @@
 	"sync"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -27,15 +28,15 @@
 	ctx, done := event.Start(ctx, "lsp.Server.documentLink")
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
 	}
 	switch snapshot.FileKind(fh) {
-	case source.Mod:
+	case file.Mod:
 		links, err = modLinks(ctx, snapshot, fh)
-	case source.Go:
+	case file.Go:
 		links, err = goLinks(ctx, snapshot, fh)
 	}
 	// Don't return errors for document links.
@@ -46,7 +47,7 @@
 	return links, nil
 }
 
-func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
+func modLinks(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]protocol.DocumentLink, error) {
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		return nil, err
@@ -102,7 +103,7 @@
 }
 
 // goLinks returns the set of hyperlink annotations for the specified Go file.
-func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
+func goLinks(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]protocol.DocumentLink, error) {
 
 	pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull)
 	if err != nil {
diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go
index 9a2a623..e53d586 100644
--- a/gopls/internal/lsp/mod/code_lens.go
+++ b/gopls/internal/lsp/mod/code_lens.go
@@ -11,6 +11,7 @@
 	"path/filepath"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -26,7 +27,7 @@
 	}
 }
 
-func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
+func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil || pm.File == nil {
 		return nil, err
@@ -87,7 +88,7 @@
 	}...), nil
 }
 
-func tidyLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
+func tidyLens(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil || pm.File == nil {
 		return nil, err
@@ -107,7 +108,7 @@
 	}}, nil
 }
 
-func vendorLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
+func vendorLens(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil || pm.File == nil {
 		return nil, err
@@ -135,7 +136,7 @@
 	return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil
 }
 
-func moduleStmtRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Range, error) {
+func moduleStmtRange(fh file.Handle, pm *source.ParsedModule) (protocol.Range, error) {
 	if pm.File == nil || pm.File.Module == nil || pm.File.Module.Syntax == nil {
 		return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI())
 	}
@@ -145,7 +146,7 @@
 
 // firstRequireRange returns the range for the first "require" in the given
 // go.mod file. This is either a require block or an individual require line.
-func firstRequireRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Range, error) {
+func firstRequireRange(fh file.Handle, pm *source.ParsedModule) (protocol.Range, error) {
 	if len(pm.File.Require) == 0 {
 		return protocol.Range{}, fmt.Errorf("no requires in the file %s", fh.URI())
 	}
@@ -164,7 +165,7 @@
 	return pm.Mapper.OffsetRange(start.Byte, end.Byte)
 }
 
-func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
+func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil || pm.File == nil {
 		return nil, err
diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go
index 1f2d7fc..29220d5 100644
--- a/gopls/internal/lsp/mod/diagnostics.go
+++ b/gopls/internal/lsp/mod/diagnostics.go
@@ -17,6 +17,7 @@
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/semver"
 	"golang.org/x/sync/errgroup"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -58,7 +59,7 @@
 	return collectDiagnostics(ctx, snapshot, ModVulnerabilityDiagnostics)
 }
 
-func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[protocol.DocumentURI][]*source.Diagnostic, error) {
+func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, file.Handle) ([]*source.Diagnostic, error)) (map[protocol.DocumentURI][]*source.Diagnostic, error) {
 	g, ctx := errgroup.WithContext(ctx)
 	cpulimit := runtime.GOMAXPROCS(0)
 	g.SetLimit(cpulimit)
@@ -93,7 +94,7 @@
 }
 
 // ModParseDiagnostics reports diagnostics from parsing the mod file.
-func ModParseDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (diagnostics []*source.Diagnostic, err error) {
+func ModParseDiagnostics(ctx context.Context, snapshot source.Snapshot, fh file.Handle) (diagnostics []*source.Diagnostic, err error) {
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		if pm == nil || len(pm.ParseErrors) == 0 {
@@ -105,7 +106,7 @@
 }
 
 // ModTidyDiagnostics reports diagnostics from running go mod tidy.
-func ModTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (diagnostics []*source.Diagnostic, err error) {
+func ModTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, fh file.Handle) (diagnostics []*source.Diagnostic, err error) {
 	pm, err := snapshot.ParseMod(ctx, fh) // memoized
 	if err != nil {
 		return nil, nil // errors reported by ModDiagnostics above
@@ -128,7 +129,7 @@
 
 // ModUpgradeDiagnostics adds upgrade quick fixes for individual modules if the upgrades
 // are recorded in the view.
-func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (upgradeDiagnostics []*source.Diagnostic, err error) {
+func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh file.Handle) (upgradeDiagnostics []*source.Diagnostic, err error) {
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		// Don't return an error if there are parse error diagnostics to be shown, but also do not
@@ -176,7 +177,7 @@
 
 // ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules
 // if the vulnerability is recorded in the view.
-func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (vulnDiagnostics []*source.Diagnostic, err error) {
+func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, fh file.Handle) (vulnDiagnostics []*source.Diagnostic, err error) {
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
 		// Don't return an error if there are parse error diagnostics to be shown, but also do not
@@ -495,7 +496,7 @@
 	return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vulnID)
 }
 
-func getUpgradeCodeAction(fh source.FileHandle, req *modfile.Require, version string) (protocol.Command, error) {
+func getUpgradeCodeAction(fh file.Handle, req *modfile.Require, version string) (protocol.Command, error) {
 	cmd, err := command.NewUpgradeDependencyCommand(upgradeTitle(version), command.DependencyArgs{
 		URI:        fh.URI(),
 		AddRequire: false,
diff --git a/gopls/internal/lsp/mod/format.go b/gopls/internal/lsp/mod/format.go
index daa12da..b04baa2 100644
--- a/gopls/internal/lsp/mod/format.go
+++ b/gopls/internal/lsp/mod/format.go
@@ -7,12 +7,13 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
 )
 
-func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) {
+func Format(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]protocol.TextEdit, error) {
 	ctx, done := event.Start(ctx, "mod.Format")
 	defer done()
 
diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go
index fc1705b..9f1d621 100644
--- a/gopls/internal/lsp/mod/hover.go
+++ b/gopls/internal/lsp/mod/hover.go
@@ -13,6 +13,7 @@
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/semver"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/vulncheck"
@@ -21,7 +22,7 @@
 	"golang.org/x/tools/internal/event"
 )
 
-func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
+func Hover(ctx context.Context, snapshot source.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
 	var found bool
 	for _, uri := range snapshot.ModFiles() {
 		if fh.URI() == uri {
@@ -55,7 +56,7 @@
 	return hoverOnRequireStatement(ctx, pm, offset, snapshot, fh)
 }
 
-func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh source.FileHandle) (*protocol.Hover, error) {
+func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh file.Handle) (*protocol.Hover, error) {
 	// Confirm that the cursor is at the position of a require statement.
 	var req *modfile.Require
 	var startOffset, endOffset int
@@ -126,7 +127,7 @@
 	}, nil
 }
 
-func hoverOnModuleStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh source.FileHandle) (*protocol.Hover, bool) {
+func hoverOnModuleStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh file.Handle) (*protocol.Hover, bool) {
 	module := pm.File.Module
 	if module == nil {
 		return nil, false // no module stmt
diff --git a/gopls/internal/lsp/mod/inlayhint.go b/gopls/internal/lsp/mod/inlayhint.go
index f522365..2fda62f 100644
--- a/gopls/internal/lsp/mod/inlayhint.go
+++ b/gopls/internal/lsp/mod/inlayhint.go
@@ -8,11 +8,12 @@
 	"fmt"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 )
 
-func InlayHint(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, rng protocol.Range) ([]protocol.InlayHint, error) {
+func InlayHint(ctx context.Context, snapshot source.Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.InlayHint, error) {
 	// Inlay hints are enabled if the client supports them.
 	pm, err := snapshot.ParseMod(ctx, fh)
 	if err != nil {
diff --git a/gopls/internal/lsp/references.go b/gopls/internal/lsp/references.go
index d132da1..03e749b 100644
--- a/gopls/internal/lsp/references.go
+++ b/gopls/internal/lsp/references.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/lsp/template"
@@ -24,12 +25,12 @@
 	ctx, done := event.Start(ctx, "lsp.Server.references", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
 	}
-	if snapshot.FileKind(fh) == source.Tmpl {
+	if snapshot.FileKind(fh) == file.Tmpl {
 		return template.References(ctx, snapshot, fh, params)
 	}
 	return source.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration)
diff --git a/gopls/internal/lsp/rename.go b/gopls/internal/lsp/rename.go
index c693507..41473a5 100644
--- a/gopls/internal/lsp/rename.go
+++ b/gopls/internal/lsp/rename.go
@@ -8,6 +8,7 @@
 	"context"
 	"path/filepath"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
@@ -18,7 +19,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.rename", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
@@ -66,7 +67,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.prepareRename", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
diff --git a/gopls/internal/lsp/selection_range.go b/gopls/internal/lsp/selection_range.go
index a0011d1..08651dc 100644
--- a/gopls/internal/lsp/selection_range.go
+++ b/gopls/internal/lsp/selection_range.go
@@ -8,6 +8,7 @@
 	"context"
 
 	"golang.org/x/tools/go/ast/astutil"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
@@ -28,7 +29,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.selectionRange")
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go
index 198c305..ff8ccc0 100644
--- a/gopls/internal/lsp/semantic.go
+++ b/gopls/internal/lsp/semantic.go
@@ -18,6 +18,7 @@
 	"strings"
 	"time"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -50,7 +51,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.semanticTokens", tag.URI.Of(td.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, td.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, td.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return nil, err
@@ -62,7 +63,7 @@
 	}
 
 	kind := snapshot.FileKind(fh)
-	if kind == source.Tmpl {
+	if kind == file.Tmpl {
 		// this is a little cumbersome to avoid both exporting 'encoded' and its methods
 		// and to avoid import cycles
 		e := &encoded{
@@ -80,7 +81,7 @@
 		}
 		return template.SemanticTokens(ctx, snapshot, fh.URI(), add, data)
 	}
-	if kind != source.Go {
+	if kind != file.Go {
 		return nil, nil
 	}
 	pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI())
diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go
index 8a6fd18..9b7304a 100644
--- a/gopls/internal/lsp/server.go
+++ b/gopls/internal/lsp/server.go
@@ -18,6 +18,7 @@
 	"os"
 	"sync"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/cache"
 	"golang.org/x/tools/gopls/internal/lsp/progress"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -143,8 +144,8 @@
 		// TODO(adonovan): opt: parallelize FileDiagnostics(URI...), either
 		// by calling it in multiple goroutines or, better, by making
 		// the relevant APIs accept a set of URIs/packages.
-		for _, file := range paramMap["files"].([]interface{}) {
-			snapshot, fh, ok, release, err := s.beginFileRequest(ctx, protocol.DocumentURI(file.(string)), source.UnknownKind)
+		for _, f := range paramMap["files"].([]interface{}) {
+			snapshot, fh, ok, release, err := s.beginFileRequest(ctx, protocol.DocumentURI(f.(string)), file.UnknownKind)
 			defer release()
 			if !ok {
 				return nil, err
@@ -180,7 +181,7 @@
 // efficient to compute the set of packages and TypeCheck and
 // Analyze them all at once. Or instead support textDocument/diagnostic
 // (golang/go#60122).
-func (s *server) diagnoseFile(ctx context.Context, snapshot source.Snapshot, uri protocol.DocumentURI) (source.FileHandle, []*source.Diagnostic, error) {
+func (s *server) diagnoseFile(ctx context.Context, snapshot source.Snapshot, uri protocol.DocumentURI) (file.Handle, []*source.Diagnostic, error) {
 	fh, err := snapshot.ReadFile(ctx, uri)
 	if err != nil {
 		return nil, nil, err
diff --git a/gopls/internal/lsp/signature_help.go b/gopls/internal/lsp/signature_help.go
index 4fc4e88..7b699b7 100644
--- a/gopls/internal/lsp/signature_help.go
+++ b/gopls/internal/lsp/signature_help.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
@@ -17,7 +18,7 @@
 	ctx, done := event.Start(ctx, "lsp.Server.signatureHelp", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.Go)
 	defer release()
 	if !ok {
 		return nil, err
diff --git a/gopls/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go
index cd8ec7a..8930e8f 100644
--- a/gopls/internal/lsp/source/add_import.go
+++ b/gopls/internal/lsp/source/add_import.go
@@ -7,12 +7,13 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/internal/imports"
 )
 
 // AddImport adds a single import statement to the given file
-func AddImport(ctx context.Context, snapshot Snapshot, fh FileHandle, importPath string) ([]protocol.TextEdit, error) {
+func AddImport(ctx context.Context, snapshot Snapshot, fh file.Handle, importPath string) ([]protocol.TextEdit, error) {
 	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go
index cdaa2c6..606ae30 100644
--- a/gopls/internal/lsp/source/call_hierarchy.go
+++ b/gopls/internal/lsp/source/call_hierarchy.go
@@ -15,6 +15,7 @@
 
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/internal/event"
@@ -22,7 +23,7 @@
 )
 
 // PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file.
-func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.CallHierarchyItem, error) {
+func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.CallHierarchyItem, error) {
 	ctx, done := event.Start(ctx, "source.PrepareCallHierarchy")
 	defer done()
 
@@ -63,7 +64,7 @@
 }
 
 // IncomingCalls returns an array of CallHierarchyIncomingCall for a file and the position within the file.
-func IncomingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyIncomingCall, error) {
+func IncomingCalls(ctx context.Context, snapshot Snapshot, fh file.Handle, pos protocol.Position) ([]protocol.CallHierarchyIncomingCall, error) {
 	ctx, done := event.Start(ctx, "source.IncomingCalls")
 	defer done()
 
@@ -177,7 +178,7 @@
 }
 
 // OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file.
-func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) {
+func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) {
 	ctx, done := event.Start(ctx, "source.OutgoingCalls")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/change_quote.go b/gopls/internal/lsp/source/change_quote.go
index a4dc132..6bb7f37 100644
--- a/gopls/internal/lsp/source/change_quote.go
+++ b/gopls/internal/lsp/source/change_quote.go
@@ -12,6 +12,7 @@
 
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/internal/diff"
@@ -23,7 +24,7 @@
 // Only the following conditions are true, the action in result is valid
 //   - [start, end) is enclosed by a string literal
 //   - if the string is interpreted string, need check whether the convert is allowed
-func ConvertStringLiteral(pgf *ParsedGoFile, fh FileHandle, rng protocol.Range) (protocol.CodeAction, bool) {
+func ConvertStringLiteral(pgf *ParsedGoFile, fh file.Handle, rng protocol.Range) (protocol.CodeAction, bool) {
 	startPos, endPos, err := pgf.RangePos(rng)
 	if err != nil {
 		bug.Reportf("(file=%v).RangePos(%v) failed: %v", pgf.URI, rng, err)
diff --git a/gopls/internal/lsp/source/change_signature.go b/gopls/internal/lsp/source/change_signature.go
index 1864092..dcd1185 100644
--- a/gopls/internal/lsp/source/change_signature.go
+++ b/gopls/internal/lsp/source/change_signature.go
@@ -17,6 +17,7 @@
 
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/imports"
@@ -37,7 +38,7 @@
 //   - Improve the extra newlines in output.
 //   - Stream type checking via ForEachPackage.
 //   - Avoid unnecessary additional type checking.
-func RemoveUnusedParameter(ctx context.Context, fh FileHandle, rng protocol.Range, snapshot Snapshot) ([]protocol.DocumentChanges, error) {
+func RemoveUnusedParameter(ctx context.Context, fh file.Handle, rng protocol.Range, snapshot Snapshot) ([]protocol.DocumentChanges, error) {
 	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go
index 7fb3559..28495f0 100644
--- a/gopls/internal/lsp/source/code_lens.go
+++ b/gopls/internal/lsp/source/code_lens.go
@@ -13,11 +13,12 @@
 	"regexp"
 	"strings"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/command"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 )
 
-type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error)
+type LensFunc func(context.Context, Snapshot, file.Handle) ([]protocol.CodeLens, error)
 
 // LensFuncs returns the supported lensFuncs for Go files.
 func LensFuncs() map[command.Command]LensFunc {
@@ -34,7 +35,7 @@
 	benchmarkRe = regexp.MustCompile(`^Benchmark([^a-z]|$)`)
 )
 
-func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
+func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
 	var codeLens []protocol.CodeLens
 
 	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
@@ -165,7 +166,7 @@
 	return namedObj.Id() == paramID
 }
 
-func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
+func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
 	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
 	if err != nil {
 		return nil, err
@@ -199,7 +200,7 @@
 	return nil, nil
 }
 
-func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
+func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
 	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
 	if err != nil {
 		return nil, err
@@ -225,7 +226,7 @@
 	return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil
 }
 
-func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
+func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
 	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go
index 42c369f..f0cd6d0 100644
--- a/gopls/internal/lsp/source/completion/completion.go
+++ b/gopls/internal/lsp/source/completion/completion.go
@@ -28,6 +28,7 @@
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/ast/astutil"
 	goplsastutil "golang.org/x/tools/gopls/internal/astutil"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/gopls/internal/lsp/snippet"
@@ -177,7 +178,7 @@
 	completionContext completionContext
 
 	// fh is a handle to the file associated with this completion request.
-	fh source.FileHandle
+	fh file.Handle
 
 	// filename is the name of the file associated with this completion request.
 	filename string
@@ -442,7 +443,7 @@
 // The selection is computed based on the preceding identifier and can be used by
 // the client to score the quality of the completion. For instance, some clients
 // may tolerate imperfect matches as valid completion results, since users may make typos.
-func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, protoPos protocol.Position, protoContext protocol.CompletionContext) ([]CompletionItem, *Selection, error) {
+func Completion(ctx context.Context, snapshot source.Snapshot, fh file.Handle, protoPos protocol.Position, protoContext protocol.CompletionContext) ([]CompletionItem, *Selection, error) {
 	ctx, done := event.Start(ctx, "completion.Completion")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go
index 7c69c8f..d4aacf4 100644
--- a/gopls/internal/lsp/source/completion/package.go
+++ b/gopls/internal/lsp/source/completion/package.go
@@ -18,6 +18,7 @@
 	"strings"
 	"unicode"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/gopls/internal/lsp/source"
@@ -26,7 +27,7 @@
 
 // packageClauseCompletions offers completions for a package declaration when
 // one is not present in the given file.
-func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) ([]CompletionItem, *Selection, error) {
+func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh file.Handle, position protocol.Position) ([]CompletionItem, *Selection, error) {
 	// We know that the AST for this file will be empty due to the missing
 	// package declaration, but parse it anyway to get a mapper.
 	// TODO(adonovan): opt: there's no need to parse just to get a mapper.
diff --git a/gopls/internal/lsp/source/definition.go b/gopls/internal/lsp/source/definition.go
index 158b3b1..7ed2860 100644
--- a/gopls/internal/lsp/source/definition.go
+++ b/gopls/internal/lsp/source/definition.go
@@ -13,12 +13,13 @@
 	"go/types"
 
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/internal/event"
 )
 
 // Definition handles the textDocument/definition request for Go files.
-func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Location, error) {
+func Definition(ctx context.Context, snapshot Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) {
 	ctx, done := event.Start(ctx, "source.Definition")
 	defer done()
 
@@ -276,7 +277,7 @@
 
 // TODO(rfindley): avoid the duplicate column mapping here, by associating a
 // column mapper with each file handle.
-func mapPosition(ctx context.Context, fset *token.FileSet, s FileSource, start, end token.Pos) (protocol.Location, error) {
+func mapPosition(ctx context.Context, fset *token.FileSet, s file.Source, start, end token.Pos) (protocol.Location, error) {
 	file := fset.File(start)
 	uri := protocol.URIFromPath(file.Name())
 	fh, err := s.ReadFile(ctx, uri)
diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go
index 53ed3a3..314403c 100644
--- a/gopls/internal/lsp/source/fix.go
+++ b/gopls/internal/lsp/source/fix.go
@@ -13,6 +13,7 @@
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/embeddirective"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname"
@@ -29,7 +30,7 @@
 	// TODO(rfindley): the signature of suggestedFixFunc should probably accept
 	// (context.Context, Snapshot, protocol.Diagnostic). No reason for us to
 	// encode as a (URI, Range) pair when we have the protocol type.
-	suggestedFixFunc func(context.Context, Snapshot, FileHandle, protocol.Range) ([]protocol.TextDocumentEdit, error)
+	suggestedFixFunc func(context.Context, Snapshot, file.Handle, protocol.Range) ([]protocol.TextDocumentEdit, error)
 	suggestedFixer   struct {
 		// fixesDiagnostic reports if a diagnostic from the analyzer can be fixed
 		// by Fix. If nil then all diagnostics from the analyzer are assumed to be
@@ -86,7 +87,7 @@
 
 // singleFile calls analyzers that expect inputs for a single file.
 func singleFile(sf singleFileFixFunc) suggestedFixFunc {
-	return func(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
+	return func(ctx context.Context, snapshot Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
 		pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
 		if err != nil {
 			return nil, err
@@ -133,7 +134,7 @@
 
 // ApplyFix applies the command's suggested fix to the given file and
 // range, returning the resulting edits.
-func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh FileHandle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
+func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
 	fixer, ok := suggestedFixes[fix]
 	if !ok {
 		return nil, fmt.Errorf("no suggested fix function for %s", fix)
@@ -198,7 +199,7 @@
 }
 
 // addEmbedImport adds a missing embed "embed" import with blank name.
-func addEmbedImport(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol.Range) ([]protocol.TextDocumentEdit, error) {
+func addEmbedImport(ctx context.Context, snapshot Snapshot, fh file.Handle, _ protocol.Range) ([]protocol.TextDocumentEdit, error) {
 	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
 	if err != nil {
 		return nil, fmt.Errorf("narrow pkg: %w", err)
diff --git a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go
index 9f63c77..b6f7faa 100644
--- a/gopls/internal/lsp/source/folding_range.go
+++ b/gopls/internal/lsp/source/folding_range.go
@@ -12,6 +12,7 @@
 	"strings"
 
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 )
@@ -23,7 +24,7 @@
 }
 
 // FoldingRange gets all of the folding range for f.
-func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) {
+func FoldingRange(ctx context.Context, snapshot Snapshot, fh file.Handle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) {
 	// TODO(suzmue): consider limiting the number of folding ranges returned, and
 	// implement a way to prioritize folding ranges in that case.
 	pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go
index 3646a24..9f3597c 100644
--- a/gopls/internal/lsp/source/format.go
+++ b/gopls/internal/lsp/source/format.go
@@ -16,6 +16,7 @@
 	"strings"
 	"text/scanner"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/internal/diff"
@@ -25,7 +26,7 @@
 )
 
 // Format formats a file with a given range.
-func Format(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.TextEdit, error) {
+func Format(ctx context.Context, snapshot Snapshot, fh file.Handle) ([]protocol.TextEdit, error) {
 	ctx, done := event.Start(ctx, "source.Format")
 	defer done()
 
@@ -88,7 +89,7 @@
 	return computeTextEdits(ctx, snapshot, pgf, formatted)
 }
 
-func formatSource(ctx context.Context, fh FileHandle) ([]byte, error) {
+func formatSource(ctx context.Context, fh file.Handle) ([]byte, error) {
 	_, done := event.Start(ctx, "source.formatSource")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go
index adfc659..05cdc41 100644
--- a/gopls/internal/lsp/source/highlight.go
+++ b/gopls/internal/lsp/source/highlight.go
@@ -12,11 +12,12 @@
 	"go/types"
 
 	"golang.org/x/tools/go/ast/astutil"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/internal/event"
 )
 
-func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Range, error) {
+func Highlight(ctx context.Context, snapshot Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Range, error) {
 	ctx, done := event.Start(ctx, "source.Highlight")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go
index 4c037e3..5e38868 100644
--- a/gopls/internal/lsp/source/hover.go
+++ b/gopls/internal/lsp/source/hover.go
@@ -25,6 +25,7 @@
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/types/typeutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/internal/event"
@@ -61,7 +62,7 @@
 }
 
 // Hover implements the "textDocument/hover" RPC for Go files.
-func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
+func Hover(ctx context.Context, snapshot Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
 	ctx, done := event.Start(ctx, "source.Hover")
 	defer done()
 
@@ -88,7 +89,7 @@
 // hover computes hover information at the given position. If we do not support
 // hovering at the position, it returns _, nil, nil: an error is only returned
 // if the position is valid but we fail to compute hover information.
-func hover(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) (protocol.Range, *HoverJSON, error) {
+func hover(ctx context.Context, snapshot Snapshot, fh file.Handle, pp protocol.Position) (protocol.Range, *HoverJSON, error) {
 	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
 	if err != nil {
 		return protocol.Range{}, nil, err
@@ -634,7 +635,7 @@
 
 // hoverEmbed computes hover information for a filepath.Match pattern.
 // Assumes that the pattern is relative to the location of fh.
-func hoverEmbed(fh FileHandle, rng protocol.Range, pattern string) (protocol.Range, *HoverJSON, error) {
+func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Range, *HoverJSON, error) {
 	s := &strings.Builder{}
 
 	dir := filepath.Dir(fh.URI().Path())
diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go
index 339daac..69de9ea 100644
--- a/gopls/internal/lsp/source/implementation.go
+++ b/gopls/internal/lsp/source/implementation.go
@@ -18,6 +18,7 @@
 
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/gopls/internal/lsp/source/methodsets"
@@ -45,7 +46,7 @@
 //
 // If the position denotes a method, the computation is applied to its
 // receiver type and then its corresponding methods are returned.
-func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) ([]protocol.Location, error) {
+func Implementation(ctx context.Context, snapshot Snapshot, f file.Handle, pp protocol.Position) ([]protocol.Location, error) {
 	ctx, done := event.Start(ctx, "source.Implementation")
 	defer done()
 
@@ -69,7 +70,7 @@
 	return locs, nil
 }
 
-func implementations(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.Location, error) {
+func implementations(ctx context.Context, snapshot Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.Location, error) {
 	obj, pkg, err := implementsObj(ctx, snapshot, fh.URI(), pp)
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go
index 9a77d16..d82b8e1 100644
--- a/gopls/internal/lsp/source/inlay_hint.go
+++ b/gopls/internal/lsp/source/inlay_hint.go
@@ -13,6 +13,7 @@
 	"go/types"
 	"strings"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/typeparams"
@@ -78,7 +79,7 @@
 	},
 }
 
-func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.InlayHint, error) {
+func InlayHint(ctx context.Context, snapshot Snapshot, fh file.Handle, pRng protocol.Range) ([]protocol.InlayHint, error) {
 	ctx, done := event.Start(ctx, "source.InlayHint")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/inline.go b/gopls/internal/lsp/source/inline.go
index 5b74367..5619b23 100644
--- a/gopls/internal/lsp/source/inline.go
+++ b/gopls/internal/lsp/source/inline.go
@@ -17,6 +17,7 @@
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/types/typeutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/internal/diff"
@@ -57,7 +58,7 @@
 	return call, fn, nil
 }
 
-func inlineCall(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (_ []protocol.TextDocumentEdit, err error) {
+func inlineCall(ctx context.Context, snapshot Snapshot, fh file.Handle, rng protocol.Range) (_ []protocol.TextDocumentEdit, err error) {
 	// Find enclosing static call.
 	callerPkg, callerPGF, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
 	if err != nil {
diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go
index 6e4e198..8f41746 100644
--- a/gopls/internal/lsp/source/known_packages.go
+++ b/gopls/internal/lsp/source/known_packages.go
@@ -13,6 +13,7 @@
 	"sync"
 	"time"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/imports"
 )
@@ -23,7 +24,7 @@
 // all dot-free paths (standard packages) appear before dotful ones.
 //
 // It is part of the gopls.list_known_packages command.
-func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]PackagePath, error) {
+func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh file.Handle) ([]PackagePath, error) {
 	// This algorithm is expressed in terms of Metadata, not Packages,
 	// so it doesn't cause or wait for type checking.
 
diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go
index f2682f1..dfd7da9 100644
--- a/gopls/internal/lsp/source/options.go
+++ b/gopls/internal/lsp/source/options.go
@@ -53,6 +53,7 @@
 	"golang.org/x/tools/go/analysis/passes/unsafeptr"
 	"golang.org/x/tools/go/analysis/passes/unusedresult"
 	"golang.org/x/tools/go/analysis/passes/unusedwrite"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/deprecated"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/embeddirective"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns"
@@ -100,8 +101,8 @@
 				HierarchicalDocumentSymbolSupport:          true,
 			},
 			ServerOptions: ServerOptions{
-				SupportedCodeActions: map[FileKind]map[protocol.CodeActionKind]bool{
-					Go: {
+				SupportedCodeActions: map[file.Kind]map[protocol.CodeActionKind]bool{
+					file.Go: {
 						protocol.SourceFixAll:          true,
 						protocol.SourceOrganizeImports: true,
 						protocol.QuickFix:              true,
@@ -109,13 +110,13 @@
 						protocol.RefactorInline:        true,
 						protocol.RefactorExtract:       true,
 					},
-					Mod: {
+					file.Mod: {
 						protocol.SourceOrganizeImports: true,
 						protocol.QuickFix:              true,
 					},
-					Work: {},
-					Sum:  {},
-					Tmpl: {},
+					file.Work: {},
+					file.Sum:  {},
+					file.Tmpl: {},
 				},
 				SupportedCommands: commands,
 			},
@@ -252,7 +253,7 @@
 // ServerOptions holds LSP-specific configuration that is provided by the
 // server.
 type ServerOptions struct {
-	SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool
+	SupportedCodeActions map[file.Kind]map[protocol.CodeActionKind]bool
 	SupportedCommands    []string
 }
 
diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go
index 295bce1..8030ef3 100644
--- a/gopls/internal/lsp/source/references.go
+++ b/gopls/internal/lsp/source/references.go
@@ -26,6 +26,7 @@
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/types/objectpath"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/gopls/internal/lsp/source/methodsets"
@@ -35,7 +36,7 @@
 // References returns a list of all references (sorted with
 // definitions before uses) to the object denoted by the identifier at
 // the given file/position, searching the entire workspace.
-func References(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position, includeDeclaration bool) ([]protocol.Location, error) {
+func References(ctx context.Context, snapshot Snapshot, fh file.Handle, pp protocol.Position, includeDeclaration bool) ([]protocol.Location, error) {
 	references, err := references(ctx, snapshot, fh, pp, includeDeclaration)
 	if err != nil {
 		return nil, err
@@ -58,7 +59,7 @@
 // references returns a list of all references (sorted with
 // definitions before uses) to the object denoted by the identifier at
 // the given file/position, searching the entire workspace.
-func references(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]reference, error) {
+func references(ctx context.Context, snapshot Snapshot, f file.Handle, pp protocol.Position, includeDeclaration bool) ([]reference, error) {
 	ctx, done := event.Start(ctx, "source.references")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go
index 4075436..936ef6a 100644
--- a/gopls/internal/lsp/source/rename.go
+++ b/gopls/internal/lsp/source/rename.go
@@ -60,6 +60,7 @@
 	"golang.org/x/tools/go/types/objectpath"
 	"golang.org/x/tools/go/types/typeutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
 	"golang.org/x/tools/internal/diff"
@@ -94,7 +95,7 @@
 // The returned usererr is intended to be displayed to the user to explain why
 // the prepare fails. Probably we could eliminate the redundancy in returning
 // two errors, but for now this is done defensively.
-func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) {
+func PrepareRename(ctx context.Context, snapshot Snapshot, f file.Handle, pp protocol.Position) (_ *PrepareItem, usererr, err error) {
 	ctx, done := event.Start(ctx, "source.PrepareRename")
 	defer done()
 
@@ -212,7 +213,7 @@
 // Rename returns a map of TextEdits for each file modified when renaming a
 // given identifier within a package and a boolean value of true for renaming
 // package and false otherwise.
-func Rename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, bool, error) {
+func Rename(ctx context.Context, snapshot Snapshot, f file.Handle, pp protocol.Position, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, bool, error) {
 	ctx, done := event.Start(ctx, "source.Rename")
 	defer done()
 
@@ -286,7 +287,7 @@
 }
 
 // renameOrdinary renames an ordinary (non-package) name throughout the workspace.
-func renameOrdinary(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, newName string) (map[protocol.DocumentURI][]diff.Edit, error) {
+func renameOrdinary(ctx context.Context, snapshot Snapshot, f file.Handle, pp protocol.Position, newName string) (map[protocol.DocumentURI][]diff.Edit, error) {
 	// Type-check the referring package and locate the object(s).
 	//
 	// Unlike NarrowestPackageForFile, this operation prefers the
@@ -637,7 +638,7 @@
 }
 
 // renamePackageName renames package declarations, imports, and go.mod files.
-func renamePackageName(ctx context.Context, s Snapshot, f FileHandle, newName PackageName) (map[protocol.DocumentURI][]diff.Edit, error) {
+func renamePackageName(ctx context.Context, s Snapshot, f file.Handle, newName PackageName) (map[protocol.DocumentURI][]diff.Edit, error) {
 	// Rename the package decl and all imports.
 	renamingEdits, err := renamePackage(ctx, s, f, newName)
 	if err != nil {
@@ -740,7 +741,7 @@
 // It updates package clauses and import paths for the renamed package as well
 // as any other packages affected by the directory renaming among all packages
 // known to the snapshot.
-func renamePackage(ctx context.Context, s Snapshot, f FileHandle, newName PackageName) (map[protocol.DocumentURI][]diff.Edit, error) {
+func renamePackage(ctx context.Context, s Snapshot, f file.Handle, newName PackageName) (map[protocol.DocumentURI][]diff.Edit, error) {
 	if strings.HasSuffix(string(newName), "_test") {
 		return nil, fmt.Errorf("cannot rename to _test package")
 	}
@@ -1245,7 +1246,7 @@
 // whether the position ppos lies within it.
 //
 // Note: also used by references.
-func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, ppos protocol.Position) (*ParsedGoFile, bool, error) {
+func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh file.Handle, ppos protocol.Position) (*ParsedGoFile, bool, error) {
 	pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader)
 	if err != nil {
 		return nil, false, err
diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go
index 3d1514a..0c58f0a 100644
--- a/gopls/internal/lsp/source/signature_help.go
+++ b/gopls/internal/lsp/source/signature_help.go
@@ -14,11 +14,12 @@
 
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/internal/event"
 )
 
-func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.SignatureInformation, int, error) {
+func SignatureHelp(ctx context.Context, snapshot Snapshot, fh file.Handle, position protocol.Position) (*protocol.SignatureInformation, int, error) {
 	ctx, done := event.Start(ctx, "source.SignatureHelp")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go
index 64cec26..d93712c 100644
--- a/gopls/internal/lsp/source/stub.go
+++ b/gopls/internal/lsp/source/stub.go
@@ -19,6 +19,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
@@ -30,7 +31,7 @@
 // stubSuggestedFixFunc returns a suggested fix to declare the missing
 // methods of the concrete type that is assigned to an interface type
 // at the cursor position.
-func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
+func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
 	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
 	if err != nil {
 		return nil, fmt.Errorf("GetTypedFile: %w", err)
diff --git a/gopls/internal/lsp/source/symbols.go b/gopls/internal/lsp/source/symbols.go
index a5c015e..8369043 100644
--- a/gopls/internal/lsp/source/symbols.go
+++ b/gopls/internal/lsp/source/symbols.go
@@ -11,11 +11,12 @@
 	"go/token"
 	"go/types"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/internal/event"
 )
 
-func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.DocumentSymbol, error) {
+func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh file.Handle) ([]protocol.DocumentSymbol, error) {
 	ctx, done := event.Start(ctx, "source.DocumentSymbols")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/type_definition.go b/gopls/internal/lsp/source/type_definition.go
index 6c26b16..2a09c59 100644
--- a/gopls/internal/lsp/source/type_definition.go
+++ b/gopls/internal/lsp/source/type_definition.go
@@ -10,12 +10,13 @@
 	"go/token"
 
 	"golang.org/x/tools/gopls/internal/bug"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/internal/event"
 )
 
 // TypeDefinition handles the textDocument/typeDefinition request for Go files.
-func TypeDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Location, error) {
+func TypeDefinition(ctx context.Context, snapshot Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) {
 	ctx, done := event.Start(ctx, "source.TypeDefinition")
 	defer done()
 
diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go
index 23510e9..8c8f5f3 100644
--- a/gopls/internal/lsp/source/util.go
+++ b/gopls/internal/lsp/source/util.go
@@ -81,25 +81,6 @@
 //	https://golang.org/s/generatedcode
 var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
 
-// FileKindForLang returns the file kind associated with the given language ID,
-// or UnknownKind if the language ID is not recognized.
-func FileKindForLang(langID string) FileKind {
-	switch langID {
-	case "go":
-		return Go
-	case "go.mod":
-		return Mod
-	case "go.sum":
-		return Sum
-	case "tmpl", "gotmpl":
-		return Tmpl
-	case "go.work":
-		return Work
-	default:
-		return UnknownKind
-	}
-}
-
 // nodeAtPos returns the index and the node whose position is contained inside
 // the node list.
 func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) {
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index fef6064..8620c42 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -7,7 +7,6 @@
 import (
 	"bytes"
 	"context"
-	"crypto/sha256"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -22,6 +21,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/go/types/objectpath"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/progress"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/safetoken"
@@ -64,7 +64,7 @@
 	// language different from the file name heuristic, e.g. that
 	// an .html file actually contains Go "html/template" syntax,
 	// or even that a .go file contains Python.
-	FileKind(FileHandle) FileKind
+	FileKind(file.Handle) file.Kind
 
 	// Options returns the options associated with this snapshot.
 	Options() *Options
@@ -79,12 +79,12 @@
 	// A Snapshot is a caching implementation of FileSource whose
 	// ReadFile method returns consistent information about the existence
 	// and content of each file throughout its lifetime.
-	FileSource
+	file.Source
 
 	// FindFile returns the FileHandle for the given URI, if it is already
 	// in the given snapshot.
 	// TODO(adonovan): delete this operation; use ReadFile instead.
-	FindFile(uri protocol.DocumentURI) FileHandle
+	FindFile(uri protocol.DocumentURI) file.Handle
 
 	// AwaitInitialized waits until the snapshot's view is initialized.
 	AwaitInitialized(ctx context.Context)
@@ -97,12 +97,12 @@
 	IgnoredFile(uri protocol.DocumentURI) bool
 
 	// Templates returns the .tmpl files
-	Templates() map[protocol.DocumentURI]FileHandle
+	Templates() map[protocol.DocumentURI]file.Handle
 
 	// ParseGo returns the parsed AST for the file.
 	// If the file is not available, returns nil and an error.
 	// Position information is added to FileSet().
-	ParseGo(ctx context.Context, fh FileHandle, mode parser.Mode) (*ParsedGoFile, error)
+	ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode) (*ParsedGoFile, error)
 
 	// Analyze runs the specified analyzers on the given packages at this snapshot.
 	//
@@ -135,11 +135,11 @@
 	ModFiles() []protocol.DocumentURI
 
 	// ParseMod is used to parse go.mod files.
-	ParseMod(ctx context.Context, fh FileHandle) (*ParsedModule, error)
+	ParseMod(ctx context.Context, fh file.Handle) (*ParsedModule, error)
 
 	// ModWhy returns the results of `go mod why` for the module specified by
 	// the given go.mod file.
-	ModWhy(ctx context.Context, fh FileHandle) (map[string]string, error)
+	ModWhy(ctx context.Context, fh file.Handle) (map[string]string, error)
 
 	// ModTidy returns the results of `go mod tidy` for the module specified by
 	// the given go.mod file.
@@ -156,7 +156,7 @@
 	WorkFile() protocol.DocumentURI
 
 	// ParseWork is used to parse go.work files.
-	ParseWork(ctx context.Context, fh FileHandle) (*ParsedWorkFile, error)
+	ParseWork(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error)
 
 	// BuiltinFile returns information about the special builtin package.
 	BuiltinFile(ctx context.Context) (*ParsedGoFile, error)
@@ -432,21 +432,11 @@
 // By far the most common of these is a change to file state, but a query of
 // module upgrade information or vulnerabilities also affects gopls' behavior.
 type StateChange struct {
-	Files          map[protocol.DocumentURI]FileHandle
+	Files          map[protocol.DocumentURI]file.Handle
 	ModuleUpgrades map[protocol.DocumentURI]map[string]string
 	Vulns          map[protocol.DocumentURI]*vulncheck.Result
 }
 
-// A FileSource maps URIs to FileHandles.
-type FileSource interface {
-	// ReadFile returns the FileHandle for a given URI, either by
-	// reading the content of the file or by obtaining it from a cache.
-	//
-	// Invariant: ReadFile must only return an error in the case of context
-	// cancellation. If ctx.Err() is nil, the resulting error must also be nil.
-	ReadFile(ctx context.Context, uri protocol.DocumentURI) (FileHandle, error)
-}
-
 // A MetadataSource maps package IDs to metadata.
 //
 // TODO(rfindley): replace this with a concrete metadata graph, once it is
@@ -713,60 +703,12 @@
 	*pmetas = res
 }
 
-var ErrViewExists = errors.New("view already exists for session")
-
-// FileModification represents a modification to a file.
-type FileModification struct {
-	URI    protocol.DocumentURI
-	Action FileAction
-
-	// OnDisk is true if a watched file is changed on disk.
-	// If true, Version will be -1 and Text will be nil.
-	OnDisk bool
-
-	// Version will be -1 and Text will be nil when they are not supplied,
-	// specifically on textDocument/didClose and for on-disk changes.
-	Version int32
-	Text    []byte
-
-	// LanguageID is only sent from the language client on textDocument/didOpen.
-	LanguageID string
-}
-
-type FileAction int
-
-const (
-	UnknownFileAction = FileAction(iota)
-	Open
-	Change
-	Close
-	Save
-	Create
-	Delete
+var (
+	ErrViewExists            = errors.New("view already exists for session")
+	ErrTmpModfileUnsupported = errors.New("-modfile is unsupported for this Go version")
+	ErrNoModOnDisk           = errors.New("go.mod file is not on disk")
 )
 
-func (a FileAction) String() string {
-	switch a {
-	case Open:
-		return "Open"
-	case Change:
-		return "Change"
-	case Close:
-		return "Close"
-	case Save:
-		return "Save"
-	case Create:
-		return "Create"
-	case Delete:
-		return "Delete"
-	default:
-		return "Unknown"
-	}
-}
-
-var ErrTmpModfileUnsupported = errors.New("-modfile is unsupported for this Go version")
-var ErrNoModOnDisk = errors.New("go.mod file is not on disk")
-
 func IsNonFatalGoModError(err error) bool {
 	return err == ErrTmpModfileUnsupported || err == ErrNoModOnDisk
 }
@@ -784,115 +726,6 @@
 	ParseFull = parser.AllErrors | parser.ParseComments | SkipObjectResolution
 )
 
-// A FileHandle represents the URI, content, hash, and optional
-// version of a file tracked by the LSP session.
-//
-// File content may be provided by the file system (for Saved files)
-// or from an overlay, for open files with unsaved edits.
-// A FileHandle may record an attempt to read a non-existent file,
-// in which case Content returns an error.
-type FileHandle interface {
-	// URI is the URI for this file handle.
-	// TODO(rfindley): this is not actually well-defined. In some cases, there
-	// may be more than one URI that resolve to the same FileHandle. Which one is
-	// this?
-	URI() protocol.DocumentURI
-	// FileIdentity returns a FileIdentity for the file, even if there was an
-	// error reading it.
-	FileIdentity() FileIdentity
-	// SameContentsOnDisk reports whether the file has the same content on disk:
-	// it is false for files open on an editor with unsaved edits.
-	SameContentsOnDisk() bool
-	// Version returns the file version, as defined by the LSP client.
-	// For on-disk file handles, Version returns 0.
-	Version() int32
-	// Content returns the contents of a file.
-	// If the file is not available, returns a nil slice and an error.
-	Content() ([]byte, error)
-}
-
-// A Hash is a cryptographic digest of the contents of a file.
-// (Although at 32B it is larger than a 16B string header, it is smaller
-// and has better locality than the string header + 64B of hex digits.)
-type Hash [sha256.Size]byte
-
-// HashOf returns the hash of some data.
-func HashOf(data []byte) Hash {
-	return Hash(sha256.Sum256(data))
-}
-
-// Hashf returns the hash of a printf-formatted string.
-func Hashf(format string, args ...interface{}) Hash {
-	// Although this looks alloc-heavy, it is faster than using
-	// Fprintf on sha256.New() because the allocations don't escape.
-	return HashOf([]byte(fmt.Sprintf(format, args...)))
-}
-
-// String returns the digest as a string of hex digits.
-func (h Hash) String() string {
-	return fmt.Sprintf("%64x", [sha256.Size]byte(h))
-}
-
-// Less returns true if the given hash is less than the other.
-func (h Hash) Less(other Hash) bool {
-	return bytes.Compare(h[:], other[:]) < 0
-}
-
-// XORWith updates *h to *h XOR h2.
-func (h *Hash) XORWith(h2 Hash) {
-	// Small enough that we don't need crypto/subtle.XORBytes.
-	for i := range h {
-		h[i] ^= h2[i]
-	}
-}
-
-// FileIdentity uniquely identifies a file at a version from a FileSystem.
-type FileIdentity struct {
-	URI  protocol.DocumentURI
-	Hash Hash // digest of file contents
-}
-
-func (id FileIdentity) String() string {
-	return fmt.Sprintf("%s%s", id.URI, id.Hash)
-}
-
-// FileKind describes the kind of the file in question.
-// It can be one of Go,mod, Sum, or Tmpl.
-type FileKind int
-
-const (
-	// UnknownKind is a file type we don't know about.
-	UnknownKind = FileKind(iota)
-
-	// Go is a normal go source file.
-	Go
-	// Mod is a go.mod file.
-	Mod
-	// Sum is a go.sum file.
-	Sum
-	// Tmpl is a template file.
-	Tmpl
-	// Work is a go.work file.
-	Work
-)
-
-func (k FileKind) String() string {
-	switch k {
-	case Go:
-		return "go"
-	case Mod:
-		return "go.mod"
-	case Sum:
-		return "go.sum"
-	case Tmpl:
-		return "tmpl"
-	case Work:
-		return "go.work"
-	default:
-		return fmt.Sprintf("internal error: unknown file kind %d", k)
-	}
-}
-
 // Analyzer represents a go/analysis analyzer with some boolean properties
 // that let the user know how to use the analyzer.
 type Analyzer struct {
diff --git a/gopls/internal/lsp/symbols.go b/gopls/internal/lsp/symbols.go
index 3fe2824..e220a2e 100644
--- a/gopls/internal/lsp/symbols.go
+++ b/gopls/internal/lsp/symbols.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/lsp/template"
@@ -18,16 +19,16 @@
 	ctx, done := event.Start(ctx, "lsp.Server.documentSymbol", tag.URI.Of(params.TextDocument.URI))
 	defer done()
 
-	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, file.UnknownKind)
 	defer release()
 	if !ok {
 		return []interface{}{}, err
 	}
 	var docSymbols []protocol.DocumentSymbol
 	switch snapshot.FileKind(fh) {
-	case source.Tmpl:
+	case file.Tmpl:
 		docSymbols, err = template.DocumentSymbols(snapshot, fh)
-	case source.Go:
+	case file.Go:
 		docSymbols, err = source.DocumentSymbols(ctx, snapshot, fh)
 	default:
 		return []interface{}{}, nil
diff --git a/gopls/internal/lsp/template/completion.go b/gopls/internal/lsp/template/completion.go
index fbb2744..7be1b43 100644
--- a/gopls/internal/lsp/template/completion.go
+++ b/gopls/internal/lsp/template/completion.go
@@ -12,6 +12,7 @@
 	"go/token"
 	"strings"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 )
@@ -25,7 +26,7 @@
 	syms   map[string]symbol
 }
 
-func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) {
+func Completion(ctx context.Context, snapshot source.Snapshot, fh file.Handle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) {
 	all := New(snapshot.Templates())
 	var start int // the beginning of the Token (completed or not)
 	syms := make(map[string]symbol)
@@ -43,7 +44,7 @@
 	}
 	if p == nil {
 		// this cannot happen unless the search missed a template file
-		return nil, fmt.Errorf("%s not found", fh.FileIdentity().URI.Path())
+		return nil, fmt.Errorf("%s not found", fh.Identity().URI.Path())
 	}
 	c := completer{
 		p:      p,
diff --git a/gopls/internal/lsp/template/highlight.go b/gopls/internal/lsp/template/highlight.go
index 47069b1..3d65403 100644
--- a/gopls/internal/lsp/template/highlight.go
+++ b/gopls/internal/lsp/template/highlight.go
@@ -9,11 +9,12 @@
 	"fmt"
 	"regexp"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 )
 
-func Highlight(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.DocumentHighlight, error) {
+func Highlight(ctx context.Context, snapshot source.Snapshot, fh file.Handle, loc protocol.Position) ([]protocol.DocumentHighlight, error) {
 	buf, err := fh.Content()
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/template/implementations.go b/gopls/internal/lsp/template/implementations.go
index 7f081de..14c62c1 100644
--- a/gopls/internal/lsp/template/implementations.go
+++ b/gopls/internal/lsp/template/implementations.go
@@ -11,6 +11,7 @@
 	"strconv"
 	"time"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 )
@@ -21,7 +22,7 @@
 // Diagnose returns parse errors. There is only one.
 // The errors are not always helpful. For instance { {end}}
 // will likely point to the end of the file.
-func Diagnose(f source.FileHandle) []*source.Diagnostic {
+func Diagnose(f file.Handle) []*source.Diagnostic {
 	// no need for skipTemplate check, as Diagnose is called on the
 	// snapshot's template files
 	buf, err := f.Content()
@@ -72,7 +73,7 @@
 // does not understand scoping (if any) in templates. This code is
 // for definitions, type definitions, and implementations.
 // Results only for variables and templates.
-func Definition(snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.Location, error) {
+func Definition(snapshot source.Snapshot, fh file.Handle, loc protocol.Position) ([]protocol.Location, error) {
 	x, _, err := symAtPosition(fh, loc)
 	if err != nil {
 		return nil, err
@@ -92,7 +93,7 @@
 	return ans, nil
 }
 
-func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
+func Hover(ctx context.Context, snapshot source.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
 	sym, p, err := symAtPosition(fh, position)
 	if sym == nil || err != nil {
 		return nil, err
@@ -123,7 +124,7 @@
 	return &ans, nil
 }
 
-func References(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, params *protocol.ReferenceParams) ([]protocol.Location, error) {
+func References(ctx context.Context, snapshot source.Snapshot, fh file.Handle, params *protocol.ReferenceParams) ([]protocol.Location, error) {
 	sym, _, err := symAtPosition(fh, params.Position)
 	if sym == nil || err != nil || sym.name == "" {
 		return nil, err
diff --git a/gopls/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go
index d7c6f75..ab09bce 100644
--- a/gopls/internal/lsp/template/parse.go
+++ b/gopls/internal/lsp/template/parse.go
@@ -24,8 +24,8 @@
 	"text/template/parse"
 	"unicode/utf8"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
-	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
 )
 
@@ -69,7 +69,7 @@
 
 // New returns the Parses of the snapshot's tmpl files
 // (maybe cache these, but then avoiding import cycles needs code rearrangements)
-func New(tmpls map[protocol.DocumentURI]source.FileHandle) *All {
+func New(tmpls map[protocol.DocumentURI]file.Handle) *All {
 	all := make(map[protocol.DocumentURI]*Parsed)
 	for k, v := range tmpls {
 		buf, err := v.Content()
@@ -376,7 +376,7 @@
 	return pos
 }
 
-func symAtPosition(fh source.FileHandle, loc protocol.Position) (*symbol, *Parsed, error) {
+func symAtPosition(fh file.Handle, loc protocol.Position) (*symbol, *Parsed, error) {
 	buf, err := fh.Content()
 	if err != nil {
 		return nil, nil, err
diff --git a/gopls/internal/lsp/template/symbols.go b/gopls/internal/lsp/template/symbols.go
index 356705f..bc2e278 100644
--- a/gopls/internal/lsp/template/symbols.go
+++ b/gopls/internal/lsp/template/symbols.go
@@ -11,6 +11,7 @@
 	"text/template/parse"
 	"unicode/utf8"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
@@ -193,7 +194,7 @@
 
 // DocumentSymbols returns a hierarchy of the symbols defined in a template file.
 // (The hierarchy is flat. SymbolInformation might be better.)
-func DocumentSymbols(snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentSymbol, error) {
+func DocumentSymbols(snapshot source.Snapshot, fh file.Handle) ([]protocol.DocumentSymbol, error) {
 	buf, err := fh.Content()
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go
index 28c3a81..3523c62 100644
--- a/gopls/internal/lsp/text_synchronization.go
+++ b/gopls/internal/lsp/text_synchronization.go
@@ -12,6 +12,7 @@
 	"path/filepath"
 	"sync"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
@@ -109,9 +110,9 @@
 			return err
 		}
 	}
-	return s.didModifyFiles(ctx, []source.FileModification{{
+	return s.didModifyFiles(ctx, []file.Modification{{
 		URI:        uri,
-		Action:     source.Open,
+		Action:     file.Open,
 		Version:    params.TextDocument.Version,
 		Text:       []byte(params.TextDocument.Text),
 		LanguageID: params.TextDocument.LanguageID,
@@ -131,13 +132,13 @@
 	if err != nil {
 		return err
 	}
-	c := source.FileModification{
+	c := file.Modification{
 		URI:     uri,
-		Action:  source.Change,
+		Action:  file.Change,
 		Version: params.TextDocument.Version,
 		Text:    text,
 	}
-	if err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidChange); err != nil {
+	if err := s.didModifyFiles(ctx, []file.Modification{c}, FromDidChange); err != nil {
 		return err
 	}
 	return s.warnAboutModifyingGeneratedFiles(ctx, uri)
@@ -185,14 +186,14 @@
 	ctx, done := event.Start(ctx, "lsp.Server.didChangeWatchedFiles")
 	defer done()
 
-	var modifications []source.FileModification
+	var modifications []file.Modification
 	for _, change := range params.Changes {
 		uri := change.URI
 		if !uri.IsFile() {
 			continue
 		}
 		action := changeTypeToFileAction(change.Type)
-		modifications = append(modifications, source.FileModification{
+		modifications = append(modifications, file.Modification{
 			URI:    uri,
 			Action: action,
 			OnDisk: true,
@@ -209,14 +210,14 @@
 	if !uri.IsFile() {
 		return nil
 	}
-	c := source.FileModification{
+	c := file.Modification{
 		URI:    uri,
-		Action: source.Save,
+		Action: file.Save,
 	}
 	if params.Text != nil {
 		c.Text = []byte(*params.Text)
 	}
-	return s.didModifyFiles(ctx, []source.FileModification{c}, FromDidSave)
+	return s.didModifyFiles(ctx, []file.Modification{c}, FromDidSave)
 }
 
 func (s *server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
@@ -227,17 +228,17 @@
 	if !uri.IsFile() {
 		return nil
 	}
-	return s.didModifyFiles(ctx, []source.FileModification{
+	return s.didModifyFiles(ctx, []file.Modification{
 		{
 			URI:     uri,
-			Action:  source.Close,
+			Action:  file.Close,
 			Version: -1,
 			Text:    nil,
 		},
 	}, FromDidClose)
 }
 
-func (s *server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) error {
+func (s *server) didModifyFiles(ctx context.Context, modifications []file.Modification, cause ModificationSource) error {
 	// wg guards two conditions:
 	//  1. didModifyFiles is complete
 	//  2. the goroutine diagnosing changes on behalf of didModifyFiles is
@@ -277,7 +278,7 @@
 
 	// Build a lookup map for file modifications, so that we can later join
 	// with the snapshot file associations.
-	modMap := make(map[protocol.DocumentURI]source.FileModification)
+	modMap := make(map[protocol.DocumentURI]file.Modification)
 	for _, mod := range modifications {
 		modMap[mod.URI] = mod
 	}
@@ -292,7 +293,7 @@
 	for snapshot, uris := range snapshots {
 		for _, uri := range uris {
 			mod := modMap[uri]
-			if snapshot.Options().ChattyDiagnostics || mod.Action == source.Open || mod.Action == source.Close {
+			if snapshot.Options().ChattyDiagnostics || mod.Action == file.Open || mod.Action == file.Close {
 				s.mustPublishDiagnostics(uri)
 			}
 		}
@@ -364,14 +365,14 @@
 	return content, nil
 }
 
-func changeTypeToFileAction(ct protocol.FileChangeType) source.FileAction {
+func changeTypeToFileAction(ct protocol.FileChangeType) file.Action {
 	switch ct {
 	case protocol.Changed:
-		return source.Change
+		return file.Change
 	case protocol.Created:
-		return source.Create
+		return file.Create
 	case protocol.Deleted:
-		return source.Delete
+		return file.Delete
 	}
-	return source.UnknownFileAction
+	return file.UnknownAction
 }
diff --git a/gopls/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go
index d4de255..e450287 100644
--- a/gopls/internal/lsp/work/completion.go
+++ b/gopls/internal/lsp/work/completion.go
@@ -13,12 +13,13 @@
 	"sort"
 	"strings"
 
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
 )
 
-func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.CompletionList, error) {
+func Completion(ctx context.Context, snapshot source.Snapshot, fh file.Handle, position protocol.Position) (*protocol.CompletionList, error) {
 	ctx, done := event.Start(ctx, "work.Completion")
 	defer done()
 
diff --git a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go
index e0f9b4d..57b5eb7 100644
--- a/gopls/internal/lsp/work/diagnostics.go
+++ b/gopls/internal/lsp/work/diagnostics.go
@@ -11,6 +11,7 @@
 	"path/filepath"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
@@ -45,7 +46,7 @@
 	return reports, nil
 }
 
-func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]*source.Diagnostic, error) {
+func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]*source.Diagnostic, error) {
 	pw, err := snapshot.ParseWork(ctx, fh)
 	if err != nil {
 		if pw == nil || len(pw.ParseErrors) == 0 {
diff --git a/gopls/internal/lsp/work/format.go b/gopls/internal/lsp/work/format.go
index 70cbe59..4fc6dd7 100644
--- a/gopls/internal/lsp/work/format.go
+++ b/gopls/internal/lsp/work/format.go
@@ -8,12 +8,13 @@
 	"context"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
 )
 
-func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) {
+func Format(ctx context.Context, snapshot source.Snapshot, fh file.Handle) ([]protocol.TextEdit, error) {
 	ctx, done := event.Start(ctx, "work.Format")
 	defer done()
 
diff --git a/gopls/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go
index d777acd..0e0f040 100644
--- a/gopls/internal/lsp/work/hover.go
+++ b/gopls/internal/lsp/work/hover.go
@@ -10,12 +10,13 @@
 	"fmt"
 
 	"golang.org/x/mod/modfile"
+	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/internal/event"
 )
 
-func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
+func Hover(ctx context.Context, snapshot source.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
 	// We only provide hover information for the view's go.work file.
 	if fh.URI() != snapshot.WorkFile() {
 		return nil, nil