diff --git a/gopls/internal/regtest/modfile_test.go b/gopls/internal/regtest/modfile_test.go index a7f33aba62..5ec9f233d8 100644 --- a/gopls/internal/regtest/modfile_test.go +++ b/gopls/internal/regtest/modfile_test.go @@ -74,7 +74,7 @@ func main() { // 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 50a628a7ef..8ed4f7be9e 100644 --- a/gopls/internal/regtest/watch_test.go +++ b/gopls/internal/regtest/watch_test.go @@ -185,7 +185,7 @@ func _() { } ` 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 @@ func main() { }) } -// 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 d672316a09..2c1a1d9e3d 100644 --- a/gopls/internal/regtest/workspace_test.go +++ b/gopls/internal/regtest/workspace_test.go @@ -6,6 +6,7 @@ package regtest import ( "fmt" + "strings" "testing" "golang.org/x/tools/internal/lsp" @@ -205,3 +206,108 @@ func Hello() int { ) }) } + +// 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 48b09124ba..7bea1325ac 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -177,8 +177,7 @@ func checkPackageKey(ctx context.Context, id packageID, pghs []*parseGoHandle, c 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 0dc975a898..52568e7357 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -199,13 +199,18 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error { // 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 28aae96daa..597e46bb4a 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -7,8 +7,6 @@ package cache import ( "context" "fmt" - "os" - "path/filepath" "strconv" "strings" "sync" @@ -173,7 +171,6 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, 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 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, 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 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, } // 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 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, // 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 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, 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 14c575d422..3d5db7a02d 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -94,6 +94,13 @@ type snapshot struct { 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 @@ func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.VersionedF 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 @@ func (s *snapshot) AwaitInitialized(ctx context.Context) { } // 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 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Ve 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 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Ve } // 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 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Ve 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 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Ve 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 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Ve } // 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 @@ copyIDs: } // 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 @@ func (s *snapshot) shouldInvalidateMetadata(ctx context.Context, newSnapshot *sn 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 @@ func (s *snapshot) buildBuiltinPackage(ctx context.Context, goFiles []string) er 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 @@ func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, err 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 @@ func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, err } // 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 @@ func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, err } 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 9412d88f5d..d2cf068c5f 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -67,16 +67,6 @@ type View struct { // 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 @@ func (v *View) Snapshot(ctx context.Context) (source.Snapshot, func()) { 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 @@ func (v *View) initialize(ctx context.Context, s *snapshot, firstAttempt bool) { 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) cancelBackground() { } 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 @@ func defaultCheckPathCase(path string) error { 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 @@ func (v *View) setBuildConfiguration() (isValid bool) { 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 f8f6bdd833..d534eb0f21 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -400,6 +400,10 @@ type FileIdentity struct { 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