internal/lsp/cache: automatically construct the workspace module

This change adds an experimental configuration, which when enabled,
shifts gopls to operate in multi-module mode. It implements the
super-module as described in
https://github.com/golang/proposal/blob/master/design/37720-gopls-workspaces.md.
Replace directives are also added when a workspace module requires
another workspace module (which has not yet been mentioned in the design
doc).

A user-provided workspace gopls.mod file is not yet supported, as it is
not yet testable. Clients will need to add support for change
notifications for the gopls.mod once it is added.

Updates golang/go#32394

Change-Id: I5089358603bca34c5c8db9e5a00f93e1cca0b93f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/247819
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
This commit is contained in:
Rebecca Stambler 2020-08-11 02:11:06 -04:00
parent 571a207697
commit d179df38ff
14 changed files with 385 additions and 27 deletions

View File

@ -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`.
<!-- END Experimental: DO NOT MANUALLY EDIT THIS SECTION -->

View File

@ -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.

View File

@ -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"`),
)
})
}

View File

@ -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 {

View File

@ -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...)

View File

@ -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}
}

View File

@ -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 {

View File

@ -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()

View File

@ -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
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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
}

View File

@ -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

File diff suppressed because one or more lines are too long