gopls/internal/lsp: consolidate the FileHandle API

Clean up several aspects of the FileHandle APIs.
- Eliminate the VersionedFileHandle. There is no need for this
  specialization, when we already had the closedFile wrapper to make an
  on-disk file satisfy the Versioning interface.
- Remove the VersionedFileIdentity concept. This was an artifact of an
  earlier time when we stored files across sessions, views, and
  snapshots. In all remaining uses, a snapshot is implied.
- Clean up now-unnecessary APIs accordingly.
- Rename cache.fileHandle and cache.overlay to cache.DiskFile and
  cache.Overlay. There was some convenience to exporting Overlay, and
  DiskFile was exported for symmetry.
- Remove a bunch of unnecessary test boilerplate.
- Remove the link from Overlay to Session, as it was only necessary for
  debug templates.

Change-Id: I3cbf599c260d8e53c8ace913bbf92b2c6f054d3a
Reviewed-on: https://go-review.googlesource.com/c/tools/+/462818
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go
index 88d1b43..ef56b12 100644
--- a/gopls/internal/lsp/cache/cache.go
+++ b/gopls/internal/lsp/cache/cache.go
@@ -50,7 +50,7 @@
 		id:          strconv.FormatInt(index, 10),
 		fset:        fset,
 		store:       store,
-		fileContent: map[span.URI]*fileHandle{},
+		fileContent: map[span.URI]*DiskFile{},
 	}
 	return c
 }
@@ -62,30 +62,40 @@
 	store *memoize.Store
 
 	fileMu      sync.Mutex
-	fileContent map[span.URI]*fileHandle
+	fileContent map[span.URI]*DiskFile
 }
 
-type fileHandle struct {
-	modTime time.Time
+// A DiskFile is a file on the filesystem, or a failure to read one.
+// It implements the source.FileHandle interface.
+type DiskFile struct {
 	uri     span.URI
+	modTime time.Time
 	content []byte
 	hash    source.Hash
 	err     error
 }
 
-func (h *fileHandle) Saved() bool {
-	return true
+func (h *DiskFile) URI() span.URI { return h.uri }
+
+func (h *DiskFile) FileIdentity() source.FileIdentity {
+	return source.FileIdentity{
+		URI:  h.uri,
+		Hash: h.hash,
+	}
+}
+
+func (h *DiskFile) Saved() bool    { return true }
+func (h *DiskFile) Version() int32 { return 0 }
+
+func (h *DiskFile) Read() ([]byte, error) {
+	return h.content, h.err
 }
 
 // GetFile stats and (maybe) reads the file, updates the cache, and returns it.
 func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
-	return c.getFile(ctx, uri)
-}
-
-func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) {
 	fi, statErr := os.Stat(uri.Filename())
 	if statErr != nil {
-		return &fileHandle{
+		return &DiskFile{
 			err: statErr,
 			uri: uri,
 		}, nil
@@ -127,7 +137,7 @@
 // ioLimit limits the number of parallel file reads per process.
 var ioLimit = make(chan struct{}, 128)
 
-func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, error) {
+func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*DiskFile, error) {
 	select {
 	case ioLimit <- struct{}{}:
 	case <-ctx.Done():
@@ -143,7 +153,7 @@
 	if err != nil {
 		content = nil // just in case
 	}
-	return &fileHandle{
+	return &DiskFile{
 		modTime: fi.ModTime(),
 		uri:     uri,
 		content: content,
@@ -166,27 +176,12 @@
 		cache:       c,
 		gocmdRunner: &gocommand.Runner{},
 		options:     options,
-		overlays:    make(map[span.URI]*overlay),
+		overlays:    make(map[span.URI]*Overlay),
 	}
 	event.Log(ctx, "New session", KeyCreateSession.Of(s))
 	return s
 }
 
-func (h *fileHandle) URI() span.URI {
-	return h.uri
-}
-
-func (h *fileHandle) FileIdentity() source.FileIdentity {
-	return source.FileIdentity{
-		URI:  h.uri,
-		Hash: h.hash,
-	}
-}
-
-func (h *fileHandle) Read() ([]byte, error) {
-	return h.content, h.err
-}
-
 var cacheIndex, sessionIndex, viewIndex int64
 
 func (c *Cache) ID() string                     { return c.id }
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index c4e0296..1d69504 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -335,7 +335,7 @@
 			rootMod = uri.Filename()
 		}
 		rootDir := filepath.Dir(rootMod)
-		nestedModules := make(map[string][]source.VersionedFileHandle)
+		nestedModules := make(map[string][]source.FileHandle)
 		for _, fh := range openFiles {
 			mod, err := findRootPattern(ctx, filepath.Dir(fh.URI().Filename()), "go.mod", s)
 			if err != nil {
@@ -375,7 +375,7 @@
 	return nil, nil
 }
 
-func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Diagnostic {
+func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.FileHandle) []*source.Diagnostic {
 	var srcDiags []*source.Diagnostic
 	for _, fh := range files {
 		// Place the diagnostics on the package or module declarations.
diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go
index 5cbcaf7..baa0deb 100644
--- a/gopls/internal/lsp/cache/maps.go
+++ b/gopls/internal/lsp/cache/maps.go
@@ -39,21 +39,21 @@
 	m.impl.Destroy()
 }
 
-func (m filesMap) Get(key span.URI) (source.VersionedFileHandle, bool) {
+func (m filesMap) Get(key span.URI) (source.FileHandle, bool) {
 	value, ok := m.impl.Get(key)
 	if !ok {
 		return nil, false
 	}
-	return value.(source.VersionedFileHandle), true
+	return value.(source.FileHandle), true
 }
 
-func (m filesMap) Range(do func(key span.URI, value source.VersionedFileHandle)) {
+func (m filesMap) Range(do func(key span.URI, value source.FileHandle)) {
 	m.impl.Range(func(key, value interface{}) {
-		do(key.(span.URI), value.(source.VersionedFileHandle))
+		do(key.(span.URI), value.(source.FileHandle))
 	})
 }
 
-func (m filesMap) Set(key span.URI, value source.VersionedFileHandle) {
+func (m filesMap) Set(key span.URI, value source.FileHandle) {
 	m.impl.Set(key, value, nil)
 }
 
diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go
index b198dff..50f557d 100644
--- a/gopls/internal/lsp/cache/mod.go
+++ b/gopls/internal/lsp/cache/mod.go
@@ -188,7 +188,7 @@
 	var sumFH source.FileHandle = s.FindFile(sumURI)
 	if sumFH == nil {
 		var err error
-		sumFH, err = s.view.cache.getFile(ctx, sumURI)
+		sumFH, err = s.view.cache.GetFile(ctx, sumURI)
 		if err != nil {
 			return nil
 		}
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
index 385b014..def10d5 100644
--- a/gopls/internal/lsp/cache/mod_tidy.go
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -51,7 +51,7 @@
 		if err != nil {
 			return nil, err
 		}
-		if _, ok := fh.(*overlay); ok {
+		if _, ok := fh.(*Overlay); ok {
 			if info, _ := os.Stat(uri.Filename()); info == nil {
 				return nil, source.ErrNoModOnDisk
 			}
diff --git a/gopls/internal/lsp/cache/mod_vuln.go b/gopls/internal/lsp/cache/mod_vuln.go
index b16c8c5..88d1a1c 100644
--- a/gopls/internal/lsp/cache/mod_vuln.go
+++ b/gopls/internal/lsp/cache/mod_vuln.go
@@ -37,7 +37,7 @@
 		if err != nil {
 			return nil, err
 		}
-		if _, ok := fh.(*overlay); ok {
+		if _, ok := fh.(*Overlay); ok {
 			if info, _ := os.Stat(modURI.Filename()); info == nil {
 				return nil, source.ErrNoModOnDisk
 			}
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index 8293873..ab42d29 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -39,11 +39,12 @@
 	viewMap map[span.URI]*View // map of URI->best view
 
 	overlayMu sync.Mutex
-	overlays  map[span.URI]*overlay
+	overlays  map[span.URI]*Overlay
 }
 
-type overlay struct {
-	session *Session
+// An Overlay is a file open in the editor.
+// It implements the source.FileSource interface.
+type Overlay struct {
 	uri     span.URI
 	text    []byte
 	hash    source.Hash
@@ -55,69 +56,19 @@
 	saved bool
 }
 
-func (o *overlay) Read() ([]byte, error) {
-	return o.text, nil
-}
+func (o *Overlay) URI() span.URI { return o.uri }
 
-func (o *overlay) FileIdentity() source.FileIdentity {
+func (o *Overlay) FileIdentity() source.FileIdentity {
 	return source.FileIdentity{
 		URI:  o.uri,
 		Hash: o.hash,
 	}
 }
 
-func (o *overlay) VersionedFileIdentity() source.VersionedFileIdentity {
-	return source.VersionedFileIdentity{
-		URI:       o.uri,
-		SessionID: o.session.id,
-		Version:   o.version,
-	}
-}
-
-func (o *overlay) Kind() source.FileKind {
-	return o.kind
-}
-
-func (o *overlay) URI() span.URI {
-	return o.uri
-}
-
-func (o *overlay) Version() int32 {
-	return o.version
-}
-
-func (o *overlay) Session() string {
-	return o.session.id
-}
-
-func (o *overlay) Saved() bool {
-	return o.saved
-}
-
-// closedFile implements LSPFile for a file that the editor hasn't told us about.
-type closedFile struct {
-	source.FileHandle
-}
-
-func (c *closedFile) VersionedFileIdentity() source.VersionedFileIdentity {
-	return source.VersionedFileIdentity{
-		URI:       c.FileHandle.URI(),
-		SessionID: "",
-		Version:   0,
-	}
-}
-
-func (c *closedFile) Saved() bool {
-	return true
-}
-
-func (c *closedFile) Session() string {
-	return ""
-}
-
-func (c *closedFile) Version() int32 {
-	return 0
-}
+func (o *Overlay) Read() ([]byte, error) { return o.text, nil }
+func (o *Overlay) Version() int32        { return o.version }
+func (o *Overlay) Saved() bool           { return o.saved }
+func (o *Overlay) Kind() source.FileKind { return o.kind }
 
 // ID returns the unique identifier for this session on this server.
 func (s *Session) ID() string     { return s.id }
@@ -442,7 +393,7 @@
 type fileChange struct {
 	content    []byte
 	exists     bool
-	fileHandle source.VersionedFileHandle
+	fileHandle source.FileHandle
 
 	// isUnchanged indicates whether the file action is one that does not
 	// change the actual contents of the file. Opens and closes should not
@@ -579,16 +530,19 @@
 					isUnchanged: isUnchanged,
 				}
 			} else {
-				fsFile, err := s.cache.getFile(ctx, c.URI)
+				fsFile, err := s.cache.GetFile(ctx, c.URI)
 				if err != nil {
 					return nil, nil, err
 				}
 				content, err := fsFile.Read()
-				fh := &closedFile{fsFile}
+				if err != nil {
+					// Ignore the error: the file may be deleted.
+					content = nil
+				}
 				views[view][c.URI] = &fileChange{
 					content:     content,
 					exists:      err == nil,
-					fileHandle:  fh,
+					fileHandle:  fsFile,
 					isUnchanged: isUnchanged,
 				}
 			}
@@ -701,7 +655,7 @@
 }
 
 // Precondition: caller holds s.viewMu lock.
-func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModification) (map[span.URI]*overlay, error) {
+func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModification) (map[span.URI]*Overlay, error) {
 	s.overlayMu.Lock()
 	defer s.overlayMu.Unlock()
 
@@ -768,15 +722,14 @@
 			}
 			sameContentOnDisk = true
 		default:
-			fh, err := s.cache.getFile(ctx, c.URI)
+			fh, err := s.cache.GetFile(ctx, c.URI)
 			if err != nil {
 				return nil, err
 			}
 			_, readErr := fh.Read()
 			sameContentOnDisk = (readErr == nil && fh.FileIdentity().Hash == hash)
 		}
-		o = &overlay{
-			session: s,
+		o = &Overlay{
 			uri:     c.URI,
 			version: version,
 			text:    text,
@@ -801,7 +754,7 @@
 
 	// Get the overlays for each change while the session's overlay map is
 	// locked.
-	overlays := make(map[span.URI]*overlay)
+	overlays := make(map[span.URI]*Overlay)
 	for _, c := range changes {
 		if o, ok := s.overlays[c.URI]; ok {
 			overlays[c.URI] = o
@@ -812,29 +765,22 @@
 
 // GetFile returns a handle for the specified file.
 func (s *Session) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
-	if overlay := s.readOverlay(uri); overlay != nil {
+	s.overlayMu.Lock()
+	overlay, ok := s.overlays[uri]
+	s.overlayMu.Unlock()
+	if ok {
 		return overlay, nil
 	}
-	// Fall back to the cache-level file system.
-	return s.cache.getFile(ctx, uri)
-}
 
-func (s *Session) readOverlay(uri span.URI) *overlay {
-	s.overlayMu.Lock()
-	defer s.overlayMu.Unlock()
-
-	if overlay, ok := s.overlays[uri]; ok {
-		return overlay
-	}
-	return nil
+	return s.cache.GetFile(ctx, uri)
 }
 
 // Overlays returns a slice of file overlays for the session.
-func (s *Session) Overlays() []source.Overlay {
+func (s *Session) Overlays() []*Overlay {
 	s.overlayMu.Lock()
 	defer s.overlayMu.Unlock()
 
-	overlays := make([]source.Overlay, 0, len(s.overlays))
+	overlays := make([]*Overlay, 0, len(s.overlays))
 	for _, overlay := range s.overlays {
 		overlays = append(overlays, overlay)
 	}
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index b78962e..0eef84e 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -268,12 +268,12 @@
 	return s.view.effectiveGOWORK()
 }
 
-func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle {
+func (s *snapshot) Templates() map[span.URI]source.FileHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	tmpls := map[span.URI]source.VersionedFileHandle{}
-	s.files.Range(func(k span.URI, fh source.VersionedFileHandle) {
+	tmpls := map[span.URI]source.FileHandle{}
+	s.files.Range(func(k span.URI, fh source.FileHandle) {
 		if s.view.FileKind(fh) == source.Tmpl {
 			tmpls[k] = fh
 		}
@@ -602,8 +602,8 @@
 	defer s.mu.Unlock()
 
 	overlays := make(map[string][]byte)
-	s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) {
-		overlay, ok := fh.(*overlay)
+	s.files.Range(func(uri span.URI, fh source.FileHandle) {
+		overlay, ok := fh.(*Overlay)
 		if !ok {
 			return
 		}
@@ -879,7 +879,7 @@
 	s.knownSubdirs.Destroy()
 	s.knownSubdirs = newKnownDirsSet()
 	s.knownSubdirsPatternCache = ""
-	s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) {
+	s.files.Range(func(uri span.URI, fh source.FileHandle) {
 		s.addKnownSubdirLocked(uri, dirs)
 	})
 }
@@ -963,7 +963,7 @@
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) {
+	s.files.Range(func(uri span.URI, fh source.FileHandle) {
 		if source.InDir(dir.Filename(), uri.Filename()) {
 			files = append(files, uri)
 		}
@@ -995,9 +995,9 @@
 // Symbols extracts and returns the symbols for each file in all the snapshot's views.
 func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol {
 	// Read the set of Go files out of the snapshot.
-	var goFiles []source.VersionedFileHandle
+	var goFiles []source.FileHandle
 	s.mu.Lock()
-	s.files.Range(func(uri span.URI, f source.VersionedFileHandle) {
+	s.files.Range(func(uri span.URI, f source.FileHandle) {
 		if s.View().FileKind(f) == source.Go {
 			goFiles = append(goFiles, f)
 		}
@@ -1153,7 +1153,7 @@
 	return ok
 }
 
-func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle {
+func (s *snapshot) FindFile(uri span.URI) source.FileHandle {
 	uri, _ = s.view.canonicalURI(uri, true)
 
 	s.mu.Lock()
@@ -1163,12 +1163,12 @@
 	return result
 }
 
-// GetVersionedFile returns a File for the given URI. If the file is unknown it
-// is added to the managed set.
+// GetFile returns a File for the given URI. If the file is unknown it is added
+// to the managed set.
 //
-// GetVersionedFile succeeds even if the file does not exist. A non-nil error return
+// GetFile 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) GetVersionedFile(ctx context.Context, uri span.URI) (source.VersionedFileHandle, error) {
+func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
 	uri, _ = s.view.canonicalURI(uri, true)
 
 	s.mu.Lock()
@@ -1178,18 +1178,12 @@
 		return fh, nil
 	}
 
-	fh, err := s.view.cache.getFile(ctx, uri) // read the file
+	fh, err := s.view.cache.GetFile(ctx, uri) // read the file
 	if err != nil {
 		return nil, err
 	}
-	closed := &closedFile{fh}
-	s.files.Set(uri, closed)
-	return closed, nil
-}
-
-// GetFile implements the fileSource interface by wrapping GetVersionedFile.
-func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
-	return s.GetVersionedFile(ctx, uri)
+	s.files.Set(uri, fh)
+	return fh, nil
 }
 
 func (s *snapshot) IsOpen(uri span.URI) bool {
@@ -1199,12 +1193,12 @@
 
 }
 
-func (s *snapshot) openFiles() []source.VersionedFileHandle {
+func (s *snapshot) openFiles() []source.FileHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	var open []source.VersionedFileHandle
-	s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) {
+	var open []source.FileHandle
+	s.files.Range(func(uri span.URI, fh source.FileHandle) {
 		if isFileOpen(fh) {
 			open = append(open, fh)
 		}
@@ -1217,8 +1211,8 @@
 	return isFileOpen(fh)
 }
 
-func isFileOpen(fh source.VersionedFileHandle) bool {
-	_, open := fh.(*overlay)
+func isFileOpen(fh source.FileHandle) bool {
+	_, open := fh.(*Overlay)
 	return open
 }
 
@@ -1479,14 +1473,14 @@
 	return nil
 }
 
-func (s *snapshot) orphanedOpenFiles() []source.VersionedFileHandle {
+func (s *snapshot) orphanedOpenFiles() []source.FileHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	var files []source.VersionedFileHandle
-	s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) {
+	var files []source.FileHandle
+	s.files.Range(func(uri span.URI, fh source.FileHandle) {
 		// Only consider open files, which will be represented as overlays.
-		if _, isOverlay := fh.(*overlay); !isOverlay {
+		if _, isOverlay := fh.(*Overlay); !isOverlay {
 			return
 		}
 		// Don't try to reload metadata for go.mod files.
@@ -1716,8 +1710,8 @@
 		// The original FileHandle for this URI is cached on the snapshot.
 		originalFH, _ := s.files.Get(uri)
 		var originalOpen, newOpen bool
-		_, originalOpen = originalFH.(*overlay)
-		_, newOpen = change.fileHandle.(*overlay)
+		_, originalOpen = originalFH.(*Overlay)
+		_, newOpen = change.fileHandle.(*Overlay)
 		anyFileOpenedOrClosed = anyFileOpenedOrClosed || (originalOpen != newOpen)
 		anyFileAdded = anyFileAdded || (originalFH == nil && change.fileHandle != nil)
 
@@ -1999,11 +1993,11 @@
 // are both overlays, and if the current FileHandle is saved while the original
 // FileHandle was not saved.
 func fileWasSaved(originalFH, currentFH source.FileHandle) bool {
-	c, ok := currentFH.(*overlay)
+	c, ok := currentFH.(*Overlay)
 	if !ok || c == nil {
 		return true
 	}
-	o, ok := originalFH.(*overlay)
+	o, ok := originalFH.(*Overlay)
 	if !ok || o == nil {
 		return c.saved
 	}
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index a4c87c3..ea9016e 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -331,9 +331,9 @@
 	// 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.(source.Overlay); ok {
-		if o.Kind() != source.UnknownKind {
-			return o.Kind()
+	if o, ok := fh.(*Overlay); ok {
+		if o.kind != source.UnknownKind {
+			return o.kind
 		}
 	}
 
diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go
index 8adfbfa..4b45681 100644
--- a/gopls/internal/lsp/cache/view_test.go
+++ b/gopls/internal/lsp/cache/view_test.go
@@ -96,7 +96,7 @@
 		rel := fake.RelativeTo(dir)
 		folderURI := span.URIFromPath(rel.AbsPath(test.folder))
 		excludeNothing := func(string) bool { return false }
-		got, err := findWorkspaceModFile(ctx, folderURI, &osFileSource{}, excludeNothing)
+		got, err := findWorkspaceModFile(ctx, folderURI, New(nil, nil), excludeNothing)
 		if err != nil {
 			t.Fatal(err)
 		}
diff --git a/gopls/internal/lsp/cache/workspace_test.go b/gopls/internal/lsp/cache/workspace_test.go
deleted file mode 100644
index 5f1e13e..0000000
--- a/gopls/internal/lsp/cache/workspace_test.go
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright 2020 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 cache
-
-import (
-	"context"
-	"os"
-
-	"golang.org/x/tools/gopls/internal/lsp/source"
-	"golang.org/x/tools/gopls/internal/span"
-)
-
-// osFileSource is a fileSource that just reads from the operating system.
-type osFileSource struct {
-	overlays map[span.URI]fakeOverlay
-}
-
-type fakeOverlay struct {
-	source.VersionedFileHandle
-	uri     span.URI
-	content string
-	err     error
-	saved   bool
-}
-
-func (o fakeOverlay) Saved() bool { return o.saved }
-
-func (o fakeOverlay) Read() ([]byte, error) {
-	if o.err != nil {
-		return nil, o.err
-	}
-	return []byte(o.content), nil
-}
-
-func (o fakeOverlay) URI() span.URI {
-	return o.uri
-}
-
-// change updates the file source with the given file content. For convenience,
-// empty content signals a deletion. If saved is true, these changes are
-// persisted to disk.
-func (s *osFileSource) change(ctx context.Context, uri span.URI, content string, saved bool) (*fileChange, error) {
-	if content == "" {
-		delete(s.overlays, uri)
-		if saved {
-			if err := os.Remove(uri.Filename()); err != nil {
-				return nil, err
-			}
-		}
-		fh, err := s.GetFile(ctx, uri)
-		if err != nil {
-			return nil, err
-		}
-		data, err := fh.Read()
-		return &fileChange{exists: err == nil, content: data, fileHandle: &closedFile{fh}}, nil
-	}
-	if s.overlays == nil {
-		s.overlays = map[span.URI]fakeOverlay{}
-	}
-	s.overlays[uri] = fakeOverlay{uri: uri, content: content, saved: saved}
-	return &fileChange{
-		exists:     content != "",
-		content:    []byte(content),
-		fileHandle: s.overlays[uri],
-	}, nil
-}
-
-func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
-	if overlay, ok := s.overlays[uri]; ok {
-		return overlay, nil
-	}
-	fi, statErr := os.Stat(uri.Filename())
-	if statErr != nil {
-		return &fileHandle{
-			err: statErr,
-			uri: uri,
-		}, nil
-	}
-	fh, err := readFile(ctx, uri, fi)
-	if err != nil {
-		return nil, err
-	}
-	return fh, nil
-}
diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go
index 8f39ea9..ec918b7 100644
--- a/gopls/internal/lsp/code_action.go
+++ b/gopls/internal/lsp/code_action.go
@@ -366,7 +366,7 @@
 	return actions, nil
 }
 
-func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.DocumentChanges {
+func documentChanges(fh source.FileHandle, edits []protocol.TextEdit) []protocol.DocumentChanges {
 	return []protocol.DocumentChanges{
 		{
 			TextDocumentEdit: &protocol.TextDocumentEdit{
@@ -410,7 +410,7 @@
 	for _, fix := range sd.SuggestedFixes {
 		var changes []protocol.DocumentChanges
 		for uri, edits := range fix.Edits {
-			fh, err := snapshot.GetVersionedFile(ctx, uri)
+			fh, err := snapshot.GetFile(ctx, uri)
 			if err != nil {
 				return nil, err
 			}
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index b9e1fe6..a1ff862 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -70,9 +70,9 @@
 // be populated, depending on which configuration is set. See comments in-line
 // for details.
 type commandDeps struct {
-	snapshot source.Snapshot            // present if cfg.forURI was set
-	fh       source.VersionedFileHandle // present if cfg.forURI was set
-	work     *progress.WorkDone         // present cfg.progress was set
+	snapshot source.Snapshot    // present if cfg.forURI was set
+	fh       source.FileHandle  // present if cfg.forURI was set
+	work     *progress.WorkDone // present cfg.progress was set
 }
 
 type commandFunc func(context.Context, commandDeps) error
@@ -606,7 +606,7 @@
 }
 
 func applyFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, newContent []byte) ([]protocol.TextDocumentEdit, error) {
-	fh, err := snapshot.GetVersionedFile(ctx, uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return nil, err
 	}
diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go
index 6934adf..3e6a905 100644
--- a/gopls/internal/lsp/debug/serve.go
+++ b/gopls/internal/lsp/debug/serve.go
@@ -727,7 +727,6 @@
 {{define "serverlink"}}<a href="/server/{{.}}">Server {{.}}</a>{{end}}
 {{define "sessionlink"}}<a href="/session/{{.}}">Session {{.}}</a>{{end}}
 {{define "viewlink"}}<a href="/view/{{.}}">View {{.}}</a>{{end}}
-{{define "filelink"}}<a href="/file/{{.Session}}/{{.FileIdentity.Hash}}">{{.FileIdentity.URI}}</a>{{end}}
 `)).Funcs(template.FuncMap{
 	"fuint64":  fuint64,
 	"fuint32":  fuint32,
@@ -876,7 +875,11 @@
 <h2>Views</h2>
 <ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}</li>{{end}}</ul>
 <h2>Overlays</h2>
-<ul>{{range .Overlays}}<li>{{template "filelink" .}}</li>{{end}}</ul>
+{{$session := .}}
+<ul>{{range .Overlays}}
+<li>
+<a href="/file/{{$session.ID}}/{{.FileIdentity.Hash}}">{{.FileIdentity.URI}}</a>
+</li>{{end}}</ul>
 <h2>Options</h2>
 {{range options .}}
 <p><b>{{.Name}}</b> {{.Type}}</p>
@@ -900,7 +903,6 @@
 {{define "title"}}Overlay {{.FileIdentity.Hash}}{{end}}
 {{define "body"}}
 {{with .}}
-	From: <b>{{template "sessionlink" .Session}}</b><br>
 	URI: <b>{{.URI}}</b><br>
 	Identifier: <b>{{.FileIdentity.Hash}}</b><br>
 	Version: <b>{{.Version}}</b><br>
diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go
index e7b290d..ef30b6f 100644
--- a/gopls/internal/lsp/diagnostics.go
+++ b/gopls/internal/lsp/diagnostics.go
@@ -247,16 +247,16 @@
 	}()
 
 	// common code for dispatching diagnostics
-	store := func(dsource diagnosticSource, operation string, diagsByFileID map[source.VersionedFileIdentity][]*source.Diagnostic, err error, merge bool) {
+	store := func(dsource diagnosticSource, operation string, diagsByFile map[span.URI][]*source.Diagnostic, err error, merge bool) {
 		if err != nil {
 			event.Error(ctx, "warning: while "+operation, err, source.SnapshotLabels(snapshot)...)
 		}
-		for id, diags := range diagsByFileID {
-			if id.URI == "" {
+		for uri, diags := range diagsByFile {
+			if uri == "" {
 				event.Error(ctx, "missing URI while "+operation, fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename()))
 				continue
 			}
-			s.storeDiagnostics(snapshot, id.URI, dsource, diags, merge)
+			s.storeDiagnostics(snapshot, uri, dsource, diags, merge)
 		}
 	}
 
@@ -425,14 +425,14 @@
 		// results. This ensures that the toggling of GC details and clearing of
 		// diagnostics does not race with storing the results here.
 		if enableGCDetails {
-			for id, diags := range gcReports {
-				fh := snapshot.FindFile(id.URI)
+			for uri, diags := range gcReports {
+				fh := snapshot.FindFile(uri)
 				// Don't publish gc details for unsaved buffers, since the underlying
 				// logic operates on the file on disk.
 				if fh == nil || !fh.Saved() {
 					continue
 				}
-				s.storeDiagnostics(snapshot, id.URI, gcDetailsSource, diags, true)
+				s.storeDiagnostics(snapshot, uri, gcDetailsSource, diags, true)
 			}
 		}
 		s.gcOptimizationDetailsMu.Unlock()
@@ -543,7 +543,7 @@
 // checkForOrphanedFile checks that the given URIs can be mapped to packages.
 // If they cannot and the workspace is not otherwise unloaded, it also surfaces
 // a warning, suggesting that the user check the file for build tags.
-func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle) *source.Diagnostic {
+func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) *source.Diagnostic {
 	// TODO(rfindley): this function may fail to produce a diagnostic for a
 	// variety of reasons, some of which should probably not be ignored. For
 	// example, should this function be tolerant of the case where fh does not
diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go
index 1d7135e..87a86f4 100644
--- a/gopls/internal/lsp/general.go
+++ b/gopls/internal/lsp/general.go
@@ -549,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.VersionedFileHandle, bool, func(), error) {
+func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.FileHandle, bool, func(), error) {
 	uri := pURI.SpanURI()
 	if !uri.IsFile() {
 		// Not a file URI. Stop processing the request, but don't return an error.
@@ -560,7 +560,7 @@
 		return nil, nil, false, func() {}, err
 	}
 	snapshot, release := view.Snapshot(ctx)
-	fh, err := snapshot.GetVersionedFile(ctx, uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		release()
 		return nil, nil, false, func() {}, err
diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go
index 65b3786..3cbe352 100644
--- a/gopls/internal/lsp/mod/diagnostics.go
+++ b/gopls/internal/lsp/mod/diagnostics.go
@@ -26,7 +26,7 @@
 // Diagnostics returns diagnostics for the modules in the workspace.
 //
 // It waits for completion of type-checking of all active packages.
-func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
+func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) {
 	ctx, done := event.Start(ctx, "mod.Diagnostics", source.SnapshotLabels(snapshot)...)
 	defer done()
 
@@ -35,7 +35,7 @@
 
 // UpgradeDiagnostics returns upgrade diagnostics for the modules in the
 // workspace with known upgrades.
-func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
+func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) {
 	ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", source.SnapshotLabels(snapshot)...)
 	defer done()
 
@@ -44,31 +44,31 @@
 
 // VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the
 // workspace with known vulnerabilites.
-func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
+func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) {
 	ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", source.SnapshotLabels(snapshot)...)
 	defer done()
 
 	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[source.VersionedFileIdentity][]*source.Diagnostic, error) {
-	reports := make(map[source.VersionedFileIdentity][]*source.Diagnostic)
+func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[span.URI][]*source.Diagnostic, error) {
+	reports := make(map[span.URI][]*source.Diagnostic)
 	for _, uri := range snapshot.ModFiles() {
-		fh, err := snapshot.GetVersionedFile(ctx, uri)
+		fh, err := snapshot.GetFile(ctx, uri)
 		if err != nil {
 			return nil, err
 		}
-		reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{}
+		reports[fh.URI()] = []*source.Diagnostic{}
 		diagnostics, err := diagFn(ctx, snapshot, fh)
 		if err != nil {
 			return nil, err
 		}
 		for _, d := range diagnostics {
-			fh, err := snapshot.GetVersionedFile(ctx, d.URI)
+			fh, err := snapshot.GetFile(ctx, d.URI)
 			if err != nil {
 				return nil, err
 			}
-			reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], d)
+			reports[fh.URI()] = append(reports[fh.URI()], d)
 		}
 	}
 	return reports, nil
diff --git a/gopls/internal/lsp/rename.go b/gopls/internal/lsp/rename.go
index e9bb2d4..359d9ac 100644
--- a/gopls/internal/lsp/rename.go
+++ b/gopls/internal/lsp/rename.go
@@ -29,7 +29,7 @@
 
 	var docChanges []protocol.DocumentChanges
 	for uri, e := range edits {
-		fh, err := snapshot.GetVersionedFile(ctx, uri)
+		fh, err := snapshot.GetFile(ctx, uri)
 		if err != nil {
 			return nil, err
 		}
diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go
index 82d90dc..3d42df1 100644
--- a/gopls/internal/lsp/server.go
+++ b/gopls/internal/lsp/server.go
@@ -136,7 +136,7 @@
 			if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
 				URI:         protocol.URIFromSpanURI(fh.URI()),
 				Diagnostics: toProtocolDiagnostics(diagnostics),
-				Version:     fileID.Version,
+				Version:     fileID.Version(),
 			}); err != nil {
 				return nil, err
 			}
diff --git a/gopls/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go
index c50c155..cd8ec7a 100644
--- a/gopls/internal/lsp/source/add_import.go
+++ b/gopls/internal/lsp/source/add_import.go
@@ -12,7 +12,7 @@
 )
 
 // AddImport adds a single import statement to the given file
-func AddImport(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, importPath string) ([]protocol.TextEdit, error) {
+func AddImport(ctx context.Context, snapshot Snapshot, fh FileHandle, 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/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go
index b8fab6a..a7350b0 100644
--- a/gopls/internal/lsp/source/diagnostics.go
+++ b/gopls/internal/lsp/source/diagnostics.go
@@ -73,22 +73,22 @@
 // "gopls/diagnoseFiles" nonstandard request handler. It would be more
 // efficient to compute the set of packages and TypeCheck and
 // Analyze them all at once.
-func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (VersionedFileIdentity, []*Diagnostic, error) {
-	fh, err := snapshot.GetVersionedFile(ctx, uri)
+func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (FileHandle, []*Diagnostic, error) {
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
-		return VersionedFileIdentity{}, nil, err
+		return nil, nil, err
 	}
 	pkg, _, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, NarrowestPackage)
 	if err != nil {
-		return VersionedFileIdentity{}, nil, err
+		return nil, nil, err
 	}
 	adiags, err := Analyze(ctx, snapshot, pkg.ID(), false)
 	if err != nil {
-		return VersionedFileIdentity{}, nil, err
+		return nil, nil, err
 	}
 	var fileDiags []*Diagnostic // combine load/parse/type + analysis diagnostics
 	CombineDiagnostics(pkg, fh.URI(), adiags, &fileDiags, &fileDiags)
-	return fh.VersionedFileIdentity(), fileDiags, nil
+	return fh, fileDiags, nil
 }
 
 // CombineDiagnostics combines and filters list/parse/type diagnostics
diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go
index 50f1cb0..68e5f8e 100644
--- a/gopls/internal/lsp/source/fix.go
+++ b/gopls/internal/lsp/source/fix.go
@@ -27,7 +27,7 @@
 	// suggested fixes with their diagnostics, so we have to compute them
 	// separately. Such analyzers should provide a function with a signature of
 	// SuggestedFixFunc.
-	SuggestedFixFunc  func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error)
+	SuggestedFixFunc  func(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error)
 	singleFileFixFunc func(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error)
 )
 
@@ -52,7 +52,7 @@
 
 // singleFile calls analyzers that expect inputs for a single file
 func singleFile(sf singleFileFixFunc) SuggestedFixFunc {
-	return func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) {
+	return func(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) {
 		fset, rng, src, file, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
 		if err != nil {
 			return nil, nil, err
@@ -72,7 +72,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 VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
+func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
 	handler, ok := suggestedFixes[fix]
 	if !ok {
 		return nil, fmt.Errorf("no suggested fix function for %s", fix)
@@ -94,7 +94,7 @@
 		if !end.IsValid() {
 			end = edit.Pos
 		}
-		fh, err := snapshot.GetVersionedFile(ctx, span.URIFromPath(tokFile.Name()))
+		fh, err := snapshot.GetFile(ctx, span.URIFromPath(tokFile.Name()))
 		if err != nil {
 			return nil, err
 		}
diff --git a/gopls/internal/lsp/source/gc_annotations.go b/gopls/internal/lsp/source/gc_annotations.go
index d5245c8..72159e6 100644
--- a/gopls/internal/lsp/source/gc_annotations.go
+++ b/gopls/internal/lsp/source/gc_annotations.go
@@ -35,7 +35,7 @@
 	Bounds Annotation = "bounds"
 )
 
-func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) (map[VersionedFileIdentity][]*Diagnostic, error) {
+func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) (map[span.URI][]*Diagnostic, error) {
 	if len(m.CompiledGoFiles) == 0 {
 		return nil, nil
 	}
@@ -74,7 +74,7 @@
 	if err != nil {
 		return nil, err
 	}
-	reports := make(map[VersionedFileIdentity][]*Diagnostic)
+	reports := make(map[span.URI][]*Diagnostic)
 	opts := snapshot.View().Options()
 	var parseError error
 	for _, fn := range files {
@@ -93,7 +93,7 @@
 			// outside the package can never be taken back.
 			continue
 		}
-		reports[fh.VersionedFileIdentity()] = diagnostics
+		reports[fh.URI()] = diagnostics
 	}
 	return reports, parseError
 }
diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go
index d84febe..07b4c30 100644
--- a/gopls/internal/lsp/source/known_packages.go
+++ b/gopls/internal/lsp/source/known_packages.go
@@ -24,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 VersionedFileHandle) ([]PackagePath, error) {
+func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]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/stub.go b/gopls/internal/lsp/source/stub.go
index c2edf08..cf360dd 100644
--- a/gopls/internal/lsp/source/stub.go
+++ b/gopls/internal/lsp/source/stub.go
@@ -25,7 +25,7 @@
 	"golang.org/x/tools/internal/typeparams"
 )
 
-func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) {
+func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) {
 	pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage)
 	if err != nil {
 		return nil, nil, fmt.Errorf("GetTypedFile: %w", err)
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index 0a96eb3..4b6fd13 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -78,11 +78,7 @@
 
 	// FindFile returns the FileHandle for the given URI, if it is already
 	// in the given snapshot.
-	FindFile(uri span.URI) VersionedFileHandle
-
-	// GetVersionedFile returns the VersionedFileHandle for a given URI,
-	// initializing it if it is not already part of the snapshot.
-	GetVersionedFile(ctx context.Context, uri span.URI) (VersionedFileHandle, error)
+	FindFile(uri span.URI) FileHandle
 
 	// GetFile returns the FileHandle for a given URI, initializing it if it is
 	// not already part of the snapshot.
@@ -99,7 +95,7 @@
 	IgnoredFile(uri span.URI) bool
 
 	// Templates returns the .tmpl files
-	Templates() map[span.URI]VersionedFileHandle
+	Templates() map[span.URI]FileHandle
 
 	// ParseGo returns the parsed AST for the file.
 	// If the file is not available, returns nil and an error.
@@ -513,12 +509,6 @@
 
 var ErrViewExists = errors.New("view already exists for session")
 
-// Overlay is the type for a file held in memory on a session.
-type Overlay interface {
-	Kind() FileKind
-	VersionedFileHandle
-}
-
 // FileModification represents a modification to a file.
 type FileModification struct {
 	URI    span.URI
@@ -613,38 +603,26 @@
 	TypecheckWorkspace
 )
 
-type VersionedFileHandle interface {
-	FileHandle
-	Version() int32
-	Session() string
-
-	// LSPIdentity returns the version identity of a file.
-	VersionedFileIdentity() VersionedFileIdentity
-}
-
-type VersionedFileIdentity struct {
-	URI span.URI
-
-	// SessionID is the ID of the LSP session.
-	SessionID string
-
-	// Version is the version of the file, as specified by the client. It should
-	// only be set in combination with SessionID.
-	Version int32
-}
-
-// FileHandle represents a handle to a specific version of a single file.
+// A FileHandle is an interface to files tracked by the LSP session, which may
+// be either files read from disk, or open in the editor session (overlays).
 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() span.URI
-
 	// FileIdentity returns a FileIdentity for the file, even if there was an
 	// error reading it.
 	FileIdentity() FileIdentity
+	// Saved reports whether the file has the same content on disk.
+	// For on-disk files, this is trivially true.
+	Saved() bool
+	// Version returns the file version, as defined by the LSP client.
+	// For on-disk file handles, Version returns 0.
+	Version() int32
 	// Read reads the contents of a file.
 	// If the file is not available, returns a nil slice and an error.
 	Read() ([]byte, error)
-	// Saved reports whether the file has the same content on disk.
-	Saved() bool
 }
 
 // A Hash is a cryptographic digest of the contents of a file.
diff --git a/gopls/internal/lsp/template/completion.go b/gopls/internal/lsp/template/completion.go
index 140c674..292563a 100644
--- a/gopls/internal/lsp/template/completion.go
+++ b/gopls/internal/lsp/template/completion.go
@@ -25,7 +25,7 @@
 	syms   map[string]symbol
 }
 
-func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) {
+func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, 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)
diff --git a/gopls/internal/lsp/template/implementations.go b/gopls/internal/lsp/template/implementations.go
index 6c90b68..ed9b986 100644
--- a/gopls/internal/lsp/template/implementations.go
+++ b/gopls/internal/lsp/template/implementations.go
@@ -22,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.VersionedFileHandle) []*source.Diagnostic {
+func Diagnose(f source.FileHandle) []*source.Diagnostic {
 	// no need for skipTemplate check, as Diagnose is called on the
 	// snapshot's template files
 	buf, err := f.Read()
@@ -73,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.VersionedFileHandle, loc protocol.Position) ([]protocol.Location, error) {
+func Definition(snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.Location, error) {
 	x, _, err := symAtPosition(fh, loc)
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go
index eb644c0..a6befdc 100644
--- a/gopls/internal/lsp/template/parse.go
+++ b/gopls/internal/lsp/template/parse.go
@@ -70,7 +70,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[span.URI]source.VersionedFileHandle) *All {
+func New(tmpls map[span.URI]source.FileHandle) *All {
 	all := make(map[span.URI]*Parsed)
 	for k, v := range tmpls {
 		buf, err := v.Read()
diff --git a/gopls/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go
index 223f022..bcdc2d1 100644
--- a/gopls/internal/lsp/work/completion.go
+++ b/gopls/internal/lsp/work/completion.go
@@ -18,7 +18,7 @@
 	"golang.org/x/tools/internal/event"
 )
 
-func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) {
+func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, 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 c64027c..cbcc850 100644
--- a/gopls/internal/lsp/work/diagnostics.go
+++ b/gopls/internal/lsp/work/diagnostics.go
@@ -17,30 +17,30 @@
 	"golang.org/x/tools/internal/event"
 )
 
-func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
+func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) {
 	ctx, done := event.Start(ctx, "work.Diagnostics", source.SnapshotLabels(snapshot)...)
 	defer done()
 
-	reports := map[source.VersionedFileIdentity][]*source.Diagnostic{}
+	reports := map[span.URI][]*source.Diagnostic{}
 	uri := snapshot.WorkFile()
 	if uri == "" {
 		return nil, nil
 	}
-	fh, err := snapshot.GetVersionedFile(ctx, uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return nil, err
 	}
-	reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{}
+	reports[fh.URI()] = []*source.Diagnostic{}
 	diagnostics, err := DiagnosticsForWork(ctx, snapshot, fh)
 	if err != nil {
 		return nil, err
 	}
 	for _, d := range diagnostics {
-		fh, err := snapshot.GetVersionedFile(ctx, d.URI)
+		fh, err := snapshot.GetFile(ctx, d.URI)
 		if err != nil {
 			return nil, err
 		}
-		reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], d)
+		reports[fh.URI()] = append(reports[fh.URI()], d)
 	}
 
 	return reports, nil
diff --git a/gopls/test/debug/debug_test.go b/gopls/test/debug/debug_test.go
index 9d5d6f0..9bd8928 100644
--- a/gopls/test/debug/debug_test.go
+++ b/gopls/test/debug/debug_test.go
@@ -13,7 +13,6 @@
 import (
 	"go/ast"
 	"html/template"
-	"log"
 	"runtime"
 	"sort"
 	"strings"
@@ -23,16 +22,12 @@
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/gopls/internal/lsp/cache"
 	"golang.org/x/tools/gopls/internal/lsp/debug"
-	"golang.org/x/tools/gopls/internal/lsp/source"
-	"golang.org/x/tools/gopls/internal/span"
 )
 
-type tdata struct {
+var templates = map[string]struct {
 	tmpl *template.Template
 	data interface{} // a value of the needed type
-}
-
-var templates = map[string]tdata{
+}{
 	"MainTmpl":    {debug.MainTmpl, &debug.Instance{}},
 	"DebugTmpl":   {debug.DebugTmpl, nil},
 	"RPCTmpl":     {debug.RPCTmpl, &debug.Rpcs{}},
@@ -42,45 +37,9 @@
 	"ViewTmpl":    {debug.ViewTmpl, &cache.View{}},
 	"ClientTmpl":  {debug.ClientTmpl, &debug.Client{}},
 	"ServerTmpl":  {debug.ServerTmpl, &debug.Server{}},
-	//"FileTmpl":    {FileTmpl, source.Overlay{}}, // need to construct a source.Overlay in init
-	"InfoTmpl":   {debug.InfoTmpl, "something"},
-	"MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}},
-}
-
-// construct a source.Overlay for fileTmpl
-type fakeOverlay struct{}
-
-func (fakeOverlay) Version() int32 {
-	return 0
-}
-func (fakeOverlay) Session() string {
-	return ""
-}
-func (fakeOverlay) VersionedFileIdentity() source.VersionedFileIdentity {
-	return source.VersionedFileIdentity{}
-}
-func (fakeOverlay) FileIdentity() source.FileIdentity {
-	return source.FileIdentity{}
-}
-func (fakeOverlay) Kind() source.FileKind {
-	return 0
-}
-func (fakeOverlay) Read() ([]byte, error) {
-	return nil, nil
-}
-func (fakeOverlay) Saved() bool {
-	return true
-}
-func (fakeOverlay) URI() span.URI {
-	return ""
-}
-
-var _ source.Overlay = fakeOverlay{}
-
-func init() {
-	log.SetFlags(log.Lshortfile)
-	var v fakeOverlay
-	templates["FileTmpl"] = tdata{debug.FileTmpl, v}
+	"FileTmpl":    {debug.FileTmpl, &cache.Overlay{}},
+	"InfoTmpl":    {debug.InfoTmpl, "something"},
+	"MemoryTmpl":  {debug.MemoryTmpl, runtime.MemStats{}},
 }
 
 func TestTemplates(t *testing.T) {
@@ -169,6 +128,7 @@
 	ast.Inspect(tree, f)
 	return ans
 }
+
 func treeOf(p *packages.Package, fname string) *ast.File {
 	for _, tree := range p.Syntax {
 		loc := tree.Package