internal/lsp/cache: consider gopls.mod when finding workspace root

gopls.mod files should take precedence over go.mod files when finding a
workspace root. To allow this, implement our own algorithm for expanding
the workspace, rather than using the go command.

For golang/go#41837

Change-Id: I943c08bdbdbdd164f108e44bae95d2c659a6e21e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/263897
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Trust: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 448273a..4b97ee6 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -126,7 +126,7 @@
 }
 
 type environmentVariables struct {
-	gocache, gopath, goprivate, gomodcache, gomod string
+	gocache, gopath, goprivate, gomodcache string
 }
 
 type workspaceMode int
@@ -651,13 +651,15 @@
 	tool, _ := exec.LookPath("gopackagesdriver")
 	hasGopackagesDriver := gopackagesdriver != "off" && (gopackagesdriver != "" || tool != "")
 
-	var modURI span.URI
-	if envVars.gomod != os.DevNull && envVars.gomod != "" {
-		modURI = span.URIFromPath(envVars.gomod)
-	}
 	root := folder
-	if options.ExpandWorkspaceToModule && modURI != "" {
-		root = span.URIFromPath(filepath.Dir(modURI.Filename()))
+	if options.ExpandWorkspaceToModule {
+		wsRoot, err := findWorkspaceRoot(ctx, root, s)
+		if err != nil {
+			return nil, err
+		}
+		if wsRoot != "" {
+			root = wsRoot
+		}
 	}
 	return &workspaceInformation{
 		hasGopackagesDriver:  hasGopackagesDriver,
@@ -669,6 +671,39 @@
 	}, nil
 }
 
+func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSource) (span.URI, error) {
+	for _, basename := range []string{"gopls.mod", "go.mod"} {
+		dir, err := findRootPattern(ctx, folder, basename, fs)
+		if err != nil {
+			return "", errors.Errorf("finding %s: %w", basename, err)
+		}
+		if dir != "" {
+			return dir, nil
+		}
+	}
+	return "", nil
+}
+
+func findRootPattern(ctx context.Context, folder span.URI, basename string, fs source.FileSource) (span.URI, error) {
+	dir := folder.Filename()
+	for dir != "" {
+		target := filepath.Join(dir, basename)
+		exists, err := fileExists(ctx, span.URIFromPath(target), fs)
+		if err != nil {
+			return "", err
+		}
+		if exists {
+			return span.URIFromPath(dir), nil
+		}
+		next, _ := filepath.Split(dir)
+		if next == dir {
+			break
+		}
+		dir = next
+	}
+	return "", nil
+}
+
 // OS-specific path case check, for case-insensitive filesystems.
 var checkPathCase = defaultCheckPathCase
 
@@ -710,7 +745,6 @@
 		"GOPATH":     &envVars.gopath,
 		"GOPRIVATE":  &envVars.goprivate,
 		"GOMODCACHE": &envVars.gomodcache,
-		"GOMOD":      &envVars.gomod,
 	}
 	// We can save ~200 ms by requesting only the variables we care about.
 	args := append([]string{"-json"}, imports.RequiredGoEnvVars...)
diff --git a/internal/lsp/cache/view_test.go b/internal/lsp/cache/view_test.go
index 0dad594..87774e6 100644
--- a/internal/lsp/cache/view_test.go
+++ b/internal/lsp/cache/view_test.go
@@ -4,10 +4,14 @@
 package cache
 
 import (
+	"context"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"testing"
+
+	"golang.org/x/tools/internal/lsp/fake"
+	"golang.org/x/tools/internal/span"
 )
 
 func TestCaseInsensitiveFilesystem(t *testing.T) {
@@ -43,3 +47,50 @@
 		}
 	}
 }
+
+func TestFindWorkspaceRoot(t *testing.T) {
+	workspace := `
+-- a/go.mod --
+module a
+-- a/x/x.go
+package x
+-- b/go.mod --
+module b
+-- b/c/go.mod --
+module bc
+-- d/gopls.mod --
+module d-goplsworkspace
+-- d/e/go.mod
+module de
+`
+	dir, err := fake.Tempdir(workspace)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	tests := []struct {
+		folder, want string
+	}{
+		// no module at root.
+		{"", ""},
+		{"a", "a"},
+		{"a/x", "a"},
+		{"b/c", "b/c"},
+		{"d", "d"},
+		{"d/e", "d"},
+	}
+
+	for _, test := range tests {
+		ctx := context.Background()
+		rel := fake.RelativeTo(dir)
+		folderURI := span.URIFromPath(rel.AbsPath(test.folder))
+		got, err := findWorkspaceRoot(ctx, folderURI, osFileSource{})
+		if err != nil {
+			t.Fatal(err)
+		}
+		if rel.RelPath(got.Filename()) != test.want {
+			t.Errorf("fileWorkspaceRoot(%q) = %q, want %q", test.folder, got, test.want)
+		}
+	}
+}