diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 6b1320bcdb..99a2078d93 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -321,20 +321,3 @@ func (v *view) link(ctx context.Context, g *importGraph) error { } return nil } - -func identicalFileHandles(old, new []source.ParseGoHandle) bool { - if len(old) != len(new) { - return false - } - oldByIdentity := make(map[string]struct{}, len(old)) - for _, ph := range old { - oldByIdentity[hashParseKey(ph)] = struct{}{} - } - for _, ph := range new { - if _, found := oldByIdentity[hashParseKey(ph)]; !found { - return false - } - delete(oldByIdentity, hashParseKey(ph)) - } - return len(oldByIdentity) == 0 -} diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 5a469e0860..4d4281da79 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -14,6 +14,7 @@ import ( "sync/atomic" "golang.org/x/tools/internal/lsp/debug" + "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/telemetry" "golang.org/x/tools/internal/span" @@ -336,8 +337,15 @@ func (s *session) buildOverlay() map[string][]byte { return overlays } -func (s *session) DidChangeOutOfBand(uri span.URI) { - s.filesWatchMap.Notify(uri) +func (s *session) DidChangeOutOfBand(ctx context.Context, f source.GoFile, changeType protocol.FileChangeType) { + if changeType == protocol.Deleted { + // After a deletion we must invalidate the package's metadata to + // force a go/packages invocation to refresh the package's file + // list. + f.(*goFile).invalidateMeta(ctx) + } + + s.filesWatchMap.Notify(f.URI()) } func (o *overlay) FileSystem() source.FileSystem { diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 767bdbd268..f235aa3ca0 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -365,6 +365,31 @@ func (f *goFile) invalidateContent(ctx context.Context) { f.handle = nil } +// invalidateMeta invalides package metadata for all files in f's +// package. This forces f's package's metadata to be reloaded next +// time the package is checked. +func (f *goFile) invalidateMeta(ctx context.Context) { + pkgs, err := f.GetPackages(ctx) + if err != nil { + log.Error(ctx, "invalidateMeta: GetPackages", err, telemetry.File.Of(f.URI())) + return + } + + for _, pkg := range pkgs { + for _, pgh := range pkg.GetHandles() { + uri := pgh.File().Identity().URI + if gof, _ := f.view.FindFile(ctx, uri).(*goFile); gof != nil { + gof.mu.Lock() + gof.meta = nil + gof.mu.Unlock() + } + } + f.view.mcache.mu.Lock() + delete(f.view.mcache.packages, packageID(pkg.ID())) + f.view.mcache.mu.Unlock() + } +} + // remove invalidates a package and its reverse dependencies in the view's // package cache. It is assumed that the caller has locked both the mutexes // of both the mcache and the pcache. diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index e3d8d03c4a..4d7ba3e1c4 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) @@ -189,7 +190,7 @@ type Session interface { // DidChangeOutOfBand is called when a file under the root folder // changes. The file is not necessarily open in the editor. - DidChangeOutOfBand(uri span.URI) + DidChangeOutOfBand(ctx context.Context, f GoFile, change protocol.FileChangeType) // Options returns a copy of the SessionOptions for this session. Options() SessionOptions @@ -270,10 +271,10 @@ type GoFile interface { // GetPackages returns the CheckPackageHandles of the packages that this file belongs to. GetCheckPackageHandles(ctx context.Context) ([]CheckPackageHandle, error) - // GetPackage returns the CheckPackageHandle for the package that this file belongs to. + // GetPackage returns the Package that this file belongs to. GetPackage(ctx context.Context) (Package, error) - // GetPackages returns the CheckPackageHandles of the packages that this file belongs to. + // GetPackages returns the Packages that this file belongs to. GetPackages(ctx context.Context) ([]Package, error) // GetActiveReverseDeps returns the active files belonging to the reverse diff --git a/internal/lsp/watched_files.go b/internal/lsp/watched_files.go index 6c962bf3f8..98315e46f6 100644 --- a/internal/lsp/watched_files.go +++ b/internal/lsp/watched_files.go @@ -8,9 +8,11 @@ import ( "context" "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/lsp/telemetry" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/log" - "golang.org/x/tools/internal/telemetry/tag" + "golang.org/x/tools/internal/telemetry/trace" ) func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { @@ -22,31 +24,75 @@ func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.Did for _, change := range params.Changes { uri := span.NewURI(change.URI) - switch change.Type { - case protocol.Changed: - view := s.session.ViewOf(uri) + ctx := telemetry.File.With(ctx, uri) + + for _, view := range s.session.Views() { + gof, _ := view.FindFile(ctx, uri).(source.GoFile) // If we have never seen this file before, there is nothing to do. - if view.FindFile(ctx, uri) == nil { - break + if gof == nil { + continue } - log.Print(ctx, "watched file changed", tag.Of("uri", uri)) - // If client has this file open, don't do anything. The client's contents // must remain the source of truth. if s.session.IsOpen(uri) { break } - s.session.DidChangeOutOfBand(uri) + switch change.Type { + case protocol.Changed: + log.Print(ctx, "watched file changed", telemetry.File) - // Refresh diagnostics to reflect updated file contents. - s.Diagnostics(ctx, view, uri) - case protocol.Created: - log.Print(ctx, "watched file created", tag.Of("uri", uri)) - case protocol.Deleted: - log.Print(ctx, "watched file deleted", tag.Of("uri", uri)) + s.session.DidChangeOutOfBand(ctx, gof, change.Type) + + // Refresh diagnostics to reflect updated file contents. + go func(view source.View) { + ctx := view.BackgroundContext() + ctx, done := trace.StartSpan(ctx, "lsp:background-worker") + defer done() + s.Diagnostics(ctx, view, uri) + }(view) + case protocol.Created: + log.Print(ctx, "watched file created", telemetry.File) + case protocol.Deleted: + log.Print(ctx, "watched file deleted", telemetry.File) + + pkg, err := gof.GetPackage(ctx) + if err != nil { + log.Error(ctx, "didChangeWatchedFiles: GetPackage", err, telemetry.File) + continue + } + + // Find a different file in the same package we can use to + // trigger diagnostics. + var otherFile source.GoFile + for _, pgh := range pkg.GetHandles() { + ident := pgh.File().Identity() + if ident.URI == gof.URI() { + continue + } + otherFile, _ = view.FindFile(ctx, ident.URI).(source.GoFile) + if otherFile != nil { + break + } + } + + s.session.DidChangeOutOfBand(ctx, gof, change.Type) + + if otherFile != nil { + // Refresh diagnostics to reflect updated file contents. + go func(view source.View) { + ctx := view.BackgroundContext() + ctx, done := trace.StartSpan(ctx, "lsp:background-worker") + defer done() + s.Diagnostics(ctx, view, otherFile.URI()) + }(view) + } else { + // TODO: Handle case when there is no other file (i.e. deleted + // file was the only file in the package). + } + } } }