Skip to content

Commit 96aee9a

Browse files
committed
lsp: add textDocument/documentHighlight support
Implement document highlight by reusing the references query and filtering results to only include occurrences within the same file. This matches the approach used by Sourcegraph's web client.
1 parent 05065c3 commit 96aee9a

File tree

3 files changed

+251
-6
lines changed

3 files changed

+251
-6
lines changed

cmd/src/lsp.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Supported LSP methods:
2525
- textDocument/definition
2626
- textDocument/references
2727
- textDocument/hover
28+
- textDocument/documentHighlight
2829
2930
Example Neovim configuration (0.11+):
3031

internal/lsp/server.go

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ func (s *Server) Run() error {
5252
SetTrace: s.handleSetTrace,
5353
TextDocumentDidOpen: s.handleTextDocumentDidOpen,
5454
TextDocumentDidClose: s.handleTextDocumentDidClose,
55-
TextDocumentDefinition: s.handleTextDocumentDefinition,
56-
TextDocumentReferences: s.handleTextDocumentReferences,
57-
TextDocumentHover: s.handleTextDocumentHover,
55+
TextDocumentDefinition: s.handleTextDocumentDefinition,
56+
TextDocumentReferences: s.handleTextDocumentReferences,
57+
TextDocumentHover: s.handleTextDocumentHover,
58+
TextDocumentDocumentHighlight: s.handleTextDocumentDocumentHighlight,
5859
}
5960

6061
srv := server.NewServer(&handler, serverName, false)
@@ -67,9 +68,10 @@ func (s *Server) handleInitialize(_ *glsp.Context, _ *protocol.InitializeParams)
6768
TextDocumentSync: &protocol.TextDocumentSyncOptions{
6869
OpenClose: &protocol.True,
6970
},
70-
DefinitionProvider: true,
71-
ReferencesProvider: true,
72-
HoverProvider: true,
71+
DefinitionProvider: true,
72+
ReferencesProvider: true,
73+
HoverProvider: true,
74+
DocumentHighlightProvider: true,
7375
},
7476
ServerInfo: &protocol.InitializeResultServerInfo{
7577
Name: serverName,
@@ -237,6 +239,50 @@ func (s *Server) handleTextDocumentHover(_ *glsp.Context, params *protocol.Hover
237239
return hover, nil
238240
}
239241

242+
func (s *Server) handleTextDocumentDocumentHighlight(_ *glsp.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) {
243+
path, err := s.uriToRepoPath(params.TextDocument.URI)
244+
if err != nil {
245+
return nil, err
246+
}
247+
248+
nodes, err := s.queryReferences(context.Background(), path, int(params.Position.Line), int(params.Position.Character))
249+
if err != nil {
250+
return nil, err
251+
}
252+
if len(nodes) == 0 {
253+
return nil, nil
254+
}
255+
256+
var highlights []protocol.DocumentHighlight
257+
for _, node := range nodes {
258+
if node.Resource.Repository.Name != s.repoName {
259+
continue
260+
}
261+
if node.Resource.Path != path {
262+
continue
263+
}
264+
265+
highlights = append(highlights, protocol.DocumentHighlight{
266+
Range: protocol.Range{
267+
Start: protocol.Position{
268+
Line: protocol.UInteger(node.Range.Start.Line),
269+
Character: protocol.UInteger(node.Range.Start.Character),
270+
},
271+
End: protocol.Position{
272+
Line: protocol.UInteger(node.Range.End.Line),
273+
Character: protocol.UInteger(node.Range.End.Character),
274+
},
275+
},
276+
})
277+
}
278+
279+
if len(highlights) == 0 {
280+
return nil, nil
281+
}
282+
283+
return highlights, nil
284+
}
285+
240286
func (s *Server) uriToRepoPath(uri string) (string, error) {
241287
parsed, err := url.Parse(uri)
242288
if err != nil {

internal/lsp/server_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package lsp
22

33
import (
4+
"context"
45
"os"
56
"path/filepath"
67
"testing"
78

9+
protocol "github.com/tliron/glsp/protocol_3_16"
10+
11+
"github.com/sourcegraph/src-cli/internal/api/mock"
812
"github.com/stretchr/testify/require"
913
)
1014

@@ -88,3 +92,197 @@ func TestRunGitCommandError(t *testing.T) {
8892
require.Error(t, err)
8993
require.Contains(t, err.Error(), "git command failed")
9094
}
95+
96+
func TestHandleTextDocumentDocumentHighlight(t *testing.T) {
97+
gitRoot, err := getGitRoot()
98+
require.NoError(t, err)
99+
100+
tests := []struct {
101+
name string
102+
path string
103+
response string
104+
wantCount int
105+
wantNil bool
106+
}{
107+
{
108+
name: "filters to same file only",
109+
path: "main.go",
110+
response: `{
111+
"repository": {
112+
"commit": {
113+
"blob": {
114+
"lsif": {
115+
"references": {
116+
"nodes": [
117+
{
118+
"resource": {
119+
"path": "main.go",
120+
"repository": {"name": "github.com/test/repo"},
121+
"commit": {"oid": "abc123"}
122+
},
123+
"range": {
124+
"start": {"line": 10, "character": 0},
125+
"end": {"line": 10, "character": 5}
126+
}
127+
},
128+
{
129+
"resource": {
130+
"path": "main.go",
131+
"repository": {"name": "github.com/test/repo"},
132+
"commit": {"oid": "abc123"}
133+
},
134+
"range": {
135+
"start": {"line": 20, "character": 0},
136+
"end": {"line": 20, "character": 5}
137+
}
138+
},
139+
{
140+
"resource": {
141+
"path": "other.go",
142+
"repository": {"name": "github.com/test/repo"},
143+
"commit": {"oid": "abc123"}
144+
},
145+
"range": {
146+
"start": {"line": 5, "character": 0},
147+
"end": {"line": 5, "character": 5}
148+
}
149+
}
150+
]
151+
}
152+
}
153+
}
154+
}
155+
}
156+
}`,
157+
wantCount: 2,
158+
},
159+
{
160+
name: "filters out other repositories",
161+
path: "main.go",
162+
response: `{
163+
"repository": {
164+
"commit": {
165+
"blob": {
166+
"lsif": {
167+
"references": {
168+
"nodes": [
169+
{
170+
"resource": {
171+
"path": "main.go",
172+
"repository": {"name": "github.com/test/repo"},
173+
"commit": {"oid": "abc123"}
174+
},
175+
"range": {
176+
"start": {"line": 10, "character": 0},
177+
"end": {"line": 10, "character": 5}
178+
}
179+
},
180+
{
181+
"resource": {
182+
"path": "main.go",
183+
"repository": {"name": "github.com/other/repo"},
184+
"commit": {"oid": "def456"}
185+
},
186+
"range": {
187+
"start": {"line": 15, "character": 0},
188+
"end": {"line": 15, "character": 5}
189+
}
190+
}
191+
]
192+
}
193+
}
194+
}
195+
}
196+
}
197+
}`,
198+
wantCount: 1,
199+
},
200+
{
201+
name: "no references returns nil",
202+
path: "main.go",
203+
response: `{
204+
"repository": {
205+
"commit": {
206+
"blob": {
207+
"lsif": {
208+
"references": {
209+
"nodes": []
210+
}
211+
}
212+
}
213+
}
214+
}
215+
}`,
216+
wantNil: true,
217+
},
218+
{
219+
name: "all references in other files returns nil",
220+
path: "main.go",
221+
response: `{
222+
"repository": {
223+
"commit": {
224+
"blob": {
225+
"lsif": {
226+
"references": {
227+
"nodes": [
228+
{
229+
"resource": {
230+
"path": "other.go",
231+
"repository": {"name": "github.com/test/repo"},
232+
"commit": {"oid": "abc123"}
233+
},
234+
"range": {
235+
"start": {"line": 10, "character": 0},
236+
"end": {"line": 10, "character": 5}
237+
}
238+
}
239+
]
240+
}
241+
}
242+
}
243+
}
244+
}
245+
}`,
246+
wantNil: true,
247+
},
248+
}
249+
250+
for _, tt := range tests {
251+
t.Run(tt.name, func(t *testing.T) {
252+
mockClient := &mock.Client{}
253+
mockRequest := &mock.Request{Response: tt.response}
254+
mockRequest.On("Do", context.Background(), &referencesResponse{}).Return(true, nil)
255+
mockClient.On("NewRequest", referencesQuery, map[string]any{
256+
"repository": "github.com/test/repo",
257+
"commit": "abc123",
258+
"path": tt.path,
259+
"line": 10,
260+
"character": 5,
261+
}).Return(mockRequest)
262+
263+
s := &Server{
264+
apiClient: mockClient,
265+
repoName: "github.com/test/repo",
266+
commit: "abc123",
267+
}
268+
269+
uri := "file://" + filepath.Join(gitRoot, tt.path)
270+
params := &protocol.DocumentHighlightParams{
271+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
272+
TextDocument: protocol.TextDocumentIdentifier{URI: uri},
273+
Position: protocol.Position{Line: 10, Character: 5},
274+
},
275+
}
276+
277+
result, err := s.handleTextDocumentDocumentHighlight(nil, params)
278+
require.NoError(t, err)
279+
280+
if tt.wantNil {
281+
require.Nil(t, result)
282+
return
283+
}
284+
285+
require.Len(t, result, tt.wantCount)
286+
})
287+
}
288+
}

0 commit comments

Comments
 (0)