1
0
mirror of https://github.com/golang/go synced 2024-11-17 20:04:47 -07:00

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>
This commit is contained in:
Rebecca Stambler 2020-09-15 02:03:17 -04:00
parent e94ab72881
commit 78fed78f7d
9 changed files with 399 additions and 167 deletions

View File

@ -74,7 +74,7 @@ func main() {
// Reproduce golang/go#40269 by deleting and recreating main.go. // Reproduce golang/go#40269 by deleting and recreating main.go.
t.Run("delete main.go", func(t *testing.T) { 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) { withOptions(WithProxyFiles(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
goModContent := env.ReadWorkspaceFile("go.mod") goModContent := env.ReadWorkspaceFile("go.mod")

View File

@ -185,7 +185,7 @@ func _() {
} }
` `
runner.Run(t, missing, func(t *testing.T, env *Env) { 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.Await(
env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), 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) { 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 = ` const files = `
-- foo/blah/blah.go -- -- foo/blah/blah.go --

View File

@ -6,6 +6,7 @@ package regtest
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
"golang.org/x/tools/internal/lsp" "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)
}
})
}

View File

@ -177,8 +177,7 @@ func checkPackageKey(ctx context.Context, id packageID, pghs []*parseGoHandle, c
b.WriteString(string(dep)) b.WriteString(string(dep))
} }
for _, cgf := range pghs { for _, cgf := range pghs {
b.WriteString(string(cgf.file.URI())) b.WriteString(cgf.file.FileIdentity().String())
b.WriteString(cgf.file.FileIdentity().Hash)
} }
return packageHandleKey(hashContents(b.Bytes())) return packageHandleKey(hashContents(b.Bytes()))
} }

View File

@ -199,13 +199,18 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error {
// packages.Loads that occur from within the workspace module. // packages.Loads that occur from within the workspace module.
func (s *snapshot) tempWorkspaceModule(ctx context.Context) (_ span.URI, cleanup func(), err error) { func (s *snapshot) tempWorkspaceModule(ctx context.Context) (_ span.URI, cleanup func(), err error) {
cleanup = func() {} cleanup = func() {}
if len(s.view.modules) == 0 { if len(s.modules) == 0 {
return "", cleanup, nil return "", cleanup, nil
} }
if s.view.workspaceModule == nil { wsModuleHandle, err := s.getWorkspaceModuleHandle(ctx)
return "", cleanup, nil 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 { if err != nil {
return "", cleanup, err return "", cleanup, err
} }

View File

@ -7,8 +7,6 @@ package cache
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -173,7 +171,6 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
name: name, name: name,
folder: folder, folder: folder,
root: folder, root: folder,
modules: make(map[span.URI]*moduleRoot),
filesByURI: make(map[span.URI]*fileBase), filesByURI: make(map[span.URI]*fileBase),
filesByBase: make(map[string][]*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), modTidyHandles: make(map[span.URI]*modTidyHandle),
modUpgradeHandles: make(map[span.URI]*modUpgradeHandle), modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
modWhyHandles: make(map[span.URI]*modWhyHandle), modWhyHandles: make(map[span.URI]*modWhyHandle),
modules: make(map[span.URI]*moduleRoot),
} }
if v.session.cache.options != nil { 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. // 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 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. // check if the view has a valid build configuration.
v.setBuildConfiguration() v.setBuildConfiguration()
// Build the workspace module, if needed. // Decide if we should use the workspace module.
if options.ExperimentalWorkspaceModule { if v.determineWorkspaceModuleLocked() {
if err := v.buildWorkspaceModule(ctx); err != nil { v.workspaceMode |= usesWorkspaceModule | moduleMode
return nil, nil, func() {}, err
}
} }
// We have v.goEnv now. // We have v.goEnv now.
@ -242,94 +238,12 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
snapshot := v.snapshot snapshot := v.snapshot
release := snapshot.generation.Acquire(initCtx) release := snapshot.generation.Acquire(initCtx)
go func() { go func() {
v.initialize(initCtx, snapshot, true) snapshot.initialize(initCtx, true)
release() release()
}() }()
return v, snapshot, snapshot.generation.Acquire(ctx), nil 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. // View returns the view by name.
func (s *Session) View(name string) source.View { func (s *Session) View(name string) source.View {
s.viewMu.Lock() s.viewMu.Lock()

View File

@ -94,6 +94,13 @@ type snapshot struct {
modTidyHandles map[span.URI]*modTidyHandle modTidyHandles map[span.URI]*modTidyHandle
modUpgradeHandles map[span.URI]*modUpgradeHandle modUpgradeHandles map[span.URI]*modUpgradeHandle
modWhyHandles map[span.URI]*modWhyHandle 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 { type packageKey struct {
@ -673,12 +680,15 @@ func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.VersionedF
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() 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 { if fh, ok := s.files[f.URI()]; ok {
return fh, nil 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 { if err != nil {
return nil, err 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 // 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. // 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. // 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)) newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1))
result := &snapshot{ result := &snapshot{
id: s.id + 1, id: s.id + 1,
generation: newGen, generation: newGen,
view: s.view, view: s.view,
builtin: s.builtin, builtin: s.builtin,
ids: make(map[span.URI][]packageID), ids: make(map[span.URI][]packageID),
importedBy: make(map[packageID][]packageID), importedBy: make(map[packageID][]packageID),
metadata: make(map[packageID]*metadata), metadata: make(map[packageID]*metadata),
packages: make(map[packageKey]*packageHandle), packages: make(map[packageKey]*packageHandle),
actions: make(map[actionKey]*actionHandle), actions: make(map[actionKey]*actionHandle),
files: make(map[span.URI]source.VersionedFileHandle), files: make(map[span.URI]source.VersionedFileHandle),
goFiles: make(map[parseKey]*parseGoHandle), goFiles: make(map[parseKey]*parseGoHandle),
workspaceDirectories: make(map[span.URI]struct{}), workspaceDirectories: make(map[span.URI]struct{}),
workspacePackages: make(map[packageID]packagePath), workspacePackages: make(map[packageID]packagePath),
unloadableFiles: make(map[span.URI]struct{}), unloadableFiles: make(map[span.URI]struct{}),
parseModHandles: make(map[span.URI]*parseModHandle), parseModHandles: make(map[span.URI]*parseModHandle),
modTidyHandles: make(map[span.URI]*modTidyHandle), modTidyHandles: make(map[span.URI]*modTidyHandle),
modUpgradeHandles: make(map[span.URI]*modUpgradeHandle), modUpgradeHandles: make(map[span.URI]*modUpgradeHandle),
modWhyHandles: make(map[span.URI]*modWhyHandle), modWhyHandles: make(map[span.URI]*modWhyHandle),
modules: make(map[span.URI]*moduleRoot),
workspaceModuleHandle: s.workspaceModuleHandle,
} }
if s.builtin != nil { 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. // Copy all of the modHandles.
for k, v := range s.parseModHandles { for k, v := range s.parseModHandles {
newGen.Inherit(v.handle)
result.parseModHandles[k] = v result.parseModHandles[k] = v
} }
// Copy all of the workspace directories. They may be reset later. // 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 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. // transitiveIDs keeps track of transitive reverse dependencies.
// If an ID is present in the map, invalidate its types. // If an ID is present in the map, invalidate its types.
// If an ID's value is true, invalidate its metadata too. // 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) delete(result.modWhyHandles, k)
} }
} }
if currentFH.Kind() == source.Mod { currentExists := currentFH.URI() != ""
// If the view's go.mod file's contents have changed, invalidate the if currentExists {
// metadata for every known package in the snapshot. 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 { if invalidateMetadata {
for k := range s.packages { for k := range s.packages {
directIDs[k.id] = struct{}{} 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) delete(result.parseModHandles, withoutURI)
if currentFH.URI() == s.view.modURI { // Check if this is a newly created go.mod file. When a new module
// The go.mod's replace directives may have changed. We may // is created, we have to retry the initial workspace load.
// need to update our set of workspace directories. Use the new rootURI := span.URIFromPath(filepath.Dir(withoutURI.Filename()))
// snapshot, as it can be locked without causing issues. if currentMod {
result.workspaceDirectories = result.findWorkspaceDirectories(ctx, currentFH) 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, // If this is a file we don't yet know about,
// then we do not yet know what packages it should belong to. // 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. // 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) delete(result.files, withoutURI)
} else { } else {
result.files[withoutURI] = currentFH result.files[withoutURI] = currentFH
@ -1087,15 +1148,21 @@ copyIDs:
} }
// Inherit all of the go.mod-related handles. // Inherit all of the go.mod-related handles.
for _, v := range s.modTidyHandles { for _, v := range result.modTidyHandles {
newGen.Inherit(v.handle) newGen.Inherit(v.handle)
} }
for _, v := range s.modUpgradeHandles { for _, v := range result.modUpgradeHandles {
newGen.Inherit(v.handle) newGen.Inherit(v.handle)
} }
for _, v := range s.modWhyHandles { for _, v := range result.modWhyHandles {
newGen.Inherit(v.handle) 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, // Don't bother copying the importedBy graph,
// as it changes each time we update metadata. // 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() { if originalFH.FileIdentity() == currentFH.FileIdentity() {
return false 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 { 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 // 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 // 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 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 // buildWorkspaceModule generates a workspace module given the modules in the
// the workspace. // the workspace.
func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, error) { 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") file.AddModuleStmt("gopls-workspace")
paths := make(map[string]*moduleRoot) paths := make(map[string]*moduleRoot)
for _, mod := range s.view.modules { for _, mod := range s.modules {
fh, err := s.view.snapshot.GetFile(ctx, mod.modURI) fh, err := s.GetFile(ctx, mod.modURI)
if err != nil { if err != nil {
return nil, err 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 // Go back through all of the modules to handle any of their replace
// statements. // statements.
for _, module := range s.view.modules { for _, module := range s.modules {
fh, err := s.view.snapshot.GetFile(ctx, module.modURI) fh, err := s.GetFile(ctx, module.modURI)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pmf, err := s.view.snapshot.ParseMod(ctx, fh) pmf, err := s.ParseMod(ctx, fh)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1326,3 +1451,52 @@ func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, err
} }
return file, nil 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,
}
}

View File

@ -67,16 +67,6 @@ type View struct {
// is just the folder. If we are in module mode, this is the module root. // is just the folder. If we are in module mode, this is the module root.
root span.URI 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 guards imports-related state, particularly the ProcessEnv.
importsMu sync.Mutex 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) 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 { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case v.initializationSema <- struct{}{}: case s.view.initializationSema <- struct{}{}:
} }
defer func() { defer func() {
<-v.initializationSema <-s.view.initializationSema
}() }()
if v.initializeOnce == nil { if s.view.initializeOnce == nil {
return return
} }
v.initializeOnce.Do(func() { s.view.initializeOnce.Do(func() {
defer func() { defer func() {
v.initializeOnce = nil s.view.initializeOnce = nil
if firstAttempt { if firstAttempt {
close(v.initialized) close(s.view.initialized)
} }
}() }()
// If we have multiple modules, we need to load them by paths. // If we have multiple modules, we need to load them by paths.
var scopes []interface{} var scopes []interface{}
if len(v.modules) > 0 { if len(s.modules) > 0 {
// TODO(rstambler): Retry the initial workspace load for whichever // TODO(rstambler): Retry the initial workspace load for whichever
// modules we failed to load. // modules we failed to load.
for _, mod := range v.modules { for _, mod := range s.modules {
fh, err := s.GetFile(ctx, mod.modURI) fh, err := s.GetFile(ctx, mod.modURI)
if err != nil { if err != nil {
v.initializedErr = err s.view.initializedErr = err
continue continue
} }
parsed, err := s.ParseMod(ctx, fh) parsed, err := s.ParseMod(ctx, fh)
if err != nil { if err != nil {
v.initializedErr = err s.view.initializedErr = err
continue continue
} }
path := parsed.File.Module.Mod.Path path := parsed.File.Module.Mod.Path
@ -738,7 +728,7 @@ func (v *View) initialize(ctx context.Context, s *snapshot, firstAttempt bool) {
if err != nil { if err != nil {
event.Error(ctx, "initial workspace load failed", err) 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() { func (v *View) maybeReinitialize() {
v.reinitialize(false)
}
func (v *View) definitelyReinitialize() {
v.reinitialize(true)
}
func (v *View) reinitialize(force bool) {
v.initializationSema <- struct{}{} v.initializationSema <- struct{}{}
defer func() { defer func() {
<-v.initializationSema <-v.initializationSema
}() }()
if v.initializedErr == nil { if !force && v.initializedErr == nil {
return return
} }
var once sync.Once var once sync.Once
@ -856,6 +854,38 @@ func defaultCheckPathCase(path string) error {
return nil 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) { func (v *View) setBuildConfiguration() (isValid bool) {
defer func() { defer func() {
v.hasValidBuildConfiguration = isValid v.hasValidBuildConfiguration = isValid
@ -870,7 +900,7 @@ func (v *View) setBuildConfiguration() (isValid bool) {
if v.modURI != "" { if v.modURI != "" {
return true return true
} }
if len(v.modules) > 0 { if len(v.snapshot.modules) > 0 {
return true return true
} }
// The user may have a multiple directories in their GOPATH. // The user may have a multiple directories in their GOPATH.

View File

@ -400,6 +400,10 @@ type FileIdentity struct {
Kind FileKind 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. // FileKind describes the kind of the file in question.
// It can be one of Go, mod, or sum. // It can be one of Go, mod, or sum.
type FileKind int type FileKind int