implements PLN-0131
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.22% (17996/28921) * Branch Coverage: 52.87% (6986/13213) * Lines of Code: 28921 * Cyclomatic Complexity: 11624 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 659
Intrepid/Prometeu/Studio/pipeline/head This commit looks good

Add PBS go-to-definition as hover editorial lookup plus physical
declaration locations on FrontendLanguageService. Empty results stay
honest when the target is not a regular file, including virtual stdlib
paths.

Housekeep DSC-0039 with LSN-0068.
This commit is contained in:
bQUARKz 2026-09-21 07:50:14 +01:00
parent 9896e7c657
commit c2112f6ee1
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
24 changed files with 885 additions and 81 deletions

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":52,"PLN":131,"LSN":68,"CLSN":1}}
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":53,"PLN":132,"LSN":69,"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"}]}
@ -25,7 +25,7 @@
{"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-0039","status":"open","ticket":"pbs-lsp-go-to-definition","title":"PBS LSP Go to Definition","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","definition"],"agendas":[{"id":"AGD-0042","file":"AGD-0042-pbs-lsp-go-to-definition.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"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"}]}
{"type":"discussion","id":"DSC-0036","status":"done","ticket":"pbs-symbol-documentation-and-hover-markdown","title":"Modelo de documentacao de simbolos em PBS e consumo markdown no hover","created_at":"2026-05-08","updated_at":"2026-07-15","tags":["compiler","compiler-pbs","studio","lsp","vscode","editor","hover","documentation","markdown"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0057","file":"discussion/lessons/DSC-0036-pbs-symbol-documentation-and-hover-markdown/LSN-0057-compiler-owned-pbs-doc-markdown-for-editor-assistance.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}

View File

@ -0,0 +1,98 @@
---
id: LSN-0068
ticket: pbs-lsp-go-to-definition
title: PBS go-to-definition uses editorial lookup and physical files
created: 2026-09-21
tags: [studio, lsp, vscode, compiler-pbs, editor, definition]
---
# PBS go-to-definition uses editorial lookup and physical files
## Original Problem
PBS editor assistance already had compiler-backed completion, hover, and signature help (`LSN-0051`). Users still could not jump from a use to a declaration. Wave 1 explicitly deferred `go to definition`.
The remaining LSP navigation family (references, rename, workspace symbols, document links, hierarchy) needed a destination policy before those features could be honest. The risk was shipping a text search, a new `SymbolId` index, or virtual documents for stdlib before a real physical target existed.
## Consolidated Decision
PBS Go to Definition is the LSP projection of **physical declaration locations** on top of the **same editorial lookup hover already uses**.
Durable locks from `DEC-0052`:
1. Resolve the identifier under the cursor through the hover path (locals, parameters, members, imports/supplementals, current-module top-level). Do not use text search or a new usage index.
2. Keep the capability on the existing aggregated `FrontendLanguageService`. Add a default-empty `definition` method and a generic location DTO. Do not add a Definition SPI.
3. Map PBS editorial spans inside `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`.
4. Jump only when `Span.fileId` maps to a **regular filesystem file**. Otherwise return an empty list. Do not create virtual documents, `untitled:` buffers, or synthetic URIs.
5. The destination is the canonical declaration, not the import-alias line. Overloads return every matching location.
6. Advertise `definitionProvider`. Partial coverage is valid. Keywords, source-less builtins, unresolved names, and virtual stdlib paths stay empty; hover is unchanged.
7. Analysis stays request-local, like hover. This discussion does not build references, rename, or snapshot cache.
Spec 23 §8.3 already listed definition as an optional editor capability. It now also states: a definition location must identify a compiler-known physical source file, or the response must be empty.
## Final Implementation
| Layer | What landed |
|---|---|
| PBS editorial | `PbsEditorialSupportService.definition` reuses hover dispatch and attaches declaration `Span`s (`PbsEditorialLocation`). |
| Generic contract | `FrontendDefinitionLocation` (absolute path + offsets) and `FrontendLanguageService.definition` defaulting to `List.of()`. |
| PBS mapping | `PBSFrontendLanguageService` maps `FileId` through `fileTable` and keeps the location only if `Files.isRegularFile(path)`. |
| LSP | `LanguageServiceBridge.definition`, `definitionProvider`, `textDocument/definition`. Compile-only frontends return empty. |
| VS Code | Unchanged thin client. |
Stdlib and SDK modules are still registered under virtual paths such as `/virtual/stdlib/...`. Those handles are not regular files, so F12 on `Gfx.clear` is empty even though hover works. That is the physical-file rule, not a missing lookup.
The location payload (path + span of an already resolved symbol) is reusable later by references (`AGD-0043`) and rename (`AGD-0046`). Those indexes were not built here. Snapshots (`AGD-0051`) remain a later performance discussion, not a prerequisite for this request-local feature.
## Examples
Same-file jump:
```pbs
fn helper() -> int { return 42; }
fn frame() -> void {
helper(); // definition -> helper's declaration in this file
}
```
Aliased import jumps to the original declaration span, not the `import` line:
```pbs
import { Log as Logger } from @sdk:log;
Logger.info("hello");
```
If `@sdk:log` is only a virtual stdlib handle, definition is empty. If a project file owns the declaration and that file exists on disk, definition uses that path.
Builtins stay hover-only:
```pbs
let n: int = 1; // hover describes the builtin; definition is empty
```
## Pitfalls
Do not implement definition as a second name search in `lsp-v1` or in the VS Code extension. It will break on imports, overloads, and members, then be thrown away when references arrive.
Do not open `/virtual/stdlib/...` or invent an `untitled:` document so stdlib F12 “does something”. Empty is the honest result until those sources are real files.
Do not pick the first overload location in the server. Return the full `Location[]`.
Do not wait for snapshot cache before definition. Hover is already request-local; definition follows that shape.
Do not split a new top-level definition service “for future languages”. PBS is a real consumer of the existing language-service surface (`LSN-0067`).
Do not treat this lesson as closing references, rename, outline, workspace symbols, or document links. Those agendas stay open. This lesson only locks the destination policy they can reuse.
## References
- Agenda: `AGD-0042` (Q1Q6 = A)
- Decision: `DEC-0052`
- Plan: `PLN-0131`
- Spec 23 §8.3 — optional definition capability; physical-file-or-empty
- `LSN-0051` — wave 1 deferred go-to-definition
- `LSN-0058` — generic `FrontendLanguageService` for LSP
- `LSN-0047` — protocol stays in `lsp-v1`; compiler owns semantics
- `LSN-0067` — no SPI or virtual-document host without a real consumer
- Still open: `AGD-0043` references, `AGD-0045` workspace symbols, `AGD-0046` rename, `AGD-0051` snapshots, `AGD-0055` document links, `AGD-0056` hierarchy

View File

@ -1,62 +0,0 @@
---
id: AGD-0042
ticket: pbs-lsp-go-to-definition
title: PBS LSP Go to Definition
status: open
created: 2026-07-15
resolved:
decision:
tags: [studio, lsp, vscode, compiler-pbs, editor, definition]
---
## Pain
Domain owner: `studio/lsp`
PBS users cannot jump from a symbol use to its declaration, which makes navigation across source files, stdlib imports, services, and generated/editorial surfaces slow and error-prone.
## Context
The current LSP exposes diagnostics, hover, completion, signature help, and semantic tokens. The compiler already builds semantic read surfaces and editorial symbol resolution for hover/completion, but there is no LSP definition capability or stable location mapping exposed to VS Code.
## Open Questions
- [ ] Which symbol categories must be supported in the first definition wave?
- [ ] Should stdlib and generated/supplemental declarations navigate to source files, virtual documents, or remain hover-only?
- [ ] What identity model should connect an editor token to the canonical declaration without relying on text search?
## Options
### Option A - Syntactic location lookup
- **Approach:** Use parser spans and local AST traversal to map the token under cursor to a declaration with matching text.
- **Pro:** Fast to implement and useful for same-file declarations.
- **Con:** Breaks on overloads, imports, stdlib, aliases, and same-name symbols.
- **Maintainability:** Weak; it creates a second navigation model separate from semantic resolution.
### Option B - Semantic identity lookup
- **Approach:** Extend the compiler/editorial surface with resolved declaration locations keyed by symbol identity, then expose `textDocument/definition`.
- **Pro:** Correct for imports, overloads, methods, stdlib-backed declarations, and future references/rename.
- **Con:** Requires explicit location metadata in semantic/editorial surfaces.
- **Maintainability:** Strong; it becomes shared infrastructure for references, rename, hierarchy, and code actions.
## Tradeoffs
The syntactic option is tempting for speed, but it would likely be thrown away once references and rename arrive. The main tradeoff is whether stdlib/supplemental declarations must navigate to physical files immediately or can initially return no location while still supporting project symbols.
## Recommendation
Prefer Option B. Build a reusable semantic location index and implement the first wave for project source declarations, then define a policy for stdlib/supplemental targets.
## Discussion
This should probably be the first navigation agenda to resolve because references, rename, workspace symbols, and hierarchy depend on the same symbol identity/location model.
## Resolution
Ainda em aberto.
## Next Step
Decide the first-wave symbol categories and whether stdlib/supplemental declarations require virtual document support.

View File

@ -272,6 +272,8 @@ Absent editor capabilities MUST have deterministic fallback behavior at tooling
3. host protocols that can represent unsupported operations SHOULD do so explicitly;
4. and absent editor capability MUST NOT be reported as a compiler failure.
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.
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

@ -6,11 +6,13 @@ import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.FrontendSemanticToken;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsEditorialCompletionCandidate;
import p.studio.compiler.pbs.semantics.PbsEditorialLocation;
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.services.FrontendCompletionCandidate;
import p.studio.compiler.services.FrontendDefinitionLocation;
import p.studio.compiler.services.FrontendDocumentRequest;
import p.studio.compiler.services.FrontendDocumentation;
import p.studio.compiler.services.FrontendEditorialContext;
@ -20,10 +22,15 @@ import p.studio.compiler.services.FrontendSignature;
import p.studio.compiler.services.FrontendSignatureHelp;
import p.studio.compiler.services.FrontendSymbolKind;
import p.studio.compiler.services.PBSFrontendPhaseService;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.tables.FileTableReader;
import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@ -61,7 +68,8 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return Optional.of(new FrontendEditorialContext(
ast,
semanticReadSurface.supplementalTopDeclsByFile().getOrDefault(fileId, ReadOnlyList.empty()),
phaseContext.feSurfaceContext()));
phaseContext.feSurfaceContext(),
phaseContext.fileTable));
}
public List<PbsEditorialCompletionCandidate> completion(
@ -167,6 +175,44 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
.orElseGet(FrontendSignatureHelp::empty);
}
public List<PbsEditorialLocation> definition(
final String sourceText,
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final FESurfaceContext feSurfaceContext,
final int offset) {
return editorialSupportService.definition(sourceText, ast, supplementalTopDecls, feSurfaceContext, offset);
}
@Override
public List<FrontendDefinitionLocation> definition(
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 ArrayList<FrontendDefinitionLocation> locations = new ArrayList<>();
for (final PbsEditorialLocation location : definition(
request.documentText(),
ast,
typedSupplementalTopDecls,
feSurfaceContext,
offset)) {
toFrontendLocation(location, fileTable).ifPresent(locations::add);
}
return List.copyOf(locations);
}
@Override
public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
return semanticTokens(request == null ? "" : request.documentText());
@ -213,4 +259,35 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
private FrontendSymbolKind toFrontendSymbolKind(final PbsEditorialSymbolKind kind) {
return FrontendSymbolKind.valueOf(kind.name());
}
private Optional<FrontendDefinitionLocation> toFrontendLocation(
final PbsEditorialLocation location,
final FileTableReader fileTable) {
if (location == null || fileTable == null) {
return Optional.empty();
}
final Span span = location.span();
if (span == null || span.isNone() || span.getFileId() == null || span.getFileId().isNone()) {
return Optional.empty();
}
final var sourceHandle = fileTable.get(span.getFileId());
if (sourceHandle == null || sourceHandle.getCanonPath() == null) {
return Optional.empty();
}
final Path path = sourceHandle.getCanonPath().toAbsolutePath().normalize();
if (!Files.isRegularFile(path)) {
return Optional.empty();
}
return Optional.of(new FrontendDefinitionLocation(
path,
toOffset(span.getStart()),
toOffset(span.getEnd())));
}
private static int toOffset(final long value) {
if (value <= 0L) {
return 0;
}
return (int) Math.min(Integer.MAX_VALUE, value);
}
}

View File

@ -0,0 +1,11 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.source.Span;
import java.util.Objects;
public record PbsEditorialLocation(Span span) {
public PbsEditorialLocation {
Objects.requireNonNull(span, "span");
}
}

View File

@ -74,6 +74,23 @@ public final class PbsEditorialSupportService {
return context.resolveSymbolHover(token.lexeme(), tokenIndex, scopeState);
}
public List<PbsEditorialLocation> definition(
final String sourceText,
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final int offset) {
return definition(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset);
}
public List<PbsEditorialLocation> definition(
final String sourceText,
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final FESurfaceContext feSurfaceContext,
final int offset) {
return buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext).definitionAt(offset);
}
public Optional<PbsEditorialSignatureHelp> signatureHelp(
final String sourceText,
final PbsAst.File ast,
@ -610,6 +627,228 @@ public final class PbsEditorialSupportService {
return Optional.empty();
}
private List<PbsEditorialLocation> definitionAt(final int offset) {
final Integer tokenIndex = identifierTokenIndexAt(offset);
if (tokenIndex == null) {
return List.of();
}
final var scopeState = scopeStateAt(offset);
final var token = token(tokenIndex);
final Integer previousTokenIndex = previousSignificantTokenIndex(tokenIndex);
if (previousTokenIndex != null && token(previousTokenIndex).kind() == PbsTokenKind.DOT) {
final TypeView receiverType = resolveReceiverTypeBeforeDot(previousTokenIndex, scopeState);
return resolveMemberDefinition(receiverType, token.lexeme());
}
return resolveSymbolDefinition(token.lexeme(), tokenIndex, scopeState, offset);
}
private List<PbsEditorialLocation> resolveSymbolDefinition(
final String symbolName,
final int tokenIndex,
final ScopeState scopeState,
final int offset) {
if (scopeState.parameterNames().contains(symbolName)) {
return parameterLocations(symbolName, offset);
}
final Scope.LocalSymbol localSymbol = scopeState.scope().entries().get(symbolName);
if (localSymbol != null) {
return localLocations(symbolName, offset);
}
final Integer previousTokenIndex = previousSignificantTokenIndex(tokenIndex);
if (previousTokenIndex != null && token(previousTokenIndex).kind() == PbsTokenKind.NEW) {
return constructorLocations(resolveTopDeclSymbol(symbolName));
}
final TopDeclSymbol importedSymbol = importedSymbols.get(symbolName);
if (importedSymbol != null) {
return topDeclLocations(importedSymbol);
}
final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(symbolName);
if (supplementalSymbol != null) {
return topDeclLocations(supplementalSymbol);
}
final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(symbolName);
if (topLevelSymbol != null) {
return topDeclLocations(topLevelSymbol);
}
return List.of();
}
private List<PbsEditorialLocation> resolveMemberDefinition(
final TypeView receiverType,
final String memberName) {
if (receiverType == null) {
return List.of();
}
if (receiverType.kind() == Kind.STRUCT) {
final Span fieldSpan = structFieldSpan(receiverType.name(), memberName);
if (fieldSpan != null) {
return locations(fieldSpan);
}
final StructInfo structInfo = model.structs.get(receiverType.name());
if (structInfo == null) {
return List.of();
}
return callableLocations(structInfo.methods().get(memberName));
}
if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) {
final Map<String, List<CallableSymbol>> methods = receiverType.kind() == Kind.SERVICE
? Optional.ofNullable(model.services.get(receiverType.name())).map(ServiceInfo::methods).orElse(Map.of())
: Optional.ofNullable(model.contracts.get(receiverType.name())).map(ContractInfo::methods).orElse(Map.of());
return callableLocations(methods.get(memberName));
}
return List.of();
}
private List<PbsEditorialLocation> parameterLocations(
final String symbolName,
final int offset) {
final CallableFrame callableFrame = callableFrameAt(offset);
if (callableFrame == null) {
return List.of();
}
for (final var parameter : callableFrame.parameters()) {
if (symbolName.equals(parameter.name())) {
return locations(parameter.span());
}
}
return List.of();
}
private List<PbsEditorialLocation> localLocations(
final String symbolName,
final int offset) {
final CallableFrame callableFrame = callableFrameAt(offset);
if (callableFrame == null) {
return List.of();
}
return locations(bindingSpan(callableFrame.body(), symbolName, offset, null));
}
private Span bindingSpan(
final PbsAst.Block block,
final String symbolName,
final int offset,
final Span current) {
Span found = current;
for (final var statement : block.statements()) {
if (statement.span().getStart() >= offset) {
break;
}
if (statement instanceof PbsAst.LetStatement letStatement && symbolName.equals(letStatement.name())) {
found = letStatement.nameSpan() == null ? letStatement.span() : letStatement.nameSpan();
}
if (statement.span().contains(offset)) {
if (statement instanceof PbsAst.IfStatement ifStatement) {
if (ifStatement.thenBlock().span().contains(offset)) {
return bindingSpan(ifStatement.thenBlock(), symbolName, offset, found);
}
if (ifStatement.elseBlock() != null && ifStatement.elseBlock().span().contains(offset)) {
return bindingSpan(ifStatement.elseBlock(), symbolName, offset, found);
}
if (ifStatement.elseIf() != null && ifStatement.elseIf().span().contains(offset)) {
final var nestedBlock = new PbsAst.Block(
ReadOnlyList.wrap(List.of(ifStatement.elseIf())),
null,
ifStatement.elseIf().span());
return bindingSpan(nestedBlock, symbolName, offset, found);
}
} else if (statement instanceof PbsAst.ForStatement forStatement) {
Span loopFound = found;
if (symbolName.equals(forStatement.iteratorName())) {
loopFound = forStatement.span();
}
if (forStatement.body().span().contains(offset)) {
return bindingSpan(forStatement.body(), symbolName, offset, loopFound);
}
} else if (statement instanceof PbsAst.WhileStatement whileStatement
&& whileStatement.body().span().contains(offset)) {
return bindingSpan(whileStatement.body(), symbolName, offset, found);
}
return found;
}
}
return found;
}
private List<PbsEditorialLocation> constructorLocations(final TopDeclSymbol topDeclSymbol) {
if (topDeclSymbol == null) {
return List.of();
}
if (topDeclSymbol.decl() instanceof PbsAst.StructDecl structDecl) {
if (structDecl.ctors().isEmpty()) {
return locations(structDecl.span());
}
final ArrayList<Span> ctorSpans = new ArrayList<>();
for (final var ctor : structDecl.ctors()) {
ctorSpans.add(ctor.span());
}
return locations(ctorSpans.toArray(Span[]::new));
}
return topDeclLocations(topDeclSymbol);
}
private List<PbsEditorialLocation> topDeclLocations(final TopDeclSymbol topDeclSymbol) {
if (topDeclSymbol == null) {
return List.of();
}
return locations(topDeclSymbol.decl().span());
}
private List<PbsEditorialLocation> callableLocations(final List<CallableSymbol> callables) {
if (callables == null || callables.isEmpty()) {
return List.of();
}
final ArrayList<Span> spans = new ArrayList<>();
for (final var callable : callables) {
spans.add(callable.span());
}
return locations(spans.toArray(Span[]::new));
}
private Span structFieldSpan(
final String typeName,
final String fieldName) {
final TopDeclSymbol typeSymbol = resolveTopDeclSymbol(typeName);
if (typeSymbol == null) {
return null;
}
if (typeSymbol.decl() instanceof PbsAst.StructDecl structDecl) {
for (final var field : structDecl.fields()) {
if (fieldName.equals(field.name())) {
return field.span();
}
}
}
if (typeSymbol.decl() instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
for (final var field : builtinTypeDecl.fields()) {
if (fieldName.equals(field.name())) {
return field.span();
}
}
}
return null;
}
private List<PbsEditorialLocation> locations(final Span... spans) {
final ArrayList<PbsEditorialLocation> locations = new ArrayList<>();
if (spans == null) {
return List.of();
}
for (final Span span : spans) {
if (hasPhysicalSpan(span)) {
locations.add(new PbsEditorialLocation(span));
}
}
return List.copyOf(locations);
}
private boolean hasPhysicalSpan(final Span span) {
return span != null
&& !span.isNone()
&& span.getFileId() != null
&& !span.getFileId().isNone();
}
private List<PbsEditorialSignature> resolveCallableSignatures(
final String callableName,
final ScopeState scopeState) {

View File

@ -265,6 +265,87 @@ final class PbsEditorialSupportServiceTest {
&& candidate.detail().equals("draw_pixel(x: int, y: int, color: int) -> void")));
}
@Test
void shouldResolveDefinitionForLocalsParametersMembersAndCanonicalImports() {
final var source = """
import { Log as Logger } from @sdk:log;
fn helper(value: int) -> int {
let total = value;
Logger.info("hello");
missingName;
return total;
}
""";
final var ast = parseOrdinary(source);
final var supplementalTopDecls = supplementalTopDecls("""
declare service Log {
fn info(message: str) -> void {
return;
}
}
""");
final var parameter = requireDefinition(source, ast, supplementalTopDecls, "value;");
assertEquals(1, parameter.size());
assertEquals(0, parameter.getFirst().span().getFileId().getId());
assertTrue(parameter.getFirst().span().contains(source.indexOf("value: int")));
final var local = requireDefinition(source, ast, supplementalTopDecls, "total;");
assertEquals(1, local.size());
assertEquals(0, local.getFirst().span().getFileId().getId());
assertTrue(local.getFirst().span().contains(source.indexOf("total = value")));
final var function = requireDefinition(source, ast, supplementalTopDecls, "helper(value");
assertEquals(1, function.size());
assertEquals(0, function.getFirst().span().getFileId().getId());
assertTrue(function.getFirst().span().getStart() <= source.indexOf("fn helper"));
final var imported = requireDefinition(source, ast, supplementalTopDecls, "Logger.info");
assertEquals(1, imported.size());
assertEquals(100, imported.getFirst().span().getFileId().getId());
assertNotEquals(ast.span().getFileId().getId(), imported.getFirst().span().getFileId().getId());
final var member = requireDefinition(source, ast, supplementalTopDecls, "info(");
assertEquals(1, member.size());
assertEquals(100, member.getFirst().span().getFileId().getId());
assertTrue(service.definition(source, ast, supplementalTopDecls, source.indexOf("fn helper")).isEmpty());
assertTrue(service.definition(source, ast, supplementalTopDecls, source.indexOf("int")).isEmpty());
assertTrue(service.definition(source, ast, supplementalTopDecls, source.indexOf("missingName")).isEmpty());
}
@Test
void shouldReturnEveryMatchingDeclarationLocationForOverloadedMembers() {
final var source = """
declare struct Vec() {
fn blend(dx: int) -> int { return dx; }
fn blend(dx: int, dy: int) -> int { return dx; }
}
fn main(vec: Vec) -> void {
vec.blend(1, 2);
}
""";
final var ast = parseOrdinary(source);
final var locations = requireDefinition(source, ast, ReadOnlyList.empty(), "blend(1");
assertEquals(2, locations.size());
assertTrue(locations.getFirst().span().contains(source.indexOf("fn blend(dx: int)")));
assertTrue(locations.get(1).span().contains(source.indexOf("fn blend(dx: int, dy: int)")));
}
private List<PbsEditorialLocation> requireDefinition(
final String source,
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final String needle) {
final int offset = source.indexOf(needle);
assertTrue(offset >= 0, "Expected to find needle: " + needle);
final var locations = service.definition(source, ast, supplementalTopDecls, offset);
assertFalse(locations.isEmpty(), "Expected definition for needle: " + needle);
return locations;
}
private PbsEditorialResolvedSymbol requireHover(
final String source,
final PbsAst.File ast,

View File

@ -0,0 +1,20 @@
package p.studio.compiler.services;
import java.nio.file.Path;
import java.util.Objects;
public record FrontendDefinitionLocation(
Path path,
int startOffset,
int endOffset) {
public FrontendDefinitionLocation {
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
if (startOffset < 0) {
throw new IllegalArgumentException("startOffset must not be negative");
}
if (endOffset < startOffset) {
throw new IllegalArgumentException("endOffset must not be before startOffset");
}
}
}

View File

@ -3,5 +3,13 @@ package p.studio.compiler.services;
public record FrontendEditorialContext(
Object syntaxTree,
Object supplementalDeclarations,
Object semanticContext) {
Object semanticContext,
Object sourceFiles) {
public FrontendEditorialContext(
final Object syntaxTree,
final Object supplementalDeclarations,
final Object semanticContext) {
this(syntaxTree, supplementalDeclarations, semanticContext, null);
}
}

View File

@ -59,4 +59,17 @@ public interface FrontendLanguageService {
final int offset) {
return signatureHelp(request, offset);
}
default List<FrontendDefinitionLocation> definition(
final FrontendDocumentRequest request,
final int offset) {
return List.of();
}
default List<FrontendDefinitionLocation> definition(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset) {
return definition(request, offset);
}
}

View File

@ -22,6 +22,20 @@ class FrontendLanguageServiceTest {
assertTrue(service.completion(request, 0).isEmpty());
assertTrue(service.hover(request, 0).isEmpty());
assertEquals(FrontendSignatureHelp.empty(), service.signatureHelp(request, 0));
assertTrue(service.definition(request, 0).isEmpty());
}
@Test
void definitionLocationRejectsBlankPathAndInvertedOffsets() {
final Path path = Path.of("src/main.pbs").toAbsolutePath();
final var location = new FrontendDefinitionLocation(path, 4, 9);
assertEquals(path.normalize(), location.path());
assertEquals(4, location.startOffset());
assertEquals(9, location.endOffset());
assertThrows(NullPointerException.class, () -> new FrontendDefinitionLocation(null, 0, 1));
assertThrows(IllegalArgumentException.class, () -> new FrontendDefinitionLocation(path, -1, 0));
assertThrows(IllegalArgumentException.class, () -> new FrontendDefinitionLocation(path, 5, 4));
}
@Test

View File

@ -0,0 +1,15 @@
package p.studio.lsp.messages;
import java.util.List;
public record BaselineDefinition(
List<BaselineDefinitionLocation> locations) {
public BaselineDefinition {
locations = locations == null ? List.of() : List.copyOf(locations);
}
public static BaselineDefinition empty() {
return new BaselineDefinition(List.of());
}
}

View File

@ -0,0 +1,28 @@
package p.studio.lsp.messages;
import java.util.Objects;
public record BaselineDefinitionLocation(
String uri,
int startLine,
int startCharacter,
int endLine,
int endCharacter) {
public BaselineDefinitionLocation {
uri = requireText(uri, "uri");
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
throw new IllegalArgumentException("definition range coordinates must not be negative");
}
}
private static String requireText(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}

View File

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

View File

@ -2,6 +2,7 @@ package p.studio.lsp.services;
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.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineSignatureHelp;
@ -17,6 +18,8 @@ public interface LanguageServiceBridge {
BaselineHover hover(LspProjectContext context, String documentUri, String text, int line, int character);
BaselineDefinition definition(LspProjectContext context, String documentUri, String text, int line, int character);
BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character);
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);

View File

@ -5,6 +5,7 @@ import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.messages.*;
import p.studio.compiler.models.*;
import p.studio.compiler.services.FrontendCompletionCandidate;
import p.studio.compiler.services.FrontendDefinitionLocation;
import p.studio.compiler.services.FrontendDocumentRequest;
import p.studio.compiler.services.FrontendEditorialContext;
import p.studio.compiler.services.FrontendHover;
@ -21,6 +22,8 @@ import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.utilities.logs.LogAggregator;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
@ -49,6 +52,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
true,
true,
true,
frontendProvider(context).languageService().isPresent(),
presentation.semanticKeys(),
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
presentation.themes().stream().map(this::mapVisualTheme).toList(),
@ -87,13 +91,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
context.projectRoot(),
normalizeDocumentPath(documentUri),
document.text());
final FrontendEditorialContext editorialContext = new FrontendEditorialContext(
document.editorialContext().syntaxTree(),
document.editorialContext().supplementalDeclarations(),
document.editorialContext().semanticContext());
final List<FrontendCompletionCandidate> candidates = languageService.orElseThrow().completion(
request,
editorialContext,
document.editorialContext(),
offset);
return new BaselineCompletion(
false,
@ -122,19 +122,47 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
context.projectRoot(),
normalizeDocumentPath(documentUri),
document.text());
final FrontendEditorialContext editorialContext = new FrontendEditorialContext(
document.editorialContext().syntaxTree(),
document.editorialContext().supplementalDeclarations(),
document.editorialContext().semanticContext());
return languageService.orElseThrow().hover(
request,
editorialContext,
document.editorialContext(),
offset);
})
.map(symbol -> new BaselineHover(formatHoverMarkdown(symbol)))
.orElseGet(() -> new BaselineHover("No symbol information."));
}
@Override
public BaselineDefinition definition(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
Objects.requireNonNull(context, "context");
final var provider = frontendProvider(context);
final var languageService = provider.languageService();
if (languageService.isEmpty()) {
return BaselineDefinition.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 List<FrontendDefinitionLocation> locations = languageService.orElseThrow().definition(
request,
document.editorialContext(),
offset);
return new BaselineDefinition(mapDefinitionLocations(
locations,
request.documentPath(),
document.text()));
})
.orElseGet(BaselineDefinition::empty);
}
@Override
public BaselineSignatureHelp signatureHelp(
final LspProjectContext context,
@ -155,13 +183,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
context.projectRoot(),
normalizeDocumentPath(documentUri),
document.text());
final FrontendEditorialContext editorialContext = new FrontendEditorialContext(
document.editorialContext().syntaxTree(),
document.editorialContext().supplementalDeclarations(),
document.editorialContext().semanticContext());
return languageService.orElseThrow().signatureHelp(
request,
editorialContext,
document.editorialContext(),
offset);
})
.map(this::mapSignatureHelp)
@ -418,6 +442,46 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
};
}
private List<BaselineDefinitionLocation> mapDefinitionLocations(
final List<FrontendDefinitionLocation> locations,
final Path currentDocumentPath,
final String currentDocumentText) {
if (locations == null || locations.isEmpty()) {
return List.of();
}
final ArrayList<BaselineDefinitionLocation> mapped = new ArrayList<>();
for (final FrontendDefinitionLocation location : locations) {
final String targetText = textForDefinitionTarget(location.path(), currentDocumentPath, currentDocumentText);
if (targetText == null) {
continue;
}
final DocumentPositionMapper mapper = new DocumentPositionMapper(targetText);
final DocumentPosition start = mapper.positionOf(location.startOffset());
final DocumentPosition end = mapper.positionOf(location.endOffset());
mapped.add(new BaselineDefinitionLocation(
location.path().toUri().toString(),
start.line(),
start.character(),
end.line(),
end.character()));
}
return List.copyOf(mapped);
}
private String textForDefinitionTarget(
final Path path,
final Path currentDocumentPath,
final String currentDocumentText) {
if (path.equals(currentDocumentPath)) {
return currentDocumentText == null ? "" : currentDocumentText;
}
try {
return Files.readString(path, StandardCharsets.UTF_8);
} catch (final Exception ignored) {
return null;
}
}
private String formatHoverMarkdown(final FrontendHover symbol) {
final StringBuilder markdown = new StringBuilder();
final List<FrontendSignature> signatures = symbol.signatures();

View File

@ -97,6 +97,20 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
params.getPosition().getCharacter())));
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(
final DefinitionParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
final List<Location> locations = protocolMessageMapper.mapDefinition(languageServiceBridge.definition(
project,
uri,
text,
params.getPosition().getLine(),
params.getPosition().getCharacter()));
return CompletableFuture.completedFuture(Either.forLeft(locations));
}
@Override
public CompletableFuture<SignatureHelp> signatureHelp(final SignatureHelpParams params) {
final String uri = params.getTextDocument().getUri();

View File

@ -36,6 +36,7 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
signatureHelpOptions.setRetriggerCharacters(List.of(","));
capabilities.setSignatureHelpProvider(signatureHelpOptions);
}
capabilities.setDefinitionProvider(description.definitionSupported());
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
semanticTokens.setFull(true);
@ -83,6 +84,20 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return result;
}
@Override
public List<Location> mapDefinition(final BaselineDefinition definition) {
if (definition == null || definition.locations().isEmpty()) {
return List.of();
}
return definition.locations().stream()
.map(location -> new Location(
location.uri(),
new Range(
new Position(location.startLine(), location.startCharacter()),
new Position(location.endLine(), location.endCharacter()))))
.toList();
}
@Override
public CompletionList mapCompletion(final BaselineCompletion completion) {
final CompletionList list = new CompletionList();

View File

@ -6,14 +6,18 @@ import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SemanticTokens;
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.BaselineHover;
import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineSignatureHelp;
import p.studio.lsp.messages.BaselineServerDescription;
import org.eclipse.lsp4j.CompletionList;
import java.util.List;
public interface ProtocolMessageMapper {
InitializeResult mapInitializeResult(BaselineServerDescription description);
@ -23,6 +27,8 @@ public interface ProtocolMessageMapper {
Hover mapHover(BaselineHover hover);
List<Location> mapDefinition(BaselineDefinition definition);
CompletionList mapCompletion(BaselineCompletion completion);
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);

View File

@ -45,6 +45,9 @@ class CompilerLanguageServiceBridgeTest {
assertEquals(0, signatureHelp.activeSignature());
assertEquals(0, signatureHelp.activeParameter());
final var definition = bridge.definition(context, documentUri, "fn main() {}", 0, 0);
assertTrue(definition.locations().isEmpty());
final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}");
assertTrue(semanticTokens.legend().isEmpty());
assertTrue(semanticTokens.tokens().isEmpty());
@ -309,6 +312,91 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(helper.documentation().contains("- returns `42`"));
}
@Test
void definitionUsesEditorialLookupAndPhysicalStdlibFiles() {
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();
vec.blend(1, 2);
Gfx.clear(5);
let n: int = 1;
n;
}
""";
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay);
final var helperUse = mapper.positionOf(overlay.indexOf("helper();"));
final var helperDefinition = bridge.definition(
context,
documentPath.toUri().toString(),
overlay,
helperUse.line(),
helperUse.character());
assertEquals(1, helperDefinition.locations().size());
assertEquals(documentPath.toUri().toString(), helperDefinition.locations().getFirst().uri());
assertTrue(helperDefinition.locations().getFirst().startLine() < helperUse.line());
final var blendUse = mapper.positionOf(overlay.indexOf("blend(1"));
final var blendDefinition = bridge.definition(
context,
documentPath.toUri().toString(),
overlay,
blendUse.line(),
blendUse.character());
assertEquals(1, blendDefinition.locations().size());
assertEquals(documentPath.toUri().toString(), blendDefinition.locations().getFirst().uri());
final var clearUse = mapper.positionOf(overlay.indexOf("clear"));
final var clearHover = bridge.hover(
context,
documentPath.toUri().toString(),
overlay,
clearUse.line(),
clearUse.character());
assertTrue(clearHover.markdown().contains("clear(color:"));
final var clearDefinition = bridge.definition(
context,
documentPath.toUri().toString(),
overlay,
clearUse.line(),
clearUse.character());
assertTrue(
clearDefinition.locations().isEmpty(),
"stdlib is loaded from a virtual path, so definition stays empty until a physical file exists");
final var builtinUse = mapper.positionOf(overlay.indexOf("int = 1"));
final var builtinHover = bridge.hover(
context,
documentPath.toUri().toString(),
overlay,
builtinUse.line(),
builtinUse.character());
assertTrue(builtinHover.markdown().contains("builtin"));
final var builtinDefinition = bridge.definition(
context,
documentPath.toUri().toString(),
overlay,
builtinUse.line(),
builtinUse.character());
assertTrue(builtinDefinition.locations().isEmpty());
}
@Test
void describeServerPublishesFrontendVisualThemes() {
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
@ -319,6 +407,7 @@ class CompilerLanguageServiceBridgeTest {
assertEquals("pbs-default", description.activeVisualThemeId());
assertTrue(description.completionSupported());
assertTrue(description.signatureHelpSupported());
assertTrue(description.definitionSupported());
assertEquals(1, description.semanticHostProjections().size());
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()

View File

@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineCompletionItem;
import p.studio.lsp.messages.BaselineCompletionItemKind;
import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineEditorPalette;
import p.studio.lsp.messages.BaselineHover;
@ -98,6 +99,7 @@ class PrometeuLanguageServerTest {
true,
true,
true,
true,
List.of(),
List.of(new BaselineSemanticHostProjection(
"vscode",
@ -145,6 +147,16 @@ class PrometeuLanguageServerTest {
return new BaselineHover("hover");
}
@Override
public BaselineDefinition definition(
final LspProjectContext project,
final String documentUri,
final String text,
final int line,
final int character) {
return BaselineDefinition.empty();
}
@Override
public BaselineSignatureHelp signatureHelp(
final LspProjectContext project,
@ -191,6 +203,11 @@ class PrometeuLanguageServerTest {
throw new UnsupportedOperationException();
}
@Override
public java.util.List<org.eclipse.lsp4j.Location> mapDefinition(final BaselineDefinition definition) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
throw new UnsupportedOperationException();

View File

@ -3,9 +3,11 @@ package p.studio.lsp.services.protocol;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.DefinitionParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.SignatureHelp;
@ -16,6 +18,8 @@ import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.BaselineCompletion;
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.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineSemanticTokens;
@ -77,6 +81,14 @@ final class PrometeuTextDocumentServiceTest {
assertEquals(1, bridge.lastSignatureHelpLine);
assertEquals(6, bridge.lastSignatureHelpCharacter);
assertTrue(mapper.mappedSignatureHelp.signatures().getFirst().label().contains("demo"));
final var definition = service.definition(new DefinitionParams(
new TextDocumentIdentifier(uri),
new Position(1, 2))).join();
assertSame(mapper.definitionLocations, definition.getLeft());
assertEquals(text, bridge.lastDefinitionText);
assertEquals(1, bridge.lastDefinitionLine);
assertEquals(2, bridge.lastDefinitionCharacter);
}
private static final class RecordingBridge implements LanguageServiceBridge {
@ -84,6 +96,9 @@ final class PrometeuTextDocumentServiceTest {
private int lastCompletionLine;
private int lastCompletionCharacter;
private String lastHoverText;
private String lastDefinitionText;
private int lastDefinitionLine;
private int lastDefinitionCharacter;
private String lastSignatureHelpText;
private int lastSignatureHelpLine;
private int lastSignatureHelpCharacter;
@ -123,6 +138,19 @@ final class PrometeuTextDocumentServiceTest {
return new BaselineHover("hover");
}
@Override
public BaselineDefinition definition(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
lastDefinitionText = text;
lastDefinitionLine = line;
lastDefinitionCharacter = character;
return new BaselineDefinition(List.of(new BaselineDefinitionLocation(documentUri, 0, 0, 0, 6)));
}
@Override
public BaselineSignatureHelp signatureHelp(
final LspProjectContext context,
@ -151,6 +179,7 @@ final class PrometeuTextDocumentServiceTest {
private static final class RecordingMapper implements ProtocolMessageMapper {
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 BaselineSignatureHelp mappedSignatureHelp;
@Override
@ -178,6 +207,11 @@ final class PrometeuTextDocumentServiceTest {
return new Hover(content);
}
@Override
public List<Location> mapDefinition(final BaselineDefinition definition) {
return definitionLocations;
}
@Override
public CompletionList mapCompletion(final BaselineCompletion completion) {
return completionList;

View File

@ -6,6 +6,8 @@ import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.BaselineCompletion;
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.BaselineEditorPalette;
import p.studio.lsp.messages.BaselineSemanticHostProjection;
import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry;
@ -35,6 +37,7 @@ final class Lsp4jProtocolMessageMapperTest {
true,
true,
true,
true,
List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection(
"vscode",
@ -55,6 +58,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());
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
@ -104,4 +108,17 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals("left: int", signatureHelp.getSignatures().getFirst().getParameters().getFirst().getLabel().getLeft());
assertEquals(1, signatureHelp.getActiveParameter());
}
@Test
void definitionMapsPhysicalFileLocations() {
final var mapper = new Lsp4jProtocolMessageMapper();
final var locations = mapper.mapDefinition(new BaselineDefinition(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(2, locations.getFirst().getRange().getStart().getLine());
assertEquals(3, locations.getFirst().getRange().getStart().getCharacter());
assertEquals(9, locations.getFirst().getRange().getEnd().getCharacter());
assertTrue(mapper.mapDefinition(BaselineDefinition.empty()).isEmpty());
}
}