From 2c3de6a5aea1814ceabe3c0b72be5223f0d8f600 Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Fri, 31 May 2019 19:41:39 -0400 Subject: [PATCH] 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 --- internal/lsp/cache/cache.go | 10 +++-- internal/lsp/cache/external.go | 33 ++++++++++++-- internal/lsp/cache/file.go | 2 +- internal/lsp/cache/session.go | 66 +++++++++++++++++++--------- internal/lsp/source/view.go | 26 +++++++++-- internal/lsp/text_synchronization.go | 2 +- 6 files changed, 106 insertions(+), 33 deletions(-) diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index 2f0e14d8..81a8c779 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -20,6 +20,7 @@ import ( 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 @@ func New() source.Cache { } 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 4bb03cdf..9936344e 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 @@ import ( // 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 d7a2b91f..424a3229 100644 --- a/internal/lsp/cache/file.go +++ b/internal/lsp/cache/file.go @@ -85,5 +85,5 @@ func (f *fileBase) read(ctx context.Context) { } } // 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 1b4881bb..1d71e0e6 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -31,12 +31,18 @@ type session struct { 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 @@ func (s *session) IsOpen(uri span.URI) bool { 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 @@ func (s *session) SetOverlay(uri span.URI, data []byte) { 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 @@ func (s *session) buildOverlay() map[string][]byte { 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 @@ func (s debugSession) Files() []*debug.File { 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 @@ func (s debugSession) File(hash string) *debug.File { 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 8f7ce8cf..ca529cc6 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -18,19 +18,37 @@ import ( "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 28c636de..73d7451e 100644 --- a/internal/lsp/text_synchronization.go +++ b/internal/lsp/text_synchronization.go @@ -66,7 +66,7 @@ func (s *Server) applyChanges(ctx context.Context, params *protocol.DidChangeTex } 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") }