gopls/internal/lsp/cache: improve ad-hoc warning for nested modules
Our warning for the specific (and somewhat arbitrary) case of opening a
nested module was stale. Update it for workspaces.
This is a very weak distillation of CL 448695, targeted at improving
this one error message.
Change-Id: Ic78e2f8ff8004a78ac2a0650b40767de9dfe153c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/454563
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 19b21c6..f79109a 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -275,26 +275,34 @@
return buf.String()
}
-// workspaceLayoutErrors returns a diagnostic for every open file, as well as
-// an error message if there are no open files.
-func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError {
+// workspaceLayoutErrors returns an error decribing a misconfiguration of the
+// workspace, along with related diagnostic.
+//
+// The unusual argument ordering of results is intentional: if the resulting
+// error is nil, so must be the resulting diagnostics.
+//
+// If ctx is cancelled, it may return ctx.Err(), nil.
+//
+// TODO(rfindley): separate workspace diagnostics from critical workspace
+// errors.
+func (s *snapshot) workspaceLayoutError(ctx context.Context) (error, []*source.Diagnostic) {
// TODO(rfindley): do we really not want to show a critical error if the user
// has no go.mod files?
if len(s.workspace.getKnownModFiles()) == 0 {
- return nil
+ return nil, nil
}
// TODO(rfindley): both of the checks below should be delegated to the workspace.
if s.view.effectiveGO111MODULE() == off {
- return nil
+ return nil, nil
}
if s.workspace.moduleSource != legacyWorkspace {
- return nil
+ return nil, nil
}
// If the user has one module per view, there is nothing to warn about.
if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 {
- return nil
+ return nil, nil
}
// Apply diagnostics about the workspace configuration to relevant open
@@ -320,10 +328,7 @@
See the documentation for more information on setting up your workspace:
https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`
}
- return &source.CriticalError{
- MainError: fmt.Errorf(msg),
- Diagnostics: s.applyCriticalErrorToFiles(ctx, msg, openFiles),
- }
+ return fmt.Errorf(msg), s.applyCriticalErrorToFiles(ctx, msg, openFiles)
}
// If the user has one active go.mod file, they may still be editing files
@@ -331,40 +336,49 @@
// that the nested module must be opened as a workspace folder.
if len(s.workspace.ActiveModFiles()) == 1 {
// Get the active root go.mod file to compare against.
- var rootModURI span.URI
+ var rootMod string
for uri := range s.workspace.ActiveModFiles() {
- rootModURI = uri
+ rootMod = uri.Filename()
}
- nestedModules := map[string][]source.VersionedFileHandle{}
+ rootDir := filepath.Dir(rootMod)
+ nestedModules := make(map[string][]source.VersionedFileHandle)
for _, fh := range openFiles {
- modURI := moduleForURI(s.workspace.knownModFiles, fh.URI())
- if modURI != rootModURI {
- modDir := filepath.Dir(modURI.Filename())
+ mod, err := findRootPattern(ctx, filepath.Dir(fh.URI().Filename()), "go.mod", s)
+ if err != nil {
+ if ctx.Err() != nil {
+ return ctx.Err(), nil
+ }
+ continue
+ }
+ if mod == "" {
+ continue
+ }
+ if mod != rootMod && source.InDir(rootDir, mod) {
+ modDir := filepath.Dir(mod)
nestedModules[modDir] = append(nestedModules[modDir], fh)
}
}
+ var multiModuleMsg string
+ if s.view.goversion >= 18 {
+ multiModuleMsg = `To work on multiple modules at once, please use a go.work file.
+See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more information on using workspaces.`
+ } else {
+ multiModuleMsg = `To work on multiple modules at once, please upgrade to Go 1.18 and use a go.work file.
+See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more information on using workspaces.`
+ }
// Add a diagnostic to each file in a nested module to mark it as
// "orphaned". Don't show a general diagnostic in the progress bar,
// because the user may still want to edit a file in a nested module.
var srcDiags []*source.Diagnostic
for modDir, uris := range nestedModules {
- msg := fmt.Sprintf(`This file is in %s, which is a nested module in the %s module.
-gopls currently requires one module per workspace folder.
-Please open %s as a separate workspace folder.
-You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.
-`, modDir, filepath.Dir(rootModURI.Filename()), modDir)
+ msg := fmt.Sprintf("This file is in %s, which is a nested module in the %s module.\n%s", modDir, rootMod, multiModuleMsg)
srcDiags = append(srcDiags, s.applyCriticalErrorToFiles(ctx, msg, uris)...)
}
if len(srcDiags) != 0 {
- return &source.CriticalError{
- MainError: fmt.Errorf(`You are working in a nested module.
-Please open it as a separate workspace folder. Learn more:
-https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`),
- Diagnostics: srcDiags,
- }
+ return fmt.Errorf("You have opened a nested module.\n%s", multiModuleMsg), srcDiags
}
}
- return nil
+ return nil, nil
}
func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Diagnostic {
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
index f433211..0a4b88e 100644
--- a/gopls/internal/lsp/cache/mod_tidy.go
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -63,6 +63,9 @@
Diagnostics: criticalErr.Diagnostics,
}, nil
}
+ if ctx.Err() != nil { // must check ctx after GetCriticalError
+ return nil, ctx.Err()
+ }
if err := s.awaitLoaded(ctx); err != nil {
return nil, err
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 0b56120..9650655 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -1212,6 +1212,8 @@
return results, nil
}
+// TODO(rfindley): clarify that this is only active modules. Or update to just
+// use findRootPattern.
func (s *snapshot) GoModForFile(uri span.URI) span.URI {
return moduleForURI(s.workspace.activeModFiles, uri)
}
@@ -1396,13 +1398,31 @@
//
// TODO(rfindley): re-evaluate this heuristic.
if containsCommandLineArguments(wsPkgs) {
- return s.workspaceLayoutError(ctx)
+ err, diags := s.workspaceLayoutError(ctx)
+ if err != nil {
+ if ctx.Err() != nil {
+ return nil // see the API documentation for source.Snapshot
+ }
+ return &source.CriticalError{
+ MainError: err,
+ Diagnostics: diags,
+ }
+ }
}
return nil
}
if errMsg := loadErr.MainError.Error(); strings.Contains(errMsg, "cannot find main module") || strings.Contains(errMsg, "go.mod file not found") {
- return s.workspaceLayoutError(ctx)
+ err, diags := s.workspaceLayoutError(ctx)
+ if err != nil {
+ if ctx.Err() != nil {
+ return nil // see the API documentation for source.Snapshot
+ }
+ return &source.CriticalError{
+ MainError: err,
+ Diagnostics: diags,
+ }
+ }
}
return loadErr
}
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index bc623ff..4fad654 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -859,33 +859,38 @@
// Otherwise, it returns folder.
// TODO (rFindley): move this to workspace.go
// TODO (rFindley): simplify this once workspace modules are enabled by default.
-func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSource, excludePath func(string) bool, experimental bool) (span.URI, error) {
+func findWorkspaceRoot(ctx context.Context, folderURI span.URI, fs source.FileSource, excludePath func(string) bool, experimental bool) (span.URI, error) {
patterns := []string{"go.work", "go.mod"}
if experimental {
patterns = []string{"go.work", "gopls.mod", "go.mod"}
}
+ folder := folderURI.Filename()
for _, basename := range patterns {
- dir, err := findRootPattern(ctx, folder, basename, fs)
+ match, err := findRootPattern(ctx, folder, basename, fs)
if err != nil {
- return "", fmt.Errorf("finding %s: %w", basename, err)
+ if ctxErr := ctx.Err(); ctxErr != nil {
+ return "", ctxErr
+ }
+ return "", err
}
- if dir != "" {
+ if match != "" {
+ dir := span.URIFromPath(filepath.Dir(match))
return dir, nil
}
}
// The experimental workspace can handle nested modules at this point...
if experimental {
- return folder, nil
+ return folderURI, nil
}
// ...else we should check if there's exactly one nested module.
- all, err := findModules(folder, excludePath, 2)
+ all, err := findModules(folderURI, excludePath, 2)
if err == errExhausted {
// Fall-back behavior: if we don't find any modules after searching 10000
// files, assume there are none.
event.Log(ctx, fmt.Sprintf("stopped searching for modules after %d files", fileLimit))
- return folder, nil
+ return folderURI, nil
}
if err != nil {
return "", err
@@ -896,19 +901,24 @@
return dirURI(uri), nil
}
}
- return folder, nil
+ return folderURI, nil
}
-func findRootPattern(ctx context.Context, folder span.URI, basename string, fs source.FileSource) (span.URI, error) {
- dir := folder.Filename()
+// findRootPattern looks for files with the given basename in dir or any parent
+// directory of dir, using the provided FileSource. It returns the first match,
+// starting from dir and search parents.
+//
+// The resulting string is either the file path of a matching file with the
+// given basename, or "" if none was found.
+func findRootPattern(ctx context.Context, dir, basename string, fs source.FileSource) (string, error) {
for dir != "" {
target := filepath.Join(dir, basename)
exists, err := fileExists(ctx, span.URIFromPath(target), fs)
if err != nil {
- return "", err
+ return "", err // not readable or context cancelled
}
if exists {
- return span.URIFromPath(dir), nil
+ return target, nil
}
// Trailing separators must be trimmed, otherwise filepath.Split is a noop.
next, _ := filepath.Split(strings.TrimRight(dir, string(filepath.Separator)))
diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go
index 19bc3ce..6b83cf8 100644
--- a/gopls/internal/lsp/diagnostics.go
+++ b/gopls/internal/lsp/diagnostics.go
@@ -295,6 +295,9 @@
return
}
criticalErr := snapshot.GetCriticalError(ctx)
+ if ctx.Err() != nil { // must check ctx after GetCriticalError
+ return
+ }
// Show the error as a progress error report so that it appears in the
// status bar. If a client doesn't support progress reports, the error
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index fa55cde..e82a89a 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -214,6 +214,8 @@
MetadataForFile(ctx context.Context, uri span.URI) ([]*Metadata, error)
// GetCriticalError returns any critical errors in the workspace.
+ //
+ // A nil result may mean success, or context cancellation.
GetCriticalError(ctx context.Context) *CriticalError
// BuildGoplsMod generates a go.mod file for all modules in the workspace.