Skip to content

Commit fff6d6d

Browse files
committed
internal/lsp: update the broken workspace message to mention go.work
For users of Go >= 1.18, update the error message presented to users with broken workspaces to suggest using a go.work. For older Go versions, suggest updating to 1.18+. Also: - add support for changing workspace folders from the fake editor - update the test to confirm that changing workspace folders resolves the error message - inline validBuildConfiguration, and make a few TODOs for how the code could be further cleaned up Fixes golang/go#53882 Change-Id: Ia03dcfec59569b1a3ac941dc40d079b9c2593825 Reviewed-on: https://go-review.googlesource.com/c/tools/+/421499 TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Dylan Le <[email protected]> Reviewed-by: Suzy Mueller <[email protected]>
1 parent 9b60852 commit fff6d6d

File tree

10 files changed

+208
-67
lines changed

10 files changed

+208
-67
lines changed

gopls/internal/regtest/workspace/broken_test.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ const F = named.D - 3
169169
}
170170

171171
func TestMultipleModules_Warning(t *testing.T) {
172+
msgForVersion := func(ver int) string {
173+
if ver >= 18 {
174+
return `gopls was not able to find modules in your workspace.`
175+
} else {
176+
return `gopls requires a module at the root of your workspace.`
177+
}
178+
}
179+
172180
const modules = `
173181
-- a/go.mod --
174182
module a.com
@@ -189,13 +197,34 @@ package b
189197
Modes(Default),
190198
EnvVars{"GO111MODULE": go111module},
191199
).Run(t, modules, func(t *testing.T, env *Env) {
200+
ver := env.GoVersion()
201+
msg := msgForVersion(ver)
192202
env.OpenFile("a/a.go")
193203
env.OpenFile("b/go.mod")
194204
env.Await(
195205
env.DiagnosticAtRegexp("a/a.go", "package a"),
196206
env.DiagnosticAtRegexp("b/go.mod", "module b.com"),
197-
OutstandingWork(lsp.WorkspaceLoadFailure, "gopls requires a module at the root of your workspace."),
207+
OutstandingWork(lsp.WorkspaceLoadFailure, msg),
198208
)
209+
210+
// Changing the workspace folders to the valid modules should resolve
211+
// the workspace error.
212+
env.ChangeWorkspaceFolders("a", "b")
213+
env.Await(NoOutstandingWork())
214+
215+
env.ChangeWorkspaceFolders(".")
216+
217+
// TODO(rfindley): when GO111MODULE=auto, we need to open or change a
218+
// file here in order to detect a critical error. This is because gopls
219+
// has forgotten about a/a.go, and therefor doesn't hit the heuristic
220+
// "all packages are command-line-arguments".
221+
//
222+
// This is broken, and could be fixed by adjusting the heuristic to
223+
// account for the scenario where there are *no* workspace packages, or
224+
// (better) trying to get workspace packages for each open file. See
225+
// also golang/go#54261.
226+
env.OpenFile("b/b.go")
227+
env.Await(OutstandingWork(lsp.WorkspaceLoadFailure, msg))
199228
})
200229
})
201230
}

internal/lsp/cache/load.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,15 +283,20 @@ func (m *moduleErrorMap) Error() string {
283283
// workspaceLayoutErrors returns a diagnostic for every open file, as well as
284284
// an error message if there are no open files.
285285
func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError {
286+
// TODO(rfindley): do we really not want to show a critical error if the user
287+
// has no go.mod files?
286288
if len(s.workspace.getKnownModFiles()) == 0 {
287289
return nil
288290
}
291+
292+
// TODO(rfindley): both of the checks below should be delegated to the workspace.
289293
if s.view.userGo111Module == off {
290294
return nil
291295
}
292296
if s.workspace.moduleSource != legacyWorkspace {
293297
return nil
294298
}
299+
295300
// If the user has one module per view, there is nothing to warn about.
296301
if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 {
297302
return nil
@@ -305,10 +310,21 @@ func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalErr
305310
// that the user has opened a directory that contains multiple modules.
306311
// Check for that an warn about it.
307312
if !s.ValidBuildConfiguration() {
308-
msg := `gopls requires a module at the root of your workspace.
309-
You can work with multiple modules by opening each one as a workspace folder.
310-
Improvements to this workflow will be coming soon, and you can learn more here:
313+
var msg string
314+
if s.view.goversion >= 18 {
315+
msg = `gopls was not able to find modules in your workspace.
316+
When outside of GOPATH, gopls needs to know which modules you are working on.
317+
You can fix this by opening your workspace to a folder inside a Go module, or
318+
by using a go.work file to specify multiple modules.
319+
See the documentation for more information on setting up your workspace:
311320
https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`
321+
} else {
322+
msg = `gopls requires a module at the root of your workspace.
323+
You can work with multiple modules by upgrading to Go 1.18 or later, and using
324+
go workspaces (go.work files).
325+
See the documentation for more information on setting up your workspace:
326+
https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`
327+
}
312328
return &source.CriticalError{
313329
MainError: fmt.Errorf(msg),
314330
Diagnostics: s.applyCriticalErrorToFiles(ctx, msg, openFiles),

internal/lsp/cache/snapshot.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,24 @@ func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle {
274274
}
275275

276276
func (s *snapshot) ValidBuildConfiguration() bool {
277-
return validBuildConfiguration(s.view.rootURI, &s.view.workspaceInformation, s.workspace.getActiveModFiles())
277+
// Since we only really understand the `go` command, if the user has a
278+
// different GOPACKAGESDRIVER, assume that their configuration is valid.
279+
if s.view.hasGopackagesDriver {
280+
return true
281+
}
282+
// Check if the user is working within a module or if we have found
283+
// multiple modules in the workspace.
284+
if len(s.workspace.getActiveModFiles()) > 0 {
285+
return true
286+
}
287+
// The user may have a multiple directories in their GOPATH.
288+
// Check if the workspace is within any of them.
289+
for _, gp := range filepath.SplitList(s.view.gopath) {
290+
if source.InDir(filepath.Join(gp, "src"), s.view.rootURI.Filename()) {
291+
return true
292+
}
293+
}
294+
return false
278295
}
279296

280297
// workspaceMode describes the way in which the snapshot's workspace should
@@ -1358,6 +1375,8 @@ func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError {
13581375
// with the user's workspace layout. Workspace packages that only have the
13591376
// ID "command-line-arguments" are usually a symptom of a bad workspace
13601377
// configuration.
1378+
//
1379+
// TODO(rfindley): re-evaluate this heuristic.
13611380
if containsCommandLineArguments(wsPkgs) {
13621381
return s.workspaceLayoutError(ctx)
13631382
}

internal/lsp/cache/view.go

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -926,27 +926,6 @@ func defaultCheckPathCase(path string) error {
926926
return nil
927927
}
928928

929-
func validBuildConfiguration(folder span.URI, ws *workspaceInformation, modFiles map[span.URI]struct{}) bool {
930-
// Since we only really understand the `go` command, if the user has a
931-
// different GOPACKAGESDRIVER, assume that their configuration is valid.
932-
if ws.hasGopackagesDriver {
933-
return true
934-
}
935-
// Check if the user is working within a module or if we have found
936-
// multiple modules in the workspace.
937-
if len(modFiles) > 0 {
938-
return true
939-
}
940-
// The user may have a multiple directories in their GOPATH.
941-
// Check if the workspace is within any of them.
942-
for _, gp := range filepath.SplitList(ws.gopath) {
943-
if source.InDir(filepath.Join(gp, "src"), folder.Filename()) {
944-
return true
945-
}
946-
}
947-
return false
948-
}
949-
950929
// getGoEnv gets the view's various GO* values.
951930
func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, go111module string, configEnv []string) (environmentVariables, map[string]string, error) {
952931
envVars := environmentVariables{}

internal/lsp/fake/client.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,11 @@ func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder,
7474
func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
7575
results := make([]interface{}, len(p.Items))
7676
for i, item := range p.Items {
77-
if item.Section != "gopls" {
78-
continue
77+
if item.Section == "gopls" {
78+
c.editor.mu.Lock()
79+
results[i] = c.editor.settingsLocked()
80+
c.editor.mu.Unlock()
7981
}
80-
results[i] = c.editor.settings()
8182
}
8283
return results, nil
8384
}

internal/lsp/fake/editor.go

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
121121
//
122122
// It returns the editor, so that it may be called as follows:
123123
//
124-
// editor, err := NewEditor(s).Connect(ctx, conn)
124+
// editor, err := NewEditor(s).Connect(ctx, conn, hooks)
125125
func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks) (*Editor, error) {
126126
bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx))
127127
conn := connector.Connect(bgCtx)
@@ -135,7 +135,7 @@ func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, ho
135135
protocol.ClientHandler(e.client,
136136
jsonrpc2.MethodNotFound)))
137137

138-
if err := e.initialize(ctx, e.config.WorkspaceFolders); err != nil {
138+
if err := e.initialize(ctx); err != nil {
139139
return nil, err
140140
}
141141
e.sandbox.Workdir.AddWatcher(e.onFileChanges)
@@ -197,11 +197,10 @@ func (e *Editor) Client() *Client {
197197
return e.client
198198
}
199199

200-
// settings builds the settings map for use in LSP settings
201-
// RPCs.
202-
func (e *Editor) settings() map[string]interface{} {
203-
e.mu.Lock()
204-
defer e.mu.Unlock()
200+
// settingsLocked builds the settings map for use in LSP settings RPCs.
201+
//
202+
// e.mu must be held while calling this function.
203+
func (e *Editor) settingsLocked() map[string]interface{} {
205204
env := make(map[string]string)
206205
for k, v := range e.defaultEnv {
207206
env[k] = v
@@ -240,26 +239,19 @@ func (e *Editor) settings() map[string]interface{} {
240239
return settings
241240
}
242241

243-
func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error {
242+
func (e *Editor) initialize(ctx context.Context) error {
244243
params := &protocol.ParamInitialize{}
245244
params.ClientInfo.Name = "fakeclient"
246245
params.ClientInfo.Version = "v1.0.0"
247-
248-
if workspaceFolders == nil {
249-
workspaceFolders = []string{string(e.sandbox.Workdir.RelativeTo)}
250-
}
251-
for _, folder := range workspaceFolders {
252-
params.WorkspaceFolders = append(params.WorkspaceFolders, protocol.WorkspaceFolder{
253-
URI: string(e.sandbox.Workdir.URI(folder)),
254-
Name: filepath.Base(folder),
255-
})
256-
}
257-
246+
e.mu.Lock()
247+
params.WorkspaceFolders = e.makeWorkspaceFoldersLocked()
248+
params.InitializationOptions = e.settingsLocked()
249+
e.mu.Unlock()
258250
params.Capabilities.Workspace.Configuration = true
259251
params.Capabilities.Window.WorkDoneProgress = true
252+
260253
// TODO: set client capabilities
261254
params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
262-
params.InitializationOptions = e.settings()
263255

264256
params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
265257
params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true
@@ -296,6 +288,27 @@ func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) erro
296288
return nil
297289
}
298290

291+
// makeWorkspaceFoldersLocked creates a slice of workspace folders to use for
292+
// this editing session, based on the editor configuration.
293+
//
294+
// e.mu must be held while calling this function.
295+
func (e *Editor) makeWorkspaceFoldersLocked() (folders []protocol.WorkspaceFolder) {
296+
paths := e.config.WorkspaceFolders
297+
if len(paths) == 0 {
298+
paths = append(paths, string(e.sandbox.Workdir.RelativeTo))
299+
}
300+
301+
for _, path := range paths {
302+
uri := string(e.sandbox.Workdir.URI(path))
303+
folders = append(folders, protocol.WorkspaceFolder{
304+
URI: uri,
305+
Name: filepath.Base(uri),
306+
})
307+
}
308+
309+
return folders
310+
}
311+
299312
// onFileChanges is registered to be called by the Workdir on any writes that
300313
// go through the Workdir API. It is called synchronously by the Workdir.
301314
func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
@@ -1195,6 +1208,54 @@ func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig
11951208
return nil
11961209
}
11971210

1211+
// ChangeWorkspaceFolders sets the new workspace folders, and sends a
1212+
// didChangeWorkspaceFolders notification to the server.
1213+
//
1214+
// The given folders must all be unique.
1215+
func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error {
1216+
// capture existing folders so that we can compute the change.
1217+
e.mu.Lock()
1218+
oldFolders := e.makeWorkspaceFoldersLocked()
1219+
e.config.WorkspaceFolders = folders
1220+
newFolders := e.makeWorkspaceFoldersLocked()
1221+
e.mu.Unlock()
1222+
1223+
if e.Server == nil {
1224+
return nil
1225+
}
1226+
1227+
var params protocol.DidChangeWorkspaceFoldersParams
1228+
1229+
// Keep track of old workspace folders that must be removed.
1230+
toRemove := make(map[protocol.URI]protocol.WorkspaceFolder)
1231+
for _, folder := range oldFolders {
1232+
toRemove[folder.URI] = folder
1233+
}
1234+
1235+
// Sanity check: if we see a folder twice the algorithm below doesn't work,
1236+
// so track seen folders to ensure that we panic in that case.
1237+
seen := make(map[protocol.URI]protocol.WorkspaceFolder)
1238+
for _, folder := range newFolders {
1239+
if _, ok := seen[folder.URI]; ok {
1240+
panic(fmt.Sprintf("folder %s seen twice", folder.URI))
1241+
}
1242+
1243+
// If this folder already exists, we don't want to remove it.
1244+
// Otherwise, we need to add it.
1245+
if _, ok := toRemove[folder.URI]; ok {
1246+
delete(toRemove, folder.URI)
1247+
} else {
1248+
params.Event.Added = append(params.Event.Added, folder)
1249+
}
1250+
}
1251+
1252+
for _, v := range toRemove {
1253+
params.Event.Removed = append(params.Event.Removed, v)
1254+
}
1255+
1256+
return e.Server.DidChangeWorkspaceFolders(ctx, &params)
1257+
}
1258+
11981259
// CodeAction executes a codeAction request on the server.
11991260
func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
12001261
if e.Server == nil {

0 commit comments

Comments
 (0)