Merge pull request 'implements PLN-0133' (#27) from dev/pbs-lsp-document-symbols-and-outline into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 62.84% (18504/29445) * Branch Coverage: 53.33% (7215/13528) * Lines of Code: 29445 * Cyclomatic Complexity: 11850 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 669
Intrepid/Prometeu/Studio/pipeline/head This commit looks good

Reviewed-on: #27
This commit is contained in:
bquarkz 2026-09-21 09:23:52 +00:00
commit 78d10cf03c
25 changed files with 1183 additions and 66 deletions

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":54,"PLN":133,"LSN":70,"CLSN":1}} {"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":55,"PLN":134,"LSN":71,"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-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-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"}]} {"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"}]}
@ -23,7 +23,7 @@
{"type":"discussion","id":"DSC-0044","status":"open","ticket":"pbs-lsp-code-actions","title":"PBS LSP Code Actions and Quick Fixes","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","code-actions","quick-fix"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-pbs-lsp-code-actions-and-quick-fixes.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0044","status":"open","ticket":"pbs-lsp-code-actions","title":"PBS LSP Code Actions and Quick Fixes","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","code-actions","quick-fix"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-pbs-lsp-code-actions-and-quick-fixes.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"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-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-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-0041","status":"done","ticket":"pbs-lsp-document-symbols-outline","title":"PBS LSP Document Symbols and Outline","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","outline","document-symbols"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0070","file":"discussion/lessons/DSC-0041-pbs-lsp-document-symbols-outline/LSN-0070-pbs-outline-is-an-ast-backed-document-symbol-tree.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
{"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-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-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-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"}]}

View File

@ -0,0 +1,141 @@
---
id: LSN-0070
ticket: pbs-lsp-document-symbols-outline
title: PBS outline is an AST-backed document-symbol tree
created: 2026-09-21
tags: [studio, lsp, vscode, compiler-pbs, editor, outline, document-symbols]
---
# PBS outline is an AST-backed document-symbol tree
## Original Problem
PBS already had compiler-backed completion, hover, signature help, go-to-definition (`LSN-0068`), and find-references (`LSN-0069`). VS Code Outline stayed empty. Long `.pbs` files had to be scanned by hand.
The risk was building outline from semantic success (so the tree vanishes while typing), flattening to `SymbolInformation`, inventing a Document Symbol SPI, or stuffing workspace search and folding into the same payload.
## Consolidated Decision
PBS Document Symbols is the LSP projection of the **named declaration tree of the open `.pbs` file**, with the **recovered AST as backbone**.
Durable locks from `DEC-0054` (AGD-0044 Q1 A, Q2 AST tree, Q3 A, Q4 A, Q5 A, Q6 `.pbs`-only, Q7 existing kinds):
1. Presence, hierarchy, and ranges come from the document AST. Kind and `detail` may use cheap semantic enrichment. Semantics must not drop a named declaration still in the syntax tree, and must not require full analysis.
2. Named `TopDecl` plus syntactic members:
- `fn` → Function, no children;
- `declare struct` → Struct, children: fields, `ctor`s, methods;
- `declare service` → Service, children: methods;
- `declare contract` → Contract, children: signatures as methods;
- `declare host` → Host, children: signatures;
- `declare builtin type` → Builtin type, children: projection fields and signatures;
- `declare enum` → Enum, children: cases;
- `declare error` → Error, children: labels;
- `declare callback` → Callback, no children;
- `declare global` / `const` → Global / Const, no children;
- `implements Contract for Owner` → top-level Contract named `contractName`, `detail` = owner name, children: methods.
Locals, parameters, `this`, imports, and comments stay out. Private fields stay in. Overloads stay siblings. Host and builtin type appear only if the open file declares them.
3. Recovered AST is enough. Unnamed `InvalidDecl` is omitted. Named decls survive later semantic errors. No `PbsAst.File` → empty list. Do not invent nodes from text or token pairs.
4. Keep the capability on aggregated `FrontendLanguageService.documentSymbols` (default empty) plus a hierarchical DTO (`name`, `kind`, optional `detail`, range offsets, selection offsets, `children`). No file path on the DTO. PBS builds the tree in `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. No Document Symbol SPI.
5. Advertise `documentSymbolProvider`. Payload is hierarchical `DocumentSymbol`, not flat `SymbolInformation`. `range` is the declaration span. `selectionRange` is the name span only if the AST already has one; this wave reuses the declaration span. Do not scan tokens for identifier spans. VS Code stays a thin client.
6. First wave is the current `.pbs` only. Out: imported supplementals, `mod.barrel`, locals, parameters, imports, comments, virtual documents, workspace symbols, folding, rename, snapshot cache. Server order is AST declaration order, not alphabetical.
7. Reuse `FrontendSymbolKind` / `PbsEditorialSymbolKind`. LSP projects to the closest host `SymbolKind`. Name is the identifier. `detail` may carry the AST signature (`(a: int) -> int`). Attributes such as `[Init]` do not enter the name.
Outline does not jump to another file. The physical-file destination policy of definition/references is not the outline policy.
Spec 23 §8.3 now also states: document symbols are the named declaration tree of the requested document; recovered syntax is enough; a missing syntax tree is empty; this is not a workspace index, folding ranges, or a foreign-file navigation list.
## Final Implementation
| Layer | What landed |
|---|---|
| PBS editorial | `PbsAstDocumentSymbols` walks `PbsAst.File.topDecls` in source order. `PbsEditorialSupportService.documentSymbols` delegates to that walk. |
| Generic contract | `FrontendLanguageService.documentSymbols` defaults to `List.of()`. `FrontendDocumentSymbol` is hierarchical and pathless. |
| PBS mapping | `PBSFrontendLanguageService` requires `editorialContext.syntaxTree()` to be `PbsAst.File`. Barrels and unknown trees stay empty. Kinds map with `valueOf(kind.name())`. |
| LSP | `documentSymbolProvider`, `textDocument/documentSymbol`, hierarchical `DocumentSymbol`. Nested enum/error children project to `EnumMember`. Compile-only frontends stay empty. |
| VS Code | Unchanged thin client. |
`mod.barrel` is assembled as `BarrelFile`, not `PbsAst.File`, so `astByFile` has no barrel outline. Empty is the honest first-wave result.
Error case labels have no per-label span in the AST. Children reuse the parent `ErrorDecl` span rather than scanning tokens.
## Examples
Outline of a file is the declarations in that file, not imports or locals:
```pbs
import { Log } from @sdk:log;
fn helper() -> int { return 42; }
fn frame() -> void {
let local = 1;
helper(local);
}
```
Outline: `helper`, `frame`. Not `Log`, not `local`.
Struct members and overloads as siblings:
```pbs
declare struct Vec(x: int) {
ctor make(x: int) { return; }
fn blend(dx: int, dy: int) -> int { return dx; }
}
fn helper() -> int { return 42; }
fn helper(value: int) -> int { return value; }
```
Outline: `Vec` (`x`, `make`, `blend`), then two `helper` siblings. Private field `x` is present. `detail` on `blend` is `(dx: int, dy: int) -> int`.
Recovered parse keeps the named decl:
```pbs
declare ;
fn ok() -> int { return 1; }
```
Outline: `ok`. The unnamed `InvalidDecl` is omitted.
`[Init] fn init()` still names the symbol `init`.
`implements TickLike for Point` is a top-level node named `TickLike` with detail `Point`.
## Pitfalls
Do not wait for semantic analysis to succeed before showing outline. The tree must survive broken intermediate edits.
Do not omit private fields because they are not in `mod.barrel`. Outline is for the author of this file.
Do not collapse overloads. AST identity forbids merging declaration sets.
Do not walk `File.imports` or supplemental imported `TopDecl`s. Those symbols are not declared in this document.
Do not implement outline for `mod.barrel` in this contract. Barrel AST is a different root. Empty is correct until a later decision.
Do not scan tokens to invent a tighter `selectionRange`. This wave uses the declaration span.
Do not put a file path on `FrontendDocumentSymbol`. The host already has the document URI. Outline is not go-to-definition.
Do not reuse the physical-file jump rule of `LSN-0068` as an outline destination policy. Clicking outline stays inside the buffer.
Do not overload document symbols with folding or scope guides (`LSN-0034`, `AGD-0053`).
Do not split a Document Symbol SPI “for future languages”. PBS is a real consumer of `FrontendLanguageService` (`LSN-0067`).
Do not treat this lesson as closing workspace symbols, folding, rename, document links, or hierarchy.
## References
- Agenda: `AGD-0044` (Q1 A, Q2 AST tree, Q3 A, Q4 A, Q5 A, Q6 `.pbs`-only, Q7 existing kinds)
- Decision: `DEC-0054`
- Plan: `PLN-0133`
- Spec 23 §8.3 — optional document symbols; recovered AST tree of the requested document
- `LSN-0068` — definition is a jump; outline is not
- `LSN-0069` — references invert definition identity
- `LSN-0034` — do not overload document symbols with structural anchors
- `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-0045` workspace symbols, `AGD-0046` rename, `AGD-0051` snapshots, `AGD-0053` folding, `AGD-0055` document links, `AGD-0056` hierarchy

View File

@ -1,62 +0,0 @@
---
id: AGD-0044
ticket: pbs-lsp-document-symbols-outline
title: PBS LSP Document Symbols and Outline
status: open
created: 2026-07-15
resolved:
decision:
tags: [studio, lsp, vscode, compiler-pbs, editor, outline, document-symbols]
---
## Pain
Domain owner: `studio/lsp`
PBS files do not populate a reliable VS Code outline, forcing users to navigate long source files manually instead of scanning declarations and members structurally.
## Context
The parser and semantic surfaces know top-level declarations and many member declarations. The LSP currently does not announce documentSymbolProvider and does not map PBS declarations to DocumentSymbol or SymbolInformation payloads.
## Open Questions
- [ ] Should the first outline be syntactic, semantic, or hybrid?
- [ ] Which hierarchy should be shown for structs, services, hosts, contracts, builtin types, enums, and methods?
- [ ] How should invalid or partially parsed files contribute to outline during editing?
## Options
### Option A - AST-only outline
- **Approach:** Map parsed declarations and member spans directly to LSP `DocumentSymbol`.
- **Pro:** Works even when semantic analysis is incomplete and gives immediate editor value.
- **Con:** Cannot classify every symbol with semantic precision.
- **Maintainability:** Good if kept as structural outline, not overloaded with semantic behavior.
### Option B - Semantic outline
- **Approach:** Build outline from semantic symbols after analysis.
- **Pro:** More accurate kinds and can hide invalid/unresolved surfaces.
- **Con:** More fragile during active editing and depends on full semantic success.
- **Maintainability:** Good for final precision, but heavier than needed for outline.
## Tradeoffs
Outline should remain available during broken intermediate edits. Semantic precision is useful, but not at the cost of disappearing structure while typing.
## Recommendation
Prefer Option A with semantic enrichment where available. Use AST spans as the stable backbone.
## Discussion
Document symbols are relatively independent and can be implemented before definition/references.
## Resolution
Ainda em aberto.
## Next Step
Decide hierarchy and symbol kinds for PBS declarations and members.

View File

@ -276,6 +276,8 @@ When a frontend exposes definition or navigation, each returned location MUST id
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. 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.
When a frontend exposes document symbols or outline, the result MUST be the named declaration tree of the requested document. Presence, hierarchy, and ranges MUST come from that document's recovered syntax tree. Semantic success MUST NOT be required. Semantic enrichment MUST NOT drop a named declaration that the syntax tree still exposes. A missing syntax tree MUST produce an empty list. Document symbols MUST NOT be a workspace symbol index, folding ranges, or a navigation list of foreign files.
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. 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. `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,6 +6,7 @@ import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.FrontendSemanticToken; import p.studio.compiler.models.FrontendSemanticToken;
import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsEditorialCompletionCandidate; import p.studio.compiler.pbs.semantics.PbsEditorialCompletionCandidate;
import p.studio.compiler.pbs.semantics.PbsEditorialDocumentSymbol;
import p.studio.compiler.pbs.semantics.PbsEditorialLocation; import p.studio.compiler.pbs.semantics.PbsEditorialLocation;
import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol; import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol;
import p.studio.compiler.pbs.semantics.PbsEditorialSignatureHelp; import p.studio.compiler.pbs.semantics.PbsEditorialSignatureHelp;
@ -16,6 +17,7 @@ import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface
import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendCompletionCandidate;
import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDefinitionLocation;
import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendDocumentRequest;
import p.studio.compiler.services.FrontendDocumentSymbol;
import p.studio.compiler.services.FrontendDocumentation; import p.studio.compiler.services.FrontendDocumentation;
import p.studio.compiler.services.FrontendEditorialContext; import p.studio.compiler.services.FrontendEditorialContext;
import p.studio.compiler.services.FrontendHover; import p.studio.compiler.services.FrontendHover;
@ -267,6 +269,18 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return List.copyOf(mapped.values()); return List.copyOf(mapped.values());
} }
@Override
public List<FrontendDocumentSymbol> documentSymbols(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
if (editorialContext == null || !(editorialContext.syntaxTree() instanceof PbsAst.File ast)) {
return List.of();
}
return editorialSupportService.documentSymbols(ast).stream()
.map(this::toFrontendDocumentSymbol)
.toList();
}
@Override @Override
public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) { public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
return semanticTokens(request == null ? "" : request.documentText()); return semanticTokens(request == null ? "" : request.documentText());
@ -314,6 +328,20 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return FrontendSymbolKind.valueOf(kind.name()); return FrontendSymbolKind.valueOf(kind.name());
} }
private FrontendDocumentSymbol toFrontendDocumentSymbol(final PbsEditorialDocumentSymbol symbol) {
final int start = toOffset(symbol.span().getStart());
final int end = toOffset(symbol.span().getEnd());
return new FrontendDocumentSymbol(
symbol.name(),
toFrontendSymbolKind(symbol.kind()),
symbol.detail(),
start,
end,
start,
end,
symbol.children().stream().map(this::toFrontendDocumentSymbol).toList());
}
private List<EditorialDocument> projectDocuments( private List<EditorialDocument> projectDocuments(
final FrontendDocumentRequest request, final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext, final FrontendEditorialContext editorialContext,

View File

@ -0,0 +1,321 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.Span;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
final class PbsAstDocumentSymbols {
private PbsAstDocumentSymbols() {
}
static List<PbsEditorialDocumentSymbol> list(final PbsAst.File ast) {
if (ast == null || ast.topDecls() == null) {
return List.of();
}
final ArrayList<PbsEditorialDocumentSymbol> symbols = new ArrayList<>();
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
symbolFor(topDecl).ifPresent(symbols::add);
}
return List.copyOf(symbols);
}
private static Optional<PbsEditorialDocumentSymbol> symbolFor(final PbsAst.TopDecl topDecl) {
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
return callableSymbol(
functionDecl.name(),
PbsEditorialSymbolKind.FUNCTION,
functionDecl.parameters(),
functionDecl.returnKind(),
functionDecl.returnType(),
functionDecl.resultErrorType(),
functionDecl.span());
}
if (topDecl instanceof PbsAst.StructDecl structDecl) {
return structSymbol(structDecl);
}
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
return namedSymbol(
serviceDecl.name(),
PbsEditorialSymbolKind.SERVICE,
"",
serviceDecl.span(),
methodSymbols(serviceDecl.methods()));
}
if (topDecl instanceof PbsAst.ContractDecl contractDecl) {
return namedSymbol(
contractDecl.name(),
PbsEditorialSymbolKind.CONTRACT,
"",
contractDecl.span(),
signatureSymbols(contractDecl.signatures()));
}
if (topDecl instanceof PbsAst.HostDecl hostDecl) {
return namedSymbol(
hostDecl.name(),
PbsEditorialSymbolKind.HOST,
"",
hostDecl.span(),
signatureSymbols(hostDecl.signatures()));
}
if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
return builtinTypeSymbol(builtinTypeDecl);
}
if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
return enumSymbol(enumDecl);
}
if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
return errorSymbol(errorDecl);
}
if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
return callableSymbol(
callbackDecl.name(),
PbsEditorialSymbolKind.CALLBACK,
callbackDecl.parameters(),
callbackDecl.returnKind(),
callbackDecl.returnType(),
callbackDecl.resultErrorType(),
callbackDecl.span());
}
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
return namedSymbol(
globalDecl.name(),
PbsEditorialSymbolKind.GLOBAL,
formatTypeRef(globalDecl.explicitType()),
globalDecl.span(),
List.of());
}
if (topDecl instanceof PbsAst.ConstDecl constDecl) {
return namedSymbol(
constDecl.name(),
PbsEditorialSymbolKind.CONST,
formatTypeRef(constDecl.explicitType()),
constDecl.span(),
List.of());
}
if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) {
return namedSymbol(
implementsDecl.contractName(),
PbsEditorialSymbolKind.CONTRACT,
implementsDecl.ownerName() == null ? "" : implementsDecl.ownerName(),
implementsDecl.span(),
methodSymbols(implementsDecl.methods()));
}
return Optional.empty();
}
private static Optional<PbsEditorialDocumentSymbol> structSymbol(final PbsAst.StructDecl structDecl) {
final ArrayList<PbsEditorialDocumentSymbol> children = new ArrayList<>();
for (final PbsAst.StructField field : structDecl.fields()) {
namedSymbol(
field.name(),
PbsEditorialSymbolKind.FIELD,
formatTypeRef(field.typeRef()),
field.span(),
List.of()).ifPresent(children::add);
}
for (final PbsAst.CtorDecl ctorDecl : structDecl.ctors()) {
namedSymbol(
ctorDecl.name(),
PbsEditorialSymbolKind.CONSTRUCTOR,
ctorDetail(ctorDecl.parameters()),
ctorDecl.span(),
List.of()).ifPresent(children::add);
}
children.addAll(methodSymbols(structDecl.methods()));
return namedSymbol(
structDecl.name(),
PbsEditorialSymbolKind.STRUCT,
"",
structDecl.span(),
children);
}
private static Optional<PbsEditorialDocumentSymbol> builtinTypeSymbol(final PbsAst.BuiltinTypeDecl builtinTypeDecl) {
final ArrayList<PbsEditorialDocumentSymbol> children = new ArrayList<>();
for (final PbsAst.BuiltinFieldDecl field : builtinTypeDecl.fields()) {
namedSymbol(
field.name(),
PbsEditorialSymbolKind.FIELD,
formatTypeRef(field.typeRef()),
field.span(),
List.of()).ifPresent(children::add);
}
children.addAll(signatureSymbols(builtinTypeDecl.signatures()));
return namedSymbol(
builtinTypeDecl.name(),
PbsEditorialSymbolKind.BUILTIN_TYPE,
"",
builtinTypeDecl.span(),
children);
}
private static Optional<PbsEditorialDocumentSymbol> enumSymbol(final PbsAst.EnumDecl enumDecl) {
final ArrayList<PbsEditorialDocumentSymbol> children = new ArrayList<>();
for (final PbsAst.EnumCase enumCase : enumDecl.cases()) {
final String detail = enumCase.explicitValue() == null ? "" : String.valueOf(enumCase.explicitValue());
namedSymbol(
enumCase.name(),
PbsEditorialSymbolKind.ENUM,
detail,
enumCase.span(),
List.of()).ifPresent(children::add);
}
return namedSymbol(
enumDecl.name(),
PbsEditorialSymbolKind.ENUM,
"",
enumDecl.span(),
children);
}
private static Optional<PbsEditorialDocumentSymbol> errorSymbol(final PbsAst.ErrorDecl errorDecl) {
final ArrayList<PbsEditorialDocumentSymbol> children = new ArrayList<>();
for (final String label : errorDecl.cases()) {
namedSymbol(
label,
PbsEditorialSymbolKind.ERROR,
"",
errorDecl.span(),
List.of()).ifPresent(children::add);
}
return namedSymbol(
errorDecl.name(),
PbsEditorialSymbolKind.ERROR,
"",
errorDecl.span(),
children);
}
private static List<PbsEditorialDocumentSymbol> methodSymbols(final ReadOnlyList<PbsAst.FunctionDecl> methods) {
if (methods == null || methods.isEmpty()) {
return List.of();
}
final ArrayList<PbsEditorialDocumentSymbol> symbols = new ArrayList<>();
for (final PbsAst.FunctionDecl method : methods) {
callableSymbol(
method.name(),
PbsEditorialSymbolKind.METHOD,
method.parameters(),
method.returnKind(),
method.returnType(),
method.resultErrorType(),
method.span()).ifPresent(symbols::add);
}
return symbols;
}
private static List<PbsEditorialDocumentSymbol> signatureSymbols(
final ReadOnlyList<PbsAst.FunctionSignature> signatures) {
if (signatures == null || signatures.isEmpty()) {
return List.of();
}
final ArrayList<PbsEditorialDocumentSymbol> symbols = new ArrayList<>();
for (final PbsAst.FunctionSignature signature : signatures) {
callableSymbol(
signature.name(),
PbsEditorialSymbolKind.METHOD,
signature.parameters(),
signature.returnKind(),
signature.returnType(),
signature.resultErrorType(),
signature.span()).ifPresent(symbols::add);
}
return symbols;
}
private static Optional<PbsEditorialDocumentSymbol> callableSymbol(
final String name,
final PbsEditorialSymbolKind kind,
final ReadOnlyList<PbsAst.Parameter> parameters,
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType,
final PbsAst.TypeRef resultErrorType,
final Span span) {
return namedSymbol(
name,
kind,
callableDetail(parameters, returnKind, returnType, resultErrorType),
span,
List.of());
}
private static Optional<PbsEditorialDocumentSymbol> namedSymbol(
final String name,
final PbsEditorialSymbolKind kind,
final String detail,
final Span span,
final List<PbsEditorialDocumentSymbol> children) {
if (name == null || name.isBlank() || span == null || span.isNone()) {
return Optional.empty();
}
return Optional.of(new PbsEditorialDocumentSymbol(name, kind, detail, span, children));
}
private static String callableDetail(
final ReadOnlyList<PbsAst.Parameter> parameters,
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType,
final PbsAst.TypeRef resultErrorType) {
return "(" + formatParameters(parameters) + ") -> " + formatReturn(returnKind, returnType, resultErrorType);
}
private static String ctorDetail(final ReadOnlyList<PbsAst.Parameter> parameters) {
return "(" + formatParameters(parameters) + ")";
}
private static String formatParameters(final ReadOnlyList<PbsAst.Parameter> parameters) {
if (parameters == null || parameters.isEmpty()) {
return "";
}
final ArrayList<String> parts = new ArrayList<>(parameters.size());
for (final PbsAst.Parameter parameter : parameters) {
parts.add(parameter.name() + ": " + formatTypeRef(parameter.typeRef()));
}
return String.join(", ", parts);
}
private static String formatReturn(
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType,
final PbsAst.TypeRef resultErrorType) {
if (returnKind == null) {
return formatTypeRef(returnType);
}
return switch (returnKind) {
case INFERRED_UNIT, EXPLICIT_UNIT -> "void";
case PLAIN -> formatTypeRef(returnType);
case RESULT -> {
final String errorType = formatTypeRef(resultErrorType);
if (returnType == null || returnType.kind() == PbsAst.TypeRefKind.UNIT) {
yield "result<" + errorType + ">";
}
yield "result<" + errorType + "> " + formatTypeRef(returnType);
}
};
}
private static String formatTypeRef(final PbsAst.TypeRef typeRef) {
if (typeRef == null) {
return "void";
}
return switch (typeRef.kind()) {
case UNIT -> typeRef.name() == null || typeRef.name().isBlank() ? "void" : typeRef.name();
case SELF -> "Self";
case SIMPLE -> typeRef.name() == null ? "void" : typeRef.name();
case OPTIONAL -> "optional " + formatTypeRef(typeRef.inner());
case GROUP -> "(" + formatTypeRef(typeRef.inner()) + ")";
case NAMED_TUPLE -> {
final ArrayList<String> fields = new ArrayList<>(typeRef.fields().size());
for (final var field : typeRef.fields()) {
fields.add(field.label() + ": " + formatTypeRef(field.typeRef()));
}
yield "(" + String.join(", ", fields) + ")";
}
case ERROR -> "unknown";
};
}
}

View File

@ -0,0 +1,22 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.source.Span;
import java.util.List;
import java.util.Objects;
public record PbsEditorialDocumentSymbol(
String name,
PbsEditorialSymbolKind kind,
String detail,
Span span,
List<PbsEditorialDocumentSymbol> children) {
public PbsEditorialDocumentSymbol {
name = Objects.requireNonNull(name, "name").trim();
kind = Objects.requireNonNull(kind, "kind");
detail = detail == null ? "" : detail;
span = Objects.requireNonNull(span, "span");
children = children == null ? List.of() : List.copyOf(children);
}
}

View File

@ -74,6 +74,10 @@ public final class PbsEditorialSupportService {
return context.resolveSymbolHover(token.lexeme(), tokenIndex, scopeState); return context.resolveSymbolHover(token.lexeme(), tokenIndex, scopeState);
} }
public List<PbsEditorialDocumentSymbol> documentSymbols(final PbsAst.File ast) {
return PbsAstDocumentSymbols.list(ast);
}
public List<PbsEditorialLocation> definition( public List<PbsEditorialLocation> definition(
final String sourceText, final String sourceText,
final PbsAst.File ast, final PbsAst.File ast,

View File

@ -434,6 +434,131 @@ final class PbsEditorialSupportServiceTest {
return locations; return locations;
} }
@Test
void shouldBuildAstBackedOutlineHierarchyAndKeepOverloadsAsSiblings() {
final var source = """
import { Log } from @sdk:log;
[Init]
fn init() -> void { return; }
fn helper(a: int) -> int { return a; }
fn helper(a: int, b: int) -> int { return a; }
declare struct Point(x: int, pub y: int) {
ctor make(x: int) { return; }
fn run() -> void { return; }
}
declare contract TickLike { fn tick(dt: int) -> void; }
declare service Physics { fn tick() -> void { return; } }
declare error Io { NotFound; Busy; }
declare enum Mode(Idle = 0, Run = 1);
declare callback OnClick(x: int) -> result<Io> int;
declare global STATE: int = 10;
declare const LIMIT: int = 10;
implements TickLike for Point using s { fn tick(dt: int) -> void { return; } }
fn frame() -> void {
let local = 1;
helper(local);
}
""";
final var ast = parseOrdinary(source);
final var symbols = service.documentSymbols(ast);
assertEquals(List.of(
"init",
"helper",
"helper",
"Point",
"TickLike",
"Physics",
"Io",
"Mode",
"OnClick",
"STATE",
"LIMIT",
"TickLike",
"frame"), names(symbols));
assertTrue(symbols.stream().noneMatch(symbol -> symbol.name().equals("Log") || symbol.name().equals("local")));
assertTrue(symbols.getFirst().name().equals("init"));
assertFalse(symbols.getFirst().name().contains("Init"));
assertEquals("(a: int) -> int", symbols.get(1).detail());
assertEquals("(a: int, b: int) -> int", symbols.get(2).detail());
final var point = requireNamed(symbols, "Point");
assertEquals(PbsEditorialSymbolKind.STRUCT, point.kind());
assertEquals(List.of("x", "y", "make", "run"), names(point.children()));
assertEquals(PbsEditorialSymbolKind.FIELD, point.children().getFirst().kind());
assertEquals("int", point.children().getFirst().detail());
assertEquals(PbsEditorialSymbolKind.CONSTRUCTOR, point.children().get(2).kind());
assertEquals("(x: int)", point.children().get(2).detail());
assertEquals(PbsEditorialSymbolKind.METHOD, point.children().get(3).kind());
final var contract = symbols.get(4);
assertEquals(PbsEditorialSymbolKind.CONTRACT, contract.kind());
assertEquals(List.of("tick"), names(contract.children()));
assertEquals("(dt: int) -> void", contract.children().getFirst().detail());
final var physics = requireNamed(symbols, "Physics");
assertEquals(PbsEditorialSymbolKind.SERVICE, physics.kind());
assertEquals(List.of("tick"), names(physics.children()));
final var error = requireNamed(symbols, "Io");
assertEquals(PbsEditorialSymbolKind.ERROR, error.kind());
assertEquals(List.of("NotFound", "Busy"), names(error.children()));
final var mode = requireNamed(symbols, "Mode");
assertEquals(PbsEditorialSymbolKind.ENUM, mode.kind());
assertEquals(List.of("Idle", "Run"), names(mode.children()));
assertEquals("0", mode.children().getFirst().detail());
final var callback = requireNamed(symbols, "OnClick");
assertEquals(PbsEditorialSymbolKind.CALLBACK, callback.kind());
assertEquals("(x: int) -> result<Io> int", callback.detail());
assertEquals(PbsEditorialSymbolKind.GLOBAL, requireNamed(symbols, "STATE").kind());
assertEquals("int", requireNamed(symbols, "STATE").detail());
assertEquals(PbsEditorialSymbolKind.CONST, requireNamed(symbols, "LIMIT").kind());
final var implementsSymbol = symbols.get(11);
assertEquals("TickLike", implementsSymbol.name());
assertEquals("Point", implementsSymbol.detail());
assertEquals(PbsEditorialSymbolKind.CONTRACT, implementsSymbol.kind());
assertEquals(List.of("tick"), names(implementsSymbol.children()));
}
@Test
void shouldOmitInvalidDeclarationsAndKeepNamedDeclsFromRecoveredAst() {
final var source = """
declare ;
fn ok() -> int { return 1; }
""";
final var ast = parseAllowingErrors(source);
final var symbols = service.documentSymbols(ast);
assertEquals(List.of("ok"), names(symbols));
assertEquals(PbsEditorialSymbolKind.FUNCTION, symbols.getFirst().kind());
}
@Test
void shouldOutlineHostAndBuiltinTypeDeclarationsFromInterfaceModules() {
final var source = """
declare host Gfx {
fn clear(color: int) -> void;
}
declare builtin type Color(
pub r: int
) {
fn pack() -> int;
}
""";
final var ast = parseInterface(source, new FileId(0));
final var symbols = service.documentSymbols(ast);
assertEquals(List.of("Gfx", "Color"), names(symbols));
assertEquals(PbsEditorialSymbolKind.HOST, symbols.getFirst().kind());
assertEquals(List.of("clear"), names(symbols.getFirst().children()));
assertEquals(PbsEditorialSymbolKind.BUILTIN_TYPE, symbols.get(1).kind());
assertEquals(List.of("r", "pack"), names(symbols.get(1).children()));
}
private PbsEditorialResolvedSymbol requireHover( private PbsEditorialResolvedSymbol requireHover(
final String source, final String source,
final PbsAst.File ast, final PbsAst.File ast,
@ -455,6 +580,26 @@ final class PbsEditorialSupportServiceTest {
return ReadOnlyList.wrap(topDecls); return ReadOnlyList.wrap(topDecls);
} }
private List<String> names(final List<PbsEditorialDocumentSymbol> symbols) {
return symbols.stream().map(PbsEditorialDocumentSymbol::name).toList();
}
private PbsEditorialDocumentSymbol requireNamed(
final List<PbsEditorialDocumentSymbol> symbols,
final String name) {
return symbols.stream()
.filter(symbol -> symbol.name().equals(name))
.findFirst()
.orElseThrow(() -> new AssertionError("missing symbol: " + name + " in " + names(symbols)));
}
private PbsAst.File parseAllowingErrors(final String source) {
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
return PbsParser.parse(tokens, fileId, diagnostics, PbsParser.ParseMode.ORDINARY);
}
private PbsAst.File parseOrdinary(final String source) { private PbsAst.File parseOrdinary(final String source) {
return parse(source, new FileId(0), PbsParser.ParseMode.ORDINARY); return parse(source, new FileId(0), PbsParser.ParseMode.ORDINARY);
} }

View File

@ -0,0 +1,44 @@
package p.studio.compiler.services;
import java.util.List;
import java.util.Objects;
public record FrontendDocumentSymbol(
String name,
FrontendSymbolKind kind,
String detail,
int rangeStart,
int rangeEnd,
int selectionStart,
int selectionEnd,
List<FrontendDocumentSymbol> children) {
public FrontendDocumentSymbol {
name = requireText(name, "name");
kind = Objects.requireNonNull(kind, "kind");
detail = detail == null ? "" : detail.trim();
if (rangeStart < 0) {
throw new IllegalArgumentException("rangeStart must not be negative");
}
if (rangeEnd < rangeStart) {
throw new IllegalArgumentException("rangeEnd must not be before rangeStart");
}
if (selectionStart < 0) {
throw new IllegalArgumentException("selectionStart must not be negative");
}
if (selectionEnd < selectionStart) {
throw new IllegalArgumentException("selectionEnd must not be before selectionStart");
}
children = children == null ? List.of() : List.copyOf(children);
}
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

@ -85,4 +85,14 @@ public interface FrontendLanguageService {
final int offset) { final int offset) {
return references(request, offset); return references(request, offset);
} }
default List<FrontendDocumentSymbol> documentSymbols(final FrontendDocumentRequest request) {
return List.of();
}
default List<FrontendDocumentSymbol> documentSymbols(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
return documentSymbols(request);
}
} }

View File

@ -24,6 +24,70 @@ class FrontendLanguageServiceTest {
assertEquals(FrontendSignatureHelp.empty(), service.signatureHelp(request, 0)); assertEquals(FrontendSignatureHelp.empty(), service.signatureHelp(request, 0));
assertTrue(service.definition(request, 0).isEmpty()); assertTrue(service.definition(request, 0).isEmpty());
assertTrue(service.references(request, 0).isEmpty()); assertTrue(service.references(request, 0).isEmpty());
assertTrue(service.documentSymbols(request).isEmpty());
}
@Test
void documentSymbolRejectsBlankNameAndInvertedRangesAndCopiesChildren() {
final var child = new FrontendDocumentSymbol(
"x",
FrontendSymbolKind.FIELD,
"int",
10,
16,
10,
16,
List.of());
final var symbol = new FrontendDocumentSymbol(
" Point ",
FrontendSymbolKind.STRUCT,
" ",
0,
20,
8,
13,
List.of(child));
assertEquals("Point", symbol.name());
assertEquals("", symbol.detail());
assertEquals(1, symbol.children().size());
assertEquals("x", symbol.children().getFirst().name());
assertThrows(UnsupportedOperationException.class, () -> symbol.children().add(child));
assertThrows(IllegalArgumentException.class, () -> new FrontendDocumentSymbol(
" ",
FrontendSymbolKind.FUNCTION,
"",
0,
1,
0,
1,
List.of()));
assertThrows(IllegalArgumentException.class, () -> new FrontendDocumentSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
-1,
1,
0,
1,
List.of()));
assertThrows(IllegalArgumentException.class, () -> new FrontendDocumentSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
5,
4,
0,
1,
List.of()));
assertThrows(IllegalArgumentException.class, () -> new FrontendDocumentSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
0,
1,
3,
2,
List.of()));
} }
@Test @Test

View File

@ -0,0 +1,41 @@
package p.studio.lsp.messages;
import java.util.List;
import java.util.Objects;
public record BaselineDocumentSymbol(
String name,
BaselineDocumentSymbolKind kind,
String detail,
int startLine,
int startCharacter,
int endLine,
int endCharacter,
int selectionStartLine,
int selectionStartCharacter,
int selectionEndLine,
int selectionEndCharacter,
List<BaselineDocumentSymbol> children) {
public BaselineDocumentSymbol {
name = requireText(name, "name");
kind = Objects.requireNonNull(kind, "kind");
detail = detail == null ? "" : detail;
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0
|| selectionStartLine < 0 || selectionStartCharacter < 0
|| selectionEndLine < 0 || selectionEndCharacter < 0) {
throw new IllegalArgumentException("document symbol range coordinates must not be negative");
}
children = children == null ? List.of() : List.copyOf(children);
}
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

@ -0,0 +1,21 @@
package p.studio.lsp.messages;
public enum BaselineDocumentSymbolKind {
KEYWORD,
LOCAL,
PARAMETER,
FIELD,
FUNCTION,
METHOD,
CONSTRUCTOR,
STRUCT,
BUILTIN_TYPE,
SERVICE,
HOST,
CONTRACT,
CALLBACK,
ENUM,
ERROR,
GLOBAL,
CONST
}

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineCompletion; import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineReferences; import p.studio.lsp.messages.BaselineReferences;
import p.studio.lsp.messages.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineSignatureHelp; import p.studio.lsp.messages.BaselineSignatureHelp;
@ -29,6 +30,8 @@ public interface LanguageServiceBridge {
int character, int character,
boolean includeDeclaration); boolean includeDeclaration);
BaselineDocumentSymbols documentSymbols(LspProjectContext context, String documentUri, String text);
BaselineSignatureHelp signatureHelp(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); BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);

View File

@ -7,6 +7,7 @@ import p.studio.compiler.models.*;
import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendCompletionCandidate;
import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDefinitionLocation;
import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendDocumentRequest;
import p.studio.compiler.services.FrontendDocumentSymbol;
import p.studio.compiler.services.FrontendEditorialContext; import p.studio.compiler.services.FrontendEditorialContext;
import p.studio.compiler.services.FrontendHover; import p.studio.compiler.services.FrontendHover;
import p.studio.compiler.services.FrontendProvider; import p.studio.compiler.services.FrontendProvider;
@ -45,6 +46,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
Objects.requireNonNull(context, "context"); Objects.requireNonNull(context, "context");
final var frontend = frontendSpec(context); final var frontend = frontendSpec(context);
final var presentation = frontend.getSemanticPresentation(); final var presentation = frontend.getSemanticPresentation();
final boolean languageServicePresent = frontendProvider(context).languageService().isPresent();
return new BaselineServerDescription( return new BaselineServerDescription(
"Prometeu Studio LSP", "Prometeu Studio LSP",
"0.1.0", "0.1.0",
@ -52,8 +54,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
true, true,
true, true,
true, true,
frontendProvider(context).languageService().isPresent(), languageServicePresent,
frontendProvider(context).languageService().isPresent(), languageServicePresent,
languageServicePresent,
presentation.semanticKeys(), presentation.semanticKeys(),
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
presentation.themes().stream().map(this::mapVisualTheme).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(),
@ -208,6 +211,30 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
.orElseGet(BaselineReferences::empty); .orElseGet(BaselineReferences::empty);
} }
@Override
public BaselineDocumentSymbols documentSymbols(
final LspProjectContext context,
final String documentUri,
final String text) {
Objects.requireNonNull(context, "context");
final var provider = frontendProvider(context);
final var languageService = provider.languageService();
if (languageService.isEmpty()) {
return BaselineDocumentSymbols.empty();
}
return editorialDocument(context, documentUri, text)
.map(document -> {
final FrontendDocumentRequest request = new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
document.text());
return new BaselineDocumentSymbols(mapDocumentSymbols(
languageService.orElseThrow().documentSymbols(request, document.editorialContext()),
document.text()));
})
.orElseGet(BaselineDocumentSymbols::empty);
}
@Override @Override
public BaselineSignatureHelp signatureHelp( public BaselineSignatureHelp signatureHelp(
final LspProjectContext context, final LspProjectContext context,
@ -491,6 +518,46 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
return location.path() + ":" + location.startOffset() + ":" + location.endOffset(); return location.path() + ":" + location.startOffset() + ":" + location.endOffset();
} }
private List<BaselineDocumentSymbol> mapDocumentSymbols(
final List<FrontendDocumentSymbol> symbols,
final String documentText) {
if (symbols == null || symbols.isEmpty()) {
return List.of();
}
final DocumentPositionMapper mapper = new DocumentPositionMapper(documentText == null ? "" : documentText);
final ArrayList<BaselineDocumentSymbol> mapped = new ArrayList<>();
for (final FrontendDocumentSymbol symbol : symbols) {
mapped.add(mapDocumentSymbol(symbol, mapper));
}
return List.copyOf(mapped);
}
private BaselineDocumentSymbol mapDocumentSymbol(
final FrontendDocumentSymbol symbol,
final DocumentPositionMapper mapper) {
final DocumentPosition rangeStart = mapper.positionOf(symbol.rangeStart());
final DocumentPosition rangeEnd = mapper.positionOf(symbol.rangeEnd());
final DocumentPosition selectionStart = mapper.positionOf(symbol.selectionStart());
final DocumentPosition selectionEnd = mapper.positionOf(symbol.selectionEnd());
final ArrayList<BaselineDocumentSymbol> children = new ArrayList<>();
for (final FrontendDocumentSymbol child : symbol.children()) {
children.add(mapDocumentSymbol(child, mapper));
}
return new BaselineDocumentSymbol(
symbol.name(),
BaselineDocumentSymbolKind.valueOf(symbol.kind().name()),
symbol.detail(),
rangeStart.line(),
rangeStart.character(),
rangeEnd.line(),
rangeEnd.character(),
selectionStart.line(),
selectionStart.character(),
selectionEnd.line(),
selectionEnd.character(),
children);
}
private List<BaselineDefinitionLocation> mapDefinitionLocations( private List<BaselineDefinitionLocation> mapDefinitionLocations(
final List<FrontendDefinitionLocation> locations, final List<FrontendDefinitionLocation> locations,
final Path currentDocumentPath, final Path currentDocumentPath,

View File

@ -8,6 +8,7 @@ import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.services.LanguageServiceBridge; import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
import java.util.ArrayList;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -127,6 +128,19 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
includeDeclaration))); includeDeclaration)));
} }
@Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
final DocumentSymbolParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
final List<Either<SymbolInformation, DocumentSymbol>> symbols = new ArrayList<>();
for (final DocumentSymbol symbol : protocolMessageMapper.mapDocumentSymbols(
languageServiceBridge.documentSymbols(project, uri, text))) {
symbols.add(Either.forRight(symbol));
}
return CompletableFuture.completedFuture(List.copyOf(symbols));
}
@Override @Override
public CompletableFuture<SignatureHelp> signatureHelp(final SignatureHelpParams params) { public CompletableFuture<SignatureHelp> signatureHelp(final SignatureHelpParams params) {
final String uri = params.getTextDocument().getUri(); final String uri = params.getTextDocument().getUri();

View File

@ -38,6 +38,7 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
} }
capabilities.setDefinitionProvider(description.definitionSupported()); capabilities.setDefinitionProvider(description.definitionSupported());
capabilities.setReferencesProvider(description.referencesSupported()); capabilities.setReferencesProvider(description.referencesSupported());
capabilities.setDocumentSymbolProvider(description.documentSymbolsSupported());
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
semanticTokens.setFull(true); semanticTokens.setFull(true);
@ -101,6 +102,62 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return mapLocations(references.locations()); return mapLocations(references.locations());
} }
@Override
public List<DocumentSymbol> mapDocumentSymbols(final BaselineDocumentSymbols symbols) {
if (symbols == null || symbols.symbols().isEmpty()) {
return List.of();
}
final ArrayList<DocumentSymbol> mapped = new ArrayList<>();
for (final BaselineDocumentSymbol symbol : symbols.symbols()) {
mapped.add(mapDocumentSymbol(symbol, null));
}
return List.copyOf(mapped);
}
private DocumentSymbol mapDocumentSymbol(
final BaselineDocumentSymbol symbol,
final BaselineDocumentSymbolKind parentKind) {
final DocumentSymbol mapped = new DocumentSymbol();
mapped.setName(symbol.name());
mapped.setDetail(symbol.detail());
mapped.setKind(toLspSymbolKind(symbol.kind(), parentKind));
mapped.setRange(new Range(
new Position(symbol.startLine(), symbol.startCharacter()),
new Position(symbol.endLine(), symbol.endCharacter())));
mapped.setSelectionRange(new Range(
new Position(symbol.selectionStartLine(), symbol.selectionStartCharacter()),
new Position(symbol.selectionEndLine(), symbol.selectionEndCharacter())));
final ArrayList<DocumentSymbol> children = new ArrayList<>();
for (final BaselineDocumentSymbol child : symbol.children()) {
children.add(mapDocumentSymbol(child, symbol.kind()));
}
mapped.setChildren(children);
return mapped;
}
private SymbolKind toLspSymbolKind(
final BaselineDocumentSymbolKind kind,
final BaselineDocumentSymbolKind parentKind) {
if (parentKind == BaselineDocumentSymbolKind.ENUM || parentKind == BaselineDocumentSymbolKind.ERROR) {
if (kind == BaselineDocumentSymbolKind.ENUM || kind == BaselineDocumentSymbolKind.ERROR) {
return SymbolKind.EnumMember;
}
}
return switch (kind) {
case FUNCTION -> SymbolKind.Function;
case METHOD -> SymbolKind.Method;
case CONSTRUCTOR -> SymbolKind.Constructor;
case FIELD -> SymbolKind.Field;
case STRUCT, BUILTIN_TYPE -> SymbolKind.Struct;
case SERVICE, HOST -> SymbolKind.Class;
case CONTRACT, CALLBACK -> SymbolKind.Interface;
case ENUM, ERROR -> SymbolKind.Enum;
case GLOBAL, LOCAL, PARAMETER -> SymbolKind.Variable;
case CONST -> SymbolKind.Constant;
case KEYWORD -> SymbolKind.Key;
};
}
private List<Location> mapLocations(final List<BaselineDefinitionLocation> locations) { private List<Location> mapLocations(final List<BaselineDefinitionLocation> locations) {
return locations.stream() return locations.stream()
.map(location -> new Location( .map(location -> new Location(

View File

@ -6,8 +6,10 @@ import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SemanticTokens; import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Location;
import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineCompletion; import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineReferences; import p.studio.lsp.messages.BaselineReferences;
@ -32,6 +34,8 @@ public interface ProtocolMessageMapper {
List<Location> mapReferences(BaselineReferences references); List<Location> mapReferences(BaselineReferences references);
List<DocumentSymbol> mapDocumentSymbols(BaselineDocumentSymbols symbols);
CompletionList mapCompletion(BaselineCompletion completion); CompletionList mapCompletion(BaselineCompletion completion);
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp); SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);

View File

@ -15,8 +15,10 @@ import p.studio.utilities.structures.ReadOnlySet;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
class CompilerLanguageServiceBridgeTest { class CompilerLanguageServiceBridgeTest {
@ -51,6 +53,9 @@ class CompilerLanguageServiceBridgeTest {
final var references = bridge.references(context, documentUri, "fn main() {}", 0, 0, true); final var references = bridge.references(context, documentUri, "fn main() {}", 0, 0, true);
assertTrue(references.locations().isEmpty()); assertTrue(references.locations().isEmpty());
final var documentSymbols = bridge.documentSymbols(context, documentUri, "fn main() {}");
assertTrue(documentSymbols.symbols().isEmpty());
final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}"); final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}");
assertTrue(semanticTokens.legend().isEmpty()); assertTrue(semanticTokens.legend().isEmpty());
assertTrue(semanticTokens.tokens().isEmpty()); assertTrue(semanticTokens.tokens().isEmpty());
@ -474,6 +479,47 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(builtinReferences.locations().isEmpty()); assertTrue(builtinReferences.locations().isEmpty());
} }
@Test
void documentSymbolsUseAstHierarchyOfTheOpenPbsFile() {
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(x: int) {
ctor make(x: int) { return; }
fn blend(dx: int, dy: int) -> int { return dx; }
}
fn helper() -> int { return 42; }
fn helper(value: int) -> int { return value; }
fn frame() -> void {
let local = 1;
helper(local);
}
""";
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final var symbols = bridge.documentSymbols(
new LspProjectContext("main", "pbs", projectRoot),
documentPath.toUri().toString(),
overlay).symbols();
assertEquals(List.of("Vec", "helper", "helper", "frame"), symbols.stream().map(symbol -> symbol.name()).toList());
assertTrue(symbols.stream().noneMatch(symbol -> symbol.name().equals("Gfx") || symbol.name().equals("local")));
final var vec = symbols.getFirst();
assertEquals(p.studio.lsp.messages.BaselineDocumentSymbolKind.STRUCT, vec.kind());
assertEquals(List.of("x", "make", "blend"), vec.children().stream().map(symbol -> symbol.name()).toList());
assertEquals("(dx: int, dy: int) -> int", vec.children().get(2).detail());
assertEquals("helper", symbols.get(1).name());
assertEquals("helper", symbols.get(2).name());
assertNotEquals(symbols.get(1).detail(), symbols.get(2).detail());
}
@Test @Test
void describeServerPublishesFrontendVisualThemes() { void describeServerPublishesFrontendVisualThemes() {
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
@ -486,6 +532,7 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(description.signatureHelpSupported()); assertTrue(description.signatureHelpSupported());
assertTrue(description.definitionSupported()); assertTrue(description.definitionSupported());
assertTrue(description.referencesSupported()); assertTrue(description.referencesSupported());
assertTrue(description.documentSymbolsSupported());
assertEquals(1, description.semanticHostProjections().size()); assertEquals(1, description.semanticHostProjections().size());
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()

View File

@ -101,6 +101,7 @@ class PrometeuLanguageServerTest {
true, true,
true, true,
true, true,
true,
List.of(), List.of(),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",
@ -169,6 +170,14 @@ class PrometeuLanguageServerTest {
return p.studio.lsp.messages.BaselineReferences.empty(); return p.studio.lsp.messages.BaselineReferences.empty();
} }
@Override
public p.studio.lsp.messages.BaselineDocumentSymbols documentSymbols(
final LspProjectContext project,
final String documentUri,
final String text) {
return p.studio.lsp.messages.BaselineDocumentSymbols.empty();
}
@Override @Override
public BaselineSignatureHelp signatureHelp( public BaselineSignatureHelp signatureHelp(
final LspProjectContext project, final LspProjectContext project,
@ -226,6 +235,12 @@ class PrometeuLanguageServerTest {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public java.util.List<org.eclipse.lsp4j.DocumentSymbol> mapDocumentSymbols(
final p.studio.lsp.messages.BaselineDocumentSymbols symbols) {
throw new UnsupportedOperationException();
}
@Override @Override
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) { public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View File

@ -4,6 +4,8 @@ import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DefinitionParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.ReferenceContext; import org.eclipse.lsp4j.ReferenceContext;
import org.eclipse.lsp4j.ReferenceParams; import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams;
@ -22,6 +24,9 @@ import p.studio.lsp.messages.BaselineCompletionItem;
import p.studio.lsp.messages.BaselineCompletionItemKind; import p.studio.lsp.messages.BaselineCompletionItemKind;
import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineDefinitionLocation; import p.studio.lsp.messages.BaselineDefinitionLocation;
import p.studio.lsp.messages.BaselineDocumentSymbol;
import p.studio.lsp.messages.BaselineDocumentSymbolKind;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineReferences; import p.studio.lsp.messages.BaselineReferences;
import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineHover;
@ -102,6 +107,13 @@ final class PrometeuTextDocumentServiceTest {
assertEquals(1, bridge.lastReferencesLine); assertEquals(1, bridge.lastReferencesLine);
assertEquals(2, bridge.lastReferencesCharacter); assertEquals(2, bridge.lastReferencesCharacter);
assertTrue(bridge.lastIncludeDeclaration); assertTrue(bridge.lastIncludeDeclaration);
final var documentSymbols = service.documentSymbol(new DocumentSymbolParams(
new TextDocumentIdentifier(uri))).join();
assertEquals(1, documentSymbols.size());
assertTrue(documentSymbols.getFirst().isRight());
assertSame(mapper.documentSymbols.getFirst(), documentSymbols.getFirst().getRight());
assertEquals(text, bridge.lastDocumentSymbolsText);
} }
private static final class RecordingBridge implements LanguageServiceBridge { private static final class RecordingBridge implements LanguageServiceBridge {
@ -116,6 +128,7 @@ final class PrometeuTextDocumentServiceTest {
private int lastReferencesLine; private int lastReferencesLine;
private int lastReferencesCharacter; private int lastReferencesCharacter;
private boolean lastIncludeDeclaration; private boolean lastIncludeDeclaration;
private String lastDocumentSymbolsText;
private String lastSignatureHelpText; private String lastSignatureHelpText;
private int lastSignatureHelpLine; private int lastSignatureHelpLine;
private int lastSignatureHelpCharacter; private int lastSignatureHelpCharacter;
@ -183,6 +196,27 @@ final class PrometeuTextDocumentServiceTest {
return new BaselineReferences(List.of(new BaselineDefinitionLocation(documentUri, 0, 0, 0, 6))); return new BaselineReferences(List.of(new BaselineDefinitionLocation(documentUri, 0, 0, 0, 6)));
} }
@Override
public BaselineDocumentSymbols documentSymbols(
final LspProjectContext context,
final String documentUri,
final String text) {
lastDocumentSymbolsText = text;
return new BaselineDocumentSymbols(List.of(new BaselineDocumentSymbol(
"frame",
BaselineDocumentSymbolKind.FUNCTION,
"",
0,
0,
2,
1,
0,
3,
0,
8,
List.of())));
}
@Override @Override
public BaselineSignatureHelp signatureHelp( public BaselineSignatureHelp signatureHelp(
final LspProjectContext context, final LspProjectContext context,
@ -213,6 +247,7 @@ final class PrometeuTextDocumentServiceTest {
private final SignatureHelp signatureHelp = new SignatureHelp(); private final SignatureHelp signatureHelp = new SignatureHelp();
private final List<Location> definitionLocations = List.of(); private final List<Location> definitionLocations = List.of();
private final List<Location> referenceLocations = List.of(); private final List<Location> referenceLocations = List.of();
private final List<DocumentSymbol> documentSymbols = List.of(new DocumentSymbol());
private BaselineSignatureHelp mappedSignatureHelp; private BaselineSignatureHelp mappedSignatureHelp;
@Override @Override
@ -250,6 +285,11 @@ final class PrometeuTextDocumentServiceTest {
return referenceLocations; return referenceLocations;
} }
@Override
public List<DocumentSymbol> mapDocumentSymbols(final BaselineDocumentSymbols symbols) {
return documentSymbols;
}
@Override @Override
public CompletionList mapCompletion(final BaselineCompletion completion) { public CompletionList mapCompletion(final BaselineCompletion completion) {
return completionList; return completionList;

View File

@ -1,6 +1,7 @@
package p.studio.lsp.services.protocol.mapping; package p.studio.lsp.services.protocol.mapping;
import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.BaselineCompletion; import p.studio.lsp.messages.BaselineCompletion;
@ -8,6 +9,9 @@ import p.studio.lsp.messages.BaselineCompletionItem;
import p.studio.lsp.messages.BaselineCompletionItemKind; import p.studio.lsp.messages.BaselineCompletionItemKind;
import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineDefinitionLocation; import p.studio.lsp.messages.BaselineDefinitionLocation;
import p.studio.lsp.messages.BaselineDocumentSymbol;
import p.studio.lsp.messages.BaselineDocumentSymbolKind;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineReferences; import p.studio.lsp.messages.BaselineReferences;
import p.studio.lsp.messages.BaselineEditorPalette; import p.studio.lsp.messages.BaselineEditorPalette;
import p.studio.lsp.messages.BaselineSemanticHostProjection; import p.studio.lsp.messages.BaselineSemanticHostProjection;
@ -40,6 +44,7 @@ final class Lsp4jProtocolMessageMapperTest {
true, true,
true, true,
true, true,
true,
List.of("demo-keyword"), List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",
@ -62,6 +67,7 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals(List.of("(", ","), result.getCapabilities().getSignatureHelpProvider().getTriggerCharacters()); assertEquals(List.of("(", ","), result.getCapabilities().getSignatureHelpProvider().getTriggerCharacters());
assertEquals(Boolean.TRUE, result.getCapabilities().getDefinitionProvider().getLeft()); assertEquals(Boolean.TRUE, result.getCapabilities().getDefinitionProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft()); assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentSymbolProvider().getLeft());
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
@ -135,4 +141,71 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals(3, locations.getFirst().getRange().getStart().getCharacter()); assertEquals(3, locations.getFirst().getRange().getStart().getCharacter());
assertTrue(mapper.mapReferences(BaselineReferences.empty()).isEmpty()); assertTrue(mapper.mapReferences(BaselineReferences.empty()).isEmpty());
} }
@Test
void documentSymbolsMapToHierarchicalDocumentSymbolTrees() {
final var mapper = new Lsp4jProtocolMessageMapper();
final var mapped = mapper.mapDocumentSymbols(new BaselineDocumentSymbols(List.of(
new BaselineDocumentSymbol(
"Mode",
BaselineDocumentSymbolKind.ENUM,
"",
0,
0,
0,
28,
0,
14,
0,
18,
List.of(new BaselineDocumentSymbol(
"Idle",
BaselineDocumentSymbolKind.ENUM,
"0",
0,
19,
0,
23,
0,
19,
0,
23,
List.of()))),
new BaselineDocumentSymbol(
"Point",
BaselineDocumentSymbolKind.STRUCT,
"",
1,
0,
4,
1,
1,
15,
1,
20,
List.of(new BaselineDocumentSymbol(
"x",
BaselineDocumentSymbolKind.FIELD,
"int",
1,
21,
1,
27,
1,
21,
1,
22,
List.of()))))));
assertEquals(2, mapped.size());
assertEquals("Mode", mapped.getFirst().getName());
assertEquals(SymbolKind.Enum, mapped.getFirst().getKind());
assertEquals("Idle", mapped.getFirst().getChildren().getFirst().getName());
assertEquals(SymbolKind.EnumMember, mapped.getFirst().getChildren().getFirst().getKind());
assertEquals("Point", mapped.get(1).getName());
assertEquals(SymbolKind.Struct, mapped.get(1).getKind());
assertEquals("x", mapped.get(1).getChildren().getFirst().getName());
assertEquals(SymbolKind.Field, mapped.get(1).getChildren().getFirst().getKind());
assertTrue(mapper.mapDocumentSymbols(BaselineDocumentSymbols.empty()).isEmpty());
}
} }