internal/lsp: provide option for case sensitive completion

In CL 192137 deep fuzzy matching was enabled by default. We also have
options independent options "deepCompletion" and "fuzzyMatching" to
control this. When fuzzy matching is disabled, case insensitive prefix
matching is used.

Provide an option, "caseSensitiveCompletion", which allows for case
sensitive prefix matching when fuzzy matching is disabled.

Change-Id: I17c8fa310b2ef79e36cc2f7303e98870690b5903
Reviewed-on: https://go-review.googlesource.com/c/tools/+/194757
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go
index d31dfe4..c04bb91 100644
--- a/internal/lsp/cmd/test/cmdtest.go
+++ b/internal/lsp/cmd/test/cmdtest.go
@@ -53,6 +53,10 @@
 	//TODO: add command line completions tests when it works
 }
 
+func (r *runner) CaseSensitiveCompletions(t *testing.T, data tests.CaseSensitiveCompletions, items tests.CompletionItems) {
+	//TODO: add command line completions tests when it works
+}
+
 func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
 	//TODO: add command line completions tests when it works
 }
diff --git a/internal/lsp/completion_test.go b/internal/lsp/completion_test.go
index be42932..d786218 100644
--- a/internal/lsp/completion_test.go
+++ b/internal/lsp/completion_test.go
@@ -98,6 +98,21 @@
 	}
 }
 
+func (r *runner) CaseSensitiveCompletions(t *testing.T, data tests.CaseSensitiveCompletions, items tests.CompletionItems) {
+	for src, test := range data {
+		got := r.callCompletion(t, src, source.CompletionOptions{
+			CaseSensitive: true,
+		})
+		if !strings.Contains(string(src.URI()), "builtins") {
+			got = tests.FilterBuiltins(got)
+		}
+		want := expected(t, test, items)
+		if msg := tests.DiffCompletionItems(want, got); msg != "" {
+			t.Errorf("%s: %s", src, msg)
+		}
+	}
+}
+
 func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
 	for src, test := range data {
 		got := r.callCompletion(t, src, source.CompletionOptions{
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 8fa06c3..12d036e 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -104,11 +104,21 @@
 	Score(candidateLabel string) (score float32)
 }
 
-// prefixMatcher implements case insensitive prefix matching.
+// prefixMatcher implements case sensitive prefix matching.
 type prefixMatcher string
 
 func (pm prefixMatcher) Score(candidateLabel string) float32 {
-	if strings.HasPrefix(strings.ToLower(candidateLabel), string(pm)) {
+	if strings.HasPrefix(candidateLabel, string(pm)) {
+		return 1
+	}
+	return -1
+}
+
+// insensitivePrefixMatcher implements case insensitive prefix matching.
+type insensitivePrefixMatcher string
+
+func (ipm insensitivePrefixMatcher) Score(candidateLabel string) float32 {
+	if strings.HasPrefix(strings.ToLower(candidateLabel), string(ipm)) {
 		return 1
 	}
 	return -1
@@ -233,8 +243,10 @@
 
 	if c.opts.FuzzyMatching {
 		c.matcher = fuzzy.NewMatcher(c.surrounding.Prefix(), fuzzy.Symbol)
+	} else if c.opts.CaseSensitive {
+		c.matcher = prefixMatcher(c.surrounding.Prefix())
 	} else {
-		c.matcher = prefixMatcher(strings.ToLower(c.surrounding.Prefix()))
+		c.matcher = insensitivePrefixMatcher(strings.ToLower(c.surrounding.Prefix()))
 	}
 }
 
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 7d35fa6..d3c95cd 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -76,6 +76,7 @@
 type CompletionOptions struct {
 	Deep              bool
 	FuzzyMatching     bool
+	CaseSensitive     bool
 	Unimported        bool
 	Documentation     bool
 	FullDocumentation bool
@@ -200,6 +201,8 @@
 		result.setBool(&o.Completion.Deep)
 	case "fuzzyMatching":
 		result.setBool(&o.Completion.FuzzyMatching)
+	case "caseSensitiveCompletion":
+		result.setBool(&o.Completion.CaseSensitive)
 	case "completeUnimported":
 		result.setBool(&o.Completion.Unimported)
 
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index a91a28f..d4058e9 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -210,6 +210,24 @@
 	}
 }
 
+func (r *runner) CaseSensitiveCompletions(t *testing.T, data tests.CaseSensitiveCompletions, items tests.CompletionItems) {
+	for src, test := range data {
+		var want []protocol.CompletionItem
+		for _, pos := range test.CompletionItems {
+			want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
+		}
+		_, list := r.callCompletion(t, src, source.CompletionOptions{
+			CaseSensitive: true,
+		})
+		if !strings.Contains(string(src.URI()), "builtins") {
+			list = tests.FilterBuiltins(list)
+		}
+		if diff := tests.DiffCompletionItems(want, list); diff != "" {
+			t.Errorf("%s: %s", src, diff)
+		}
+	}
+}
+
 func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
 	for src, test := range data {
 		var want []protocol.CompletionItem
diff --git a/internal/lsp/testdata/casesensitive/casesensitive.go b/internal/lsp/testdata/casesensitive/casesensitive.go
new file mode 100644
index 0000000..6f49d36
--- /dev/null
+++ b/internal/lsp/testdata/casesensitive/casesensitive.go
@@ -0,0 +1,16 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package casesensitive
+
+func _() {
+	var lower int //@item(lower, "lower", "int", "var")
+	var Upper int //@item(upper, "Upper", "int", "var")
+
+	l //@casesensitive(" //", lower)
+	U //@casesensitive(" //", upper)
+
+	L //@casesensitive(" //")
+	u //@casesensitive(" //")
+}
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 6e647ab..aa08245 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -29,26 +29,27 @@
 // We hardcode the expected number of test cases to ensure that all tests
 // are being executed. If a test is added, this number must be changed.
 const (
-	ExpectedCompletionsCount           = 169
-	ExpectedCompletionSnippetCount     = 36
-	ExpectedUnimportedCompletionsCount = 1
-	ExpectedDeepCompletionsCount       = 5
-	ExpectedFuzzyCompletionsCount      = 6
-	ExpectedRankedCompletionsCount     = 1
-	ExpectedDiagnosticsCount           = 21
-	ExpectedFormatCount                = 6
-	ExpectedImportCount                = 2
-	ExpectedSuggestedFixCount          = 1
-	ExpectedDefinitionsCount           = 39
-	ExpectedTypeDefinitionsCount       = 2
-	ExpectedFoldingRangesCount         = 2
-	ExpectedHighlightsCount            = 2
-	ExpectedReferencesCount            = 6
-	ExpectedRenamesCount               = 20
-	ExpectedPrepareRenamesCount        = 8
-	ExpectedSymbolsCount               = 1
-	ExpectedSignaturesCount            = 21
-	ExpectedLinksCount                 = 4
+	ExpectedCompletionsCount              = 169
+	ExpectedCompletionSnippetCount        = 36
+	ExpectedUnimportedCompletionsCount    = 1
+	ExpectedDeepCompletionsCount          = 5
+	ExpectedFuzzyCompletionsCount         = 6
+	ExpectedCaseSensitiveCompletionsCount = 4
+	ExpectedRankedCompletionsCount        = 1
+	ExpectedDiagnosticsCount              = 21
+	ExpectedFormatCount                   = 6
+	ExpectedImportCount                   = 2
+	ExpectedSuggestedFixCount             = 1
+	ExpectedDefinitionsCount              = 39
+	ExpectedTypeDefinitionsCount          = 2
+	ExpectedFoldingRangesCount            = 2
+	ExpectedHighlightsCount               = 2
+	ExpectedReferencesCount               = 6
+	ExpectedRenamesCount                  = 20
+	ExpectedPrepareRenamesCount           = 8
+	ExpectedSymbolsCount                  = 1
+	ExpectedSignaturesCount               = 21
+	ExpectedLinksCount                    = 4
 )
 
 const (
@@ -67,6 +68,7 @@
 type UnimportedCompletions map[span.Span]Completion
 type DeepCompletions map[span.Span]Completion
 type FuzzyCompletions map[span.Span]Completion
+type CaseSensitiveCompletions map[span.Span]Completion
 type RankCompletions map[span.Span]Completion
 type FoldingRanges []span.Span
 type Formats []span.Span
@@ -83,29 +85,30 @@
 type Links map[span.URI][]Link
 
 type Data struct {
-	Config                packages.Config
-	Exported              *packagestest.Exported
-	Diagnostics           Diagnostics
-	CompletionItems       CompletionItems
-	Completions           Completions
-	CompletionSnippets    CompletionSnippets
-	UnimportedCompletions UnimportedCompletions
-	DeepCompletions       DeepCompletions
-	FuzzyCompletions      FuzzyCompletions
-	RankCompletions       RankCompletions
-	FoldingRanges         FoldingRanges
-	Formats               Formats
-	Imports               Imports
-	SuggestedFixes        SuggestedFixes
-	Definitions           Definitions
-	Highlights            Highlights
-	References            References
-	Renames               Renames
-	PrepareRenames        PrepareRenames
-	Symbols               Symbols
-	symbolsChildren       SymbolsChildren
-	Signatures            Signatures
-	Links                 Links
+	Config                   packages.Config
+	Exported                 *packagestest.Exported
+	Diagnostics              Diagnostics
+	CompletionItems          CompletionItems
+	Completions              Completions
+	CompletionSnippets       CompletionSnippets
+	UnimportedCompletions    UnimportedCompletions
+	DeepCompletions          DeepCompletions
+	FuzzyCompletions         FuzzyCompletions
+	CaseSensitiveCompletions CaseSensitiveCompletions
+	RankCompletions          RankCompletions
+	FoldingRanges            FoldingRanges
+	Formats                  Formats
+	Imports                  Imports
+	SuggestedFixes           SuggestedFixes
+	Definitions              Definitions
+	Highlights               Highlights
+	References               References
+	Renames                  Renames
+	PrepareRenames           PrepareRenames
+	Symbols                  Symbols
+	symbolsChildren          SymbolsChildren
+	Signatures               Signatures
+	Links                    Links
 
 	t         testing.TB
 	fragments map[string]string
@@ -123,6 +126,7 @@
 	UnimportedCompletions(*testing.T, UnimportedCompletions, CompletionItems)
 	DeepCompletions(*testing.T, DeepCompletions, CompletionItems)
 	FuzzyCompletions(*testing.T, FuzzyCompletions, CompletionItems)
+	CaseSensitiveCompletions(*testing.T, CaseSensitiveCompletions, CompletionItems)
 	RankCompletions(*testing.T, RankCompletions, CompletionItems)
 	FoldingRange(*testing.T, FoldingRanges)
 	Format(*testing.T, Formats)
@@ -160,6 +164,9 @@
 	// Fuzzy tests deep completion and fuzzy matching.
 	CompletionFuzzy
 
+	// CaseSensitive tests case sensitive completion
+	CompletionCaseSensitve
+
 	// CompletionRank candidates in test must be valid and in the right relative order.
 	CompletionRank
 )
@@ -209,23 +216,24 @@
 	t.Helper()
 
 	data := &Data{
-		Diagnostics:           make(Diagnostics),
-		CompletionItems:       make(CompletionItems),
-		Completions:           make(Completions),
-		CompletionSnippets:    make(CompletionSnippets),
-		UnimportedCompletions: make(UnimportedCompletions),
-		DeepCompletions:       make(DeepCompletions),
-		FuzzyCompletions:      make(FuzzyCompletions),
-		RankCompletions:       make(RankCompletions),
-		Definitions:           make(Definitions),
-		Highlights:            make(Highlights),
-		References:            make(References),
-		Renames:               make(Renames),
-		PrepareRenames:        make(PrepareRenames),
-		Symbols:               make(Symbols),
-		symbolsChildren:       make(SymbolsChildren),
-		Signatures:            make(Signatures),
-		Links:                 make(Links),
+		Diagnostics:              make(Diagnostics),
+		CompletionItems:          make(CompletionItems),
+		Completions:              make(Completions),
+		CompletionSnippets:       make(CompletionSnippets),
+		UnimportedCompletions:    make(UnimportedCompletions),
+		DeepCompletions:          make(DeepCompletions),
+		FuzzyCompletions:         make(FuzzyCompletions),
+		RankCompletions:          make(RankCompletions),
+		CaseSensitiveCompletions: make(CaseSensitiveCompletions),
+		Definitions:              make(Definitions),
+		Highlights:               make(Highlights),
+		References:               make(References),
+		Renames:                  make(Renames),
+		PrepareRenames:           make(PrepareRenames),
+		Symbols:                  make(Symbols),
+		symbolsChildren:          make(SymbolsChildren),
+		Signatures:               make(Signatures),
+		Links:                    make(Links),
 
 		t:         t,
 		dir:       dir,
@@ -295,28 +303,29 @@
 
 	// Collect any data that needs to be used by subsequent tests.
 	if err := data.Exported.Expect(map[string]interface{}{
-		"diag":         data.collectDiagnostics,
-		"item":         data.collectCompletionItems,
-		"complete":     data.collectCompletions(CompletionDefault),
-		"unimported":   data.collectCompletions(CompletionUnimported),
-		"deep":         data.collectCompletions(CompletionDeep),
-		"fuzzy":        data.collectCompletions(CompletionFuzzy),
-		"rank":         data.collectCompletions(CompletionRank),
-		"snippet":      data.collectCompletionSnippets,
-		"fold":         data.collectFoldingRanges,
-		"format":       data.collectFormats,
-		"import":       data.collectImports,
-		"godef":        data.collectDefinitions,
-		"typdef":       data.collectTypeDefinitions,
-		"hover":        data.collectHoverDefinitions,
-		"highlight":    data.collectHighlights,
-		"refs":         data.collectReferences,
-		"rename":       data.collectRenames,
-		"prepare":      data.collectPrepareRenames,
-		"symbol":       data.collectSymbols,
-		"signature":    data.collectSignatures,
-		"link":         data.collectLinks,
-		"suggestedfix": data.collectSuggestedFixes,
+		"diag":          data.collectDiagnostics,
+		"item":          data.collectCompletionItems,
+		"complete":      data.collectCompletions(CompletionDefault),
+		"unimported":    data.collectCompletions(CompletionUnimported),
+		"deep":          data.collectCompletions(CompletionDeep),
+		"fuzzy":         data.collectCompletions(CompletionFuzzy),
+		"casesensitive": data.collectCompletions(CompletionCaseSensitve),
+		"rank":          data.collectCompletions(CompletionRank),
+		"snippet":       data.collectCompletionSnippets,
+		"fold":          data.collectFoldingRanges,
+		"format":        data.collectFormats,
+		"import":        data.collectImports,
+		"godef":         data.collectDefinitions,
+		"typdef":        data.collectTypeDefinitions,
+		"hover":         data.collectHoverDefinitions,
+		"highlight":     data.collectHighlights,
+		"refs":          data.collectReferences,
+		"rename":        data.collectRenames,
+		"prepare":       data.collectPrepareRenames,
+		"symbol":        data.collectSymbols,
+		"signature":     data.collectSignatures,
+		"link":          data.collectLinks,
+		"suggestedfix":  data.collectSuggestedFixes,
 	}); err != nil {
 		t.Fatal(err)
 	}
@@ -382,6 +391,14 @@
 		tests.FuzzyCompletions(t, data.FuzzyCompletions, data.CompletionItems)
 	})
 
+	t.Run("CaseSensitiveCompletion", func(t *testing.T) {
+		t.Helper()
+		if len(data.CaseSensitiveCompletions) != ExpectedCaseSensitiveCompletionsCount {
+			t.Errorf("got %v case sensitive completions expected %v", len(data.CaseSensitiveCompletions), ExpectedCaseSensitiveCompletionsCount)
+		}
+		tests.CaseSensitiveCompletions(t, data.CaseSensitiveCompletions, data.CompletionItems)
+	})
+
 	t.Run("RankCompletions", func(t *testing.T) {
 		t.Helper()
 		if len(data.RankCompletions) != ExpectedRankedCompletionsCount {
@@ -638,6 +655,10 @@
 		return func(src span.Span, expected []token.Pos) {
 			result(data.RankCompletions, src, expected)
 		}
+	case CompletionCaseSensitve:
+		return func(src span.Span, expected []token.Pos) {
+			result(data.CaseSensitiveCompletions, src, expected)
+		}
 	default:
 		return func(src span.Span, expected []token.Pos) {
 			result(data.Completions, src, expected)