diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index 9f5e2b098b8..cdaee1b973b 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go @@ -473,7 +473,7 @@ func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, nam return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err) } - opts := opts.Clone() + opts = opts.Clone() for _, config := range configs { if err := s.handleOptionResults(ctx, settings.SetOptions(opts, config)); err != nil { return nil, err @@ -493,15 +493,23 @@ func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, nam }, nil } +// fetchFolderOptions makes a workspace/configuration request for the given +// folder, and populates options with the result. +// +// If folder is "", fetchFolderOptions makes an unscoped request. func (s *server) fetchFolderOptions(ctx context.Context, folder protocol.DocumentURI) (*settings.Options, error) { opts := s.Options() if !opts.ConfigurationSupported { return opts, nil } - scope := string(folder) + var scopeURI *string + if folder != "" { + scope := string(folder) + scopeURI = &scope + } configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{ Items: []protocol.ConfigurationItem{{ - ScopeURI: &scope, + ScopeURI: scopeURI, Section: "gopls", }}, }, diff --git a/gopls/internal/test/integration/fake/client.go b/gopls/internal/test/integration/fake/client.go index 2859d9b4047..f940821eefe 100644 --- a/gopls/internal/test/integration/fake/client.go +++ b/gopls/internal/test/integration/fake/client.go @@ -99,9 +99,12 @@ func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { results := make([]interface{}, len(p.Items)) for i, item := range p.Items { + if item.ScopeURI != nil && *item.ScopeURI == "" { + return nil, fmt.Errorf(`malformed ScopeURI ""`) + } if item.Section == "gopls" { config := c.editor.Config() - results[i] = makeSettings(c.editor.sandbox, config) + results[i] = makeSettings(c.editor.sandbox, config, item.ScopeURI) } } return results, nil diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index e2eec07f51d..7c15106603f 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -110,7 +110,13 @@ type EditorConfig struct { FileAssociations map[string]string // Settings holds user-provided configuration for the LSP server. - Settings map[string]interface{} + Settings map[string]any + + // FolderSettings holds user-provided per-folder configuration, if any. + // + // It maps each folder (as a relative path to the sandbox workdir) to its + // configuration mapping (like Settings). + FolderSettings map[string]map[string]any // CapabilitiesJSON holds JSON client capabilities to overlay over the // editor's default client capabilities. @@ -216,7 +222,7 @@ func (e *Editor) Client() *Client { } // makeSettings builds the settings map for use in LSP settings RPCs. -func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{} { +func makeSettings(sandbox *Sandbox, config EditorConfig, scopeURI *protocol.URI) map[string]any { env := make(map[string]string) for k, v := range sandbox.GoEnv() { env[k] = v @@ -229,7 +235,7 @@ func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{} env[k] = v } - settings := map[string]interface{}{ + settings := map[string]any{ "env": env, // Use verbose progress reporting so that integration tests can assert on @@ -248,6 +254,28 @@ func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{} settings[k] = v } + // If the server is requesting configuration for a specific scope, apply + // settings for the nearest folder that has customized settings, if any. + if scopeURI != nil { + var ( + scopePath = protocol.DocumentURI(*scopeURI).Path() + closestDir string // longest dir with settings containing the scope, if any + closestSettings map[string]any // settings for that dir, if any + ) + for relPath, settings := range config.FolderSettings { + dir := sandbox.Workdir.AbsPath(relPath) + if strings.HasPrefix(scopePath+string(filepath.Separator), dir+string(filepath.Separator)) && len(dir) > len(closestDir) { + closestDir = dir + closestSettings = settings + } + } + if closestSettings != nil { + for k, v := range closestSettings { + settings[k] = v + } + } + } + return settings } @@ -261,7 +289,7 @@ func (e *Editor) initialize(ctx context.Context) error { Version: "v1.0.0", } } - params.InitializationOptions = makeSettings(e.sandbox, config) + params.InitializationOptions = makeSettings(e.sandbox, config, nil) params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) capabilities, err := clientCapabilities(config) diff --git a/gopls/internal/test/integration/misc/configuration_test.go b/gopls/internal/test/integration/misc/configuration_test.go index 2d39dc9fad1..fff4576bc5b 100644 --- a/gopls/internal/test/integration/misc/configuration_test.go +++ b/gopls/internal/test/integration/misc/configuration_test.go @@ -49,6 +49,48 @@ var FooErr = errors.New("foo") }) } +// Test that clients can configure per-workspace configuration, which is +// queried via the scopeURI of a workspace/configuration request. +// (this was broken in golang/go#65519). +func TestWorkspaceConfiguration(t *testing.T) { + const files = ` +-- go.mod -- +module example.com/config + +go 1.18 + +-- a/a.go -- +package a + +import "example.com/config/b" + +func _() { + _ = b.B{2} +} + +-- b/b.go -- +package b + +type B struct { + F int +} +` + + WithOptions( + WorkspaceFolders("a"), + FolderSettings(map[string]Settings{ + "a": { + "analyses": map[string]bool{ + "composites": false, + }, + }, + }), + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + env.AfterChange(NoDiagnostics()) + }) +} + // TestMajorOptionsChange is like TestChangeConfiguration, but modifies an // an open buffer before making a major (but inconsequential) change that // causes gopls to recreate the view. diff --git a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go index 274b9e4ac6c..a6c394e3467 100644 --- a/gopls/internal/test/integration/options.go +++ b/gopls/internal/test/integration/options.go @@ -104,11 +104,29 @@ func WorkspaceFolders(relFolders ...string) RunOption { // Use an empty non-nil slice to signal explicitly no folders. relFolders = []string{} } + return optionSetter(func(opts *runConfig) { opts.editor.WorkspaceFolders = relFolders }) } +// FolderSettings defines per-folder workspace settings, keyed by relative path +// to the folder. +// +// Use in conjunction with WorkspaceFolders to have different settings for +// different folders. +func FolderSettings(folderSettings map[string]Settings) RunOption { + // Re-use the Settings type, for symmetry, but translate back into maps for + // the editor config. + folders := make(map[string]map[string]any) + for k, v := range folderSettings { + folders[k] = v + } + return optionSetter(func(opts *runConfig) { + opts.editor.FolderSettings = folders + }) +} + // EnvVars sets environment variables for the LSP session. When applying these // variables to the session, the special string $SANDBOX_WORKDIR is replaced by // the absolute path to the sandbox working directory. diff --git a/gopls/internal/test/marker/testdata/configuration/static.txt b/gopls/internal/test/marker/testdata/configuration/static.txt new file mode 100644 index 00000000000..c84b55db117 --- /dev/null +++ b/gopls/internal/test/marker/testdata/configuration/static.txt @@ -0,0 +1,41 @@ +This test confirms that gopls honors configuration even if the client does not +support dynamic configuration. + +-- capabilities.json -- +{ + "configuration": false +} + +-- settings.json -- +{ + "usePlaceholders": true, + "analyses": { + "composites": false + } +} + +-- go.mod -- +module example.com/config + +go 1.18 + +-- a/a.go -- +package a + +import "example.com/config/b" + +func Identity[P ~int](p P) P { //@item(Identity, "Identity", "", "") + return p +} + +func _() { + _ = b.B{2} + _ = Identi //@snippet(" //", Identity, "Identity(${1:p P})"), diag("Ident", re"(undefined|undeclared)") +} + +-- b/b.go -- +package b + +type B struct { + F int +}