From 9458312d73ed4f4e0719dff5f79f792026e79245 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 21 Sep 2026 08:54:11 +0100 Subject: [PATCH] implements PLN-0132 Add PBS find-references as the request-local inverse of definition identity on FrontendLanguageService. Usage sites are identifier tokens whose definition locations intersect the cursor set. Empty results stay honest when definition is empty, including virtual stdlib paths. Honor includeDeclaration in the LSP bridge. Housekeep DSC-0040 with LSN-0069. --- discussion/index.ndjson | 4 +- ...s-is-the-physical-inverse-of-definition.md | 111 ++++++++++++++++ .../AGD-0043-pbs-lsp-find-references.md | 62 --------- ...ler Pipeline Entry Points Specification.md | 2 + .../compiler/PBSFrontendLanguageService.java | 124 +++++++++++++++++- .../semantics/PbsEditorialSupportService.java | 122 +++++++++++++++++ .../PbsEditorialSupportServiceTest.java | 88 +++++++++++++ .../services/FrontendEditorialContext.java | 13 +- .../services/FrontendLanguageService.java | 13 ++ .../services/FrontendLanguageServiceTest.java | 1 + .../lsp/messages/BaselineReferences.java | 16 +++ .../messages/BaselineServerDescription.java | 1 + .../lsp/services/LanguageServiceBridge.java | 9 ++ .../CompilerLanguageServiceBridge.java | 49 +++++++ .../protocol/PrometeuTextDocumentService.java | 16 +++ .../mapping/Lsp4jProtocolMessageMapper.java | 15 ++- .../mapping/ProtocolMessageMapper.java | 3 + .../CompilerLanguageServiceBridgeTest.java | 78 +++++++++++ .../protocol/PrometeuLanguageServerTest.java | 18 +++ .../PrometeuTextDocumentServiceTest.java | 38 ++++++ .../Lsp4jProtocolMessageMapperTest.java | 14 ++ 21 files changed, 729 insertions(+), 68 deletions(-) create mode 100644 discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md delete mode 100644 discussion/workflow/agendas/AGD-0043-pbs-lsp-find-references.md create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineReferences.java diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 086ba976..9ea463fd 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":53,"PLN":132,"LSN":69,"CLSN":1}} +{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":54,"PLN":133,"LSN":70,"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"}]} @@ -24,7 +24,7 @@ {"type":"discussion","id":"DSC-0043","status":"open","ticket":"pbs-lsp-rename-symbol","title":"PBS LSP Rename Symbol","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","rename"],"agendas":[{"id":"AGD-0046","file":"AGD-0046-pbs-lsp-rename-symbol.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"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-0040","status":"done","ticket":"pbs-lsp-find-references","title":"PBS LSP Find References","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","references"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0069","file":"discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]} {"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"}]} diff --git a/discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md b/discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md new file mode 100644 index 00000000..31d1daa2 --- /dev/null +++ b/discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md @@ -0,0 +1,111 @@ +--- +id: LSN-0069 +ticket: pbs-lsp-find-references +title: PBS find-references is the physical inverse of definition +created: 2026-09-21 +tags: [studio, lsp, vscode, compiler-pbs, editor, references] +--- + +# PBS find-references is the physical inverse of definition + +## Original Problem + +PBS already had compiler-backed completion, hover, signature help, and go-to-definition (`LSN-0051`, `LSN-0068`). Users still could not ask where a resolved symbol is used. Text search confuses overloads, shadowing, members, comments, and coincidental names. Rename (`AGD-0046`) and hierarchy (`AGD-0056`) need an honest usage list before they exist. + +The risk was inventing a `SymbolId` / `RefIndex`, waiting for snapshot cache (`AGD-0051`), or listing uses of stdlib APIs whose declarations are still virtual paths. + +## Consolidated Decision + +PBS Find References is the LSP projection of **physical usage sites** of the **same identity definition already returns**. + +Durable locks from `DEC-0053` (AGD-0043 Q1–Q6 = A): + +1. Resolve the cursor through the definition path (locals, parameters, members, imports/supplementals, current-module top-level). Identity is that physical declaration-location set. Do not text-search. Do not add `SymbolId` or a persistent `RefIndex`. +2. A site is a usage when its definition locations intersect the cursor identity. Overloads follow that intersection. +3. If definition at the same offset is empty, references is empty. +4. Walk regular `fileTable` files from the current `analyze()` surface. Do not scan `/virtual/stdlib` or other non-regular handles. +5. Keep the capability on the existing aggregated `FrontendLanguageService`. Reuse `FrontendDefinitionLocation`. Map PBS spans in `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. No References SPI. +6. Honor LSP `includeDeclaration` in the server: union usage sites with definition locations when the host asks and the declaration is a regular file. No read/write classification. +7. Advertise `referencesProvider`. Partial coverage is valid. Analysis stays request-local. This discussion does not build rename, hierarchy, workspace symbols, virtual documents, or snapshot cache. + +Spec 23 §8.3 now also states: a references location must identify a compiler-known physical source file; empty definition implies empty references; `includeDeclaration` must not invent a virtual location. + +## Final Implementation + +| Layer | What landed | +|---|---| +| PBS editorial | `PbsEditorialSupportService.references` walks `IDENTIFIER` tokens and keeps sites whose `definition` locations intersect the cursor identity (`FileId` + start + end). | +| Generic contract | `FrontendLanguageService.references` defaults to `List.of()`. `FrontendEditorialContext.projectSurface` carries the PBS semantic read surface so the walk can see `astByFile`. | +| PBS mapping | `PBSFrontendLanguageService` requires a non-empty physical definition set, skips non-regular files, and maps surviving spans through the same `Files.isRegularFile` helper as definition. | +| LSP | `referencesProvider`, `textDocument/references`. The bridge applies `includeDeclaration` by unioning `references` with `definition`. Compile-only frontends stay empty. | +| VS Code | Unchanged thin client. | + +Stdlib and SDK modules remain virtual (`/virtual/stdlib/...`). Shift+F12 on `Gfx.clear` is empty, matching F12. Hover is unchanged. + +Local binding names are not usage sites: definition at `let total` is empty because the name is not yet in scope. The use `return total` matches. `includeDeclaration` adds the declaration span from definition, not a second name search. + +## Examples + +Same-file function uses, including the declaring identifier (top-level names resolve to themselves): + +```pbs +fn helper() -> int { return 42; } + +fn frame() -> void { + helper(); + helper(); + // helper // not a reference + let text = "helper"; // not a reference +} +``` + +Imported project (or otherwise physical) symbols collect uses in the importing file. Identity is the foreign declaration span. + +```pbs +import { Log as Logger } from @sdk:log; +Logger.info("a"); +Logger.info("b"); +``` + +If `@sdk:log` is only a virtual handle, definition is empty, so references is empty. + +Builtins stay hover-only: + +```pbs +let n: int = 1; // hover works; references is empty +``` + +`includeDeclaration=true` adds the canonical declaration location on top of identifier usage sites. `false` returns usages only. + +## Pitfalls + +Do not implement find-references as workspace text search in `lsp-v1` or in the VS Code extension. It will lie about overloads, members, and imports, then poison rename. + +Do not add a `SymbolId` index “for rename later”. Definition already gave the key: physical declaration locations. Rename can consume this list once it has its own decision. + +Do not wait for snapshot cache. The LSP already reanalyzes on each editorial request. Snapshots (`AGD-0051`) can make the walk cheaper without changing the contract. + +Do not treat empty stdlib references as a missing feature. Virtual paths are not regular files. Empty is the honest result until those sources live on disk. + +Do not expect `let total` to appear as a usage of `total`. Inverse-of-definition only keeps identifier sites that themselves resolve. The declaration span arrives through `includeDeclaration`. + +Do not put `includeDeclaration` into PBS types. It is a host flag applied in the LSP bridge. + +Do not scan `/virtual/stdlib` to “find more uses”. That reopens virtual documents. + +Do not split a References SPI “for future languages”. PBS is a real consumer of `FrontendLanguageService` (`LSN-0067`). + +Do not treat this lesson as closing rename, snapshots, workspace symbols, document links, or hierarchy. + +## References + +- Agenda: `AGD-0043` (Q1–Q6 = A) +- Decision: `DEC-0053` +- Plan: `PLN-0132` +- Spec 23 §8.3 — optional navigation; physical-file-or-empty; empty definition implies empty references +- `LSN-0068` — definition identity this feature inverts +- `LSN-0051` — wave 1 deferred navigation +- `LSN-0058` — generic `FrontendLanguageService` for LSP +- `LSN-0047` — protocol stays in `lsp-v1`; compiler owns semantics +- `LSN-0067` — no SPI without a real in-repo consumer +- Still open: `AGD-0046` rename, `AGD-0045` workspace symbols, `AGD-0051` snapshots, `AGD-0055` document links, `AGD-0056` hierarchy diff --git a/discussion/workflow/agendas/AGD-0043-pbs-lsp-find-references.md b/discussion/workflow/agendas/AGD-0043-pbs-lsp-find-references.md deleted file mode 100644 index aa85d49d..00000000 --- a/discussion/workflow/agendas/AGD-0043-pbs-lsp-find-references.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0043 -ticket: pbs-lsp-find-references -title: PBS LSP Find References -status: open -created: 2026-07-15 -resolved: -decision: -tags: [studio, lsp, vscode, compiler-pbs, editor, references] ---- - -## Pain - -Domain owner: `studio/lsp` - -PBS users cannot ask where a symbol is used, so changes to functions, services, structs, constants, and stdlib-facing APIs require manual search that confuses overloads and unrelated text matches. - -## Context - -Completion and hover already resolve symbols in local editor context, but the LSP does not expose references and the compiler/editorial layer does not yet provide project-wide usage locations keyed by semantic identity. - -## Open Questions - -- [ ] Which declarations need reference tracking first: local functions, methods, types, constants, imports, stdlib symbols, or all of them? -- [ ] How should overloads and same-name symbols in different scopes be disambiguated? -- [ ] Should references include declarations, write/read classification, or only usage sites in the first wave? - -## Options - -### Option A - Text search with filters - -- **Approach:** Search workspace text for the selected identifier and filter obvious false positives by token kind. -- **Pro:** Simple and fast for early demos. -- **Con:** Incorrect for overloads, scope, shadowing, fields, methods, imports, and generated/stdlib surfaces. -- **Maintainability:** Poor; later semantic references would duplicate and replace it. - -### Option B - Semantic usage index - -- **Approach:** During semantic analysis, emit definition and usage sites tied to stable symbol identities and expose `textDocument/references`. -- **Pro:** Correct foundation for rename, call hierarchy, diagnostics UX, and code actions. -- **Con:** Requires compiler-facing usage modeling and careful invalidation. -- **Maintainability:** Strong; one source of truth for all symbol usage features. - -## Tradeoffs - -Reference accuracy matters more than early breadth. False positives are worse than missing unsupported categories because users will trust rename and references for safe edits. - -## Recommendation - -Prefer Option B, but scope the first wave to project-owned declarations whose identity is already resolved reliably. - -## Discussion - -References should follow go-to-definition because both need the same identity and location model. - -## Resolution - -Ainda em aberto. - -## Next Step - -Define first-wave symbol categories and whether declarations are included in reference results. 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 5a22fe5a..d3a577a2 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -274,6 +274,8 @@ Absent editor capabilities MUST have deterministic fallback behavior at tooling 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. +When a frontend exposes find-references, each returned usage or declaration location MUST identify a compiler-known physical source file. A cursor offset whose definition response is empty MUST produce an empty references response. Host `includeDeclaration` MAY add the canonical declaration location only when that declaration is a compiler-known physical source file; it MUST NOT invent a virtual, untitled, or synthetic location. + 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 44a54174..18e3cc3c 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 @@ -11,6 +11,8 @@ 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.pbs.semantics.PbsEditorialSupportService.EditorialDocument; +import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface; import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDocumentRequest; @@ -31,6 +33,7 @@ import p.studio.utilities.structures.ReadOnlyList; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -69,7 +72,8 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService ast, semanticReadSurface.supplementalTopDeclsByFile().getOrDefault(fileId, ReadOnlyList.empty()), phaseContext.feSurfaceContext(), - phaseContext.fileTable)); + phaseContext.fileTable, + semanticReadSurface)); } public List completion( @@ -213,6 +217,56 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return List.copyOf(locations); } + @Override + public List references( + 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 List editorialIdentity = definition( + request.documentText(), + ast, + typedSupplementalTopDecls, + feSurfaceContext, + offset); + final ArrayList physicalIdentity = new ArrayList<>(); + for (final PbsEditorialLocation location : editorialIdentity) { + if (toFrontendLocation(location, fileTable).isPresent()) { + physicalIdentity.add(location); + } + } + if (physicalIdentity.isEmpty()) { + return List.of(); + } + + final EditorialDocument cursorDocument = new EditorialDocument( + request.documentText(), + ast, + typedSupplementalTopDecls, + feSurfaceContext); + final List usages = editorialSupportService.references( + cursorDocument, + offset, + projectDocuments(request, editorialContext, cursorDocument, fileTable)); + final LinkedHashMap mapped = new LinkedHashMap<>(); + for (final PbsEditorialLocation usage : usages) { + toFrontendLocation(usage, fileTable).ifPresent(location -> + mapped.putIfAbsent(frontendLocationKey(location), location)); + } + return List.copyOf(mapped.values()); + } + @Override public List semanticTokens(final FrontendDocumentRequest request) { return semanticTokens(request == null ? "" : request.documentText()); @@ -260,6 +314,74 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return FrontendSymbolKind.valueOf(kind.name()); } + private List projectDocuments( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final EditorialDocument cursorDocument, + final FileTableReader fileTable) { + if (!(editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface) + || surface.astByFile() == null + || surface.astByFile().isEmpty()) { + return List.of(cursorDocument); + } + final ArrayList documents = new ArrayList<>(); + for (final var entry : surface.astByFile().entrySet()) { + if (!isRegularSource(entry.getKey(), fileTable)) { + continue; + } + final PbsAst.File fileAst = entry.getValue(); + if (fileAst == null) { + continue; + } + final ReadOnlyList fileSupplementals = fileAst == cursorDocument.ast() + ? cursorDocument.supplementalTopDecls() + : surface.supplementalTopDeclsByFile().getOrDefault(entry.getKey(), ReadOnlyList.empty()); + documents.add(new EditorialDocument( + textForFile(entry.getKey(), fileTable, request), + fileAst, + fileSupplementals, + cursorDocument.feSurfaceContext())); + } + if (documents.isEmpty()) { + return List.of(cursorDocument); + } + return List.copyOf(documents); + } + + private boolean isRegularSource( + final FileId fileId, + final FileTableReader fileTable) { + if (fileId == null || fileId.isNone() || fileTable == null) { + return false; + } + final var sourceHandle = fileTable.get(fileId); + if (sourceHandle == null || sourceHandle.getCanonPath() == null) { + return false; + } + return Files.isRegularFile(sourceHandle.getCanonPath().toAbsolutePath().normalize()); + } + + private String textForFile( + final FileId fileId, + final FileTableReader fileTable, + final FrontendDocumentRequest request) { + final var sourceHandle = fileTable.get(fileId); + if (sourceHandle == null || sourceHandle.getCanonPath() == null) { + return ""; + } + final Path path = sourceHandle.getCanonPath().toAbsolutePath().normalize(); + if (request != null + && request.documentPath() != null + && path.equals(request.documentPath().toAbsolutePath().normalize())) { + return request.documentText() == null ? "" : request.documentText(); + } + return sourceHandle.readUtf8().orElse(""); + } + + private static String frontendLocationKey(final FrontendDefinitionLocation location) { + return location.path() + ":" + location.startOffset() + ":" + location.endOffset(); + } + private Optional toFrontendLocation( final PbsEditorialLocation location, final FileTableReader fileTable) { 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 35af295a..c6b73de4 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 @@ -91,6 +91,73 @@ public final class PbsEditorialSupportService { return buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext).definitionAt(offset); } + public List references( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final int offset) { + return references(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset); + } + + public List references( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + final EditorialDocument cursor = new EditorialDocument(sourceText, ast, supplementalTopDecls, feSurfaceContext); + return references(cursor, offset, List.of(cursor)); + } + + public List references( + final EditorialDocument cursorDocument, + final int offset, + final List projectDocuments) { + if (cursorDocument == null) { + return List.of(); + } + final List identity = definition( + cursorDocument.sourceText(), + cursorDocument.ast(), + cursorDocument.supplementalTopDecls(), + cursorDocument.feSurfaceContext(), + offset); + if (identity.isEmpty()) { + return List.of(); + } + final Set identityKeys = locationKeys(identity); + final LinkedHashMap usages = new LinkedHashMap<>(); + final List documents = projectDocuments == null || projectDocuments.isEmpty() + ? List.of(cursorDocument) + : projectDocuments; + for (final EditorialDocument document : documents) { + if (document == null || document.ast() == null) { + continue; + } + for (final PbsEditorialLocation usage : buildContext( + document.sourceText(), + document.ast(), + document.supplementalTopDecls(), + document.feSurfaceContext()).usagesMatching(identityKeys)) { + usages.putIfAbsent(locationKey(usage.span()), usage); + } + } + return List.copyOf(usages.values()); + } + + public record EditorialDocument( + String sourceText, + PbsAst.File ast, + ReadOnlyList supplementalTopDecls, + FESurfaceContext feSurfaceContext) { + public EditorialDocument { + sourceText = sourceText == null ? "" : sourceText; + Objects.requireNonNull(ast, "ast"); + supplementalTopDecls = supplementalTopDecls == null ? ReadOnlyList.empty() : supplementalTopDecls; + feSurfaceContext = feSurfaceContext == null ? FESurfaceContext.empty() : feSurfaceContext; + } + } + public Optional signatureHelp( final String sourceText, final PbsAst.File ast, @@ -133,6 +200,25 @@ public final class PbsEditorialSupportService { return new DocumentContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); } + private static Set locationKeys(final List locations) { + final LinkedHashSet keys = new LinkedHashSet<>(); + if (locations == null) { + return Set.of(); + } + for (final PbsEditorialLocation location : locations) { + if (location != null && location.span() != null) { + keys.add(locationKey(location.span())); + } + } + return Set.copyOf(keys); + } + + private static String locationKey(final Span span) { + final FileId fileId = span.getFileId(); + final int fileNumber = fileId == null ? -1 : fileId.getId(); + return fileNumber + ":" + span.getStart() + ":" + span.getEnd(); + } + private static final class DocumentContext { private final String sourceText; private final PbsAst.File ast; @@ -642,6 +728,42 @@ public final class PbsEditorialSupportService { return resolveSymbolDefinition(token.lexeme(), tokenIndex, scopeState, offset); } + private List usagesMatching(final Set identityKeys) { + if (identityKeys == null || identityKeys.isEmpty()) { + return List.of(); + } + final FileId fileId = ast.span() == null ? FileId.none() : ast.span().getFileId(); + final LinkedHashMap usages = new LinkedHashMap<>(); + for (final PbsToken token : tokens) { + if (token.kind() != PbsTokenKind.IDENTIFIER) { + continue; + } + if (!intersects(definitionAt(token.start()), identityKeys)) { + continue; + } + final Span usageSpan = new Span(fileId, token.start(), token.end()); + if (!hasPhysicalSpan(usageSpan)) { + continue; + } + usages.putIfAbsent(locationKey(usageSpan), new PbsEditorialLocation(usageSpan)); + } + return List.copyOf(usages.values()); + } + + private boolean intersects( + final List locations, + final Set identityKeys) { + if (locations == null || locations.isEmpty()) { + return false; + } + for (final PbsEditorialLocation location : locations) { + if (location != null && location.span() != null && identityKeys.contains(locationKey(location.span()))) { + return true; + } + } + return false; + } + private List resolveSymbolDefinition( final String symbolName, final int tokenIndex, 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 346a5ab3..6b1931df 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 @@ -334,6 +334,94 @@ final class PbsEditorialSupportServiceTest { assertTrue(locations.get(1).span().contains(source.indexOf("fn blend(dx: int, dy: int)"))); } + @Test + void shouldCollectSameFileReferencesForPhysicalSymbolsAndSkipTrivia() { + final var source = """ + fn helper(value: int) -> int { + let total = value; + return total; + } + + fn frame() -> void { + helper(1); + helper(2); + // helper + let text = "helper"; + missingName; + } + """; + final var ast = parseOrdinary(source); + + final var helperRefs = service.references(source, ast, ReadOnlyList.empty(), source.indexOf("helper(1)")); + assertEquals(3, helperRefs.size(), helperRefs.toString()); + assertTrue(helperRefs.stream().anyMatch(location -> location.span().contains(source.indexOf("helper(value")))); + assertTrue(helperRefs.stream().anyMatch(location -> location.span().contains(source.indexOf("helper(1)")))); + assertTrue(helperRefs.stream().anyMatch(location -> location.span().contains(source.indexOf("helper(2)")))); + assertTrue(helperRefs.stream().noneMatch(location -> location.span().contains(source.indexOf("// helper")))); + assertTrue(helperRefs.stream().noneMatch(location -> location.span().contains(source.indexOf("\"helper\"")))); + + final var parameterRefs = service.references(source, ast, ReadOnlyList.empty(), source.indexOf("value;")); + assertEquals(2, parameterRefs.size()); + assertTrue(parameterRefs.stream().anyMatch(location -> location.span().contains(source.indexOf("value: int")))); + assertTrue(parameterRefs.stream().anyMatch(location -> location.span().contains(source.indexOf("value;")))); + + final var localRefs = service.references(source, ast, ReadOnlyList.empty(), source.indexOf("total;")); + assertEquals(1, localRefs.size()); + assertTrue(localRefs.getFirst().span().contains(source.indexOf("total;"))); + assertTrue(service.definition(source, ast, ReadOnlyList.empty(), source.indexOf("total = value")).isEmpty()); + + assertTrue(service.references(source, ast, ReadOnlyList.empty(), source.indexOf("fn helper")).isEmpty()); + assertTrue(service.references(source, ast, ReadOnlyList.empty(), source.indexOf("int")).isEmpty()); + assertTrue(service.references(source, ast, ReadOnlyList.empty(), source.indexOf("missingName")).isEmpty()); + } + + @Test + void shouldCollectImportedAndOverloadedReferencesByDefinitionIntersection() { + final var importedSource = """ + declare service Log { + fn info(message: str) -> void { + return; + } + } + """; + final var importedAst = parseInterface(importedSource, new FileId(100)); + final var source = """ + import { Log as Logger } from @sdk:log; + + declare struct Vec() { + fn blend(dx: int) -> int { return dx; } + fn blend(dx: int, dy: int) -> int { return dx; } + } + + fn frame(vec: Vec) -> void { + Logger.info("a"); + Logger.info("b"); + vec.blend(1, 2); + } + """; + final var ast = parseOrdinary(source); + final var supplementalTopDecls = importedAst.topDecls(); + + final var infoRefs = service.references( + new PbsEditorialSupportService.EditorialDocument(source, ast, supplementalTopDecls, null), + source.indexOf("info(\"a\")"), + List.of( + new PbsEditorialSupportService.EditorialDocument(source, ast, supplementalTopDecls, null), + new PbsEditorialSupportService.EditorialDocument( + importedSource, + importedAst, + ReadOnlyList.empty(), + null))); + assertEquals(2, infoRefs.stream().filter(location -> location.span().getFileId().getId() == 0).count()); + assertTrue(infoRefs.stream().anyMatch(location -> location.span().contains(source.indexOf("info(\"a\")")))); + assertTrue(infoRefs.stream().anyMatch(location -> location.span().contains(source.indexOf("info(\"b\")")))); + + final var blendRefs = service.references(source, ast, supplementalTopDecls, source.indexOf("blend(1")); + assertTrue(blendRefs.stream().anyMatch(location -> location.span().contains(source.indexOf("blend(1")))); + final var blendIdentity = service.definition(source, ast, supplementalTopDecls, source.indexOf("blend(1")); + assertEquals(2, blendIdentity.size()); + } + private List requireDefinition( final String source, final PbsAst.File ast, 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 60c7256f..d22c8c65 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 @@ -4,12 +4,21 @@ public record FrontendEditorialContext( Object syntaxTree, Object supplementalDeclarations, Object semanticContext, - Object sourceFiles) { + Object sourceFiles, + Object projectSurface) { public FrontendEditorialContext( final Object syntaxTree, final Object supplementalDeclarations, final Object semanticContext) { - this(syntaxTree, supplementalDeclarations, semanticContext, null); + this(syntaxTree, supplementalDeclarations, semanticContext, null, null); + } + + public FrontendEditorialContext( + final Object syntaxTree, + final Object supplementalDeclarations, + final Object semanticContext, + final Object sourceFiles) { + this(syntaxTree, supplementalDeclarations, semanticContext, sourceFiles, 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 0a18be8e..b59564d4 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 @@ -72,4 +72,17 @@ public interface FrontendLanguageService { final int offset) { return definition(request, offset); } + + default List references( + final FrontendDocumentRequest request, + final int offset) { + return List.of(); + } + + default List references( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + return references(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 e0f79366..d6607a02 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 @@ -23,6 +23,7 @@ class FrontendLanguageServiceTest { assertTrue(service.hover(request, 0).isEmpty()); assertEquals(FrontendSignatureHelp.empty(), service.signatureHelp(request, 0)); assertTrue(service.definition(request, 0).isEmpty()); + assertTrue(service.references(request, 0).isEmpty()); } @Test diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineReferences.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineReferences.java new file mode 100644 index 00000000..a4744330 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineReferences.java @@ -0,0 +1,16 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineReferences( + List locations) { + + public BaselineReferences { + locations = List.copyOf(Objects.requireNonNull(locations, "locations")); + } + + public static BaselineReferences empty() { + return new BaselineReferences(List.of()); + } +} 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 5f0397b6..7fdd5693 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 @@ -11,6 +11,7 @@ public record BaselineServerDescription( boolean completionSupported, boolean signatureHelpSupported, boolean definitionSupported, + boolean referencesSupported, 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 f9dfb485..0204cb66 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 @@ -4,6 +4,7 @@ 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.BaselineReferences; import p.studio.lsp.messages.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineSignatureHelp; import p.studio.lsp.messages.BaselineServerDescription; @@ -20,6 +21,14 @@ public interface LanguageServiceBridge { BaselineDefinition definition(LspProjectContext context, String documentUri, String text, int line, int character); + BaselineReferences references( + LspProjectContext context, + String documentUri, + String text, + int line, + int character, + boolean includeDeclaration); + 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 71cdb02e..e753b305 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 @@ -53,6 +53,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg true, true, frontendProvider(context).languageService().isPresent(), + frontendProvider(context).languageService().isPresent(), presentation.semanticKeys(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(), @@ -163,6 +164,50 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg .orElseGet(BaselineDefinition::empty); } + @Override + public BaselineReferences references( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character, + final boolean includeDeclaration) { + Objects.requireNonNull(context, "context"); + final var provider = frontendProvider(context); + final var languageService = provider.languageService(); + if (languageService.isEmpty()) { + return BaselineReferences.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 LinkedHashMap merged = new LinkedHashMap<>(); + for (final FrontendDefinitionLocation location : languageService.orElseThrow().references( + request, + document.editorialContext(), + offset)) { + merged.putIfAbsent(frontendLocationKey(location), location); + } + if (includeDeclaration) { + for (final FrontendDefinitionLocation location : languageService.orElseThrow().definition( + request, + document.editorialContext(), + offset)) { + merged.putIfAbsent(frontendLocationKey(location), location); + } + } + return new BaselineReferences(mapDefinitionLocations( + List.copyOf(merged.values()), + request.documentPath(), + document.text())); + }) + .orElseGet(BaselineReferences::empty); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext context, @@ -442,6 +487,10 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg }; } + private static String frontendLocationKey(final FrontendDefinitionLocation location) { + return location.path() + ":" + location.startOffset() + ":" + location.endOffset(); + } + private List mapDefinitionLocations( final List locations, final Path currentDocumentPath, 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 bd0cf28f..21eaab39 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 @@ -111,6 +111,22 @@ public final class PrometeuTextDocumentService implements TextDocumentService { return CompletableFuture.completedFuture(Either.forLeft(locations)); } + @Override + public CompletableFuture> references(final ReferenceParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.get(uri); + final boolean includeDeclaration = params.getContext() != null + && Boolean.TRUE.equals(params.getContext().isIncludeDeclaration()); + return CompletableFuture.completedFuture(protocolMessageMapper.mapReferences( + languageServiceBridge.references( + project, + uri, + text, + params.getPosition().getLine(), + params.getPosition().getCharacter(), + includeDeclaration))); + } + @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 31de9a7e..e4c2a96e 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 @@ -37,6 +37,7 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { capabilities.setSignatureHelpProvider(signatureHelpOptions); } capabilities.setDefinitionProvider(description.definitionSupported()); + capabilities.setReferencesProvider(description.referencesSupported()); final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setFull(true); @@ -89,7 +90,19 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { if (definition == null || definition.locations().isEmpty()) { return List.of(); } - return definition.locations().stream() + return mapLocations(definition.locations()); + } + + @Override + public List mapReferences(final BaselineReferences references) { + if (references == null || references.locations().isEmpty()) { + return List.of(); + } + return mapLocations(references.locations()); + } + + private List mapLocations(final List locations) { + return locations.stream() .map(location -> new Location( location.uri(), new Range( 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 3cb2aa96..e8b4384f 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 @@ -10,6 +10,7 @@ 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.BaselineReferences; import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineSignatureHelp; @@ -29,6 +30,8 @@ public interface ProtocolMessageMapper { List mapDefinition(BaselineDefinition definition); + List mapReferences(BaselineReferences references); + 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 99dfb98f..bd59adff 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 @@ -48,6 +48,9 @@ class CompilerLanguageServiceBridgeTest { final var definition = bridge.definition(context, documentUri, "fn main() {}", 0, 0); assertTrue(definition.locations().isEmpty()); + final var references = bridge.references(context, documentUri, "fn main() {}", 0, 0, true); + assertTrue(references.locations().isEmpty()); + final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}"); assertTrue(semanticTokens.legend().isEmpty()); assertTrue(semanticTokens.tokens().isEmpty()); @@ -397,6 +400,80 @@ class CompilerLanguageServiceBridgeTest { assertTrue(builtinDefinition.locations().isEmpty()); } + @Test + void referencesUsesDefinitionIdentityOnPhysicalFilesAndHonorsIncludeDeclaration() { + 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(); + helper(); + vec.blend(1, 2); + Gfx.clear(5); + let n: int = 1; + n; + // helper + let text = "helper"; + } + """; + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot); + final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay); + final String documentUri = documentPath.toUri().toString(); + + final var helperUse = mapper.positionOf(overlay.indexOf("helper();")); + final var helperUsages = bridge.references( + context, + documentUri, + overlay, + helperUse.line(), + helperUse.character(), + false); + assertEquals(3, helperUsages.locations().size(), helperUsages.locations().toString()); + assertTrue(helperUsages.locations().stream().allMatch(location -> location.uri().equals(documentUri))); + + final var helperWithDeclaration = bridge.references( + context, + documentUri, + overlay, + helperUse.line(), + helperUse.character(), + true); + assertTrue(helperWithDeclaration.locations().size() > helperUsages.locations().size()); + + final var clearUse = mapper.positionOf(overlay.indexOf("clear")); + final var clearReferences = bridge.references( + context, + documentUri, + overlay, + clearUse.line(), + clearUse.character(), + true); + assertTrue(clearReferences.locations().isEmpty()); + + final var builtinUse = mapper.positionOf(overlay.indexOf("int = 1")); + final var builtinReferences = bridge.references( + context, + documentUri, + overlay, + builtinUse.line(), + builtinUse.character(), + true); + assertTrue(builtinReferences.locations().isEmpty()); + } + @Test void describeServerPublishesFrontendVisualThemes() { final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); @@ -408,6 +485,7 @@ class CompilerLanguageServiceBridgeTest { assertTrue(description.completionSupported()); assertTrue(description.signatureHelpSupported()); assertTrue(description.definitionSupported()); + assertTrue(description.referencesSupported()); 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 7711015f..46327d34 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 @@ -100,6 +100,7 @@ class PrometeuLanguageServerTest { true, true, true, + true, List.of(), List.of(new BaselineSemanticHostProjection( "vscode", @@ -157,6 +158,17 @@ class PrometeuLanguageServerTest { return BaselineDefinition.empty(); } + @Override + public p.studio.lsp.messages.BaselineReferences references( + final LspProjectContext project, + final String documentUri, + final String text, + final int line, + final int character, + final boolean includeDeclaration) { + return p.studio.lsp.messages.BaselineReferences.empty(); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext project, @@ -208,6 +220,12 @@ class PrometeuLanguageServerTest { throw new UnsupportedOperationException(); } + @Override + public java.util.List mapReferences( + final p.studio.lsp.messages.BaselineReferences references) { + 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 7937458d..cd211317 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 @@ -4,6 +4,8 @@ import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.ReferenceContext; +import org.eclipse.lsp4j.ReferenceParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; @@ -20,6 +22,7 @@ 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.BaselineReferences; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineSemanticTokens; @@ -89,6 +92,16 @@ final class PrometeuTextDocumentServiceTest { assertEquals(text, bridge.lastDefinitionText); assertEquals(1, bridge.lastDefinitionLine); assertEquals(2, bridge.lastDefinitionCharacter); + + final var references = service.references(new ReferenceParams( + new TextDocumentIdentifier(uri), + new Position(1, 2), + new ReferenceContext(true))).join(); + assertSame(mapper.referenceLocations, references); + assertEquals(text, bridge.lastReferencesText); + assertEquals(1, bridge.lastReferencesLine); + assertEquals(2, bridge.lastReferencesCharacter); + assertTrue(bridge.lastIncludeDeclaration); } private static final class RecordingBridge implements LanguageServiceBridge { @@ -99,6 +112,10 @@ final class PrometeuTextDocumentServiceTest { private String lastDefinitionText; private int lastDefinitionLine; private int lastDefinitionCharacter; + private String lastReferencesText; + private int lastReferencesLine; + private int lastReferencesCharacter; + private boolean lastIncludeDeclaration; private String lastSignatureHelpText; private int lastSignatureHelpLine; private int lastSignatureHelpCharacter; @@ -151,6 +168,21 @@ final class PrometeuTextDocumentServiceTest { return new BaselineDefinition(List.of(new BaselineDefinitionLocation(documentUri, 0, 0, 0, 6))); } + @Override + public BaselineReferences references( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character, + final boolean includeDeclaration) { + lastReferencesText = text; + lastReferencesLine = line; + lastReferencesCharacter = character; + lastIncludeDeclaration = includeDeclaration; + return new BaselineReferences(List.of(new BaselineDefinitionLocation(documentUri, 0, 0, 0, 6))); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext context, @@ -180,6 +212,7 @@ final class PrometeuTextDocumentServiceTest { 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 final List referenceLocations = List.of(); private BaselineSignatureHelp mappedSignatureHelp; @Override @@ -212,6 +245,11 @@ final class PrometeuTextDocumentServiceTest { return definitionLocations; } + @Override + public List mapReferences(final BaselineReferences references) { + return referenceLocations; + } + @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 e1cec4ea..e194c530 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 @@ -8,6 +8,7 @@ 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.BaselineReferences; import p.studio.lsp.messages.BaselineEditorPalette; import p.studio.lsp.messages.BaselineSemanticHostProjection; import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry; @@ -38,6 +39,7 @@ final class Lsp4jProtocolMessageMapperTest { true, true, true, + true, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", @@ -59,6 +61,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()); + assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); @@ -121,4 +124,15 @@ final class Lsp4jProtocolMessageMapperTest { assertEquals(9, locations.getFirst().getRange().getEnd().getCharacter()); assertTrue(mapper.mapDefinition(BaselineDefinition.empty()).isEmpty()); } + + @Test + void referencesMapsPhysicalFileLocations() { + final var mapper = new Lsp4jProtocolMessageMapper(); + final var locations = mapper.mapReferences(new BaselineReferences(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(3, locations.getFirst().getRange().getStart().getCharacter()); + assertTrue(mapper.mapReferences(BaselineReferences.empty()).isEmpty()); + } }