diff --git a/server/call_hierarchy_contributor.go b/server/call_hierarchy_contributor.go new file mode 100644 index 0000000..cb26e58 --- /dev/null +++ b/server/call_hierarchy_contributor.go @@ -0,0 +1,71 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/lsp" +) + +// CallHierarchyContributor provides language-specific logic for call hierarchy navigation. +// +// Usage: +// +// type MyCallHierarchyContributor struct{ sc *service.Container } +// +// func (c *MyCallHierarchyContributor) PrepareItem(ctx context.Context, token *core.Token, node core.AstNode) *lsp.CallHierarchyItem { +// if funcDecl, ok := node.(*ast.FunctionDeclaration); ok { +// return &lsp.CallHierarchyItem{ +// Name: funcDecl.Name, +// Kind: lsp.Function, +// URI: node.Document().URI.DocumentURI(), +// Range: node.Segment().Range.LspRange(), +// } +// } +// return nil +// } +// +// func (c *MyCallHierarchyContributor) FindIncomingCalls(ctx context.Context, item *lsp.CallHierarchyItem) []lsp.CallHierarchyIncomingCall { +// // Use references to find call sites +// return calls +// } +type CallHierarchyContributor interface { + // PrepareItem creates a call hierarchy item for a symbol at the given position. + // Return nil if the symbol at the position is not callable (not a function/method). + // The returned item will be passed to incoming/outgoing call requests. + PrepareItem(ctx context.Context, token *core.Token, node core.AstNode) *lsp.CallHierarchyItem + + // FindIncomingCalls finds all locations that call the given symbol. + // Each incoming call should include the call site location and the item + // representing the caller. + FindIncomingCalls(ctx context.Context, item *lsp.CallHierarchyItem) []lsp.CallHierarchyIncomingCall + + // FindOutgoingCalls finds all locations called by the given symbol. + // Each outgoing call should include the call site location and the item + // representing the callee. + FindOutgoingCalls(ctx context.Context, item *lsp.CallHierarchyItem) []lsp.CallHierarchyOutgoingCall +} + +// DefaultCallHierarchyContributor is the default implementation of [CallHierarchyContributor]. +// It returns nil for all operations, effectively disabling call hierarchy. +type DefaultCallHierarchyContributor struct{} + +func NewDefaultCallHierarchyContributor() CallHierarchyContributor { + return &DefaultCallHierarchyContributor{} +} + +func (c *DefaultCallHierarchyContributor) PrepareItem(ctx context.Context, token *core.Token, node core.AstNode) *lsp.CallHierarchyItem { + return nil +} + +func (c *DefaultCallHierarchyContributor) FindIncomingCalls(ctx context.Context, item *lsp.CallHierarchyItem) []lsp.CallHierarchyIncomingCall { + return nil +} + +func (c *DefaultCallHierarchyContributor) FindOutgoingCalls(ctx context.Context, item *lsp.CallHierarchyItem) []lsp.CallHierarchyOutgoingCall { + return nil +} diff --git a/server/call_hierarchy_provider.go b/server/call_hierarchy_provider.go new file mode 100644 index 0000000..b87141a --- /dev/null +++ b/server/call_hierarchy_provider.go @@ -0,0 +1,87 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/util/service" + "typefox.dev/fastbelt/workspace" + "typefox.dev/lsp" +) + +// CallHierarchyProvider is a service for handling LSP call hierarchy requests, delegating +// the language-specific call detection to [CallHierarchyContributor]. +type CallHierarchyProvider interface { + HandlePrepareCallHierarchyRequest(ctx context.Context, params *lsp.CallHierarchyPrepareParams) ([]lsp.CallHierarchyItem, error) + HandleIncomingCallsRequest(ctx context.Context, params *lsp.CallHierarchyIncomingCallsParams) ([]lsp.CallHierarchyIncomingCall, error) + HandleOutgoingCallsRequest(ctx context.Context, params *lsp.CallHierarchyOutgoingCallsParams) ([]lsp.CallHierarchyOutgoingCall, error) +} + +// DefaultCallHierarchyProvider is the default implementation of [CallHierarchyProvider]. +// It delegates call detection to [CallHierarchyContributor]. +type DefaultCallHierarchyProvider struct { + sc *service.Container +} + +func NewDefaultCallHierarchyProvider(sc *service.Container) CallHierarchyProvider { + return &DefaultCallHierarchyProvider{sc: sc} +} + +func (p *DefaultCallHierarchyProvider) HandlePrepareCallHierarchyRequest(ctx context.Context, params *lsp.CallHierarchyPrepareParams) ([]lsp.CallHierarchyItem, error) { + documentManager := service.MustGet[workspace.DocumentManager](p.sc) + uri := core.ParseURI(string(params.TextDocument.URI)) + doc := documentManager.Get(uri) + if doc == nil { + return nil, nil + } + + contributor, err := service.Get[CallHierarchyContributor](p.sc) + if err != nil { + // No contributor available + return nil, nil + } + + // Find token at cursor position + offset := doc.TextDoc.OffsetAt(params.Position) + first, _ := doc.Tokens.SearchOffset2(offset) + if first == nil { + return nil, nil + } + + // Get the AST node for the token and delegate to the contributor + node := first.Element + item := contributor.PrepareItem(ctx, first, node) + if item == nil { + return nil, nil + } + + return []lsp.CallHierarchyItem{*item}, nil +} + +func (p *DefaultCallHierarchyProvider) HandleIncomingCallsRequest(ctx context.Context, params *lsp.CallHierarchyIncomingCallsParams) ([]lsp.CallHierarchyIncomingCall, error) { + contributor, err := service.Get[CallHierarchyContributor](p.sc) + if err != nil { + // No contributor available + return nil, nil + } + + incomingCalls := contributor.FindIncomingCalls(ctx, ¶ms.Item) + + return incomingCalls, nil +} + +func (p *DefaultCallHierarchyProvider) HandleOutgoingCallsRequest(ctx context.Context, params *lsp.CallHierarchyOutgoingCallsParams) ([]lsp.CallHierarchyOutgoingCall, error) { + contributor, err := service.Get[CallHierarchyContributor](p.sc) + if err != nil { + // No contributor available + return nil, nil + } + + outgoingCalls := contributor.FindOutgoingCalls(ctx, ¶ms.Item) + + return outgoingCalls, nil +} diff --git a/server/code_action_provider.go b/server/code_action_provider.go new file mode 100644 index 0000000..4e3ec24 --- /dev/null +++ b/server/code_action_provider.go @@ -0,0 +1,45 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + "typefox.dev/fastbelt/util/service" + "typefox.dev/lsp" +) + +// CodeActionProvider is a service for handling LSP code action requests. +// +// Usage: +// +// type MyCodeActionProvider struct{ sc *service.Container } +// +// func (p *MyCodeActionProvider) HandleCodeActionRequest(ctx context.Context, params *lsp.CodeActionParams) ([]lsp.CodeAction, error) { +// // Analyze diagnostics and context, return quick fixes +// return []lsp.CodeAction{ +// { +// Title: "Add missing import", +// Kind: lsp.QuickFix, +// Edit: &lsp.WorkspaceEdit{...}, +// }, +// }, nil +// } +type CodeActionProvider interface { + HandleCodeActionRequest(ctx context.Context, params *lsp.CodeActionParams) ([]lsp.CodeAction, error) +} + +// DefaultCodeActionProvider returns no code actions. +type DefaultCodeActionProvider struct { + sc *service.Container +} + +func NewDefaultCodeActionProvider(sc *service.Container) CodeActionProvider { + return &DefaultCodeActionProvider{sc: sc} +} + +func (p *DefaultCodeActionProvider) HandleCodeActionRequest(ctx context.Context, params *lsp.CodeActionParams) ([]lsp.CodeAction, error) { + return []lsp.CodeAction{}, nil +} diff --git a/server/code_lens_provider.go b/server/code_lens_provider.go new file mode 100644 index 0000000..0ccd8a6 --- /dev/null +++ b/server/code_lens_provider.go @@ -0,0 +1,47 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + "typefox.dev/fastbelt/util/service" + "typefox.dev/lsp" +) + +// CodeLensProvider is a service for handling LSP code lens requests. +// +// Usage: +// +// type MyCodeLensProvider struct{ sc *service.Container } +// +// func (p *MyCodeLensProvider) HandleCodeLensRequest(ctx context.Context, params *lsp.CodeLensParams) ([]lsp.CodeLens, error) { +// // Analyze document and return inline commands/info +// return []lsp.CodeLens{ +// { +// Range: lsp.Range{...}, +// Command: &lsp.Command{ +// Title: "3 references", +// Command: "showReferences", +// }, +// }, +// }, nil +// } +type CodeLensProvider interface { + HandleCodeLensRequest(ctx context.Context, params *lsp.CodeLensParams) ([]lsp.CodeLens, error) +} + +// DefaultCodeLensProvider returns no code lenses. +type DefaultCodeLensProvider struct { + sc *service.Container +} + +func NewDefaultCodeLensProvider(sc *service.Container) CodeLensProvider { + return &DefaultCodeLensProvider{sc: sc} +} + +func (p *DefaultCodeLensProvider) HandleCodeLensRequest(ctx context.Context, params *lsp.CodeLensParams) ([]lsp.CodeLens, error) { + return []lsp.CodeLens{}, nil +} diff --git a/server/code_lens_provider_test.go b/server/code_lens_provider_test.go new file mode 100644 index 0000000..8af62b9 --- /dev/null +++ b/server/code_lens_provider_test.go @@ -0,0 +1,152 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/internal/grammar" + "typefox.dev/fastbelt/server" + "typefox.dev/fastbelt/test" + "typefox.dev/fastbelt/util/service" + "typefox.dev/fastbelt/workspace" + "typefox.dev/lsp" +) + +func TestCodeLensProvider_Default(t *testing.T) { + sc := service.NewContainer() + server.SetupDefaultServices(sc) + sc.Seal() + + provider := service.MustGet[server.CodeLensProvider](sc) + result, err := provider.HandleCodeLensRequest(context.Background(), &lsp.CodeLensParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: "file:///test.txt"}, + }) + + assert.NoError(t, err) + assert.Empty(t, result) +} + +func TestCodeLensProvider_DefaultWithGrammar(t *testing.T) { + // Test that the default provider returns empty code lenses + sc := service.NewContainer() + grammar.SetupServices(sc) + server.SetupDefaultServices(sc) + sc.Seal() + + f := test.New(t, sc) + doc := f.Parse(` + grammar Test; + interface Person { Name string } + Person: Name=ID; + ` + commonTokens) + doc.AssertNoErrors() + + provider := service.MustGet[server.CodeLensProvider](f.Services()) + result, err := provider.HandleCodeLensRequest( + context.Background(), + &lsp.CodeLensParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: doc.Document.URI.DocumentURI()}, + }, + ) + + assert.NoError(t, err) + assert.Empty(t, result, "Default provider should return no code lenses") +} + +func TestCodeLensProvider_CustomImplementation(t *testing.T) { + // Create services and register custom provider before sealing + sc := service.NewContainer() + grammar.SetupServices(sc) + server.SetupDefaultServices(sc) + + // Register custom provider (provider-only pattern - full implementation) + service.Override[server.CodeLensProvider](sc, &grammarCodeLensProvider{sc: sc}) + sc.Seal() + + f := test.New(t, sc) + doc := f.Parse(` + grammar Test; + interface Person { Name string } + Person: Name=ID; + ` + commonTokens) + doc.AssertNoErrors() + + provider := service.MustGet[server.CodeLensProvider](f.Services()) + result, err := provider.HandleCodeLensRequest( + context.Background(), + &lsp.CodeLensParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: doc.Document.URI.DocumentURI()}, + }, + ) + + assert.NoError(t, err) + assert.NotEmpty(t, result, "Should find code lenses for grammar elements") + + // Verify we have code lenses for both the rule and interface + var foundRuleLens, foundInterfaceLens bool + for _, lens := range result { + if lens.Command != nil { + if lens.Command.Title == "Parser Rule" { + foundRuleLens = true + assert.Equal(t, "Person", lens.Command.Command) + } + if lens.Command.Title == "Interface Declaration" { + foundInterfaceLens = true + assert.Equal(t, "Person", lens.Command.Command) + } + } + } + assert.True(t, foundRuleLens, "Should have code lens for parser rule") + assert.True(t, foundInterfaceLens, "Should have code lens for interface") +} + +// grammarCodeLensProvider provides custom code lenses for grammar language elements. +// Provider-only pattern: adopter implements the full provider interface. +type grammarCodeLensProvider struct { + sc *service.Container +} + +func (p *grammarCodeLensProvider) HandleCodeLensRequest(ctx context.Context, params *lsp.CodeLensParams) ([]lsp.CodeLens, error) { + documentManager := service.MustGet[workspace.DocumentManager](p.sc) + uri := core.ParseURI(string(params.TextDocument.URI)) + doc := documentManager.Get(uri) + if doc == nil || doc.Root == nil { + return []lsp.CodeLens{}, nil + } + + var lenses []lsp.CodeLens + + // Iterate over all nodes to find rules and interfaces + for node := range core.AllNodes(doc.Root) { + switch n := node.(type) { + case *grammar.ParserRuleImpl: + if seg := n.Segment(); seg != nil && n.Name() != "" { + lenses = append(lenses, lsp.CodeLens{ + Range: seg.Range.LspRange(), + Command: &lsp.Command{ + Title: "Parser Rule", + Command: n.Name(), + }, + }) + } + case *grammar.InterfaceImpl: + if seg := n.Segment(); seg != nil && n.Name() != "" { + lenses = append(lenses, lsp.CodeLens{ + Range: seg.Range.LspRange(), + Command: &lsp.Command{ + Title: "Interface Declaration", + Command: n.Name(), + }, + }) + } + } + } + + return lenses, nil +} diff --git a/server/command_provider.go b/server/command_provider.go new file mode 100644 index 0000000..5f4e055 --- /dev/null +++ b/server/command_provider.go @@ -0,0 +1,45 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + "fmt" + + "typefox.dev/fastbelt/util/service" + "typefox.dev/lsp" +) + +// CommandProvider is a service for handling LSP execute command requests. +// +// Usage: +// +// type MyCommandProvider struct{ sc *service.Container } +// +// func (p *MyCommandProvider) HandleExecuteCommandRequest(ctx context.Context, params *lsp.ExecuteCommandParams) (any, error) { +// switch params.Command { +// case "myLanguage.refactor": +// // Execute refactoring with params.Arguments +// return "Refactoring completed", nil +// default: +// return nil, fmt.Errorf("unknown command: %s", params.Command) +// } +// } +type CommandProvider interface { + HandleExecuteCommandRequest(ctx context.Context, params *lsp.ExecuteCommandParams) (any, error) +} + +// DefaultCommandProvider returns an error for all commands. +type DefaultCommandProvider struct { + sc *service.Container +} + +func NewDefaultCommandProvider(sc *service.Container) CommandProvider { + return &DefaultCommandProvider{sc: sc} +} + +func (p *DefaultCommandProvider) HandleExecuteCommandRequest(ctx context.Context, params *lsp.ExecuteCommandParams) (any, error) { + return nil, fmt.Errorf("command not found: %s", params.Command) +} diff --git a/server/declaration_provider.go b/server/declaration_provider.go new file mode 100644 index 0000000..a47928d --- /dev/null +++ b/server/declaration_provider.go @@ -0,0 +1,46 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + "typefox.dev/fastbelt/util/service" + "typefox.dev/lsp" +) + +// DeclarationProvider is a service for handling LSP declaration requests. + +type DeclarationProvider interface { + HandleDeclarationRequest(ctx context.Context, params *lsp.DeclarationParams) ([]lsp.DefinitionLink, error) +} + +// DefaultDeclarationProvider is the default implementation of [DeclarationProvider]. +// Language adopters should override this service if their language distinguishes +// between declaration and definition. +type DefaultDeclarationProvider struct { + sc *service.Container +} + +func NewDefaultDeclarationProvider(sc *service.Container) DeclarationProvider { + return &DefaultDeclarationProvider{sc: sc} +} + +func (p *DefaultDeclarationProvider) HandleDeclarationRequest(ctx context.Context, params *lsp.DeclarationParams) ([]lsp.DefinitionLink, error) { + // Delegate to definition provider + definitionProvider, err := service.Get[DefinitionProvider](p.sc) + if err != nil { + // No definition provider available + return nil, nil + } + + defParams := &lsp.DefinitionParams{ + TextDocumentPositionParams: params.TextDocumentPositionParams, + WorkDoneProgressParams: params.WorkDoneProgressParams, + PartialResultParams: params.PartialResultParams, + } + + return definitionProvider.HandleDefinitionRequest(ctx, defParams) +} diff --git a/server/document_link_provider.go b/server/document_link_provider.go new file mode 100644 index 0000000..c106058 --- /dev/null +++ b/server/document_link_provider.go @@ -0,0 +1,44 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + "typefox.dev/fastbelt/util/service" + "typefox.dev/lsp" +) + +// DocumentLinkProvider is a service for handling LSP document link requests. +// +// Usage: +// +// type MyDocumentLinkProvider struct{ sc *service.Container } +// +// func (p *MyDocumentLinkProvider) HandleDocumentLinkRequest(ctx context.Context, params *lsp.DocumentLinkParams) ([]lsp.DocumentLink, error) { +// // Find URLs, file references, etc. in the document +// return []lsp.DocumentLink{ +// { +// Range: lsp.Range{...}, +// Target: "file:///path/to/file.txt", +// }, +// }, nil +// } +type DocumentLinkProvider interface { + HandleDocumentLinkRequest(ctx context.Context, params *lsp.DocumentLinkParams) ([]lsp.DocumentLink, error) +} + +// DefaultDocumentLinkProvider returns no document links. +type DefaultDocumentLinkProvider struct { + sc *service.Container +} + +func NewDefaultDocumentLinkProvider(sc *service.Container) DocumentLinkProvider { + return &DefaultDocumentLinkProvider{sc: sc} +} + +func (p *DefaultDocumentLinkProvider) HandleDocumentLinkRequest(ctx context.Context, params *lsp.DocumentLinkParams) ([]lsp.DocumentLink, error) { + return []lsp.DocumentLink{}, nil +} diff --git a/server/implementation_provider.go b/server/implementation_provider.go new file mode 100644 index 0000000..122700b --- /dev/null +++ b/server/implementation_provider.go @@ -0,0 +1,121 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/util/service" + "typefox.dev/fastbelt/workspace" + "typefox.dev/lsp" +) + +// ImplementationFilter provides language-specific filtering for implementation detection. +// +// Usage: +// +// type MyImplementationFilter struct{} +// +// func (f *MyImplementationFilter) ShouldInclude(refDesc *core.ReferenceDescription, targetNode core.AstNode) bool { +// // Check if reference is in an "implements" or "extends" clause +// return refDesc.Field == "implements" || refDesc.Field == "extends" +// } +// +// // Register with custom filter +// service.Put(sc, server.NewImplementationProviderWithFilter(sc, &MyImplementationFilter{})) +type ImplementationFilter interface { + ShouldInclude(refDesc *core.ReferenceDescription, targetNode core.AstNode) bool +} + +// DefaultImplementationFilter is the default implementation of [ImplementationFilter]. +// It includes all references as potential implementations, providing a conservative +// approximation that may include false positives. +type DefaultImplementationFilter struct{} + +func (f *DefaultImplementationFilter) ShouldInclude(refDesc *core.ReferenceDescription, targetNode core.AstNode) bool { + return true +} + +// ImplementationProvider is a service for handling LSP implementation requests. +type ImplementationProvider interface { + HandleImplementationRequest(ctx context.Context, params *lsp.ImplementationParams) ([]lsp.DefinitionLink, error) +} + +// DefaultImplementationProvider is the default implementation of [ImplementationProvider]. +// +// The default implementation finds all references to the target symbol and uses an +// [ImplementationFilter] to determine which references represent actual implementations. +type DefaultImplementationProvider struct { + sc *service.Container + filter ImplementationFilter +} + +// NewDefaultImplementationProvider creates an implementation provider with the default filter. +func NewDefaultImplementationProvider(sc *service.Container) ImplementationProvider { + return &DefaultImplementationProvider{ + sc: sc, + filter: &DefaultImplementationFilter{}, + } +} + +// NewImplementationProviderWithFilter creates an implementation provider with a custom filter. +// The filter determines which references should be considered implementations. +func NewImplementationProviderWithFilter(sc *service.Container, filter ImplementationFilter) ImplementationProvider { + return &DefaultImplementationProvider{ + sc: sc, + filter: filter, + } +} + +func (p *DefaultImplementationProvider) HandleImplementationRequest(ctx context.Context, params *lsp.ImplementationParams) ([]lsp.DefinitionLink, error) { + documentManager := service.MustGet[workspace.DocumentManager](p.sc) + uri := core.ParseURI(string(params.TextDocument.URI)) + doc := documentManager.Get(uri) + if doc == nil { + return nil, nil + } + + offset := doc.TextDoc.OffsetAt(params.Position) + first, second := doc.Tokens.SearchOffset2(offset) + if first == nil { + return nil, nil + } + + nameFinder := service.MustGet[NameFinder](p.sc) + foundName := nameFinder.Find(ctx, first, second) + if foundName.Target == nil { + return nil, nil + } + + targetNode := foundName.Target.Owner() + + referencesFinder, err := service.Get[ReferencesFinder](p.sc) + if err != nil { + // No references finder available + return nil, nil + } + + var results []lsp.DefinitionLink + sourceRange := foundName.Source.Segment().Range.LspRange() + + for refDesc := range referencesFinder.Find(ctx, targetNode, FindReferencesOptions{ + IncludeDeclaration: false, + }) { + if !p.filter.ShouldInclude(refDesc, targetNode) { + continue + } + + link := lsp.DefinitionLink{ + OriginSelectionRange: &sourceRange, + TargetURI: refDesc.SourceURI().DocumentURI(), + TargetRange: refDesc.Segment.Range.LspRange(), + TargetSelectionRange: refDesc.Segment.Range.LspRange(), + } + results = append(results, link) + } + + return results, nil +} diff --git a/server/inlay_hint_provider.go b/server/inlay_hint_provider.go new file mode 100644 index 0000000..ef5fc9c --- /dev/null +++ b/server/inlay_hint_provider.go @@ -0,0 +1,109 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/util/service" + "typefox.dev/fastbelt/workspace" + "typefox.dev/lsp" +) + +// InlayHintComputer provides per-node logic for computing inlay hints. +// Language adopters implement this lightweight interface to emit hints for specific AST nodes. +// +// Usage: +// +// type MyInlayHintComputer struct{} +// +// func (c *MyInlayHintComputer) ComputeInlayHint(ctx context.Context, node core.AstNode, accept func(lsp.InlayHint)) { +// if funcCall, ok := node.(*ast.FunctionCall); ok { +// accept(lsp.InlayHint{ +// Position: funcCall.Segment().Range.End.LspPosition(), +// Label: []lsp.InlayHintLabelPart{{Value: "paramName:"}}, +// Kind: lsp.Parameter, +// }) +// } +// } +// +// // Register provider with custom computer +// service.Put(sc, server.NewInlayHintProviderWithComputer(sc, &MyInlayHintComputer{})) +type InlayHintComputer interface { + ComputeInlayHint(ctx context.Context, node core.AstNode, accept func(lsp.InlayHint)) +} + +// DefaultInlayHintComputer is a no-op computer that emits no hints. +type DefaultInlayHintComputer struct{} + +func (c *DefaultInlayHintComputer) ComputeInlayHint(ctx context.Context, node core.AstNode, accept func(lsp.InlayHint)) { +} + +type InlayHintProvider interface { + HandleInlayHintRequest(ctx context.Context, params *lsp.InlayHintParams) ([]lsp.InlayHint, error) +} + +// DefaultInlayHintProvider iterates over all AST nodes in the requested +// range and delegates to InlayHintComputer for per-node hint computation. +type DefaultInlayHintProvider struct { + sc *service.Container + computer InlayHintComputer +} + +// NewDefaultInlayHintProvider creates an inlay hint provider with the default computer (no hints). +func NewDefaultInlayHintProvider(sc *service.Container) InlayHintProvider { + return &DefaultInlayHintProvider{ + sc: sc, + computer: &DefaultInlayHintComputer{}, + } +} + +// NewInlayHintProviderWithComputer creates an inlay hint provider with a custom computer. +// The computer determines which hints to emit for each AST node. +func NewInlayHintProviderWithComputer(sc *service.Container, computer InlayHintComputer) InlayHintProvider { + return &DefaultInlayHintProvider{ + sc: sc, + computer: computer, + } +} + +func (p *DefaultInlayHintProvider) HandleInlayHintRequest(ctx context.Context, params *lsp.InlayHintParams) ([]lsp.InlayHint, error) { + documentManager, err := service.Get[workspace.DocumentManager](p.sc) + if err != nil { + return []lsp.InlayHint{}, nil + } + + uri := core.ParseURI(string(params.TextDocument.URI)) + doc := documentManager.Get(uri) + if doc == nil || doc.Root == nil { + return []lsp.InlayHint{}, nil + } + + startOffset := doc.TextDoc.OffsetAt(params.Range.Start) + endOffset := doc.TextDoc.OffsetAt(params.Range.End) + + var hints []lsp.InlayHint + acceptor := func(hint lsp.InlayHint) { + hints = append(hints, hint) + } + + for node := range core.AllNodes(doc.Root) { + seg := node.Segment() + if seg == nil { + continue + } + nodeStart := int(seg.Indices.Start) + nodeEnd := int(seg.Indices.End) + + if nodeEnd <= startOffset || nodeStart >= endOffset { + continue + } + + p.computer.ComputeInlayHint(ctx, node, acceptor) + } + + return hints, nil +} diff --git a/server/inlay_hint_provider_test.go b/server/inlay_hint_provider_test.go new file mode 100644 index 0000000..88fac64 --- /dev/null +++ b/server/inlay_hint_provider_test.go @@ -0,0 +1,145 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/internal/grammar" + "typefox.dev/fastbelt/server" + "typefox.dev/fastbelt/test" + "typefox.dev/fastbelt/util/service" + "typefox.dev/lsp" +) + +const commonTokens = ` +token ID: /[a-zA-Z_][a-zA-Z0-9_]*/; +hidden token WS: /[ \n\r\t]+/; +` + +func TestInlayHintProvider_Default(t *testing.T) { + sc := service.NewContainer() + server.SetupDefaultServices(sc) + sc.Seal() + + provider := service.MustGet[server.InlayHintProvider](sc) + result, err := provider.HandleInlayHintRequest(context.Background(), &lsp.InlayHintParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: "file:///test.txt"}, + Range: lsp.Range{}, + }) + + assert.NoError(t, err) + assert.Empty(t, result) +} + +func TestInlayHintProvider_DefaultWithGrammar(t *testing.T) { + // Test that the default provider returns empty hints + sc := service.NewContainer() + grammar.SetupServices(sc) + server.SetupDefaultServices(sc) + sc.Seal() + + f := test.New(t, sc) + doc := f.Parse(` + grammar Test; + interface Person { Name string } + Person: Name=ID; + ` + commonTokens) + doc.AssertNoErrors() + + provider := service.MustGet[server.InlayHintProvider](f.Services()) + result, err := provider.HandleInlayHintRequest( + context.Background(), + &lsp.InlayHintParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: doc.Document.URI.DocumentURI()}, + Range: lsp.Range{ + Start: lsp.Position{Line: 0, Character: 0}, + End: lsp.Position{Line: 10, Character: 0}, + }, + }, + ) + + assert.NoError(t, err) + assert.Empty(t, result, "Default provider should return no hints") +} + +func TestInlayHintProvider_CustomImplementation(t *testing.T) { + // Create services and register provider with custom computer before sealing + sc := service.NewContainer() + grammar.SetupServices(sc) + server.SetupDefaultServices(sc) + + // Register provider with custom computer (lightweight filter-like pattern) + service.Override(sc, server.NewInlayHintProviderWithComputer(sc, &grammarInlayHintComputer{})) + sc.Seal() + + f := test.New(t, sc) + doc := f.Parse(` + grammar Test; + interface Person { Name string } + Person: Name=ID; + ` + commonTokens) + doc.AssertNoErrors() + + provider := service.MustGet[server.InlayHintProvider](f.Services()) + result, err := provider.HandleInlayHintRequest( + context.Background(), + &lsp.InlayHintParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: doc.Document.URI.DocumentURI()}, + Range: lsp.Range{ + Start: lsp.Position{Line: 0, Character: 0}, + End: lsp.Position{Line: 10, Character: 0}, + }, + }, + ) + + assert.NoError(t, err) + assert.NotEmpty(t, result, "Should find inlay hints for grammar rules") + + // Verify we have hints for both the rule and interface + var foundRuleTypeHint, foundInterfaceTypeHint bool + for _, hint := range result { + for _, part := range hint.Label { + if part.Value == ": ParserRule" { + foundRuleTypeHint = true + assert.Equal(t, lsp.Type, hint.Kind) + } + if part.Value == ": Interface" { + foundInterfaceTypeHint = true + assert.Equal(t, lsp.Type, hint.Kind) + } + } + } + assert.True(t, foundRuleTypeHint, "Should have type hint for parser rule") + assert.True(t, foundInterfaceTypeHint, "Should have type hint for interface") +} + +// grammarInlayHintComputer provides custom inlay hints for grammar language nodes. +// This is a lightweight filter-like interface, not a separate service. +type grammarInlayHintComputer struct{} + +func (c *grammarInlayHintComputer) ComputeInlayHint(ctx context.Context, node core.AstNode, accept func(lsp.InlayHint)) { + switch n := node.(type) { + case *grammar.ParserRuleImpl: + if seg := n.Segment(); seg != nil && n.Name() != "" { + accept(lsp.InlayHint{ + Position: seg.Range.End.LspPosition(), + Label: []lsp.InlayHintLabelPart{{Value: ": ParserRule"}}, + Kind: lsp.Type, + }) + } + case *grammar.InterfaceImpl: + if seg := n.Segment(); seg != nil && n.Name() != "" { + accept(lsp.InlayHint{ + Position: seg.Range.End.LspPosition(), + Label: []lsp.InlayHintLabelPart{{Value: ": Interface"}}, + Kind: lsp.Type, + }) + } + } +} diff --git a/server/semantic_tokens_contributor.go b/server/semantic_tokens_contributor.go new file mode 100644 index 0000000..61a1892 --- /dev/null +++ b/server/semantic_tokens_contributor.go @@ -0,0 +1,111 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" +) + +// SemanticTokensContributor provides language-specific logic for semantic token classification. +// +// Usage: +// +// type MySemanticTokensContributor struct{} +// +// const ( +// TokenTypeClass = iota +// TokenTypeFunction +// TokenTypeVariable +// TokenTypeKeyword +// ) +// +// const ( +// TokenModifierReadonly = iota +// TokenModifierDeprecated +// ) +// +// func (c *MySemanticTokensContributor) TokenTypes() []string { +// return []string{"class", "function", "variable", "keyword"} +// } +// +// func (c *MySemanticTokensContributor) TokenModifiers() []string { +// return []string{"readonly", "deprecated"} +// } +// +// func (c *MySemanticTokensContributor) ClassifyToken(ctx context.Context, token *core.Token, node core.AstNode) int { +// if _, ok := node.(*ast.ClassDeclaration); ok { +// return TokenTypeClass +// } +// return -1 // Not classified +// } +// +// func (c *MySemanticTokensContributor) GetModifiers(ctx context.Context, token *core.Token, node core.AstNode) []int { +// if varDecl, ok := node.(*ast.VarDeclaration); ok && varDecl.Const { +// return []int{TokenModifierReadonly} +// } +// return nil +// } +type SemanticTokensContributor interface { + // TokenTypes returns the semantic token types supported by this contributor. + // The order matters: it defines the token type indices used in LSP encoding. + // + // Standard LSP token types include: + // - "namespace", "class", "enum", "interface", "struct", "typeParameter" + // - "type", "parameter", "variable", "property", "enumMember" + // - "function", "method", "macro", "keyword", "comment" + // - "string", "number", "regexp", "operator" + // + // Return an empty array to disable semantic tokens. + TokenTypes() []string + + // TokenModifiers returns the semantic token modifiers supported by this contributor. + // Modifiers are bit flags that can be combined. + // + // Standard LSP token modifiers include: + // - "declaration", "definition", "readonly", "static" + // - "deprecated", "abstract", "async", "modification" + // - "documentation", "defaultLibrary" + // + // Return an empty array if no modifiers are used. + TokenModifiers() []string + + // ClassifyToken determines the semantic token type for a lexer token and its AST node. + // + // Return the token type index (position in TokenTypes() array), or -1 if + // the token should not receive semantic highlighting. + ClassifyToken(ctx context.Context, token *core.Token, node core.AstNode) int + + // GetModifiers determines which semantic token modifiers apply to a token. + // + // Return modifier indices (positions in TokenModifiers() array), or nil/empty if + // no modifiers apply. Multiple modifiers can be returned and will be combined. + GetModifiers(ctx context.Context, token *core.Token, node core.AstNode) []int +} + +// DefaultSemanticTokensContributor is the default implementation of [SemanticTokensContributor]. +// It returns empty token types and modifiers, effectively disabling semantic tokens. +type DefaultSemanticTokensContributor struct{} + +func NewDefaultSemanticTokensContributor() SemanticTokensContributor { + return &DefaultSemanticTokensContributor{} +} + +func (c *DefaultSemanticTokensContributor) TokenTypes() []string { + return []string{} +} + +func (c *DefaultSemanticTokensContributor) TokenModifiers() []string { + return []string{} +} + +func (c *DefaultSemanticTokensContributor) ClassifyToken(ctx context.Context, token *core.Token, node core.AstNode) int { + return -1 +} + +func (c *DefaultSemanticTokensContributor) GetModifiers(ctx context.Context, token *core.Token, node core.AstNode) []int { + return nil +} diff --git a/server/semantic_tokens_provider.go b/server/semantic_tokens_provider.go new file mode 100644 index 0000000..e15a211 --- /dev/null +++ b/server/semantic_tokens_provider.go @@ -0,0 +1,151 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/util/service" + "typefox.dev/fastbelt/workspace" + "typefox.dev/lsp" +) + +// SemanticTokensProvider is a service for handling LSP semantic tokens requests. +type SemanticTokensProvider interface { + HandleSemanticTokensFullRequest(ctx context.Context, params *lsp.SemanticTokensParams) (*lsp.SemanticTokens, error) + HandleSemanticTokensRangeRequest(ctx context.Context, params *lsp.SemanticTokensRangeParams) (*lsp.SemanticTokens, error) + HandleSemanticTokensFullDeltaRequest(ctx context.Context, params *lsp.SemanticTokensDeltaParams) (any, error) +} + +// DefaultSemanticTokensProvider is the default implementation of [SemanticTokensProvider]. +// It delegates token classification to [SemanticTokensContributor] and handles LSP encoding. +type DefaultSemanticTokensProvider struct { + sc *service.Container +} + +func NewDefaultSemanticTokensProvider(sc *service.Container) SemanticTokensProvider { + return &DefaultSemanticTokensProvider{sc: sc} +} + +func (p *DefaultSemanticTokensProvider) HandleSemanticTokensFullRequest(ctx context.Context, params *lsp.SemanticTokensParams) (*lsp.SemanticTokens, error) { + return p.encodeSemanticTokens(ctx, string(params.TextDocument.URI), nil) +} + +func (p *DefaultSemanticTokensProvider) HandleSemanticTokensRangeRequest(ctx context.Context, params *lsp.SemanticTokensRangeParams) (*lsp.SemanticTokens, error) { + return p.encodeSemanticTokens(ctx, string(params.TextDocument.URI), ¶ms.Range) +} + +func (p *DefaultSemanticTokensProvider) HandleSemanticTokensFullDeltaRequest(ctx context.Context, params *lsp.SemanticTokensDeltaParams) (any, error) { + // Delta support is not implemented. Since we recompute the entire document + // on every change, delta computation would not provide meaningful benefits. + // Return full tokens instead (same approach as Langium's default). + // Future enhancement: cache token builders per document and compute diffs. + return p.encodeSemanticTokens(ctx, string(params.TextDocument.URI), nil) +} + +func (p *DefaultSemanticTokensProvider) encodeSemanticTokens(ctx context.Context, uriStr string, lspRange *lsp.Range) (*lsp.SemanticTokens, error) { + documentManager := service.MustGet[workspace.DocumentManager](p.sc) + uri := core.ParseURI(uriStr) + doc := documentManager.Get(uri) + if doc == nil { + return nil, nil + } + + contributor, err := service.Get[SemanticTokensContributor](p.sc) + if err != nil || len(contributor.TokenTypes()) == 0 { + return &lsp.SemanticTokens{Data: []uint32{}}, nil + } + + data := p.encodeTokens(ctx, doc, lspRange, contributor) + return &lsp.SemanticTokens{Data: data}, nil +} + +// encodeTokens traverses document tokens and encodes them in LSP format. +func (p *DefaultSemanticTokensProvider) encodeTokens( + ctx context.Context, + doc *core.Document, + lspRange *lsp.Range, + contributor SemanticTokensContributor, +) []uint32 { + var data []uint32 + var prevLine, prevChar uint32 + + // Determine token range + var startOffset, endOffset int + if lspRange != nil { + startOffset = doc.TextDoc.OffsetAt(lspRange.Start) + endOffset = doc.TextDoc.OffsetAt(lspRange.End) + } else { + startOffset = 0 + endOffset = len(doc.TextDoc.Content()) + } + + // Traverse all tokens + for i := 0; i < len(doc.Tokens); i++ { + token := &doc.Tokens[i] + tokenStart := token.TextSegment.Indices.Start + tokenEnd := token.TextSegment.Indices.End + + // Skip tokens outside the range + if int(tokenEnd) <= startOffset || int(tokenStart) >= endOffset { + continue + } + + // Get the AST node for this token + node := token.Element + + // Classify the token + typeIndex := contributor.ClassifyToken(ctx, token, node) + if typeIndex < 0 { + continue + } + + // Get modifiers + modifiers := contributor.GetModifiers(ctx, token, node) + modifierBits := encodeModifiers(modifiers) + + // Get token position + tokenRange := token.TextSegment.Range + line := uint32(tokenRange.Start.Line) + char := uint32(tokenRange.Start.Column) + length := uint32(tokenEnd - tokenStart) + + // Compute deltas + deltaLine := line - prevLine + var deltaStart uint32 + if deltaLine == 0 { + deltaStart = char - prevChar + } else { + deltaStart = char + } + + // Append encoded token (5 integers) + data = append(data, + deltaLine, + deltaStart, + length, + uint32(typeIndex), + modifierBits, + ) + + // Update previous position + prevLine = line + prevChar = char + } + + return data +} + +// encodeModifiers combines modifier indices into a bit field. +func encodeModifiers(modifiers []int) uint32 { + var bits uint32 + for _, index := range modifiers { + if index >= 0 && index < 32 { + bits |= (1 << index) + } + } + return bits +} diff --git a/server/semantic_tokens_provider_test.go b/server/semantic_tokens_provider_test.go new file mode 100644 index 0000000..1978137 --- /dev/null +++ b/server/semantic_tokens_provider_test.go @@ -0,0 +1,193 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/internal/grammar" + "typefox.dev/fastbelt/server" + "typefox.dev/fastbelt/test" + "typefox.dev/fastbelt/util/service" + "typefox.dev/lsp" +) + +func TestSemanticTokensProvider_Default(t *testing.T) { + sc := service.NewContainer() + server.SetupDefaultServices(sc) + sc.Seal() + + // Just verify the service exists + provider := service.MustGet[server.SemanticTokensProvider](sc) + assert.NotNil(t, provider) + + // Also verify the default contributor + contributor := service.MustGet[server.SemanticTokensContributor](sc) + assert.NotNil(t, contributor) + assert.Empty(t, contributor.TokenTypes(), "Default contributor should return empty token types") +} + +func TestSemanticTokensProvider_DefaultWithGrammar(t *testing.T) { + // Test that the default contributor returns empty tokens + sc := service.NewContainer() + grammar.SetupServices(sc) + server.SetupDefaultServices(sc) + sc.Seal() + + f := test.New(t, sc) + doc := f.Parse(` + grammar Test; + interface Person { Name string } + Person: Name=ID; + ` + commonTokens) + doc.AssertNoErrors() + + provider := service.MustGet[server.SemanticTokensProvider](f.Services()) + result, err := provider.HandleSemanticTokensFullRequest( + context.Background(), + &lsp.SemanticTokensParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: doc.Document.URI.DocumentURI()}, + }, + ) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result.Data, "Default contributor should return no tokens") +} + +func TestSemanticTokensProvider_CustomContributor(t *testing.T) { + // Create services and register custom contributor before sealing + sc := service.NewContainer() + grammar.SetupServices(sc) + server.SetupDefaultServices(sc) + + // Register custom contributor (contributor pattern - separate service) + service.Override[server.SemanticTokensContributor](sc, &grammarSemanticTokensContributor{}) + sc.Seal() + + f := test.New(t, sc) + doc := f.Parse(` + grammar Test; + interface Person { Name string } + Person: Name=ID; + ` + commonTokens) + doc.AssertNoErrors() + + provider := service.MustGet[server.SemanticTokensProvider](f.Services()) + result, err := provider.HandleSemanticTokensFullRequest( + context.Background(), + &lsp.SemanticTokensParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: doc.Document.URI.DocumentURI()}, + }, + ) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.NotEmpty(t, result.Data, "Should find semantic tokens for grammar elements") + + // Semantic tokens are encoded as 5-integer tuples: [deltaLine, deltaStart, length, tokenType, tokenModifiers] + assert.Equal(t, 0, len(result.Data)%5, "Data should be 5-integer tuples") + assert.Greater(t, len(result.Data), 0, "Should have at least one token") +} + +func TestSemanticTokensProvider_TokenTypesAndModifiers(t *testing.T) { + // Test that contributor provides token types and modifiers + sc := service.NewContainer() + grammar.SetupServices(sc) + server.SetupDefaultServices(sc) + + contributor := &grammarSemanticTokensContributor{} + service.Override[server.SemanticTokensContributor](sc, contributor) + sc.Seal() + + // Verify token types and modifiers are exposed + tokenTypes := contributor.TokenTypes() + assert.NotEmpty(t, tokenTypes) + assert.Contains(t, tokenTypes, "keyword") + assert.Contains(t, tokenTypes, "type") + + tokenModifiers := contributor.TokenModifiers() + assert.NotEmpty(t, tokenModifiers) + assert.Contains(t, tokenModifiers, "declaration") +} + +// grammarSemanticTokensContributor provides custom semantic tokens for grammar language. +type grammarSemanticTokensContributor struct{} + +// Token type indices (must match TokenTypes() order) +const ( + TokenTypeKeyword = iota + TokenTypeType + TokenTypeProperty +) + +// Token modifier indices (must match TokenModifiers() order) +const ( + TokenModifierDeclaration = iota +) + +func (c *grammarSemanticTokensContributor) TokenTypes() []string { + return []string{ + "keyword", // 0 + "type", // 1 + "property", // 2 + } +} + +func (c *grammarSemanticTokensContributor) TokenModifiers() []string { + return []string{ + "declaration", // 0 + } +} + +func (c *grammarSemanticTokensContributor) ClassifyToken(ctx context.Context, token *core.Token, node core.AstNode) int { + if token == nil || token.Type == nil { + return -1 + } + + // Classify keywords + switch token.Type.Name { + case "grammar", "interface", "returns", "token", "hidden": + return TokenTypeKeyword + } + + // Classify based on AST node type + switch n := node.(type) { + case *grammar.InterfaceImpl: + // Interface name is a type + if token.Element == n { + return TokenTypeType + } + case *grammar.ParserRuleImpl: + // Rule name is a type + if token.Element == n { + return TokenTypeType + } + case *grammar.FieldImpl: + // Field name is a property + if token.Element == n { + return TokenTypeProperty + } + } + + return -1 +} + +func (c *grammarSemanticTokensContributor) GetModifiers(ctx context.Context, token *core.Token, node core.AstNode) []int { + if token == nil { + return nil + } + + // Mark declarations + switch node.(type) { + case *grammar.InterfaceImpl, *grammar.ParserRuleImpl, *grammar.FieldImpl: + return []int{TokenModifierDeclaration} + } + + return nil +} diff --git a/server/server.go b/server/server.go index fa38a4e..6916194 100644 --- a/server/server.go +++ b/server/server.go @@ -81,10 +81,90 @@ func (s *DefaultLanguageServer) Initialize(ctx context.Context, params *lsp.Para Value: service.Has[ReferencesProvider](s.sc), }, RenameProvider: service.Has[RenameProvider](s.sc), + DeclarationProvider: &lsp.Or_ServerCapabilities_declarationProvider{ + Value: service.Has[DeclarationProvider](s.sc), + }, + ImplementationProvider: &lsp.Or_ServerCapabilities_implementationProvider{ + Value: service.Has[ImplementationProvider](s.sc), + }, + TypeDefinitionProvider: &lsp.Or_ServerCapabilities_typeDefinitionProvider{ + Value: service.Has[TypeDefinitionProvider](s.sc), + }, + SemanticTokensProvider: buildSemanticTokensOptions(s.sc), + CallHierarchyProvider: &lsp.Or_ServerCapabilities_callHierarchyProvider{ + Value: service.Has[CallHierarchyProvider](s.sc), + }, + TypeHierarchyProvider: &lsp.Or_ServerCapabilities_typeHierarchyProvider{ + Value: service.Has[TypeHierarchyProvider](s.sc), + }, + InlayHintProvider: func() *lsp.Or_ServerCapabilities_inlayHintProvider { + if service.Has[InlayHintProvider](s.sc) { + return &lsp.Or_ServerCapabilities_inlayHintProvider{Value: &lsp.InlayHintOptions{}} + } + return nil + }(), + SignatureHelpProvider: buildSignatureHelpOptions(s.sc), + CodeActionProvider: func() *lsp.CodeActionOptions { + if service.Has[CodeActionProvider](s.sc) { + return &lsp.CodeActionOptions{} + } + return nil + }(), + CodeLensProvider: func() *lsp.CodeLensOptions { + if service.Has[CodeLensProvider](s.sc) { + return &lsp.CodeLensOptions{ResolveProvider: false} + } + return nil + }(), + DocumentLinkProvider: func() *lsp.DocumentLinkOptions { + if service.Has[DocumentLinkProvider](s.sc) { + return &lsp.DocumentLinkOptions{ResolveProvider: false} + } + return nil + }(), + ExecuteCommandProvider: func() *lsp.ExecuteCommandOptions { + if service.Has[CommandProvider](s.sc) { + return &lsp.ExecuteCommandOptions{Commands: []string{}} + } + return nil + }(), }, }, nil } +func buildSemanticTokensOptions(sc *service.Container) *lsp.SemanticTokensOptions { + if !service.Has[SemanticTokensProvider](sc) { + return nil + } + contributor, err := service.Get[SemanticTokensContributor](sc) + if err != nil || len(contributor.TokenTypes()) == 0 { + return nil + } + return &lsp.SemanticTokensOptions{ + Legend: lsp.SemanticTokensLegend{ + TokenTypes: contributor.TokenTypes(), + TokenModifiers: contributor.TokenModifiers(), + }, + Full: &lsp.Or_SemanticTokensOptions_full{Value: true}, + Range: &lsp.Or_SemanticTokensOptions_range{Value: true}, + } +} + +func buildSignatureHelpOptions(sc *service.Container) *lsp.SignatureHelpOptions { + provider, err := service.Get[SignatureHelpProvider](sc) + if err != nil { + return nil + } + triggerChars := provider.TriggerCharacters() + if len(triggerChars) == 0 { + return nil + } + return &lsp.SignatureHelpOptions{ + TriggerCharacters: triggerChars, + RetriggerCharacters: provider.RetriggerCharacters(), + } +} + func (s *DefaultLanguageServer) Initialized(ctx context.Context, params *lsp.InitializedParams) error { if initializer, err := service.Get[workspace.Initializer](s.sc); err == nil { log.Print("LS Initializer running...") @@ -215,10 +295,40 @@ func (s *DefaultLanguageServer) SetTrace(ctx context.Context, params *lsp.SetTra return nil } func (s *DefaultLanguageServer) IncomingCalls(ctx context.Context, params *lsp.CallHierarchyIncomingCallsParams) ([]lsp.CallHierarchyIncomingCall, error) { - return nil, nil + var result []lsp.CallHierarchyIncomingCall + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[CallHierarchyProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleIncomingCallsRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) OutgoingCalls(ctx context.Context, params *lsp.CallHierarchyOutgoingCallsParams) ([]lsp.CallHierarchyOutgoingCall, error) { - return nil, nil + var result []lsp.CallHierarchyOutgoingCall + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[CallHierarchyProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleOutgoingCallsRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) ResolveCodeAction(ctx context.Context, params *lsp.CodeAction) (*lsp.CodeAction, error) { return nil, nil @@ -248,16 +358,61 @@ func (s *DefaultLanguageServer) DidSaveNotebookDocument(ctx context.Context, par return nil } func (s *DefaultLanguageServer) CodeAction(ctx context.Context, params *lsp.CodeActionParams) ([]lsp.CodeAction, error) { - return nil, nil + var result []lsp.CodeAction + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[CodeActionProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleCodeActionRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) CodeLens(ctx context.Context, params *lsp.CodeLensParams) ([]lsp.CodeLens, error) { - return nil, nil + var result []lsp.CodeLens + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[CodeLensProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleCodeLensRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) ColorPresentation(ctx context.Context, params *lsp.ColorPresentationParams) ([]lsp.ColorPresentation, error) { return nil, nil } func (s *DefaultLanguageServer) Declaration(ctx context.Context, params *lsp.DeclarationParams) ([]lsp.DefinitionLink, error) { - return nil, nil + var result []lsp.DefinitionLink + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[DeclarationProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleDeclarationRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) Diagnostic(ctx context.Context, params *lsp.DocumentDiagnosticParams) (*lsp.DocumentDiagnosticReport, error) { return nil, nil @@ -284,7 +439,22 @@ func (s *DefaultLanguageServer) DocumentHighlight(ctx context.Context, params *l return result, providerErr } func (s *DefaultLanguageServer) DocumentLink(ctx context.Context, params *lsp.DocumentLinkParams) ([]lsp.DocumentLink, error) { - return nil, nil + var result []lsp.DocumentLink + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[DocumentLinkProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleDocumentLinkRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) DocumentSymbol(ctx context.Context, params *lsp.DocumentSymbolParams) ([]any, error) { var result []lsp.DocumentSymbol @@ -344,10 +514,40 @@ func (s *DefaultLanguageServer) Hover(ctx context.Context, params *lsp.HoverPara return result, providerErr } func (s *DefaultLanguageServer) Implementation(ctx context.Context, params *lsp.ImplementationParams) ([]lsp.DefinitionLink, error) { - return nil, nil + var result []lsp.DefinitionLink + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[ImplementationProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleImplementationRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) InlayHint(ctx context.Context, params *lsp.InlayHintParams) ([]lsp.InlayHint, error) { - return nil, nil + var result []lsp.InlayHint + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[InlayHintProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleInlayHintRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) InlineCompletion(ctx context.Context, params *lsp.InlineCompletionParams) (*lsp.Or_Result_textDocument_inlineCompletion, error) { return nil, nil @@ -380,7 +580,11 @@ func (s *DefaultLanguageServer) DidRenameFiles(ctx context.Context, params *lsp. return nil } func (s *DefaultLanguageServer) ExecuteCommand(ctx context.Context, params *lsp.ExecuteCommandParams) (any, error) { - return nil, nil + provider, err := service.Get[CommandProvider](s.sc) + if err != nil { + return nil, nil + } + return provider.HandleExecuteCommandRequest(ctx, params) } func (s *DefaultLanguageServer) Symbol(ctx context.Context, params *lsp.WorkspaceSymbolParams) ([]lsp.SymbolInformation, error) { var result []lsp.SymbolInformation @@ -422,7 +626,22 @@ func (s *DefaultLanguageServer) OnTypeFormatting(ctx context.Context, params *ls return nil, nil } func (s *DefaultLanguageServer) PrepareCallHierarchy(ctx context.Context, params *lsp.CallHierarchyPrepareParams) ([]lsp.CallHierarchyItem, error) { - return nil, nil + var result []lsp.CallHierarchyItem + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[CallHierarchyProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandlePrepareCallHierarchyRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) PrepareRename(ctx context.Context, params *lsp.PrepareRenameParams) (*lsp.PrepareRenameResult, error) { lock, err := service.Get[workspace.Lock](s.sc) @@ -443,7 +662,22 @@ func (s *DefaultLanguageServer) PrepareRename(ctx context.Context, params *lsp.P return result, providerErr } func (s *DefaultLanguageServer) PrepareTypeHierarchy(ctx context.Context, params *lsp.TypeHierarchyPrepareParams) ([]lsp.TypeHierarchyItem, error) { - return nil, nil + var result []lsp.TypeHierarchyItem + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[TypeHierarchyProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandlePrepareTypeHierarchyRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) RangeFormatting(ctx context.Context, params *lsp.DocumentRangeFormattingParams) ([]lsp.TextEdit, error) { return nil, nil @@ -473,25 +707,130 @@ func (s *DefaultLanguageServer) SelectionRange(ctx context.Context, params *lsp. return nil, nil } func (s *DefaultLanguageServer) SemanticTokensFull(ctx context.Context, params *lsp.SemanticTokensParams) (*lsp.SemanticTokens, error) { - return nil, nil + var result *lsp.SemanticTokens + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[SemanticTokensProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleSemanticTokensFullRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) SemanticTokensFullDelta(ctx context.Context, params *lsp.SemanticTokensDeltaParams) (any, error) { - return nil, nil + var result any + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[SemanticTokensProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleSemanticTokensFullDeltaRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) SemanticTokensRange(ctx context.Context, params *lsp.SemanticTokensRangeParams) (*lsp.SemanticTokens, error) { - return nil, nil + var result *lsp.SemanticTokens + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[SemanticTokensProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleSemanticTokensRangeRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) SignatureHelp(ctx context.Context, params *lsp.SignatureHelpParams) (*lsp.SignatureHelp, error) { - return nil, nil + var result *lsp.SignatureHelp + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[SignatureHelpProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleSignatureHelpRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) TypeDefinition(ctx context.Context, params *lsp.TypeDefinitionParams) ([]lsp.DefinitionLink, error) { - return nil, nil + var result []lsp.DefinitionLink + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[TypeDefinitionProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleTypeDefinitionRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) Subtypes(ctx context.Context, params *lsp.TypeHierarchySubtypesParams) ([]lsp.TypeHierarchyItem, error) { - return nil, nil + var result []lsp.TypeHierarchyItem + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[TypeHierarchyProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleSubtypesRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) Supertypes(ctx context.Context, params *lsp.TypeHierarchySupertypesParams) ([]lsp.TypeHierarchyItem, error) { - return nil, nil + var result []lsp.TypeHierarchyItem + var providerErr error + lock, err := service.Get[workspace.Lock](s.sc) + if err != nil { + return nil, err + } + provider, err := service.Get[TypeHierarchyProvider](s.sc) + if err != nil { + return nil, nil + } + if err := lock.Read(ctx, func(ctx context.Context) { + result, providerErr = provider.HandleSupertypesRequest(ctx, params) + }); err != nil { + return nil, err + } + return result, providerErr } func (s *DefaultLanguageServer) WorkDoneProgressCancel(ctx context.Context, params *lsp.WorkDoneProgressCancelParams) error { return nil diff --git a/server/services.go b/server/services.go index 4a5c9cb..b90377d 100644 --- a/server/services.go +++ b/server/services.go @@ -81,11 +81,56 @@ func SetupDefaultServices(sc *service.Container) { } if !service.Has[WorkspaceSymbolProvider](sc) { service.Put(sc, NewDefaultWorkspaceSymbolProvider(sc)) - if !service.Has[DocumentationProvider](sc) { - service.Put(sc, NewDefaultDocumentationProvider()) - } - if !service.Has[HoverProvider](sc) { - service.Put(sc, NewDefaultHoverProvider(sc)) - } + } + if !service.Has[DocumentationProvider](sc) { + service.Put(sc, NewDefaultDocumentationProvider()) + } + if !service.Has[HoverProvider](sc) { + service.Put(sc, NewDefaultHoverProvider(sc)) + } + if !service.Has[DeclarationProvider](sc) { + service.Put(sc, NewDefaultDeclarationProvider(sc)) + } + if !service.Has[ImplementationProvider](sc) { + service.Put(sc, NewDefaultImplementationProvider(sc)) + } + if !service.Has[TypeDefinitionProvider](sc) { + service.Put(sc, NewDefaultTypeDefinitionProvider(sc)) + } + if !service.Has[SemanticTokensProvider](sc) { + service.Put(sc, NewDefaultSemanticTokensProvider(sc)) + } + if !service.Has[CallHierarchyProvider](sc) { + service.Put(sc, NewDefaultCallHierarchyProvider(sc)) + } + if !service.Has[TypeHierarchyProvider](sc) { + service.Put(sc, NewDefaultTypeHierarchyProvider(sc)) + } + if !service.Has[InlayHintProvider](sc) { + service.Put(sc, NewDefaultInlayHintProvider(sc)) + } + if !service.Has[SignatureHelpProvider](sc) { + service.Put(sc, NewDefaultSignatureHelpProvider(sc)) + } + if !service.Has[SemanticTokensContributor](sc) { + service.Put(sc, NewDefaultSemanticTokensContributor()) + } + if !service.Has[CallHierarchyContributor](sc) { + service.Put(sc, NewDefaultCallHierarchyContributor()) + } + if !service.Has[TypeHierarchyContributor](sc) { + service.Put(sc, NewDefaultTypeHierarchyContributor()) + } + if !service.Has[CodeActionProvider](sc) { + service.Put(sc, NewDefaultCodeActionProvider(sc)) + } + if !service.Has[CodeLensProvider](sc) { + service.Put(sc, NewDefaultCodeLensProvider(sc)) + } + if !service.Has[DocumentLinkProvider](sc) { + service.Put(sc, NewDefaultDocumentLinkProvider(sc)) + } + if !service.Has[CommandProvider](sc) { + service.Put(sc, NewDefaultCommandProvider(sc)) } } diff --git a/server/signature_help_provider.go b/server/signature_help_provider.go new file mode 100644 index 0000000..7e4d099 --- /dev/null +++ b/server/signature_help_provider.go @@ -0,0 +1,156 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/util/service" + "typefox.dev/fastbelt/workspace" + "typefox.dev/lsp" +) + +// SignatureHelpComputer provides per-node logic for computing signature help. +// Language adopters implement this lightweight interface to provide signatures for specific AST nodes. +// +// Usage: +// +// type MySignatureHelpComputer struct{} +// +// func (c *MySignatureHelpComputer) TriggerCharacters() []string { +// return []string{"(", ","} +// } +// +// func (c *MySignatureHelpComputer) RetriggerCharacters() []string { +// return []string{","} +// } +// +// func (c *MySignatureHelpComputer) GetSignatureHelp(ctx context.Context, node core.AstNode) (*lsp.SignatureHelp, error) { +// if funcCall, ok := node.(*ast.FunctionCall); ok { +// return &lsp.SignatureHelp{ +// Signatures: []lsp.SignatureInformation{ +// { +// Label: "myFunc(param1: string, param2: int)", +// Parameters: []lsp.ParameterInformation{ +// {Label: "param1: string"}, +// {Label: "param2: int"}, +// }, +// }, +// }, +// }, nil +// } +// return nil, nil +// } +// +// // Register provider with custom computer +// service.Put(sc, server.NewSignatureHelpProviderWithComputer(sc, &MySignatureHelpComputer{})) +type SignatureHelpComputer interface { + // TriggerCharacters returns the characters that should trigger signature help. + // For example: []string{"(", ","} for function calls. + TriggerCharacters() []string + + // RetriggerCharacters returns the characters that should re-trigger signature help + // when it's already active. For example: []string{","} to update active parameter. + RetriggerCharacters() []string + + // GetSignatureHelp is called for the AST node at the cursor position. + // Implementations should check the node type and return signature information, + // or nil if no signature help is available for this node. + GetSignatureHelp(ctx context.Context, node core.AstNode) (*lsp.SignatureHelp, error) +} + +// DefaultSignatureHelpComputer is a no-op computer that provides no signatures +// but returns common default trigger characters. +type DefaultSignatureHelpComputer struct{} + +func (c *DefaultSignatureHelpComputer) TriggerCharacters() []string { + return []string{"("} +} + +func (c *DefaultSignatureHelpComputer) RetriggerCharacters() []string { + return []string{","} +} + +func (c *DefaultSignatureHelpComputer) GetSignatureHelp(ctx context.Context, node core.AstNode) (*lsp.SignatureHelp, error) { + return nil, nil +} + +type SignatureHelpProvider interface { + // TriggerCharacters returns the characters that should trigger signature help. + TriggerCharacters() []string + + // RetriggerCharacters returns the characters that should re-trigger signature help + // when it's already active. + RetriggerCharacters() []string + + HandleSignatureHelpRequest(ctx context.Context, params *lsp.SignatureHelpParams) (*lsp.SignatureHelp, error) +} + +// DefaultSignatureHelpProvider finds the AST node at the cursor position and +// delegates to SignatureHelpComputer for signature computation. +// Language adopters implement SignatureHelpComputer and pass it to +// NewSignatureHelpProviderWithComputer. +type DefaultSignatureHelpProvider struct { + sc *service.Container + computer SignatureHelpComputer +} + +// NewDefaultSignatureHelpProvider creates a signature help provider with the default computer (no signatures). +func NewDefaultSignatureHelpProvider(sc *service.Container) SignatureHelpProvider { + return &DefaultSignatureHelpProvider{ + sc: sc, + computer: &DefaultSignatureHelpComputer{}, + } +} + +// NewSignatureHelpProviderWithComputer creates a signature help provider with a custom computer. +// The computer determines which signatures to provide for each AST node and which characters trigger help. +func NewSignatureHelpProviderWithComputer(sc *service.Container, computer SignatureHelpComputer) SignatureHelpProvider { + return &DefaultSignatureHelpProvider{ + sc: sc, + computer: computer, + } +} + +func (p *DefaultSignatureHelpProvider) TriggerCharacters() []string { + return p.computer.TriggerCharacters() +} + +func (p *DefaultSignatureHelpProvider) RetriggerCharacters() []string { + return p.computer.RetriggerCharacters() +} + +func (p *DefaultSignatureHelpProvider) HandleSignatureHelpRequest(ctx context.Context, params *lsp.SignatureHelpParams) (*lsp.SignatureHelp, error) { + documentManager, err := service.Get[workspace.DocumentManager](p.sc) + if err != nil { + return nil, nil + } + + uri := core.ParseURI(string(params.TextDocument.URI)) + doc := documentManager.Get(uri) + if doc == nil || doc.Root == nil { + return nil, nil + } + + offset := doc.TextDoc.OffsetAt(params.Position) + first, second := doc.Tokens.SearchOffset2(offset) + if first == nil { + return nil, nil + } + + var node core.AstNode + if first.Element != nil { + node = first.Element + } else if second != nil && second.Element != nil { + node = second.Element + } + + if node == nil { + return nil, nil + } + + return p.computer.GetSignatureHelp(ctx, node) +} diff --git a/server/type_definition_provider.go b/server/type_definition_provider.go new file mode 100644 index 0000000..16adaa4 --- /dev/null +++ b/server/type_definition_provider.go @@ -0,0 +1,87 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/util/service" + "typefox.dev/fastbelt/workspace" + "typefox.dev/lsp" +) + +// TypeDefinitionProvider is a service for handling LSP type definition requests. +type TypeDefinitionProvider interface { + HandleTypeDefinitionRequest(ctx context.Context, params *lsp.TypeDefinitionParams) ([]lsp.DefinitionLink, error) +} + +// DefaultTypeDefinitionProvider is the default implementation of [TypeDefinitionProvider]. +// The default implementation provides basic support by looking for type references +// in the AST node's cross-references. Language adopters may need to override this +// service to handle language-specific type inference or implicit type relationships. +type DefaultTypeDefinitionProvider struct { + sc *service.Container +} + +func NewDefaultTypeDefinitionProvider(sc *service.Container) TypeDefinitionProvider { + return &DefaultTypeDefinitionProvider{sc: sc} +} + +func (p *DefaultTypeDefinitionProvider) HandleTypeDefinitionRequest(ctx context.Context, params *lsp.TypeDefinitionParams) ([]lsp.DefinitionLink, error) { + documentManager := service.MustGet[workspace.DocumentManager](p.sc) + uri := core.ParseURI(string(params.TextDocument.URI)) + doc := documentManager.Get(uri) + if doc == nil { + return nil, nil + } + + offset := doc.TextDoc.OffsetAt(params.Position) + first, second := doc.Tokens.SearchOffset2(offset) + if first == nil { + return nil, nil + } + + nameFinder := service.MustGet[NameFinder](p.sc) + foundName := nameFinder.Find(ctx, first, second) + if foundName.Source == nil { + return nil, nil + } + + sourceNode := foundName.Source.Owner() + sourceRange := foundName.Source.Segment().Range.LspRange() + + var results []lsp.DefinitionLink + + // Look for type references in the node's cross-references + for xref := range core.References(sourceNode) { + // Check if this is a type reference (many languages have a "type" feature) + // This is a heuristic and may need language-specific customization + targetDesc := xref.Description() + if targetDesc != nil && targetDesc.Node != nil { + targetNode := targetDesc.Node + + // Check if the target looks like a type definition + // (This is a simple heuristic; language-specific logic would be better) + fullRange := targetNode.Segment().Range.LspRange() + + // Try to get a more specific range if available + targetRange := fullRange + if targetDesc.Name != nil { + targetRange = targetDesc.Name.Segment().Range.LspRange() + } + + link := lsp.DefinitionLink{ + OriginSelectionRange: &sourceRange, + TargetURI: targetNode.Document().URI.DocumentURI(), + TargetRange: fullRange, + TargetSelectionRange: targetRange, + } + results = append(results, link) + } + } + + return results, nil +} diff --git a/server/type_hierarchy_contributor.go b/server/type_hierarchy_contributor.go new file mode 100644 index 0000000..4f8aa6d --- /dev/null +++ b/server/type_hierarchy_contributor.go @@ -0,0 +1,71 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/lsp" +) + +// TypeHierarchyContributor provides language-specific logic for type hierarchy navigation. +// +// Usage: +// +// type MyTypeHierarchyContributor struct{ sc *service.Container } +// +// func (c *MyTypeHierarchyContributor) PrepareItem(ctx context.Context, token *core.Token, node core.AstNode) *lsp.TypeHierarchyItem { +// if classDecl, ok := node.(*ast.ClassDeclaration); ok { +// return &lsp.TypeHierarchyItem{ +// Name: classDecl.Name, +// Kind: lsp.Class, +// URI: node.Document().URI.DocumentURI(), +// Range: node.Segment().Range.LspRange(), +// } +// } +// return nil +// } +// +// func (c *MyTypeHierarchyContributor) FindSupertypes(ctx context.Context, item *lsp.TypeHierarchyItem) []lsp.TypeHierarchyItem { +// // Find base classes and implemented interfaces +// return supertypes +// } +type TypeHierarchyContributor interface { + // PrepareItem creates a type hierarchy item for a symbol at the given position. + // Return nil if the symbol at the position is not a type (not a class/interface/struct). + // The returned item will be passed to supertype/subtype requests. + PrepareItem(ctx context.Context, token *core.Token, node core.AstNode) *lsp.TypeHierarchyItem + + // FindSupertypes finds all supertypes of the given type. + // Supertypes include base classes, extended interfaces, implemented interfaces, + // or parent types depending on the language semantics. + FindSupertypes(ctx context.Context, item *lsp.TypeHierarchyItem) []lsp.TypeHierarchyItem + + // FindSubtypes finds all subtypes of the given type. + // Subtypes include derived classes, implementing classes, extended interfaces, + // or child types depending on the language semantics. + FindSubtypes(ctx context.Context, item *lsp.TypeHierarchyItem) []lsp.TypeHierarchyItem +} + +// DefaultTypeHierarchyContributor is the default implementation of [TypeHierarchyContributor]. +// It returns nil for all operations, effectively disabling type hierarchy. +type DefaultTypeHierarchyContributor struct{} + +func NewDefaultTypeHierarchyContributor() TypeHierarchyContributor { + return &DefaultTypeHierarchyContributor{} +} + +func (c *DefaultTypeHierarchyContributor) PrepareItem(ctx context.Context, token *core.Token, node core.AstNode) *lsp.TypeHierarchyItem { + return nil +} + +func (c *DefaultTypeHierarchyContributor) FindSupertypes(ctx context.Context, item *lsp.TypeHierarchyItem) []lsp.TypeHierarchyItem { + return nil +} + +func (c *DefaultTypeHierarchyContributor) FindSubtypes(ctx context.Context, item *lsp.TypeHierarchyItem) []lsp.TypeHierarchyItem { + return nil +} diff --git a/server/type_hierarchy_provider.go b/server/type_hierarchy_provider.go new file mode 100644 index 0000000..cfee5b1 --- /dev/null +++ b/server/type_hierarchy_provider.go @@ -0,0 +1,88 @@ +// Copyright 2026 TypeFox GmbH +// This program and the accompanying materials are made available under the +// terms of the MIT License, which is available in the project root. + +package server + +import ( + "context" + + core "typefox.dev/fastbelt" + "typefox.dev/fastbelt/util/service" + "typefox.dev/fastbelt/workspace" + "typefox.dev/lsp" +) + +// TypeHierarchyProvider is a service for handling LSP type hierarchy requests. +type TypeHierarchyProvider interface { + HandlePrepareTypeHierarchyRequest(ctx context.Context, params *lsp.TypeHierarchyPrepareParams) ([]lsp.TypeHierarchyItem, error) + HandleSupertypesRequest(ctx context.Context, params *lsp.TypeHierarchySupertypesParams) ([]lsp.TypeHierarchyItem, error) + HandleSubtypesRequest(ctx context.Context, params *lsp.TypeHierarchySubtypesParams) ([]lsp.TypeHierarchyItem, error) +} + +// DefaultTypeHierarchyProvider is the default implementation of [TypeHierarchyProvider]. +// It delegates type relationship detection to [TypeHierarchyContributor]. +type DefaultTypeHierarchyProvider struct { + sc *service.Container +} + +func NewDefaultTypeHierarchyProvider(sc *service.Container) TypeHierarchyProvider { + return &DefaultTypeHierarchyProvider{sc: sc} +} + +func (p *DefaultTypeHierarchyProvider) HandlePrepareTypeHierarchyRequest(ctx context.Context, params *lsp.TypeHierarchyPrepareParams) ([]lsp.TypeHierarchyItem, error) { + documentManager := service.MustGet[workspace.DocumentManager](p.sc) + uri := core.ParseURI(string(params.TextDocument.URI)) + doc := documentManager.Get(uri) + if doc == nil { + return nil, nil + } + + contributor, err := service.Get[TypeHierarchyContributor](p.sc) + if err != nil { + // No contributor available + return nil, nil + } + + // Find token at cursor position + offset := doc.TextDoc.OffsetAt(params.Position) + first, _ := doc.Tokens.SearchOffset2(offset) + if first == nil { + return nil, nil + } + + // Get the AST node for the token + node := first.Element + + // Delegate to contributor + item := contributor.PrepareItem(ctx, first, node) + if item == nil { + return nil, nil + } + + return []lsp.TypeHierarchyItem{*item}, nil +} + +func (p *DefaultTypeHierarchyProvider) HandleSupertypesRequest(ctx context.Context, params *lsp.TypeHierarchySupertypesParams) ([]lsp.TypeHierarchyItem, error) { + contributor, err := service.Get[TypeHierarchyContributor](p.sc) + if err != nil { + // No contributor available + return nil, nil + } + + supertypes := contributor.FindSupertypes(ctx, ¶ms.Item) + + return supertypes, nil +} + +func (p *DefaultTypeHierarchyProvider) HandleSubtypesRequest(ctx context.Context, params *lsp.TypeHierarchySubtypesParams) ([]lsp.TypeHierarchyItem, error) { + contributor, err := service.Get[TypeHierarchyContributor](p.sc) + if err != nil { + // No contributor available + return nil, nil + } + + subtypes := contributor.FindSubtypes(ctx, ¶ms.Item) + + return subtypes, nil +}