Skip to content

Commit 45586dd

Browse files
committed
internal/lsp: nest the workspace root if there is only one module
If the user opens a parent directory of a single module, we can load their workspace without using some of the workarounds of experimental workspace mode, simply by narrowing the workspace to this nested subdirectory. Do this by extending findAllModules to support a limit on the number of modules and files to search. Updates golang/go#41558 Fixes golang/go#42108 Change-Id: Idba800a0deaeee5137d46f16a30b2012ff902a36 Reviewed-on: https://go-review.googlesource.com/c/tools/+/268877 Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]> Trust: Robert Findley <[email protected]>
1 parent 780cb80 commit 45586dd

File tree

9 files changed

+112
-46
lines changed

9 files changed

+112
-46
lines changed

gopls/doc/settings.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,12 @@ semantic tokens to the client.
249249

250250
Default: `false`.
251251
### **expandWorkspaceToModule** *bool*
252-
expandWorkspaceToModule instructs `gopls` to expand the scope of the workspace to include the
253-
modules containing the workspace folders. Set this to false to avoid loading
254-
your entire module. This is particularly useful for those working in a monorepo.
252+
expandWorkspaceToModule instructs `gopls` to adjust the scope of the
253+
workspace to find the best available module root. `gopls` first looks for
254+
a go.mod file in any parent directory of the workspace folder, expanding
255+
the scope to that directory if it exists. If no viable parent directory is
256+
found, gopls will check if there is exactly one child directory containing
257+
a go.mod file, narrowing the scope to that directory if it exists.
255258

256259

257260
Default: `true`.

gopls/internal/regtest/modfile_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func runModfileTest(t *testing.T, files, proxy string, f TestFunc) {
3838
withOptions(WithProxyFiles(proxy)).run(t, files, f)
3939
})
4040
t.Run("nested", func(t *testing.T) {
41-
withOptions(WithProxyFiles(proxy), NestWorkdir(), WithModes(Experimental)).run(t, files, f)
41+
withOptions(WithProxyFiles(proxy), NestWorkdir(), WithModes(Singleton|Experimental)).run(t, files, f)
4242
})
4343
}
4444

internal/lsp/cache/session.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,16 @@ func (s *Session) createView(ctx context.Context, name string, folder, tempWorks
173173
if err != nil {
174174
return nil, nil, func() {}, err
175175
}
176+
root := folder
177+
if options.ExpandWorkspaceToModule {
178+
root, err = findWorkspaceRoot(ctx, root, s, options.ExperimentalWorkspaceModule)
179+
if err != nil {
180+
return nil, nil, func() {}, err
181+
}
182+
}
176183

177184
// Build the gopls workspace, collecting active modules in the view.
178-
workspace, err := newWorkspace(ctx, ws.rootURI, s, options.ExperimentalWorkspaceModule)
185+
workspace, err := newWorkspace(ctx, root, s, options.ExperimentalWorkspaceModule)
179186
if err != nil {
180187
return nil, nil, func() {}, err
181188
}
@@ -198,6 +205,7 @@ func (s *Session) createView(ctx context.Context, name string, folder, tempWorks
198205
folder: folder,
199206
filesByURI: make(map[span.URI]*fileBase),
200207
filesByBase: make(map[string][]*fileBase),
208+
rootURI: root,
201209
workspaceInformation: *ws,
202210
tempWorkspace: tempWorkspace,
203211
}

internal/lsp/cache/snapshot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,7 @@ func (s *snapshot) buildBuiltinPackage(ctx context.Context, goFiles []string) er
15611561
// BuildGoplsMod generates a go.mod file for all modules in the workspace. It
15621562
// bypasses any existing gopls.mod.
15631563
func BuildGoplsMod(ctx context.Context, root span.URI, fs source.FileSource) (*modfile.File, error) {
1564-
allModules, err := findAllModules(ctx, root)
1564+
allModules, err := findModules(ctx, root, 0, 0)
15651565
if err != nil {
15661566
return nil, err
15671567
}

internal/lsp/cache/view.go

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ type View struct {
8585
// context is canceled.
8686
initializationSema chan struct{}
8787

88+
// rootURI is the rootURI directory of this view. If we are in GOPATH mode, this
89+
// is just the folder. If we are in module mode, this is the module rootURI.
90+
rootURI span.URI
91+
8892
// workspaceInformation tracks various details about this view's
8993
// environment variables, go version, and use of modules.
9094
workspaceInformation
@@ -112,10 +116,6 @@ type workspaceInformation struct {
112116
// goEnv is the `go env` output collected when a view is created.
113117
// It includes the values of the environment variables above.
114118
goEnv map[string]string
115-
116-
// rootURI is the rootURI directory of this view. If we are in GOPATH mode, this
117-
// is just the folder. If we are in module mode, this is the module rootURI.
118-
rootURI span.URI
119119
}
120120

121121
type environmentVariables struct {
@@ -316,7 +316,7 @@ func (s *snapshot) RunProcessEnvFunc(ctx context.Context, fn func(*imports.Optio
316316
}
317317

318318
func (v *View) contains(uri span.URI) bool {
319-
return strings.HasPrefix(string(uri), string(v.rootURI))
319+
return source.InDir(v.rootURI.Filename(), uri.Filename()) || source.InDir(v.folder.Filename(), uri.Filename())
320320
}
321321

322322
func (v *View) mapFile(uri span.URI, f *fileBase) {
@@ -673,28 +673,31 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI,
673673
tool, _ := exec.LookPath("gopackagesdriver")
674674
hasGopackagesDriver := gopackagesdriver != "off" && (gopackagesdriver != "" || tool != "")
675675

676-
root := folder
677-
if options.ExpandWorkspaceToModule {
678-
wsRoot, err := findWorkspaceRoot(ctx, root, s)
679-
if err != nil {
680-
return nil, err
681-
}
682-
if wsRoot != "" {
683-
root = wsRoot
684-
}
685-
}
686676
return &workspaceInformation{
687677
hasGopackagesDriver: hasGopackagesDriver,
688678
go111module: go111module,
689679
goversion: goversion,
690-
rootURI: root,
691680
environmentVariables: envVars,
692681
goEnv: env,
693682
}, nil
694683
}
695684

696-
func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSource) (span.URI, error) {
697-
for _, basename := range []string{"gopls.mod", "go.mod"} {
685+
// findWorkspaceRoot searches for the best workspace root according to the
686+
// following heuristics:
687+
// - First, look for a parent directory containing a gopls.mod file
688+
// (experimental only).
689+
// - Then, a parent directory containing a go.mod file.
690+
// - Then, a child directory containing a go.mod file, if there is exactly
691+
// one (non-experimental only).
692+
// Otherwise, it returns folder.
693+
// TODO (rFindley): move this to workspace.go
694+
// TODO (rFindley): simplify this once workspace modules are enabled by default.
695+
func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSource, experimental bool) (span.URI, error) {
696+
patterns := []string{"go.mod"}
697+
if experimental {
698+
patterns = []string{"gopls.mod", "go.mod"}
699+
}
700+
for _, basename := range patterns {
698701
dir, err := findRootPattern(ctx, folder, basename, fs)
699702
if err != nil {
700703
return "", errors.Errorf("finding %s: %w", basename, err)
@@ -703,7 +706,31 @@ func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSourc
703706
return dir, nil
704707
}
705708
}
706-
return "", nil
709+
710+
// The experimental workspace can handle nested modules at this point...
711+
if experimental {
712+
return folder, nil
713+
}
714+
715+
// ...else we should check if there's exactly one nested module.
716+
const filesToSearch = 10000
717+
all, err := findModules(ctx, folder, 2, filesToSearch)
718+
if err == errExhausted {
719+
// Fall-back behavior: if we don't find any modules after searching 10000
720+
// files, assume there are none.
721+
event.Log(ctx, fmt.Sprintf("stopped searching for modules after %d files", filesToSearch))
722+
return folder, nil
723+
}
724+
if err != nil {
725+
return "", err
726+
}
727+
if len(all) == 1 {
728+
// range to access first element.
729+
for uri := range all {
730+
return dirURI(uri), nil
731+
}
732+
}
733+
return folder, nil
707734
}
708735

709736
func findRootPattern(ctx context.Context, folder span.URI, basename string, fs source.FileSource) (span.URI, error) {

internal/lsp/cache/view_test.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ module b
6060
module bc
6161
-- d/gopls.mod --
6262
module d-goplsworkspace
63-
-- d/e/go.mod
63+
-- d/e/go.mod --
6464
module de
65+
-- f/g/go.mod --
66+
module fg
6567
`
6668
dir, err := fake.Tempdir(workspace)
6769
if err != nil {
@@ -71,26 +73,30 @@ module de
7173

7274
tests := []struct {
7375
folder, want string
76+
experimental bool
7477
}{
75-
// no module at root.
76-
{"", ""},
77-
{"a", "a"},
78-
{"a/x", "a"},
79-
{"b/c", "b/c"},
80-
{"d", "d"},
81-
{"d/e", "d"},
78+
{"", "", false}, // no module at root, and more than one nested module
79+
{"a", "a", false},
80+
{"a/x", "a", false},
81+
{"b/c", "b/c", false},
82+
{"d", "d/e", false},
83+
{"d", "d", true},
84+
{"d/e", "d/e", false},
85+
{"d/e", "d", true},
86+
{"f", "f/g", false},
87+
{"f", "f", true},
8288
}
8389

8490
for _, test := range tests {
8591
ctx := context.Background()
8692
rel := fake.RelativeTo(dir)
8793
folderURI := span.URIFromPath(rel.AbsPath(test.folder))
88-
got, err := findWorkspaceRoot(ctx, folderURI, osFileSource{})
94+
got, err := findWorkspaceRoot(ctx, folderURI, osFileSource{}, test.experimental)
8995
if err != nil {
9096
t.Fatal(err)
9197
}
92-
if rel.RelPath(got.Filename()) != test.want {
93-
t.Errorf("fileWorkspaceRoot(%q) = %q, want %q", test.folder, got, test.want)
98+
if gotf, wantf := filepath.Clean(got.Filename()), rel.AbsPath(test.want); gotf != wantf {
99+
t.Errorf("findWorkspaceRoot(%q, %t) = %q, want %q", test.folder, test.experimental, gotf, wantf)
94100
}
95101
}
96102
}

internal/lsp/cache/workspace.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, expe
101101
moduleSource: goplsModWorkspace,
102102
}, nil
103103
}
104-
modFiles, err := findAllModules(ctx, root)
104+
modFiles, err := findModules(ctx, root, 0, 0)
105105
if err != nil {
106106
return nil, err
107107
}
@@ -234,7 +234,7 @@ func (wm *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileC
234234
} else {
235235
// gopls.mod is deleted. search for modules again.
236236
moduleSource = fileSystemWorkspace
237-
modFiles, err = findAllModules(ctx, wm.root)
237+
modFiles, err = findModules(ctx, wm.root, 0, 0)
238238
// the modFile is no longer valid.
239239
if err != nil {
240240
event.Error(ctx, "finding file system modules", err)
@@ -372,13 +372,21 @@ func parseGoplsMod(root, uri span.URI, contents []byte) (*modfile.File, map[span
372372
return modFile, modFiles, nil
373373
}
374374

375-
// findAllModules recursively walks the root directory looking for go.mod
376-
// files, returning the set of modules it discovers.
375+
// errExhausted is returned by findModules if the file scan limit is reached.
376+
var errExhausted = errors.New("exhausted")
377+
378+
// findModules recursively walks the root directory looking for go.mod files,
379+
// returning the set of modules it discovers. If modLimit is non-zero,
380+
// searching stops once modLimit modules have been found. If fileLimit is
381+
// non-zero, searching stops once fileLimit files have been checked.
377382
// TODO(rfindley): consider overlays.
378-
func findAllModules(ctx context.Context, root span.URI) (map[span.URI]struct{}, error) {
383+
func findModules(ctx context.Context, root span.URI, modLimit, fileLimit int) (map[span.URI]struct{}, error) {
379384
// Walk the view's folder to find all modules in the view.
380385
modFiles := make(map[span.URI]struct{})
381-
return modFiles, filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error {
386+
searched := 0
387+
388+
errDone := errors.New("done")
389+
err := filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error {
382390
if err != nil {
383391
// Probably a permission error. Keep looking.
384392
return filepath.SkipDir
@@ -399,6 +407,17 @@ func findAllModules(ctx context.Context, root span.URI) (map[span.URI]struct{},
399407
if isGoMod(uri) {
400408
modFiles[uri] = struct{}{}
401409
}
410+
if modLimit > 0 && len(modFiles) >= modLimit {
411+
return errDone
412+
}
413+
searched++
414+
if fileLimit > 0 && searched >= fileLimit {
415+
return errExhausted
416+
}
402417
return nil
403418
})
419+
if err == errDone {
420+
return modFiles, nil
421+
}
422+
return modFiles, err
404423
}

0 commit comments

Comments
 (0)