internal/lsp: read files eagerly

We use file identities pervasively throughout gopls. Prior to this
change, the identity is the modification date of an unopened file, or
the hash of an opened file. That means that opening a file changes its
identity, which causes unnecessary churn in the cache.

Unfortunately, there isn't an easy way to fix this. Changing the
cache key to something else, such as the modification time, means that
we won't unify cache entries if a change is made and then undone. The
approach here is to read files eagerly in GetFile, so that we know their
hashes immediately. That resolves the churn, but means that we do a ton
of file IO at startup.

Incidental changes:

Remove the FileSystem interface; there was only one implementation and
it added a fair amount of cruft. We have many other places that assume
os.Stat and such work.

Add direct accessors to FileHandle for URI, Kind, and Version. Most uses
of (FileHandle).Identity were for stuff that we derive solely from the
URI, and this helped me disentangle them. It is a *ton* of churn,
though. I can revert it if you want.

Change-Id: Ia2133bc527f71daf81c9d674951726a232ca5bc9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/237037
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index 0f26c23..86b7e22 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -9,20 +9,24 @@
 	"crypto/sha1"
 	"fmt"
 	"go/token"
+	"io/ioutil"
+	"os"
 	"reflect"
 	"strconv"
 	"sync/atomic"
+	"time"
 
 	"golang.org/x/tools/internal/event"
+	"golang.org/x/tools/internal/lsp/debug/tag"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/memoize"
 	"golang.org/x/tools/internal/span"
+	errors "golang.org/x/xerrors"
 )
 
 func New(ctx context.Context, options func(*source.Options)) *Cache {
 	index := atomic.AddInt64(&cacheIndex, 1)
 	c := &Cache{
-		fs:      &nativeFileSystem{},
 		id:      strconv.FormatInt(index, 10),
 		fset:    token.NewFileSet(),
 		options: options,
@@ -31,7 +35,6 @@
 }
 
 type Cache struct {
-	fs      source.FileSystem
 	id      string
 	fset    *token.FileSet
 	options func(*source.Options)
@@ -40,36 +43,65 @@
 }
 
 type fileKey struct {
-	identity source.FileIdentity
+	uri     span.URI
+	modTime time.Time
 }
 
 type fileHandle struct {
-	cache      *Cache
-	underlying source.FileHandle
-	handle     *memoize.Handle
-}
-
-type fileData struct {
+	uri span.URI
 	memoize.NoCopy
 	bytes []byte
 	hash  string
 	err   error
 }
 
-func (c *Cache) GetFile(uri span.URI) source.FileHandle {
-	underlying := c.fs.GetFile(uri)
+func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
+	var modTime time.Time
+	if fi, err := os.Stat(uri.Filename()); err == nil {
+		modTime = fi.ModTime()
+	}
+
 	key := fileKey{
-		identity: underlying.Identity(),
+		uri:     uri,
+		modTime: modTime,
 	}
 	h := c.store.Bind(key, func(ctx context.Context) interface{} {
-		data := &fileData{}
-		data.bytes, data.hash, data.err = underlying.Read(ctx)
-		return data
+		return readFile(ctx, uri, modTime)
 	})
+	v := h.Get(ctx)
+	if v == nil {
+		return nil, ctx.Err()
+	}
+	return v.(*fileHandle), nil
+}
+
+// ioLimit limits the number of parallel file reads per process.
+var ioLimit = make(chan struct{}, 128)
+
+func readFile(ctx context.Context, uri span.URI, origTime time.Time) *fileHandle {
+	ctx, done := event.Start(ctx, "cache.getFile", tag.File.Of(uri.Filename()))
+	_ = ctx
+	defer done()
+
+	ioLimit <- struct{}{}
+	defer func() { <-ioLimit }()
+
+	var modTime time.Time
+	if fi, err := os.Stat(uri.Filename()); err == nil {
+		modTime = fi.ModTime()
+	}
+
+	if modTime != origTime {
+		return &fileHandle{err: errors.Errorf("%s: file has been modified", uri.Filename())}
+	}
+	data, err := ioutil.ReadFile(uri.Filename())
+	if err != nil {
+		return &fileHandle{err: err}
+	}
 	return &fileHandle{
-		cache:      c,
-		underlying: underlying,
-		handle:     h,
+		uri:   uri,
+		bytes: data,
+		hash:  hashContents(data),
 	}
 }
 
@@ -89,21 +121,28 @@
 	return c.fset
 }
 
-func (h *fileHandle) FileSystem() source.FileSystem {
-	return h.cache
+func (h *fileHandle) URI() span.URI {
+	return h.uri
+}
+
+func (h *fileHandle) Kind() source.FileKind {
+	return source.DetectLanguage("", h.uri.Filename())
+}
+
+func (h *fileHandle) Version() float64 {
+	return 0
 }
 
 func (h *fileHandle) Identity() source.FileIdentity {
-	return h.underlying.Identity()
+	return source.FileIdentity{
+		URI:        h.uri,
+		Identifier: h.hash,
+		Kind:       h.Kind(),
+	}
 }
 
-func (h *fileHandle) Read(ctx context.Context) ([]byte, string, error) {
-	v := h.handle.Get(ctx)
-	if v == nil {
-		return nil, "", ctx.Err()
-	}
-	data := v.(*fileData)
-	return data.bytes, data.hash, data.err
+func (h *fileHandle) Read() ([]byte, error) {
+	return h.bytes, h.err
 }
 
 func hashContents(contents []byte) string {
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index 3f20557..ccc28f5 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -156,11 +156,11 @@
 		deps[depHandle.m.pkgPath] = depHandle
 		depKeys = append(depKeys, depHandle.key)
 	}
-	ph.key = checkPackageKey(ph.m.id, ph.compiledGoFiles, m.config, depKeys)
+	ph.key = checkPackageKey(ctx, ph.m.id, ph.compiledGoFiles, m.config, depKeys)
 	return ph, deps, nil
 }
 
-func checkPackageKey(id packageID, pghs []*parseGoHandle, cfg *packages.Config, deps []packageHandleKey) packageHandleKey {
+func checkPackageKey(ctx context.Context, id packageID, pghs []*parseGoHandle, cfg *packages.Config, deps []packageHandleKey) packageHandleKey {
 	var depBytes []byte
 	for _, dep := range deps {
 		depBytes = append(depBytes, []byte(dep)...)
@@ -254,15 +254,15 @@
 }
 
 func (s *snapshot) parseGoHandles(ctx context.Context, files []span.URI, mode source.ParseMode) ([]*parseGoHandle, error) {
-	phs := make([]*parseGoHandle, 0, len(files))
+	pghs := make([]*parseGoHandle, 0, len(files))
 	for _, uri := range files {
-		fh, err := s.GetFile(uri)
+		fh, err := s.GetFile(ctx, uri)
 		if err != nil {
 			return nil, err
 		}
-		phs = append(phs, s.view.session.cache.parseGoHandle(fh, mode))
+		pghs = append(pghs, s.view.session.cache.parseGoHandle(ctx, fh, mode))
 	}
-	return phs, nil
+	return pghs, nil
 }
 
 func typeCheck(ctx context.Context, fset *token.FileSet, m *metadata, mode source.ParseMode, goFiles, compiledGoFiles []*parseGoHandle, deps map[packagePath]*packageHandle) (*pkg, error) {
diff --git a/internal/lsp/cache/errors.go b/internal/lsp/cache/errors.go
index 14995b9..eff1c80 100644
--- a/internal/lsp/cache/errors.go
+++ b/internal/lsp/cache/errors.go
@@ -195,10 +195,7 @@
 	if err != nil {
 		return span.Span{}, err
 	}
-	if err != nil {
-		return span.Span{}, err
-	}
-	data, _, err := ph.File().Read(ctx)
+	data, err := ph.File().Read()
 	if err != nil {
 		return span.Span{}, err
 	}
@@ -221,7 +218,7 @@
 	}
 	tok := fset.File(file.Pos())
 	if tok == nil {
-		return span.Span{}, errors.Errorf("no token.File for %s", ph.File().Identity().URI)
+		return span.Span{}, errors.Errorf("no token.File for %s", ph.File().URI())
 	}
 	pos := tok.Pos(posn.Offset)
 	return span.NewRange(fset, pos, pos).Span()
diff --git a/internal/lsp/cache/external.go b/internal/lsp/cache/external.go
deleted file mode 100644
index 1f04d15..0000000
--- a/internal/lsp/cache/external.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright 2019 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"
-	"io/ioutil"
-	"os"
-
-	"golang.org/x/tools/internal/event"
-	"golang.org/x/tools/internal/lsp/debug/tag"
-	"golang.org/x/tools/internal/lsp/source"
-	"golang.org/x/tools/internal/span"
-	errors "golang.org/x/xerrors"
-)
-
-// ioLimit limits the number of parallel file reads per process.
-var ioLimit = make(chan struct{}, 128)
-
-// nativeFileSystem implements FileSystem reading from the normal os file system.
-type nativeFileSystem struct{}
-
-// nativeFileHandle implements FileHandle for nativeFileSystem
-type nativeFileHandle struct {
-	fs       *nativeFileSystem
-	identity source.FileIdentity
-}
-
-func (fs *nativeFileSystem) GetFile(uri span.URI) source.FileHandle {
-	return &nativeFileHandle{
-		fs: fs,
-		identity: source.FileIdentity{
-			URI:        uri,
-			Identifier: identifier(uri.Filename()),
-			Kind:       source.DetectLanguage("", uri.Filename()),
-		},
-	}
-}
-
-func (h *nativeFileHandle) FileSystem() source.FileSystem {
-	return h.fs
-}
-
-func (h *nativeFileHandle) Identity() source.FileIdentity {
-	return h.identity
-}
-
-func (h *nativeFileHandle) Read(ctx context.Context) ([]byte, string, error) {
-	ctx, done := event.Start(ctx, "cache.nativeFileHandle.Read", tag.File.Of(h.identity.URI.Filename()))
-	_ = ctx
-	defer done()
-
-	ioLimit <- struct{}{}
-	defer func() { <-ioLimit }()
-
-	if id := identifier(h.identity.URI.Filename()); id != h.identity.Identifier {
-		return nil, "", errors.Errorf("%s: file has been modified", h.identity.URI.Filename())
-	}
-	data, err := ioutil.ReadFile(h.identity.URI.Filename())
-	if err != nil {
-		return nil, "", err
-	}
-	return data, hashContents(data), nil
-}
-
-func identifier(filename string) string {
-	if fi, err := os.Stat(filename); err == nil {
-		return fi.ModTime().String()
-	}
-	return "DOES NOT EXIST"
-}
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index 8d25ac3..1fb4413 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -94,7 +94,7 @@
 }
 
 func (mh *modHandle) String() string {
-	return mh.File().Identity().URI.Filename()
+	return mh.File().URI().Filename()
 }
 
 func (mh *modHandle) File() source.FileHandle {
@@ -104,7 +104,7 @@
 func (mh *modHandle) Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error) {
 	v := mh.handle.Get(ctx)
 	if v == nil {
-		return nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
+		return nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI())
 	}
 	data := v.(*modData)
 	return data.origParsedFile, data.origMapper, data.err
@@ -113,7 +113,7 @@
 func (mh *modHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
 	v := mh.handle.Get(ctx)
 	if v == nil {
-		return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
+		return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI())
 	}
 	data := v.(*modData)
 	return data.origParsedFile, data.origMapper, data.upgrades, data.err
@@ -122,14 +122,14 @@
 func (mh *modHandle) Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
 	v := mh.handle.Get(ctx)
 	if v == nil {
-		return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
+		return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI())
 	}
 	data := v.(*modData)
 	return data.origParsedFile, data.origMapper, data.why, data.err
 }
 
 func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.ModHandle {
-	uri := fh.Identity().URI
+	uri := fh.URI()
 	if handle := s.getModHandle(uri); handle != nil {
 		return handle
 	}
@@ -137,7 +137,6 @@
 	realURI, tempURI := s.view.ModFiles()
 	folder := s.View().Folder().Filename()
 	cfg := s.Config(ctx)
-
 	key := modKey{
 		sessionID: s.view.session.id,
 		cfg:       hashConfig(cfg),
@@ -148,7 +147,7 @@
 		ctx, done := event.Start(ctx, "cache.ModHandle", tag.URI.Of(uri))
 		defer done()
 
-		contents, _, err := fh.Read(ctx)
+		contents, err := fh.Read()
 		if err != nil {
 			return &modData{
 				err: err,
@@ -275,7 +274,7 @@
 func (mh *modHandle) Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []source.Error, error) {
 	v := mh.handle.Get(ctx)
 	if v == nil {
-		return nil, nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
+		return nil, nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI())
 	}
 	data := v.(*modData)
 	return data.origParsedFile, data.origMapper, data.missingDeps, data.parseErrors, data.err
@@ -323,7 +322,7 @@
 		ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(realURI))
 		defer done()
 
-		realContents, _, err := realfh.Read(ctx)
+		realContents, err := realfh.Read()
 		if err != nil {
 			return &modData{
 				err: err,
@@ -480,7 +479,7 @@
 		}
 		// Handle unused dependencies.
 		if data.missingDeps[dep] == nil {
-			rng, err := rangeFromPositions(data.origfh.Identity().URI, data.origMapper, req.Syntax.Start, req.Syntax.End)
+			rng, err := rangeFromPositions(data.origfh.URI(), data.origMapper, req.Syntax.Start, req.Syntax.End)
 			if err != nil {
 				return nil, err
 			}
@@ -492,10 +491,10 @@
 				Category: ModTidyError,
 				Message:  fmt.Sprintf("%s is not used in this module.", dep),
 				Range:    rng,
-				URI:      data.origfh.Identity().URI,
+				URI:      data.origfh.URI(),
 				SuggestedFixes: []source.SuggestedFix{{
 					Title: fmt.Sprintf("Remove dependency: %s", dep),
-					Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
+					Edits: map[span.URI][]protocol.TextEdit{data.origfh.URI(): edits},
 				}},
 			})
 		}
@@ -505,7 +504,7 @@
 
 // modDirectnessErrors extracts errors when a dependency is labeled indirect when it should be direct and vice versa.
 func modDirectnessErrors(options source.Options, data *modData, req *modfile.Require) (source.Error, error) {
-	rng, err := rangeFromPositions(data.origfh.Identity().URI, data.origMapper, req.Syntax.Start, req.Syntax.End)
+	rng, err := rangeFromPositions(data.origfh.URI(), data.origMapper, req.Syntax.Start, req.Syntax.End)
 	if err != nil {
 		return source.Error{}, err
 	}
@@ -515,7 +514,7 @@
 			end := comments.Suffix[0].Start
 			end.LineRune += len(comments.Suffix[0].Token)
 			end.Byte += len([]byte(comments.Suffix[0].Token))
-			rng, err = rangeFromPositions(data.origfh.Identity().URI, data.origMapper, comments.Suffix[0].Start, end)
+			rng, err = rangeFromPositions(data.origfh.URI(), data.origMapper, comments.Suffix[0].Start, end)
 			if err != nil {
 				return source.Error{}, err
 			}
@@ -528,10 +527,10 @@
 			Category: ModTidyError,
 			Message:  fmt.Sprintf("%s should be a direct dependency.", req.Mod.Path),
 			Range:    rng,
-			URI:      data.origfh.Identity().URI,
+			URI:      data.origfh.URI(),
 			SuggestedFixes: []source.SuggestedFix{{
 				Title: fmt.Sprintf("Make %s direct", req.Mod.Path),
-				Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
+				Edits: map[span.URI][]protocol.TextEdit{data.origfh.URI(): edits},
 			}},
 		}, nil
 	}
@@ -544,10 +543,10 @@
 		Category: ModTidyError,
 		Message:  fmt.Sprintf("%s should be an indirect dependency.", req.Mod.Path),
 		Range:    rng,
-		URI:      data.origfh.Identity().URI,
+		URI:      data.origfh.URI(),
 		SuggestedFixes: []source.SuggestedFix{{
 			Title: fmt.Sprintf("Make %s indirect", req.Mod.Path),
-			Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
+			Edits: map[span.URI][]protocol.TextEdit{data.origfh.URI(): edits},
 		}},
 	}, nil
 }
@@ -576,7 +575,7 @@
 	// Reset the *modfile.File back to before we dropped the dependency.
 	data.origParsedFile.AddNewRequire(req.Mod.Path, req.Mod.Version, req.Indirect)
 	// Calculate the edits to be made due to the change.
-	diff := options.ComputeEdits(data.origfh.Identity().URI, string(data.origMapper.Content), string(newContents))
+	diff := options.ComputeEdits(data.origfh.URI(), string(data.origMapper.Content), string(newContents))
 	edits, err := source.ToProtocolEdits(data.origMapper, diff)
 	if err != nil {
 		return nil, err
@@ -624,7 +623,7 @@
 	}
 	data.origParsedFile.SetRequire(newReq)
 	// Calculate the edits to be made due to the change.
-	diff := options.ComputeEdits(data.origfh.Identity().URI, string(data.origMapper.Content), string(newContents))
+	diff := options.ComputeEdits(data.origfh.URI(), string(data.origMapper.Content), string(newContents))
 	edits, err := source.ToProtocolEdits(data.origMapper, diff)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index 81ef3c7..46aea45 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -50,11 +50,11 @@
 	err        error // any other errors
 }
 
-func (c *Cache) ParseGoHandle(fh source.FileHandle, mode source.ParseMode) source.ParseGoHandle {
-	return c.parseGoHandle(fh, mode)
+func (c *Cache) ParseGoHandle(ctx context.Context, fh source.FileHandle, mode source.ParseMode) source.ParseGoHandle {
+	return c.parseGoHandle(ctx, fh, mode)
 }
 
-func (c *Cache) parseGoHandle(fh source.FileHandle, mode source.ParseMode) *parseGoHandle {
+func (c *Cache) parseGoHandle(ctx context.Context, fh source.FileHandle, mode source.ParseMode) *parseGoHandle {
 	key := parseKey{
 		file: fh.Identity(),
 		mode: mode,
@@ -71,7 +71,7 @@
 }
 
 func (pgh *parseGoHandle) String() string {
-	return pgh.File().Identity().URI.Filename()
+	return pgh.File().URI().Filename()
 }
 
 func (pgh *parseGoHandle) File() source.FileHandle {
@@ -94,7 +94,7 @@
 	v := pgh.handle.Get(ctx)
 	data, ok := v.(*parseGoData)
 	if !ok {
-		return nil, errors.Errorf("no parsed file for %s", pgh.File().Identity().URI)
+		return nil, errors.Errorf("no parsed file for %s", pgh.File().URI())
 	}
 	return data, nil
 }
@@ -102,35 +102,29 @@
 func (pgh *parseGoHandle) Cached() (*ast.File, []byte, *protocol.ColumnMapper, error, error) {
 	v := pgh.handle.Cached()
 	if v == nil {
-		return nil, nil, nil, nil, errors.Errorf("no cached AST for %s", pgh.file.Identity().URI)
+		return nil, nil, nil, nil, errors.Errorf("no cached AST for %s", pgh.file.URI())
 	}
 	data := v.(*parseGoData)
 	return data.ast, data.src, data.mapper, data.parseError, data.err
 }
 
-func hashParseKey(ph source.ParseGoHandle) string {
+func hashParseKeys(pghs []*parseGoHandle) string {
 	b := bytes.NewBuffer(nil)
-	b.WriteString(ph.File().Identity().String())
-	b.WriteString(string(rune(ph.Mode())))
-	return hashContents(b.Bytes())
-}
-
-func hashParseKeys(phs []*parseGoHandle) string {
-	b := bytes.NewBuffer(nil)
-	for _, ph := range phs {
-		b.WriteString(hashParseKey(ph))
+	for _, pgh := range pghs {
+		b.WriteString(pgh.file.Identity().String())
+		b.WriteByte(byte(pgh.Mode()))
 	}
 	return hashContents(b.Bytes())
 }
 
 func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) *parseGoData {
-	ctx, done := event.Start(ctx, "cache.parseGo", tag.File.Of(fh.Identity().URI.Filename()))
+	ctx, done := event.Start(ctx, "cache.parseGo", tag.File.Of(fh.URI().Filename()))
 	defer done()
 
-	if fh.Identity().Kind != source.Go {
-		return &parseGoData{err: errors.Errorf("cannot parse non-Go file %s", fh.Identity().URI)}
+	if fh.Kind() != source.Go {
+		return &parseGoData{err: errors.Errorf("cannot parse non-Go file %s", fh.URI())}
 	}
-	buf, _, err := fh.Read(ctx)
+	buf, err := fh.Read()
 	if err != nil {
 		return &parseGoData{err: err}
 	}
@@ -139,13 +133,13 @@
 	if mode == source.ParseHeader {
 		parserMode = parser.ImportsOnly | parser.ParseComments
 	}
-	file, parseError := parser.ParseFile(fset, fh.Identity().URI.Filename(), buf, parserMode)
+	file, parseError := parser.ParseFile(fset, fh.URI().Filename(), buf, parserMode)
 	var tok *token.File
 	var fixed bool
 	if file != nil {
 		tok = fset.File(file.Pos())
 		if tok == nil {
-			return &parseGoData{err: errors.Errorf("successfully parsed but no token.File for %s (%v)", fh.Identity().URI, parseError)}
+			return &parseGoData{err: errors.Errorf("successfully parsed but no token.File for %s (%v)", fh.URI(), parseError)}
 		}
 
 		// Fix any badly parsed parts of the AST.
@@ -154,7 +148,7 @@
 		// Fix certain syntax errors that render the file unparseable.
 		newSrc := fixSrc(file, tok, buf)
 		if newSrc != nil {
-			newFile, _ := parser.ParseFile(fset, fh.Identity().URI.Filename(), newSrc, parserMode)
+			newFile, _ := parser.ParseFile(fset, fh.URI().Filename(), newSrc, parserMode)
 			if newFile != nil {
 				// Maintain the original parseError so we don't try formatting the doctored file.
 				file = newFile
@@ -174,12 +168,12 @@
 		// the parse errors are the actual errors.
 		err := parseError
 		if err == nil {
-			err = errors.Errorf("no AST for %s", fh.Identity().URI)
+			err = errors.Errorf("no AST for %s", fh.URI())
 		}
 		return &parseGoData{parseError: parseError, err: err}
 	}
 	m := &protocol.ColumnMapper{
-		URI:       fh.Identity().URI,
+		URI:       fh.URI(),
 		Converter: span.NewTokenConverter(fset, tok),
 		Content:   buf,
 	}
diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go
index b69e7bf..bbfa04a1 100644
--- a/internal/lsp/cache/pkg.go
+++ b/internal/lsp/cache/pkg.go
@@ -61,12 +61,12 @@
 
 func (p *pkg) File(uri span.URI) (source.ParseGoHandle, error) {
 	for _, ph := range p.compiledGoFiles {
-		if ph.File().Identity().URI == uri {
+		if ph.File().URI() == uri {
 			return ph, nil
 		}
 	}
 	for _, ph := range p.goFiles {
-		if ph.File().Identity().URI == uri {
+		if ph.File().URI() == uri {
 			return ph, nil
 		}
 	}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 2787cd6..543f6ff 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -46,8 +46,8 @@
 	saved bool
 }
 
-func (o *overlay) FileSystem() source.FileSystem {
-	return o.session
+func (o *overlay) Read() ([]byte, error) {
+	return o.text, nil
 }
 
 func (o *overlay) Identity() source.FileIdentity {
@@ -60,8 +60,16 @@
 	}
 }
 
-func (o *overlay) Read(ctx context.Context) ([]byte, string, error) {
-	return o.text, o.hash, nil
+func (o *overlay) Kind() source.FileKind {
+	return o.kind
+}
+
+func (o *overlay) URI() span.URI {
+	return o.uri
+}
+
+func (o *overlay) Version() float64 {
+	return o.version
 }
 
 func (o *overlay) Session() source.Session { return o.session }
@@ -329,7 +337,11 @@
 			if o, ok := overlays[c.URI]; ok {
 				views[view][c.URI] = o
 			} else {
-				views[view][c.URI] = s.cache.GetFile(c.URI)
+				fh, err := s.cache.GetFile(ctx, c.URI)
+				if err != nil {
+					return nil, err
+				}
+				views[view][c.URI] = fh
 			}
 		}
 	}
@@ -390,8 +402,12 @@
 		var sameContentOnDisk bool
 		switch c.Action {
 		case source.Open:
-			_, h, err := s.cache.GetFile(c.URI).Read(ctx)
-			sameContentOnDisk = (err == nil && h == hash)
+			fh, err := s.cache.GetFile(ctx, c.URI)
+			if err != nil {
+				return nil, err
+			}
+			_, readErr := fh.Read()
+			sameContentOnDisk = (readErr == nil && fh.Identity().Identifier == hash)
 		case source.Save:
 			// Make sure the version and content (if present) is the same.
 			if o.version != c.Version {
@@ -424,13 +440,12 @@
 	return overlays, nil
 }
 
-// GetFile implements the source.FileSystem interface.
-func (s *Session) GetFile(uri span.URI) source.FileHandle {
+func (s *Session) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
 	if overlay := s.readOverlay(uri); overlay != nil {
-		return overlay
+		return overlay, nil
 	}
 	// Fall back to the cache-level file system.
-	return s.cache.GetFile(uri)
+	return s.cache.GetFile(ctx, uri)
 }
 
 func (s *Session) readOverlay(uri span.URI) *overlay {
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 7009564..cc975bb 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -161,16 +161,16 @@
 }
 
 func (s *snapshot) PackageHandles(ctx context.Context, fh source.FileHandle) ([]source.PackageHandle, error) {
-	if fh.Identity().Kind != source.Go {
+	if fh.Kind() != source.Go {
 		panic("called PackageHandles on a non-Go FileHandle")
 	}
 
-	ctx = event.Label(ctx, tag.URI.Of(fh.Identity().URI))
+	ctx = event.Label(ctx, tag.URI.Of(fh.URI()))
 
 	// Check if we should reload metadata for the file. We don't invalidate IDs
 	// (though we should), so the IDs will be a better source of truth than the
 	// metadata. If there are no IDs for the file, then we should also reload.
-	ids := s.getIDsForURI(fh.Identity().URI)
+	ids := s.getIDsForURI(fh.URI())
 	reload := len(ids) == 0
 	for _, id := range ids {
 		// Reload package metadata if any of the metadata has missing
@@ -185,13 +185,13 @@
 		// calls to packages.Load. Determine what we should do instead.
 	}
 	if reload {
-		if err := s.load(ctx, fileURI(fh.Identity().URI)); err != nil {
+		if err := s.load(ctx, fileURI(fh.URI())); err != nil {
 			return nil, err
 		}
 	}
 	// Get the list of IDs from the snapshot again, in case it has changed.
 	var phs []source.PackageHandle
-	for _, id := range s.getIDsForURI(fh.Identity().URI) {
+	for _, id := range s.getIDsForURI(fh.URI()) {
 		ph, err := s.packageHandle(ctx, id, source.ParseFull)
 		if err != nil {
 			return nil, err
@@ -522,7 +522,7 @@
 
 // GetFile returns a File for the given URI. It will always succeed because it
 // adds the file to the managed set if needed.
-func (s *snapshot) GetFile(uri span.URI) (source.FileHandle, error) {
+func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
 	f, err := s.view.getFile(uri)
 	if err != nil {
 		return nil, err
@@ -531,10 +531,16 @@
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	if _, ok := s.files[f.URI()]; !ok {
-		s.files[f.URI()] = s.view.session.cache.GetFile(uri)
+	if fh, ok := s.files[f.URI()]; ok {
+		return fh, nil
 	}
-	return s.files[f.URI()], nil
+
+	fh, err := s.view.session.cache.GetFile(ctx, uri)
+	if err != nil {
+		return nil, err
+	}
+	s.files[f.URI()] = fh
+	return fh, nil
 }
 
 func (s *snapshot) IsOpen(uri span.URI) bool {
@@ -644,7 +650,7 @@
 	scopeSet := make(map[span.URI]struct{})
 	for uri, fh := range s.files {
 		// Don't try to reload metadata for go.mod files.
-		if fh.Identity().Kind != source.Go {
+		if fh.Kind() != source.Go {
 			continue
 		}
 		// If the URI doesn't belong to this view, then it's not in a workspace
@@ -733,7 +739,7 @@
 		if invalidateMetadata || fileWasSaved(originalFH, currentFH) {
 			result.modTidyHandle = nil
 		}
-		if currentFH.Identity().Kind == source.Mod {
+		if currentFH.Kind() == source.Mod {
 			// If the view's go.mod file's contents have changed, invalidate the metadata
 			// for all of the packages in the workspace.
 			if invalidateMetadata {
@@ -780,7 +786,7 @@
 		}
 
 		// Handle the invalidated file; it may have new contents or not exist.
-		if _, _, err := currentFH.Read(ctx); os.IsNotExist(err) {
+		if _, err := currentFH.Read(); os.IsNotExist(err) {
 			delete(result.files, withoutURI)
 		} else {
 			result.files[withoutURI] = currentFH
@@ -858,20 +864,20 @@
 // determine if the file requires a metadata reload.
 func (s *snapshot) shouldInvalidateMetadata(ctx context.Context, originalFH, currentFH source.FileHandle) bool {
 	if originalFH == nil {
-		return currentFH.Identity().Kind == source.Go
+		return currentFH.Kind() == source.Go
 	}
 	// If the file hasn't changed, there's no need to reload.
 	if originalFH.Identity().String() == currentFH.Identity().String() {
 		return false
 	}
 	// If a go.mod file's contents have changed, always invalidate metadata.
-	if kind := originalFH.Identity().Kind; kind == source.Mod {
+	if kind := originalFH.Kind(); kind == source.Mod {
 		modfile, _ := s.view.ModFiles()
-		return originalFH.Identity().URI == modfile
+		return originalFH.URI() == modfile
 	}
 	// Get the original and current parsed files in order to check package name and imports.
-	original, _, _, _, originalErr := s.view.session.cache.ParseGoHandle(originalFH, source.ParseHeader).Parse(ctx)
-	current, _, _, _, currentErr := s.view.session.cache.ParseGoHandle(currentFH, source.ParseHeader).Parse(ctx)
+	original, _, _, _, originalErr := s.view.session.cache.ParseGoHandle(ctx, originalFH, source.ParseHeader).Parse(ctx)
+	current, _, _, _, currentErr := s.view.session.cache.ParseGoHandle(ctx, currentFH, source.ParseHeader).Parse(ctx)
 	if originalErr != nil || currentErr != nil {
 		return (originalErr == nil) != (currentErr == nil)
 	}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 339debb..76bb7aae 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -251,9 +251,13 @@
 
 	// Get the FileHandle through the cache to avoid adding it to the snapshot
 	// and to get the file content from disk.
-	pgh := v.session.cache.ParseGoHandle(v.session.cache.GetFile(uri), source.ParseFull)
+	fh, err := v.session.cache.GetFile(ctx, uri)
+	if err != nil {
+		return err
+	}
+	pgh := v.session.cache.ParseGoHandle(ctx, fh, source.ParseFull)
 	fset := v.session.cache.fset
-	h := v.session.cache.store.Bind(pgh.File().Identity(), func(ctx context.Context) interface{} {
+	h := v.session.cache.store.Bind(fh.Identity(), func(ctx context.Context) interface{} {
 		data := &builtinPackageData{}
 		file, _, _, _, err := pgh.Parse(ctx)
 		if err != nil {
@@ -261,7 +265,7 @@
 			return data
 		}
 		data.pkg, data.err = ast.NewPackage(fset, map[string]*ast.File{
-			pgh.File().Identity().URI.Filename(): file,
+			pgh.File().URI().Filename(): file,
 		}, nil, nil)
 		return data
 	})
@@ -305,7 +309,11 @@
 
 	// In module mode, check if the mod file has changed.
 	if v.realMod != "" {
-		if mod := v.session.cache.GetFile(v.realMod); mod.Identity() != v.cachedModFileVersion {
+		mod, err := v.session.cache.GetFile(ctx, v.realMod)
+		if err != nil {
+			return err
+		}
+		if mod.Identity() != v.cachedModFileVersion {
 			v.processEnv.GetResolver().(*imports.ModuleResolver).ClearForNewMod()
 			v.cachedModFileVersion = mod.Identity()
 		}
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 25c6d51..178b9b3 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -18,16 +18,16 @@
 )
 
 func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
 	if !ok {
 		return nil, err
 	}
-	uri := fh.Identity().URI
+	uri := fh.URI()
 
 	// Determine the supported actions for this file kind.
-	supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[fh.Identity().Kind]
+	supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[fh.Kind()]
 	if !ok {
-		return nil, fmt.Errorf("no supported code actions for %v file kind", fh.Identity().Kind)
+		return nil, fmt.Errorf("no supported code actions for %v file kind", fh.Kind())
 	}
 
 	// The Only field of the context specifies which code actions the client wants.
@@ -46,7 +46,7 @@
 	}
 
 	var codeActions []protocol.CodeAction
-	switch fh.Identity().Kind {
+	switch fh.Kind() {
 	case source.Mod:
 		if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
 			modFixes, err := mod.SuggestedFixes(ctx, snapshot, fh, diagnostics)
@@ -62,7 +62,7 @@
 				Command: &protocol.Command{
 					Title:     "Tidy",
 					Command:   "tidy",
-					Arguments: []interface{}{fh.Identity().URI},
+					Arguments: []interface{}{fh.URI()},
 				},
 			})
 		}
@@ -285,7 +285,7 @@
 				Edit:        protocol.WorkspaceEdit{},
 			}
 			for uri, edits := range fix.Edits {
-				fh, err := snapshot.GetFile(uri)
+				fh, err := snapshot.GetFile(ctx, uri)
 				if err != nil {
 					return nil, nil, err
 				}
@@ -346,9 +346,9 @@
 	return []protocol.TextDocumentEdit{
 		{
 			TextDocument: protocol.VersionedTextDocumentIdentifier{
-				Version: fh.Identity().Version,
+				Version: fh.Version(),
 				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-					URI: protocol.URIFromSpanURI(fh.Identity().URI),
+					URI: protocol.URIFromSpanURI(fh.URI()),
 				},
 			},
 			Edits: edits,
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 2af2fd6..9a77947 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -40,11 +40,11 @@
 		if err != nil {
 			return nil, err
 		}
-		snapshot, fh, ok, err := s.beginFileRequest(protocol.DocumentURI(uri), source.Go)
+		snapshot, fh, ok, err := s.beginFileRequest(ctx, protocol.DocumentURI(uri), source.Go)
 		if !ok {
 			return nil, err
 		}
-		dir := filepath.Dir(fh.Identity().URI.Filename())
+		dir := filepath.Dir(fh.URI().Filename())
 		go s.runTest(ctx, funcName, dir, snapshot)
 	case source.CommandGenerate:
 		dir, recursive, err := getGenerateRequest(params.Arguments)
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index f6d3049..bca9a83 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -16,13 +16,13 @@
 )
 
 func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
 	if !ok {
 		return nil, err
 	}
 	var candidates []source.CompletionItem
 	var surrounding *source.Selection
-	switch fh.Identity().Kind {
+	switch fh.Kind() {
 	case source.Go:
 		candidates, surrounding, err = source.Completion(ctx, snapshot, fh, params.Position)
 	case source.Mod:
diff --git a/internal/lsp/definition.go b/internal/lsp/definition.go
index 3943877..427501b 100644
--- a/internal/lsp/definition.go
+++ b/internal/lsp/definition.go
@@ -12,7 +12,7 @@
 )
 
 func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
@@ -38,7 +38,7 @@
 }
 
 func (s *Server) typeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index da55688..64e31c1 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -115,8 +115,8 @@
 			defer wg.Done()
 			// Only run analyses for packages with open files.
 			withAnalyses := alwaysAnalyze
-			for _, fh := range ph.CompiledGoFiles() {
-				if snapshot.IsOpen(fh.File().Identity().URI) {
+			for _, pgh := range ph.CompiledGoFiles() {
+				if snapshot.IsOpen(pgh.File().URI()) {
 					withAnalyses = true
 				}
 			}
diff --git a/internal/lsp/folding_range.go b/internal/lsp/folding_range.go
index 5bae8f5..2de472e 100644
--- a/internal/lsp/folding_range.go
+++ b/internal/lsp/folding_range.go
@@ -8,7 +8,7 @@
 )
 
 func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index f9bf5aa..5251d59 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -13,11 +13,11 @@
 )
 
 func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
 	if !ok {
 		return nil, err
 	}
-	switch fh.Identity().Kind {
+	switch fh.Kind() {
 	case source.Mod:
 		return mod.Format(ctx, snapshot, fh)
 	case source.Go:
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 385b122d..89dff8d 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -269,7 +269,7 @@
 // it to a snapshot.
 // 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.
-func (s *Server) beginFileRequest(pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.FileHandle, bool, error) {
+func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.FileHandle, bool, error) {
 	uri := pURI.SpanURI()
 	if !uri.IsFile() {
 		// Not a file URI. Stop processing the request, but don't return an error.
@@ -280,11 +280,11 @@
 		return nil, nil, false, err
 	}
 	snapshot := view.Snapshot()
-	fh, err := snapshot.GetFile(uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return nil, nil, false, err
 	}
-	if expectKind != source.UnknownKind && fh.Identity().Kind != expectKind {
+	if expectKind != source.UnknownKind && fh.Kind() != expectKind {
 		// Wrong kind of file. Nothing to do.
 		return nil, nil, false, nil
 	}
diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go
index ef72c54..5e5a985 100644
--- a/internal/lsp/highlight.go
+++ b/internal/lsp/highlight.go
@@ -14,7 +14,7 @@
 )
 
 func (s *Server) documentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go
index 32af7e2..e9eaddf 100644
--- a/internal/lsp/hover.go
+++ b/internal/lsp/hover.go
@@ -13,11 +13,11 @@
 )
 
 func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
 	if !ok {
 		return nil, err
 	}
-	switch fh.Identity().Kind {
+	switch fh.Kind() {
 	case source.Mod:
 		return mod.Hover(ctx, snapshot, fh, params.Position)
 	case source.Go:
diff --git a/internal/lsp/implementation.go b/internal/lsp/implementation.go
index e4b3650..9250232 100644
--- a/internal/lsp/implementation.go
+++ b/internal/lsp/implementation.go
@@ -12,7 +12,7 @@
 )
 
 func (s *Server) implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index ddb4e76..73865bd 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -25,11 +25,11 @@
 )
 
 func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) (links []protocol.DocumentLink, err error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
 	if !ok {
 		return nil, err
 	}
-	switch fh.Identity().Kind {
+	switch fh.Kind() {
 	case source.Mod:
 		links, err = modLinks(ctx, snapshot, fh)
 	case source.Go:
@@ -37,7 +37,7 @@
 	}
 	// Don't return errors for document links.
 	if err != nil {
-		event.Error(ctx, "failed to compute document links", err, tag.URI.Of(fh.Identity().URI))
+		event.Error(ctx, "failed to compute document links", err, tag.URI.Of(fh.URI()))
 		return nil, nil
 	}
 	return links, nil
@@ -100,7 +100,8 @@
 	if err != nil {
 		return nil, err
 	}
-	file, _, m, _, err := view.Session().Cache().ParseGoHandle(fh, source.ParseFull).Parse(ctx)
+	pgh := view.Session().Cache().ParseGoHandle(ctx, fh, source.ParseFull)
+	file, _, m, _, err := pgh.Parse(ctx)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index 39ca193..8f117dc 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -29,7 +29,7 @@
 	ctx, done := event.Start(ctx, "mod.CodeLens", tag.URI.Of(realURI))
 	defer done()
 
-	fh, err := snapshot.GetFile(realURI)
+	fh, err := snapshot.GetFile(ctx, realURI)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 0b39f83..247586f 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -27,7 +27,7 @@
 	ctx, done := event.Start(ctx, "mod.Diagnostics", tag.URI.Of(realURI))
 	defer done()
 
-	realfh, err := snapshot.GetFile(realURI)
+	realfh, err := snapshot.GetFile(ctx, realURI)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -88,15 +88,15 @@
 					Edit:        protocol.WorkspaceEdit{},
 				}
 				for uri, edits := range fix.Edits {
-					fh, err := snapshot.GetFile(uri)
+					fh, err := snapshot.GetFile(ctx, uri)
 					if err != nil {
 						return nil, err
 					}
 					action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, protocol.TextDocumentEdit{
 						TextDocument: protocol.VersionedTextDocumentIdentifier{
-							Version: fh.Identity().Version,
+							Version: fh.Version(),
 							TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-								URI: protocol.URIFromSpanURI(fh.Identity().URI),
+								URI: protocol.URIFromSpanURI(fh.URI()),
 							},
 						},
 						Edits: edits,
@@ -120,7 +120,7 @@
 	ctx, done := event.Start(ctx, "mod.SuggestedGoFixes", tag.URI.Of(realURI))
 	defer done()
 
-	realfh, err := snapshot.GetFile(realURI)
+	realfh, err := snapshot.GetFile(ctx, realURI)
 	if err != nil {
 		return nil, err
 	}
@@ -136,7 +136,7 @@
 		return nil, nil
 	}
 	// Get the contents of the go.mod file before we make any changes.
-	oldContents, _, err := realfh.Read(ctx)
+	oldContents, err := realfh.Read()
 	if err != nil {
 		return nil, err
 	}
@@ -156,16 +156,16 @@
 			return nil, err
 		}
 		// Calculate the edits to be made due to the change.
-		diff := snapshot.View().Options().ComputeEdits(realfh.Identity().URI, string(oldContents), string(newContents))
+		diff := snapshot.View().Options().ComputeEdits(realfh.URI(), string(oldContents), string(newContents))
 		edits, err := source.ToProtocolEdits(realMapper, diff)
 		if err != nil {
 			return nil, err
 		}
 		textDocumentEdits[dep] = protocol.TextDocumentEdit{
 			TextDocument: protocol.VersionedTextDocumentIdentifier{
-				Version: realfh.Identity().Version,
+				Version: realfh.Version(),
 				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-					URI: protocol.URIFromSpanURI(realfh.Identity().URI),
+					URI: protocol.URIFromSpanURI(realfh.URI()),
 				},
 			},
 			Edits: edits,
diff --git a/internal/lsp/mod/format.go b/internal/lsp/mod/format.go
index d1c567e..a635cb8 100644
--- a/internal/lsp/mod/format.go
+++ b/internal/lsp/mod/format.go
@@ -21,6 +21,6 @@
 		return nil, err
 	}
 	// Calculate the edits to be made due to the change.
-	diff := snapshot.View().Options().ComputeEdits(fh.Identity().URI, string(m.Content), string(formatted))
+	diff := snapshot.View().Options().ComputeEdits(fh.URI(), string(m.Content), string(formatted))
 	return source.ToProtocolEdits(m, diff)
 }
diff --git a/internal/lsp/mod/hover.go b/internal/lsp/mod/hover.go
index 2439002..97d3dd3 100644
--- a/internal/lsp/mod/hover.go
+++ b/internal/lsp/mod/hover.go
@@ -17,7 +17,7 @@
 func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
 	realURI, _ := snapshot.View().ModFiles()
 	// Only get hover information on the go.mod for the view.
-	if realURI == "" || fh.Identity().URI != realURI {
+	if realURI == "" || fh.URI() != realURI {
 		return nil, nil
 	}
 	ctx, done := event.Start(ctx, "mod.Hover")
@@ -74,7 +74,7 @@
 	}
 	end := span.NewPoint(line, col, endPos)
 
-	spn = span.New(fh.Identity().URI, start, end)
+	spn = span.New(fh.URI(), start, end)
 	rng, err := m.Range(spn)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/references.go b/internal/lsp/references.go
index 8a5700d..8d65bf9 100644
--- a/internal/lsp/references.go
+++ b/internal/lsp/references.go
@@ -12,7 +12,7 @@
 )
 
 func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
diff --git a/internal/lsp/rename.go b/internal/lsp/rename.go
index 9fe59e9..cbd06ec 100644
--- a/internal/lsp/rename.go
+++ b/internal/lsp/rename.go
@@ -12,7 +12,7 @@
 )
 
 func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
@@ -23,7 +23,7 @@
 
 	var docChanges []protocol.TextDocumentEdit
 	for uri, e := range edits {
-		fh, err := snapshot.GetFile(uri)
+		fh, err := snapshot.GetFile(ctx, uri)
 		if err != nil {
 			return nil, err
 		}
@@ -35,7 +35,7 @@
 }
 
 func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.Range, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 77d6bc7..5366303 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -94,13 +94,13 @@
 }
 
 func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
 	if !ok {
 		return nil, err
 	}
-	switch fh.Identity().Kind {
+	switch fh.Kind() {
 	case source.Mod:
-		return mod.CodeLens(ctx, snapshot, fh.Identity().URI)
+		return mod.CodeLens(ctx, snapshot, fh.URI())
 	case source.Go:
 		return source.CodeLens(ctx, snapshot, fh)
 	}
@@ -112,17 +112,17 @@
 	paramMap := params.(map[string]interface{})
 	if method == "gopls/diagnoseFiles" {
 		for _, file := range paramMap["files"].([]interface{}) {
-			snapshot, fh, ok, err := s.beginFileRequest(protocol.DocumentURI(file.(string)), source.UnknownKind)
+			snapshot, fh, ok, err := s.beginFileRequest(ctx, protocol.DocumentURI(file.(string)), source.UnknownKind)
 			if !ok {
 				return nil, err
 			}
 
-			fileID, diagnostics, err := source.FileDiagnostics(ctx, snapshot, fh.Identity().URI)
+			fileID, diagnostics, err := source.FileDiagnostics(ctx, snapshot, fh.URI())
 			if err != nil {
 				return nil, err
 			}
 			if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
-				URI:         protocol.URIFromSpanURI(fh.Identity().URI),
+				URI:         protocol.URIFromSpanURI(fh.URI()),
 				Diagnostics: toProtocolDiagnostics(diagnostics),
 				Version:     fileID.Version,
 			}); err != nil {
diff --git a/internal/lsp/signature_help.go b/internal/lsp/signature_help.go
index 4739548..87dfeb0 100644
--- a/internal/lsp/signature_help.go
+++ b/internal/lsp/signature_help.go
@@ -14,7 +14,7 @@
 )
 
 func (s *Server) signatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) {
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return nil, err
 	}
diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go
index f95c171..3de0647 100644
--- a/internal/lsp/source/code_lens.go
+++ b/internal/lsp/source/code_lens.go
@@ -26,7 +26,8 @@
 
 // CodeLens computes code lens for Go source code.
 func CodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
-	f, _, m, _, err := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull).Parse(ctx)
+	pgh := snapshot.View().Session().Cache().ParseGoHandle(ctx, fh, ParseFull)
+	f, _, m, _, err := pgh.Parse(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -57,7 +58,7 @@
 		return nil, err
 	}
 
-	if !strings.HasSuffix(fh.Identity().URI.Filename(), "_test.go") {
+	if !strings.HasSuffix(fh.URI().Filename(), "_test.go") {
 		return nil, nil
 	}
 
@@ -74,7 +75,7 @@
 				return nil, err
 			}
 
-			uri := fh.Identity().URI
+			uri := fh.URI()
 			codeLens = append(codeLens, protocol.CodeLens{
 				Range: rng,
 				Command: protocol.Command{
@@ -137,7 +138,7 @@
 			if err != nil {
 				return nil, err
 			}
-			dir := filepath.Dir(fh.Identity().URI.Filename())
+			dir := filepath.Dir(fh.URI().Filename())
 			return []protocol.CodeLens{
 				{
 					Range: rng,
@@ -183,7 +184,7 @@
 			Command: protocol.Command{
 				Title:     "regenerate cgo definitions",
 				Command:   CommandRegenerateCgo,
-				Arguments: []interface{}{fh.Identity().URI},
+				Arguments: []interface{}{fh.URI()},
 			},
 		},
 	}, nil
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 034e5c2..2c7a3af 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -491,7 +491,7 @@
 		pkg:                       pkg,
 		snapshot:                  snapshot,
 		qf:                        qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo()),
-		filename:                  fh.Identity().URI.Filename(),
+		filename:                  fh.URI().Filename(),
 		file:                      file,
 		path:                      path,
 		pos:                       pos,
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index a36bebd..95cd014 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -216,7 +216,7 @@
 	uri := span.URIFromPath(c.filename)
 	var ph ParseGoHandle
 	for _, h := range c.pkg.CompiledGoFiles() {
-		if h.File().Identity().URI == uri {
+		if h.File().URI() == uri {
 			ph = h
 		}
 	}
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index d1ba85c..2d59d5a 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -83,10 +83,10 @@
 
 	// Prepare the reports we will send for the files in this package.
 	reports := make(map[FileIdentity][]*Diagnostic)
-	for _, fh := range pkg.CompiledGoFiles() {
-		clearReports(snapshot, reports, fh.File().Identity().URI)
+	for _, pgh := range pkg.CompiledGoFiles() {
+		clearReports(ctx, snapshot, reports, pgh.File().URI())
 		if len(missing) > 0 {
-			if err := missingModulesDiagnostics(ctx, snapshot, reports, missing, fh.File().Identity().URI); err != nil {
+			if err := missingModulesDiagnostics(ctx, snapshot, reports, missing, pgh.File().URI()); err != nil {
 				return nil, warn, err
 			}
 		}
@@ -99,13 +99,13 @@
 		}
 		// If no file is associated with the error, pick an open file from the package.
 		if e.URI.Filename() == "" {
-			for _, ph := range pkg.CompiledGoFiles() {
-				if snapshot.IsOpen(ph.File().Identity().URI) {
-					e.URI = ph.File().Identity().URI
+			for _, pgh := range pkg.CompiledGoFiles() {
+				if snapshot.IsOpen(pgh.File().URI()) {
+					e.URI = pgh.File().URI()
 				}
 			}
 		}
-		clearReports(snapshot, reports, e.URI)
+		clearReports(ctx, snapshot, reports, e.URI)
 	}
 	// Run diagnostics for the package that this URI belongs to.
 	hadDiagnostics, hadTypeErrors, err := diagnostics(ctx, snapshot, reports, pkg, len(ph.MissingDependencies()) > 0)
@@ -135,7 +135,7 @@
 }
 
 func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (FileIdentity, []*Diagnostic, error) {
-	fh, err := snapshot.GetFile(uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return FileIdentity{}, nil, err
 	}
@@ -205,7 +205,7 @@
 		} else if len(set.typeErrors) > 0 {
 			hasTypeErrors = true
 		}
-		if err := addReports(snapshot, reports, uri, diags...); err != nil {
+		if err := addReports(ctx, snapshot, reports, uri, diags...); err != nil {
 			return false, false, err
 		}
 	}
@@ -216,11 +216,12 @@
 	if snapshot.View().Ignore(uri) || len(missingModules) == 0 {
 		return nil
 	}
-	fh, err := snapshot.GetFile(uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return err
 	}
-	file, _, m, _, err := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseHeader).Parse(ctx)
+	pgh := snapshot.View().Session().Cache().ParseGoHandle(ctx, fh, ParseHeader)
+	file, _, m, _, err := pgh.Parse(ctx)
 	if err != nil {
 		return err
 	}
@@ -289,7 +290,7 @@
 		if onlyDeletions(e.SuggestedFixes) {
 			tags = append(tags, protocol.Unnecessary)
 		}
-		if err := addReports(snapshot, reports, e.URI, &Diagnostic{
+		if err := addReports(ctx, snapshot, reports, e.URI, &Diagnostic{
 			Range:    e.Range,
 			Message:  e.Message,
 			Source:   e.Category,
@@ -303,7 +304,7 @@
 	return nil
 }
 
-func clearReports(snapshot Snapshot, reports map[FileIdentity][]*Diagnostic, uri span.URI) {
+func clearReports(ctx context.Context, snapshot Snapshot, reports map[FileIdentity][]*Diagnostic, uri span.URI) {
 	if snapshot.View().Ignore(uri) {
 		return
 	}
@@ -314,7 +315,7 @@
 	reports[fh.Identity()] = []*Diagnostic{}
 }
 
-func addReports(snapshot Snapshot, reports map[FileIdentity][]*Diagnostic, uri span.URI, diagnostics ...*Diagnostic) error {
+func addReports(ctx context.Context, snapshot Snapshot, reports map[FileIdentity][]*Diagnostic, uri span.URI, diagnostics ...*Diagnostic) error {
 	if snapshot.View().Ignore(uri) {
 		return nil
 	}
@@ -322,8 +323,7 @@
 	if fh == nil {
 		return nil
 	}
-	identity := fh.Identity()
-	existingDiagnostics, ok := reports[identity]
+	existingDiagnostics, ok := reports[fh.Identity()]
 	if !ok {
 		return fmt.Errorf("diagnostics for unexpected file %s", uri)
 	}
@@ -337,7 +337,7 @@
 				if d1.Message != d2.Message {
 					continue
 				}
-				reports[identity][i].Tags = append(reports[identity][i].Tags, d1.Tags...)
+				reports[fh.Identity()][i].Tags = append(reports[fh.Identity()][i].Tags, d1.Tags...)
 			}
 			return nil
 		}
diff --git a/internal/lsp/source/fill_struct.go b/internal/lsp/source/fill_struct.go
index c412a74..e44c53b 100644
--- a/internal/lsp/source/fill_struct.go
+++ b/internal/lsp/source/fill_struct.go
@@ -123,9 +123,9 @@
 				DocumentChanges: []protocol.TextDocumentEdit{
 					{
 						TextDocument: protocol.VersionedTextDocumentIdentifier{
-							Version: fh.Identity().Version,
+							Version: fh.Version(),
 							TextDocumentIdentifier: protocol.TextDocumentIdentifier{
-								URI: protocol.URIFromSpanURI(fh.Identity().URI),
+								URI: protocol.URIFromSpanURI(fh.URI()),
 							},
 						},
 						Edits: []protocol.TextEdit{
diff --git a/internal/lsp/source/folding_range.go b/internal/lsp/source/folding_range.go
index 99f0aa5..74ad3e6 100644
--- a/internal/lsp/source/folding_range.go
+++ b/internal/lsp/source/folding_range.go
@@ -18,7 +18,7 @@
 func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, 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.
-	pgh := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull)
+	pgh := snapshot.View().Session().Cache().ParseGoHandle(ctx, fh, ParseFull)
 	file, _, m, _, err := pgh.Parse(ctx)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index f6ab7fc..9ff9b3b 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -26,7 +26,7 @@
 	ctx, done := event.Start(ctx, "source.Format")
 	defer done()
 
-	pgh := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull)
+	pgh := snapshot.View().Session().Cache().ParseGoHandle(ctx, fh, ParseFull)
 	file, _, m, parseErrors, err := pgh.Parse(ctx)
 	if err != nil {
 		return nil, err
@@ -59,7 +59,7 @@
 	ctx, done := event.Start(ctx, "source.formatSource")
 	defer done()
 
-	data, _, err := fh.Read(ctx)
+	data, err := fh.Read()
 	if err != nil {
 		return nil, err
 	}
@@ -79,7 +79,7 @@
 	ctx, done := event.Start(ctx, "source.AllImportsFixes")
 	defer done()
 
-	pgh := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull)
+	pgh := snapshot.View().Session().Cache().ParseGoHandle(ctx, fh, ParseFull)
 	if err := snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
 		allFixEdits, editsPerFix, err = computeImportEdits(ctx, snapshot.View(), pgh, opts)
 		return err
@@ -92,10 +92,10 @@
 // computeImportEdits computes a set of edits that perform one or all of the
 // necessary import fixes.
 func computeImportEdits(ctx context.Context, view View, ph ParseGoHandle, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
-	filename := ph.File().Identity().URI.Filename()
+	filename := ph.File().URI().Filename()
 
 	// Build up basic information about the original file.
-	origData, _, err := ph.File().Read(ctx)
+	origData, err := ph.File().Read()
 	if err != nil {
 		return nil, nil, err
 	}
@@ -130,7 +130,7 @@
 }
 
 func computeOneImportFixEdits(ctx context.Context, view View, ph ParseGoHandle, fix *imports.ImportFix) ([]protocol.TextEdit, error) {
-	origData, _, err := ph.File().Read(ctx)
+	origData, err := ph.File().Read()
 	if err != nil {
 		return nil, err
 	}
@@ -175,7 +175,7 @@
 	if fixedData == nil || fixedData[len(fixedData)-1] != '\n' {
 		fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure.
 	}
-	uri := ph.File().Identity().URI
+	uri := ph.File().URI()
 	edits := view.Options().ComputeEdits(uri, left, string(fixedData))
 	return ToProtocolEdits(origMapper, edits)
 }
@@ -215,11 +215,11 @@
 	ctx, done := event.Start(ctx, "source.computeTextEdits")
 	defer done()
 
-	data, _, err := fh.Read(ctx)
+	data, err := fh.Read()
 	if err != nil {
 		return nil, err
 	}
-	edits := view.Options().ComputeEdits(fh.Identity().URI, string(data), formatted)
+	edits := view.Options().ComputeEdits(fh.URI(), string(data), formatted)
 	return ToProtocolEdits(m, edits)
 }
 
diff --git a/internal/lsp/source/implementation.go b/internal/lsp/source/implementation.go
index 9826d23..527c8e6 100644
--- a/internal/lsp/source/implementation.go
+++ b/internal/lsp/source/implementation.go
@@ -285,7 +285,7 @@
 }
 
 func getASTFile(pkg Package, f FileHandle, pos protocol.Position) (*ast.File, token.Pos, error) {
-	pgh, err := pkg.File(f.Identity().URI)
+	pgh, err := pkg.File(f.URI())
 	if err != nil {
 		return nil, 0, err
 	}
diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go
index 5d8068f..beba1ad 100644
--- a/internal/lsp/source/rename.go
+++ b/internal/lsp/source/rename.go
@@ -88,7 +88,7 @@
 		return nil, errors.Errorf("invalid identifier to rename: %q", newName)
 	}
 	if pkg == nil || pkg.IsIllTyped() {
-		return nil, errors.Errorf("package for %s is ill typed", f.Identity().URI)
+		return nil, errors.Errorf("package for %s is ill typed", f.URI())
 	}
 	refs, err := references(ctx, s, qos, true)
 	if err != nil {
@@ -126,11 +126,11 @@
 	for uri, edits := range changes {
 		// These edits should really be associated with FileHandles for maximal correctness.
 		// For now, this is good enough.
-		fh, err := s.GetFile(uri)
+		fh, err := s.GetFile(ctx, uri)
 		if err != nil {
 			return nil, err
 		}
-		data, _, err := fh.Read(ctx)
+		data, err := fh.Read()
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index a923e6e..af702f1 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -226,7 +226,7 @@
 }
 
 func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*source.Options)) (string, []protocol.CompletionItem) {
-	fh, err := r.view.Snapshot().GetFile(src.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, src.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -272,11 +272,11 @@
 func (r *runner) FoldingRanges(t *testing.T, spn span.Span) {
 	uri := spn.URI()
 
-	fh, err := r.view.Snapshot().GetFile(spn.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, spn.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
-	data, _, err := fh.Read(r.ctx)
+	data, err := fh.Read()
 	if err != nil {
 		t.Error(err)
 		return
@@ -412,7 +412,7 @@
 		out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files
 		return out, nil
 	}))
-	fh, err := r.view.Snapshot().GetFile(spn.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, spn.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -423,7 +423,7 @@
 		}
 		return
 	}
-	data, _, err := fh.Read(r.ctx)
+	data, err := fh.Read()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -442,7 +442,7 @@
 }
 
 func (r *runner) Import(t *testing.T, spn span.Span) {
-	fh, err := r.view.Snapshot().GetFile(spn.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, spn.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -450,11 +450,11 @@
 	if err != nil {
 		t.Error(err)
 	}
-	data, _, err := fh.Read(r.ctx)
+	data, err := fh.Read()
 	if err != nil {
 		t.Fatal(err)
 	}
-	m, err := r.data.Mapper(fh.Identity().URI)
+	m, err := r.data.Mapper(fh.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -480,7 +480,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	fh, err := r.view.Snapshot().GetFile(spn.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, spn.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -540,7 +540,7 @@
 	if err != nil {
 		t.Fatalf("failed for %v: %v", spn, err)
 	}
-	fh, err := r.view.Snapshot().GetFile(spn.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, spn.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -584,7 +584,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	fh, err := r.view.Snapshot().GetFile(src.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, src.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -623,7 +623,7 @@
 		t.Fatal(err)
 	}
 	snapshot := r.view.Snapshot()
-	fh, err := snapshot.GetFile(src.URI())
+	fh, err := snapshot.GetFile(r.ctx, src.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -662,14 +662,13 @@
 }
 
 func (r *runner) Rename(t *testing.T, spn span.Span, newText string) {
-	ctx := r.ctx
 	tag := fmt.Sprintf("%s-rename", newText)
 
 	_, srcRng, err := spanToRange(r.data, spn)
 	if err != nil {
 		t.Fatal(err)
 	}
-	fh, err := r.view.Snapshot().GetFile(spn.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, spn.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -686,15 +685,15 @@
 
 	var res []string
 	for editURI, edits := range changes {
-		fh, err := r.view.Snapshot().GetFile(editURI)
+		fh, err := r.view.Snapshot().GetFile(r.ctx, editURI)
 		if err != nil {
 			t.Fatal(err)
 		}
-		data, _, err := fh.Read(ctx)
+		data, err := fh.Read()
 		if err != nil {
 			t.Fatal(err)
 		}
-		m, err := r.data.Mapper(fh.Identity().URI)
+		m, err := r.data.Mapper(fh.URI())
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -751,7 +750,7 @@
 		t.Fatal(err)
 	}
 	// Find the identifier at the position.
-	fh, err := r.view.Snapshot().GetFile(src.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, src.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -786,7 +785,7 @@
 }
 
 func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
-	fh, err := r.view.Snapshot().GetFile(uri)
+	fh, err := r.view.Snapshot().GetFile(r.ctx, uri)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -836,7 +835,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	fh, err := r.view.Snapshot().GetFile(spn.URI())
+	fh, err := r.view.Snapshot().GetFile(r.ctx, spn.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -868,7 +867,7 @@
 }
 
 func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
-	fh, err := r.view.Snapshot().GetFile(uri)
+	fh, err := r.view.Snapshot().GetFile(r.ctx, uri)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 69cfba9..1d2f2ba 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -80,7 +80,7 @@
 	if err != nil {
 		return nil, nil, err
 	}
-	pgh, err := pkg.File(fh.Identity().URI)
+	pgh, err := pkg.File(fh.URI())
 	return pkg, pgh, err
 }
 
@@ -142,11 +142,11 @@
 }
 
 func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool {
-	fh, err := snapshot.GetFile(uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return false
 	}
-	ph := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseHeader)
+	ph := snapshot.View().Session().Cache().ParseGoHandle(ctx, fh, ParseHeader)
 	parsed, _, _, _, err := ph.Parse(ctx)
 	if err != nil {
 		return false
@@ -555,7 +555,7 @@
 		return nil, nil, err
 	}
 	if !(file.Pos() <= pos && pos <= file.End()) {
-		return nil, nil, fmt.Errorf("pos %v, apparently in file %q, is not between %v and %v", pos, ph.File().Identity().URI, file.Pos(), file.End())
+		return nil, nil, fmt.Errorf("pos %v, apparently in file %q, is not between %v and %v", pos, ph.File().URI(), file.Pos(), file.End())
 	}
 	return file, pkg, nil
 }
@@ -582,11 +582,14 @@
 }
 
 func findIgnoredFile(v View, uri span.URI) (ParseGoHandle, error) {
-	fh, err := v.Snapshot().GetFile(uri)
+	// Using the View's context here is not good, but threading a context
+	// through to this function is prohibitively difficult for such a rare
+	// code path.
+	fh, err := v.Snapshot().GetFile(v.BackgroundContext(), uri)
 	if err != nil {
 		return nil, err
 	}
-	return v.Session().Cache().ParseGoHandle(fh, ParseFull), nil
+	return v.Session().Cache().ParseGoHandle(v.BackgroundContext(), fh, ParseFull), nil
 }
 
 // FindFileInPackage finds uri in pkg or its dependencies.
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 20e79a6..529e985 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -37,7 +37,7 @@
 
 	// GetFile returns the FileHandle for a given URI, initializing it
 	// if it is not already part of the snapshot.
-	GetFile(uri span.URI) (FileHandle, error)
+	GetFile(ctx context.Context, uri span.URI) (FileHandle, error)
 
 	// IsOpen returns whether the editor currently has a file open.
 	IsOpen(uri span.URI) bool
@@ -181,9 +181,8 @@
 	// Shutdown the session and all views it has created.
 	Shutdown(ctx context.Context)
 
-	// A FileSystem prefers the contents from overlays, and falls back to the
-	// content from the underlying cache if no overlay is present.
-	FileSystem
+	// GetFile returns a handle for the specified file.
+	GetFile(ctx context.Context, uri span.URI) (FileHandle, error)
 
 	// DidModifyFile reports a file modification to the session.
 	// It returns the resulting snapshots, a guaranteed one per view.
@@ -253,20 +252,11 @@
 // sharing between all consumers.
 // A cache may have many active sessions at any given time.
 type Cache interface {
-	// A FileSystem that reads file contents from external storage.
-	FileSystem
-
 	// FileSet returns the shared fileset used by all files in the system.
 	FileSet() *token.FileSet
 
 	// ParseGoHandle returns a ParseGoHandle for the given file handle.
-	ParseGoHandle(fh FileHandle, mode ParseMode) ParseGoHandle
-}
-
-// FileSystem is the interface to something that provides file contents.
-type FileSystem interface {
-	// GetFile returns a handle for the specified file.
-	GetFile(uri span.URI) FileHandle
+	ParseGoHandle(ctx context.Context, fh FileHandle, mode ParseMode) ParseGoHandle
 }
 
 // ParseGoHandle represents a handle to the AST for a file.
@@ -336,18 +326,20 @@
 	ParseFull
 )
 
-// FileHandle represents a handle to a specific version of a single file from
-// a specific file system.
+// FileHandle represents a handle to a specific version of a single file.
 type FileHandle interface {
-	// FileSystem returns the file system this handle was acquired from.
-	FileSystem() FileSystem
+	URI() span.URI
+	Kind() FileKind
+	Version() float64
 
-	// Identity returns the FileIdentity for the file.
+	// Identity returns a FileIdentity for the file, even if there was an error
+	// reading it.
+	// It is a fatal error to call Identity on a file that has not yet been read.
 	Identity() FileIdentity
 
-	// Read reads the contents of a file and returns it along with its hash value.
+	// Read reads the contents of a file.
 	// If the file is not available, returns a nil slice and an error.
-	Read(ctx context.Context) ([]byte, string, error)
+	Read() ([]byte, error)
 }
 
 // FileIdentity uniquely identifies a file at a version from a FileSystem.
diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go
index 7848eee..0f101a6 100644
--- a/internal/lsp/symbols.go
+++ b/internal/lsp/symbols.go
@@ -17,13 +17,13 @@
 	ctx, done := event.Start(ctx, "lsp.Server.documentSymbol")
 	defer done()
 
-	snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
 	if !ok {
 		return []interface{}{}, err
 	}
 	docSymbols, err := source.DocumentSymbols(ctx, snapshot, fh)
 	if err != nil {
-		event.Error(ctx, "DocumentSymbols failed", err, tag.URI.Of(fh.Identity().URI))
+		event.Error(ctx, "DocumentSymbols failed", err, tag.URI.Of(fh.URI()))
 		return []interface{}{}, nil
 	}
 	// Convert the symbols to an interface array.
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index b5f4913..8f2b282 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -188,12 +188,12 @@
 	if snapshot == nil {
 		return errors.Errorf("no snapshot for %s", uri)
 	}
-	fh, err := snapshot.GetFile(uri)
+	fh, err := snapshot.GetFile(ctx, uri)
 	if err != nil {
 		return err
 	}
 	// If a file has been closed and is not on disk, clear its diagnostics.
-	if _, _, err := fh.Read(ctx); err != nil {
+	if _, err := fh.Read(); err != nil {
 		return s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
 			URI:         protocol.URIFromSpanURI(uri),
 			Diagnostics: []protocol.Diagnostic{},
@@ -259,11 +259,11 @@
 				if !snapshot.IsSaved(uri) {
 					continue
 				}
-				fh, err := snapshot.GetFile(uri)
+				fh, err := snapshot.GetFile(ctx, uri)
 				if err != nil {
 					return nil, err
 				}
-				switch fh.Identity().Kind {
+				switch fh.Kind() {
 				case source.Mod:
 					newSnapshot, err := snapshot.View().Rebuild(ctx)
 					if err != nil {
@@ -312,7 +312,11 @@
 }
 
 func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) {
-	content, _, err := s.session.GetFile(uri).Read(ctx)
+	fh, err := s.session.GetFile(ctx, uri)
+	if err != nil {
+		return nil, err
+	}
+	content, err := fh.Read()
 	if err != nil {
 		return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err)
 	}