implements PLN-0132 #26
@ -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"}]}
|
||||
|
||||
@ -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
|
||||
@ -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.
|
||||
@ -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.
|
||||
|
||||
@ -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<PbsEditorialCompletionCandidate> completion(
|
||||
@ -213,6 +217,56 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
return List.copyOf(locations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendDefinitionLocation> 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<PbsAst.TopDecl> typedSupplementalTopDecls =
|
||||
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
|
||||
final List<PbsEditorialLocation> editorialIdentity = definition(
|
||||
request.documentText(),
|
||||
ast,
|
||||
typedSupplementalTopDecls,
|
||||
feSurfaceContext,
|
||||
offset);
|
||||
final ArrayList<PbsEditorialLocation> 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<PbsEditorialLocation> usages = editorialSupportService.references(
|
||||
cursorDocument,
|
||||
offset,
|
||||
projectDocuments(request, editorialContext, cursorDocument, fileTable));
|
||||
final LinkedHashMap<String, FrontendDefinitionLocation> 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<FrontendSemanticToken> 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<EditorialDocument> 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<EditorialDocument> 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<PbsAst.TopDecl> 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<FrontendDefinitionLocation> toFrontendLocation(
|
||||
final PbsEditorialLocation location,
|
||||
final FileTableReader fileTable) {
|
||||
|
||||
@ -91,6 +91,73 @@ public final class PbsEditorialSupportService {
|
||||
return buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext).definitionAt(offset);
|
||||
}
|
||||
|
||||
public List<PbsEditorialLocation> references(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final int offset) {
|
||||
return references(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset);
|
||||
}
|
||||
|
||||
public List<PbsEditorialLocation> references(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> 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<PbsEditorialLocation> references(
|
||||
final EditorialDocument cursorDocument,
|
||||
final int offset,
|
||||
final List<EditorialDocument> projectDocuments) {
|
||||
if (cursorDocument == null) {
|
||||
return List.of();
|
||||
}
|
||||
final List<PbsEditorialLocation> identity = definition(
|
||||
cursorDocument.sourceText(),
|
||||
cursorDocument.ast(),
|
||||
cursorDocument.supplementalTopDecls(),
|
||||
cursorDocument.feSurfaceContext(),
|
||||
offset);
|
||||
if (identity.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final Set<String> identityKeys = locationKeys(identity);
|
||||
final LinkedHashMap<String, PbsEditorialLocation> usages = new LinkedHashMap<>();
|
||||
final List<EditorialDocument> 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<PbsAst.TopDecl> 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<PbsEditorialSignatureHelp> 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<String> locationKeys(final List<PbsEditorialLocation> locations) {
|
||||
final LinkedHashSet<String> 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<PbsEditorialLocation> usagesMatching(final Set<String> identityKeys) {
|
||||
if (identityKeys == null || identityKeys.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final FileId fileId = ast.span() == null ? FileId.none() : ast.span().getFileId();
|
||||
final LinkedHashMap<String, PbsEditorialLocation> 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<PbsEditorialLocation> locations,
|
||||
final Set<String> 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<PbsEditorialLocation> resolveSymbolDefinition(
|
||||
final String symbolName,
|
||||
final int tokenIndex,
|
||||
|
||||
@ -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<PbsEditorialLocation> requireDefinition(
|
||||
final String source,
|
||||
final PbsAst.File ast,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,4 +72,17 @@ public interface FrontendLanguageService {
|
||||
final int offset) {
|
||||
return definition(request, offset);
|
||||
}
|
||||
|
||||
default List<FrontendDefinitionLocation> references(
|
||||
final FrontendDocumentRequest request,
|
||||
final int offset) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendDefinitionLocation> references(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset) {
|
||||
return references(request, offset);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineReferences(
|
||||
List<BaselineDefinitionLocation> locations) {
|
||||
|
||||
public BaselineReferences {
|
||||
locations = List.copyOf(Objects.requireNonNull(locations, "locations"));
|
||||
}
|
||||
|
||||
public static BaselineReferences empty() {
|
||||
return new BaselineReferences(List.of());
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ public record BaselineServerDescription(
|
||||
boolean completionSupported,
|
||||
boolean signatureHelpSupported,
|
||||
boolean definitionSupported,
|
||||
boolean referencesSupported,
|
||||
List<String> semanticTokenTypes,
|
||||
List<BaselineSemanticHostProjection> semanticHostProjections,
|
||||
List<BaselineVisualTheme> visualThemes,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<String, FrontendDefinitionLocation> 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<BaselineDefinitionLocation> mapDefinitionLocations(
|
||||
final List<FrontendDefinitionLocation> locations,
|
||||
final Path currentDocumentPath,
|
||||
|
||||
@ -111,6 +111,22 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
return CompletableFuture.completedFuture(Either.forLeft(locations));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<? extends Location>> 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> signatureHelp(final SignatureHelpParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
|
||||
@ -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<Location> mapReferences(final BaselineReferences references) {
|
||||
if (references == null || references.locations().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return mapLocations(references.locations());
|
||||
}
|
||||
|
||||
private List<Location> mapLocations(final List<BaselineDefinitionLocation> locations) {
|
||||
return locations.stream()
|
||||
.map(location -> new Location(
|
||||
location.uri(),
|
||||
new Range(
|
||||
|
||||
@ -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<Location> mapDefinition(BaselineDefinition definition);
|
||||
|
||||
List<Location> mapReferences(BaselineReferences references);
|
||||
|
||||
CompletionList mapCompletion(BaselineCompletion completion);
|
||||
|
||||
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<org.eclipse.lsp4j.Location> mapReferences(
|
||||
final p.studio.lsp.messages.BaselineReferences references) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@ -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<Location> definitionLocations = List.of();
|
||||
private final List<Location> referenceLocations = List.of();
|
||||
private BaselineSignatureHelp mappedSignatureHelp;
|
||||
|
||||
@Override
|
||||
@ -212,6 +245,11 @@ final class PrometeuTextDocumentServiceTest {
|
||||
return definitionLocations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Location> mapReferences(final BaselineReferences references) {
|
||||
return referenceLocations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
return completionList;
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user