Skip to content

Commit 2fbc435

Browse files
committed
add tests to LSP and fix an issue with requestDiagnostics, and add support for textDocument/definition, and fix mapper
1 parent e20db9f commit 2fbc435

File tree

14 files changed

+618
-47
lines changed

14 files changed

+618
-47
lines changed

internal/lsp/handlers.go

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"path/filepath"
78
"strings"
89

910
"github.com/jzelinskie/persistent"
@@ -34,11 +35,6 @@ func (s *Server) textDocDiagnostic(ctx context.Context, r *jsonrpc2.Request) (Fu
3435
return FullDocumentDiagnosticReport{}, err
3536
}
3637

37-
log.Info().
38-
Str("uri", string(params.TextDocument.URI)).
39-
Int("diagnostics", len(diagnostics)).
40-
Msg("diagnostics complete")
41-
4238
return FullDocumentDiagnosticReport{
4339
Kind: "full",
4440
Items: diagnostics,
@@ -102,7 +98,6 @@ func (s *Server) computeDiagnostics(ctx context.Context, uri lsp.DocumentURI) ([
10298
return nil, err
10399
}
104100

105-
log.Info().Int("diagnostics", len(diagnostics)).Str("uri", string(uri)).Msg("computed diagnostics")
106101
return diagnostics, nil
107102
}
108103

@@ -172,15 +167,16 @@ func (s *Server) publishDiagnosticsIfNecessary(ctx context.Context, conn *jsonrp
172167
return nil
173168
}
174169

175-
log.Debug().
176-
Str("uri", string(uri)).
177-
Msg("publishing diagnostics")
178-
179170
diagnostics, err := s.computeDiagnostics(ctx, uri)
180171
if err != nil {
181172
return fmt.Errorf("failed to compute diagnostics: %w", err)
182173
}
183174

175+
log.Info().
176+
Str("uri", string(uri)).
177+
Int("diagnostics", len(diagnostics)).
178+
Msg("publishing diagnostics")
179+
184180
return conn.Notify(ctx, "textDocument/publishDiagnostics", lsp.PublishDiagnosticsParams{
185181
URI: uri,
186182
Diagnostics: diagnostics,
@@ -284,6 +280,63 @@ func (s *Server) textDocHover(_ context.Context, r *jsonrpc2.Request) (*Hover, e
284280
return hoverContents, nil
285281
}
286282

283+
func (s *Server) textDocDefinition(_ context.Context, r *jsonrpc2.Request) (*lsp.Location, error) {
284+
params, err := unmarshalParams[lsp.TextDocumentPositionParams](r)
285+
if err != nil {
286+
return nil, err
287+
}
288+
289+
var location *lsp.Location
290+
err = s.withFiles(func(files *persistent.Map[lsp.DocumentURI, trackedFile]) error {
291+
compiled, err := s.getCompiledContents(params.TextDocument.URI, files)
292+
if err != nil {
293+
return err
294+
}
295+
296+
resolver, err := development.NewSchemaPositionMapper(compiled)
297+
if err != nil {
298+
return err
299+
}
300+
301+
position := input.Position{
302+
LineNumber: params.Position.Line,
303+
ColumnPosition: params.Position.Character,
304+
}
305+
306+
resolved, err := resolver.ReferenceAtPosition(input.Source("schema"), position)
307+
if err != nil {
308+
return err
309+
}
310+
311+
if resolved == nil || resolved.TargetPosition == nil {
312+
return nil
313+
}
314+
315+
// Determine the target file URI from TargetSource.
316+
targetURI := params.TextDocument.URI
317+
if resolved.TargetSource != nil && *resolved.TargetSource != "schema" {
318+
sourceDir := uriToSourceDir(params.TextDocument.URI)
319+
targetURI = lsp.DocumentURI("file://" + filepath.Join(sourceDir, string(*resolved.TargetSource)))
320+
}
321+
322+
nameStart := resolved.TargetPosition.ColumnPosition + resolved.TargetNamePositionOffset
323+
location = &lsp.Location{
324+
URI: targetURI,
325+
Range: lsp.Range{
326+
Start: lsp.Position{Line: resolved.TargetPosition.LineNumber, Character: nameStart},
327+
End: lsp.Position{Line: resolved.TargetPosition.LineNumber, Character: nameStart + len(resolved.Text)},
328+
},
329+
}
330+
331+
return nil
332+
})
333+
if err != nil {
334+
return nil, err
335+
}
336+
337+
return location, nil
338+
}
339+
287340
func (s *Server) textDocFormat(_ context.Context, r *jsonrpc2.Request) ([]lsp.TextEdit, error) {
288341
params, err := unmarshalParams[lsp.DocumentFormattingParams](r)
289342
if err != nil {
@@ -337,8 +390,8 @@ func (s *Server) initialize(_ context.Context, r *jsonrpc2.Request) (any, error)
337390
return nil, err
338391
}
339392

340-
s.requestsDiagnostics = ip.Capabilities.Diagnostics.RefreshSupport
341-
log.Debug().
393+
s.requestsDiagnostics = ip.Capabilities.Workspace.Diagnostics.RefreshSupport
394+
log.Info().
342395
Bool("requestsDiagnostics", s.requestsDiagnostics).
343396
Msg("initialize")
344397

@@ -355,6 +408,7 @@ func (s *Server) initialize(_ context.Context, r *jsonrpc2.Request) (any, error)
355408
DocumentFormattingProvider: true,
356409
DiagnosticProvider: &DiagnosticOptions{Identifier: "spicedb", InterFileDependencies: false, WorkspaceDiagnostics: false},
357410
HoverProvider: true,
411+
DefinitionProvider: true,
358412
},
359413
}, nil
360414
}

internal/lsp/lsp.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ func (s *Server) handle(ctx context.Context, conn *jsonrpc2.Conn, r *jsonrpc2.Re
9696
result, err = s.textDocFormat(ctx, r)
9797
case "textDocument/hover":
9898
result, err = s.textDocHover(ctx, r)
99+
case "textDocument/definition":
100+
result, err = s.textDocDefinition(ctx, r)
99101
default:
100102
log.Ctx(ctx).Warn().
101103
Str("method", r.Method).

internal/lsp/lsp_test.go

Lines changed: 221 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,10 @@ func TestDiagnosticsRefreshSupport(t *testing.T) {
292292
// Initialize with diagnostic refresh support enabled
293293
resp, serverState := sendAndReceive[lsp.InitializeResult](tester, "initialize", InitializeParams{
294294
Capabilities: ClientCapabilities{
295-
Diagnostics: DiagnosticWorkspaceClientCapabilities{
296-
RefreshSupport: true,
295+
Workspace: WorkspaceClientCapabilities{
296+
Diagnostics: DiagnosticWorkspaceClientCapabilities{
297+
RefreshSupport: true,
298+
},
297299
},
298300
},
299301
})
@@ -305,8 +307,10 @@ func TestDiagnosticsRefreshSupport(t *testing.T) {
305307
tester2 := newLSPTester(t)
306308
resp2, serverState2 := sendAndReceive[lsp.InitializeResult](tester2, "initialize", InitializeParams{
307309
Capabilities: ClientCapabilities{
308-
Diagnostics: DiagnosticWorkspaceClientCapabilities{
309-
RefreshSupport: false,
310+
Workspace: WorkspaceClientCapabilities{
311+
Diagnostics: DiagnosticWorkspaceClientCapabilities{
312+
RefreshSupport: false,
313+
},
310314
},
311315
},
312316
})
@@ -357,6 +361,219 @@ func TestUnmarshalParamsErrors(t *testing.T) {
357361
}().Code)
358362
}
359363

364+
func TestMultiFileNoDiagnostics(t *testing.T) {
365+
tester := newLSPTester(t)
366+
tester.initialize()
367+
368+
tester.setFileContents("file:///testdir/users.zed", "definition user {}")
369+
tester.setFileContents("file:///testdir/root.zed", `use import
370+
371+
import "users.zed"
372+
373+
definition resource {
374+
relation viewer: user
375+
permission view = viewer
376+
}
377+
`)
378+
379+
resp, _ := sendAndReceive[FullDocumentDiagnosticReport](tester, "textDocument/diagnostic",
380+
TextDocumentDiagnosticParams{
381+
TextDocument: TextDocument{URI: "file:///testdir/root.zed"},
382+
})
383+
require.Equal(t, "full", resp.Kind)
384+
require.Empty(t, resp.Items)
385+
}
386+
387+
func TestMultiFileUndefinedDefinitionDiagnostics(t *testing.T) {
388+
tester := newLSPTester(t)
389+
tester.initialize()
390+
391+
tester.setFileContents("file:///testdir/broken.zed", `
392+
definition resource {
393+
relation viewer: organization
394+
permission view = viewer
395+
}`)
396+
tester.setFileContents("file:///testdir/root.zed", `use import
397+
398+
import "broken.zed"
399+
`)
400+
401+
resp, _ := sendAndReceive[FullDocumentDiagnosticReport](tester, "textDocument/diagnostic",
402+
TextDocumentDiagnosticParams{
403+
TextDocument: TextDocument{URI: "file:///testdir/root.zed"},
404+
})
405+
require.Equal(t, "full", resp.Kind)
406+
require.Len(t, resp.Items, 1)
407+
require.Equal(t, lsp.Error, resp.Items[0].Severity)
408+
t.Log(resp.Items[0].Message)
409+
require.Contains(t, resp.Items[0].Message, "could not lookup definition `organization` for relation `viewer`: object definition `organization` not found")
410+
}
411+
412+
func TestMultiFileBrokenImportDiagnostics(t *testing.T) {
413+
tester := newLSPTester(t)
414+
tester.initialize()
415+
416+
tester.setFileContents("file:///testdir/root.zed", `use import
417+
import "unknown.zed"
418+
`)
419+
420+
resp, _ := sendAndReceive[FullDocumentDiagnosticReport](tester, "textDocument/diagnostic",
421+
TextDocumentDiagnosticParams{
422+
TextDocument: TextDocument{URI: "file:///testdir/root.zed"},
423+
})
424+
require.Equal(t, "full", resp.Kind)
425+
require.Len(t, resp.Items, 1)
426+
require.Equal(t, lsp.Error, resp.Items[0].Severity)
427+
require.Contains(t, resp.Items[0].Message, "failed to read import \"unknown.zed\": open unknown.zed: no such file or director")
428+
}
429+
430+
func TestDefinitionSameFileTypeReference(t *testing.T) {
431+
tester := newLSPTester(t)
432+
tester.initialize()
433+
434+
tester.setFileContents("file:///test", `definition user {}
435+
436+
definition resource {
437+
relation viewer: user
438+
permission view = viewer
439+
}
440+
`)
441+
442+
// Click on "user" in "relation viewer: user" (line 3, character 18)
443+
resp, _ := sendAndReceive[lsp.Location](tester, "textDocument/definition",
444+
lsp.TextDocumentPositionParams{
445+
TextDocument: lsp.TextDocumentIdentifier{URI: "file:///test"},
446+
Position: lsp.Position{Line: 3, Character: 18},
447+
})
448+
require.Equal(t, lsp.DocumentURI("file:///test"), resp.URI)
449+
require.Equal(t, 0, resp.Range.Start.Line)
450+
require.Equal(t, len("definition "), resp.Range.Start.Character)
451+
}
452+
453+
func TestDefinitionSameFileRelationReference(t *testing.T) {
454+
tester := newLSPTester(t)
455+
tester.initialize()
456+
457+
tester.setFileContents("file:///test", `definition user {}
458+
459+
definition resource {
460+
relation viewer: user
461+
permission view = viewer
462+
}
463+
`)
464+
465+
// Click on "viewer" in "permission view = viewer" (line 4, character 19)
466+
resp, _ := sendAndReceive[lsp.Location](tester, "textDocument/definition",
467+
lsp.TextDocumentPositionParams{
468+
TextDocument: lsp.TextDocumentIdentifier{URI: "file:///test"},
469+
Position: lsp.Position{Line: 4, Character: 19},
470+
})
471+
require.Equal(t, lsp.DocumentURI("file:///test"), resp.URI)
472+
require.Equal(t, 3, resp.Range.Start.Line)
473+
require.Equal(t, len("\trelation "), resp.Range.Start.Character)
474+
}
475+
476+
func TestDefinitionCrossFileTypeReference(t *testing.T) {
477+
tester := newLSPTester(t)
478+
tester.initialize()
479+
480+
tester.setFileContents("file:///testdir/users.zed", "definition user {}")
481+
tester.setFileContents("file:///testdir/root.zed", `use import
482+
483+
import "users.zed"
484+
485+
definition resource {
486+
relation viewer: user
487+
permission view = viewer
488+
}
489+
`)
490+
491+
// Click on "user" in "relation viewer: user" (line 5, character 18)
492+
// It should point to "users.zed"
493+
resp, _ := sendAndReceive[lsp.Location](tester, "textDocument/definition",
494+
lsp.TextDocumentPositionParams{
495+
TextDocument: lsp.TextDocumentIdentifier{URI: "file:///testdir/root.zed"},
496+
Position: lsp.Position{Line: 5, Character: 18},
497+
})
498+
require.Equal(t, lsp.DocumentURI("file:///testdir/users.zed"), resp.URI)
499+
require.Equal(t, 0, resp.Range.Start.Line)
500+
require.Equal(t, len("definition "), resp.Range.Start.Character)
501+
}
502+
503+
func TestDefinitionImportReference(t *testing.T) {
504+
tester := newLSPTester(t)
505+
tester.initialize()
506+
507+
tester.setFileContents("file:///testdir/users.zed", "definition user {}")
508+
tester.setFileContents("file:///testdir/root.zed", `use import
509+
510+
import "users.zed"
511+
512+
definition resource {
513+
relation viewer: user
514+
permission view = viewer
515+
}
516+
`)
517+
518+
// Click on import "users.zed" (line 2, character 10)
519+
// It should point on the very begginning of "users.zed"
520+
resp, _ := sendAndReceive[lsp.Location](tester, "textDocument/definition",
521+
lsp.TextDocumentPositionParams{
522+
TextDocument: lsp.TextDocumentIdentifier{URI: "file:///testdir/root.zed"},
523+
Position: lsp.Position{Line: 2, Character: 10},
524+
})
525+
require.Equal(t, lsp.DocumentURI("file:///testdir/users.zed"), resp.URI)
526+
require.Equal(t, 0, resp.Range.Start.Line)
527+
require.Equal(t, 0, resp.Range.Start.Character)
528+
}
529+
530+
func TestDefinitionCrossFileCaveatReference(t *testing.T) {
531+
tester := newLSPTester(t)
532+
tester.initialize()
533+
534+
tester.setFileContents("file:///testdir/caveats.zed", `caveat some_caveat(some_param int) {
535+
some_param < 100
536+
}`)
537+
tester.setFileContents("file:///testdir/root.zed", `use import
538+
539+
import "caveats.zed"
540+
541+
definition user {}
542+
543+
definition resource {
544+
relation viewer: user with some_caveat
545+
}
546+
`)
547+
548+
// Click on "some_caveat" in "relation viewer: user with some_caveat" (line 7, character 30)
549+
// "\trelation viewer: user with some_caveat"
550+
// 0 1 2 3
551+
// 0123456789012345678901234567890123456789
552+
resp, _ := sendAndReceive[lsp.Location](tester, "textDocument/definition",
553+
lsp.TextDocumentPositionParams{
554+
TextDocument: lsp.TextDocumentIdentifier{URI: "file:///testdir/root.zed"},
555+
Position: lsp.Position{Line: 7, Character: 30},
556+
})
557+
require.Equal(t, lsp.DocumentURI("file:///testdir/caveats.zed"), resp.URI)
558+
require.Equal(t, 0, resp.Range.Start.Line)
559+
require.Equal(t, len("caveat "), resp.Range.Start.Character)
560+
}
561+
562+
func TestDefinitionNoReference(t *testing.T) {
563+
tester := newLSPTester(t)
564+
tester.initialize()
565+
566+
tester.setFileContents("file:///test", "definition user {}")
567+
568+
// Click on whitespace / keyword where no reference exists
569+
resp, _ := sendAndReceive[*lsp.Location](tester, "textDocument/definition",
570+
lsp.TextDocumentPositionParams{
571+
TextDocument: lsp.TextDocumentIdentifier{URI: "file:///test"},
572+
Position: lsp.Position{Line: 0, Character: 0},
573+
})
574+
require.Nil(t, resp)
575+
}
576+
360577
func TestInvalidParams(t *testing.T) {
361578
err := invalidParams(errors.New("test error"))
362579
require.Equal(t, int64(jsonrpc2.CodeInvalidParams), err.Code)

0 commit comments

Comments
 (0)