internal/lsp: merge completion options into source.Options

This change flattens the completion options type into UserOptions and
DebuggingOptions, which will enable us to generate documentation for
these options more effectively. This results in some modifications in
the tests.

Additionally, the fuzzyMatching and caseSensitive boolean flags are
merged into one setting, matcher, which can be used to specify the type
of matcher that is used for completion. Other requests (notably
workspaceSymbols) may need to use a matcher in the future.

Change-Id: I185875e50351be4090c7a2b3340d40286dc9f4a0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/212635
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index 820e039..7edc184 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -24,7 +24,6 @@
 		return nil, err
 	}
 	snapshot := view.Snapshot()
-	options := view.Options()
 	fh, err := snapshot.GetFile(uri)
 	if err != nil {
 		return nil, err
@@ -33,8 +32,7 @@
 	var surrounding *source.Selection
 	switch fh.Identity().Kind {
 	case source.Go:
-		options.Completion.FullDocumentation = options.HoverKind == source.FullDocumentation
-		candidates, surrounding, err = source.Completion(ctx, snapshot, fh, params.Position, options.Completion)
+		candidates, surrounding, err = source.Completion(ctx, snapshot, fh, params.Position)
 	case source.Mod:
 		candidates, surrounding = nil, nil
 	}
@@ -62,7 +60,8 @@
 
 	// When using deep completions/fuzzy matching, report results as incomplete so
 	// client fetches updated completions after every key stroke.
-	incompleteResults := options.Completion.Deep || options.Completion.FuzzyMatching
+	options := view.Options()
+	incompleteResults := options.DeepCompletion || options.Matcher == source.Fuzzy
 
 	items := toProtocolCompletionItems(candidates, rng, options)
 
@@ -94,7 +93,7 @@
 		// Limit the number of deep completions to not overwhelm the user in cases
 		// with dozens of deep completion matches.
 		if candidate.Depth > 0 {
-			if !options.Completion.Deep {
+			if !options.DeepCompletion {
 				continue
 			}
 			if numDeepCompletionsSeen >= source.MaxDeepCompletions {
diff --git a/internal/lsp/completion_test.go b/internal/lsp/completion_test.go
index 846a54b..a8955e6 100644
--- a/internal/lsp/completion_test.go
+++ b/internal/lsp/completion_test.go
@@ -11,11 +11,10 @@
 )
 
 func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
-	got := r.callCompletion(t, src, source.CompletionOptions{
-		Deep:          false,
-		FuzzyMatching: false,
-		Documentation: true,
-		Literal:       strings.Contains(string(src.URI()), "literal"),
+	got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.DeepCompletion = false
+		opts.Matcher = source.CaseInsensitive
+		opts.Literal = strings.Contains(string(src.URI()), "literal")
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		got = tests.FilterBuiltins(got)
@@ -27,11 +26,11 @@
 }
 
 func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) {
-	list := r.callCompletion(t, src, source.CompletionOptions{
-		Placeholders:  placeholders,
-		Deep:          true,
-		FuzzyMatching: true,
-		Literal:       true,
+	list := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.Placeholders = placeholders
+		opts.DeepCompletion = true
+		opts.Matcher = source.Fuzzy
+		opts.Literal = true
 	})
 	got := tests.FindItem(list, *items[expected.CompletionItem])
 	want := expected.PlainSnippet
@@ -44,8 +43,8 @@
 }
 
 func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
-	got := r.callCompletion(t, src, source.CompletionOptions{
-		Unimported: true,
+	got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.UnimportedCompletion = true
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		got = tests.FilterBuiltins(got)
@@ -57,9 +56,9 @@
 }
 
 func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
-	got := r.callCompletion(t, src, source.CompletionOptions{
-		Deep:          true,
-		Documentation: true,
+	got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.DeepCompletion = true
+		opts.Matcher = source.CaseInsensitive
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		got = tests.FilterBuiltins(got)
@@ -71,9 +70,9 @@
 }
 
 func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
-	got := r.callCompletion(t, src, source.CompletionOptions{
-		FuzzyMatching: true,
-		Deep:          true,
+	got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.DeepCompletion = true
+		opts.Matcher = source.Fuzzy
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		got = tests.FilterBuiltins(got)
@@ -85,8 +84,8 @@
 }
 
 func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
-	got := r.callCompletion(t, src, source.CompletionOptions{
-		CaseSensitive: true,
+	got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.Matcher = source.CaseSensitive
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		got = tests.FilterBuiltins(got)
@@ -98,10 +97,10 @@
 }
 
 func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
-	got := r.callCompletion(t, src, source.CompletionOptions{
-		FuzzyMatching: true,
-		Deep:          true,
-		Literal:       true,
+	got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.DeepCompletion = true
+		opts.Matcher = source.Fuzzy
+		opts.Literal = true
 	})
 	want := expected(t, test, items)
 	if msg := tests.CheckCompletionOrder(want, got, true); msg != "" {
@@ -120,7 +119,7 @@
 	return want
 }
 
-func (r *runner) callCompletion(t *testing.T, src span.Span, options source.CompletionOptions) []protocol.CompletionItem {
+func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*source.Options)) []protocol.CompletionItem {
 	t.Helper()
 
 	view, err := r.server.session.ViewOf(src.URI())
@@ -129,8 +128,7 @@
 	}
 	original := view.Options()
 	modified := original
-	modified.InsertTextFormat = protocol.SnippetTextFormat
-	modified.Completion = options
+	options(&modified)
 	view, err = view.SetOptions(r.ctx, modified)
 	if err != nil {
 		t.Error(err)
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 30f3d6e..4f212a1 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -131,9 +131,8 @@
 type completer struct {
 	snapshot Snapshot
 	pkg      Package
-
-	qf   types.Qualifier
-	opts CompletionOptions
+	qf       types.Qualifier
+	opts     *completionOptions
 
 	// ctx is the context associated with this completion request.
 	ctx context.Context
@@ -247,10 +246,10 @@
 }
 
 func (c *completer) deepCompletionContext() (context.Context, context.CancelFunc) {
-	if c.opts.Budget == 0 {
+	if c.opts.budget == 0 {
 		return context.WithCancel(c.ctx)
 	}
-	return context.WithDeadline(c.ctx, c.startTime.Add(c.opts.Budget))
+	return context.WithDeadline(c.ctx, c.startTime.Add(c.opts.budget))
 }
 
 func (c *completer) setSurrounding(ident *ast.Ident) {
@@ -268,11 +267,12 @@
 		mappedRange: newMappedRange(c.snapshot.View().Session().Cache().FileSet(), c.mapper, ident.Pos(), ident.End()),
 	}
 
-	if c.opts.FuzzyMatching {
+	switch c.opts.matcher {
+	case Fuzzy:
 		c.matcher = fuzzy.NewMatcher(c.surrounding.Prefix())
-	} else if c.opts.CaseSensitive {
+	case CaseSensitive:
 		c.matcher = prefixMatcher(c.surrounding.Prefix())
-	} else {
+	default:
 		c.matcher = insensitivePrefixMatcher(strings.ToLower(c.surrounding.Prefix()))
 	}
 }
@@ -405,7 +405,7 @@
 // The selection is computed based on the preceding identifier and can be used by
 // the client to score the quality of the completion. For instance, some clients
 // may tolerate imperfect matches as valid completion results, since users may make typos.
-func Completion(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position, opts CompletionOptions) ([]CompletionItem, *Selection, error) {
+func Completion(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]CompletionItem, *Selection, error) {
 	ctx, done := trace.StartSpan(ctx, "source.Completion")
 	defer done()
 
@@ -439,6 +439,7 @@
 		return nil, nil, nil
 	}
 
+	opts := snapshot.View().Options()
 	c := &completer{
 		pkg:                       pkg,
 		snapshot:                  snapshot,
@@ -451,7 +452,16 @@
 		seen:                      make(map[types.Object]bool),
 		enclosingFunc:             enclosingFunction(path, rng.Start, pkg.GetTypesInfo()),
 		enclosingCompositeLiteral: enclosingCompositeLiteral(path, rng.Start, pkg.GetTypesInfo()),
-		opts:                      opts,
+		opts: &completionOptions{
+			matcher:           opts.Matcher,
+			deepCompletion:    opts.DeepCompletion,
+			unimported:        opts.UnimportedCompletion,
+			documentation:     opts.CompletionDocumentation,
+			fullDocumentation: opts.HoverKind == FullDocumentation,
+			placeholders:      opts.Placeholders,
+			literal:           opts.Literal,
+			budget:            opts.CompletionBudget,
+		},
 		// default to a matcher that always matches
 		matcher:        prefixMatcher(""),
 		methodSetCache: make(map[methodSetKey]*types.MethodSet),
@@ -459,7 +469,7 @@
 		startTime:      startTime,
 	}
 
-	if opts.Deep {
+	if c.opts.deepCompletion {
 		// Initialize max search depth to unlimited.
 		c.deepState.maxDepth = -1
 	}
@@ -629,7 +639,7 @@
 	}
 
 	// Try unimported packages.
-	if id, ok := sel.X.(*ast.Ident); ok && c.opts.Unimported && len(c.items) < unimportedTarget {
+	if id, ok := sel.X.(*ast.Ident); ok && c.opts.unimported && len(c.items) < unimportedTarget {
 		if err := c.unimportedMembers(id); err != nil {
 			return err
 		}
@@ -862,7 +872,7 @@
 		}
 	}
 
-	if c.opts.Unimported && len(c.items) < unimportedTarget {
+	if c.opts.unimported && len(c.items) < unimportedTarget {
 		ctx, cancel := c.deepCompletionContext()
 		defer cancel()
 		// Suggest packages that have not been imported yet.
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index 77713dd..168308f 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -168,7 +168,7 @@
 		snippet:             snip,
 	}
 	// If the user doesn't want documentation for completion items.
-	if !c.opts.Documentation {
+	if !c.opts.documentation {
 		return item, nil
 	}
 	pos := c.snapshot.View().Session().Cache().FileSet().Position(obj.Pos())
@@ -200,7 +200,7 @@
 		return item, nil
 	}
 	item.Documentation = hover.Synopsis
-	if c.opts.FullDocumentation {
+	if c.opts.fullDocumentation {
 		item.Documentation = hover.FullDocumentation
 	}
 	return item, nil
diff --git a/internal/lsp/source/completion_literal.go b/internal/lsp/source/completion_literal.go
index 287d43d..89ecda6 100644
--- a/internal/lsp/source/completion_literal.go
+++ b/internal/lsp/source/completion_literal.go
@@ -20,7 +20,7 @@
 // literal generates composite literal, function literal, and make()
 // completion items.
 func (c *completer) literal(literalType types.Type, imp *importInfo) {
-	if !c.opts.Literal {
+	if !c.opts.literal {
 		return
 	}
 
@@ -213,7 +213,7 @@
 			// Our parameter names are guesses, so they must be placeholders
 			// for easy correction. If placeholders are disabled, don't
 			// offer the completion.
-			if !c.opts.Placeholders {
+			if !c.opts.placeholders {
 				return
 			}
 
@@ -367,7 +367,7 @@
 	if secondArg != "" {
 		snip.WriteText(", ")
 		snip.WritePlaceholder(func(b *snippet.Builder) {
-			if c.opts.Placeholders {
+			if c.opts.placeholders {
 				b.WriteText(secondArg)
 			}
 		})
diff --git a/internal/lsp/source/completion_snippet.go b/internal/lsp/source/completion_snippet.go
index 5678256..6aaf28d 100644
--- a/internal/lsp/source/completion_snippet.go
+++ b/internal/lsp/source/completion_snippet.go
@@ -36,7 +36,7 @@
 	snip.WriteText(label + ": ")
 	snip.WritePlaceholder(func(b *snippet.Builder) {
 		// A placeholder snippet turns "Foo{Ba<>" into "Foo{Bar: <*int*>".
-		if c.opts.Placeholders {
+		if c.opts.placeholders {
 			b.WriteText(detail)
 		}
 	})
@@ -79,7 +79,7 @@
 	snip := &snippet.Builder{}
 	snip.WriteText(name + "(")
 
-	if c.opts.Placeholders {
+	if c.opts.placeholders {
 		// A placeholder snippet turns "someFun<>" into "someFunc(<*i int*>, *s string*)".
 		for i, p := range params {
 			if i > 0 {
diff --git a/internal/lsp/source/deep_completion.go b/internal/lsp/source/deep_completion.go
index 24643d8..74f2bbf 100644
--- a/internal/lsp/source/deep_completion.go
+++ b/internal/lsp/source/deep_completion.go
@@ -107,8 +107,8 @@
 	}
 
 	// Check our remaining budget every 100 candidates.
-	if c.opts.Budget > 0 && c.deepState.candidateCount%100 == 0 {
-		spent := float64(time.Since(c.startTime)) / float64(c.opts.Budget)
+	if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 {
+		spent := float64(time.Since(c.startTime)) / float64(c.opts.budget)
 
 		switch {
 		case spent >= 0.90:
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index e339821..a2eeb0b 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -74,16 +74,13 @@
 		},
 	}
 	DefaultUserOptions = UserOptions{
-		Env:       os.Environ(),
-		HoverKind: SynopsisDocumentation,
-		Completion: CompletionOptions{
-			Documentation: true,
-			Deep:          true,
-			FuzzyMatching: true,
-			Literal:       true,
-			Budget:        100 * time.Millisecond,
-		},
-		LinkTarget: "pkg.go.dev",
+		Env:                     os.Environ(),
+		HoverKind:               SynopsisDocumentation,
+		LinkTarget:              "pkg.go.dev",
+		Matcher:                 Fuzzy,
+		DeepCompletion:          true,
+		CompletionDocumentation: true,
+		Literal:                 true,
 	}
 	DefaultHooks = Hooks{
 		ComputeEdits: myers.ComputeEdits,
@@ -94,7 +91,9 @@
 	DefaultExperimentalOptions = ExperimentalOptions{
 		TempModfile: false,
 	}
-	DefaultDebuggingOptions = DebuggingOptions{}
+	DefaultDebuggingOptions = DebuggingOptions{
+		CompletionBudget: 100 * time.Millisecond,
+	}
 )
 
 type Options struct {
@@ -142,7 +141,37 @@
 	// LocalPrefix is used to specify goimports's -local behavior.
 	LocalPrefix string
 
-	Completion CompletionOptions
+	// Matcher specifies the type of matcher to use for completion requests.
+	Matcher Matcher
+
+	// DeepCompletion allows completion to perform nested searches through
+	// possible candidates.
+	DeepCompletion bool
+
+	// UnimportedCompletion enables completion for unimported packages.
+	UnimportedCompletion bool
+
+	// CompletionDocumentation returns additional documentation with completion
+	// requests.
+	CompletionDocumentation bool
+
+	// Placeholders adds placeholders to parameters and structs in completion
+	// results.
+	Placeholders bool
+
+	// Literal enables completion for map, slice, and function literals.
+	Literal bool
+}
+
+type completionOptions struct {
+	deepCompletion    bool
+	unimported        bool
+	documentation     bool
+	fullDocumentation bool
+	placeholders      bool
+	literal           bool
+	matcher           Matcher
+	budget            time.Duration
 }
 
 type Hooks struct {
@@ -161,26 +190,23 @@
 
 type DebuggingOptions struct {
 	VerboseOutput bool
-}
 
-type CompletionOptions struct {
-	Deep              bool
-	FuzzyMatching     bool
-	CaseSensitive     bool
-	Unimported        bool
-	Documentation     bool
-	FullDocumentation bool
-	Placeholders      bool
-	Literal           bool
-
-	// Budget is the soft latency goal for completion requests. Most
+	// CompletionBudget is the soft latency goal for completion requests. Most
 	// requests finish in a couple milliseconds, but in some cases deep
 	// completions can take much longer. As we use up our budget we
 	// dynamically reduce the search scope to ensure we return timely
 	// results. Zero means unlimited.
-	Budget time.Duration
+	CompletionBudget time.Duration
 }
 
+type Matcher int
+
+const (
+	Fuzzy = Matcher(iota)
+	CaseInsensitive
+	CaseSensitive
+)
+
 type HoverKind int
 
 const (
@@ -280,17 +306,13 @@
 		o.BuildFlags = flags
 
 	case "completionDocumentation":
-		result.setBool(&o.Completion.Documentation)
+		result.setBool(&o.CompletionDocumentation)
 	case "usePlaceholders":
-		result.setBool(&o.Completion.Placeholders)
+		result.setBool(&o.Placeholders)
 	case "deepCompletion":
-		result.setBool(&o.Completion.Deep)
-	case "fuzzyMatching":
-		result.setBool(&o.Completion.FuzzyMatching)
-	case "caseSensitiveCompletion":
-		result.setBool(&o.Completion.CaseSensitive)
+		result.setBool(&o.DeepCompletion)
 	case "completeUnimported":
-		result.setBool(&o.Completion.Unimported)
+		result.setBool(&o.UnimportedCompletion)
 	case "completionBudget":
 		if v, ok := result.asString(); ok {
 			d, err := time.ParseDuration(v)
@@ -298,13 +320,26 @@
 				result.errorf("failed to parse duration %q: %v", v, err)
 				break
 			}
-			o.Completion.Budget = d
+			o.CompletionBudget = d
+		}
+
+	case "matcher":
+		matcher, ok := result.asString()
+		if !ok {
+			break
+		}
+		switch matcher {
+		case "fuzzy":
+			o.Matcher = Fuzzy
+		case "caseSensitive":
+			o.Matcher = CaseSensitive
+		default:
+			o.Matcher = CaseInsensitive
 		}
 
 	case "hoverKind":
-		hoverKind, ok := value.(string)
+		hoverKind, ok := result.asString()
 		if !ok {
-			result.errorf("invalid type %T for string option %q", value, name)
 			break
 		}
 		switch hoverKind {
@@ -378,6 +413,14 @@
 		result.State = OptionDeprecated
 		result.Replacement = "completeUnimported"
 
+	case "fuzzyMatching":
+		result.State = OptionDeprecated
+		result.Replacement = "matcher"
+
+	case "caseSensitiveCompletion":
+		result.State = OptionDeprecated
+		result.Replacement = "matcher"
+
 	case "noIncrementalSync":
 		result.State = OptionDeprecated
 
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index defab5e..f6d36e9 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -107,10 +107,10 @@
 	for _, pos := range test.CompletionItems {
 		want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
 	}
-	prefix, list := r.callCompletion(t, src, source.CompletionOptions{
-		Documentation: true,
-		FuzzyMatching: true,
-		Literal:       strings.Contains(string(src.URI()), "literal"),
+	prefix, list := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.Matcher = source.Fuzzy
+		opts.Literal = strings.Contains(string(src.URI()), "literal")
+		opts.DeepCompletion = false
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		list = tests.FilterBuiltins(list)
@@ -128,10 +128,10 @@
 }
 
 func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) {
-	_, list := r.callCompletion(t, src, source.CompletionOptions{
-		Placeholders: placeholders,
-		Deep:         true,
-		Literal:      true,
+	_, list := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.Placeholders = placeholders
+		opts.DeepCompletion = true
+		opts.Literal = true
 	})
 	got := tests.FindItem(list, *items[expected.CompletionItem])
 	want := expected.PlainSnippet
@@ -148,8 +148,8 @@
 	for _, pos := range test.CompletionItems {
 		want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
 	}
-	_, got := r.callCompletion(t, src, source.CompletionOptions{
-		Unimported: true,
+	_, got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.UnimportedCompletion = true
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		got = tests.FilterBuiltins(got)
@@ -164,9 +164,9 @@
 	for _, pos := range test.CompletionItems {
 		want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
 	}
-	prefix, list := r.callCompletion(t, src, source.CompletionOptions{
-		Deep:          true,
-		Documentation: true,
+	prefix, list := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.DeepCompletion = true
+		opts.Matcher = source.CaseInsensitive
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		list = tests.FilterBuiltins(list)
@@ -189,9 +189,9 @@
 	for _, pos := range test.CompletionItems {
 		want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
 	}
-	_, got := r.callCompletion(t, src, source.CompletionOptions{
-		FuzzyMatching: true,
-		Deep:          true,
+	_, got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.DeepCompletion = true
+		opts.Matcher = source.Fuzzy
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		got = tests.FilterBuiltins(got)
@@ -206,8 +206,8 @@
 	for _, pos := range test.CompletionItems {
 		want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
 	}
-	_, list := r.callCompletion(t, src, source.CompletionOptions{
-		CaseSensitive: true,
+	_, list := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.Matcher = source.CaseSensitive
 	})
 	if !strings.Contains(string(src.URI()), "builtins") {
 		list = tests.FilterBuiltins(list)
@@ -222,25 +222,34 @@
 	for _, pos := range test.CompletionItems {
 		want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
 	}
-	_, got := r.callCompletion(t, src, source.CompletionOptions{
-		FuzzyMatching: true,
-		Deep:          true,
-		Literal:       true,
+	_, got := r.callCompletion(t, src, func(opts *source.Options) {
+		opts.DeepCompletion = true
+		opts.Matcher = source.Fuzzy
+		opts.Literal = true
 	})
 	if msg := tests.CheckCompletionOrder(want, got, true); msg != "" {
 		t.Errorf("%s: %s", src, msg)
 	}
 }
 
-func (r *runner) callCompletion(t *testing.T, src span.Span, options source.CompletionOptions) (string, []protocol.CompletionItem) {
+func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*source.Options)) (string, []protocol.CompletionItem) {
 	fh, err := r.view.Snapshot().GetFile(src.URI())
 	if err != nil {
 		t.Fatal(err)
 	}
-	list, surrounding, err := source.Completion(r.ctx, r.view.Snapshot(), fh, protocol.Position{
+	original := r.view.Options()
+	modified := original
+	options(&modified)
+	view, err := r.view.SetOptions(r.ctx, modified)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer r.view.SetOptions(r.ctx, original)
+
+	list, surrounding, err := source.Completion(r.ctx, view.Snapshot(), fh, protocol.Position{
 		Line:      float64(src.Start().Line() - 1),
 		Character: float64(src.Start().Column() - 1),
-	}, options)
+	})
 	if err != nil && !errors.As(err, &source.ErrIsDefinition{}) {
 		t.Fatalf("failed for %v: %v", src, err)
 	}
@@ -261,7 +270,7 @@
 	// Apply deep completion filtering.
 	for _, item := range list {
 		if item.Depth > 0 {
-			if !options.Deep {
+			if !modified.DeepCompletion {
 				continue
 			}
 			if numDeepCompletionsSeen >= source.MaxDeepCompletions {
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 3b9d129..49608d3 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -191,7 +191,7 @@
 	}
 	o.HoverKind = source.SynopsisDocumentation
 	o.InsertTextFormat = protocol.SnippetTextFormat
-	o.Completion.Budget = time.Minute
+	o.CompletionBudget = time.Minute
 	return o
 }