gopls/internal/lsp: add quick-fixes to manage the go.work file
Update OrphanedFileDiagnostics to provide suggested fixes for
diagnostics related to modules that are not activated by a relevant
go.work file.
Also remove the Snapshot.openFiles method, which was completely
redundant with Snapshot.overlays.
Fixes golang/go#53880
Change-Id: I7e7aed97fb0b93415fe3dc383b6daea15241f31b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/494738
Reviewed-by: Mouffull <dickmouth8340@gmail.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 2035364..45a32a1 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -31,6 +31,7 @@
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/types/objectpath"
"golang.org/x/tools/gopls/internal/bug"
+ "golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/filecache"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
@@ -191,6 +192,16 @@
// detect ignored files.
ignoreFilterOnce sync.Once
ignoreFilter *ignoreFilter
+
+ // If non-nil, the result of computing orphaned file diagnostics.
+ //
+ // Only the field, not the map itself, is guarded by the mutex. The map must
+ // not be mutated.
+ //
+ // Used to save work across diagnostics+code action passes.
+ // TODO(rfindley): refactor all of this so there's no need to re-evaluate
+ // diagnostics during code-action.
+ orphanedFileDiagnostics map[span.URI]*source.Diagnostic
}
var globalSnapshotID uint64
@@ -293,7 +304,8 @@
}
func (s *snapshot) WorkFile() span.URI {
- return s.view.effectiveGOWORK()
+ gowork, _ := s.view.GOWORK()
+ return gowork
}
func (s *snapshot) Templates() map[span.URI]source.FileHandle {
@@ -544,7 +556,7 @@
// the main (workspace) module. Otherwise, we should use the module for
// the passed-in working dir.
if mode == source.LoadWorkspace {
- if s.view.effectiveGOWORK() == "" && s.view.gomod != "" {
+ if gowork, _ := s.view.GOWORK(); gowork == "" && s.view.gomod != "" {
modURI = s.view.gomod
}
} else {
@@ -929,7 +941,7 @@
}
// If GOWORK is outside the folder, ensure we are watching it.
- gowork := s.view.effectiveGOWORK()
+ gowork, _ := s.view.GOWORK()
if gowork != "" && !source.InDir(s.view.folder.Filename(), gowork.Filename()) {
patterns[gowork.Filename()] = struct{}{}
}
@@ -1351,19 +1363,6 @@
return open
}
-func (s *snapshot) openFiles() []*Overlay {
- s.mu.Lock()
- defer s.mu.Unlock()
-
- var open []*Overlay
- s.files.Range(func(uri span.URI, fh source.FileHandle) {
- if o, ok := fh.(*Overlay); ok {
- open = append(open, o)
- }
- })
- return open
-}
-
func isFileOpen(fh source.FileHandle) bool {
_, open := fh.(*Overlay)
return open
@@ -1588,7 +1587,7 @@
// that exist only in overlays. As a workaround, we search all of the files
// available in the snapshot and reload their metadata individually using a
// file= query if the metadata is unavailable.
- open := s.openFiles()
+ open := s.overlays()
var files []*Overlay
for _, o := range open {
uri := o.URI()
@@ -1686,10 +1685,26 @@
// TODO(rfindley): reconcile the definition of "orphaned" here with
// reloadOrphanedFiles. The latter does not include files with
// command-line-arguments packages.
-func (s *snapshot) OrphanedFileDiagnostics(ctx context.Context) map[span.URI]*source.Diagnostic {
+func (s *snapshot) OrphanedFileDiagnostics(ctx context.Context) (map[span.URI]*source.Diagnostic, error) {
+ // Orphaned file diagnostics are queried from code actions to produce
+ // quick-fixes (and may be queried many times, once for each file).
+ //
+ // Because they are non-trivial to compute, record them optimistically to
+ // avoid most redundant work.
+ //
+ // This is a hacky workaround: in the future we should avoid recomputing
+ // anything when codeActions provide a diagnostic: simply read the published
+ // diagnostic, if it exists.
s.mu.Lock()
- meta := s.meta
+ existing := s.orphanedFileDiagnostics
s.mu.Unlock()
+ if existing != nil {
+ return existing, nil
+ }
+
+ if err := s.awaitLoaded(ctx); err != nil {
+ return nil, err
+ }
var files []*Overlay
@@ -1699,20 +1714,30 @@
if s.IsBuiltin(uri) || s.view.FileKind(o) != source.Go {
continue
}
- for _, id := range meta.ids[o.URI()] {
- if !source.IsCommandLineArguments(id) || meta.metadata[id].Standalone {
+ md, err := s.MetadataForFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ for _, m := range md {
+ if !source.IsCommandLineArguments(m.ID) || m.Standalone {
continue searchOverlays
}
}
files = append(files, o)
}
if len(files) == 0 {
- return nil
+ return nil, nil
}
- loadedModFiles := make(map[span.URI]struct{})
- ignoredFiles := make(map[span.URI]bool)
- for _, meta := range meta.metadata {
+ loadedModFiles := make(map[span.URI]struct{}) // all mod files, including dependencies
+ ignoredFiles := make(map[span.URI]bool) // files reported in packages.Package.IgnoredFiles
+
+ meta, err := s.AllMetadata(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, meta := range meta {
if meta.Module != nil && meta.Module.GoMod != "" {
gomod := span.URIFromPath(meta.Module.GoMod)
loadedModFiles[gomod] = struct{}{}
@@ -1740,15 +1765,25 @@
continue
}
+ var (
+ msg string // if non-empty, report a diagnostic with this message
+ suggestedFixes []source.SuggestedFix // associated fixes, if any
+ )
+
// If we have a relevant go.mod file, check whether the file is orphaned
// due to its go.mod file being inactive. We could also offer a
// prescriptive diagnostic in the case that there is no go.mod file, but it
// is harder to be precise in that case, and less important.
- var msg string
if goMod, err := nearestModFile(ctx, fh.URI(), s); err == nil && goMod != "" {
if _, ok := loadedModFiles[goMod]; !ok {
modDir := filepath.Dir(goMod.Filename())
- if rel, err := filepath.Rel(s.view.folder.Filename(), modDir); err == nil {
+ viewDir := s.view.folder.Filename()
+
+ // When the module is underneath the view dir, we offer
+ // "use all modules" quick-fixes.
+ inDir := source.InDir(viewDir, modDir)
+
+ if rel, err := filepath.Rel(viewDir, modDir); err == nil {
modDir = rel
}
@@ -1756,8 +1791,59 @@
if s.view.goversion >= 18 {
if s.view.gowork != "" {
fix = fmt.Sprintf("To fix this problem, you can add this module to your go.work file (%s)", s.view.gowork)
+ if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use`", command.RunGoWorkArgs{
+ ViewID: s.view.ID(),
+ Args: []string{"use", modDir},
+ }); err == nil {
+ suggestedFixes = append(suggestedFixes, source.SuggestedFix{
+ Title: "Use this module in your go.work file",
+ Command: &cmd,
+ ActionKind: protocol.QuickFix,
+ })
+ }
+
+ if inDir {
+ if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use -r`", command.RunGoWorkArgs{
+ ViewID: s.view.ID(),
+ Args: []string{"use", "-r", "."},
+ }); err == nil {
+ suggestedFixes = append(suggestedFixes, source.SuggestedFix{
+ Title: "Use all modules in your workspace",
+ Command: &cmd,
+ ActionKind: protocol.QuickFix,
+ })
+ }
+ }
} else {
fix = "To fix this problem, you can add a go.work file that uses this directory."
+
+ if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use`", command.RunGoWorkArgs{
+ ViewID: s.view.ID(),
+ InitFirst: true,
+ Args: []string{"use", modDir},
+ }); err == nil {
+ suggestedFixes = []source.SuggestedFix{
+ {
+ Title: "Add a go.work file using this module",
+ Command: &cmd,
+ ActionKind: protocol.QuickFix,
+ },
+ }
+ }
+
+ if inDir {
+ if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use -r`", command.RunGoWorkArgs{
+ ViewID: s.view.ID(),
+ InitFirst: true,
+ Args: []string{"use", "-r", "."},
+ }); err == nil {
+ suggestedFixes = append(suggestedFixes, source.SuggestedFix{
+ Title: "Add a go.work file using all modules in your workspace",
+ Command: &cmd,
+ ActionKind: protocol.QuickFix,
+ })
+ }
+ }
}
} else {
fix = `To work with multiple modules simultaneously, please upgrade to Go 1.18 or
@@ -1794,16 +1880,22 @@
if msg != "" {
// Only report diagnostics if we detect an actual exclusion.
diagnostics[fh.URI()] = &source.Diagnostic{
- URI: fh.URI(),
- Range: rng,
- Severity: protocol.SeverityWarning,
- Source: source.ListError,
- Message: msg,
+ URI: fh.URI(),
+ Range: rng,
+ Severity: protocol.SeverityWarning,
+ Source: source.ListError,
+ Message: msg,
+ SuggestedFixes: suggestedFixes,
}
}
}
- return diagnostics
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if s.orphanedFileDiagnostics == nil { // another thread may have won the race
+ s.orphanedFileDiagnostics = diagnostics
+ }
+ return s.orphanedFileDiagnostics, nil
}
// TODO(golang/go#53756): this function needs to consider more than just the
@@ -1848,7 +1940,7 @@
reinit := false
wsModFiles, wsModFilesErr := s.workspaceModFiles, s.workspaceModFilesErr
- if workURI := s.view.effectiveGOWORK(); workURI != "" {
+ if workURI, _ := s.view.GOWORK(); workURI != "" {
if change, ok := changes[workURI]; ok {
wsModFiles, wsModFilesErr = computeWorkspaceModFiles(ctx, s.view.gomod, workURI, s.view.effectiveGO111MODULE(), &unappliedChanges{
originalSnapshot: s,