diff --git a/internal/lsp/cache/builtin.go b/internal/lsp/cache/builtin.go index 00d68326da..f9d2fe52d7 100644 --- a/internal/lsp/cache/builtin.go +++ b/internal/lsp/cache/builtin.go @@ -44,7 +44,7 @@ func (v *view) buildBuiltinPackage(ctx context.Context) error { pkg := pkgs[0] files := make(map[string]*ast.File) for _, filename := range pkg.GoFiles { - fh := v.session.GetFile(span.FileURI(filename), source.Go) + fh := v.session.GetFile(span.FileURI(filename)) ph := v.session.cache.ParseGoHandle(fh, source.ParseFull) v.builtin.files = append(v.builtin.files, ph) file, _, _, err := ph.Parse(ctx) diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index 41d3d807c2..e6a185a746 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -56,8 +56,8 @@ type fileData struct { err error } -func (c *cache) GetFile(uri span.URI, kind source.FileKind) source.FileHandle { - underlying := c.fs.GetFile(uri, kind) +func (c *cache) GetFile(uri span.URI) source.FileHandle { + underlying := c.fs.GetFile(uri) key := fileKey{ identity: underlying.Identity(), } diff --git a/internal/lsp/cache/external.go b/internal/lsp/cache/external.go index 2498bf6842..3f98b139cf 100644 --- a/internal/lsp/cache/external.go +++ b/internal/lsp/cache/external.go @@ -27,11 +27,12 @@ type nativeFileHandle struct { identity source.FileIdentity } -func (fs *nativeFileSystem) GetFile(uri span.URI, kind source.FileKind) source.FileHandle { +func (fs *nativeFileSystem) GetFile(uri span.URI) source.FileHandle { identifier := "DOES NOT EXIST" if fi, err := os.Stat(uri.Filename()); err == nil { identifier = fi.ModTime().String() } + kind := source.DetectLanguage("", uri.Filename()) return &nativeFileHandle{ fs: fs, identity: source.FileIdentity{ diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go deleted file mode 100644 index 534892ba7e..0000000000 --- a/internal/lsp/cache/file.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2018 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 ( - "go/token" - "path/filepath" - "strings" - - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" -) - -// fileBase holds the common functionality for all files. -// It is intended to be embedded in the file implementations -type fileBase struct { - uris []span.URI - fname string - kind source.FileKind - - view *view -} - -func dir(filename string) string { - return strings.ToLower(filepath.Dir(filename)) -} - -func basename(filename string) string { - return strings.ToLower(filepath.Base(filename)) -} - -func (f *fileBase) URI() span.URI { - return f.uris[0] -} - -func (f *fileBase) filename() string { - return f.fname -} - -// View returns the view associated with the file. -func (f *fileBase) View() source.View { - return f.view -} - -func (f *fileBase) FileSet() *token.FileSet { - return f.view.Session().Cache().FileSet() -} diff --git a/internal/lsp/cache/overlay.go b/internal/lsp/cache/overlay.go index 8e6b794cf2..ad07a18f79 100644 --- a/internal/lsp/cache/overlay.go +++ b/internal/lsp/cache/overlay.go @@ -41,7 +41,7 @@ func (o *overlay) Read(ctx context.Context) ([]byte, string, error) { return o.text, o.hash, nil } -func (s *session) updateOverlay(ctx context.Context, c source.FileModification) error { +func (s *session) updateOverlay(ctx context.Context, c source.FileModification) (source.FileKind, error) { s.overlayMu.Lock() defer s.overlayMu.Unlock() @@ -54,18 +54,18 @@ func (s *session) updateOverlay(ctx context.Context, c source.FileModification) kind = source.DetectLanguage(c.LanguageID, c.URI.Filename()) default: if !ok { - return errors.Errorf("updateOverlay: modifying unopened overlay %v", c.URI) + return -1, errors.Errorf("updateOverlay: modifying unopened overlay %v", c.URI) } kind = o.kind } if kind == source.UnknownKind { - return errors.Errorf("updateOverlay: unknown file kind for %s", c.URI) + return -1, errors.Errorf("updateOverlay: unknown file kind for %s", c.URI) } // Closing a file just deletes its overlay. if c.Action == source.Close { delete(s.overlays, c.URI) - return nil + return kind, nil } // If the file is on disk, check if its content is the same as the overlay. @@ -77,15 +77,15 @@ func (s *session) updateOverlay(ctx context.Context, c source.FileModification) var sameContentOnDisk bool switch c.Action { case source.Open: - _, h, err := s.cache.GetFile(c.URI, kind).Read(ctx) + _, h, err := s.cache.GetFile(c.URI).Read(ctx) sameContentOnDisk = (err == nil && h == hash) case source.Save: // Make sure the version and content (if present) is the same. if o.version != c.Version { - return errors.Errorf("updateOverlay: saving %s at version %v, currently at %v", c.URI, c.Version, o.version) + return -1, errors.Errorf("updateOverlay: saving %s at version %v, currently at %v", c.URI, c.Version, o.version) } if c.Text != nil && o.hash != hash { - return errors.Errorf("updateOverlay: overlay %s changed on save", c.URI) + return -1, errors.Errorf("updateOverlay: overlay %s changed on save", c.URI) } sameContentOnDisk = true } @@ -98,7 +98,7 @@ func (s *session) updateOverlay(ctx context.Context, c source.FileModification) hash: hash, sameContentOnDisk: sameContentOnDisk, } - return nil + return kind, nil } func (s *session) readOverlay(uri span.URI) *overlay { diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 2c23aa85c0..00f59f4639 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -256,7 +256,8 @@ func (s *session) DidModifyFile(ctx context.Context, c source.FileModification) ctx = telemetry.URI.With(ctx, c.URI) // Perform the session-specific updates. - if err := s.updateOverlay(ctx, c); err != nil { + kind, err := s.updateOverlay(ctx, c) + if err != nil { return nil, err } @@ -265,12 +266,11 @@ func (s *session) DidModifyFile(ctx context.Context, c source.FileModification) if view.Ignore(c.URI) { return nil, errors.Errorf("ignored file %v", c.URI) } - // Set the content for the file, only for didChange and didClose events. - f, err := view.getFileLocked(ctx, c.URI) - if err != nil { + // Make sure to add the file to the view. + if _, err := view.getFileLocked(ctx, c.URI); err != nil { return nil, err } - snapshots = append(snapshots, view.invalidateContent(ctx, c.URI, f.kind, c.Action)) + snapshots = append(snapshots, view.invalidateContent(ctx, c.URI, kind, c.Action)) } return snapshots, nil } @@ -283,12 +283,12 @@ func (s *session) IsOpen(uri span.URI) bool { return open } -func (s *session) GetFile(uri span.URI, kind source.FileKind) source.FileHandle { +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.GetFile(uri, kind) + return s.cache.GetFile(uri) } func (s *session) DidChangeOutOfBand(ctx context.Context, uri span.URI, action source.FileAction) bool { @@ -296,10 +296,12 @@ func (s *session) DidChangeOutOfBand(ctx context.Context, uri span.URI, action s if err != nil { return false } - f, err := view.getFileLocked(ctx, uri) - if err != nil { + // Make sure that the file is part of the view. + if _, err := view.getFileLocked(ctx, uri); err != nil { return false } - view.invalidateContent(ctx, f.URI(), f.kind, action) + // TODO(golang/go#31553): Remove this when this issue has been resolved. + kind := source.DetectLanguage("", uri.Filename()) + view.invalidateContent(ctx, uri, kind, action) return true } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index d9c41420b6..7e3d3b7b67 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -8,6 +8,8 @@ import ( "context" "io" "os" + "path/filepath" + "strings" "sync" "golang.org/x/tools/go/analysis" @@ -92,7 +94,7 @@ func (s *snapshot) ModFiles(ctx context.Context) (source.FileHandle, source.File return nil, nil, err } // Go directly to disk to get the correct FileHandle, since we just copied the file without invalidating the snapshot. - tempfh := s.view.Session().Cache().GetFile(span.FileURI(s.view.modfiles.temp), source.Mod) + tempfh := s.view.Session().Cache().GetFile(span.FileURI(s.view.modfiles.temp)) if tempfh == nil { return nil, nil, errors.Errorf("temporary go.mod filehandle is nil") } @@ -533,9 +535,7 @@ func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle s.view.mu.Lock() defer s.view.mu.Unlock() - // TODO(rstambler): Should there be a version that provides a kind explicitly? - kind := source.DetectLanguage("", uri.Filename()) - f, err := s.view.getFile(ctx, uri, kind) + f, err := s.view.getFile(ctx, uri) if err != nil { return nil, err } @@ -547,7 +547,7 @@ func (s *snapshot) getFileHandle(ctx context.Context, f *fileBase) source.FileHa defer s.mu.Unlock() if _, ok := s.files[f.URI()]; !ok { - s.files[f.URI()] = s.view.session.GetFile(f.URI(), f.kind) + s.files[f.URI()] = s.view.session.GetFile(f.URI()) } return s.files[f.URI()] } @@ -633,7 +633,7 @@ func (s *snapshot) clone(ctx context.Context, withoutURI span.URI, withoutFileKi result.files[k] = v } // Handle the invalidated file; it may have new contents or not exist. - currentFH := s.view.session.GetFile(withoutURI, withoutFileKind) + currentFH := s.view.session.GetFile(withoutURI) if _, _, err := currentFH.Read(ctx); os.IsNotExist(err) { delete(result.files, withoutURI) } else { @@ -680,6 +680,10 @@ func (s *snapshot) clone(ctx context.Context, withoutURI span.URI, withoutFileKi return result } +func dir(filename string) string { + return strings.ToLower(filepath.Dir(filename)) +} + func (s *snapshot) ID() uint64 { return s.id } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index c546ff7457..f335c9c379 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -96,6 +96,28 @@ type view struct { initializationError error } +// fileBase holds the common functionality for all files. +// It is intended to be embedded in the file implementations +type fileBase struct { + uris []span.URI + fname string + + view *view +} + +func (f *fileBase) URI() span.URI { + return f.uris[0] +} + +func (f *fileBase) filename() string { + return f.fname +} + +func (f *fileBase) addURI(uri span.URI) int { + f.uris = append(f.uris, uri) + return len(f.uris) +} + // modfiles holds the real and temporary go.mod files that are attributed to a view. type modfiles struct { real, temp string @@ -278,7 +300,7 @@ func (v *view) modFilesChanged() bool { // and modules included by a replace directive. Return true if // any of these file versions do not match. for filename, version := range v.modFileVersions { - if version != v.fileVersion(filename, source.Mod) { + if version != v.fileVersion(filename) { return true } } @@ -297,17 +319,101 @@ func (v *view) storeModFileVersions() { // and modules included by a replace directive in the resolver. for _, mod := range r.ModsByModPath { if (mod.Main || mod.Replace != nil) && mod.GoMod != "" { - v.modFileVersions[mod.GoMod] = v.fileVersion(mod.GoMod, source.Mod) + v.modFileVersions[mod.GoMod] = v.fileVersion(mod.GoMod) } } } -func (v *view) fileVersion(filename string, kind source.FileKind) string { +func (v *view) fileVersion(filename string) string { uri := span.FileURI(filename) - fh := v.session.GetFile(uri, kind) + fh := v.session.GetFile(uri) return fh.Identity().String() } +func (v *view) mapFile(uri span.URI, f *fileBase) { + v.filesByURI[uri] = f + if f.addURI(uri) == 1 { + basename := basename(f.filename()) + v.filesByBase[basename] = append(v.filesByBase[basename], f) + } +} + +func basename(filename string) string { + return strings.ToLower(filepath.Base(filename)) +} + +// FindFile returns the file if the given URI is already a part of the view. +func (v *view) findFileLocked(ctx context.Context, uri span.URI) *fileBase { + v.mu.Lock() + defer v.mu.Unlock() + + f, err := v.findFile(uri) + if err != nil { + return nil + } + return f +} + +// getFileLocked returns a File for the given URI. It will always succeed because it +// adds the file to the managed set if needed. +func (v *view) getFileLocked(ctx context.Context, uri span.URI) (*fileBase, error) { + v.mu.Lock() + defer v.mu.Unlock() + + return v.getFile(ctx, uri) +} + +// getFile is the unlocked internal implementation of GetFile. +func (v *view) getFile(ctx context.Context, uri span.URI) (*fileBase, error) { + f, err := v.findFile(uri) + if err != nil { + return nil, err + } else if f != nil { + return f, nil + } + f = &fileBase{ + view: v, + fname: uri.Filename(), + } + v.mapFile(uri, f) + return f, nil +} + +// findFile checks the cache for any file matching the given uri. +// +// An error is only returned for an irreparable failure, for example, if the +// filename in question does not exist. +func (v *view) findFile(uri span.URI) (*fileBase, error) { + if f := v.filesByURI[uri]; f != nil { + // a perfect match + return f, nil + } + // no exact match stored, time to do some real work + // check for any files with the same basename + fname := uri.Filename() + basename := basename(fname) + if candidates := v.filesByBase[basename]; candidates != nil { + pathStat, err := os.Stat(fname) + if os.IsNotExist(err) { + return nil, err + } + if err != nil { + return nil, nil // the file may exist, return without an error + } + for _, c := range candidates { + if cStat, err := os.Stat(c.filename()); err == nil { + if os.SameFile(pathStat, cStat) { + // same file, map it + v.mapFile(uri, c) + return c, nil + } + } + } + } + // no file with a matching name was found, it wasn't in our cache + return nil, nil +} + func (v *view) Shutdown(ctx context.Context) { v.session.removeView(ctx, v) } @@ -427,94 +533,6 @@ func (v *view) cancelBackground() { v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx) } -// FindFile returns the file if the given URI is already a part of the view. -func (v *view) findFileLocked(ctx context.Context, uri span.URI) *fileBase { - v.mu.Lock() - defer v.mu.Unlock() - - f, err := v.findFile(uri) - if err != nil { - return nil - } - return f -} - -// getFileLocked returns a File for the given URI. It will always succeed because it -// adds the file to the managed set if needed. -func (v *view) getFileLocked(ctx context.Context, uri span.URI) (*fileBase, error) { - v.mu.Lock() - defer v.mu.Unlock() - - // TODO(rstambler): Should there be a version that provides a kind explicitly? - kind := source.DetectLanguage("", uri.Filename()) - return v.getFile(ctx, uri, kind) -} - -// getFile is the unlocked internal implementation of GetFile. -func (v *view) getFile(ctx context.Context, uri span.URI, kind source.FileKind) (*fileBase, error) { - f, err := v.findFile(uri) - if err != nil { - return nil, err - } else if f != nil { - return f, nil - } - f = &fileBase{ - view: v, - fname: uri.Filename(), - kind: kind, - } - v.mapFile(uri, f) - return f, nil -} - -// findFile checks the cache for any file matching the given uri. -// -// An error is only returned for an irreparable failure, for example, if the -// filename in question does not exist. -func (v *view) findFile(uri span.URI) (*fileBase, error) { - if f := v.filesByURI[uri]; f != nil { - // a perfect match - return f, nil - } - // no exact match stored, time to do some real work - // check for any files with the same basename - fname := uri.Filename() - basename := basename(fname) - if candidates := v.filesByBase[basename]; candidates != nil { - pathStat, err := os.Stat(fname) - if os.IsNotExist(err) { - return nil, err - } - if err != nil { - return nil, nil // the file may exist, return without an error - } - for _, c := range candidates { - if cStat, err := os.Stat(c.filename()); err == nil { - if os.SameFile(pathStat, cStat) { - // same file, map it - v.mapFile(uri, c) - return c, nil - } - } - } - } - // no file with a matching name was found, it wasn't in our cache - return nil, nil -} - -func (f *fileBase) addURI(uri span.URI) int { - f.uris = append(f.uris, uri) - return len(f.uris) -} - -func (v *view) mapFile(uri span.URI, f *fileBase) { - v.filesByURI[uri] = f - if f.addURI(uri) == 1 { - basename := basename(f.filename()) - v.filesByBase[basename] = append(v.filesByBase[basename], f) - } -} - func (v *view) FindPosInPackage(searchpkg source.Package, pos token.Pos) (*ast.File, source.Package, error) { tok := v.session.cache.fset.File(pos) if tok == nil { diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 541d9e203a..3dd8ec9626 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -235,7 +235,7 @@ type Cache interface { // 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, kind FileKind) FileHandle + GetFile(uri span.URI) FileHandle } // ParseGoHandle represents a handle to the AST for a file. diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go index 3795e70abf..2f2ad81082 100644 --- a/internal/lsp/text_synchronization.go +++ b/internal/lsp/text_synchronization.go @@ -144,7 +144,7 @@ func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protoc } func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { - content, _, err := s.session.GetFile(uri, source.UnknownKind).Read(ctx) + content, _, err := s.session.GetFile(uri).Read(ctx) if err != nil { return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found (%v)", err) }