From c2112f6ee1c7e7ede8b6b2daa119b100a9146f3f Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 21 Sep 2026 07:50:14 +0100 Subject: [PATCH] implements PLN-0131 Add PBS go-to-definition as hover editorial lookup plus physical declaration locations on FrontendLanguageService. Empty results stay honest when the target is not a regular file, including virtual stdlib paths. Housekeep DSC-0039 with LSN-0068. --- discussion/index.ndjson | 4 +- ...ses-editorial-lookup-and-physical-files.md | 98 +++++++ .../AGD-0042-pbs-lsp-go-to-definition.md | 62 ----- ...ler Pipeline Entry Points Specification.md | 2 + .../compiler/PBSFrontendLanguageService.java | 79 +++++- .../pbs/semantics/PbsEditorialLocation.java | 11 + .../semantics/PbsEditorialSupportService.java | 239 ++++++++++++++++++ .../PbsEditorialSupportServiceTest.java | 81 ++++++ .../services/FrontendDefinitionLocation.java | 20 ++ .../services/FrontendEditorialContext.java | 10 +- .../services/FrontendLanguageService.java | 13 + .../services/FrontendLanguageServiceTest.java | 14 + .../lsp/messages/BaselineDefinition.java | 15 ++ .../messages/BaselineDefinitionLocation.java | 28 ++ .../messages/BaselineServerDescription.java | 1 + .../lsp/services/LanguageServiceBridge.java | 3 + .../CompilerLanguageServiceBridge.java | 94 +++++-- .../protocol/PrometeuTextDocumentService.java | 14 + .../mapping/Lsp4jProtocolMessageMapper.java | 15 ++ .../mapping/ProtocolMessageMapper.java | 6 + .../CompilerLanguageServiceBridgeTest.java | 89 +++++++ .../protocol/PrometeuLanguageServerTest.java | 17 ++ .../PrometeuTextDocumentServiceTest.java | 34 +++ .../Lsp4jProtocolMessageMapperTest.java | 17 ++ 24 files changed, 885 insertions(+), 81 deletions(-) create mode 100644 discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md delete mode 100644 discussion/workflow/agendas/AGD-0042-pbs-lsp-go-to-definition.md create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialLocation.java create mode 100644 prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDefinitionLocation.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDefinition.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDefinitionLocation.java diff --git a/discussion/index.ndjson b/discussion/index.ndjson index adb26d14..086ba976 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":52,"PLN":131,"LSN":68,"CLSN":1}} +{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":53,"PLN":132,"LSN":69,"CLSN":1}} {"type":"discussion","id":"DSC-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} @@ -25,7 +25,7 @@ {"type":"discussion","id":"DSC-0042","status":"open","ticket":"pbs-lsp-workspace-symbols","title":"PBS LSP Workspace Symbols","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","workspace-symbols"],"agendas":[{"id":"AGD-0045","file":"AGD-0045-pbs-lsp-workspace-symbols.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"pbs-lsp-document-symbols-outline","title":"PBS LSP Document Symbols and Outline","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","outline","document-symbols"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-pbs-lsp-document-symbols-and-outline.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0040","status":"open","ticket":"pbs-lsp-find-references","title":"PBS LSP Find References","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","references"],"agendas":[{"id":"AGD-0043","file":"AGD-0043-pbs-lsp-find-references.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0039","status":"open","ticket":"pbs-lsp-go-to-definition","title":"PBS LSP Go to Definition","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","definition"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-pbs-lsp-go-to-definition.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0039","status":"done","ticket":"pbs-lsp-go-to-definition","title":"PBS LSP Go to Definition","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","definition"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0068","file":"discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"studio-packer-rgba8888-asset-pipeline","title":"Studio and Packer RGBA8888 Asset Pipeline Alignment","created_at":"2026-05-23","updated_at":"2026-07-14","tags":["studio","packer","assets","glyph-bank","palette","rgba8888","runtime-alignment"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0053","file":"discussion/lessons/DSC-0038-studio-packer-rgba8888-asset-pipeline/LSN-0053-rgba8888-is-the-canonical-studio-packer-palette-contract.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14"}]} {"type":"discussion","id":"DSC-0037","status":"done","ticket":"pbs-autocomplete-parameter-names","title":"PBS autocomplete parameter names for stdlib and method calls","created_at":"2026-05-08","updated_at":"2026-05-14","tags":["compiler-pbs","studio","lsp","autocomplete","signature-help","stdlib"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0037-pbs-autocomplete-parameter-names/LSN-0052-canonical-callable-parameter-names-through-pbs-editor-assistance.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]} {"type":"discussion","id":"DSC-0036","status":"done","ticket":"pbs-symbol-documentation-and-hover-markdown","title":"Modelo de documentacao de simbolos em PBS e consumo markdown no hover","created_at":"2026-05-08","updated_at":"2026-07-15","tags":["compiler","compiler-pbs","studio","lsp","vscode","editor","hover","documentation","markdown"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0057","file":"discussion/lessons/DSC-0036-pbs-symbol-documentation-and-hover-markdown/LSN-0057-compiler-owned-pbs-doc-markdown-for-editor-assistance.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} diff --git a/discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md b/discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md new file mode 100644 index 00000000..23a62fdc --- /dev/null +++ b/discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md @@ -0,0 +1,98 @@ +--- +id: LSN-0068 +ticket: pbs-lsp-go-to-definition +title: PBS go-to-definition uses editorial lookup and physical files +created: 2026-09-21 +tags: [studio, lsp, vscode, compiler-pbs, editor, definition] +--- + +# PBS go-to-definition uses editorial lookup and physical files + +## Original Problem + +PBS editor assistance already had compiler-backed completion, hover, and signature help (`LSN-0051`). Users still could not jump from a use to a declaration. Wave 1 explicitly deferred `go to definition`. + +The remaining LSP navigation family (references, rename, workspace symbols, document links, hierarchy) needed a destination policy before those features could be honest. The risk was shipping a text search, a new `SymbolId` index, or virtual documents for stdlib before a real physical target existed. + +## Consolidated Decision + +PBS Go to Definition is the LSP projection of **physical declaration locations** on top of the **same editorial lookup hover already uses**. + +Durable locks from `DEC-0052`: + +1. Resolve the identifier under the cursor through the hover path (locals, parameters, members, imports/supplementals, current-module top-level). Do not use text search or a new usage index. +2. Keep the capability on the existing aggregated `FrontendLanguageService`. Add a default-empty `definition` method and a generic location DTO. Do not add a Definition SPI. +3. Map PBS editorial spans inside `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. +4. Jump only when `Span.fileId` maps to a **regular filesystem file**. Otherwise return an empty list. Do not create virtual documents, `untitled:` buffers, or synthetic URIs. +5. The destination is the canonical declaration, not the import-alias line. Overloads return every matching location. +6. Advertise `definitionProvider`. Partial coverage is valid. Keywords, source-less builtins, unresolved names, and virtual stdlib paths stay empty; hover is unchanged. +7. Analysis stays request-local, like hover. This discussion does not build references, rename, or snapshot cache. + +Spec 23 §8.3 already listed definition as an optional editor capability. It now also states: a definition location must identify a compiler-known physical source file, or the response must be empty. + +## Final Implementation + +| Layer | What landed | +|---|---| +| PBS editorial | `PbsEditorialSupportService.definition` reuses hover dispatch and attaches declaration `Span`s (`PbsEditorialLocation`). | +| Generic contract | `FrontendDefinitionLocation` (absolute path + offsets) and `FrontendLanguageService.definition` defaulting to `List.of()`. | +| PBS mapping | `PBSFrontendLanguageService` maps `FileId` through `fileTable` and keeps the location only if `Files.isRegularFile(path)`. | +| LSP | `LanguageServiceBridge.definition`, `definitionProvider`, `textDocument/definition`. Compile-only frontends return empty. | +| VS Code | Unchanged thin client. | + +Stdlib and SDK modules are still registered under virtual paths such as `/virtual/stdlib/...`. Those handles are not regular files, so F12 on `Gfx.clear` is empty even though hover works. That is the physical-file rule, not a missing lookup. + +The location payload (path + span of an already resolved symbol) is reusable later by references (`AGD-0043`) and rename (`AGD-0046`). Those indexes were not built here. Snapshots (`AGD-0051`) remain a later performance discussion, not a prerequisite for this request-local feature. + +## Examples + +Same-file jump: + +```pbs +fn helper() -> int { return 42; } + +fn frame() -> void { + helper(); // definition -> helper's declaration in this file +} +``` + +Aliased import jumps to the original declaration span, not the `import` line: + +```pbs +import { Log as Logger } from @sdk:log; +Logger.info("hello"); +``` + +If `@sdk:log` is only a virtual stdlib handle, definition is empty. If a project file owns the declaration and that file exists on disk, definition uses that path. + +Builtins stay hover-only: + +```pbs +let n: int = 1; // hover describes the builtin; definition is empty +``` + +## Pitfalls + +Do not implement definition as a second name search in `lsp-v1` or in the VS Code extension. It will break on imports, overloads, and members, then be thrown away when references arrive. + +Do not open `/virtual/stdlib/...` or invent an `untitled:` document so stdlib F12 “does something”. Empty is the honest result until those sources are real files. + +Do not pick the first overload location in the server. Return the full `Location[]`. + +Do not wait for snapshot cache before definition. Hover is already request-local; definition follows that shape. + +Do not split a new top-level definition service “for future languages”. PBS is a real consumer of the existing language-service surface (`LSN-0067`). + +Do not treat this lesson as closing references, rename, outline, workspace symbols, or document links. Those agendas stay open. This lesson only locks the destination policy they can reuse. + +## References + +- Agenda: `AGD-0042` (Q1–Q6 = A) +- Decision: `DEC-0052` +- Plan: `PLN-0131` +- Spec 23 §8.3 — optional definition capability; physical-file-or-empty +- `LSN-0051` — wave 1 deferred go-to-definition +- `LSN-0058` — generic `FrontendLanguageService` for LSP +- `LSN-0047` — protocol stays in `lsp-v1`; compiler owns semantics +- `LSN-0067` — no SPI or virtual-document host without a real consumer +- Still open: `AGD-0043` references, `AGD-0045` workspace symbols, `AGD-0046` rename, `AGD-0051` snapshots, `AGD-0055` document links, `AGD-0056` hierarchy diff --git a/discussion/workflow/agendas/AGD-0042-pbs-lsp-go-to-definition.md b/discussion/workflow/agendas/AGD-0042-pbs-lsp-go-to-definition.md deleted file mode 100644 index 808338c5..00000000 --- a/discussion/workflow/agendas/AGD-0042-pbs-lsp-go-to-definition.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0042 -ticket: pbs-lsp-go-to-definition -title: PBS LSP Go to Definition -status: open -created: 2026-07-15 -resolved: -decision: -tags: [studio, lsp, vscode, compiler-pbs, editor, definition] ---- - -## Pain - -Domain owner: `studio/lsp` - -PBS users cannot jump from a symbol use to its declaration, which makes navigation across source files, stdlib imports, services, and generated/editorial surfaces slow and error-prone. - -## Context - -The current LSP exposes diagnostics, hover, completion, signature help, and semantic tokens. The compiler already builds semantic read surfaces and editorial symbol resolution for hover/completion, but there is no LSP definition capability or stable location mapping exposed to VS Code. - -## Open Questions - -- [ ] Which symbol categories must be supported in the first definition wave? -- [ ] Should stdlib and generated/supplemental declarations navigate to source files, virtual documents, or remain hover-only? -- [ ] What identity model should connect an editor token to the canonical declaration without relying on text search? - -## Options - -### Option A - Syntactic location lookup - -- **Approach:** Use parser spans and local AST traversal to map the token under cursor to a declaration with matching text. -- **Pro:** Fast to implement and useful for same-file declarations. -- **Con:** Breaks on overloads, imports, stdlib, aliases, and same-name symbols. -- **Maintainability:** Weak; it creates a second navigation model separate from semantic resolution. - -### Option B - Semantic identity lookup - -- **Approach:** Extend the compiler/editorial surface with resolved declaration locations keyed by symbol identity, then expose `textDocument/definition`. -- **Pro:** Correct for imports, overloads, methods, stdlib-backed declarations, and future references/rename. -- **Con:** Requires explicit location metadata in semantic/editorial surfaces. -- **Maintainability:** Strong; it becomes shared infrastructure for references, rename, hierarchy, and code actions. - -## Tradeoffs - -The syntactic option is tempting for speed, but it would likely be thrown away once references and rename arrive. The main tradeoff is whether stdlib/supplemental declarations must navigate to physical files immediately or can initially return no location while still supporting project symbols. - -## Recommendation - -Prefer Option B. Build a reusable semantic location index and implement the first wave for project source declarations, then define a policy for stdlib/supplemental targets. - -## Discussion - -This should probably be the first navigation agenda to resolve because references, rename, workspace symbols, and hierarchy depend on the same symbol identity/location model. - -## Resolution - -Ainda em aberto. - -## Next Step - -Decide the first-wave symbol categories and whether stdlib/supplemental declarations require virtual document support. diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index ad6844f7..5a22fe5a 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -272,6 +272,8 @@ Absent editor capabilities MUST have deterministic fallback behavior at tooling 3. host protocols that can represent unsupported operations SHOULD do so explicitly; 4. and absent editor capability MUST NOT be reported as a compiler failure. +When a frontend exposes definition or navigation, each returned location MUST identify a compiler-known physical source file. A resolved symbol without a physical file MUST produce an empty definition response. Tooling MUST NOT require virtual documents, untitled buffers, or synthetic URIs for this capability. + Compiler diagnostics and editor diagnostics are distinct ownership surfaces. Diagnostics returned by `analyze`, `compile`, and `build` are compiler contract output. Editor diagnostics MAY reuse compiler analysis results, live overlays, caches, or cancellation-aware tooling state, but a frontend MUST NOT be required to provide editor diagnostics in order to compile. `FrontendSpec` remains the source of static frontend-owned presentation metadata such as semantic vocabularies, host projections, and visual themes. Producing semantic tokens for a live document is an optional editor-facing capability; the existence of static presentation metadata MUST NOT imply that every frontend can provide live semantic-token results. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index f91fbc08..44a54174 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -6,11 +6,13 @@ import p.studio.compiler.messages.FrontendPhaseContext; import p.studio.compiler.models.FrontendSemanticToken; import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.pbs.semantics.PbsEditorialCompletionCandidate; +import p.studio.compiler.pbs.semantics.PbsEditorialLocation; import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol; import p.studio.compiler.pbs.semantics.PbsEditorialSignatureHelp; import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind; import p.studio.compiler.pbs.semantics.PbsEditorialSupportService; import p.studio.compiler.services.FrontendCompletionCandidate; +import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendDocumentation; import p.studio.compiler.services.FrontendEditorialContext; @@ -20,10 +22,15 @@ import p.studio.compiler.services.FrontendSignature; import p.studio.compiler.services.FrontendSignatureHelp; import p.studio.compiler.services.FrontendSymbolKind; import p.studio.compiler.services.PBSFrontendPhaseService; +import p.studio.compiler.source.Span; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; +import p.studio.compiler.source.tables.FileTableReader; import p.studio.utilities.structures.ReadOnlyList; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -61,7 +68,8 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return Optional.of(new FrontendEditorialContext( ast, semanticReadSurface.supplementalTopDeclsByFile().getOrDefault(fileId, ReadOnlyList.empty()), - phaseContext.feSurfaceContext())); + phaseContext.feSurfaceContext(), + phaseContext.fileTable)); } public List completion( @@ -167,6 +175,44 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService .orElseGet(FrontendSignatureHelp::empty); } + public List definition( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + return editorialSupportService.definition(sourceText, ast, supplementalTopDecls, feSurfaceContext, offset); + } + + @Override + public List definition( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + if (request == null + || editorialContext == null + || !(editorialContext.syntaxTree() instanceof PbsAst.File ast) + || !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList supplementalTopDecls) + || !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext) + || !(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) { + return List.of(); + } + + @SuppressWarnings("unchecked") + final ReadOnlyList typedSupplementalTopDecls = + (ReadOnlyList) supplementalTopDecls; + final ArrayList locations = new ArrayList<>(); + for (final PbsEditorialLocation location : definition( + request.documentText(), + ast, + typedSupplementalTopDecls, + feSurfaceContext, + offset)) { + toFrontendLocation(location, fileTable).ifPresent(locations::add); + } + return List.copyOf(locations); + } + @Override public List semanticTokens(final FrontendDocumentRequest request) { return semanticTokens(request == null ? "" : request.documentText()); @@ -213,4 +259,35 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService private FrontendSymbolKind toFrontendSymbolKind(final PbsEditorialSymbolKind kind) { return FrontendSymbolKind.valueOf(kind.name()); } + + private Optional toFrontendLocation( + final PbsEditorialLocation location, + final FileTableReader fileTable) { + if (location == null || fileTable == null) { + return Optional.empty(); + } + final Span span = location.span(); + if (span == null || span.isNone() || span.getFileId() == null || span.getFileId().isNone()) { + return Optional.empty(); + } + final var sourceHandle = fileTable.get(span.getFileId()); + if (sourceHandle == null || sourceHandle.getCanonPath() == null) { + return Optional.empty(); + } + final Path path = sourceHandle.getCanonPath().toAbsolutePath().normalize(); + if (!Files.isRegularFile(path)) { + return Optional.empty(); + } + return Optional.of(new FrontendDefinitionLocation( + path, + toOffset(span.getStart()), + toOffset(span.getEnd()))); + } + + private static int toOffset(final long value) { + if (value <= 0L) { + return 0; + } + return (int) Math.min(Integer.MAX_VALUE, value); + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialLocation.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialLocation.java new file mode 100644 index 00000000..2f97b814 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialLocation.java @@ -0,0 +1,11 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.source.Span; + +import java.util.Objects; + +public record PbsEditorialLocation(Span span) { + public PbsEditorialLocation { + Objects.requireNonNull(span, "span"); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java index d91c5b56..35af295a 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java @@ -74,6 +74,23 @@ public final class PbsEditorialSupportService { return context.resolveSymbolHover(token.lexeme(), tokenIndex, scopeState); } + public List definition( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final int offset) { + return definition(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset); + } + + public List definition( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + return buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext).definitionAt(offset); + } + public Optional signatureHelp( final String sourceText, final PbsAst.File ast, @@ -610,6 +627,228 @@ public final class PbsEditorialSupportService { return Optional.empty(); } + private List definitionAt(final int offset) { + final Integer tokenIndex = identifierTokenIndexAt(offset); + if (tokenIndex == null) { + return List.of(); + } + final var scopeState = scopeStateAt(offset); + final var token = token(tokenIndex); + final Integer previousTokenIndex = previousSignificantTokenIndex(tokenIndex); + if (previousTokenIndex != null && token(previousTokenIndex).kind() == PbsTokenKind.DOT) { + final TypeView receiverType = resolveReceiverTypeBeforeDot(previousTokenIndex, scopeState); + return resolveMemberDefinition(receiverType, token.lexeme()); + } + return resolveSymbolDefinition(token.lexeme(), tokenIndex, scopeState, offset); + } + + private List resolveSymbolDefinition( + final String symbolName, + final int tokenIndex, + final ScopeState scopeState, + final int offset) { + if (scopeState.parameterNames().contains(symbolName)) { + return parameterLocations(symbolName, offset); + } + final Scope.LocalSymbol localSymbol = scopeState.scope().entries().get(symbolName); + if (localSymbol != null) { + return localLocations(symbolName, offset); + } + final Integer previousTokenIndex = previousSignificantTokenIndex(tokenIndex); + if (previousTokenIndex != null && token(previousTokenIndex).kind() == PbsTokenKind.NEW) { + return constructorLocations(resolveTopDeclSymbol(symbolName)); + } + final TopDeclSymbol importedSymbol = importedSymbols.get(symbolName); + if (importedSymbol != null) { + return topDeclLocations(importedSymbol); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(symbolName); + if (supplementalSymbol != null) { + return topDeclLocations(supplementalSymbol); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(symbolName); + if (topLevelSymbol != null) { + return topDeclLocations(topLevelSymbol); + } + return List.of(); + } + + private List resolveMemberDefinition( + final TypeView receiverType, + final String memberName) { + if (receiverType == null) { + return List.of(); + } + if (receiverType.kind() == Kind.STRUCT) { + final Span fieldSpan = structFieldSpan(receiverType.name(), memberName); + if (fieldSpan != null) { + return locations(fieldSpan); + } + final StructInfo structInfo = model.structs.get(receiverType.name()); + if (structInfo == null) { + return List.of(); + } + return callableLocations(structInfo.methods().get(memberName)); + } + if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) { + final Map> methods = receiverType.kind() == Kind.SERVICE + ? Optional.ofNullable(model.services.get(receiverType.name())).map(ServiceInfo::methods).orElse(Map.of()) + : Optional.ofNullable(model.contracts.get(receiverType.name())).map(ContractInfo::methods).orElse(Map.of()); + return callableLocations(methods.get(memberName)); + } + return List.of(); + } + + private List parameterLocations( + final String symbolName, + final int offset) { + final CallableFrame callableFrame = callableFrameAt(offset); + if (callableFrame == null) { + return List.of(); + } + for (final var parameter : callableFrame.parameters()) { + if (symbolName.equals(parameter.name())) { + return locations(parameter.span()); + } + } + return List.of(); + } + + private List localLocations( + final String symbolName, + final int offset) { + final CallableFrame callableFrame = callableFrameAt(offset); + if (callableFrame == null) { + return List.of(); + } + return locations(bindingSpan(callableFrame.body(), symbolName, offset, null)); + } + + private Span bindingSpan( + final PbsAst.Block block, + final String symbolName, + final int offset, + final Span current) { + Span found = current; + for (final var statement : block.statements()) { + if (statement.span().getStart() >= offset) { + break; + } + if (statement instanceof PbsAst.LetStatement letStatement && symbolName.equals(letStatement.name())) { + found = letStatement.nameSpan() == null ? letStatement.span() : letStatement.nameSpan(); + } + if (statement.span().contains(offset)) { + if (statement instanceof PbsAst.IfStatement ifStatement) { + if (ifStatement.thenBlock().span().contains(offset)) { + return bindingSpan(ifStatement.thenBlock(), symbolName, offset, found); + } + if (ifStatement.elseBlock() != null && ifStatement.elseBlock().span().contains(offset)) { + return bindingSpan(ifStatement.elseBlock(), symbolName, offset, found); + } + if (ifStatement.elseIf() != null && ifStatement.elseIf().span().contains(offset)) { + final var nestedBlock = new PbsAst.Block( + ReadOnlyList.wrap(List.of(ifStatement.elseIf())), + null, + ifStatement.elseIf().span()); + return bindingSpan(nestedBlock, symbolName, offset, found); + } + } else if (statement instanceof PbsAst.ForStatement forStatement) { + Span loopFound = found; + if (symbolName.equals(forStatement.iteratorName())) { + loopFound = forStatement.span(); + } + if (forStatement.body().span().contains(offset)) { + return bindingSpan(forStatement.body(), symbolName, offset, loopFound); + } + } else if (statement instanceof PbsAst.WhileStatement whileStatement + && whileStatement.body().span().contains(offset)) { + return bindingSpan(whileStatement.body(), symbolName, offset, found); + } + return found; + } + } + return found; + } + + private List constructorLocations(final TopDeclSymbol topDeclSymbol) { + if (topDeclSymbol == null) { + return List.of(); + } + if (topDeclSymbol.decl() instanceof PbsAst.StructDecl structDecl) { + if (structDecl.ctors().isEmpty()) { + return locations(structDecl.span()); + } + final ArrayList ctorSpans = new ArrayList<>(); + for (final var ctor : structDecl.ctors()) { + ctorSpans.add(ctor.span()); + } + return locations(ctorSpans.toArray(Span[]::new)); + } + return topDeclLocations(topDeclSymbol); + } + + private List topDeclLocations(final TopDeclSymbol topDeclSymbol) { + if (topDeclSymbol == null) { + return List.of(); + } + return locations(topDeclSymbol.decl().span()); + } + + private List callableLocations(final List callables) { + if (callables == null || callables.isEmpty()) { + return List.of(); + } + final ArrayList spans = new ArrayList<>(); + for (final var callable : callables) { + spans.add(callable.span()); + } + return locations(spans.toArray(Span[]::new)); + } + + private Span structFieldSpan( + final String typeName, + final String fieldName) { + final TopDeclSymbol typeSymbol = resolveTopDeclSymbol(typeName); + if (typeSymbol == null) { + return null; + } + if (typeSymbol.decl() instanceof PbsAst.StructDecl structDecl) { + for (final var field : structDecl.fields()) { + if (fieldName.equals(field.name())) { + return field.span(); + } + } + } + if (typeSymbol.decl() instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + for (final var field : builtinTypeDecl.fields()) { + if (fieldName.equals(field.name())) { + return field.span(); + } + } + } + return null; + } + + private List locations(final Span... spans) { + final ArrayList locations = new ArrayList<>(); + if (spans == null) { + return List.of(); + } + for (final Span span : spans) { + if (hasPhysicalSpan(span)) { + locations.add(new PbsEditorialLocation(span)); + } + } + return List.copyOf(locations); + } + + private boolean hasPhysicalSpan(final Span span) { + return span != null + && !span.isNone() + && span.getFileId() != null + && !span.getFileId().isNone(); + } + private List resolveCallableSignatures( final String callableName, final ScopeState scopeState) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java index 8ba0a828..346a5ab3 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java @@ -265,6 +265,87 @@ final class PbsEditorialSupportServiceTest { && candidate.detail().equals("draw_pixel(x: int, y: int, color: int) -> void"))); } + @Test + void shouldResolveDefinitionForLocalsParametersMembersAndCanonicalImports() { + final var source = """ + import { Log as Logger } from @sdk:log; + + fn helper(value: int) -> int { + let total = value; + Logger.info("hello"); + missingName; + return total; + } + """; + final var ast = parseOrdinary(source); + final var supplementalTopDecls = supplementalTopDecls(""" + declare service Log { + fn info(message: str) -> void { + return; + } + } + """); + + final var parameter = requireDefinition(source, ast, supplementalTopDecls, "value;"); + assertEquals(1, parameter.size()); + assertEquals(0, parameter.getFirst().span().getFileId().getId()); + assertTrue(parameter.getFirst().span().contains(source.indexOf("value: int"))); + + final var local = requireDefinition(source, ast, supplementalTopDecls, "total;"); + assertEquals(1, local.size()); + assertEquals(0, local.getFirst().span().getFileId().getId()); + assertTrue(local.getFirst().span().contains(source.indexOf("total = value"))); + + final var function = requireDefinition(source, ast, supplementalTopDecls, "helper(value"); + assertEquals(1, function.size()); + assertEquals(0, function.getFirst().span().getFileId().getId()); + assertTrue(function.getFirst().span().getStart() <= source.indexOf("fn helper")); + + final var imported = requireDefinition(source, ast, supplementalTopDecls, "Logger.info"); + assertEquals(1, imported.size()); + assertEquals(100, imported.getFirst().span().getFileId().getId()); + assertNotEquals(ast.span().getFileId().getId(), imported.getFirst().span().getFileId().getId()); + + final var member = requireDefinition(source, ast, supplementalTopDecls, "info("); + assertEquals(1, member.size()); + assertEquals(100, member.getFirst().span().getFileId().getId()); + + assertTrue(service.definition(source, ast, supplementalTopDecls, source.indexOf("fn helper")).isEmpty()); + assertTrue(service.definition(source, ast, supplementalTopDecls, source.indexOf("int")).isEmpty()); + assertTrue(service.definition(source, ast, supplementalTopDecls, source.indexOf("missingName")).isEmpty()); + } + + @Test + void shouldReturnEveryMatchingDeclarationLocationForOverloadedMembers() { + final var source = """ + declare struct Vec() { + fn blend(dx: int) -> int { return dx; } + fn blend(dx: int, dy: int) -> int { return dx; } + } + + fn main(vec: Vec) -> void { + vec.blend(1, 2); + } + """; + final var ast = parseOrdinary(source); + final var locations = requireDefinition(source, ast, ReadOnlyList.empty(), "blend(1"); + assertEquals(2, locations.size()); + assertTrue(locations.getFirst().span().contains(source.indexOf("fn blend(dx: int)"))); + assertTrue(locations.get(1).span().contains(source.indexOf("fn blend(dx: int, dy: int)"))); + } + + private List requireDefinition( + final String source, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final String needle) { + final int offset = source.indexOf(needle); + assertTrue(offset >= 0, "Expected to find needle: " + needle); + final var locations = service.definition(source, ast, supplementalTopDecls, offset); + assertFalse(locations.isEmpty(), "Expected definition for needle: " + needle); + return locations; + } + private PbsEditorialResolvedSymbol requireHover( final String source, final PbsAst.File ast, diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDefinitionLocation.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDefinitionLocation.java new file mode 100644 index 00000000..de57aa57 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDefinitionLocation.java @@ -0,0 +1,20 @@ +package p.studio.compiler.services; + +import java.nio.file.Path; +import java.util.Objects; + +public record FrontendDefinitionLocation( + Path path, + int startOffset, + int endOffset) { + + public FrontendDefinitionLocation { + path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); + if (startOffset < 0) { + throw new IllegalArgumentException("startOffset must not be negative"); + } + if (endOffset < startOffset) { + throw new IllegalArgumentException("endOffset must not be before startOffset"); + } + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendEditorialContext.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendEditorialContext.java index 783d7616..60c7256f 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendEditorialContext.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendEditorialContext.java @@ -3,5 +3,13 @@ package p.studio.compiler.services; public record FrontendEditorialContext( Object syntaxTree, Object supplementalDeclarations, - Object semanticContext) { + Object semanticContext, + Object sourceFiles) { + + public FrontendEditorialContext( + final Object syntaxTree, + final Object supplementalDeclarations, + final Object semanticContext) { + this(syntaxTree, supplementalDeclarations, semanticContext, null); + } } diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index cad07baa..0a18be8e 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -59,4 +59,17 @@ public interface FrontendLanguageService { final int offset) { return signatureHelp(request, offset); } + + default List definition( + final FrontendDocumentRequest request, + final int offset) { + return List.of(); + } + + default List definition( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + return definition(request, offset); + } } diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java index 25c3dc25..e0f79366 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java @@ -22,6 +22,20 @@ class FrontendLanguageServiceTest { assertTrue(service.completion(request, 0).isEmpty()); assertTrue(service.hover(request, 0).isEmpty()); assertEquals(FrontendSignatureHelp.empty(), service.signatureHelp(request, 0)); + assertTrue(service.definition(request, 0).isEmpty()); + } + + @Test + void definitionLocationRejectsBlankPathAndInvertedOffsets() { + final Path path = Path.of("src/main.pbs").toAbsolutePath(); + final var location = new FrontendDefinitionLocation(path, 4, 9); + assertEquals(path.normalize(), location.path()); + assertEquals(4, location.startOffset()); + assertEquals(9, location.endOffset()); + + assertThrows(NullPointerException.class, () -> new FrontendDefinitionLocation(null, 0, 1)); + assertThrows(IllegalArgumentException.class, () -> new FrontendDefinitionLocation(path, -1, 0)); + assertThrows(IllegalArgumentException.class, () -> new FrontendDefinitionLocation(path, 5, 4)); } @Test diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDefinition.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDefinition.java new file mode 100644 index 00000000..c6140344 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDefinition.java @@ -0,0 +1,15 @@ +package p.studio.lsp.messages; + +import java.util.List; + +public record BaselineDefinition( + List locations) { + + public BaselineDefinition { + locations = locations == null ? List.of() : List.copyOf(locations); + } + + public static BaselineDefinition empty() { + return new BaselineDefinition(List.of()); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDefinitionLocation.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDefinitionLocation.java new file mode 100644 index 00000000..195b861a --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDefinitionLocation.java @@ -0,0 +1,28 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record BaselineDefinitionLocation( + String uri, + int startLine, + int startCharacter, + int endLine, + int endCharacter) { + + public BaselineDefinitionLocation { + uri = requireText(uri, "uri"); + if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) { + throw new IllegalArgumentException("definition range coordinates must not be negative"); + } + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index 96a5461c..5f0397b6 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -10,6 +10,7 @@ public record BaselineServerDescription( boolean hoverSupported, boolean completionSupported, boolean signatureHelpSupported, + boolean definitionSupported, List semanticTokenTypes, List semanticHostProjections, List visualThemes, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index f7e7d543..f9dfb485 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -2,6 +2,7 @@ package p.studio.lsp.services; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineCompletion; +import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineSignatureHelp; @@ -17,6 +18,8 @@ public interface LanguageServiceBridge { BaselineHover hover(LspProjectContext context, String documentUri, String text, int line, int character); + BaselineDefinition definition(LspProjectContext context, String documentUri, String text, int line, int character); + BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character); BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index 1129b92d..71cdb02e 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -5,6 +5,7 @@ import p.studio.compiler.FrontendRegistryService; import p.studio.compiler.messages.*; import p.studio.compiler.models.*; import p.studio.compiler.services.FrontendCompletionCandidate; +import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendEditorialContext; import p.studio.compiler.services.FrontendHover; @@ -21,6 +22,8 @@ import p.studio.lsp.services.LanguageServiceBridge; import p.studio.utilities.logs.LogAggregator; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.function.Function; @@ -49,6 +52,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg true, true, true, + frontendProvider(context).languageService().isPresent(), presentation.semanticKeys(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(), @@ -87,13 +91,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg context.projectRoot(), normalizeDocumentPath(documentUri), document.text()); - final FrontendEditorialContext editorialContext = new FrontendEditorialContext( - document.editorialContext().syntaxTree(), - document.editorialContext().supplementalDeclarations(), - document.editorialContext().semanticContext()); final List candidates = languageService.orElseThrow().completion( request, - editorialContext, + document.editorialContext(), offset); return new BaselineCompletion( false, @@ -122,19 +122,47 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg context.projectRoot(), normalizeDocumentPath(documentUri), document.text()); - final FrontendEditorialContext editorialContext = new FrontendEditorialContext( - document.editorialContext().syntaxTree(), - document.editorialContext().supplementalDeclarations(), - document.editorialContext().semanticContext()); return languageService.orElseThrow().hover( request, - editorialContext, + document.editorialContext(), offset); }) .map(symbol -> new BaselineHover(formatHoverMarkdown(symbol))) .orElseGet(() -> new BaselineHover("No symbol information.")); } + @Override + public BaselineDefinition definition( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + Objects.requireNonNull(context, "context"); + final var provider = frontendProvider(context); + final var languageService = provider.languageService(); + if (languageService.isEmpty()) { + return BaselineDefinition.empty(); + } + return editorialDocument(context, documentUri, text) + .map(document -> { + final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character); + final FrontendDocumentRequest request = new FrontendDocumentRequest( + context.projectRoot(), + normalizeDocumentPath(documentUri), + document.text()); + final List locations = languageService.orElseThrow().definition( + request, + document.editorialContext(), + offset); + return new BaselineDefinition(mapDefinitionLocations( + locations, + request.documentPath(), + document.text())); + }) + .orElseGet(BaselineDefinition::empty); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext context, @@ -155,13 +183,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg context.projectRoot(), normalizeDocumentPath(documentUri), document.text()); - final FrontendEditorialContext editorialContext = new FrontendEditorialContext( - document.editorialContext().syntaxTree(), - document.editorialContext().supplementalDeclarations(), - document.editorialContext().semanticContext()); return languageService.orElseThrow().signatureHelp( request, - editorialContext, + document.editorialContext(), offset); }) .map(this::mapSignatureHelp) @@ -418,6 +442,46 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg }; } + private List mapDefinitionLocations( + final List locations, + final Path currentDocumentPath, + final String currentDocumentText) { + if (locations == null || locations.isEmpty()) { + return List.of(); + } + final ArrayList mapped = new ArrayList<>(); + for (final FrontendDefinitionLocation location : locations) { + final String targetText = textForDefinitionTarget(location.path(), currentDocumentPath, currentDocumentText); + if (targetText == null) { + continue; + } + final DocumentPositionMapper mapper = new DocumentPositionMapper(targetText); + final DocumentPosition start = mapper.positionOf(location.startOffset()); + final DocumentPosition end = mapper.positionOf(location.endOffset()); + mapped.add(new BaselineDefinitionLocation( + location.path().toUri().toString(), + start.line(), + start.character(), + end.line(), + end.character())); + } + return List.copyOf(mapped); + } + + private String textForDefinitionTarget( + final Path path, + final Path currentDocumentPath, + final String currentDocumentText) { + if (path.equals(currentDocumentPath)) { + return currentDocumentText == null ? "" : currentDocumentText; + } + try { + return Files.readString(path, StandardCharsets.UTF_8); + } catch (final Exception ignored) { + return null; + } + } + private String formatHoverMarkdown(final FrontendHover symbol) { final StringBuilder markdown = new StringBuilder(); final List signatures = symbol.signatures(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java index ef4b139d..bd0cf28f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -97,6 +97,20 @@ public final class PrometeuTextDocumentService implements TextDocumentService { params.getPosition().getCharacter()))); } + @Override + public CompletableFuture, List>> definition( + final DefinitionParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.get(uri); + final List locations = protocolMessageMapper.mapDefinition(languageServiceBridge.definition( + project, + uri, + text, + params.getPosition().getLine(), + params.getPosition().getCharacter())); + return CompletableFuture.completedFuture(Either.forLeft(locations)); + } + @Override public CompletableFuture signatureHelp(final SignatureHelpParams params) { final String uri = params.getTextDocument().getUri(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index 7b3d548c..31de9a7e 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -36,6 +36,7 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { signatureHelpOptions.setRetriggerCharacters(List.of(",")); capabilities.setSignatureHelpProvider(signatureHelpOptions); } + capabilities.setDefinitionProvider(description.definitionSupported()); final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setFull(true); @@ -83,6 +84,20 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { return result; } + @Override + public List mapDefinition(final BaselineDefinition definition) { + if (definition == null || definition.locations().isEmpty()) { + return List.of(); + } + return definition.locations().stream() + .map(location -> new Location( + location.uri(), + new Range( + new Position(location.startLine(), location.startCharacter()), + new Position(location.endLine(), location.endCharacter())))) + .toList(); + } + @Override public CompletionList mapCompletion(final BaselineCompletion completion) { final CompletionList list = new CompletionList(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index 7897adae..3cb2aa96 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -6,14 +6,18 @@ import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.Location; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineCompletion; +import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineSignatureHelp; import p.studio.lsp.messages.BaselineServerDescription; import org.eclipse.lsp4j.CompletionList; +import java.util.List; + public interface ProtocolMessageMapper { InitializeResult mapInitializeResult(BaselineServerDescription description); @@ -23,6 +27,8 @@ public interface ProtocolMessageMapper { Hover mapHover(BaselineHover hover); + List mapDefinition(BaselineDefinition definition); + CompletionList mapCompletion(BaselineCompletion completion); SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 7efb440d..99dfb98f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -45,6 +45,9 @@ class CompilerLanguageServiceBridgeTest { assertEquals(0, signatureHelp.activeSignature()); assertEquals(0, signatureHelp.activeParameter()); + final var definition = bridge.definition(context, documentUri, "fn main() {}", 0, 0); + assertTrue(definition.locations().isEmpty()); + final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}"); assertTrue(semanticTokens.legend().isEmpty()); assertTrue(semanticTokens.tokens().isEmpty()); @@ -309,6 +312,91 @@ class CompilerLanguageServiceBridgeTest { assertTrue(helper.documentation().contains("- returns `42`")); } + @Test + void definitionUsesEditorialLookupAndPhysicalStdlibFiles() { + final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()) + .resolve("test-projects") + .resolve("main") + .toAbsolutePath() + .normalize(); + final Path documentPath = projectRoot.resolve("src").resolve("main.pbs"); + final String overlay = """ + import { Gfx } from @sdk:gfx; + + declare struct Vec() { + fn blend(dx: int, dy: int) -> int { return dx; } + } + + fn helper() -> int { return 42; } + + fn frame(vec: Vec) -> void { + helper(); + vec.blend(1, 2); + Gfx.clear(5); + let n: int = 1; + n; + } + """; + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot); + final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay); + + final var helperUse = mapper.positionOf(overlay.indexOf("helper();")); + final var helperDefinition = bridge.definition( + context, + documentPath.toUri().toString(), + overlay, + helperUse.line(), + helperUse.character()); + assertEquals(1, helperDefinition.locations().size()); + assertEquals(documentPath.toUri().toString(), helperDefinition.locations().getFirst().uri()); + assertTrue(helperDefinition.locations().getFirst().startLine() < helperUse.line()); + + final var blendUse = mapper.positionOf(overlay.indexOf("blend(1")); + final var blendDefinition = bridge.definition( + context, + documentPath.toUri().toString(), + overlay, + blendUse.line(), + blendUse.character()); + assertEquals(1, blendDefinition.locations().size()); + assertEquals(documentPath.toUri().toString(), blendDefinition.locations().getFirst().uri()); + + final var clearUse = mapper.positionOf(overlay.indexOf("clear")); + final var clearHover = bridge.hover( + context, + documentPath.toUri().toString(), + overlay, + clearUse.line(), + clearUse.character()); + assertTrue(clearHover.markdown().contains("clear(color:")); + final var clearDefinition = bridge.definition( + context, + documentPath.toUri().toString(), + overlay, + clearUse.line(), + clearUse.character()); + assertTrue( + clearDefinition.locations().isEmpty(), + "stdlib is loaded from a virtual path, so definition stays empty until a physical file exists"); + + final var builtinUse = mapper.positionOf(overlay.indexOf("int = 1")); + final var builtinHover = bridge.hover( + context, + documentPath.toUri().toString(), + overlay, + builtinUse.line(), + builtinUse.character()); + assertTrue(builtinHover.markdown().contains("builtin")); + final var builtinDefinition = bridge.definition( + context, + documentPath.toUri().toString(), + overlay, + builtinUse.line(), + builtinUse.character()); + assertTrue(builtinDefinition.locations().isEmpty()); + } + @Test void describeServerPublishesFrontendVisualThemes() { final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); @@ -319,6 +407,7 @@ class CompilerLanguageServiceBridgeTest { assertEquals("pbs-default", description.activeVisualThemeId()); assertTrue(description.completionSupported()); assertTrue(description.signatureHelpSupported()); + assertTrue(description.definitionSupported()); assertEquals(1, description.semanticHostProjections().size()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index ae67bbb9..7711015f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import p.studio.lsp.messages.BaselineCompletion; import p.studio.lsp.messages.BaselineCompletionItem; import p.studio.lsp.messages.BaselineCompletionItemKind; +import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineEditorPalette; import p.studio.lsp.messages.BaselineHover; @@ -98,6 +99,7 @@ class PrometeuLanguageServerTest { true, true, true, + true, List.of(), List.of(new BaselineSemanticHostProjection( "vscode", @@ -145,6 +147,16 @@ class PrometeuLanguageServerTest { return new BaselineHover("hover"); } + @Override + public BaselineDefinition definition( + final LspProjectContext project, + final String documentUri, + final String text, + final int line, + final int character) { + return BaselineDefinition.empty(); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext project, @@ -191,6 +203,11 @@ class PrometeuLanguageServerTest { throw new UnsupportedOperationException(); } + @Override + public java.util.List mapDefinition(final BaselineDefinition definition) { + throw new UnsupportedOperationException(); + } + @Override public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) { throw new UnsupportedOperationException(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java index 05908c78..7937458d 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java @@ -3,9 +3,11 @@ package p.studio.lsp.services.protocol; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; +import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.SignatureHelp; @@ -16,6 +18,8 @@ import org.junit.jupiter.api.Test; import p.studio.lsp.messages.BaselineCompletion; import p.studio.lsp.messages.BaselineCompletionItem; import p.studio.lsp.messages.BaselineCompletionItemKind; +import p.studio.lsp.messages.BaselineDefinition; +import p.studio.lsp.messages.BaselineDefinitionLocation; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineSemanticTokens; @@ -77,6 +81,14 @@ final class PrometeuTextDocumentServiceTest { assertEquals(1, bridge.lastSignatureHelpLine); assertEquals(6, bridge.lastSignatureHelpCharacter); assertTrue(mapper.mappedSignatureHelp.signatures().getFirst().label().contains("demo")); + + final var definition = service.definition(new DefinitionParams( + new TextDocumentIdentifier(uri), + new Position(1, 2))).join(); + assertSame(mapper.definitionLocations, definition.getLeft()); + assertEquals(text, bridge.lastDefinitionText); + assertEquals(1, bridge.lastDefinitionLine); + assertEquals(2, bridge.lastDefinitionCharacter); } private static final class RecordingBridge implements LanguageServiceBridge { @@ -84,6 +96,9 @@ final class PrometeuTextDocumentServiceTest { private int lastCompletionLine; private int lastCompletionCharacter; private String lastHoverText; + private String lastDefinitionText; + private int lastDefinitionLine; + private int lastDefinitionCharacter; private String lastSignatureHelpText; private int lastSignatureHelpLine; private int lastSignatureHelpCharacter; @@ -123,6 +138,19 @@ final class PrometeuTextDocumentServiceTest { return new BaselineHover("hover"); } + @Override + public BaselineDefinition definition( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + lastDefinitionText = text; + lastDefinitionLine = line; + lastDefinitionCharacter = character; + return new BaselineDefinition(List.of(new BaselineDefinitionLocation(documentUri, 0, 0, 0, 6))); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext context, @@ -151,6 +179,7 @@ final class PrometeuTextDocumentServiceTest { private static final class RecordingMapper implements ProtocolMessageMapper { private final CompletionList completionList = new CompletionList(false, List.of(new CompletionItem("demo"))); private final SignatureHelp signatureHelp = new SignatureHelp(); + private final List definitionLocations = List.of(); private BaselineSignatureHelp mappedSignatureHelp; @Override @@ -178,6 +207,11 @@ final class PrometeuTextDocumentServiceTest { return new Hover(content); } + @Override + public List mapDefinition(final BaselineDefinition definition) { + return definitionLocations; + } + @Override public CompletionList mapCompletion(final BaselineCompletion completion) { return completionList; diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index 80e91e7f..e1cec4ea 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -6,6 +6,8 @@ import org.junit.jupiter.api.Test; import p.studio.lsp.messages.BaselineCompletion; import p.studio.lsp.messages.BaselineCompletionItem; import p.studio.lsp.messages.BaselineCompletionItemKind; +import p.studio.lsp.messages.BaselineDefinition; +import p.studio.lsp.messages.BaselineDefinitionLocation; import p.studio.lsp.messages.BaselineEditorPalette; import p.studio.lsp.messages.BaselineSemanticHostProjection; import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry; @@ -35,6 +37,7 @@ final class Lsp4jProtocolMessageMapperTest { true, true, true, + true, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", @@ -55,6 +58,7 @@ final class Lsp4jProtocolMessageMapperTest { assertEquals(List.of("."), result.getCapabilities().getCompletionProvider().getTriggerCharacters()); assertEquals(List.of("(", ","), result.getCapabilities().getSignatureHelpProvider().getTriggerCharacters()); + assertEquals(Boolean.TRUE, result.getCapabilities().getDefinitionProvider().getLeft()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); @@ -104,4 +108,17 @@ final class Lsp4jProtocolMessageMapperTest { assertEquals("left: int", signatureHelp.getSignatures().getFirst().getParameters().getFirst().getLabel().getLeft()); assertEquals(1, signatureHelp.getActiveParameter()); } + + @Test + void definitionMapsPhysicalFileLocations() { + final var mapper = new Lsp4jProtocolMessageMapper(); + final var locations = mapper.mapDefinition(new BaselineDefinition(List.of( + new BaselineDefinitionLocation("file:///tmp/demo.pbs", 2, 3, 2, 9)))); + assertEquals(1, locations.size()); + assertEquals("file:///tmp/demo.pbs", locations.getFirst().getUri()); + assertEquals(2, locations.getFirst().getRange().getStart().getLine()); + assertEquals(3, locations.getFirst().getRange().getStart().getCharacter()); + assertEquals(9, locations.getFirst().getRange().getEnd().getCharacter()); + assertTrue(mapper.mapDefinition(BaselineDefinition.empty()).isEmpty()); + } } -- 2.47.2