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