Skip to content

Commit b041b8f

Browse files
feat: update DevContext and LSP to support composable schemas (#2965)
Co-authored-by: Maria Ines Parnisari <maria.ines.parnisari@authzed.com>
1 parent 5d081d3 commit b041b8f

23 files changed

Lines changed: 1190 additions & 187 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

66
## [Unreleased]
7+
### Changed
8+
- Updated DevContext and LSP to support composable schemas (https://github.com/authzed/spicedb/pull/2965)
9+
710
### Fixed
811
- Fix duplicate diagnostics in LSP server when VS Code pulls diagnostics (https://github.com/authzed/spicedb/pull/2977)
912
- In DevContext's schema position mapper, only the first occurrence of a caveat parameter could be found (https://github.com/authzed/spicedb/pull/2972)

internal/lsp/handlers.go

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

910
"github.com/jzelinskie/persistent"
@@ -12,6 +13,7 @@ import (
1213

1314
log "github.com/authzed/spicedb/internal/logging"
1415
"github.com/authzed/spicedb/pkg/development"
16+
"github.com/authzed/spicedb/pkg/genutil/slicez"
1517
developerv1 "github.com/authzed/spicedb/pkg/proto/developer/v1"
1618
"github.com/authzed/spicedb/pkg/schemadsl/compiler"
1719
"github.com/authzed/spicedb/pkg/schemadsl/generator"
@@ -34,11 +36,6 @@ func (s *Server) textDocDiagnostic(ctx context.Context, r *jsonrpc2.Request) (Fu
3436
return FullDocumentDiagnosticReport{}, err
3537
}
3638

37-
log.Info().
38-
Str("uri", string(params.TextDocument.URI)).
39-
Int("diagnostics", len(diagnostics)).
40-
Msg("diagnostics complete")
41-
4239
return FullDocumentDiagnosticReport{
4340
Kind: "full",
4441
Items: diagnostics,
@@ -57,42 +54,34 @@ func (s *Server) computeDiagnostics(ctx context.Context, uri lsp.DocumentURI) ([
5754
return &jsonrpc2.Error{Code: jsonrpc2.CodeInternalError, Message: "file not found"}
5855
}
5956

57+
// We assume that the current uri that we are diagnosing is the root of a composable schema.
58+
overlayFS := newLSPOverlayFS(uriToSourceDir(uri), files)
6059
devCtx, devErrs, err := development.NewDevContext(ctx, &developerv1.RequestContext{
6160
Schema: file.contents,
6261
Relationships: nil,
63-
})
62+
}, development.WithSourceFS(overlayFS), development.WithRootFileName(string(uri)))
6463
if err != nil {
6564
return err
6665
}
67-
6866
// Get errors.
69-
for _, devErr := range devErrs.GetInputErrors() {
70-
diagnostics = append(diagnostics, lsp.Diagnostic{
71-
Severity: lsp.Error,
72-
Range: lsp.Range{
73-
Start: lsp.Position{Line: int(devErr.Line) - 1, Character: int(devErr.Column) - 1},
74-
End: lsp.Position{Line: int(devErr.Line) - 1, Character: int(devErr.Column) - 1},
75-
},
76-
Message: devErr.Message,
77-
})
67+
// We filter out errors that are *not* specifically for URI.
68+
errors := devErrs.GetInputErrors()
69+
errorsForURI := slicez.Filter(errors, func(developerError *developerv1.DeveloperError) bool {
70+
return slices.Contains(developerError.Path, string(uri))
71+
})
72+
for _, devErr := range errorsForURI {
73+
diagnostics = append(diagnostics, newLspDiagnostic(devErr, lsp.Error))
7874
}
7975

8076
// If there are no errors, we can also check for warnings.
81-
if len(diagnostics) == 0 {
77+
if len(errors) == 0 {
8278
warnings, err := development.GetWarnings(ctx, devCtx)
8379
if err != nil {
8480
return err
8581
}
8682

8783
for _, devWarning := range warnings {
88-
diagnostics = append(diagnostics, lsp.Diagnostic{
89-
Severity: lsp.Warning,
90-
Range: lsp.Range{
91-
Start: lsp.Position{Line: int(devWarning.Line) - 1, Character: int(devWarning.Column) - 1},
92-
End: lsp.Position{Line: int(devWarning.Line) - 1, Character: int(devWarning.Column) - 1},
93-
},
94-
Message: devWarning.Message,
95-
})
84+
diagnostics = append(diagnostics, newLspDiagnostic(devWarning, lsp.Warning))
9685
}
9786
}
9887

@@ -101,10 +90,43 @@ func (s *Server) computeDiagnostics(ctx context.Context, uri lsp.DocumentURI) ([
10190
return nil, err
10291
}
10392

104-
log.Info().Int("diagnostics", len(diagnostics)).Str("uri", string(uri)).Msg("computed diagnostics")
10593
return diagnostics, nil
10694
}
10795

96+
type DeveloperErrorWithPosition interface {
97+
// 1-indexed Line
98+
GetLine() uint32
99+
// 1-indexed Column
100+
GetColumn() uint32
101+
GetMessage() string
102+
}
103+
104+
func newLspDiagnostic(devErr DeveloperErrorWithPosition, severity lsp.DiagnosticSeverity) lsp.Diagnostic {
105+
// lines and columns are 1-indexed
106+
return lsp.Diagnostic{
107+
Severity: severity,
108+
Range: lsp.Range{
109+
Start: lsp.Position{Line: int(devErr.GetLine()) - 1, Character: int(devErr.GetColumn()) - 1},
110+
End: lsp.Position{Line: int(devErr.GetLine()) - 1, Character: int(devErr.GetColumn()) - 1},
111+
},
112+
Message: devErr.GetMessage(),
113+
}
114+
}
115+
116+
func newLspRange(resolved *development.SchemaReference) *lsp.Range {
117+
// lines and columns are 0-indexed
118+
return &lsp.Range{
119+
Start: lsp.Position{
120+
Line: resolved.TargetPosition.LineNumber,
121+
Character: resolved.TargetPosition.ColumnPosition + resolved.TargetNamePositionOffset,
122+
},
123+
End: lsp.Position{
124+
Line: resolved.TargetPosition.LineNumber,
125+
Character: resolved.TargetPosition.ColumnPosition + resolved.TargetNamePositionOffset + len(resolved.Text),
126+
},
127+
}
128+
}
129+
108130
func (s *Server) textDocDidSave(ctx context.Context, r *jsonrpc2.Request, conn *jsonrpc2.Conn) (any, error) {
109131
params, err := unmarshalParams[lsp.DidSaveTextDocumentParams](r)
110132
if err != nil {
@@ -171,15 +193,16 @@ func (s *Server) publishDiagnosticsIfNecessary(ctx context.Context, conn *jsonrp
171193
return nil
172194
}
173195

174-
log.Debug().
175-
Str("uri", string(uri)).
176-
Msg("publishing diagnostics")
177-
178196
diagnostics, err := s.computeDiagnostics(ctx, uri)
179197
if err != nil {
180198
return fmt.Errorf("failed to compute diagnostics: %w", err)
181199
}
182200

201+
log.Info().
202+
Str("uri", string(uri)).
203+
Int("diagnostics", len(diagnostics)).
204+
Msg("publishing diagnostics")
205+
183206
return conn.Notify(ctx, "textDocument/publishDiagnostics", lsp.PublishDiagnosticsParams{
184207
URI: uri,
185208
Diagnostics: diagnostics,
@@ -197,7 +220,8 @@ func (s *Server) getCompiledContents(path lsp.DocumentURI, files *persistent.Map
197220
return compiled, nil
198221
}
199222

200-
justCompiled, derr, err := development.CompileSchema(file.contents)
223+
overlayFS := newLSPOverlayFS(uriToSourceDir(path), files)
224+
justCompiled, derr, err := development.CompileSchema(file.contents, development.WithSourceFS(overlayFS))
201225
if err != nil {
202226
return nil, err
203227
}
@@ -243,16 +267,7 @@ func (s *Server) textDocHover(_ context.Context, r *jsonrpc2.Request) (*Hover, e
243267

244268
var lspRange *lsp.Range
245269
if resolved.TargetPosition != nil {
246-
lspRange = &lsp.Range{
247-
Start: lsp.Position{
248-
Line: resolved.TargetPosition.LineNumber,
249-
Character: resolved.TargetPosition.ColumnPosition + resolved.TargetNamePositionOffset,
250-
},
251-
End: lsp.Position{
252-
Line: resolved.TargetPosition.LineNumber,
253-
Character: resolved.TargetPosition.ColumnPosition + resolved.TargetNamePositionOffset + len(resolved.Text),
254-
},
255-
}
270+
lspRange = newLspRange(resolved)
256271
}
257272

258273
if resolved.TargetSourceCode != "" {
@@ -282,6 +297,58 @@ func (s *Server) textDocHover(_ context.Context, r *jsonrpc2.Request) (*Hover, e
282297
return hoverContents, nil
283298
}
284299

300+
func (s *Server) textDocDefinition(_ context.Context, r *jsonrpc2.Request) (*lsp.Location, error) {
301+
params, err := unmarshalParams[lsp.TextDocumentPositionParams](r)
302+
if err != nil {
303+
return nil, err
304+
}
305+
306+
var location *lsp.Location
307+
err = s.withFiles(func(files *persistent.Map[lsp.DocumentURI, trackedFile]) error {
308+
compiled, err := s.getCompiledContents(params.TextDocument.URI, files)
309+
if err != nil {
310+
return err
311+
}
312+
313+
resolver, err := development.NewSchemaPositionMapper(compiled)
314+
if err != nil {
315+
return err
316+
}
317+
318+
position := input.Position{
319+
LineNumber: params.Position.Line,
320+
ColumnPosition: params.Position.Character,
321+
}
322+
323+
resolved, err := resolver.ReferenceAtPosition(input.Source("schema"), position)
324+
if err != nil {
325+
return err
326+
}
327+
328+
if resolved == nil || resolved.TargetPosition == nil {
329+
return nil
330+
}
331+
332+
// Determine the target file URI from TargetSource.
333+
targetURI := params.TextDocument.URI
334+
if resolved.TargetSource != nil && *resolved.TargetSource != "schema" {
335+
targetURI = resolveURI(params.TextDocument.URI, string(*resolved.TargetSource))
336+
}
337+
338+
location = &lsp.Location{
339+
URI: targetURI,
340+
Range: *newLspRange(resolved),
341+
}
342+
343+
return nil
344+
})
345+
if err != nil {
346+
return nil, err
347+
}
348+
349+
return location, nil
350+
}
351+
285352
func (s *Server) textDocFormat(ctx context.Context, r *jsonrpc2.Request) ([]lsp.TextEdit, error) {
286353
params, err := unmarshalParams[lsp.DocumentFormattingParams](r)
287354
if err != nil {
@@ -336,9 +403,6 @@ func (s *Server) initialize(_ context.Context, r *jsonrpc2.Request) (any, error)
336403
}
337404

338405
s.requestsDiagnostics = ip.Capabilities.SupportsPullDiagnostics()
339-
log.Debug().
340-
Bool("requestsDiagnostics", s.requestsDiagnostics).
341-
Msg("initialize")
342406

343407
if s.state != serverStateNotInitialized {
344408
return nil, invalidRequest(errors.New("already initialized"))
@@ -353,6 +417,7 @@ func (s *Server) initialize(_ context.Context, r *jsonrpc2.Request) (any, error)
353417
DocumentFormattingProvider: true,
354418
DiagnosticProvider: &DiagnosticOptions{Identifier: "spicedb", InterFileDependencies: false, WorkspaceDiagnostics: false},
355419
HoverProvider: true,
420+
DefinitionProvider: true,
356421
},
357422
}, nil
358423
}

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).

0 commit comments

Comments
 (0)