internal/lsp: add support for document highlight

Change-Id: I232dbb0b66d690e45079808fd0dbf026c4459400
Reviewed-on: https://go-review.googlesource.com/c/tools/+/169277
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go
index 925e417..3a1fa3f 100644
--- a/go/packages/packagestest/expect.go
+++ b/go/packages/packagestest/expect.go
@@ -52,6 +52,7 @@
 //   *regexp.Regexp : can only be supplied a regular expression literal
 //   token.Pos : has a file position calculated as described below.
 //   token.Position : has a file position calculated as described below.
+//   expect.Range: has a start and end position as described below.
 //   interface{} : will be passed any value
 //
 // Position calculation
diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go
new file mode 100644
index 0000000..403ac28
--- /dev/null
+++ b/internal/lsp/highlight.go
@@ -0,0 +1,24 @@
+// 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 lsp
+
+import (
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+)
+
+func toProtocolHighlight(m *protocol.ColumnMapper, spans []span.Span) []protocol.DocumentHighlight {
+	result := make([]protocol.DocumentHighlight, 0, len(spans))
+	kind := protocol.Text
+	for _, span := range spans {
+		r, err := m.Range(span)
+		if err != nil {
+			continue
+		}
+		h := protocol.DocumentHighlight{Kind: &kind, Range: r}
+		result = append(result, h)
+	}
+	return result
+}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 3fe4bc9..ee44f9c 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -42,6 +42,7 @@
 	const expectedFormatCount = 4
 	const expectedDefinitionsCount = 16
 	const expectedTypeDefinitionsCount = 2
+	const expectedHighlightsCount = 2
 
 	files := packagestest.MustCopyFileTree(dir)
 	for fragment, operation := range files {
@@ -85,15 +86,17 @@
 	expectedFormat := make(formats)
 	expectedDefinitions := make(definitions)
 	expectedTypeDefinitions := make(definitions)
+	expectedHighlights := make(highlights)
 
 	// Collect any data that needs to be used by subsequent tests.
 	if err := exported.Expect(map[string]interface{}{
-		"diag":     expectedDiagnostics.collect,
-		"item":     completionItems.collect,
-		"complete": expectedCompletions.collect,
-		"format":   expectedFormat.collect,
-		"godef":    expectedDefinitions.collect,
-		"typdef":   expectedTypeDefinitions.collect,
+		"diag":      expectedDiagnostics.collect,
+		"item":      completionItems.collect,
+		"complete":  expectedCompletions.collect,
+		"format":    expectedFormat.collect,
+		"godef":     expectedDefinitions.collect,
+		"typdef":    expectedTypeDefinitions.collect,
+		"highlight": expectedHighlights.collect,
 	}); err != nil {
 		t.Fatal(err)
 	}
@@ -155,6 +158,16 @@
 		}
 		expectedTypeDefinitions.test(t, s, true)
 	})
+
+	t.Run("Highlights", func(t *testing.T) {
+		t.Helper()
+		if goVersion111 { // TODO(rstambler): Remove this when we no longer support Go 1.10.
+			if len(expectedHighlights) != expectedHighlightsCount {
+				t.Errorf("got %v highlights expected %v", len(expectedHighlights), expectedHighlightsCount)
+			}
+		}
+		expectedHighlights.test(t, s)
+	})
 }
 
 type diagnostics map[span.URI][]protocol.Diagnostic
@@ -162,6 +175,7 @@
 type completions map[token.Position][]token.Pos
 type formats map[string]string
 type definitions map[protocol.Location]protocol.Location
+type highlights map[string][]protocol.Location
 
 func (d diagnostics) test(t *testing.T, v source.View) int {
 	count := 0
@@ -456,6 +470,39 @@
 	d[lSrc] = lTarget
 }
 
+func (h highlights) collect(e *packagestest.Exported, fset *token.FileSet, name string, rng packagestest.Range) {
+	s, m := testLocation(e, fset, rng)
+	loc, err := m.Location(s)
+	if err != nil {
+		return
+	}
+
+	h[name] = append(h[name], loc)
+}
+
+func (h highlights) test(t *testing.T, s *server) {
+	for name, locations := range h {
+		params := &protocol.TextDocumentPositionParams{
+			TextDocument: protocol.TextDocumentIdentifier{
+				URI: locations[0].URI,
+			},
+			Position: locations[0].Range.Start,
+		}
+		highlights, err := s.DocumentHighlight(context.Background(), params)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(highlights) != len(locations) {
+			t.Fatalf("got %d highlights for %s, expected %d", len(highlights), name, len(locations))
+		}
+		for i := range highlights {
+			if highlights[i].Range != locations[i].Range {
+				t.Errorf("want %v, got %v\n", locations[i].Range, highlights[i].Range)
+			}
+		}
+	}
+}
+
 func testLocation(e *packagestest.Exported, fset *token.FileSet, rng packagestest.Range) (span.Span, *protocol.ColumnMapper) {
 	spn, err := span.NewRange(fset, rng.Start, rng.End).Span()
 	if err != nil {
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 842ae35..5f6d2a7 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -127,6 +127,7 @@
 				DocumentRangeFormattingProvider: true,
 				DocumentSymbolProvider:          true,
 				HoverProvider:                   true,
+				DocumentHighlightProvider:       true,
 				SignatureHelpProvider: &protocol.SignatureHelpOptions{
 					TriggerCharacters: []string{"(", ","},
 				},
@@ -423,8 +424,24 @@
 	return nil, notImplemented("References")
 }
 
-func (s *server) DocumentHighlight(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
-	return nil, notImplemented("DocumentHighlight")
+func (s *server) DocumentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
+	f, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI))
+	if err != nil {
+		return nil, err
+	}
+
+	spn, err := m.PointSpan(params.Position)
+	if err != nil {
+		return nil, err
+	}
+
+	rng, err := spn.Range(m.Converter)
+	if err != nil {
+		return nil, err
+	}
+
+	spans := source.Highlight(ctx, f, rng.Start)
+	return toProtocolHighlight(m, spans), nil
 }
 
 func (s *server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
diff --git a/internal/lsp/source/highlight.go b/internal/lsp/source/highlight.go
new file mode 100644
index 0000000..2952d13
--- /dev/null
+++ b/internal/lsp/source/highlight.go
@@ -0,0 +1,42 @@
+// 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 source
+
+import (
+	"context"
+	"go/ast"
+	"go/token"
+
+	"golang.org/x/tools/go/ast/astutil"
+	"golang.org/x/tools/internal/span"
+)
+
+func Highlight(ctx context.Context, f File, pos token.Pos) []span.Span {
+	fAST := f.GetAST(ctx)
+	fset := f.GetFileSet(ctx)
+	path, _ := astutil.PathEnclosingInterval(fAST, pos, pos)
+	if len(path) == 0 {
+		return nil
+	}
+
+	id, ok := path[0].(*ast.Ident)
+	if !ok {
+		return nil
+	}
+
+	var result []span.Span
+	if id.Obj != nil {
+		ast.Inspect(path[len(path)-1], func(n ast.Node) bool {
+			if n, ok := n.(*ast.Ident); ok && n.Obj == id.Obj {
+				s, err := nodeSpan(n, fset)
+				if err == nil {
+					result = append(result, s)
+				}
+			}
+			return true
+		})
+	}
+	return result
+}
diff --git a/internal/lsp/testdata/highlights/highlights.go b/internal/lsp/testdata/highlights/highlights.go
new file mode 100644
index 0000000..9314842
--- /dev/null
+++ b/internal/lsp/testdata/highlights/highlights.go
@@ -0,0 +1,15 @@
+package highlights
+
+import "fmt"
+
+type F struct{ bar int }
+
+var foo = F{bar: 52} //@highlight("foo", "foo")
+
+func Print() {
+	fmt.Println(foo) //@highlight("foo", "foo")
+}
+
+func (x *F) Inc() { //@highlight("x", "x")
+	x.bar++ //@highlight("x", "x")
+}