mirror of https://github.com/golang/go.git
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:
parent
571a207697
commit
d179df38ff
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"`),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue