implements PLN-0132 #26

Merged
bquarkz merged 1 commits from dev/pbs-lsp-find-references into master 2026-09-21 08:00:42 +00:00
21 changed files with 729 additions and 68 deletions
Showing only changes of commit 9458312d73 - Show all commits

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