implements PLN-0132
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 62.51% (18192/29102) * Branch Coverage: 53.15% (7093/13345) * Lines of Code: 29102 * Cyclomatic Complexity: 11715 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 663

Add PBS find-references as the request-local inverse of definition
identity on FrontendLanguageService. Usage sites are identifier
tokens whose definition locations intersect the cursor set.
Empty results stay honest when definition is empty, including
virtual stdlib paths. Honor includeDeclaration in the LSP bridge.

Housekeep DSC-0040 with LSN-0069.
This commit is contained in:
bQUARKz 2026-09-21 08:54:11 +01:00
parent 4b72b2a0dd
commit 9458312d73
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
21 changed files with 729 additions and 68 deletions

View File

@ -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"}]}

View File

@ -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 Q1Q6 = 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` (Q1Q6 = 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

View File

@ -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.

View File

@ -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.

View File

@ -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) {

View File

@ -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,

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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

View File

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

View File

@ -11,6 +11,7 @@ public record BaselineServerDescription(
boolean completionSupported,
boolean signatureHelpSupported,
boolean definitionSupported,
boolean referencesSupported,
List<String> semanticTokenTypes,
List<BaselineSemanticHostProjection> semanticHostProjections,
List<BaselineVisualTheme> visualThemes,

View File

@ -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);

View File

@ -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,

View File

@ -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();

View File

@ -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(

View File

@ -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);

View File

@ -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()

View File

@ -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();

View File

@ -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;

View File

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