diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index c8b12993db..2524861d35 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -205,4 +205,9 @@ modules containing the workspace folders. Set this to false to avoid loading your entire module. This is particularly useful for those working in a monorepo. Default: `true`. +### **experimentalWorkspaceModule** *bool* +experimentalWorkspaceModule opts a user into the experimental support +for multi-module workspaces. + +Default: `false`. diff --git a/gopls/internal/regtest/env.go b/gopls/internal/regtest/env.go index 262851faf8..6bcfd5c4e6 100644 --- a/gopls/internal/regtest/env.go +++ b/gopls/internal/regtest/env.go @@ -700,6 +700,39 @@ func DiagnosticAt(name string, line, col int) DiagnosticExpectation { } } +// NoDiagnosticAtRegexp expects that there is no diagnostic entry at the start +// position matching the regexp search string re in the buffer specified by +// name. Note that this currently ignores the end position. +// This should only be used in combination with OnceMet for a given condition, +// otherwise it may always succeed. +func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation { + e.T.Helper() + pos := e.RegexpSearch(name, re) + expectation := NoDiagnosticAt(name, pos.Line, pos.Column) + expectation.description += fmt.Sprintf(" (location of %q)", re) + return expectation +} + +// NoDiagnosticAt asserts that there is no diagnostic entry at the position +// specified by line and col, for the workdir-relative path name. +// This should only be used in combination with OnceMet for a given condition, +// otherwise it may always succeed. +func NoDiagnosticAt(name string, line, col int) DiagnosticExpectation { + isMet := func(diags *protocol.PublishDiagnosticsParams) bool { + for _, d := range diags.Diagnostics { + if d.Range.Start.Line == float64(line) && d.Range.Start.Character == float64(col) { + return false + } + } + return true + } + return DiagnosticExpectation{ + isMet: isMet, + description: fmt.Sprintf("no diagnostic at {line:%d, column:%d}", line, col), + path: name, + } +} + // DiagnosticsFor returns the current diagnostics for the file. It is useful // after waiting on AnyDiagnosticAtCurrentVersion, when the desired diagnostic // is not simply described by DiagnosticAt. diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace_test.go index 4bb31e364e..7b997aee0a 100644 --- a/gopls/internal/regtest/workspace_test.go +++ b/gopls/internal/regtest/workspace_test.go @@ -9,6 +9,7 @@ import ( "testing" "golang.org/x/tools/internal/lsp" + "golang.org/x/tools/internal/lsp/fake" ) const workspaceProxy = ` @@ -156,3 +157,57 @@ replace random.org => %s ) }) } + +const workspaceModuleProxy = ` +-- b.com@v1.2.3/go.mod -- +module b.com + +go 1.12 +-- b.com@v1.2.3/b/b.go -- +package b + +func Hello() {} +` + +func TestAutomaticWorkspaceModule_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), + WithEditorConfig(fake.EditorConfig{ExperimentalWorkspaceModule: true}), + ).run(t, multiModule, func(t *testing.T, env *Env) { + env.Await( + CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1), + ) + env.Await( + env.DiagnosticAtRegexp("moda/a/a.go", "x"), + env.DiagnosticAtRegexp("modb/b/b.go", "x"), + env.NoDiagnosticAtRegexp("moda/a/a.go", `"b.com/b"`), + ) + }) +} diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 4b93bbad8a..52ff7eea4a 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -8,6 +8,9 @@ import ( "context" "fmt" "go/types" + "io/ioutil" + "os" + "path/filepath" "sort" "strings" @@ -62,6 +65,8 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error { q = "./..." } query = append(query, q) + case moduleLoadScope: + query = append(query, fmt.Sprintf("%s/...", scope)) case viewLoadScope: // If we are outside of GOPATH, a module, or some other known // build system, don't load subdirectories. @@ -84,8 +89,20 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error { defer done() cfg := s.config(ctx) + cleanup := func() {} - if s.view.tmpMod { + switch { + case s.view.workspaceMode&workspaceModule != 0: + var ( + tmpDir span.URI + err error + ) + tmpDir, cleanup, err = s.tempWorkspaceModule(ctx) + if err != nil { + return err + } + cfg.Dir = tmpDir.Filename() + case s.view.workspaceMode&tempModfile != 0: modFH, err := s.GetFile(ctx, s.view.modURI) if err != nil { return err @@ -129,7 +146,6 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error { } return errors.Errorf("%v: %w", err, source.PackagesLoadError) } - for _, pkg := range pkgs { if !containsDir || s.view.Options().VerboseOutput { event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.PackagePath.Of(pkg.PkgPath), tag.Files.Of(pkg.CompiledGoFiles)) @@ -165,6 +181,37 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error { return nil } +// tempWorkspaceModule creates a temporary directory for use with +// 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 { + return "", cleanup, nil + } + if s.view.workspaceModule == nil { + return "", cleanup, nil + } + content, err := s.view.workspaceModule.Format() + if err != nil { + return "", cleanup, err + } + // Create a temporary working directory for the go command that contains + // the workspace module file. + name, err := ioutil.TempDir("", "gopls-mod") + if err != nil { + return "", cleanup, err + } + cleanup = func() { + os.RemoveAll(name) + } + filename := filepath.Join(name, "go.mod") + if err := ioutil.WriteFile(filename, content, 0644); err != nil { + cleanup() + return "", cleanup, err + } + return span.URIFromPath(filepath.Dir(filename)), cleanup, nil +} + func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) (*metadata, error) { id := packageID(pkg.ID) if _, ok := seen[id]; ok { diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index e62d7fdcff..c689e2cd21 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -196,6 +196,9 @@ func (mwh *modWhyHandle) why(ctx context.Context, snapshot *snapshot) (map[strin } func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) { + if fh.Kind() != source.Mod { + return nil, fmt.Errorf("%s is not a go.mod file", fh.URI()) + } if err := s.awaitLoaded(ctx); err != nil { return nil, err } @@ -285,6 +288,9 @@ type moduleUpgrade struct { } func (s *snapshot) ModUpgrade(ctx context.Context, fh source.FileHandle) (map[string]string, error) { + if fh.Kind() != source.Mod { + return nil, fmt.Errorf("%s is not a go.mod file", fh.URI()) + } if err := s.awaitLoaded(ctx); err != nil { return nil, err } @@ -318,7 +324,7 @@ func (s *snapshot) ModUpgrade(ctx context.Context, fh source.FileHandle) (map[st // Run "go list -mod readonly -u -m all" to be able to see which deps can be // upgraded without modifying mod file. args := []string{"-u", "-m", "-json", "all"} - if !snapshot.view.tmpMod || containsVendor(fh.URI()) { + if s.view.workspaceMode&tempModfile == 0 || containsVendor(fh.URI()) { // Use -mod=readonly if the module contains a vendor directory // (see golang/go#38711). args = append([]string{"-mod", "readonly"}, args...) diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index e2f010d00c..646a90f78f 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -52,7 +52,10 @@ func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source } func (s *snapshot) ModTidy(ctx context.Context, fh source.FileHandle) (*source.TidiedModule, error) { - if !s.view.tmpMod { + if fh.Kind() != source.Mod { + return nil, fmt.Errorf("%s is not a go.mod file", fh.URI()) + } + if s.view.workspaceMode&tempModfile == 0 { return nil, source.ErrTmpModfileUnsupported } if handle := s.getModTidyHandle(fh.URI()); handle != nil { @@ -75,7 +78,7 @@ func (s *snapshot) ModTidy(ctx context.Context, fh source.FileHandle) (*source.T cfg := s.configWithDir(ctx, filepath.Dir(fh.URI().Filename())) key := modTidyKey{ sessionID: s.view.session.id, - view: s.view.root.Filename(), + view: s.view.folder.Filename(), imports: importHash, unsavedOverlays: overlayHash, gomod: fh.FileIdentity(), @@ -103,7 +106,7 @@ func (s *snapshot) ModTidy(ctx context.Context, fh source.FileHandle) (*source.T err: err, } } - tmpURI, runner, inv, cleanup, err := snapshot.goCommandInvocation(ctx, true, "mod", []string{"tidy"}) + tmpURI, runner, inv, cleanup, err := snapshot.goCommandInvocation(ctx, cfg, true, "mod", []string{"tidy"}) if err != nil { return &modTidyData{err: err} } diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go index 66321fbda4..b29eb27b33 100644 --- a/internal/lsp/cache/pkg.go +++ b/internal/lsp/cache/pkg.go @@ -41,9 +41,10 @@ type ( // Declare explicit types for files and directories to distinguish between the two. type ( - fileURI span.URI - directoryURI span.URI - viewLoadScope span.URI + fileURI span.URI + directoryURI span.URI + moduleLoadScope string + viewLoadScope span.URI ) func (p *pkg) ID() string { diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index a55c7705d7..a0aa93f895 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -7,6 +7,8 @@ package cache import ( "context" "fmt" + "os" + "path/filepath" "strconv" "strings" "sync" @@ -171,6 +173,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, name: name, folder: folder, root: folder, + modules: make(map[span.URI]*module), filesByURI: make(map[span.URI]*fileBase), filesByBase: make(map[string][]*fileBase), } @@ -196,11 +199,21 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, if v.session.cache.options != nil { v.session.cache.options(&v.options) } + // Set the module-specific information. if err := v.setBuildInformation(ctx, folder, options); err != nil { return nil, nil, func() {}, err } + // Find all of the modules in the workspace. + if err := v.findAndBuildWorkspaceModule(ctx, options); err != nil { + return nil, nil, func() {}, err + } + + // Now that we have set all required fields, + // check if the view has a valid build configuration. + v.setBuildConfiguration() + // We have v.goEnv now. v.processEnv = &imports.ProcessEnv{ GocmdRunner: s.gocmdRunner, @@ -227,6 +240,66 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, return v, v.snapshot, v.snapshot.generation.Acquire(ctx), nil } +// findAndBuildWorkspaceModule 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) findAndBuildWorkspaceModule(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 + } + + v.workspaceMode |= workspaceModule + + // Walk the view's folder to find all modules in the view. + root := v.root.Filename() + if err := 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] = &module{ + rootURI: rootURI, + modURI: modURI, + sumURI: span.URIFromPath(sumFilename(modURI)), + } + return nil + }); err != nil { + return err + } + // 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 ddb3626ba2..b72b62c619 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -19,6 +19,7 @@ import ( "strings" "sync" + "golang.org/x/mod/modfile" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" @@ -168,7 +169,8 @@ func (s *snapshot) configWithDir(ctx context.Context, dir string) *packages.Conf } func (s *snapshot) RunGoCommandDirect(ctx context.Context, verb string, args []string) error { - _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, false, verb, args) + cfg := s.config(ctx) + _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, false, verb, args) if err != nil { return err } @@ -179,7 +181,8 @@ func (s *snapshot) RunGoCommandDirect(ctx context.Context, verb string, args []s } func (s *snapshot) RunGoCommand(ctx context.Context, verb string, args []string) (*bytes.Buffer, error) { - _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, true, verb, args) + cfg := s.config(ctx) + _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, true, verb, args) if err != nil { return nil, err } @@ -189,7 +192,8 @@ func (s *snapshot) RunGoCommand(ctx context.Context, verb string, args []string) } func (s *snapshot) RunGoCommandPiped(ctx context.Context, verb string, args []string, stdout, stderr io.Writer) error { - _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, true, verb, args) + cfg := s.config(ctx) + _, runner, inv, cleanup, err := s.goCommandInvocation(ctx, cfg, true, verb, args) if err != nil { return err } @@ -198,10 +202,9 @@ func (s *snapshot) RunGoCommandPiped(ctx context.Context, verb string, args []st } // Assumes that modURI is only provided when the -modfile flag is enabled. -func (s *snapshot) goCommandInvocation(ctx context.Context, allowTempModfile bool, verb string, args []string) (tmpURI span.URI, runner *gocommand.Runner, inv *gocommand.Invocation, cleanup func(), err error) { +func (s *snapshot) goCommandInvocation(ctx context.Context, cfg *packages.Config, allowTempModfile bool, verb string, args []string) (tmpURI span.URI, runner *gocommand.Runner, inv *gocommand.Invocation, cleanup func(), err error) { cleanup = func() {} // fallback - cfg := s.config(ctx) - if allowTempModfile && s.view.tmpMod { + if allowTempModfile && s.view.workspaceMode&tempModfile != 0 { modFH, err := s.GetFile(ctx, s.view.modURI) if err != nil { return "", nil, nil, cleanup, err @@ -1229,3 +1232,66 @@ func (s *snapshot) buildBuiltinPackage(ctx context.Context, goFiles []string) er s.builtin = &builtinPackageHandle{handle: h} return nil } + +const workspaceModuleVersion = "v0.0.0-00010101000000-000000000000" + +// buildWorkspaceModule generates a workspace module given the modules in the +// the workspace. +func (s *snapshot) buildWorkspaceModule(ctx context.Context) (*modfile.File, error) { + file := &modfile.File{} + file.AddModuleStmt("gopls-workspace") + + paths := make(map[string]*module) + for _, mod := range s.view.modules { + fh, err := s.view.snapshot.GetFile(ctx, mod.modURI) + if err != nil { + return nil, err + } + parsed, err := s.ParseMod(ctx, fh) + if err != nil { + return nil, err + } + path := parsed.File.Module.Mod.Path + paths[path] = mod + file.AddNewRequire(path, workspaceModuleVersion, false) + if err := file.AddReplace(path, "", mod.rootURI.Filename(), ""); err != nil { + return nil, 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) + if err != nil { + return nil, err + } + pmf, err := s.view.snapshot.ParseMod(ctx, fh) + if err != nil { + return nil, err + } + // If any of the workspace modules have replace directives, they need + // to be reflected in the workspace module. + for _, rep := range pmf.File.Replace { + // Don't replace any modules that are in our workspace--we should + // always use the version in the workspace. + if _, ok := paths[rep.Old.Path]; ok { + continue + } + newPath := rep.New.Path + newVersion := rep.New.Version + // If a replace points to a module in the workspace, make sure we + // direct it to version of the module in the workspace. + if mod, ok := paths[rep.New.Path]; ok { + newPath = mod.rootURI.Filename() + newVersion = "" + } else if rep.New.Version == "" && !filepath.IsAbs(rep.New.Path) { + // Make any relative paths absolute. + newPath = filepath.Join(module.rootURI.Filename(), rep.New.Path) + } + if err := file.AddReplace(rep.Old.Path, rep.Old.Version, newPath, newVersion); err != nil { + return nil, err + } + } + } + return file, nil +} diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 964dba17ba..40baa94006 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -20,6 +20,7 @@ import ( "sync" "time" + "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/gocommand" @@ -64,6 +65,16 @@ 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]*module + + // 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 @@ -122,9 +133,9 @@ type View struct { // The real go.mod and go.sum files that are attributed to a view. modURI, sumURI span.URI - // True if this view runs go commands using temporary mod files. - // Only possible with Go versions 1.14 and above. - tmpMod bool + // workspaceMode describes the way in which the view's workspace should be + // loaded. + workspaceMode workspaceMode // hasGopackagesDriver is true if the user has a value set for the // GOPACKAGESDRIVER environment variable or a gopackagesdriver binary on @@ -139,6 +150,19 @@ type View struct { goEnv map[string]string } +type workspaceMode int + +const ( + standard workspaceMode = 1 << iota + + // tempModfile indicates whether or not the -modfile flag should be used. + tempModfile + + // workspaceModule indicates support for the experimental workspace module + // feature. + workspaceModule +) + type builtinPackageHandle struct { handle *memoize.Handle } @@ -147,6 +171,10 @@ type builtinPackageData struct { parsed *source.BuiltinPackage err error } +type module struct { + rootURI span.URI + modURI, sumURI span.URI +} // fileBase holds the common functionality for all files. // It is intended to be embedded in the file implementations @@ -436,7 +464,7 @@ func (v *View) populateProcessEnv(ctx context.Context, modFH, sumFH source.FileH v.optionsMu.Unlock() // Add -modfile to the build flags, if we are using it. - if v.tmpMod && modFH != nil { + if v.workspaceMode&tempModfile != 0 && modFH != nil { var tmpURI span.URI tmpURI, cleanup, err = tempModFile(modFH, sumFH) if err != nil { @@ -643,7 +671,29 @@ func (v *View) initialize(ctx context.Context, s *snapshot, firstAttempt bool) { } }() - err := s.load(ctx, viewLoadScope("LOAD_VIEW"), packagePath("builtin")) + // If we have multiple modules, we need to load them by paths. + var scopes []interface{} + if len(v.modules) > 0 { + // TODO(rstambler): Retry the initial workspace load for whichever + // modules we failed to load. + for _, mod := range v.modules { + fh, err := s.GetFile(ctx, mod.modURI) + if err != nil { + v.initializedErr = err + continue + } + parsed, err := s.ParseMod(ctx, fh) + if err != nil { + v.initializedErr = err + continue + } + path := parsed.File.Module.Mod.Path + scopes = append(scopes, moduleLoadScope(path)) + } + } else { + scopes = append(scopes, viewLoadScope("LOAD_VIEW")) + } + err := s.load(ctx, append(scopes, packagePath("builtin"))...) if ctx.Err() != nil { return } @@ -738,18 +788,15 @@ func (v *View) setBuildInformation(ctx context.Context, folder span.URI, options v.root = span.URIFromPath(filepath.Dir(v.modURI.Filename())) } - // Now that we have set all required fields, - // check if the view has a valid build configuration. - v.setBuildConfiguration() - // The user has disabled the use of the -modfile flag or has no go.mod file. if !options.TempModfile || v.modURI == "" { return nil } + v.workspaceMode = standard if modfileFlag, err := v.modfileFlagExists(ctx, v.Options().Env); err != nil { return err } else if modfileFlag { - v.tmpMod = true + v.workspaceMode |= tempModfile } return nil } @@ -770,10 +817,14 @@ func (v *View) setBuildConfiguration() (isValid bool) { if v.hasGopackagesDriver { return true } - // Check if the user is working within a module. + // Check if the user is working within a module or if we have found + // multiple modules in the workspace. if v.modURI != "" { return true } + if len(v.modules) > 0 { + return true + } // The user may have a multiple directories in their GOPATH. // Check if the workspace is within any of them. for _, gp := range filepath.SplitList(v.gopath) { diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go index d9b90f8572..7fb3a5c33c 100644 --- a/internal/lsp/code_action.go +++ b/internal/lsp/code_action.go @@ -51,6 +51,10 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara var codeActions []protocol.CodeAction switch fh.Kind() { case source.Mod: + // TODO: Support code actions for views with multiple modules. + if snapshot.View().ModFile() == "" { + return nil, nil + } if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 { modQuickFixes, err := moduleQuickFixes(ctx, snapshot, diagnostics) if err == source.ErrTmpModfileUnsupported { diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index be0733928c..d5cd277904 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -84,6 +84,10 @@ type EditorConfig struct { // EnableStaticcheck enables staticcheck analyzers. EnableStaticcheck bool + + // ExperimentalWorkspaceModule enables the experimental support for + // multi-module workspaces. + ExperimentalWorkspaceModule bool } // NewEditor Creates a new Editor. @@ -192,6 +196,9 @@ func (e *Editor) configuration() map[string]interface{} { if e.Config.EnableStaticcheck { config["staticcheck"] = true } + if e.Config.ExperimentalWorkspaceModule { + config["experimentalWorkspaceModule"] = true + } return config } diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index aeba4603ef..a1a415cd56 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -338,6 +338,10 @@ type ExperimentalOptions struct { // modules containing the workspace folders. Set this to false to avoid loading // your entire module. This is particularly useful for those working in a monorepo. ExpandWorkspaceToModule bool + + // ExperimentalWorkspaceModule opts a user into the experimental support + // for multi-module workspaces. + ExperimentalWorkspaceModule bool } // DebuggingOptions should not affect the logical execution of Gopls, but may @@ -647,6 +651,9 @@ func (o *Options) set(name string, value interface{}) OptionResult { case "expandWorkspaceToModule": result.setBool(&o.ExpandWorkspaceToModule) + case "experimentalWorkspaceModule": + result.setBool(&o.ExperimentalWorkspaceModule) + // Replaced settings. case "experimentalDisabledAnalyses": result.State = OptionDeprecated diff --git a/internal/lsp/source/options_json.go b/internal/lsp/source/options_json.go index 7007fb2921..f6c7e06bf8 100755 --- a/internal/lsp/source/options_json.go +++ b/internal/lsp/source/options_json.go @@ -2,4 +2,4 @@ package source -const OptionsJson = "{\"Debugging\":[{\"Name\":\"verboseOutput\",\"Type\":\"bool\",\"Doc\":\"verboseOutput enables additional debug logging.\\n\",\"Default\":\"false\"},{\"Name\":\"completionBudget\",\"Type\":\"time.Duration\",\"Doc\":\"completionBudget is the soft latency goal for completion requests. Most\\nrequests finish in a couple milliseconds, but in some cases deep\\ncompletions can take much longer. As we use up our budget we\\ndynamically reduce the search scope to ensure we return timely\\nresults. Zero means unlimited.\\n\",\"Default\":\"\\\"100ms\\\"\"},{\"Name\":\"literalCompletions\",\"Type\":\"bool\",\"Doc\":\"literalCompletions controls whether literal candidates such as\\n\\\"\\u0026someStruct{}\\\" are offered. Tests disable this flag to simplify\\ntheir expected values.\\n\",\"Default\":\"true\"}],\"Experimental\":[{\"Name\":\"analyses\",\"Type\":\"map[string]bool\",\"Doc\":\"analyses specify analyses that the user would like to enable or disable.\\nA map of the names of analysis passes that should be enabled/disabled.\\nA full list of analyzers that gopls uses can be found [here](analyzers.md)\\n\\nExample Usage:\\n```json5\\n...\\n\\\"analyses\\\": {\\n \\\"unreachable\\\": false, // Disable the unreachable analyzer.\\n \\\"unusedparams\\\": true // Enable the unusedparams analyzer.\\n}\\n...\\n```\\n\",\"Default\":\"null\"},{\"Name\":\"codelens\",\"Type\":\"map[string]bool\",\"Doc\":\"overrides the enabled/disabled state of various code lenses. Currently, we\\nsupport several code lenses:\\n\\n* `generate`: run `go generate` as specified by a `//go:generate` directive.\\n* `upgrade_dependency`: upgrade a dependency listed in a `go.mod` file.\\n* `test`: run `go test -run` for a test func.\\n* `gc_details`: Show the gc compiler's choices for inline analysis and escaping.\\n\\nExample Usage:\\n```json5\\n\\\"gopls\\\": {\\n...\\n \\\"codelens\\\": {\\n \\\"generate\\\": false, // Don't run `go generate`.\\n \\\"gc_details\\\": true // Show a code lens toggling the display of gc's choices.\\n }\\n...\\n}\\n```\\n\",\"Default\":\"{\\\"gc_details\\\":false,\\\"generate\\\":true,\\\"regenerate_cgo\\\":true,\\\"tidy\\\":true,\\\"upgrade_dependency\\\":true,\\\"vendor\\\":true}\"},{\"Name\":\"completionDocumentation\",\"Type\":\"bool\",\"Doc\":\"completionDocumentation enables documentation with completion results.\\n\",\"Default\":\"true\"},{\"Name\":\"completeUnimported\",\"Type\":\"bool\",\"Doc\":\"completeUnimported enables completion for packages that you do not currently import.\\n\",\"Default\":\"true\"},{\"Name\":\"deepCompletion\",\"Type\":\"bool\",\"Doc\":\"deepCompletion If true, this turns on the ability to return completions from deep inside relevant entities, rather than just the locally accessible ones.\\n\\nConsider this example:\\n\\n```go\\npackage main\\n\\nimport \\\"fmt\\\"\\n\\ntype wrapString struct {\\n str string\\n}\\n\\nfunc main() {\\n x := wrapString{\\\"hello world\\\"}\\n fmt.Printf(\\u003c\\u003e)\\n}\\n```\\n\\nAt the location of the `\\u003c\\u003e` in this program, deep completion would suggest the result `x.str`.\\n\",\"Default\":\"true\"},{\"Name\":\"matcher\",\"Type\":\"golang.org/x/tools/internal/lsp/source.Matcher\",\"Doc\":\"matcher sets the algorithm that is used when calculating completion candidates. Must be one of:\\n\\n* `\\\"fuzzy\\\"`\\n* `\\\"caseSensitive\\\"`\\n* `\\\"caseInsensitive\\\"`\\n\",\"Default\":\"\\\"Fuzzy\\\"\"},{\"Name\":\"annotations\",\"Type\":\"map[string]bool\",\"Doc\":\"annotations suppress various kinds of optimization diagnostics\\nthat would be reported by the gc_details command.\\n noNilcheck suppresses display of nilchecks.\\n noEscape suppresses escape choices.\\n noInline suppresses inlining choices.\\n noBounds suppresses bounds checking diagnositcs.\\n\",\"Default\":\"null\"},{\"Name\":\"staticcheck\",\"Type\":\"bool\",\"Doc\":\"staticcheck enables additional analyses from staticcheck.io.\\n\",\"Default\":\"false\"},{\"Name\":\"symbolMatcher\",\"Type\":\"golang.org/x/tools/internal/lsp/source.SymbolMatcher\",\"Doc\":\"symbolMatcher sets the algorithm that is used when finding workspace symbols. Must be one of:\\n\\n* `\\\"fuzzy\\\"`\\n* `\\\"caseSensitive\\\"`\\n* `\\\"caseInsensitive\\\"`\\n\",\"Default\":\"\\\"SymbolFuzzy\\\"\"},{\"Name\":\"symbolStyle\",\"Type\":\"golang.org/x/tools/internal/lsp/source.SymbolStyle\",\"Doc\":\"symbolStyle specifies what style of symbols to return in symbol requests. Must be one of:\\n\\n* `\\\"full\\\"`\\n* `\\\"dynamic\\\"`\\n* `\\\"package\\\"`\\n\",\"Default\":\"\\\"PackageQualifiedSymbols\\\"\"},{\"Name\":\"linksInHover\",\"Type\":\"bool\",\"Doc\":\"linksInHover toggles the presence of links to documentation in hover.\\n\",\"Default\":\"true\"},{\"Name\":\"tempModfile\",\"Type\":\"bool\",\"Doc\":\"tempModfile controls the use of the -modfile flag in Go 1.14.\\n\",\"Default\":\"true\"},{\"Name\":\"importShortcut\",\"Type\":\"golang.org/x/tools/internal/lsp/source.ImportShortcut\",\"Doc\":\"importShortcut specifies whether import statements should link to\\ndocumentation or go to definitions. Must be one of:\\n\\n* `\\\"both\\\"`\\n* `\\\"link\\\"`\\n* `\\\"definition\\\"`\\n\",\"Default\":\"\\\"Both\\\"\"},{\"Name\":\"verboseWorkDoneProgress\",\"Type\":\"bool\",\"Doc\":\"verboseWorkDoneProgress controls whether the LSP server should send\\nprogress reports for all work done outside the scope of an RPC.\\n\",\"Default\":\"false\"},{\"Name\":\"expandWorkspaceToModule\",\"Type\":\"bool\",\"Doc\":\"expandWorkspaceToModule instructs `gopls` to expand the scope of the workspace to include the\\nmodules containing the workspace folders. Set this to false to avoid loading\\nyour entire module. This is particularly useful for those working in a monorepo.\\n\",\"Default\":\"true\"}],\"User\":[{\"Name\":\"buildFlags\",\"Type\":\"[]string\",\"Doc\":\"buildFlags is the set of flags passed on to the build system when invoked.\\nIt is applied to queries like `go list`, which is used when discovering files.\\nThe most common use is to set `-tags`.\\n\",\"Default\":\"[]\"},{\"Name\":\"env\",\"Type\":\"[]string\",\"Doc\":\"env adds environment variables to external commands run by `gopls`, most notably `go list`.\\n\",\"Default\":\"[]\"},{\"Name\":\"hoverKind\",\"Type\":\"golang.org/x/tools/internal/lsp/source.HoverKind\",\"Doc\":\"hoverKind controls the information that appears in the hover text.\\nIt must be one of:\\n* `\\\"NoDocumentation\\\"`\\n* `\\\"SynopsisDocumentation\\\"`\\n* `\\\"FullDocumentation\\\"`\\n\\nAuthors of editor clients may wish to handle hover text differently, and so might use different settings. The options below are not intended for use by anyone other than the authors of editor plugins.\\n\\n* `\\\"SingleLine\\\"`\\n* `\\\"Structured\\\"`\\n\",\"Default\":\"\\\"FullDocumentation\\\"\"},{\"Name\":\"usePlaceholders\",\"Type\":\"bool\",\"Doc\":\"placeholders enables placeholders for function parameters or struct fields in completion responses.\\n\",\"Default\":\"false\"},{\"Name\":\"linkTarget\",\"Type\":\"string\",\"Doc\":\"linkTarget controls where documentation links go.\\nIt might be one of:\\n\\n* `\\\"godoc.org\\\"`\\n* `\\\"pkg.go.dev\\\"`\\n\\nIf company chooses to use its own `godoc.org`, its address can be used as well.\\n\",\"Default\":\"\\\"pkg.go.dev\\\"\"},{\"Name\":\"local\",\"Type\":\"string\",\"Doc\":\"local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.\\nIt should be the prefix of the import path whose imports should be grouped separately.\\n\",\"Default\":\"\\\"\\\"\"},{\"Name\":\"gofumpt\",\"Type\":\"bool\",\"Doc\":\"gofumpt indicates if we should run gofumpt formatting.\\n\",\"Default\":\"false\"}]}" +const OptionsJson = "{\"Debugging\":[{\"Name\":\"verboseOutput\",\"Type\":\"bool\",\"Doc\":\"verboseOutput enables additional debug logging.\\n\",\"Default\":\"false\"},{\"Name\":\"completionBudget\",\"Type\":\"time.Duration\",\"Doc\":\"completionBudget is the soft latency goal for completion requests. Most\\nrequests finish in a couple milliseconds, but in some cases deep\\ncompletions can take much longer. As we use up our budget we\\ndynamically reduce the search scope to ensure we return timely\\nresults. Zero means unlimited.\\n\",\"Default\":\"\\\"100ms\\\"\"},{\"Name\":\"literalCompletions\",\"Type\":\"bool\",\"Doc\":\"literalCompletions controls whether literal candidates such as\\n\\\"\\u0026someStruct{}\\\" are offered. Tests disable this flag to simplify\\ntheir expected values.\\n\",\"Default\":\"true\"}],\"Experimental\":[{\"Name\":\"analyses\",\"Type\":\"map[string]bool\",\"Doc\":\"analyses specify analyses that the user would like to enable or disable.\\nA map of the names of analysis passes that should be enabled/disabled.\\nA full list of analyzers that gopls uses can be found [here](analyzers.md)\\n\\nExample Usage:\\n```json5\\n...\\n\\\"analyses\\\": {\\n \\\"unreachable\\\": false, // Disable the unreachable analyzer.\\n \\\"unusedparams\\\": true // Enable the unusedparams analyzer.\\n}\\n...\\n```\\n\",\"Default\":\"null\"},{\"Name\":\"codelens\",\"Type\":\"map[string]bool\",\"Doc\":\"overrides the enabled/disabled state of various code lenses. Currently, we\\nsupport several code lenses:\\n\\n* `generate`: run `go generate` as specified by a `//go:generate` directive.\\n* `upgrade_dependency`: upgrade a dependency listed in a `go.mod` file.\\n* `test`: run `go test -run` for a test func.\\n* `gc_details`: Show the gc compiler's choices for inline analysis and escaping.\\n\\nExample Usage:\\n```json5\\n\\\"gopls\\\": {\\n...\\n \\\"codelens\\\": {\\n \\\"generate\\\": false, // Don't run `go generate`.\\n \\\"gc_details\\\": true // Show a code lens toggling the display of gc's choices.\\n }\\n...\\n}\\n```\\n\",\"Default\":\"{\\\"gc_details\\\":false,\\\"generate\\\":true,\\\"regenerate_cgo\\\":true,\\\"tidy\\\":true,\\\"upgrade_dependency\\\":true,\\\"vendor\\\":true}\"},{\"Name\":\"completionDocumentation\",\"Type\":\"bool\",\"Doc\":\"completionDocumentation enables documentation with completion results.\\n\",\"Default\":\"true\"},{\"Name\":\"completeUnimported\",\"Type\":\"bool\",\"Doc\":\"completeUnimported enables completion for packages that you do not currently import.\\n\",\"Default\":\"true\"},{\"Name\":\"deepCompletion\",\"Type\":\"bool\",\"Doc\":\"deepCompletion If true, this turns on the ability to return completions from deep inside relevant entities, rather than just the locally accessible ones.\\n\\nConsider this example:\\n\\n```go\\npackage main\\n\\nimport \\\"fmt\\\"\\n\\ntype wrapString struct {\\n str string\\n}\\n\\nfunc main() {\\n x := wrapString{\\\"hello world\\\"}\\n fmt.Printf(\\u003c\\u003e)\\n}\\n```\\n\\nAt the location of the `\\u003c\\u003e` in this program, deep completion would suggest the result `x.str`.\\n\",\"Default\":\"true\"},{\"Name\":\"matcher\",\"Type\":\"golang.org/x/tools/internal/lsp/source.Matcher\",\"Doc\":\"matcher sets the algorithm that is used when calculating completion candidates. Must be one of:\\n\\n* `\\\"fuzzy\\\"`\\n* `\\\"caseSensitive\\\"`\\n* `\\\"caseInsensitive\\\"`\\n\",\"Default\":\"\\\"Fuzzy\\\"\"},{\"Name\":\"annotations\",\"Type\":\"map[string]bool\",\"Doc\":\"annotations suppress various kinds of optimization diagnostics\\nthat would be reported by the gc_details command.\\n noNilcheck suppresses display of nilchecks.\\n noEscape suppresses escape choices.\\n noInline suppresses inlining choices.\\n noBounds suppresses bounds checking diagnositcs.\\n\",\"Default\":\"null\"},{\"Name\":\"staticcheck\",\"Type\":\"bool\",\"Doc\":\"staticcheck enables additional analyses from staticcheck.io.\\n\",\"Default\":\"false\"},{\"Name\":\"symbolMatcher\",\"Type\":\"golang.org/x/tools/internal/lsp/source.SymbolMatcher\",\"Doc\":\"symbolMatcher sets the algorithm that is used when finding workspace symbols. Must be one of:\\n\\n* `\\\"fuzzy\\\"`\\n* `\\\"caseSensitive\\\"`\\n* `\\\"caseInsensitive\\\"`\\n\",\"Default\":\"\\\"SymbolFuzzy\\\"\"},{\"Name\":\"symbolStyle\",\"Type\":\"golang.org/x/tools/internal/lsp/source.SymbolStyle\",\"Doc\":\"symbolStyle specifies what style of symbols to return in symbol requests. Must be one of:\\n\\n* `\\\"full\\\"`\\n* `\\\"dynamic\\\"`\\n* `\\\"package\\\"`\\n\",\"Default\":\"\\\"PackageQualifiedSymbols\\\"\"},{\"Name\":\"linksInHover\",\"Type\":\"bool\",\"Doc\":\"linksInHover toggles the presence of links to documentation in hover.\\n\",\"Default\":\"true\"},{\"Name\":\"tempModfile\",\"Type\":\"bool\",\"Doc\":\"tempModfile controls the use of the -modfile flag in Go 1.14.\\n\",\"Default\":\"true\"},{\"Name\":\"importShortcut\",\"Type\":\"golang.org/x/tools/internal/lsp/source.ImportShortcut\",\"Doc\":\"importShortcut specifies whether import statements should link to\\ndocumentation or go to definitions. Must be one of:\\n\\n* `\\\"both\\\"`\\n* `\\\"link\\\"`\\n* `\\\"definition\\\"`\\n\",\"Default\":\"\\\"Both\\\"\"},{\"Name\":\"verboseWorkDoneProgress\",\"Type\":\"bool\",\"Doc\":\"verboseWorkDoneProgress controls whether the LSP server should send\\nprogress reports for all work done outside the scope of an RPC.\\n\",\"Default\":\"false\"},{\"Name\":\"expandWorkspaceToModule\",\"Type\":\"bool\",\"Doc\":\"expandWorkspaceToModule instructs `gopls` to expand the scope of the workspace to include the\\nmodules containing the workspace folders. Set this to false to avoid loading\\nyour entire module. This is particularly useful for those working in a monorepo.\\n\",\"Default\":\"true\"},{\"Name\":\"experimentalWorkspaceModule\",\"Type\":\"bool\",\"Doc\":\"experimentalWorkspaceModule opts a user into the experimental support\\nfor multi-module workspaces.\\n\",\"Default\":\"false\"}],\"User\":[{\"Name\":\"buildFlags\",\"Type\":\"[]string\",\"Doc\":\"buildFlags is the set of flags passed on to the build system when invoked.\\nIt is applied to queries like `go list`, which is used when discovering files.\\nThe most common use is to set `-tags`.\\n\",\"Default\":\"[]\"},{\"Name\":\"env\",\"Type\":\"[]string\",\"Doc\":\"env adds environment variables to external commands run by `gopls`, most notably `go list`.\\n\",\"Default\":\"[]\"},{\"Name\":\"hoverKind\",\"Type\":\"golang.org/x/tools/internal/lsp/source.HoverKind\",\"Doc\":\"hoverKind controls the information that appears in the hover text.\\nIt must be one of:\\n* `\\\"NoDocumentation\\\"`\\n* `\\\"SynopsisDocumentation\\\"`\\n* `\\\"FullDocumentation\\\"`\\n\\nAuthors of editor clients may wish to handle hover text differently, and so might use different settings. The options below are not intended for use by anyone other than the authors of editor plugins.\\n\\n* `\\\"SingleLine\\\"`\\n* `\\\"Structured\\\"`\\n\",\"Default\":\"\\\"FullDocumentation\\\"\"},{\"Name\":\"usePlaceholders\",\"Type\":\"bool\",\"Doc\":\"placeholders enables placeholders for function parameters or struct fields in completion responses.\\n\",\"Default\":\"false\"},{\"Name\":\"linkTarget\",\"Type\":\"string\",\"Doc\":\"linkTarget controls where documentation links go.\\nIt might be one of:\\n\\n* `\\\"godoc.org\\\"`\\n* `\\\"pkg.go.dev\\\"`\\n\\nIf company chooses to use its own `godoc.org`, its address can be used as well.\\n\",\"Default\":\"\\\"pkg.go.dev\\\"\"},{\"Name\":\"local\",\"Type\":\"string\",\"Doc\":\"local is the equivalent of the `goimports -local` flag, which puts imports beginning with this string after 3rd-party packages.\\nIt should be the prefix of the import path whose imports should be grouped separately.\\n\",\"Default\":\"\\\"\\\"\"},{\"Name\":\"gofumpt\",\"Type\":\"bool\",\"Doc\":\"gofumpt indicates if we should run gofumpt formatting.\\n\",\"Default\":\"false\"}]}"