diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index f48dc3ffac..f0e7314444 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -209,6 +209,15 @@ Must be one of: * `"Fuzzy"` Default: `"Fuzzy"`. +##### **experimentalPostfixCompletions** *bool* + +**This setting is experimental and may be deleted.** + +experimentalPostfixCompletions enables artifical method snippets +such as "someSlice.sort!". + +Default: `false`. + #### Diagnostic ##### **analyses** *map[string]bool* diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index 002916ccd8..4c59ef90de 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -9,6 +9,7 @@ import ( "testing" . "golang.org/x/tools/gopls/internal/regtest" + "golang.org/x/tools/internal/lsp/source" ) func TestPostfixSnippetCompletion(t *testing.T) { @@ -373,7 +374,10 @@ func _() { }, } - Run(t, mod, func(t *testing.T, env *Env) { + r := WithOptions(Options(func(o *source.Options) { + o.ExperimentalPostfixCompletions = true + })) + r.Run(t, mod, func(t *testing.T, env *Env) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { c.before = strings.Trim(c.before, "\n") diff --git a/gopls/internal/regtest/runner.go b/gopls/internal/regtest/runner.go index 68087d701c..cfa64af540 100644 --- a/gopls/internal/regtest/runner.go +++ b/gopls/internal/regtest/runner.go @@ -70,19 +70,21 @@ type Runner struct { } type runConfig struct { - editor fake.EditorConfig - sandbox fake.SandboxConfig - modes Mode - timeout time.Duration - debugAddr string - skipLogs bool - skipHooks bool + editor fake.EditorConfig + sandbox fake.SandboxConfig + modes Mode + timeout time.Duration + debugAddr string + skipLogs bool + skipHooks bool + optionsHook func(*source.Options) } func (r *Runner) defaultConfig() *runConfig { return &runConfig{ - modes: r.DefaultModes, - timeout: r.Timeout, + modes: r.DefaultModes, + timeout: r.Timeout, + optionsHook: hooks.Options, } } @@ -118,6 +120,19 @@ func Modes(modes Mode) RunOption { }) } +// Options configures the various server and user options. +func Options(hook func(*source.Options)) RunOption { + return optionSetter(func(opts *runConfig) { + old := opts.optionsHook + opts.optionsHook = func(o *source.Options) { + if old != nil { + old(o) + } + hook(o) + } + }) +} + func SendPID() RunOption { return optionSetter(func(opts *runConfig) { opts.editor.SendPID = true @@ -216,7 +231,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio tests := []struct { name string mode Mode - getServer func(context.Context, *testing.T) jsonrpc2.StreamServer + getServer func(context.Context, *testing.T, func(*source.Options)) jsonrpc2.StreamServer }{ {"singleton", Singleton, singletonServer}, {"forwarded", Forwarded, r.forwardedServer}, @@ -268,7 +283,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio // better solution to ensure that all Go processes started by gopls have // exited before we clean up. r.AddCloser(sandbox) - ss := tc.getServer(ctx, t) + ss := tc.getServer(ctx, t, config.optionsHook) framer := jsonrpc2.NewRawStream ls := &loggingFramer{} if !config.skipLogs { @@ -367,38 +382,38 @@ func (s *loggingFramer) printBuffers(testname string, w io.Writer) { fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) } -func singletonServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer { - return lsprpc.NewStreamServer(cache.New(hooks.Options), false) +func singletonServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { + return lsprpc.NewStreamServer(cache.New(optsHook), false) } -func experimentalWorkspaceModule(_ context.Context, t *testing.T) jsonrpc2.StreamServer { +func experimentalWorkspaceModule(_ context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { options := func(o *source.Options) { - hooks.Options(o) + optsHook(o) o.ExperimentalWorkspaceModule = true } return lsprpc.NewStreamServer(cache.New(options), false) } -func (r *Runner) forwardedServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer { - ts := r.getTestServer() +func (r *Runner) forwardedServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { + ts := r.getTestServer(optsHook) return lsprpc.NewForwarder("tcp", ts.Addr) } // getTestServer gets the shared test server instance to connect to, or creates // one if it doesn't exist. -func (r *Runner) getTestServer() *servertest.TCPServer { +func (r *Runner) getTestServer(optsHook func(*source.Options)) *servertest.TCPServer { r.mu.Lock() defer r.mu.Unlock() if r.ts == nil { ctx := context.Background() ctx = debug.WithInstance(ctx, "", "off") - ss := lsprpc.NewStreamServer(cache.New(hooks.Options), false) + ss := lsprpc.NewStreamServer(cache.New(optsHook), false) r.ts = servertest.NewTCPServer(ctx, ss, nil) } return r.ts } -func (r *Runner) separateProcessServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer { +func (r *Runner) separateProcessServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { // TODO(rfindley): can we use the autostart behavior here, instead of // pre-starting the remote? socket := r.getRemoteSocket(t) diff --git a/internal/lsp/completion_test.go b/internal/lsp/completion_test.go index 59d7823544..d496a40a5c 100644 --- a/internal/lsp/completion_test.go +++ b/internal/lsp/completion_test.go @@ -21,7 +21,7 @@ func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, opts.CompleteUnimported = false opts.InsertTextFormat = protocol.SnippetTextFormat opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal") - opts.PostfixCompletions = strings.Contains(string(src.URI()), "postfix") + opts.ExperimentalPostfixCompletions = strings.Contains(string(src.URI()), "postfix") }) got = tests.FilterBuiltins(src, got) want := expected(t, test, items) @@ -100,7 +100,7 @@ func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completi opts.Matcher = source.Fuzzy opts.CompleteUnimported = false opts.LiteralCompletions = true - opts.PostfixCompletions = true + opts.ExperimentalPostfixCompletions = true }) want := expected(t, test, items) if msg := tests.CheckCompletionOrder(want, got, true); msg != "" { diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 5a91a59aec..a106c61a54 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -221,6 +221,19 @@ var GeneratedAPIJSON = &APIJSON{ Status: "advanced", Hierarchy: "ui.completion", }, + { + Name: "experimentalPostfixCompletions", + Type: "bool", + Doc: "experimentalPostfixCompletions enables artifical method snippets\nsuch as \"someSlice.sort!\".\n", + EnumKeys: EnumKeys{ + ValueType: "", + Keys: nil, + }, + EnumValues: nil, + Default: "false", + Status: "experimental", + Hierarchy: "ui.completion", + }, { Name: "importShortcut", Type: "enum", diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go index 0ed474f3d9..886408108f 100644 --- a/internal/lsp/source/completion/completion.go +++ b/internal/lsp/source/completion/completion.go @@ -522,7 +522,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan literal: opts.LiteralCompletions && opts.InsertTextFormat == protocol.SnippetTextFormat, budget: opts.CompletionBudget, snippets: opts.InsertTextFormat == protocol.SnippetTextFormat, - postfix: opts.PostfixCompletions, + postfix: opts.ExperimentalPostfixCompletions, }, // default to a matcher that always matches matcher: prefixMatcher(""), diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 49936a06af..826faa65b9 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -129,8 +129,9 @@ func DefaultOptions() *Options { SymbolStyle: DynamicSymbols, }, CompletionOptions: CompletionOptions{ - Matcher: Fuzzy, - CompletionBudget: 100 * time.Millisecond, + Matcher: Fuzzy, + CompletionBudget: 100 * time.Millisecond, + ExperimentalPostfixCompletions: false, }, Codelenses: map[string]bool{ string(command.Generate): true, @@ -144,7 +145,6 @@ func DefaultOptions() *Options { }, InternalOptions: InternalOptions{ LiteralCompletions: true, - PostfixCompletions: true, TempModfile: true, CompleteUnimported: true, CompletionDocumentation: true, @@ -294,6 +294,10 @@ type CompletionOptions struct { // Matcher sets the algorithm that is used when calculating completion // candidates. Matcher Matcher `status:"advanced"` + + // ExperimentalPostfixCompletions enables artifical method snippets + // such as "someSlice.sort!". + ExperimentalPostfixCompletions bool `status:"experimental"` } type DocumentationOptions struct { @@ -437,11 +441,6 @@ type InternalOptions struct { // their expected values. LiteralCompletions bool - // PostfixCompletions enables pseudo method snippets such as - // "someSlice.sort!". Tests disable this flag to simplify their - // expected values. - PostfixCompletions bool - // VerboseWorkDoneProgress controls whether the LSP server should send // progress reports for all work done outside the scope of an RPC. // Used by the regression tests. @@ -687,6 +686,7 @@ func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer) { // should be enabled in enableAllExperimentMaps. func (o *Options) enableAllExperiments() { o.SemanticTokens = true + o.ExperimentalPostfixCompletions = true } func (o *Options) enableAllExperimentMaps() { @@ -858,6 +858,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "expandWorkspaceToModule": result.setBool(&o.ExpandWorkspaceToModule) + case "experimentalPostfixCompletions": + result.setBool(&o.ExperimentalPostfixCompletions) + case "experimentalWorkspaceModule": result.setBool(&o.ExperimentalWorkspaceModule) diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index b08c570dcf..23df0c5d12 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -178,7 +178,7 @@ func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, opts.CompleteUnimported = false opts.InsertTextFormat = protocol.SnippetTextFormat opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal") - opts.PostfixCompletions = strings.Contains(string(src.URI()), "postfix") + opts.ExperimentalPostfixCompletions = strings.Contains(string(src.URI()), "postfix") }) got = tests.FilterBuiltins(src, got) if diff := tests.DiffCompletionItems(want, got); diff != "" { @@ -277,7 +277,7 @@ func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completi _, got := r.callCompletion(t, src, func(opts *source.Options) { opts.DeepCompletion = true opts.Matcher = source.Fuzzy - opts.PostfixCompletions = true + opts.ExperimentalPostfixCompletions = true }) if msg := tests.CheckCompletionOrder(want, got, true); msg != "" { t.Errorf("%s: %s", src, msg) @@ -931,6 +931,7 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa // These are pure LSP features, no source level functionality to be tested. func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {} + func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, expectedActions int) { } func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {}