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()); + } }