Skip to content

Commit 3a5dbf3

Browse files
committed
gopls: add a new "subdirWatchPatterns" setting
As discovered in golang/go#60089, file watching patterns behave very differently in different clients. We avoided a bad client-side bug in VS Code by splitting our subdirectory watch pattern, but this appears to be very expensive in other clients (notably coc.nvim, or any vim client that uses watchman). The subdirectory watch patterns were only known to be necessary for VS Code, due to microsoft/vscode#109754. Other clients work as expected when we watch e.g. **/*.go. For that reason, let's revert all other clients to just use simple watch patterns, and only specialize to have subdirectory watch patterns for VS Code. It's truly unfortunate to have to specialize in this way. To paper over this hole in the wall, add an internal setting that allows clients to configure this behavior explicitly. The new "subdirWatchPatterns" setting may accepts the following values: - "on": request watch patterns for each subdirectory (as before) - "off": do not request subdirectory watch patterns - "auto": same as "on" for VS Code, "off" for all others, based on the provided initializeParams.clientInfo.Name. Includes some minor cleanup for the fake editor, and fixes some stale comments. Updates golang/go#golang/go#60089 Fixes golang/go#59635 Change-Id: I1eab5c08790bd86a5910657169edcb20511c0280 Reviewed-on: https://go-review.googlesource.com/c/tools/+/496835 TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> Run-TryBot: Robert Findley <rfindley@google.com>
1 parent 3c02551 commit 3a5dbf3

File tree

9 files changed

+251
-73
lines changed

9 files changed

+251
-73
lines changed

gopls/internal/lsp/cache/snapshot.go

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -936,30 +936,56 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
936936
patterns[fmt.Sprintf("%s/**/*.{%s}", dirName, extensions)] = struct{}{}
937937
}
938938

939-
// Some clients (e.g. VSCode) do not send notifications for
940-
// changes to directories that contain Go code (golang/go#42348).
941-
// To handle this, explicitly watch all of the directories in
942-
// the workspace. We find them by adding the directories of
943-
// every file in the snapshot's workspace directories.
944-
// There may be thousands of patterns, each a single directory.
945-
//
946-
// (A previous iteration created a single glob pattern holding a
947-
// union of all the directories, but this was found to cause
948-
// VSCode to get stuck for several minutes after a buffer was
949-
// saved twice in a workspace that had >8000 watched directories.)
950-
//
951-
// Some clients (notably coc.nvim, which uses watchman for
952-
// globs) perform poorly with a large list of individual
953-
// directories, though they work fine with one large
954-
// comma-separated element. Sadly no one size fits all, so we
955-
// may have to resort to sniffing the client to determine the
956-
// best behavior, though that would set a poor precedent.
957-
// TODO(adonovan): improve the nvim situation.
958-
s.addKnownSubdirs(patterns, dirs)
939+
if s.watchSubdirs() {
940+
// Some clients (e.g. VS Code) do not send notifications for changes to
941+
// directories that contain Go code (golang/go#42348). To handle this,
942+
// explicitly watch all of the directories in the workspace. We find them
943+
// by adding the directories of every file in the snapshot's workspace
944+
// directories. There may be thousands of patterns, each a single
945+
// directory.
946+
//
947+
// (A previous iteration created a single glob pattern holding a union of
948+
// all the directories, but this was found to cause VS Code to get stuck
949+
// for several minutes after a buffer was saved twice in a workspace that
950+
// had >8000 watched directories.)
951+
//
952+
// Some clients (notably coc.nvim, which uses watchman for globs) perform
953+
// poorly with a large list of individual directories.
954+
s.addKnownSubdirs(patterns, dirs)
955+
}
959956

960957
return patterns
961958
}
962959

960+
// watchSubdirs reports whether gopls should request separate file watchers for
961+
// each relevant subdirectory. This is necessary only for clients (namely VS
962+
// Code) that do not send notifications for individual files in a directory
963+
// when the entire directory is deleted.
964+
func (s *snapshot) watchSubdirs() bool {
965+
opts := s.view.Options()
966+
switch p := opts.SubdirWatchPatterns; p {
967+
case source.SubdirWatchPatternsOn:
968+
return true
969+
case source.SubdirWatchPatternsOff:
970+
return false
971+
case source.SubdirWatchPatternsAuto:
972+
// See the documentation of InternalOptions.SubdirWatchPatterns for an
973+
// explanation of why VS Code gets a different default value here.
974+
//
975+
// Unfortunately, there is no authoritative list of client names, nor any
976+
// requirements that client names do not change. We should update the VS
977+
// Code extension to set a default value of "subdirWatchPatterns" to "on",
978+
// so that this workaround is only temporary.
979+
if opts.ClientInfo != nil && opts.ClientInfo.Name == "Visual Studio Code" {
980+
return true
981+
}
982+
return false
983+
default:
984+
bug.Reportf("invalid subdirWatchPatterns: %q", p)
985+
return false
986+
}
987+
}
988+
963989
func (s *snapshot) addKnownSubdirs(patterns map[string]struct{}, wsDirs []span.URI) {
964990
s.mu.Lock()
965991
defer s.mu.Unlock()

gopls/internal/lsp/fake/client.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,8 @@ func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration
9494
results := make([]interface{}, len(p.Items))
9595
for i, item := range p.Items {
9696
if item.Section == "gopls" {
97-
c.editor.mu.Lock()
98-
results[i] = c.editor.settingsLocked()
99-
c.editor.mu.Unlock()
97+
config := c.editor.Config()
98+
results[i] = makeSettings(c.editor.sandbox, config)
10099
}
101100
}
102101
return results, nil

gopls/internal/lsp/fake/editor.go

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ type Editor struct {
3737
serverConn jsonrpc2.Conn
3838
client *Client
3939
sandbox *Sandbox
40-
defaultEnv map[string]string
4140

4241
// TODO(adonovan): buffers should be keyed by protocol.DocumentURI.
4342
mu sync.Mutex
@@ -75,8 +74,14 @@ func (b buffer) text() string {
7574
// source.UserOptions, but we use a separate type here so that we expose only
7675
// that configuration which we support.
7776
//
78-
// The zero value for EditorConfig should correspond to its defaults.
77+
// The zero value for EditorConfig is the default configuration.
7978
type EditorConfig struct {
79+
// ClientName sets the clientInfo.name for the LSP session (in the initialize request).
80+
//
81+
// Since this can only be set during initialization, changing this field via
82+
// Editor.ChangeConfiguration has no effect.
83+
ClientName string
84+
8085
// Env holds environment variables to apply on top of the default editor
8186
// environment. When applying these variables, the special string
8287
// $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working
@@ -109,10 +114,9 @@ type EditorConfig struct {
109114
// NewEditor creates a new Editor.
110115
func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
111116
return &Editor{
112-
buffers: make(map[string]buffer),
113-
sandbox: sandbox,
114-
defaultEnv: sandbox.GoEnv(),
115-
config: config,
117+
buffers: make(map[string]buffer),
118+
sandbox: sandbox,
119+
config: config,
116120
}
117121
}
118122

@@ -198,19 +202,17 @@ func (e *Editor) Client() *Client {
198202
return e.client
199203
}
200204

201-
// settingsLocked builds the settings map for use in LSP settings RPCs.
202-
//
203-
// e.mu must be held while calling this function.
204-
func (e *Editor) settingsLocked() map[string]interface{} {
205+
// makeSettings builds the settings map for use in LSP settings RPCs.
206+
func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{} {
205207
env := make(map[string]string)
206-
for k, v := range e.defaultEnv {
208+
for k, v := range sandbox.GoEnv() {
207209
env[k] = v
208210
}
209-
for k, v := range e.config.Env {
211+
for k, v := range config.Env {
210212
env[k] = v
211213
}
212214
for k, v := range env {
213-
v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename())
215+
v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", sandbox.Workdir.RootURI().SpanURI().Filename())
214216
env[k] = v
215217
}
216218

@@ -226,7 +228,7 @@ func (e *Editor) settingsLocked() map[string]interface{} {
226228
"completionBudget": "10s",
227229
}
228230

229-
for k, v := range e.config.Settings {
231+
for k, v := range config.Settings {
230232
if k == "env" {
231233
panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead")
232234
}
@@ -237,20 +239,22 @@ func (e *Editor) settingsLocked() map[string]interface{} {
237239
}
238240

239241
func (e *Editor) initialize(ctx context.Context) error {
242+
config := e.Config()
243+
240244
params := &protocol.ParamInitialize{}
241-
params.ClientInfo = &protocol.Msg_XInitializeParams_clientInfo{}
242-
params.ClientInfo.Name = "fakeclient"
243-
params.ClientInfo.Version = "v1.0.0"
244-
e.mu.Lock()
245-
params.WorkspaceFolders = e.makeWorkspaceFoldersLocked()
246-
params.InitializationOptions = e.settingsLocked()
247-
e.mu.Unlock()
248-
params.Capabilities.Workspace.Configuration = true
249-
params.Capabilities.Window.WorkDoneProgress = true
245+
if e.config.ClientName != "" {
246+
params.ClientInfo = &protocol.Msg_XInitializeParams_clientInfo{}
247+
params.ClientInfo.Name = e.config.ClientName
248+
params.ClientInfo.Version = "v1.0.0"
249+
}
250+
params.InitializationOptions = makeSettings(e.sandbox, config)
251+
params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
252+
params.Capabilities.Workspace.Configuration = true // support workspace/configuration
253+
params.Capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress
250254

251-
// TODO: set client capabilities
252-
params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
255+
// TODO(rfindley): set client capabilities (note from the future: why?)
253256

257+
params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
254258
params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
255259
params.Capabilities.TextDocument.SemanticTokens.Requests.Full.Value = true
256260
// copied from lsp/semantic.go to avoid import cycle in tests
@@ -269,11 +273,12 @@ func (e *Editor) initialize(ctx context.Context) error {
269273
// but really we should test both ways for older editors.
270274
params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true
271275

272-
// This is a bit of a hack, since the fake editor doesn't actually support
273-
// watching changed files that match a specific glob pattern. However, the
274-
// editor does send didChangeWatchedFiles notifications, so set this to
275-
// true.
276+
// Glob pattern watching is enabled.
276277
params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
278+
279+
// "rename" operations are used for package renaming.
280+
//
281+
// TODO(rfindley): add support for other resource operations (create, delete, ...)
277282
params.Capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{
278283
ResourceOperations: []protocol.ResourceOperationKind{
279284
"rename",
@@ -300,18 +305,15 @@ func (e *Editor) initialize(ctx context.Context) error {
300305
return nil
301306
}
302307

303-
// makeWorkspaceFoldersLocked creates a slice of workspace folders to use for
308+
// makeWorkspaceFolders creates a slice of workspace folders to use for
304309
// this editing session, based on the editor configuration.
305-
//
306-
// e.mu must be held while calling this function.
307-
func (e *Editor) makeWorkspaceFoldersLocked() (folders []protocol.WorkspaceFolder) {
308-
paths := e.config.WorkspaceFolders
310+
func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.WorkspaceFolder) {
309311
if len(paths) == 0 {
310-
paths = append(paths, string(e.sandbox.Workdir.RelativeTo))
312+
paths = []string{string(sandbox.Workdir.RelativeTo)}
311313
}
312314

313315
for _, path := range paths {
314-
uri := string(e.sandbox.Workdir.URI(path))
316+
uri := string(sandbox.Workdir.URI(path))
315317
folders = append(folders, protocol.WorkspaceFolder{
316318
URI: uri,
317319
Name: filepath.Base(uri),
@@ -1329,14 +1331,18 @@ func (e *Editor) Config() EditorConfig {
13291331
return e.config
13301332
}
13311333

1334+
func (e *Editor) SetConfig(cfg EditorConfig) {
1335+
e.mu.Lock()
1336+
e.config = cfg
1337+
e.mu.Unlock()
1338+
}
1339+
13321340
// ChangeConfiguration sets the new editor configuration, and if applicable
13331341
// sends a didChangeConfiguration notification.
13341342
//
13351343
// An error is returned if the change notification failed to send.
13361344
func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error {
1337-
e.mu.Lock()
1338-
e.config = newConfig
1339-
e.mu.Unlock() // don't hold e.mu during server calls
1345+
e.SetConfig(newConfig)
13401346
if e.Server != nil {
13411347
var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field
13421348
if err := e.Server.DidChangeConfiguration(ctx, &params); err != nil {
@@ -1351,12 +1357,13 @@ func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig
13511357
//
13521358
// The given folders must all be unique.
13531359
func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error {
1360+
config := e.Config()
1361+
13541362
// capture existing folders so that we can compute the change.
1355-
e.mu.Lock()
1356-
oldFolders := e.makeWorkspaceFoldersLocked()
1357-
e.config.WorkspaceFolders = folders
1358-
newFolders := e.makeWorkspaceFoldersLocked()
1359-
e.mu.Unlock()
1363+
oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
1364+
newFolders := makeWorkspaceFolders(e.sandbox, folders)
1365+
config.WorkspaceFolders = folders
1366+
e.SetConfig(config)
13601367

13611368
if e.Server == nil {
13621369
return nil

gopls/internal/lsp/general.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitializ
5959
if err := s.handleOptionResults(ctx, source.SetOptions(options, params.InitializationOptions)); err != nil {
6060
return nil, err
6161
}
62-
options.ForClientCapabilities(params.Capabilities)
62+
options.ForClientCapabilities(params.ClientInfo, params.Capabilities)
6363

6464
if options.ShowBugReports {
6565
// Report the next bug that occurs on the server.

gopls/internal/lsp/regtest/expectation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func ShownMessage(containing string) Expectation {
235235
}
236236
return Expectation{
237237
Check: check,
238-
Description: "received ShowMessage",
238+
Description: fmt.Sprintf("received window/showMessage containing %q", containing),
239239
}
240240
}
241241

gopls/internal/lsp/regtest/options.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,14 @@ func WindowsLineEndings() RunOption {
6464
})
6565
}
6666

67-
// Settings is a RunOption that sets user-provided configuration for the LSP
68-
// server.
67+
// ClientName sets the LSP client name.
68+
func ClientName(name string) RunOption {
69+
return optionSetter(func(opts *runConfig) {
70+
opts.editor.ClientName = name
71+
})
72+
}
73+
74+
// Settings sets user-provided configuration for the LSP server.
6975
//
7076
// As a special case, the env setting must not be provided via Settings: use
7177
// EnvVars instead.

0 commit comments

Comments
 (0)