internal/lsp: change file system to allow lazy reads

We split aquiring a "handle" from reading a files contents so that we can do the
former eagerly and the latter lazily.
We also "version" the handles so that the same file at different versions is a
different handle.

Change-Id: I06cc346d4b4c77d784aa454702c54689f2f177e0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/179917
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index 2f0e14d..81a8c77 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -20,6 +20,7 @@
 func New() source.Cache {
 	index := atomic.AddInt64(&cacheIndex, 1)
 	c := &cache{
+		fs:   &nativeFileSystem{},
 		id:   strconv.FormatInt(index, 10),
 		fset: token.NewFileSet(),
 	}
@@ -28,19 +29,22 @@
 }
 
 type cache struct {
-	nativeFileSystem
-
+	fs   source.FileSystem
 	id   string
 	fset *token.FileSet
 }
 
+func (c *cache) GetFile(uri span.URI) source.FileHandle {
+	return c.fs.GetFile(uri)
+}
+
 func (c *cache) NewSession(log xlog.Logger) source.Session {
 	index := atomic.AddInt64(&sessionIndex, 1)
 	s := &session{
 		cache:         c,
 		id:            strconv.FormatInt(index, 10),
 		log:           log,
-		overlays:      make(map[span.URI]*source.FileContent),
+		overlays:      make(map[span.URI]*overlay),
 		filesWatchMap: NewWatchMap(),
 	}
 	debug.AddSession(debugSession{s})
diff --git a/internal/lsp/cache/external.go b/internal/lsp/cache/external.go
index 4bb03cd..9936344 100644
--- a/internal/lsp/cache/external.go
+++ b/internal/lsp/cache/external.go
@@ -5,6 +5,7 @@
 package cache
 
 import (
+	"context"
 	"io/ioutil"
 
 	"golang.org/x/tools/internal/lsp/source"
@@ -14,9 +15,35 @@
 // nativeFileSystem implements FileSystem reading from the normal os file system.
 type nativeFileSystem struct{}
 
-func (nativeFileSystem) ReadFile(uri span.URI) *source.FileContent {
-	r := &source.FileContent{URI: uri}
-	filename, err := uri.Filename()
+// 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,
+			// TODO: decide what the version string is for a native file system
+			// could be the mtime?
+			Version: "",
+		},
+	}
+}
+
+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) *source.FileContent {
+	r := &source.FileContent{}
+	filename, err := h.identity.URI.Filename()
 	if err != nil {
 		r.Error = err
 		return r
diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go
index d7a2b91..424a322 100644
--- a/internal/lsp/cache/file.go
+++ b/internal/lsp/cache/file.go
@@ -85,5 +85,5 @@
 		}
 	}
 	// We don't know the content yet, so read it.
-	f.fc = f.view.Session().ReadFile(f.URI())
+	f.fc = f.view.Session().GetFile(f.URI()).Read(ctx)
 }
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 1b4881b..1d71e0e 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -31,12 +31,18 @@
 	viewMap map[span.URI]source.View
 
 	overlayMu sync.Mutex
-	overlays  map[span.URI]*source.FileContent
+	overlays  map[span.URI]*overlay
 
 	openFiles     sync.Map
 	filesWatchMap *WatchMap
 }
 
+type overlay struct {
+	session *session
+	uri     span.URI
+	content source.FileContent
+}
+
 func (s *session) Shutdown(ctx context.Context) {
 	s.viewMu.Lock()
 	defer s.viewMu.Unlock()
@@ -186,12 +192,12 @@
 	return open
 }
 
-func (s *session) ReadFile(uri span.URI) *source.FileContent {
+func (s *session) GetFile(uri span.URI) source.FileHandle {
 	if overlay := s.readOverlay(uri); overlay != nil {
 		return overlay
 	}
 	// Fall back to the cache-level file system.
-	return s.Cache().ReadFile(uri)
+	return s.Cache().GetFile(uri)
 }
 
 func (s *session) SetOverlay(uri span.URI, data []byte) {
@@ -205,14 +211,17 @@
 		delete(s.overlays, uri)
 		return
 	}
-	s.overlays[uri] = &source.FileContent{
-		URI:  uri,
-		Data: data,
-		Hash: hashContents(data),
+	s.overlays[uri] = &overlay{
+		session: s,
+		uri:     uri,
+		content: source.FileContent{
+			Data: data,
+			Hash: hashContents(data),
+		},
 	}
 }
 
-func (s *session) readOverlay(uri span.URI) *source.FileContent {
+func (s *session) readOverlay(uri span.URI) *overlay {
 	s.overlayMu.Lock()
 	defer s.overlayMu.Unlock()
 
@@ -229,16 +238,31 @@
 
 	overlays := make(map[string][]byte)
 	for uri, overlay := range s.overlays {
-		if overlay.Error != nil {
+		if overlay.content.Error != nil {
 			continue
 		}
 		if filename, err := uri.Filename(); err == nil {
-			overlays[filename] = overlay.Data
+			overlays[filename] = overlay.content.Data
 		}
 	}
 	return overlays
 }
 
+func (o *overlay) FileSystem() source.FileSystem {
+	return o.session
+}
+
+func (o *overlay) Identity() source.FileIdentity {
+	return source.FileIdentity{
+		URI:     o.uri,
+		Version: o.content.Hash,
+	}
+}
+
+func (o *overlay) Read(ctx context.Context) *source.FileContent {
+	return &o.content
+}
+
 type debugSession struct{ *session }
 
 func (s debugSession) ID() string         { return s.id }
@@ -258,15 +282,15 @@
 	s.overlayMu.Lock()
 	defer s.overlayMu.Unlock()
 	for _, overlay := range s.overlays {
-		f, ok := seen[overlay.URI]
+		f, ok := seen[overlay.uri]
 		if !ok {
-			f = &debug.File{Session: s, URI: overlay.URI}
-			seen[overlay.URI] = f
+			f = &debug.File{Session: s, URI: overlay.uri}
+			seen[overlay.uri] = f
 			files = append(files, f)
 		}
-		f.Data = string(overlay.Data)
-		f.Error = overlay.Error
-		f.Hash = overlay.Hash
+		f.Data = string(overlay.content.Data)
+		f.Error = overlay.content.Error
+		f.Hash = overlay.content.Hash
 	}
 	sort.Slice(files, func(i int, j int) bool {
 		return files[i].URI < files[j].URI
@@ -278,13 +302,13 @@
 	s.overlayMu.Lock()
 	defer s.overlayMu.Unlock()
 	for _, overlay := range s.overlays {
-		if overlay.Hash == hash {
+		if overlay.content.Hash == hash {
 			return &debug.File{
 				Session: s,
-				URI:     overlay.URI,
-				Data:    string(overlay.Data),
-				Error:   overlay.Error,
-				Hash:    overlay.Hash,
+				URI:     overlay.uri,
+				Data:    string(overlay.content.Data),
+				Error:   overlay.content.Error,
+				Hash:    overlay.content.Hash,
 			}
 		}
 	}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 8f7ce8c..ca529cc 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -18,19 +18,37 @@
 	"golang.org/x/tools/internal/span"
 )
 
-// FileContents is returned from FileSystem implementation to represent the
+// FileContent is returned from FileSystem implementation to represent the
 // contents of a file.
 type FileContent struct {
-	URI   span.URI
 	Data  []byte
 	Error error
 	Hash  string
 }
 
+// FileIdentity uniquely identifies a file at a version from a FileSystem.
+type FileIdentity struct {
+	URI     span.URI
+	Version string
+}
+
+// FileHandle represents a handle to a specific version of a single file from
+// a specific file system.
+type FileHandle interface {
+	// FileSystem returns the file system this handle was aquired from.
+	FileSystem() FileSystem
+	// Return the Identity for the file.
+	Identity() FileIdentity
+	// Read reads the contents of a file and returns it.
+	// If the file is not available, the returned FileContent will have no
+	// data and an error.
+	Read(ctx context.Context) *FileContent
+}
+
 // FileSystem is the interface to something that provides file contents.
 type FileSystem interface {
-	// ReadFile reads the contents of a file and returns it.
-	ReadFile(uri span.URI) *FileContent
+	// GetFile returns a handle for the specified file.
+	GetFile(uri span.URI) FileHandle
 }
 
 // Cache abstracts the core logic of dealing with the environment from the
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 28c636d..73d7451 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -66,7 +66,7 @@
 	}
 
 	uri := span.NewURI(params.TextDocument.URI)
-	fc := s.session.ReadFile(uri)
+	fc := s.session.GetFile(uri).Read(ctx)
 	if fc.Error != nil {
 		return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
 	}