internal/lsp: handle modifications to the workspace module
We should be able to smoothly add and remove modules from the workspace.
This change moves the module-related fields from the view into the
snapshot so that they can be easily modified and recomputed. The
workspace module is now a workspaceModuleHandle.
Updates golang/go#32394
Change-Id: I6ade7f223cc6070a29b6021b825586b753a0daf1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/254940
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/regtest/modfile_test.go b/gopls/internal/regtest/modfile_test.go
index a7f33ab..5ec9f23 100644
--- a/gopls/internal/regtest/modfile_test.go
+++ b/gopls/internal/regtest/modfile_test.go
@@ -74,7 +74,7 @@
// Reproduce golang/go#40269 by deleting and recreating main.go.
t.Run("delete main.go", func(t *testing.T) {
- t.Skipf("This test will be flaky until golang/go#40269 is resolved.")
+ t.Skip("This test will be flaky until golang/go#40269 is resolved.")
withOptions(WithProxyFiles(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
goModContent := env.ReadWorkspaceFile("go.mod")
diff --git a/gopls/internal/regtest/watch_test.go b/gopls/internal/regtest/watch_test.go
index 50a628a..8ed4f7b 100644
--- a/gopls/internal/regtest/watch_test.go
+++ b/gopls/internal/regtest/watch_test.go
@@ -185,7 +185,7 @@
}
`
runner.Run(t, missing, func(t *testing.T, env *Env) {
- t.Skipf("the initial workspace load fails and never retries")
+ t.Skip("the initial workspace load fails and never retries")
env.Await(
env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""),
@@ -586,9 +586,9 @@
})
}
-// Reproduces golang/go#37069.
+// Reproduces golang/go#40340.
func TestSwitchFromGOPATHToModules(t *testing.T) {
- t.Skipf("golang/go#37069 is not yet resolved.")
+ t.Skip("golang/go#40340 is not yet resolved.")
const files = `
-- foo/blah/blah.go --
diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace_test.go
index d672316..2c1a1d9 100644
--- a/gopls/internal/regtest/workspace_test.go
+++ b/gopls/internal/regtest/workspace_test.go
@@ -6,6 +6,7 @@
import (
"fmt"
+ "strings"
"testing"
"golang.org/x/tools/internal/lsp"
@@ -205,3 +206,108 @@
)
})
}
+
+// This change tests that the version of the module used changes after it has
+// been deleted from the workspace.
+func TestDeleteModule_Interdependent(t *testing.T) {
+ const multiModule = `
+-- moda/a/go.mod --
+module a.com
+
+require b.com v1.2.3
+
+-- moda/a/a.go --
+package a
+
+import (
+ "b.com/b"
+)
+
+func main() {
+ var x int
+ _ = b.Hello()
+}
+-- modb/go.mod --
+module b.com
+
+-- modb/b/b.go --
+package b
+
+func Hello() int {
+ var x int
+}
+`
+ withOptions(
+ WithProxyFiles(workspaceModuleProxy),
+ ).run(t, multiModule, func(t *testing.T, env *Env) {
+ env.Await(InitialWorkspaceLoad)
+ env.OpenFile("moda/a/a.go")
+
+ original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+ if want := "modb/b/b.go"; !strings.HasSuffix(original, want) {
+ t.Errorf("expected %s, got %v", want, original)
+ }
+ env.CloseBuffer(original)
+ env.RemoveWorkspaceFile("modb/b/b.go")
+ env.RemoveWorkspaceFile("modb/go.mod")
+ env.Await(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
+ )
+ got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+ if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) {
+ t.Errorf("expected %s, got %v", want, got)
+ }
+ })
+}
+
+// This change tests that the version of the module used changes after it has
+// been added to the workspace.
+func TestCreateModule_Interdependent(t *testing.T) {
+ const multiModule = `
+-- moda/a/go.mod --
+module a.com
+
+require b.com v1.2.3
+
+-- moda/a/a.go --
+package a
+
+import (
+ "b.com/b"
+)
+
+func main() {
+ var x int
+ _ = b.Hello()
+}
+`
+ withOptions(
+ WithProxyFiles(workspaceModuleProxy),
+ ).run(t, multiModule, func(t *testing.T, env *Env) {
+ env.Await(InitialWorkspaceLoad)
+ env.OpenFile("moda/a/a.go")
+ original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+ if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(original, want) {
+ t.Errorf("expected %s, got %v", want, original)
+ }
+ env.WriteWorkspaceFiles(map[string]string{
+ "modb/go.mod": "module b.com",
+ "modb/b/b.go": `package b
+
+func Hello() int {
+ var x int
+}
+`,
+ })
+ env.Await(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+ env.DiagnosticAtRegexp("modb/b/b.go", "x"),
+ ),
+ )
+ got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+ if want := "modb/b/b.go"; !strings.HasSuffix(got, want) {
+ t.Errorf("expected %s, got %v", want, original)
+ }
+ })
+}
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index 48b0912..7bea132 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -177,8 +177,7 @@
b.WriteString(string(dep))
}
for _, cgf := range pghs {
- b.WriteString(string(cgf.file.URI()))
- b.WriteString(cgf.file.FileIdentity().Hash)
+ b.WriteString(cgf.file.FileIdentity().String())
}
return packageHandleKey(hashContents(b.Bytes()))
}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 0dc975a..52568e7 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -199,13 +199,18 @@
// packages.Loads that occur from within the workspace module.
func (s *snapshot) tempWorkspaceModule(ctx context.Context) (_ span.URI, cleanup func(), err error) {
cleanup = func() {}
- if len(s.view.modules) == 0 {
+ if len(s.modules) == 0 {
return "", cleanup, nil
}
- if s.view.workspaceModule == nil {
- return "", cleanup, nil
+ wsModuleHandle, err := s.getWorkspaceModuleHandle(ctx)
+ if err != nil {
+ return "", nil, err
}
- content, err := s.view.workspaceModule.Format()
+ file, err := wsModuleHandle.build(ctx, s)
+ if err != nil {
+ return "", nil, err
+ }
+ content, err := file.Format()
if err != nil {
return "", cleanup, err
}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 28aae96..597e46b 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -7,8 +7,6 @@
import (
"context"
"fmt"
- "os"
- "path/filepath"
"strconv"
"strings"
"sync"
@@ -173,7 +171,6 @@
name: name,
folder: folder,
root: folder,
- modules: make(map[span.URI]*moduleRoot),
filesByURI: make(map[span.URI]*fileBase),
filesByBase: make(map[string][]*fileBase),
}
@@ -194,6 +191,7 @@
modTidyHandles: make(map[span.URI]*modTidyHandle),
modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
modWhyHandles: make(map[span.URI]*modWhyHandle),
+ modules: make(map[span.URI]*moduleRoot),
}
if v.session.cache.options != nil {
@@ -206,7 +204,7 @@
}
// Find all of the modules in the workspace.
- if err := v.findWorkspaceModules(ctx, options); err != nil {
+ if err := v.snapshot.findWorkspaceModules(ctx, options); err != nil {
return nil, nil, func() {}, err
}
@@ -214,11 +212,9 @@
// check if the view has a valid build configuration.
v.setBuildConfiguration()
- // Build the workspace module, if needed.
- if options.ExperimentalWorkspaceModule {
- if err := v.buildWorkspaceModule(ctx); err != nil {
- return nil, nil, func() {}, err
- }
+ // Decide if we should use the workspace module.
+ if v.determineWorkspaceModuleLocked() {
+ v.workspaceMode |= usesWorkspaceModule | moduleMode
}
// We have v.goEnv now.
@@ -242,94 +238,12 @@
snapshot := v.snapshot
release := snapshot.generation.Acquire(initCtx)
go func() {
- v.initialize(initCtx, snapshot, true)
+ snapshot.initialize(initCtx, true)
release()
}()
return v, snapshot, snapshot.generation.Acquire(ctx), nil
}
-// findWorkspaceModules walks the view's root folder, looking for go.mod files.
-// Any that are found are added to the view's set of modules, which are then
-// used to construct the workspace module.
-//
-// It assumes that the caller has not yet created the view, and therefore does
-// not lock any of the internal data structures before accessing them.
-func (v *View) findWorkspaceModules(ctx context.Context, options *source.Options) error {
- // If the user is intentionally limiting their workspace scope, add their
- // folder to the roots and return early.
- if !options.ExpandWorkspaceToModule {
- return nil
- }
- // The workspace module has been disabled by the user.
- if !options.ExperimentalWorkspaceModule {
- return nil
- }
-
- // Walk the view's folder to find all modules in the view.
- root := v.root.Filename()
- return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- // For any path that is not the workspace folder, check if the path
- // would be ignored by the go command. Vendor directories also do not
- // contain workspace modules.
- if info.IsDir() && path != root {
- suffix := strings.TrimPrefix(path, root)
- switch {
- case checkIgnored(suffix),
- strings.Contains(filepath.ToSlash(suffix), "/vendor/"):
- return filepath.SkipDir
- }
- }
- // We're only interested in go.mod files.
- if filepath.Base(path) != "go.mod" {
- return nil
- }
- // At this point, we definitely have a go.mod file in the workspace,
- // so add it to the view.
- modURI := span.URIFromPath(path)
- rootURI := span.URIFromPath(filepath.Dir(path))
- v.modules[rootURI] = &moduleRoot{
- rootURI: rootURI,
- modURI: modURI,
- sumURI: span.URIFromPath(sumFilename(modURI)),
- }
- return nil
- })
-}
-
-func (v *View) buildWorkspaceModule(ctx context.Context) error {
- // If the view has an invalid configuration, don't build the workspace
- // module.
- if !v.hasValidBuildConfiguration {
- return nil
- }
- // If the view is not in a module and contains no modules, but still has a
- // valid workspace configuration, do not create the workspace module.
- // It could be using GOPATH or a different build system entirely.
- if v.modURI == "" && len(v.modules) == 0 && v.hasValidBuildConfiguration {
- return nil
- }
- v.workspaceMode |= moduleMode
-
- // Don't default to multi-workspace mode if one of the modules contains a
- // vendor directory. We still have to decide how to handle vendoring.
- for _, mod := range v.modules {
- if info, _ := os.Stat(filepath.Join(mod.rootURI.Filename(), "vendor")); info != nil {
- return nil
- }
- }
-
- v.workspaceMode |= usesWorkspaceModule
-
- // If the user does not have a gopls.mod, we need to create one, based on
- // modules we found in the user's workspace.
- var err error
- v.workspaceModule, err = v.snapshot.buildWorkspaceModule(ctx)
- return err
-}
-
// View returns the view by name.
func (s *Session) View(name string) source.View {
s.viewMu.Lock()
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 14c575d..3d5db7a 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -94,6 +94,13 @@
modTidyHandles map[span.URI]*modTidyHandle
modUpgradeHandles map[span.URI]*modUpgradeHandle
modWhyHandles map[span.URI]*modWhyHandle
+
+ // modules is the set of modules currently in this workspace.
+ modules map[span.URI]*moduleRoot
+
+ // workspaceModuleHandle keeps track of the in-memory representation of the
+ // go.mod file for the workspace module.
+ workspaceModuleHandle *workspaceModuleHandle
}
type packageKey struct {
@@ -673,12 +680,15 @@
s.mu.Lock()
defer s.mu.Unlock()
+ return s.getFileLocked(ctx, f)
+}
+func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.VersionedFileHandle, error) {
if fh, ok := s.files[f.URI()]; ok {
return fh, nil
}
- fh, err := s.view.session.cache.getFile(ctx, uri)
+ fh, err := s.view.session.cache.getFile(ctx, f.URI())
if err != nil {
return nil, err
}
@@ -732,7 +742,7 @@
}
// We typically prefer to run something as intensive as the IWL without
// blocking. I'm not sure if there is a way to do that here.
- s.view.initialize(ctx, s, false)
+ s.initialize(ctx, false)
}
// reloadWorkspace reloads the metadata for all invalidated workspace packages.
@@ -851,24 +861,26 @@
newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1))
result := &snapshot{
- id: s.id + 1,
- generation: newGen,
- view: s.view,
- builtin: s.builtin,
- ids: make(map[span.URI][]packageID),
- importedBy: make(map[packageID][]packageID),
- metadata: make(map[packageID]*metadata),
- packages: make(map[packageKey]*packageHandle),
- actions: make(map[actionKey]*actionHandle),
- files: make(map[span.URI]source.VersionedFileHandle),
- goFiles: make(map[parseKey]*parseGoHandle),
- workspaceDirectories: make(map[span.URI]struct{}),
- workspacePackages: make(map[packageID]packagePath),
- unloadableFiles: make(map[span.URI]struct{}),
- parseModHandles: make(map[span.URI]*parseModHandle),
- modTidyHandles: make(map[span.URI]*modTidyHandle),
- modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
- modWhyHandles: make(map[span.URI]*modWhyHandle),
+ id: s.id + 1,
+ generation: newGen,
+ view: s.view,
+ builtin: s.builtin,
+ ids: make(map[span.URI][]packageID),
+ importedBy: make(map[packageID][]packageID),
+ metadata: make(map[packageID]*metadata),
+ packages: make(map[packageKey]*packageHandle),
+ actions: make(map[actionKey]*actionHandle),
+ files: make(map[span.URI]source.VersionedFileHandle),
+ goFiles: make(map[parseKey]*parseGoHandle),
+ workspaceDirectories: make(map[span.URI]struct{}),
+ workspacePackages: make(map[packageID]packagePath),
+ unloadableFiles: make(map[span.URI]struct{}),
+ parseModHandles: make(map[span.URI]*parseModHandle),
+ modTidyHandles: make(map[span.URI]*modTidyHandle),
+ modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
+ modWhyHandles: make(map[span.URI]*modWhyHandle),
+ modules: make(map[span.URI]*moduleRoot),
+ workspaceModuleHandle: s.workspaceModuleHandle,
}
if s.builtin != nil {
@@ -885,7 +897,6 @@
}
// Copy all of the modHandles.
for k, v := range s.parseModHandles {
- newGen.Inherit(v.handle)
result.parseModHandles[k] = v
}
// Copy all of the workspace directories. They may be reset later.
@@ -923,6 +934,11 @@
result.modWhyHandles[k] = v
}
+ // Add all of the modules now. They may be deleted or added to later.
+ for k, v := range s.modules {
+ result.modules[k] = v
+ }
+
// transitiveIDs keeps track of transitive reverse dependencies.
// If an ID is present in the map, invalidate its types.
// If an ID's value is true, invalidate its metadata too.
@@ -957,24 +973,69 @@
delete(result.modWhyHandles, k)
}
}
- if currentFH.Kind() == source.Mod {
- // If the view's go.mod file's contents have changed, invalidate the
- // metadata for every known package in the snapshot.
+ currentExists := currentFH.URI() != ""
+ if currentExists {
+ if _, err := currentFH.Read(); os.IsNotExist(err) {
+ currentExists = false
+ }
+ }
+ // If the file invalidation is for a go.mod. originalFH is nil if the
+ // file is newly created.
+ currentMod := currentExists && currentFH.Kind() == source.Mod
+ originalMod := originalFH != nil && originalFH.Kind() == source.Mod
+ if currentMod || originalMod {
+ // If the view's go.mod file's contents have changed, invalidate
+ // the metadata for every known package in the snapshot.
if invalidateMetadata {
for k := range s.packages {
directIDs[k.id] = struct{}{}
}
+ // If a go.mod file in the workspace has changed, we need to
+ // rebuild the workspace module.
+ result.workspaceModuleHandle = nil
}
-
delete(result.parseModHandles, withoutURI)
- if currentFH.URI() == s.view.modURI {
- // The go.mod's replace directives may have changed. We may
- // need to update our set of workspace directories. Use the new
- // snapshot, as it can be locked without causing issues.
- result.workspaceDirectories = result.findWorkspaceDirectories(ctx, currentFH)
+ // Check if this is a newly created go.mod file. When a new module
+ // is created, we have to retry the initial workspace load.
+ rootURI := span.URIFromPath(filepath.Dir(withoutURI.Filename()))
+ if currentMod {
+ if _, ok := result.modules[rootURI]; !ok {
+ result.addModule(ctx, currentFH.URI())
+ result.view.definitelyReinitialize()
+ }
+ } else if originalMod {
+ // Similarly, we need to retry the IWL if a go.mod in the workspace
+ // was deleted.
+ if _, ok := result.modules[rootURI]; ok {
+ delete(result.modules, rootURI)
+ result.view.definitelyReinitialize()
+ }
}
}
+ // Keep track of the creations and deletions of go.sum files.
+ // Creating a go.sum without an associated go.mod has no effect on the
+ // set of modules.
+ currentSum := currentExists && currentFH.Kind() == source.Sum
+ originalSum := originalFH != nil && originalFH.Kind() == source.Sum
+ if currentSum || originalSum {
+ rootURI := span.URIFromPath(filepath.Dir(withoutURI.Filename()))
+ if currentSum {
+ if mod, ok := result.modules[rootURI]; ok {
+ mod.sumURI = currentFH.URI()
+ }
+ } else if originalSum {
+ if mod, ok := result.modules[rootURI]; ok {
+ mod.sumURI = ""
+ }
+ }
+ }
+ if withoutURI == s.view.modURI {
+ // The go.mod's replace directives may have changed. We may
+ // need to update our set of workspace directories. Use the new
+ // snapshot, as it can be locked without causing issues.
+ result.workspaceDirectories = result.findWorkspaceDirectories(ctx, currentFH)
+ }
// If this is a file we don't yet know about,
// then we do not yet know what packages it should belong to.
@@ -1017,7 +1078,7 @@
}
// Handle the invalidated file; it may have new contents or not exist.
- if _, err := currentFH.Read(); os.IsNotExist(err) {
+ if !currentExists {
delete(result.files, withoutURI)
} else {
result.files[withoutURI] = currentFH
@@ -1087,15 +1148,21 @@
}
// Inherit all of the go.mod-related handles.
- for _, v := range s.modTidyHandles {
+ for _, v := range result.modTidyHandles {
newGen.Inherit(v.handle)
}
- for _, v := range s.modUpgradeHandles {
+ for _, v := range result.modUpgradeHandles {
newGen.Inherit(v.handle)
}
- for _, v := range s.modWhyHandles {
+ for _, v := range result.modWhyHandles {
newGen.Inherit(v.handle)
}
+ for _, v := range result.parseModHandles {
+ newGen.Inherit(v.handle)
+ }
+ if result.workspaceModuleHandle != nil {
+ newGen.Inherit(result.workspaceModuleHandle.handle)
+ }
// Don't bother copying the importedBy graph,
// as it changes each time we update metadata.
@@ -1128,9 +1195,9 @@
if originalFH.FileIdentity() == currentFH.FileIdentity() {
return false
}
- // If a go.mod file's contents have changed, always invalidate metadata.
+ // If a go.mod in the workspace has been changed, invalidate metadata.
if kind := originalFH.Kind(); kind == source.Mod {
- return originalFH.URI() == s.view.modURI
+ return isSubdirectory(filepath.Dir(s.view.root.Filename()), filepath.Dir(originalFH.URI().Filename()))
}
// Get the original and current parsed files in order to check package name
// and imports. Use the new snapshot to parse to avoid modifying the
@@ -1263,6 +1330,64 @@
return nil
}
+type workspaceModuleHandle struct {
+ handle *memoize.Handle
+}
+
+type workspaceModuleData struct {
+ file *modfile.File
+ err error
+}
+
+type workspaceModuleKey string
+
+func (wmh *workspaceModuleHandle) build(ctx context.Context, snapshot *snapshot) (*modfile.File, error) {
+ v, err := wmh.handle.Get(ctx, snapshot.generation, snapshot)
+ if err != nil {
+ return nil, err
+ }
+ data := v.(*workspaceModuleData)
+ return data.file, data.err
+}
+
+func (s *snapshot) getWorkspaceModuleHandle(ctx context.Context) (*workspaceModuleHandle, error) {
+ s.mu.Lock()
+ wsModule := s.workspaceModuleHandle
+ s.mu.Unlock()
+ if wsModule != nil {
+ return wsModule, nil
+ }
+ var fhs []source.FileHandle
+ for _, mod := range s.modules {
+ fh, err := s.GetFile(ctx, mod.modURI)
+ if err != nil {
+ return nil, err
+ }
+ fhs = append(fhs, fh)
+ }
+ sort.Slice(fhs, func(i, j int) bool {
+ return fhs[i].URI() < fhs[j].URI()
+ })
+ var k string
+ for _, fh := range fhs {
+ k += fh.FileIdentity().String()
+ }
+ key := workspaceModuleKey(hashContents([]byte(k)))
+ h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
+ s := arg.(*snapshot)
+ data := &workspaceModuleData{}
+ data.file, data.err = s.buildWorkspaceModule(ctx)
+ return data
+ })
+ wsModule = &workspaceModuleHandle{
+ handle: h,
+ }
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.workspaceModuleHandle = wsModule
+ return s.workspaceModuleHandle, nil
+}
+
// buildWorkspaceModule generates a workspace module given the modules in the
// the workspace.
func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, error) {
@@ -1270,8 +1395,8 @@
file.AddModuleStmt("gopls-workspace")
paths := make(map[string]*moduleRoot)
- for _, mod := range s.view.modules {
- fh, err := s.view.snapshot.GetFile(ctx, mod.modURI)
+ for _, mod := range s.modules {
+ fh, err := s.GetFile(ctx, mod.modURI)
if err != nil {
return nil, err
}
@@ -1291,12 +1416,12 @@
}
// Go back through all of the modules to handle any of their replace
// statements.
- for _, module := range s.view.modules {
- fh, err := s.view.snapshot.GetFile(ctx, module.modURI)
+ for _, module := range s.modules {
+ fh, err := s.GetFile(ctx, module.modURI)
if err != nil {
return nil, err
}
- pmf, err := s.view.snapshot.ParseMod(ctx, fh)
+ pmf, err := s.ParseMod(ctx, fh)
if err != nil {
return nil, err
}
@@ -1326,3 +1451,52 @@
}
return file, nil
}
+
+// findWorkspaceModules walks the view's root folder, looking for go.mod
+// files. Any that are found are added to the view's set of modules, which are
+// then used to construct the workspace module.
+//
+// It assumes that the caller has not yet created the view, and therefore does
+// not lock any of the internal data structures before accessing them.
+func (s *snapshot) findWorkspaceModules(ctx context.Context, options *source.Options) error {
+ // Walk the view's folder to find all modules in the view.
+ root := s.view.root.Filename()
+ return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ // For any path that is not the workspace folder, check if the path
+ // would be ignored by the go command. Vendor directories also do not
+ // contain workspace modules.
+ if info.IsDir() && path != root {
+ suffix := strings.TrimPrefix(path, root)
+ switch {
+ case checkIgnored(suffix),
+ strings.Contains(filepath.ToSlash(suffix), "/vendor/"):
+ return filepath.SkipDir
+ }
+ }
+ // We're only interested in go.mod files.
+ if filepath.Base(path) != "go.mod" {
+ return nil
+ }
+ // At this point, we definitely have a go.mod file in the workspace,
+ // so add it to the view.
+ modURI := span.URIFromPath(path)
+ s.addModule(ctx, modURI)
+ return nil
+ })
+}
+
+func (s *snapshot) addModule(ctx context.Context, modURI span.URI) {
+ rootURI := span.URIFromPath(filepath.Dir(modURI.Filename()))
+ sumURI := span.URIFromPath(sumFilename(modURI))
+ if info, _ := os.Stat(sumURI.Filename()); info == nil {
+ sumURI = ""
+ }
+ s.modules[rootURI] = &moduleRoot{
+ rootURI: rootURI,
+ modURI: modURI,
+ sumURI: sumURI,
+ }
+}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 9412d88..d2cf068 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -67,16 +67,6 @@
// is just the folder. If we are in module mode, this is the module root.
root span.URI
- // TODO: The modules and workspaceModule fields should probably be moved to
- // the snapshot and invalidated on file changes.
-
- // modules is the set of modules currently in this workspace.
- modules map[span.URI]*moduleRoot
-
- // workspaceModule is an in-memory representation of the go.mod file for
- // the workspace module.
- workspaceModule *modfile.File
-
// importsMu guards imports-related state, particularly the ProcessEnv.
importsMu sync.Mutex
@@ -687,42 +677,42 @@
return v.snapshot, v.snapshot.generation.Acquire(ctx)
}
-func (v *View) initialize(ctx context.Context, s *snapshot, firstAttempt bool) {
+func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) {
select {
case <-ctx.Done():
return
- case v.initializationSema <- struct{}{}:
+ case s.view.initializationSema <- struct{}{}:
}
defer func() {
- <-v.initializationSema
+ <-s.view.initializationSema
}()
- if v.initializeOnce == nil {
+ if s.view.initializeOnce == nil {
return
}
- v.initializeOnce.Do(func() {
+ s.view.initializeOnce.Do(func() {
defer func() {
- v.initializeOnce = nil
+ s.view.initializeOnce = nil
if firstAttempt {
- close(v.initialized)
+ close(s.view.initialized)
}
}()
// If we have multiple modules, we need to load them by paths.
var scopes []interface{}
- if len(v.modules) > 0 {
+ if len(s.modules) > 0 {
// TODO(rstambler): Retry the initial workspace load for whichever
// modules we failed to load.
- for _, mod := range v.modules {
+ for _, mod := range s.modules {
fh, err := s.GetFile(ctx, mod.modURI)
if err != nil {
- v.initializedErr = err
+ s.view.initializedErr = err
continue
}
parsed, err := s.ParseMod(ctx, fh)
if err != nil {
- v.initializedErr = err
+ s.view.initializedErr = err
continue
}
path := parsed.File.Module.Mod.Path
@@ -738,7 +728,7 @@
if err != nil {
event.Error(ctx, "initial workspace load failed", err)
}
- v.initializedErr = err
+ s.view.initializedErr = err
})
}
@@ -779,12 +769,20 @@
}
func (v *View) maybeReinitialize() {
+ v.reinitialize(false)
+}
+
+func (v *View) definitelyReinitialize() {
+ v.reinitialize(true)
+}
+
+func (v *View) reinitialize(force bool) {
v.initializationSema <- struct{}{}
defer func() {
<-v.initializationSema
}()
- if v.initializedErr == nil {
+ if !force && v.initializedErr == nil {
return
}
var once sync.Once
@@ -856,6 +854,38 @@
return nil
}
+func (v *View) determineWorkspaceModuleLocked() bool {
+ // If the user is intentionally limiting their workspace scope, add their
+ // folder to the roots and return early.
+ if !v.options.ExpandWorkspaceToModule {
+ return false
+ }
+ // The workspace module has been disabled by the user.
+ if !v.options.ExperimentalWorkspaceModule {
+ return false
+ }
+ // If the view has an invalid configuration, don't build the workspace
+ // module.
+ if !v.hasValidBuildConfiguration {
+ return false
+ }
+ // If the view is not in a module and contains no modules, but still has a
+ // valid workspace configuration, do not create the workspace module.
+ // It could be using GOPATH or a different build system entirely.
+ if v.modURI == "" && len(v.snapshot.modules) == 0 && v.hasValidBuildConfiguration {
+ return false
+ }
+
+ // Don't default to multi-workspace mode if one of the modules contains a
+ // vendor directory. We still have to decide how to handle vendoring.
+ for _, mod := range v.snapshot.modules {
+ if info, _ := os.Stat(filepath.Join(mod.rootURI.Filename(), "vendor")); info != nil {
+ return false
+ }
+ }
+ return true
+}
+
func (v *View) setBuildConfiguration() (isValid bool) {
defer func() {
v.hasValidBuildConfiguration = isValid
@@ -870,7 +900,7 @@
if v.modURI != "" {
return true
}
- if len(v.modules) > 0 {
+ if len(v.snapshot.modules) > 0 {
return true
}
// The user may have a multiple directories in their GOPATH.
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index f8f6bdd..d534eb0 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -400,6 +400,10 @@
Kind FileKind
}
+func (id FileIdentity) String() string {
+ return fmt.Sprintf("%s%s%s", id.URI, id.Hash, id.Kind)
+}
+
// FileKind describes the kind of the file in question.
// It can be one of Go, mod, or sum.
type FileKind int