Compare commits
18 Commits
dev/pbs-ls
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7011209675 | |||
| 274ce28e8d | |||
| 1fcecdb04d | |||
| e456bb8d7a | |||
| 202c785f94 | |||
| 7629a73e0a | |||
| ce2e3e4cd3 | |||
| 3320f222bb | |||
| 099857b636 | |||
| 20adc196d9 | |||
| ae891262e1 | |||
| 6f45f4f205 | |||
| 65d2d40310 | |||
| fa21a029ae | |||
| 12e2a4edcc | |||
| 78d10cf03c | |||
| 5d3d48e802 | |||
| 331ad752d1 |
@ -1,4 +1,5 @@
|
||||
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":54,"PLN":133,"LSN":70,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":67,"AGD":70,"DEC":59,"PLN":138,"LSN":75,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0066","status":"done","ticket":"pbs-lsp-remaining-editor-surface","title":"PBS LSP Remaining Editor Surface","created_at":"2026-09-22","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0074","file":"discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/LSN-0074-pbs-remaining-editor-surface-is-frontend-owned.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
|
||||
{"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"}]}
|
||||
@ -11,19 +12,19 @@
|
||||
{"type":"discussion","id":"DSC-0056","status":"done","ticket":"multi-frontend-remove-pbs-branches","title":"Generalizar o contrato LSP/editorial para frontends","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0058","file":"discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
|
||||
{"type":"discussion","id":"DSC-0055","status":"done","ticket":"multi-frontend-compiler-vs-language-services","title":"Separar compilacao de servicos editoriais de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","lsp","editor","frontend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0056","file":"discussion/lessons/DSC-0055-multi-frontend-compiler-vs-language-services/LSN-0056-compile-first-frontends-with-optional-editorial-capabilities.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
|
||||
{"type":"discussion","id":"DSC-0054","status":"done","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0055","file":"discussion/lessons/DSC-0054-multi-frontend-provider-contract/LSN-0055-static-frontend-providers-before-plugin-architecture.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
|
||||
{"type":"discussion","id":"DSC-0053","status":"open","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0052","status":"open","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[{"id":"AGD-0055","file":"AGD-0055-pbs-lsp-document-links.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0051","status":"open","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[{"id":"AGD-0054","file":"AGD-0054-pbs-lsp-diagnostics-ux.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0050","status":"open","ticket":"pbs-lsp-folding-and-selection-ranges","title":"PBS LSP Folding and Selection Ranges","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","folding","selection-range"],"agendas":[{"id":"AGD-0053","file":"AGD-0053-pbs-lsp-folding-and-selection-ranges.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0049","status":"open","ticket":"pbs-lsp-completion-depth","title":"PBS LSP Completion Depth","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","completion"],"agendas":[{"id":"AGD-0052","file":"AGD-0052-pbs-lsp-completion-depth.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0048","status":"open","ticket":"pbs-lsp-performance-cache-and-snapshots","title":"PBS LSP Performance Cache and Snapshots","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","performance","caching"],"agendas":[{"id":"AGD-0051","file":"AGD-0051-pbs-lsp-performance-cache-and-snapshots.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0047","status":"open","ticket":"pbs-lsp-semantic-tokens-semantic-classification","title":"PBS LSP Semantic Token Classification","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","semantic-tokens"],"agendas":[{"id":"AGD-0050","file":"AGD-0050-pbs-lsp-semantic-token-classification.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0046","status":"open","ticket":"pbs-lsp-import-assistance","title":"PBS LSP Import Assistance","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","imports","completion"],"agendas":[{"id":"AGD-0049","file":"AGD-0049-pbs-lsp-import-assistance.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0045","status":"open","ticket":"pbs-lsp-formatting","title":"PBS LSP Formatting","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","formatting"],"agendas":[{"id":"AGD-0048","file":"AGD-0048-pbs-lsp-formatting.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-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-0053","status":"done","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0052","status":"done","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0051","status":"done","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0050","status":"done","ticket":"pbs-lsp-folding-and-selection-ranges","title":"PBS LSP Folding and Selection Ranges","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","folding","selection-range"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0049","status":"done","ticket":"pbs-lsp-completion-depth","title":"PBS LSP Completion Depth","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","completion"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0048","status":"done","ticket":"pbs-lsp-performance-cache-and-snapshots","title":"PBS LSP Performance Cache and Snapshots","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","performance","caching"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0047","status":"done","ticket":"pbs-lsp-semantic-tokens-semantic-classification","title":"PBS LSP Semantic Token Classification","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","semantic-tokens"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0046","status":"done","ticket":"pbs-lsp-import-assistance","title":"PBS LSP Import Assistance","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","imports","completion"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0045","status":"done","ticket":"pbs-lsp-formatting","title":"PBS LSP Formatting","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","formatting"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0044","status":"done","ticket":"pbs-lsp-code-actions","title":"PBS LSP Code Actions and Quick Fixes","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","code-actions","quick-fix"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0073","file":"discussion/lessons/DSC-0044-pbs-lsp-code-actions/LSN-0073-pbs-quick-fixes-are-frontend-repairs-transported-by-the-lsp.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
|
||||
{"type":"discussion","id":"DSC-0043","status":"done","ticket":"pbs-lsp-rename-symbol","title":"PBS LSP Rename Symbol","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","rename"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0072","file":"discussion/lessons/DSC-0043-pbs-lsp-rename-symbol/LSN-0072-pbs-rename-is-a-validated-physical-workspace-edit.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
|
||||
{"type":"discussion","id":"DSC-0042","status":"done","ticket":"pbs-lsp-workspace-symbols","title":"PBS LSP Workspace Symbols","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","workspace-symbols"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0071","file":"discussion/lessons/DSC-0042-pbs-lsp-workspace-symbols/LSN-0071-pbs-workspace-symbols-are-a-physical-named-declaration-search.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
|
||||
{"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-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"}]}
|
||||
|
||||
@ -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
|
||||
@ -0,0 +1,149 @@
|
||||
---
|
||||
id: LSN-0071
|
||||
ticket: pbs-lsp-workspace-symbols
|
||||
title: PBS workspace symbols are a physical named-declaration search
|
||||
created: 2026-09-21
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, workspace-symbols]
|
||||
---
|
||||
|
||||
# PBS workspace symbols are a physical named-declaration search
|
||||
|
||||
## Original Problem
|
||||
|
||||
PBS already had compiler-backed completion, hover, signature help, go-to-definition (`LSN-0068`), find-references (`LSN-0069`), and an AST-backed outline of the open `.pbs` file (`LSN-0070`). VS Code workspace search (`Ctrl+T` / `workspace/symbol`) stayed empty.
|
||||
|
||||
The risk was treating workspace symbols as a second outline, inventing a `SymbolId` index, waiting for snapshot cache (`AGD-0051`), or returning stdlib/SDK hits whose declarations are still virtual paths (`/virtual/stdlib`). Workspace symbols is a jump. Outline is not.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
PBS Workspace Symbols is the LSP projection of a **flat named-declaration search** over **regular filesystem files** of the current analysis, flattening the **same outline set** and using the **same physical-file destination policy** as definition.
|
||||
|
||||
Durable locks from `DEC-0055` (AGD-0045 Q1 A, Q2 A, Q3 A, Q4 A, Q5 A, Q6 first-wave exclusions):
|
||||
|
||||
1. Emit only declarations whose `fileTable` handle is a regular filesystem file. Virtual stdlib/SDK, including `/virtual/stdlib`, is omitted. Do not invent virtual documents, `untitled:` buffers, or synthetic URIs.
|
||||
2. `workspace/symbol` does not require an open text document. The server obtains the project analysis surface (`astByFile` / `projectSurface`) from `analyze()`. Missing surface → empty list.
|
||||
3. The set is the flatten, file by file, of the named-declaration tree locked by outline (`LSN-0070`):
|
||||
- `fn` → Function;
|
||||
- `declare struct` → Struct plus fields, `ctor`s, methods;
|
||||
- `declare service` → Service plus methods;
|
||||
- `declare contract` → Contract plus signatures;
|
||||
- `declare host` → Host plus signatures;
|
||||
- `declare builtin type` → Builtin type plus projection fields and signatures;
|
||||
- `declare enum` → Enum plus cases;
|
||||
- `declare error` → Error plus labels;
|
||||
- `declare callback` → Callback;
|
||||
- `declare global` / `const` → Global / Const;
|
||||
- `implements Contract for Owner` → `contractName`, kind CONTRACT, `detail` = owner, plus methods.
|
||||
Locals, parameters, `this`, imports, and comments stay out. Private fields stay in. Overloads stay sibling hits. Unnamed `InvalidDecl` is omitted. Named decls survive later semantic errors.
|
||||
4. First wave is `.pbs` whose recovered root is `PbsAst.File`. `mod.barrel` (`BarrelFile`) stays empty. Presence, name, kind, `detail`, and spans come from recovered AST. Do not invent nodes from text search.
|
||||
5. Query is a case-insensitive substring of `name` only. Blank query returns the full first-wave corpus. Do not match `detail`, path, `containerName`, or qualified `Vec.blend`. No fuzzy ranking, no `top N`. After the filter, order is stable by name, then path, then start offset.
|
||||
6. `containerName` is the parent name for members and empty for top-level declarations. `detail` may repeat the outline AST signature. Attributes such as `[Init]` do not enter the name.
|
||||
7. Keep the capability on aggregated `FrontendLanguageService`. Add `workspaceSymbols` (default empty) plus a flat DTO with `name`, `kind`, optional `detail`, `containerName`, absolute path, and offsets. Do not reuse pathless `FrontendDocumentSymbol`. PBS flattens in `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. No Workspace Symbol SPI, `SymbolId`, `RefIndex`, or snapshot cache.
|
||||
8. Advertise `workspaceSymbolProvider` when the frontend exposes a language service. Payload is flat `SymbolInformation`. Do not emit hierarchical `DocumentSymbol` on this request. Compile-only frontends stay empty. VS Code stays a thin client.
|
||||
|
||||
Spec 23 §8.3 now also states: workspace symbols are a flat search of named declarations in compiler-known physical source files of the current analysis; recovered syntax is enough; a missing analysis surface is empty; the request must not require an open document; a declaration without a physical file is omitted; this is not outline, folding, a persistent index, or navigation to virtual locations.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
| Layer | What landed |
|
||||
|---|---|
|
||||
| PBS editorial | `PbsAstWorkspaceSymbols` flattens `PbsAstDocumentSymbols` and filters by name. `PbsEditorialSupportService.workspaceSymbols(ast, query)` delegates to that flatten. |
|
||||
| Generic contract | `FrontendWorkspaceRequest` (`projectRoot`, `query`). `FrontendWorkspaceSymbol` carries path. `FrontendLanguageService.workspaceSymbols` defaults to `List.of()`. `prepareEditorialContext(FrontendPhaseContext)` defaults to empty so a workspace request can obtain project surface without a `FileId`. |
|
||||
| PBS mapping | Project-level `prepareEditorialContext` attaches `fileTable` + `astByFile`. Regular `PbsAst.File` entries are flattened; non-regular handles are skipped. Results sort by name, path, offset. |
|
||||
| LSP | `workspaceSymbolProvider`, `workspace/symbol` on `PrometeuWorkspaceService` (no document URI). `CompilerLanguageServiceBridge` analyzes the project with an empty overlay map. Mapper emits flat `SymbolInformation`. Nested enum/error hits with a container project to `EnumMember`. Compile-only frontends stay empty. |
|
||||
| VS Code | Unchanged thin client. |
|
||||
|
||||
`workspace/symbol` in this wave reads the project on disk. Unsaved editor overlays are not shared into the search. Empty is honest until a later overlay/snapshot decision.
|
||||
|
||||
`mod.barrel` is still `BarrelFile`, not `PbsAst.File`. Empty barrel hits are the honest first-wave result.
|
||||
|
||||
Error case labels still have no per-label span. Flattened children reuse the parent `ErrorDecl` span, same as outline.
|
||||
|
||||
## Examples
|
||||
|
||||
Workspace search of a project file is the flatten of that file's outline, not imports or locals:
|
||||
|
||||
```pbs
|
||||
import { Log } from @sdk:log;
|
||||
|
||||
fn helper() -> int { return 42; }
|
||||
|
||||
fn frame() -> void {
|
||||
let local = 1;
|
||||
helper(local);
|
||||
}
|
||||
```
|
||||
|
||||
Query `frame` → `frame`. Query `helper` → `helper`. Query empty → `helper`, `frame` (plus other physical decls), sorted. Not `Log`, not `local`.
|
||||
|
||||
Members keep a container; overloads stay 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; }
|
||||
```
|
||||
|
||||
Query `blend` → method `blend` with `containerName` `Vec`. Query `helper` → two hits. Query `Vec.blend` → empty (this wave matches `name` only).
|
||||
|
||||
Stdlib remains hover-only while the handle is virtual:
|
||||
|
||||
```pbs
|
||||
import { Gfx } from @sdk:gfx;
|
||||
Gfx.clear(0);
|
||||
```
|
||||
|
||||
Query `Gfx` is empty. F12 on `Gfx.clear` is already empty (`LSN-0068`). Workspace search does not reopen that jump.
|
||||
|
||||
Recovered parse keeps the named decl:
|
||||
|
||||
```pbs
|
||||
declare ;
|
||||
fn ok() -> int { return 1; }
|
||||
```
|
||||
|
||||
Query empty → `ok`. The unnamed `InvalidDecl` is omitted.
|
||||
|
||||
`[Init] fn init()` still names the symbol `init`.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
Do not implement workspace symbols as workspace text search in `lsp-v1` or in the VS Code extension. It will lie about overloads, members, and imports.
|
||||
|
||||
Do not reuse `FrontendDocumentSymbol`. Outline is pathless because the host already has the document URI. Workspace symbols is a jump and must carry a path.
|
||||
|
||||
Do not require an open editor buffer to answer `workspace/symbol`. Analyze the project and walk `astByFile`.
|
||||
|
||||
Do not include `/virtual/stdlib` so Ctrl+T on `Gfx` “does something”. Empty is the honest result until those sources are regular files.
|
||||
|
||||
Do not wait for snapshot cache. References already walks the current analysis request-locally. Snapshots (`AGD-0051`) can make the walk cheaper without changing this contract.
|
||||
|
||||
Do not add a `SymbolId` index “for rename later”. Outline already named the declarations; definition already named the destination.
|
||||
|
||||
Do not match `containerName`, path, or `Vec.blend` in this wave. The query is a substring of `name`.
|
||||
|
||||
Do not emit hierarchical `DocumentSymbol` on `workspace/symbol`. That payload is outline.
|
||||
|
||||
Do not treat unsaved overlay absence as a missing feature of this contract. First-wave analysis is the project on disk.
|
||||
|
||||
Do not split a Workspace Symbol SPI “for future languages”. PBS is a real consumer of `FrontendLanguageService` (`LSN-0067`).
|
||||
|
||||
Do not treat this lesson as closing rename, snapshots, folding, document links, or hierarchy.
|
||||
|
||||
## References
|
||||
|
||||
- Agenda: `AGD-0045` (Q1 A, Q2 A, Q3 A, Q4 A, Q5 A, Q6 first-wave exclusions)
|
||||
- Decision: `DEC-0055`
|
||||
- Plan: `PLN-0134`
|
||||
- Spec 23 §8.3 — optional workspace symbols; physical-file-or-omit; no open document required
|
||||
- `LSN-0070` — outline set this feature flattens
|
||||
- `LSN-0068` — physical-file destination this feature reuses
|
||||
- `LSN-0069` — project `astByFile` walk already exists for references
|
||||
- `LSN-0058` — generic `FrontendLanguageService` for LSP
|
||||
- `LSN-0047` — protocol stays in `lsp-v1`; compiler owns semantics
|
||||
- `LSN-0067` — no SPI without a real in-repo consumer
|
||||
- Still open: `AGD-0046` rename, `AGD-0051` snapshots, `AGD-0053` folding, `AGD-0055` document links, `AGD-0056` hierarchy
|
||||
@ -0,0 +1,128 @@
|
||||
---
|
||||
id: LSN-0072
|
||||
ticket: pbs-lsp-rename-symbol
|
||||
title: PBS rename is a validated physical workspace edit
|
||||
created: 2026-09-22
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, rename]
|
||||
---
|
||||
|
||||
# PBS rename is a validated physical workspace edit
|
||||
|
||||
## Original Problem
|
||||
|
||||
PBS already had compiler-backed completion, hover, signature help, go-to-definition (`LSN-0068`), find-references (`LSN-0069`), outline (`LSN-0070`), and workspace symbols (`LSN-0071`). Users still could not rename a symbol through the editor. Manual text edits miss uses or rewrite unrelated overloads, comments, strings, and same-name declarations.
|
||||
|
||||
The risk was a file-local rename, a lexeme replace, a new `SymbolId` / `RefIndex`, snapshot cache (`AGD-0051`) as a prerequisite, virtual stdlib documents, or a successful empty `WorkspaceEdit` that pretends the symbol changed.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
PBS Rename Symbol is the LSP projection of a **validated `WorkspaceEdit`** over **physical identifier spans** of the **same identity definition already returns**.
|
||||
|
||||
Durable locks from `DEC-0056` (AGD-0046 Q1–Q6 = A):
|
||||
|
||||
1. Identity is the physical declaration-location set from definition. Do not text-search. Do not add `SymbolId` or a persistent `RefIndex`.
|
||||
2. Sites are identifier tokens whose definition intersects that identity, plus identifier-sized declaration name spans. Overloads of the same name in the same owner rewrite together. Members rewrite only that member. Comments, strings, and homonyms of another identity stay.
|
||||
3. Walk regular `fileTable` files of the current `analyze()` surface. Omit `/virtual/stdlib` and other non-regular handles. Every edit location must be a compiler-known physical source file.
|
||||
4. Refuse when physical identity is empty: builtins, virtual stdlib/SDK, unresolved names, keywords, comments, strings, `this`, `Self`. Refuse `mod.barrel`. Refuse `declare error` case labels that have no per-label span.
|
||||
5. Exception: a local or parameter name span is renameable even when definition at that offset is empty. Identity becomes that name span; uses are identifiers whose definition intersects it.
|
||||
6. Refuse when the cursor lexeme is an import alias (`item.alias()` distinct from `item.name()`), including uses of that alias. Do not follow alias definition and rewrite the export. Export rename happens only when the cursor writes the original name. Export rewrite must not change `as Alias` tokens or uses that spell a differing alias.
|
||||
7. `prepareRename` validates the cursor and returns the identifier range, or null. `rename` also validates the new name: PBS identifier, not a keyword, no collision in the same binding space (top-level `fn`/`service`/`global`/`const`, local/param in the same scope, field in the same struct, method in the same owner, case in the same enum), ignoring rewritten sites and the overload set. Failure is an LSP error. Never return a successful empty `WorkspaceEdit`. The server does not write files and does not rename paths.
|
||||
8. Keep the capability on aggregated `FrontendLanguageService`. `prepareRename` defaults empty; `rename` defaults unsupported. PBS maps in `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. No Rename SPI. Advertise `renameProvider` with `prepareProvider`. Compile-only stays unsupported. VS Code stays a thin client.
|
||||
|
||||
Spec 23 §8.3 now also states: rename is a validated workspace edit of identifier spans in compiler-known physical source files; identity is definition; local/param name spans remain renameable; alias, virtual, and error-label cursors refuse; invalid names and collisions are protocol errors, not empty success; the server does not write files; `prepareProvider` is advertised when rename is exposed.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
| Layer | What landed |
|
||||
|---|---|
|
||||
| PBS editorial | `PbsEditorialSupportService.planRename` builds identity from definition or local/param name span, refuses aliases and error labels, walks `IDENTIFIER` tokens whose definition intersects identity and whose lexeme is the original name, then validates identifier/keyword/collision. |
|
||||
| Generic contract | `FrontendLanguageService.prepareRename` defaults empty. `rename` defaults `FrontendRenameResult.unsupported()`. `FrontendRenameEdit` is path + offsets + `newText`. Applied results reject an empty edit list. |
|
||||
| PBS mapping | `PBSFrontendLanguageService` reuses `projectDocuments` and `toFrontendLocation`. Empty physical identity is refuse, even if editorial sites exist on virtual handles. |
|
||||
| LSP | `renameSupported`, `prepareRename`, `rename`. Mapper advertises `RenameOptions.prepareProvider`. Refused rename is `ResponseErrorException`, not an empty `WorkspaceEdit`. Compile-only does not advertise. |
|
||||
| VS Code | Unchanged thin client. |
|
||||
|
||||
Stdlib and SDK modules remain virtual (`/virtual/stdlib/...`). F2 on `Gfx.clear` is refused, matching empty F12 / Shift+F12. Hover is unchanged.
|
||||
|
||||
`let total` at the binding itself still has empty definition. Rename uses the name span as identity so F2 on the binding works.
|
||||
|
||||
## Examples
|
||||
|
||||
Same-file function, including overloads and the declaring identifier. Comment and string stay:
|
||||
|
||||
```pbs
|
||||
fn helper() -> int { return 1; }
|
||||
fn helper(value: int) -> int { return value; }
|
||||
|
||||
fn frame() -> void {
|
||||
helper();
|
||||
helper(2);
|
||||
// helper
|
||||
let text = "helper";
|
||||
}
|
||||
```
|
||||
|
||||
F2 on `helper` → `aid` rewrites both declarations and both calls.
|
||||
|
||||
Local name span:
|
||||
|
||||
```pbs
|
||||
fn frame() -> void {
|
||||
let total = 1;
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
F2 on `let total` or on the use rewrites both identifier sites.
|
||||
|
||||
Alias is refused. Export rename keeps the local alias:
|
||||
|
||||
```pbs
|
||||
import { Log as Logger } from @sdk:log;
|
||||
Logger.info("a");
|
||||
```
|
||||
|
||||
F2 on `Logger` or `Logger.info` is refused. F2 on a physical `Log` declaration rewrites the original name and `import { Log as Logger }`, and leaves `Logger` / `Logger.info`.
|
||||
|
||||
Builtins and virtual stdlib stay hover-only:
|
||||
|
||||
```pbs
|
||||
let n: int = 1;
|
||||
Gfx.clear(5);
|
||||
```
|
||||
|
||||
Invalid new names (`fn`, `1ab`) and collisions (`helper` → existing `frame`) fail with an LSP error.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
Do not implement rename as workspace text search in `lsp-v1` or in the VS Code extension. It will rewrite comments, strings, overloads of another identity, and import aliases.
|
||||
|
||||
Do not follow definition through an import alias. `Logger` currently resolves to `Log`'s span; a blind inverse-of-definition rename would rewrite the export.
|
||||
|
||||
Do not return a successful empty `WorkspaceEdit` for a refused cursor. The editor will treat that as “renamed”.
|
||||
|
||||
Do not skip the local/param name-span exception. F2 on `let total` is the common case, and definition there is empty.
|
||||
|
||||
Do not rewrite `declare error` case labels by using the parent `ErrorDecl` span. Those labels have no per-label span.
|
||||
|
||||
Do not wait for snapshot cache. Rename reuses the request-local walk already used by references.
|
||||
|
||||
Do not treat empty/refused stdlib rename as a missing feature. Virtual paths are not regular files.
|
||||
|
||||
Do not split a Rename SPI “for future languages”. PBS is a real consumer of `FrontendLanguageService` (`LSN-0067`).
|
||||
|
||||
Do not treat this lesson as closing snapshots, folding, document links, hierarchy, code actions, formatting, file rename, or local-alias rename.
|
||||
|
||||
## References
|
||||
|
||||
- Agenda: `AGD-0046` (Q1–Q6 = A)
|
||||
- Decision: `DEC-0056`
|
||||
- Plan: `PLN-0135`
|
||||
- Spec 23 §8.3 — optional rename; physical-file-or-empty; alias refuse; error instead of empty success; `prepareProvider`
|
||||
- Spec: `docs/specs/compiler-languages/pbs/3. Core Syntax Specification.md` §4.3–4.4
|
||||
- `LSN-0068` — definition identity this feature writes
|
||||
- `LSN-0069` — physical inverse that supplies usage sites
|
||||
- `LSN-0051` — wave 1 deferred rename
|
||||
- `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-0051` snapshots, `AGD-0053` folding, `AGD-0055` document links, `AGD-0056` hierarchy
|
||||
@ -0,0 +1,130 @@
|
||||
---
|
||||
id: LSN-0073
|
||||
ticket: pbs-lsp-code-actions
|
||||
title: PBS quick fixes are frontend repairs transported by the LSP
|
||||
created: 2026-09-22
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, code-actions, quick-fix]
|
||||
---
|
||||
|
||||
# PBS quick fixes are frontend repairs transported by the LSP
|
||||
|
||||
## Original Problem
|
||||
|
||||
PBS diagnostics already reach the editor, but the server did not advertise `codeActionProvider`. A user could see a duplicate attribute or a bad `InitAllowed` and still had to repair it by hand.
|
||||
|
||||
The tempting shortcut was a code-to-edit table in the LSP. That shortcut does not work. Compiler `Diagnostic` values carry a stable code, phase, template, placeholders, span, and related locations, but the reserved-attribute reports use the short constructor: empty placeholders, and the English sentence is the only fine discriminator. `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` covers Doc without `markdown`, a wrong payload, empty markdown, an unknown argument, `InitAllowed` with arguments, an out-of-range `AssetLowering`, and a bad `Host` shape. A switch on the code cannot pick a safe edit without reading the message. Diagnostic wording is rendering, not identity. Code actions are an optional editorial capability: the frontend owns the meaning, and the LSP transports it.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
PBS quick fix is one `WorkspaceEdit` tied to one diagnostic of the requested physical document. The frontend produces the repair from the syntax tree. The LSP does not invent it.
|
||||
|
||||
Durable locks from `DEC-0057` (AGD-0047: scope A, producer B, residence B, first wave C):
|
||||
|
||||
1. This wave is quick fix only. No organize-imports, source action, refactor, function or struct stub, code lens, or server command.
|
||||
2. The server does not write files. The client applies the edit.
|
||||
3. Each repair names the stable diagnostic code, the primary span of that diagnostic, and exactly one local text edit. The LSP must not keep a code-to-edit table and must not read the diagnostic message to build the edit.
|
||||
4. The repair is computed on `textDocument/codeAction` from the current document analysis. It is not a required field of compiler `Diagnostic`, and it is not published inside `publishDiagnostics`, including `data`.
|
||||
5. `publishDiagnostics` carries the stable compiler code on `Diagnostic.code` so the client can echo it. Source, severity, message, and related information stay as they were. Those presentation questions remain with `AGD-0054`.
|
||||
6. A repair is returned only when the request context contains a diagnostic with the same code and the same primary range. An empty diagnostic list returns an empty action list. An `only` filter that does not include quick fix returns an empty action list.
|
||||
7. Each item is a QuickFix `CodeAction`, not a `Command`. The edit touches one file, the requested physical document, and contains one `TextEdit`. There is no lazy resolve.
|
||||
8. A path that is not a compiler-known regular file, including `/virtual/stdlib`, gets no edit.
|
||||
9. If the frontend does not expose the capability, the server does not advertise `codeActionProvider`. A request that arrives anyway returns an empty list, not a protocol error. Absence of a fix is not a refused cursor.
|
||||
10. PBS exposes the capability. Another frontend does not gain it merely by having a language service.
|
||||
11. The first wave is two shapes, not three diagnostic codes.
|
||||
- For every extra reserved attribute the declaration validator already reports as `E_SEM_DUPLICATE_RESERVED_ATTRIBUTE`, delete exactly that occurrence's span. Do not delete the first occurrence, the declaration, or neighboring whitespace.
|
||||
- For an `InitAllowed` attribute with a non-empty argument list on a host signature, the shape the validator reports as `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` when the file is an interface module, replace that attribute span with `[InitAllowed]`. Do not emit that repair for any other attribute that shares the code. `[InitAllowed()]` is not diagnosed today and gets no repair.
|
||||
12. Nothing else gets a quick fix in this wave. In particular: inserting `[Doc(markdown = """""")]` or any other Doc rewrite, inserting an empty or incomplete `[Host]` or `[BuiltinType]`, `W_SEM_IGNORED_VALUE`, unresolved imports, an invalid reserved-attribute target, parse errors, unresolved overloads, and method stubs.
|
||||
|
||||
`AGD-0048` still owns formatting. `AGD-0049` still owns import assistance and organize-imports. `AGD-0054` still owns range, related locations, and diagnostic presentation. The PBS diagnostics spec does not gain a mandatory remediation field.
|
||||
|
||||
Spec 23 §8.3 now states the shared contract: code actions are quick fixes only; the frontend produces each repair from the document syntax; tooling transports it and does not parse the message or switch on codes; the published diagnostic carries the stable code; a repair is returned only for a matching context diagnostic; the edit is one physical file; the server does not write files, return a command, or require resolve; a missing capability is an empty list, not an error; virtual documents get no edit.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
| Layer | What landed |
|
||||
|---|---|
|
||||
| PBS repairs | `PbsQuickFixCollector` walks the same attribute surfaces as `PbsDeclarationSemanticsValidator`, including the interface-module gate. Duplicate reserved attributes become an empty replacement of the extra span. The first `InitAllowed` with arguments on an interface host signature becomes `[InitAllowed]`. |
|
||||
| Source kind | `PbsSemanticReadSurface.sourceKindByFile` records the kind used to parse each file. A missing kind means project rules, so a project `declare host` does not pretend the validator emitted the interface-only `InitAllowed` diagnostic. |
|
||||
| Generic contract | `FrontendLanguageService.codeActionsSupported` defaults false. `codeActions` defaults empty. `FrontendCodeAction` is title, diagnostic code, path, offsets, and `newText`. Empty `newText` is a deletion and is allowed. |
|
||||
| PBS mapping | `PBSFrontendLanguageService` exposes the capability and returns repairs only for the requested regular file. `/virtual/stdlib` fails `Files.isRegularFile` and produces nothing. |
|
||||
| LSP | `BaselineDocumentIssue.code` is copied onto `Diagnostic.code`. `data` stays unset. The bridge drops repairs whose code and range do not match a context diagnostic. `PrometeuTextDocumentService.codeAction` honors the quick-fix `only` filter and never fails the request for “no fix”. |
|
||||
| Protocol | `codeActionProvider` is `CodeActionOptions` with kind `quickfix` and `resolveProvider` false, and only when the frontend reports support. Mapped actions are `CodeAction` values with a `WorkspaceEdit`. They are not `Command`s. |
|
||||
|
||||
Titles are `Remove duplicate reserved attribute` and `Remove InitAllowed arguments`.
|
||||
|
||||
## Examples
|
||||
|
||||
Duplicate `Doc` on a project file. The first copy stays. Each later copy is deleted at its own span:
|
||||
|
||||
```pbs
|
||||
[Doc(markdown = """
|
||||
First.
|
||||
""")]
|
||||
[Doc(markdown = """
|
||||
Second.
|
||||
""")]
|
||||
fn duplicated() -> void { return; }
|
||||
```
|
||||
|
||||
The quick fix removes the second attribute and leaves `First.`. A context diagnostic with the same range but code `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` produces no action. An empty diagnostic list produces no action.
|
||||
|
||||
Malformed or empty Doc is visible and not repairable here:
|
||||
|
||||
```pbs
|
||||
[Doc(text = """
|
||||
Wrong argument.
|
||||
""")]
|
||||
fn malformed() -> void { return; }
|
||||
```
|
||||
|
||||
`InitAllowed` with arguments is repaired only on an interface-module host signature:
|
||||
|
||||
```pbs
|
||||
declare host Gfx {
|
||||
[Host(module = "gfx2d", name = "clear", version = 1)]
|
||||
[InitAllowed(flag = 1)]
|
||||
fn clear() -> void;
|
||||
|
||||
[Host(module = "gfx2d", name = "fill", version = 1)]
|
||||
[InitAllowed()]
|
||||
fn fill() -> void;
|
||||
}
|
||||
```
|
||||
|
||||
`clear` becomes `[InitAllowed]`. `fill` is already valid to the validator, so `[InitAllowed()]` is left alone. The same `InitAllowed(flag = 1)` inside a project file does not get that repair, because the validator does not emit the malformed-attribute diagnostic there.
|
||||
|
||||
The same collector deletes the extra copy of `BuiltinType`, `IntrinsicCall`, `BuiltinConst`, `Host`, and `AssetLowering` when the validator reports `E_SEM_DUPLICATE_RESERVED_ATTRIBUTE` for that surface. A malformed `Host` that merely shares the malformed-attribute code gets no edit.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
Do not choose the edit in `prometeu-lsp` by switching on `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` or by parsing the English message. The code is too coarse, and the message is not the identity.
|
||||
|
||||
Do not store the repair on compiler `Diagnostic` or in `Diagnostic.data`. Compilation and quick fix stay different surfaces. The published code exists so the client can echo it back.
|
||||
|
||||
Do not match on `Diagnostic.source` as if it were the long-term identity field. Today the bridge still copies the code into `source` for the existing presentation. The code-action match uses `Diagnostic.code`. `AGD-0054` may change `source` later.
|
||||
|
||||
Do not delete the first duplicate, and do not trim the whitespace around the deleted span. That cleanup is formatting and belongs to `AGD-0048`.
|
||||
|
||||
Do not insert `[Doc(markdown = """""")]`, an empty `[Host]`, or an empty `[BuiltinType]`. Those edits trade one diagnostic for another, or invent a declaration.
|
||||
|
||||
Do not treat `W_SEM_IGNORED_VALUE` as a source edit. Discard exists in lowering, not as PBS syntax.
|
||||
|
||||
Do not offer the `InitAllowed` rewrite for `[InitAllowed()]` or for a project file. The validator does not diagnose those shapes as “arguments not allowed”, so there is no diagnostic to attach.
|
||||
|
||||
Do not return a protocol error when there is no fix. Rename refuses a bad cursor with an error. Quick fix answers “nothing to do” with an empty list.
|
||||
|
||||
Do not advertise `codeActionProvider` for every frontend that has a language service. The default capability is unsupported.
|
||||
|
||||
Do not treat this lesson as closing formatting, import assistance, diagnostic presentation, snapshots, folding, document links, or hierarchy.
|
||||
|
||||
## References
|
||||
|
||||
- Agenda: `AGD-0047` (scope A, producer B, residence B, first wave C)
|
||||
- Decision: `DEC-0057`
|
||||
- Plan: `PLN-0136`
|
||||
- Spec 23 §8.3 — optional code actions; frontend-owned repair; match by code and range; empty list when unsupported
|
||||
- Spec: `docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md` — wording is not identity; remediation stays outside the v1 diagnostic minimum
|
||||
- `LSN-0056` — code actions are an optional editorial capability
|
||||
- `LSN-0058` — generic `FrontendLanguageService` for the LSP
|
||||
- `LSN-0067` — no new editorial SPI without a real consumer; PBS implements the existing language service
|
||||
- Still open: `AGD-0048` formatting, `AGD-0049` import assistance, `AGD-0054` diagnostics UX, `AGD-0051` snapshots, `AGD-0056` hierarchy
|
||||
@ -0,0 +1,70 @@
|
||||
---
|
||||
id: LSN-0074
|
||||
ticket: pbs-lsp-remaining-editor-surface
|
||||
title: PBS remaining editor surface is frontend-owned and transported by the LSP
|
||||
created: 2026-09-22
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor]
|
||||
---
|
||||
|
||||
# PBS remaining editor surface is frontend-owned and transported by the LSP
|
||||
|
||||
## Original Problem
|
||||
|
||||
Nine open LSP agendas still described formatting, imports, semantic tokens, snapshots, completion, folding, diagnostics, document links, and call hierarchy as if none of that surface existed. By the time those agendas were still open, the server already advertised hover, completion, signature help, definition, references, outline, workspace symbols, rename, quick fix, and full semantic tokens. Each request still rebuilt analysis. References and rename had already shipped without a snapshot cache. There was no diagnostic whose meaning was "this name is not imported."
|
||||
|
||||
Treating the nine agendas as nine decisions would have reimplemented finished work and locked stale premises.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
`DEC-0058` retired that split. One plan, `PLN-0137`, landed seven steps. The frontend produces the editorial result. The LSP transports it and does not invent a second policy. A missing capability is not advertised and returns an empty list, not a protocol error.
|
||||
|
||||
The wave explicitly does not add a project snapshot cache, type hierarchy, `completionItem/resolve`, snippets, a ranking score, range or on-type formatting, organize imports, a missing-import quick fix, `relatedInformation`, or a new semantic-token key.
|
||||
|
||||
1. Published editor diagnostic `source` is the bound frontend `languageId`. For PBS that is `pbs`. `code` stays the stable compiler code. The code is not copied into `source`.
|
||||
2. A document link covers only the span of a `ModuleRef` whose module assembly already resolved to a regular file. Virtual stdlib and unresolved modules produce no link.
|
||||
3. Folding comes from recovered spans of the listed declarations, their bodies, parameter lists, and `Doc` text blocks. A brace, parenthesis, or text block the tree does not cover still folds, including an unmatched opener through the end of the text. Selection grows outward from identifier, to argument or parameter list, to block or `Doc` text block, to declaration.
|
||||
4. Formatting is full-document only. It reprints the token stream, indents by four spaces where brace or parenthesis nesting changes, leaves `Doc` text-block interiors untouched, and does not reorder declarations or move comments off their line.
|
||||
5. Semantic tokens keep `PBSSemanticTokenProvider` as the baseline. Hover resolution may replace an identifier token only when an existing `PbsSemanticKind` matches the symbol. Failure, a broken file, or a symbol with no existing key keeps the baseline token.
|
||||
6. Completion inside a `ModuleRef` lists project and stdlib modules the current resolver can already name. Every other offset keeps the existing member or general completion. Items stay eager and `resolveProvider` stays false.
|
||||
7. Call hierarchy walks `CallExpr` nodes for functions and methods. It does not use the references index. A cursor that is not on a callable is empty. Type hierarchy is not advertised.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
`CompilerLanguageServiceBridge` reads `LspProjectContext.languageId` into `BaselineDocumentIssue.source`. Document links read the physical module map that module assembly already built; stdlib files under `/virtual/stdlib` are not regular files, so they are not links. Folding, selection, and formatting reparse the open buffer with the existing lexer and parser. They do not require a successful project analysis. The semantic-token overlay runs only when editorial context is available and otherwise returns the provider tokens, including when project discovery fails. Module completion labels are the project modules from that same physical map plus the modules `ResourceStdlibEnvironmentResolver` can already resolve. Call hierarchy collects function, method, and constructor declarations from the recovered AST and matches `CallExpr` callees by name and call shape.
|
||||
|
||||
Spec 23 §8.3 states each of these rules. The nine earlier agendas, `AGD-0048` through `AGD-0056`, were abandoned and are not normative.
|
||||
|
||||
## Examples
|
||||
|
||||
A project import `@app:a` that resolves to `src/a/source.pbs` is one document link on that module-reference span. `@sdk:log` is not, because the stdlib file is virtual.
|
||||
|
||||
```pbs
|
||||
fn helper() -> int { return 1; }
|
||||
fn main() -> int { return helper; }
|
||||
```
|
||||
|
||||
The use of `helper` without a call is an identifier in the lexical pass. Hover resolves it as a function, so the overlay key is `pbs-function`. A local such as `value` has no existing key, so it stays `pbs-identifier`.
|
||||
|
||||
```pbs
|
||||
fn helper() -> int { return 1; }
|
||||
fn main() -> int { return helper(); }
|
||||
```
|
||||
|
||||
Prepare on `main` returns that function. Outgoing calls include `helper`. Incoming calls of `helper` include `main`. A cursor on a struct name returns no item.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Do not copy the diagnostic code into `source`. The editor then shows the code twice and loses the language id.
|
||||
- Do not build document links to `/virtual/stdlib`. Definition already refuses that destination.
|
||||
- Do not treat document symbols as folding ranges. Outline and folding are different payloads.
|
||||
- Do not pretty-print from the AST. Comments and `Doc` text-block interiors are not fully modeled as trivia on the tree.
|
||||
- Do not add `pbs-local`, `pbs-parameter`, or `pbs-field` in the overlay. Those symbols stay on the baseline token until a later decision adds the key.
|
||||
- Do not implement call hierarchy by inverting the references index. A reference is a use of a name, not a call.
|
||||
- Do not start a snapshot cache because a later feature feels slow. This wave deliberately kept request-local analysis.
|
||||
|
||||
## References
|
||||
|
||||
- `DEC-0058` and `PLN-0137`, removed by housekeeping after this lesson
|
||||
- `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` §8.3
|
||||
- Earlier surface: `LSN-0068`, `LSN-0069`, `LSN-0070`, `LSN-0071`, `LSN-0072`, `LSN-0073`
|
||||
- Abandoned agendas: `AGD-0048` through `AGD-0056`
|
||||
@ -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.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0045
|
||||
ticket: pbs-lsp-workspace-symbols
|
||||
title: PBS LSP Workspace Symbols
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, workspace-symbols]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
VS Code cannot search PBS symbols across the workspace, so users cannot quickly jump to functions, services, structs, constants, or stdlib-exposed APIs by name.
|
||||
|
||||
## Context
|
||||
|
||||
Document-level editorial features exist, but the LSP does not announce workspaceSymbolProvider and the compiler pipeline does not expose a project-wide symbol index designed for editor queries.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should workspace symbols include only project files or also imported stdlib and SDK surfaces?
|
||||
- [ ] What symbol kinds and ranking should be returned for duplicate names or overloads?
|
||||
- [ ] Should the symbol index be cached from analysis snapshots or rebuilt per request in the first implementation?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Project source index only
|
||||
|
||||
- **Approach:** Index project-owned declarations from analysis snapshots and expose `workspace/symbol`.
|
||||
- **Pro:** Clear ownership and physical locations.
|
||||
- **Con:** Does not help users discover stdlib/SDK symbols.
|
||||
- **Maintainability:** Strong as a first wave.
|
||||
|
||||
### Option B - Project plus stdlib index
|
||||
|
||||
- **Approach:** Include project declarations and imported stdlib/SDK surfaces in the workspace symbol result set.
|
||||
- **Pro:** Better discovery, especially for new users.
|
||||
- **Con:** Requires a navigation policy for stdlib source or virtual targets.
|
||||
- **Maintainability:** Good if stdlib target handling is solved; awkward otherwise.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Workspace symbols are partly navigation and partly discovery. Including stdlib too early can create broken jumps if target locations are not defined.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option A first, then add stdlib once definition has a target policy.
|
||||
|
||||
## Discussion
|
||||
|
||||
This should follow document symbols and the semantic location/index work.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide whether first-wave workspace search includes stdlib or project-only symbols.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0046
|
||||
ticket: pbs-lsp-rename-symbol
|
||||
title: PBS LSP Rename Symbol
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, rename]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS users cannot safely rename a symbol through the editor, so they must edit text manually and risk missing references or changing unrelated overloads and same-name declarations.
|
||||
|
||||
## Context
|
||||
|
||||
Rename requires the same semantic identity and reference machinery needed by definition/references, plus workspace edits and conflict detection. The current LSP does not announce renameProvider.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which symbols should be renameable in the first wave, and which should be protected such as stdlib, generated, host, or builtin declarations?
|
||||
- [ ] How should rename handle overloads, member methods, imports, and labels with the same text?
|
||||
- [ ] What validation should run before returning a WorkspaceEdit?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Local file rename
|
||||
|
||||
- **Approach:** Rename only references in the current file using local semantic resolution.
|
||||
- **Pro:** Smaller blast radius and easier conflict handling.
|
||||
- **Con:** Incomplete for imported symbols and project-wide APIs.
|
||||
- **Maintainability:** Acceptable only if explicitly presented as limited.
|
||||
|
||||
### Option B - Workspace semantic rename
|
||||
|
||||
- **Approach:** Use a project-wide reference index and return a `WorkspaceEdit` for all usage and declaration sites.
|
||||
- **Pro:** Matches user expectations for editor rename.
|
||||
- **Con:** Requires reliable references, collision checks, and write-safety across files.
|
||||
- **Maintainability:** Strong once references exist.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Rename is high trust. A partial or text-based rename can corrupt code silently, so scope must be explicit and conservative.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B after semantic references are implemented. If delivered earlier, ship only a clearly limited local rename.
|
||||
|
||||
## Discussion
|
||||
|
||||
Do not implement rename before definition/references are resolved.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide whether rename waits for workspace references or ships a limited local-only wave.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0047
|
||||
ticket: pbs-lsp-code-actions
|
||||
title: PBS LSP Code Actions and Quick Fixes
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, code-actions, quick-fix]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS diagnostics are visible, but users do not get actionable quick fixes for common compiler/editor problems, making simple repairs slower than necessary.
|
||||
|
||||
## Context
|
||||
|
||||
The LSP maps diagnostics but does not announce codeActionProvider. Compiler diagnostics already carry codes and ranges, but not every diagnostic has enough structured repair data to generate safe edits.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which diagnostics should receive quick fixes first?
|
||||
- [ ] Should code actions be produced from diagnostic codes only, or from structured compiler repair hints?
|
||||
- [ ] How should imports, reserved attributes, missing methods, and invalid Doc shapes be prioritized?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Diagnostic-code quick fixes
|
||||
|
||||
- **Approach:** Map known diagnostic codes to simple edits in the LSP layer.
|
||||
- **Pro:** Fast for a small set of obvious repairs.
|
||||
- **Con:** Couples fixes to diagnostic text/ranges and can become brittle.
|
||||
- **Maintainability:** Moderate if limited to trivial edits.
|
||||
|
||||
### Option B - Compiler repair hints
|
||||
|
||||
- **Approach:** Extend diagnostics with structured repair metadata and let the LSP map hints to code actions.
|
||||
- **Pro:** Keeps product logic close to compiler knowledge.
|
||||
- **Con:** Requires a new diagnostic/repair contract.
|
||||
- **Maintainability:** Strong for non-trivial fixes and future frontends.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Quick fixes are only useful if safe. Simple code-based fixes can ship first, but imports and generated stubs need stronger compiler context.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B as the target design, with Option A allowed only for a small bootstrap set of unambiguous repairs.
|
||||
|
||||
## Discussion
|
||||
|
||||
This should follow diagnostics UX and import assistance for the first high-value fixes.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Pick the first three diagnostic codes eligible for safe quick fixes.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0048
|
||||
ticket: pbs-lsp-formatting
|
||||
title: PBS LSP Formatting
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, formatting]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS formatting is not automated, so style drifts across files, especially around attributes, services, blocks, imports, and multi-line Doc text blocks.
|
||||
|
||||
## Context
|
||||
|
||||
The LSP does not announce documentFormattingProvider. PBS has a parser and AST, but formatting must preserve documentation text block content while normalizing surrounding syntax and indentation.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should formatting be AST-based, token-based, or hybrid to preserve comments and text blocks?
|
||||
- [ ] What canonical style should PBS use for attributes, braces, imports, params, and text blocks?
|
||||
- [ ] Should the first wave support full document formatting only, or also range/on-type formatting?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Token-preserving formatter
|
||||
|
||||
- **Approach:** Format from tokens and trivia, preserving comments and documentation text block contents.
|
||||
- **Pro:** Safer for active editing and text blocks.
|
||||
- **Con:** Harder to enforce deep structural layout.
|
||||
- **Maintainability:** Good if PBS formatting remains mostly syntactic.
|
||||
|
||||
### Option B - AST pretty-printer
|
||||
|
||||
- **Approach:** Reprint code from the AST using canonical style rules.
|
||||
- **Pro:** Produces consistent output.
|
||||
- **Con:** Risks losing comments/trivia and cannot handle malformed files well.
|
||||
- **Maintainability:** Strong only after AST/trivia ownership is explicit.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Formatting must not damage `Doc` text blocks. That makes trivia preservation a first-class requirement rather than an implementation detail.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option A or a hybrid token/AST formatter. Avoid a pure AST pretty-printer until comments/trivia are modeled.
|
||||
|
||||
## Discussion
|
||||
|
||||
Formatter decision should define canonical style before implementation.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide PBS canonical formatting rules for attributes, braces, imports, params, and text blocks.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0049
|
||||
ticket: pbs-lsp-import-assistance
|
||||
title: PBS LSP Import Assistance
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, imports, completion]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS users do not get import-aware assistance, so discovering exported symbols and adding the right import from barrels, stdlib, or project modules remains manual.
|
||||
|
||||
## Context
|
||||
|
||||
Completion already exists for local/editorial contexts, and stdlib imports are understood by the compiler pipeline. The LSP does not yet provide auto-import completion, organize imports, or import quick fixes.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should auto-import target project modules, stdlib modules, or both in the first wave?
|
||||
- [ ] How should barrels and explicit module imports be ranked and rendered in completion?
|
||||
- [ ] Should organize imports be a code action, source action, formatter responsibility, or separate feature?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Import completion only
|
||||
|
||||
- **Approach:** Suggest symbols from known modules and include import detail, but do not edit imports automatically.
|
||||
- **Pro:** Low risk and improves discovery.
|
||||
- **Con:** Still leaves manual import edits.
|
||||
- **Maintainability:** Good as a stepping stone.
|
||||
|
||||
### Option B - Auto-import and organize imports
|
||||
|
||||
- **Approach:** Completion/code actions insert missing imports and source actions sort/remove unused imports.
|
||||
- **Pro:** Matches modern editor expectations.
|
||||
- **Con:** Requires robust module export indexing and edit placement rules.
|
||||
- **Maintainability:** Strong if built on a proper import graph.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Auto-import depends on knowing canonical exports and choosing an edit location. Doing that ad hoc in completion would create formatting and duplicate import issues.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B as the target, but start with import-aware suggestions plus a limited missing-import quick fix.
|
||||
|
||||
## Discussion
|
||||
|
||||
This should be coordinated with code actions and formatter.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide first-wave import sources: stdlib only, project modules only, or both.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0050
|
||||
ticket: pbs-lsp-semantic-tokens-semantic-classification
|
||||
title: PBS LSP Semantic Token Classification
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, semantic-tokens]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS semantic highlighting exists, but parts of the token classification can remain lexical or superficial, which limits visual accuracy for same-looking identifiers with different semantic roles.
|
||||
|
||||
## Context
|
||||
|
||||
The current LSP announces full semantic tokens and maps frontend semantic presentation to VS Code. The compiler has semantic read surfaces that can classify symbols more accurately than raw tokenization.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which token categories must become semantic identity-based first?
|
||||
- [ ] Should semantic tokens be produced from compiler semantic surfaces, parser AST, lexer tokens, or layered output?
|
||||
- [ ] How should tokens behave in files with syntax or semantic errors during active editing?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Lexical tokens with semantic overlays
|
||||
|
||||
- **Approach:** Keep lexer-based tokenization as baseline and overlay semantic categories where resolution succeeds.
|
||||
- **Pro:** Stable during broken edits and incremental to improve.
|
||||
- **Con:** Some unresolved identifiers remain visually generic.
|
||||
- **Maintainability:** Strong because fallback behavior is explicit.
|
||||
|
||||
### Option B - Fully semantic token stream
|
||||
|
||||
- **Approach:** Generate all semantic tokens from compiler semantic surfaces.
|
||||
- **Pro:** Maximum accuracy when analysis succeeds.
|
||||
- **Con:** Highlighting may disappear or degrade sharply on incomplete code.
|
||||
- **Maintainability:** Risky unless snapshot/error recovery is excellent.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Highlighting must be resilient while the user is typing. A perfect semantic pass that fails on partial code is worse than a layered model.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option A: lexical baseline plus semantic overlays from resolved symbols.
|
||||
|
||||
## Discussion
|
||||
|
||||
This can proceed independently but benefits from LSP snapshot/cache work.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Choose semantic overlay categories for the first pass.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0051
|
||||
ticket: pbs-lsp-performance-cache-and-snapshots
|
||||
title: PBS LSP Performance Cache and Snapshots
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, performance, caching]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
Several PBS LSP requests rebuild or reanalyze substantial project state, which can become slow or jittery as projects grow and as richer features are added.
|
||||
|
||||
## Context
|
||||
|
||||
The current bridge favors correctness and simplicity by invoking compiler pipeline analysis for diagnostics and editorial documents. A complete LSP needs snapshot caching, invalidation, cancellation, and predictable latency.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] What is the canonical project/document snapshot boundary for LSP requests?
|
||||
- [ ] Which requests can reuse semantic read surfaces safely across document changes?
|
||||
- [ ] How should cancellation, stale responses, and background reanalysis be handled?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Request-local analysis
|
||||
|
||||
- **Approach:** Keep each LSP request self-contained and rebuild analysis as needed.
|
||||
- **Pro:** Simple, deterministic, and less stateful.
|
||||
- **Con:** Latency grows with project size and feature richness.
|
||||
- **Maintainability:** Good for correctness, weak for interactive performance.
|
||||
|
||||
### Option B - Project snapshot cache
|
||||
|
||||
- **Approach:** Maintain per-project analysis snapshots with document overlays, invalidation, and cancellation-aware request handling.
|
||||
- **Pro:** Required for responsive definition, references, rename, and semantic tokens.
|
||||
- **Con:** Introduces lifecycle complexity and stale-result risks.
|
||||
- **Maintainability:** Strong if snapshot ownership and invalidation are explicit.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
A complete LSP needs shared state, but shared state must not leak stale analysis into edits. The decision is about the boundary, not just caching.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B before implementing workspace-wide features like references and rename.
|
||||
|
||||
## Discussion
|
||||
|
||||
Define snapshot lifecycle, invalidation triggers, and cancellation policy.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide whether snapshot caching is a prerequisite for references/rename or can be staged later.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0052
|
||||
ticket: pbs-lsp-completion-depth
|
||||
title: PBS LSP Completion Depth
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, completion]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS completion works for core cases, but a complete daily-use experience needs ranking, resolve details, snippets, overload handling, context-aware suggestions, and richer documentation.
|
||||
|
||||
## Context
|
||||
|
||||
The current LSP announces completion with trigger character '.' and returns detail/documentation for resolved compiler candidates. It does not use resolveProvider, snippets, broad trigger contexts, or advanced ranking.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which completion contexts should be supported first beyond member access?
|
||||
- [ ] Should completion use resolveProvider for expensive documentation/details or return everything eagerly?
|
||||
- [ ] How should overloads, snippets, imports, keywords, and stdlib candidates be ranked?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Eager rich completion
|
||||
|
||||
- **Approach:** Return labels, details, docs, and snippets directly in every completion response.
|
||||
- **Pro:** Simple client behavior and immediate docs.
|
||||
- **Con:** Can become expensive for broad/global completion.
|
||||
- **Maintainability:** Good while candidate sets remain small.
|
||||
|
||||
### Option B - Completion resolve provider
|
||||
|
||||
- **Approach:** Return lightweight candidates first and populate expensive docs/details through `completionItem/resolve`.
|
||||
- **Pro:** Scales better for large candidate sets and import-aware suggestions.
|
||||
- **Con:** Requires client/server resolve plumbing and stable candidate identities.
|
||||
- **Maintainability:** Strong for a full LSP.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
The current eager model is fine for scoped completions, but global/import-aware completion may need resolve to avoid latency.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer a staged approach: keep eager for current contexts, design resolve before broad workspace/import completion.
|
||||
|
||||
## Discussion
|
||||
|
||||
Define completion contexts and ranking before adding snippets or resolve.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
List first-wave completion contexts beyond member access.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0053
|
||||
ticket: pbs-lsp-folding-and-selection-ranges
|
||||
title: PBS LSP Folding and Selection Ranges
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, folding, selection-range]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS users do not get structural folding or smart selection ranges, so navigating and selecting nested functions, blocks, services, attributes, and Doc text blocks is more manual than it should be.
|
||||
|
||||
## Context
|
||||
|
||||
The parser knows syntactic spans for declarations and blocks. The LSP currently does not announce foldingRangeProvider or selectionRangeProvider.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should folding ranges be computed from AST spans, token pairs, or a hybrid that handles malformed files?
|
||||
- [ ] Which constructs should fold in the first wave: imports, declarations, blocks, attributes, text blocks, or all of them?
|
||||
- [ ] What selection range hierarchy should VS Code expose for identifiers, calls, params, blocks, and declarations?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Parser span ranges
|
||||
|
||||
- **Approach:** Use AST spans for declarations, blocks, parameter lists, and text blocks.
|
||||
- **Pro:** Accurate for well-formed code and easy to map to LSP ranges.
|
||||
- **Con:** Can degrade when parsing fails mid-file.
|
||||
- **Maintainability:** Good because parser owns structure.
|
||||
|
||||
### Option B - Token pair scanning
|
||||
|
||||
- **Approach:** Compute ranges from brace/paren/text-block tokens without relying on full AST validity.
|
||||
- **Pro:** More resilient during malformed edits.
|
||||
- **Con:** Less semantically meaningful and can produce noisy ranges.
|
||||
- **Maintainability:** Good only as fallback.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Folding and selection should keep working while typing. AST ranges are higher quality, but token fallback may be needed for broken files.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer a hybrid: AST ranges first, token fallback for unmatched/incomplete constructs.
|
||||
|
||||
## Discussion
|
||||
|
||||
This is an editor UX feature with low semantic dependency and can be planned independently.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide first-wave foldable constructs and selection hierarchy.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0054
|
||||
ticket: pbs-lsp-diagnostics-ux
|
||||
title: PBS LSP Diagnostics UX
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, diagnostics, ux]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS diagnostics reach VS Code, but the editor experience can still be hard to act on when ranges, related information, severity, and phase context are too generic.
|
||||
|
||||
## Context
|
||||
|
||||
The LSP maps compiler diagnostics to LSP diagnostics. Compiler diagnostics include phase/code/message/range in several paths, but not all editor-friendly fields are consistently exposed.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which diagnostics need better ranges or related information first?
|
||||
- [ ] Should diagnostic source include compiler phase, frontend, or both?
|
||||
- [ ] What structured diagnostic metadata is needed for future quick fixes without coupling UI to text messages?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Mapper-only UX improvements
|
||||
|
||||
- **Approach:** Improve LSP diagnostic mapping using existing diagnostic fields.
|
||||
- **Pro:** Quick improvements to source, severity, and display consistency.
|
||||
- **Con:** Cannot add missing related spans or repair metadata that compiler diagnostics do not expose.
|
||||
- **Maintainability:** Good for presentation-only changes.
|
||||
|
||||
### Option B - Structured diagnostic contract
|
||||
|
||||
- **Approach:** Extend compiler diagnostics with editor-facing metadata such as phase, related locations, stable quick-fix hints, and refined ranges.
|
||||
- **Pro:** Enables better diagnostics and future code actions without parsing message text.
|
||||
- **Con:** Requires compiler/core diagnostic API changes.
|
||||
- **Maintainability:** Strong as the foundation for quick fixes and richer UX.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Some improvements belong in the LSP mapper, but the most valuable changes require structured information at diagnostic creation time.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B as the target, with mapper-only cleanup allowed for low-risk display fixes.
|
||||
|
||||
## Discussion
|
||||
|
||||
This agenda should inform code actions before quick fixes become broad.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Inventory high-frequency diagnostics and identify which need better ranges, related information, or repair hints.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0055
|
||||
ticket: pbs-lsp-document-links
|
||||
title: PBS LSP Document Links
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, document-links, imports]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS import/module references and asset-like surfaces are plain text in VS Code, so users cannot click through to imported modules or related project resources.
|
||||
|
||||
## Context
|
||||
|
||||
The compiler understands module references and project files. The LSP does not announce documentLinkProvider, and the VS Code extension delegates all language behavior to the server.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which references should become document links first: imports, barrels, stdlib modules, assets/addressables, or docs?
|
||||
- [ ] How should virtual or stdlib resources be represented as link targets?
|
||||
- [ ] Should document links overlap with go-to-definition or remain a separate lightweight navigation feature?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Import/module links only
|
||||
|
||||
- **Approach:** Expose `documentLink` for module references such as `@sdk:gfx` and barrel paths.
|
||||
- **Pro:** Narrow, useful, and mostly aligned with existing module resolution.
|
||||
- **Con:** Does not cover assets/addressables or documentation links.
|
||||
- **Maintainability:** Strong first wave.
|
||||
|
||||
### Option B - General resource links
|
||||
|
||||
- **Approach:** Link imports, stdlib modules, assets, addressables, and possibly documentation/resource surfaces.
|
||||
- **Pro:** Richer navigation across project resources.
|
||||
- **Con:** Requires resource-specific target policy and may overlap with definition.
|
||||
- **Maintainability:** Good only after resource ownership is defined.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Document links should remain lightweight. If a link requires semantic identity and symbol resolution, it may belong in go-to-definition instead.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option A first: imports and module references only.
|
||||
|
||||
## Discussion
|
||||
|
||||
Coordinate stdlib target handling with go-to-definition.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide first-wave link targets and how stdlib/module URIs are represented.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0056
|
||||
ticket: pbs-lsp-call-and-type-hierarchy
|
||||
title: PBS LSP Call and Type Hierarchy
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, call-hierarchy, type-hierarchy]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS users cannot inspect call relationships or type/member relationships from the editor, which limits understanding of larger projects after basic navigation exists.
|
||||
|
||||
## Context
|
||||
|
||||
Call hierarchy and type hierarchy are advanced LSP features. They depend on reliable semantic identity, references, callsite classification, and type ownership surfaces, so they should probably follow definition/references rather than precede them.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should call hierarchy and type hierarchy be one discussion or split after dependencies are clearer?
|
||||
- [ ] Which relationships are valuable for PBS: function call graph, service method calls, struct methods, contract implementations, host/intrinsic calls, or all of them?
|
||||
- [ ] What prerequisite symbol/reference infrastructure must be completed before this feature is safe to implement?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Call hierarchy only
|
||||
|
||||
- **Approach:** Implement incoming/outgoing calls for functions and methods after callsite references are stable.
|
||||
- **Pro:** Directly useful and builds on executable call resolution.
|
||||
- **Con:** Does not cover type ownership, contracts, or implementations.
|
||||
- **Maintainability:** Strong if based on the reference/callsite index.
|
||||
|
||||
### Option B - Split call hierarchy and type hierarchy
|
||||
|
||||
- **Approach:** Treat call graph and type/member/contract hierarchy as separate decisions and implementations.
|
||||
- **Pro:** Avoids mixing two different semantic models.
|
||||
- **Con:** Creates more workflow artifacts and sequencing work.
|
||||
- **Maintainability:** Strong; each feature can evolve on its own contract.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Call hierarchy and type hierarchy share LSP vocabulary but not the same compiler data. Coupling them too early would make the first implementation harder to reason about.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B: split after this agenda confirms dependencies, likely implementing call hierarchy first.
|
||||
|
||||
## Discussion
|
||||
|
||||
This should wait until definition and references are complete.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide whether to split into two discussions before moving to decisions.
|
||||
@ -276,8 +276,32 @@ 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 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.
|
||||
|
||||
When a frontend exposes workspace symbols, the result MUST be a flat search of named declarations whose locations identify compiler-known physical source files of the current analysis. Presence, names, kinds, details, and ranges MUST come from the recovered syntax of those files. Semantic success MUST NOT be required. Semantic enrichment MUST NOT drop a named declaration that the syntax tree still exposes. A missing analysis surface MUST produce an empty list. The workspace-symbol request MUST NOT require an open text document. A declaration without a compiler-known physical file MUST be omitted. Tooling MUST NOT require virtual documents, untitled buffers, or synthetic URIs for this capability. Workspace symbols MUST NOT be a document outline, folding ranges, a persistent symbol index, or a navigation list of virtual locations.
|
||||
|
||||
When a frontend exposes rename, the result MUST be a validated workspace edit of identifier spans whose locations identify compiler-known physical source files. Identity MUST be the physical declaration locations that definition returns at the cursor. A local or parameter name span MUST still be renameable when definition at that offset is empty. Each rewritten location MUST identify a compiler-known physical source file. A cursor that is not renameable, including builtins, virtual paths, unresolved names, keywords, comments, strings, import aliases and uses of those aliases, error case labels without their own span, and barrel files, MUST produce a protocol error or a null prepareRename result. Tooling MUST NOT return a successful empty workspace edit for a refused cursor or an invalid new name. The new name MUST be a language identifier that is not a keyword and MUST NOT collide in the same binding space, ignoring the sites being rewritten and the overload set being renamed. The server MUST NOT write files and MUST NOT rename file paths. Host prepareRename MUST advertise a prepareProvider when rename is exposed. Tooling MUST NOT require virtual documents, untitled buffers, or synthetic URIs for this capability.
|
||||
|
||||
When a frontend exposes code actions, the result MUST be quick fixes only: one workspace edit tied to a diagnostic of the requested physical document. The frontend MUST produce each repair from that document's syntax. Each repair MUST name the stable diagnostic code, the primary span, and exactly one local text edit. Tooling MUST transport that repair. Tooling MUST NOT choose the edit by parsing the diagnostic message or by a diagnostic-code table. A repair MUST NOT be a required field of compiler diagnostics and MUST NOT be published inside diagnostic data. The published diagnostic MUST carry the stable code so a later code-action request can match it. A repair MUST be returned only when the request context contains a diagnostic with the same code and the same primary range. An empty diagnostic context MUST produce an empty list. The edit MUST change only the requested physical source file. The server MUST NOT write files, MUST NOT return a command, and MUST NOT require a resolve round trip. A missing code-action capability MUST NOT advertise code actions and MUST produce an empty list rather than a protocol error. Virtual, untitled, or synthetic documents MUST NOT receive an edit. Code actions MUST NOT include organize-imports, source actions, refactors, or generated stubs unless a future accepted decision says so.
|
||||
|
||||
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.
|
||||
|
||||
When tooling publishes an editor diagnostic, `source` MUST be the `languageId` of the bound frontend project. `code` MUST be the stable compiler diagnostic code when one exists, and MUST be empty when the compiler diagnostic has none. Tooling MUST NOT copy that code into `source`. Tooling MUST NOT publish related locations, compiler phase, or a repair payload on the editor diagnostic.
|
||||
|
||||
When a frontend exposes document links, each link MUST cover the span of a module reference whose resolved destination is a compiler-known regular file. A module reference that does not resolve to a regular file MUST produce no link. Tooling MUST NOT target virtual, untitled, or synthetic URIs, including virtual stdlib documents. Document links MUST NOT replace definition. A missing document-link capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
|
||||
|
||||
When a frontend exposes folding ranges, the ranges MUST come from recovered syntax spans for top-level `fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, and `host` declarations, their bodies, and `Doc` text blocks. A parameter list MUST fold from the parenthesis pair. A brace, parenthesis, or text-block delimiter that the recovered tree does not cover MUST still produce a token range, including an unmatched opener through the end of the text. Folding ranges MUST NOT be document symbols.
|
||||
|
||||
When a frontend exposes selection ranges, the chain at a cursor MUST run from the inside outward in this order: identifier, argument or parameter list, block or `Doc` text block, declaration. A layer that does not contain the cursor MUST be omitted. A missing folding or selection capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
|
||||
|
||||
When a frontend exposes call hierarchy, incoming and outgoing calls MUST be callsites found by walking call expressions in the syntax tree. The references index MUST NOT be used as the call graph. A cursor that is not on a function, method, or constructor MUST produce an empty prepare result. Type hierarchy MUST NOT be advertised. A missing call-hierarchy capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
|
||||
|
||||
When a frontend exposes completion inside a module reference, the candidates MUST be project and stdlib modules the current module resolver can already name. Completion outside a module reference MUST keep its existing member and general candidates. Completion items MUST stay eager. `resolveProvider` MUST stay false. Snippets and a new ranking score MUST NOT be added.
|
||||
|
||||
When a frontend exposes semantic tokens, the lexical token provider remains the baseline. An identifier token MAY be replaced with an existing semantic key when the same resolution used by hover names that symbol and an existing key matches it. Resolution failure, a broken file, or a symbol with no existing key MUST keep the baseline token. This overlay MUST NOT add a semantic-token key.
|
||||
|
||||
When a frontend exposes formatting, it MUST be full-document formatting only. Range formatting and on-type formatting MUST NOT be advertised. The formatter MUST reprint the existing token stream. It MUST NOT reorder declarations, join lines, or split lines. Indentation MUST be four spaces and MUST change only where brace or parenthesis nesting changes. The interior of a `Doc` text block MUST be copied unchanged. A comment MUST stay on the line of the token it already follows. A missing formatting capability MUST NOT be advertised and MUST produce an empty edit list rather than a protocol error.
|
||||
|
||||
`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.
|
||||
|
||||
The frontend registry MUST resolve providers by `languageId`. A lookup for an unknown `languageId` MUST fail explicitly with a diagnostic-friendly error. Unknown languages MUST NOT silently fall back to PBS or to any other frontend.
|
||||
|
||||
@ -6,16 +6,36 @@ 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.PbsEditorialDocumentSymbol;
|
||||
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.pbs.semantics.PbsEditorialSupportService.EditorialDocument;
|
||||
import p.studio.compiler.models.SourceKind;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialRename;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol;
|
||||
import p.studio.compiler.pbs.PbsCallHierarchy;
|
||||
import p.studio.compiler.pbs.PbsDocumentFormatter;
|
||||
import p.studio.compiler.pbs.PbsSemanticTokenOverlay;
|
||||
import p.studio.compiler.pbs.PbsStructureRanges;
|
||||
import p.studio.compiler.pbs.semantics.PbsQuickFix;
|
||||
import p.studio.compiler.pbs.semantics.PbsQuickFixCollector;
|
||||
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
|
||||
import p.studio.compiler.services.FrontendCallHierarchyItem;
|
||||
import p.studio.compiler.services.FrontendCallSite;
|
||||
import p.studio.compiler.services.FrontendCodeAction;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendSymbolKind;
|
||||
import p.studio.compiler.services.FrontendDocumentLink;
|
||||
import p.studio.compiler.services.FrontendFoldingRange;
|
||||
import p.studio.compiler.services.FrontendSelectionRange;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendDocumentSymbol;
|
||||
import p.studio.compiler.services.FrontendRenameEdit;
|
||||
import p.studio.compiler.services.FrontendRenameResult;
|
||||
import p.studio.compiler.services.FrontendDocumentation;
|
||||
import p.studio.compiler.services.FrontendEditorialContext;
|
||||
import p.studio.compiler.services.FrontendHover;
|
||||
@ -23,16 +43,20 @@ import p.studio.compiler.services.FrontendLanguageService;
|
||||
import p.studio.compiler.services.FrontendSignature;
|
||||
import p.studio.compiler.services.FrontendSignatureHelp;
|
||||
import p.studio.compiler.services.FrontendSymbolKind;
|
||||
import p.studio.compiler.services.FrontendWorkspaceRequest;
|
||||
import p.studio.compiler.services.FrontendWorkspaceSymbol;
|
||||
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.compiler.source.tables.ModuleReference;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@ -53,6 +77,23 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
this.semanticTokenProvider = Objects.requireNonNull(semanticTokenProvider, "semanticTokenProvider");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<FrontendEditorialContext> prepareEditorialContext(final FrontendPhaseContext phaseContext) {
|
||||
if (phaseContext == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
final var semanticReadSurface = PBSFrontendPhaseService.semanticReadSurface(
|
||||
phaseContext,
|
||||
DiagnosticSink.empty(),
|
||||
BuildingIssueSink.empty());
|
||||
return Optional.of(new FrontendEditorialContext(
|
||||
null,
|
||||
ReadOnlyList.empty(),
|
||||
phaseContext.feSurfaceContext(),
|
||||
phaseContext.fileTable,
|
||||
semanticReadSurface));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<FrontendEditorialContext> prepareEditorialContext(
|
||||
final FrontendPhaseContext phaseContext,
|
||||
@ -98,6 +139,10 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (insideModuleRef(ast, offset)
|
||||
&& editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface) {
|
||||
return moduleCompletions(surface.moduleCompletionLabels());
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
|
||||
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
|
||||
@ -267,15 +312,404 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
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
|
||||
public List<FrontendWorkspaceSymbol> workspaceSymbols(
|
||||
final FrontendWorkspaceRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
if (request == null
|
||||
|| editorialContext == null
|
||||
|| !(editorialContext.sourceFiles() instanceof FileTableReader fileTable)
|
||||
|| !(editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface)
|
||||
|| surface.astByFile() == null
|
||||
|| surface.astByFile().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<FrontendWorkspaceSymbol> symbols = new ArrayList<>();
|
||||
for (final var entry : surface.astByFile().entrySet()) {
|
||||
final Path path = pathForRegularSource(entry.getKey(), fileTable);
|
||||
if (path == null) {
|
||||
continue;
|
||||
}
|
||||
final PbsAst.File fileAst = entry.getValue();
|
||||
if (fileAst == null) {
|
||||
continue;
|
||||
}
|
||||
for (final PbsEditorialWorkspaceSymbol symbol : editorialSupportService.workspaceSymbols(
|
||||
fileAst,
|
||||
request.query())) {
|
||||
toFrontendWorkspaceSymbol(symbol, path).ifPresent(symbols::add);
|
||||
}
|
||||
}
|
||||
symbols.sort(Comparator
|
||||
.comparing(FrontendWorkspaceSymbol::name, String.CASE_INSENSITIVE_ORDER)
|
||||
.thenComparing(symbol -> symbol.path().toString())
|
||||
.thenComparingInt(FrontendWorkspaceSymbol::startOffset));
|
||||
return List.copyOf(symbols);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<FrontendDefinitionLocation> prepareRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset) {
|
||||
final PbsEditorialRename planned = editorialRename(request, editorialContext, offset, null);
|
||||
if (planned == null || !planned.allowed()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (physicalIdentity(planned, fileTable).isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return toFrontendLocation(planned.placeholder(), fileTable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrontendRenameResult rename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
if (newName == null) {
|
||||
return FrontendRenameResult.refused("invalid identifier");
|
||||
}
|
||||
final PbsEditorialRename planned = editorialRename(request, editorialContext, offset, newName);
|
||||
if (planned == null) {
|
||||
return FrontendRenameResult.refused("not renameable");
|
||||
}
|
||||
if (!planned.allowed()) {
|
||||
return FrontendRenameResult.refused(planned.refusal());
|
||||
}
|
||||
if (!(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
|
||||
return FrontendRenameResult.refused("not renameable");
|
||||
}
|
||||
if (physicalIdentity(planned, fileTable).isEmpty()) {
|
||||
return FrontendRenameResult.refused("not renameable");
|
||||
}
|
||||
final LinkedHashMap<String, FrontendRenameEdit> edits = new LinkedHashMap<>();
|
||||
for (final PbsEditorialLocation site : planned.sites()) {
|
||||
toFrontendLocation(site, fileTable).ifPresent(location ->
|
||||
edits.putIfAbsent(
|
||||
frontendLocationKey(location),
|
||||
new FrontendRenameEdit(
|
||||
location.path(),
|
||||
location.startOffset(),
|
||||
location.endOffset(),
|
||||
newName)));
|
||||
}
|
||||
if (edits.isEmpty()) {
|
||||
return FrontendRenameResult.refused("not renameable");
|
||||
}
|
||||
return FrontendRenameResult.applied(List.copyOf(edits.values()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean documentLinksSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendDocumentLink> documentLinks(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
if (request == null
|
||||
|| editorialContext == null
|
||||
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|
||||
|| !(editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface)
|
||||
|| surface.physicalModuleFileByReference() == null
|
||||
|| ast.imports() == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<FrontendDocumentLink> links = new ArrayList<>();
|
||||
for (final PbsAst.ImportDecl importDecl : ast.imports()) {
|
||||
if (importDecl == null || importDecl.moduleRef() == null) {
|
||||
continue;
|
||||
}
|
||||
final PbsAst.ModuleRef moduleRef = importDecl.moduleRef();
|
||||
final Span span = moduleRef.span();
|
||||
if (span == null || span.isNone() || moduleRef.project() == null || moduleRef.project().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
final Path target = surface.physicalModuleFileByReference().get(
|
||||
new ModuleReference(moduleRef.project(), moduleRef.pathSegments()));
|
||||
if (target == null) {
|
||||
continue;
|
||||
}
|
||||
links.add(new FrontendDocumentLink(
|
||||
target,
|
||||
toOffset(span.getStart()),
|
||||
toOffset(span.getEnd())));
|
||||
}
|
||||
return List.copyOf(links);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean foldingRangesSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendFoldingRange> foldingRanges(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return PbsStructureRanges.foldingRanges(request == null || request.documentText() == null
|
||||
? ""
|
||||
: request.documentText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean selectionRangesSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendSelectionRange> selectionRanges(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final List<Integer> offsets) {
|
||||
return PbsStructureRanges.selectionRanges(
|
||||
request == null || request.documentText() == null ? "" : request.documentText(),
|
||||
offsets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean callHierarchySupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendCallHierarchyItem> prepareCallHierarchy(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset) {
|
||||
final PbsAst.File ast = callHierarchyAst(editorialContext);
|
||||
if (ast == null) {
|
||||
return List.of();
|
||||
}
|
||||
return PbsCallHierarchy.prepare(ast, offset).stream().map(this::toCallItem).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendCallSite> incomingCalls(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final FrontendCallHierarchyItem target) {
|
||||
return callSites(callHierarchyAst(editorialContext), target, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendCallSite> outgoingCalls(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final FrontendCallHierarchyItem target) {
|
||||
return callSites(callHierarchyAst(editorialContext), target, false);
|
||||
}
|
||||
|
||||
private List<FrontendCallSite> callSites(
|
||||
final PbsAst.File ast,
|
||||
final FrontendCallHierarchyItem target,
|
||||
final boolean incoming) {
|
||||
if (ast == null || target == null) {
|
||||
return List.of();
|
||||
}
|
||||
final PbsCallHierarchy.Callable callable = new PbsCallHierarchy.Callable(
|
||||
target.name(),
|
||||
target.method(),
|
||||
target.startOffset(),
|
||||
target.endOffset());
|
||||
final var calls = incoming
|
||||
? PbsCallHierarchy.incomingCalls(ast, callable)
|
||||
: PbsCallHierarchy.outgoingCalls(ast, callable);
|
||||
return calls.stream()
|
||||
.map(call -> new FrontendCallSite(
|
||||
toCallItem(incoming ? call.caller() : call.callee()),
|
||||
call.startOffset(),
|
||||
call.endOffset()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static PbsAst.File callHierarchyAst(final FrontendEditorialContext editorialContext) {
|
||||
if (editorialContext == null || !(editorialContext.syntaxTree() instanceof PbsAst.File ast)) {
|
||||
return null;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
private FrontendCallHierarchyItem toCallItem(final PbsCallHierarchy.Callable callable) {
|
||||
return new FrontendCallHierarchyItem(
|
||||
callable.name(),
|
||||
callable.method(),
|
||||
callable.startOffset(),
|
||||
callable.endOffset());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean formattingSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> formatDocument(final FrontendDocumentRequest request) {
|
||||
return Optional.of(PbsDocumentFormatter.format(
|
||||
request == null || request.documentText() == null ? "" : request.documentText()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean codeActionsSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendCodeAction> codeActions(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
if (request == null
|
||||
|| editorialContext == null
|
||||
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|
||||
|| !(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
|
||||
return List.of();
|
||||
}
|
||||
final FileId fileId = ast.span() == null ? null : ast.span().getFileId();
|
||||
final Path path = pathForRegularSource(fileId, fileTable);
|
||||
if (path == null || !path.equals(request.documentPath().toAbsolutePath().normalize())) {
|
||||
return List.of();
|
||||
}
|
||||
final boolean interfaceModule = editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface
|
||||
&& surface.sourceKindByFile() != null
|
||||
&& surface.sourceKindByFile().get(fileId) == SourceKind.SDK_INTERFACE;
|
||||
final ArrayList<FrontendCodeAction> actions = new ArrayList<>();
|
||||
for (final PbsQuickFix repair : PbsQuickFixCollector.collect(ast, interfaceModule)) {
|
||||
actions.add(new FrontendCodeAction(
|
||||
repair.title(),
|
||||
repair.diagnosticCode(),
|
||||
path,
|
||||
repair.startOffset(),
|
||||
repair.endOffset(),
|
||||
repair.newText()));
|
||||
}
|
||||
return List.copyOf(actions);
|
||||
}
|
||||
|
||||
private PbsEditorialRename editorialRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
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 null;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
|
||||
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
|
||||
final EditorialDocument cursorDocument = new EditorialDocument(
|
||||
request.documentText(),
|
||||
ast,
|
||||
typedSupplementalTopDecls,
|
||||
feSurfaceContext);
|
||||
return editorialSupportService.rename(
|
||||
cursorDocument,
|
||||
offset,
|
||||
newName,
|
||||
projectDocuments(request, editorialContext, cursorDocument, fileTable));
|
||||
}
|
||||
|
||||
private List<FrontendDefinitionLocation> physicalIdentity(
|
||||
final PbsEditorialRename planned,
|
||||
final FileTableReader fileTable) {
|
||||
final ArrayList<FrontendDefinitionLocation> locations = new ArrayList<>();
|
||||
for (final PbsEditorialLocation location : planned.identity()) {
|
||||
toFrontendLocation(location, fileTable).ifPresent(locations::add);
|
||||
}
|
||||
return List.copyOf(locations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
|
||||
return semanticTokens(request == null ? "" : request.documentText());
|
||||
return semanticTokens(request, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendSemanticToken> semanticTokens(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
final String text = request == null || request.documentText() == null ? "" : request.documentText();
|
||||
final List<FrontendSemanticToken> baseline = semanticTokenProvider.tokenize(text);
|
||||
if (editorialContext == null
|
||||
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|
||||
|| !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList<?> supplementalTopDecls)
|
||||
|| !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext)) {
|
||||
return baseline;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
|
||||
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
|
||||
return PbsSemanticTokenOverlay.apply(
|
||||
baseline,
|
||||
text,
|
||||
ast,
|
||||
typedSupplementalTopDecls,
|
||||
feSurfaceContext,
|
||||
editorialSupportService);
|
||||
}
|
||||
|
||||
public List<FrontendSemanticToken> semanticTokens(final String documentText) {
|
||||
return semanticTokenProvider.tokenize(documentText == null ? "" : documentText);
|
||||
}
|
||||
|
||||
private static boolean insideModuleRef(final PbsAst.File ast, final int offset) {
|
||||
if (ast == null || ast.imports() == null || offset < 0) {
|
||||
return false;
|
||||
}
|
||||
for (final PbsAst.ImportDecl importDecl : ast.imports()) {
|
||||
if (importDecl == null || importDecl.moduleRef() == null || importDecl.moduleRef().span() == null) {
|
||||
continue;
|
||||
}
|
||||
final long start = importDecl.moduleRef().span().getStart();
|
||||
final long end = importDecl.moduleRef().span().getEnd();
|
||||
if (end > start && start <= offset && offset <= end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<FrontendCompletionCandidate> moduleCompletions(final List<String> labels) {
|
||||
if (labels == null || labels.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<FrontendCompletionCandidate> candidates = new ArrayList<>();
|
||||
for (final String label : labels) {
|
||||
if (label == null || label.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
candidates.add(new FrontendCompletionCandidate(
|
||||
label,
|
||||
FrontendSymbolKind.MODULE,
|
||||
"module",
|
||||
"",
|
||||
""));
|
||||
}
|
||||
return List.copyOf(candidates);
|
||||
}
|
||||
|
||||
private FrontendCompletionCandidate toFrontendCompletionCandidate(final PbsEditorialCompletionCandidate candidate) {
|
||||
return new FrontendCompletionCandidate(
|
||||
candidate.label(),
|
||||
@ -314,6 +748,36 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
return FrontendSymbolKind.valueOf(kind.name());
|
||||
}
|
||||
|
||||
private Optional<FrontendWorkspaceSymbol> toFrontendWorkspaceSymbol(
|
||||
final PbsEditorialWorkspaceSymbol symbol,
|
||||
final Path path) {
|
||||
if (symbol == null || path == null || symbol.span() == null || symbol.span().isNone()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new FrontendWorkspaceSymbol(
|
||||
symbol.name(),
|
||||
toFrontendSymbolKind(symbol.kind()),
|
||||
symbol.detail(),
|
||||
symbol.containerName(),
|
||||
path,
|
||||
toOffset(symbol.span().getStart()),
|
||||
toOffset(symbol.span().getEnd())));
|
||||
}
|
||||
|
||||
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(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
@ -348,6 +812,15 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
return List.copyOf(documents);
|
||||
}
|
||||
|
||||
private Path pathForRegularSource(
|
||||
final FileId fileId,
|
||||
final FileTableReader fileTable) {
|
||||
if (!isRegularSource(fileId, fileTable)) {
|
||||
return null;
|
||||
}
|
||||
return fileTable.get(fileId).getCanonPath().toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private boolean isRegularSource(
|
||||
final FileId fileId,
|
||||
final FileTableReader fileTable) {
|
||||
|
||||
@ -0,0 +1,335 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.source.Span;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class PbsCallHierarchy {
|
||||
private PbsCallHierarchy() {
|
||||
}
|
||||
|
||||
public record Callable(
|
||||
String name,
|
||||
boolean method,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
}
|
||||
|
||||
public record CallSite(
|
||||
Callable caller,
|
||||
Callable callee,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
}
|
||||
|
||||
public static List<Callable> prepare(final PbsAst.File ast, final int offset) {
|
||||
Callable best = null;
|
||||
for (final Callable callable : callables(ast)) {
|
||||
if (callable.startOffset() > offset || offset >= callable.endOffset()) {
|
||||
continue;
|
||||
}
|
||||
if (best == null || callable.endOffset() - callable.startOffset() < best.endOffset() - best.startOffset()) {
|
||||
best = callable;
|
||||
}
|
||||
}
|
||||
return best == null ? List.of() : List.of(best);
|
||||
}
|
||||
|
||||
public static List<CallSite> incomingCalls(final PbsAst.File ast, final Callable target) {
|
||||
if (target == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<CallSite> calls = new ArrayList<>();
|
||||
for (final CallSite call : calls(ast)) {
|
||||
if (sameCallable(call.callee(), target)) {
|
||||
calls.add(call);
|
||||
}
|
||||
}
|
||||
return List.copyOf(calls);
|
||||
}
|
||||
|
||||
public static List<CallSite> outgoingCalls(final PbsAst.File ast, final Callable target) {
|
||||
if (target == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<CallSite> calls = new ArrayList<>();
|
||||
for (final CallSite call : calls(ast)) {
|
||||
if (sameCallable(call.caller(), target)) {
|
||||
calls.add(call);
|
||||
}
|
||||
}
|
||||
return List.copyOf(calls);
|
||||
}
|
||||
|
||||
private static boolean sameCallable(final Callable left, final Callable right) {
|
||||
return left != null
|
||||
&& right != null
|
||||
&& left.method() == right.method()
|
||||
&& left.name().equals(right.name())
|
||||
&& left.startOffset() == right.startOffset()
|
||||
&& left.endOffset() == right.endOffset();
|
||||
}
|
||||
|
||||
private static List<Callable> callables(final PbsAst.File ast) {
|
||||
final ArrayList<Callable> callables = new ArrayList<>();
|
||||
if (ast == null || ast.topDecls() == null) {
|
||||
return callables;
|
||||
}
|
||||
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
|
||||
collectCallables(topDecl, callables);
|
||||
}
|
||||
return callables;
|
||||
}
|
||||
|
||||
private static void collectCallables(final PbsAst.TopDecl topDecl, final List<Callable> callables) {
|
||||
switch (topDecl) {
|
||||
case null -> {
|
||||
}
|
||||
case PbsAst.FunctionDecl functionDecl -> callables.add(callable(functionDecl.name(), false, functionDecl.span()));
|
||||
case PbsAst.StructDecl structDecl -> {
|
||||
if (structDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
|
||||
if (method != null) {
|
||||
callables.add(callable(method.name(), true, method.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (structDecl.ctors() != null) {
|
||||
for (final PbsAst.CtorDecl ctor : structDecl.ctors()) {
|
||||
if (ctor != null) {
|
||||
callables.add(callable(ctor.name(), true, ctor.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.ServiceDecl serviceDecl -> {
|
||||
if (serviceDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
|
||||
if (method != null) {
|
||||
callables.add(callable(method.name(), true, method.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<CallSite> calls(final PbsAst.File ast) {
|
||||
final List<Callable> callables = callables(ast);
|
||||
final ArrayList<CallSite> calls = new ArrayList<>();
|
||||
for (final Callable caller : callables) {
|
||||
final PbsAst.Block body = body(ast, caller);
|
||||
if (body == null) {
|
||||
continue;
|
||||
}
|
||||
final ArrayList<PbsAst.CallExpr> found = new ArrayList<>();
|
||||
collectCalls(body, found);
|
||||
for (final PbsAst.CallExpr call : found) {
|
||||
final Callable callee = resolveCallee(call, callables);
|
||||
final int[] span = offsets(call.span());
|
||||
if (callee == null || span == null) {
|
||||
continue;
|
||||
}
|
||||
calls.add(new CallSite(caller, callee, span[0], span[1]));
|
||||
}
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
private static Callable resolveCallee(final PbsAst.CallExpr call, final List<Callable> callables) {
|
||||
if (call == null || call.callee() == null) {
|
||||
return null;
|
||||
}
|
||||
final boolean method = call.callee() instanceof PbsAst.MemberExpr;
|
||||
final String name = switch (call.callee()) {
|
||||
case PbsAst.IdentifierExpr identifier -> identifier.name();
|
||||
case PbsAst.MemberExpr member -> member.memberName();
|
||||
default -> null;
|
||||
};
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
for (final Callable callable : callables) {
|
||||
if (callable.method() == method && callable.name().equals(name)) {
|
||||
return callable;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PbsAst.Block body(final PbsAst.File ast, final Callable caller) {
|
||||
for (final Callable callable : callables(ast)) {
|
||||
if (!sameCallable(callable, caller)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return findBody(ast, caller);
|
||||
}
|
||||
|
||||
private static PbsAst.Block findBody(final PbsAst.File ast, final Callable caller) {
|
||||
if (ast.topDecls() == null) {
|
||||
return null;
|
||||
}
|
||||
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
|
||||
final PbsAst.Block body = findBody(topDecl, caller);
|
||||
if (body != null) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PbsAst.Block findBody(final PbsAst.TopDecl topDecl, final Callable caller) {
|
||||
switch (topDecl) {
|
||||
case null -> {
|
||||
return null;
|
||||
}
|
||||
case PbsAst.FunctionDecl functionDecl -> {
|
||||
if (matches(functionDecl.name(), false, functionDecl.span(), caller)) {
|
||||
return functionDecl.body();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case PbsAst.StructDecl structDecl -> {
|
||||
if (structDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
|
||||
if (method != null && matches(method.name(), true, method.span(), caller)) {
|
||||
return method.body();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (structDecl.ctors() != null) {
|
||||
for (final PbsAst.CtorDecl ctor : structDecl.ctors()) {
|
||||
if (ctor != null && matches(ctor.name(), true, ctor.span(), caller)) {
|
||||
return ctor.body();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case PbsAst.ServiceDecl serviceDecl -> {
|
||||
if (serviceDecl.methods() == null) {
|
||||
return null;
|
||||
}
|
||||
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
|
||||
if (method != null && matches(method.name(), true, method.span(), caller)) {
|
||||
return method.body();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectCalls(final PbsAst.Block block, final List<PbsAst.CallExpr> calls) {
|
||||
if (block == null || block.statements() == null) {
|
||||
return;
|
||||
}
|
||||
for (final PbsAst.Statement statement : block.statements()) {
|
||||
collectCalls(statement, calls);
|
||||
}
|
||||
collectCalls(block.tailExpression(), calls);
|
||||
}
|
||||
|
||||
private static void collectCalls(final PbsAst.Statement statement, final List<PbsAst.CallExpr> calls) {
|
||||
switch (statement) {
|
||||
case null -> {
|
||||
}
|
||||
case PbsAst.LetStatement letStatement -> collectCalls(letStatement.initializer(), calls);
|
||||
case PbsAst.ReturnStatement returnStatement -> collectCalls(returnStatement.value(), calls);
|
||||
case PbsAst.AssignStatement assignStatement -> collectCalls(assignStatement.value(), calls);
|
||||
case PbsAst.ExpressionStatement expressionStatement -> collectCalls(expressionStatement.expression(), calls);
|
||||
case PbsAst.IfStatement ifStatement -> {
|
||||
collectCalls(ifStatement.condition(), calls);
|
||||
collectCalls(ifStatement.thenBlock(), calls);
|
||||
collectCalls(ifStatement.elseBlock(), calls);
|
||||
collectCalls(ifStatement.elseIf(), calls);
|
||||
}
|
||||
case PbsAst.ForStatement forStatement -> {
|
||||
collectCalls(forStatement.fromExpression(), calls);
|
||||
collectCalls(forStatement.untilExpression(), calls);
|
||||
collectCalls(forStatement.stepExpression(), calls);
|
||||
collectCalls(forStatement.body(), calls);
|
||||
}
|
||||
case PbsAst.WhileStatement whileStatement -> {
|
||||
collectCalls(whileStatement.condition(), calls);
|
||||
collectCalls(whileStatement.body(), calls);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectCalls(final PbsAst.Expression expression, final List<PbsAst.CallExpr> calls) {
|
||||
switch (expression) {
|
||||
case null -> {
|
||||
}
|
||||
case PbsAst.CallExpr callExpr -> {
|
||||
calls.add(callExpr);
|
||||
collectCalls(callExpr.callee(), calls);
|
||||
if (callExpr.arguments() != null) {
|
||||
for (final PbsAst.Expression argument : callExpr.arguments()) {
|
||||
collectCalls(argument, calls);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.MemberExpr memberExpr -> collectCalls(memberExpr.receiver(), calls);
|
||||
case PbsAst.BinaryExpr binaryExpr -> {
|
||||
collectCalls(binaryExpr.left(), calls);
|
||||
collectCalls(binaryExpr.right(), calls);
|
||||
}
|
||||
case PbsAst.UnaryExpr unaryExpr -> collectCalls(unaryExpr.expression(), calls);
|
||||
case PbsAst.GroupExpr groupExpr -> collectCalls(groupExpr.expression(), calls);
|
||||
case PbsAst.IfExpr ifExpr -> {
|
||||
collectCalls(ifExpr.condition(), calls);
|
||||
collectCalls(ifExpr.thenBlock(), calls);
|
||||
collectCalls(ifExpr.elseExpression(), calls);
|
||||
}
|
||||
case PbsAst.BlockExpr blockExpr -> collectCalls(blockExpr.block(), calls);
|
||||
case PbsAst.ApplyExpr applyExpr -> {
|
||||
collectCalls(applyExpr.callee(), calls);
|
||||
collectCalls(applyExpr.argument(), calls);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matches(
|
||||
final String name,
|
||||
final boolean method,
|
||||
final Span span,
|
||||
final Callable caller) {
|
||||
final Callable callable = callable(name, method, span);
|
||||
return callable != null && sameCallable(callable, caller);
|
||||
}
|
||||
|
||||
private static Callable callable(final String name, final boolean method, final Span span) {
|
||||
final int[] offsets = offsets(span);
|
||||
if (name == null || name.isBlank() || offsets == null) {
|
||||
return null;
|
||||
}
|
||||
return new Callable(name, method, offsets[0], offsets[1]);
|
||||
}
|
||||
|
||||
private static int[] offsets(final Span span) {
|
||||
if (span == null || span.getEnd() <= span.getStart()) {
|
||||
return null;
|
||||
}
|
||||
return new int[] {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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.lexer.PbsToken;
|
||||
import p.studio.compiler.pbs.lexer.PbsTokenKind;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class PbsDocumentFormatter {
|
||||
private static final int INDENT_SPACES = 4;
|
||||
|
||||
private PbsDocumentFormatter() {
|
||||
}
|
||||
|
||||
public static String format(final String documentText) {
|
||||
final String text = documentText == null ? "" : documentText;
|
||||
if (text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
final ReadOnlyList<PbsToken> tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty());
|
||||
final ArrayList<SourceLine> lines = splitLines(text);
|
||||
final StringBuilder formatted = new StringBuilder(text.length());
|
||||
int depth = 0;
|
||||
for (final SourceLine line : lines) {
|
||||
if (isDocInterior(line, tokens)) {
|
||||
formatted.append(line.content()).append(line.ending());
|
||||
continue;
|
||||
}
|
||||
final int leadingClosers = leadingClosers(line, tokens);
|
||||
final int indent = Math.max(0, depth - leadingClosers);
|
||||
formatted.append(reindent(line.content(), indent)).append(line.ending());
|
||||
depth = depthAfter(line, tokens, depth);
|
||||
}
|
||||
return formatted.toString();
|
||||
}
|
||||
|
||||
private static boolean isDocInterior(final SourceLine line, final ReadOnlyList<PbsToken> tokens) {
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token.kind() != PbsTokenKind.DOC_TEXT_BLOCK) {
|
||||
continue;
|
||||
}
|
||||
if (line.startByte() > token.start() && line.endByte() <= token.end()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int leadingClosers(final SourceLine line, final ReadOnlyList<PbsToken> tokens) {
|
||||
int closers = 0;
|
||||
boolean seenCode = false;
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token.start() < line.startByte() || token.start() >= line.endByte()) {
|
||||
continue;
|
||||
}
|
||||
if (token.kind() == PbsTokenKind.COMMENT) {
|
||||
continue;
|
||||
}
|
||||
if (!seenCode && isCloser(token.kind())) {
|
||||
closers += 1;
|
||||
continue;
|
||||
}
|
||||
seenCode = true;
|
||||
}
|
||||
return closers;
|
||||
}
|
||||
|
||||
private static int depthAfter(
|
||||
final SourceLine line,
|
||||
final ReadOnlyList<PbsToken> tokens,
|
||||
final int depth) {
|
||||
int next = depth;
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token.start() < line.startByte() || token.start() >= line.endByte()) {
|
||||
continue;
|
||||
}
|
||||
if (isOpener(token.kind())) {
|
||||
next += 1;
|
||||
} else if (isCloser(token.kind())) {
|
||||
next = Math.max(0, next - 1);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private static boolean isOpener(final PbsTokenKind kind) {
|
||||
return kind == PbsTokenKind.LEFT_BRACE || kind == PbsTokenKind.LEFT_PAREN;
|
||||
}
|
||||
|
||||
private static boolean isCloser(final PbsTokenKind kind) {
|
||||
return kind == PbsTokenKind.RIGHT_BRACE || kind == PbsTokenKind.RIGHT_PAREN;
|
||||
}
|
||||
|
||||
private static String reindent(final String content, final int indent) {
|
||||
int index = 0;
|
||||
while (index < content.length()) {
|
||||
final char character = content.charAt(index);
|
||||
if (character != ' ' && character != '\t') {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if (index == content.length()) {
|
||||
return content;
|
||||
}
|
||||
return " ".repeat(indent * INDENT_SPACES) + content.substring(index);
|
||||
}
|
||||
|
||||
private static ArrayList<SourceLine> splitLines(final String text) {
|
||||
final ArrayList<SourceLine> lines = new ArrayList<>();
|
||||
final byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
||||
int charIndex = 0;
|
||||
int byteIndex = 0;
|
||||
int lineCharStart = 0;
|
||||
int lineByteStart = 0;
|
||||
while (charIndex < text.length()) {
|
||||
final int codePoint = text.codePointAt(charIndex);
|
||||
final int charWidth = Character.charCount(codePoint);
|
||||
final int byteWidth = utf8Length(codePoint);
|
||||
if (codePoint == '\n') {
|
||||
lines.add(new SourceLine(
|
||||
text.substring(lineCharStart, charIndex),
|
||||
"\n",
|
||||
lineByteStart,
|
||||
byteIndex));
|
||||
charIndex += charWidth;
|
||||
byteIndex += byteWidth;
|
||||
lineCharStart = charIndex;
|
||||
lineByteStart = byteIndex;
|
||||
continue;
|
||||
}
|
||||
if (codePoint == '\r' && charIndex + charWidth < text.length() && text.codePointAt(charIndex + charWidth) == '\n') {
|
||||
lines.add(new SourceLine(
|
||||
text.substring(lineCharStart, charIndex),
|
||||
"\r\n",
|
||||
lineByteStart,
|
||||
byteIndex));
|
||||
charIndex += charWidth + 1;
|
||||
byteIndex += byteWidth + 1;
|
||||
lineCharStart = charIndex;
|
||||
lineByteStart = byteIndex;
|
||||
continue;
|
||||
}
|
||||
charIndex += charWidth;
|
||||
byteIndex += byteWidth;
|
||||
}
|
||||
if (lineCharStart < text.length()) {
|
||||
lines.add(new SourceLine(
|
||||
text.substring(lineCharStart),
|
||||
"",
|
||||
lineByteStart,
|
||||
bytes.length));
|
||||
}
|
||||
if (lines.isEmpty()) {
|
||||
lines.add(new SourceLine(text, "", 0, bytes.length));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static int utf8Length(final int codePoint) {
|
||||
if (codePoint <= 0x7F) {
|
||||
return 1;
|
||||
}
|
||||
if (codePoint <= 0x7FF) {
|
||||
return 2;
|
||||
}
|
||||
if (codePoint <= 0xFFFF) {
|
||||
return 3;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
private record SourceLine(String content, String ending, int startByte, int endByte) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import p.studio.compiler.messages.FESurfaceContext;
|
||||
import p.studio.compiler.models.FrontendSemanticToken;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class PbsSemanticTokenOverlay {
|
||||
private static final Set<String> LEXICAL_KEYS = Set.of(
|
||||
PbsSemanticKind.COMMENT.semanticKey(),
|
||||
PbsSemanticKind.STRING.semanticKey(),
|
||||
PbsSemanticKind.NUMBER.semanticKey(),
|
||||
PbsSemanticKind.LITERAL.semanticKey(),
|
||||
PbsSemanticKind.KEYWORD.semanticKey(),
|
||||
PbsSemanticKind.OPERATOR.semanticKey(),
|
||||
PbsSemanticKind.PUNCTUATION.semanticKey());
|
||||
|
||||
private PbsSemanticTokenOverlay() {
|
||||
}
|
||||
|
||||
public static List<FrontendSemanticToken> apply(
|
||||
final List<FrontendSemanticToken> baseline,
|
||||
final String documentText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final FESurfaceContext feSurfaceContext,
|
||||
final PbsEditorialSupportService editorialSupportService) {
|
||||
if (baseline == null || baseline.isEmpty() || ast == null || editorialSupportService == null) {
|
||||
return baseline == null ? List.of() : List.copyOf(baseline);
|
||||
}
|
||||
final String text = documentText == null ? "" : documentText;
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementals = supplementalTopDecls == null
|
||||
? ReadOnlyList.empty()
|
||||
: supplementalTopDecls;
|
||||
final FESurfaceContext surface = feSurfaceContext == null ? FESurfaceContext.empty() : feSurfaceContext;
|
||||
final ArrayList<FrontendSemanticToken> overlaid = new ArrayList<>(baseline.size());
|
||||
for (final FrontendSemanticToken token : baseline) {
|
||||
overlaid.add(overlay(token, text, ast, supplementals, surface, editorialSupportService));
|
||||
}
|
||||
return List.copyOf(overlaid);
|
||||
}
|
||||
|
||||
private static FrontendSemanticToken overlay(
|
||||
final FrontendSemanticToken token,
|
||||
final String text,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementals,
|
||||
final FESurfaceContext surface,
|
||||
final PbsEditorialSupportService editorialSupportService) {
|
||||
if (token == null || LEXICAL_KEYS.contains(token.semanticKey())) {
|
||||
return token;
|
||||
}
|
||||
try {
|
||||
final String resolvedKey = editorialSupportService.hover(
|
||||
text,
|
||||
ast,
|
||||
supplementals,
|
||||
surface,
|
||||
token.startOffset())
|
||||
.map(PbsSemanticTokenOverlay::semanticKey)
|
||||
.orElse(null);
|
||||
if (resolvedKey == null || resolvedKey.equals(token.semanticKey())) {
|
||||
return token;
|
||||
}
|
||||
return new FrontendSemanticToken(token.startOffset(), token.endOffset(), resolvedKey);
|
||||
} catch (final RuntimeException ignored) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
private static String semanticKey(final PbsEditorialResolvedSymbol symbol) {
|
||||
if (symbol == null || symbol.kind() == null) {
|
||||
return null;
|
||||
}
|
||||
final PbsSemanticKind kind = switch (symbol.kind()) {
|
||||
case FUNCTION -> PbsSemanticKind.FUNCTION;
|
||||
case METHOD -> PbsSemanticKind.METHOD;
|
||||
case CONSTRUCTOR -> PbsSemanticKind.CONSTRUCTOR;
|
||||
case STRUCT -> PbsSemanticKind.STRUCT;
|
||||
case BUILTIN_TYPE -> PbsSemanticKind.BUILTIN_TYPE;
|
||||
case SERVICE -> PbsSemanticKind.SERVICE;
|
||||
case HOST -> PbsSemanticKind.HOST;
|
||||
case CONTRACT -> PbsSemanticKind.CONTRACT;
|
||||
case CALLBACK -> PbsSemanticKind.CALLBACK;
|
||||
case ENUM -> PbsSemanticKind.ENUM;
|
||||
case ERROR -> PbsSemanticKind.ERROR;
|
||||
case GLOBAL -> PbsSemanticKind.GLOBAL;
|
||||
case CONST -> PbsSemanticKind.CONST;
|
||||
case KEYWORD, LOCAL, PARAMETER, FIELD -> null;
|
||||
};
|
||||
return kind == null ? null : kind.semanticKey();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,380 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.lexer.PbsToken;
|
||||
import p.studio.compiler.pbs.lexer.PbsTokenKind;
|
||||
import p.studio.compiler.pbs.parser.PbsParser;
|
||||
import p.studio.compiler.services.FrontendFoldingRange;
|
||||
import p.studio.compiler.services.FrontendSelectionRange;
|
||||
import p.studio.compiler.source.Span;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
public final class PbsStructureRanges {
|
||||
private PbsStructureRanges() {
|
||||
}
|
||||
|
||||
public static List<FrontendFoldingRange> foldingRanges(final String documentText) {
|
||||
final String text = documentText == null ? "" : documentText;
|
||||
final ReadOnlyList<PbsToken> tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty());
|
||||
final PbsAst.File ast = PbsParser.parse(tokens, FileId.none(), DiagnosticSink.empty());
|
||||
return foldingRanges(text, ast, tokens);
|
||||
}
|
||||
|
||||
public static List<FrontendSelectionRange> selectionRanges(
|
||||
final String documentText,
|
||||
final List<Integer> offsets) {
|
||||
final String text = documentText == null ? "" : documentText;
|
||||
final ReadOnlyList<PbsToken> tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty());
|
||||
final PbsAst.File ast = PbsParser.parse(tokens, FileId.none(), DiagnosticSink.empty());
|
||||
if (offsets == null || offsets.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final Collected collected = collect(ast);
|
||||
final List<SpanPair> delimiters = delimiters(tokens, textEnd(text));
|
||||
final ArrayList<FrontendSelectionRange> ranges = new ArrayList<>();
|
||||
for (final Integer offset : offsets) {
|
||||
if (offset == null || offset < 0) {
|
||||
continue;
|
||||
}
|
||||
final FrontendSelectionRange range = selectionAt(offset, tokens, collected, delimiters);
|
||||
if (range != null) {
|
||||
ranges.add(range);
|
||||
}
|
||||
}
|
||||
return List.copyOf(ranges);
|
||||
}
|
||||
|
||||
private static List<FrontendFoldingRange> foldingRanges(
|
||||
final String text,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsToken> tokens) {
|
||||
final Collected collected = collect(ast);
|
||||
final LinkedHashSet<String> emitted = new LinkedHashSet<>();
|
||||
final ArrayList<SpanPair> ranges = new ArrayList<>();
|
||||
for (final SpanPair span : collected.folding()) {
|
||||
addRange(ranges, emitted, span);
|
||||
}
|
||||
for (final SpanPair delimiter : delimiters(tokens, textEnd(text))) {
|
||||
if (collected.coveredKeys().contains(delimiter.key())) {
|
||||
continue;
|
||||
}
|
||||
addRange(ranges, emitted, delimiter);
|
||||
}
|
||||
ranges.sort(Comparator.comparingInt(SpanPair::start).thenComparingInt(SpanPair::end));
|
||||
return ranges.stream()
|
||||
.map(span -> new FrontendFoldingRange(span.start(), span.end()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static FrontendSelectionRange selectionAt(
|
||||
final int offset,
|
||||
final ReadOnlyList<PbsToken> tokens,
|
||||
final Collected collected,
|
||||
final List<SpanPair> delimiters) {
|
||||
final SpanPair identifier = smallest(identifierSpans(tokens), offset);
|
||||
final SpanPair parameters = smallest(delimiters, offset);
|
||||
final SpanPair block = smallest(collected.blocks(), offset);
|
||||
final SpanPair declaration = smallest(collected.declarations(), offset);
|
||||
FrontendSelectionRange node = null;
|
||||
SpanPair previous = null;
|
||||
for (final SpanPair layer : new SpanPair[] {declaration, block, parameters, identifier}) {
|
||||
if (layer == null) {
|
||||
continue;
|
||||
}
|
||||
if (previous != null && previous.start() == layer.start() && previous.end() == layer.end()) {
|
||||
continue;
|
||||
}
|
||||
node = new FrontendSelectionRange(layer.start(), layer.end(), node);
|
||||
previous = layer;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private static Collected collect(final PbsAst.File ast) {
|
||||
final Collected collected = new Collected();
|
||||
if (ast == null || ast.topDecls() == null) {
|
||||
return collected;
|
||||
}
|
||||
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
|
||||
visitTop(topDecl, true, collected);
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
private static void visitTop(
|
||||
final PbsAst.TopDecl topDecl,
|
||||
final boolean topLevel,
|
||||
final Collected collected) {
|
||||
if (topDecl == null) {
|
||||
return;
|
||||
}
|
||||
if (topLevel && isListedDeclaration(topDecl)) {
|
||||
addSpan(collected.folding(), topDecl.span());
|
||||
}
|
||||
if (isListedDeclaration(topDecl) || topDecl instanceof PbsAst.FunctionDecl) {
|
||||
addSpan(collected.declarations(), topDecl.span());
|
||||
}
|
||||
switch (topDecl) {
|
||||
case PbsAst.FunctionDecl functionDecl -> visitFunction(functionDecl, collected);
|
||||
case PbsAst.StructDecl structDecl -> {
|
||||
visitAttributes(structDecl.attributes(), collected);
|
||||
if (structDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
|
||||
visitTop(method, false, collected);
|
||||
}
|
||||
}
|
||||
if (structDecl.ctors() != null) {
|
||||
for (final PbsAst.CtorDecl ctor : structDecl.ctors()) {
|
||||
visitCtor(ctor, collected);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.ServiceDecl serviceDecl -> {
|
||||
visitAttributes(serviceDecl.attributes(), collected);
|
||||
if (serviceDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
|
||||
visitTop(method, false, collected);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.ContractDecl contractDecl -> visitAttributes(contractDecl.attributes(), collected);
|
||||
case PbsAst.HostDecl hostDecl -> visitAttributes(hostDecl.attributes(), collected);
|
||||
case PbsAst.EnumDecl enumDecl -> visitAttributes(enumDecl.attributes(), collected);
|
||||
case PbsAst.ErrorDecl errorDecl -> visitAttributes(errorDecl.attributes(), collected);
|
||||
case PbsAst.CallbackDecl callbackDecl -> visitAttributes(callbackDecl.attributes(), collected);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void visitFunction(
|
||||
final PbsAst.FunctionDecl functionDecl,
|
||||
final Collected collected) {
|
||||
visitAttributes(functionDecl.attributes(), collected);
|
||||
coverBlock(functionDecl.body(), true, collected);
|
||||
}
|
||||
|
||||
private static void visitCtor(
|
||||
final PbsAst.CtorDecl ctor,
|
||||
final Collected collected) {
|
||||
if (ctor == null) {
|
||||
return;
|
||||
}
|
||||
addSpan(collected.declarations(), ctor.span());
|
||||
coverBlock(ctor.body(), true, collected);
|
||||
}
|
||||
|
||||
private static void visitAttributes(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final Collected collected) {
|
||||
if (attributes == null) {
|
||||
return;
|
||||
}
|
||||
for (final PbsAst.Attribute attribute : attributes) {
|
||||
if (attribute == null || attribute.arguments() == null) {
|
||||
continue;
|
||||
}
|
||||
for (final PbsAst.AttributeArgument argument : attribute.arguments()) {
|
||||
if (argument != null && argument.value() instanceof PbsAst.AttributeDocTextBlockValue doc) {
|
||||
addSpan(collected.folding(), doc.span());
|
||||
addSpan(collected.blocks(), doc.span());
|
||||
collected.cover(doc.span());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void coverBlock(
|
||||
final PbsAst.Block block,
|
||||
final boolean emit,
|
||||
final Collected collected) {
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
if (emit) {
|
||||
addSpan(collected.folding(), block.span());
|
||||
}
|
||||
addSpan(collected.blocks(), block.span());
|
||||
collected.cover(block.span());
|
||||
if (block.statements() == null) {
|
||||
return;
|
||||
}
|
||||
for (final PbsAst.Statement statement : block.statements()) {
|
||||
coverStatement(statement, collected);
|
||||
}
|
||||
}
|
||||
|
||||
private static void coverStatement(
|
||||
final PbsAst.Statement statement,
|
||||
final Collected collected) {
|
||||
switch (statement) {
|
||||
case null -> {
|
||||
}
|
||||
case PbsAst.IfStatement ifStatement -> {
|
||||
coverBlock(ifStatement.thenBlock(), false, collected);
|
||||
coverBlock(ifStatement.elseBlock(), false, collected);
|
||||
coverStatement(ifStatement.elseIf(), collected);
|
||||
}
|
||||
case PbsAst.ForStatement forStatement -> coverBlock(forStatement.body(), false, collected);
|
||||
case PbsAst.WhileStatement whileStatement -> coverBlock(whileStatement.body(), false, collected);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isListedDeclaration(final PbsAst.TopDecl topDecl) {
|
||||
return topDecl instanceof PbsAst.FunctionDecl
|
||||
|| topDecl instanceof PbsAst.StructDecl
|
||||
|| topDecl instanceof PbsAst.ServiceDecl
|
||||
|| topDecl instanceof PbsAst.ContractDecl
|
||||
|| topDecl instanceof PbsAst.HostDecl
|
||||
|| topDecl instanceof PbsAst.EnumDecl
|
||||
|| topDecl instanceof PbsAst.ErrorDecl
|
||||
|| topDecl instanceof PbsAst.CallbackDecl;
|
||||
}
|
||||
|
||||
private static List<SpanPair> delimiters(
|
||||
final ReadOnlyList<PbsToken> tokens,
|
||||
final int textEnd) {
|
||||
final ArrayList<PbsToken> stack = new ArrayList<>();
|
||||
final ArrayList<SpanPair> pairs = new ArrayList<>();
|
||||
if (tokens == null) {
|
||||
return pairs;
|
||||
}
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
if (token.kind() == PbsTokenKind.LEFT_BRACE || token.kind() == PbsTokenKind.LEFT_PAREN) {
|
||||
stack.add(token);
|
||||
continue;
|
||||
}
|
||||
if (token.kind() == PbsTokenKind.DOC_TEXT_BLOCK) {
|
||||
addPair(pairs, token.start(), token.end());
|
||||
continue;
|
||||
}
|
||||
if (token.kind() == PbsTokenKind.RIGHT_BRACE || token.kind() == PbsTokenKind.RIGHT_PAREN) {
|
||||
final PbsTokenKind openKind = token.kind() == PbsTokenKind.RIGHT_BRACE
|
||||
? PbsTokenKind.LEFT_BRACE
|
||||
: PbsTokenKind.LEFT_PAREN;
|
||||
if (!stack.isEmpty() && stack.getLast().kind() == openKind) {
|
||||
final PbsToken open = stack.removeLast();
|
||||
addPair(pairs, open.start(), token.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final PbsToken open : stack) {
|
||||
addPair(pairs, open.start(), textEnd);
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
private static List<SpanPair> identifierSpans(final ReadOnlyList<PbsToken> tokens) {
|
||||
final ArrayList<SpanPair> spans = new ArrayList<>();
|
||||
if (tokens == null) {
|
||||
return spans;
|
||||
}
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token != null && token.kind() == PbsTokenKind.IDENTIFIER) {
|
||||
addPair(spans, token.start(), token.end());
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
private static SpanPair smallest(final List<SpanPair> spans, final int offset) {
|
||||
SpanPair best = null;
|
||||
for (final SpanPair span : spans) {
|
||||
if (span.start() > offset || offset >= span.end()) {
|
||||
continue;
|
||||
}
|
||||
if (best == null
|
||||
|| span.end() - span.start() < best.end() - best.start()
|
||||
|| (span.end() - span.start() == best.end() - best.start() && span.start() > best.start())) {
|
||||
best = span;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static void addSpan(final List<SpanPair> spans, final Span span) {
|
||||
if (span == null) {
|
||||
return;
|
||||
}
|
||||
addPair(spans, toOffset(span.getStart()), toOffset(span.getEnd()));
|
||||
}
|
||||
|
||||
private static void addPair(final List<SpanPair> spans, final int start, final int end) {
|
||||
if (start < 0 || end <= start) {
|
||||
return;
|
||||
}
|
||||
spans.add(new SpanPair(start, end));
|
||||
}
|
||||
|
||||
private static void addRange(
|
||||
final List<SpanPair> ranges,
|
||||
final LinkedHashSet<String> emitted,
|
||||
final SpanPair span) {
|
||||
if (span == null || !emitted.add(span.key())) {
|
||||
return;
|
||||
}
|
||||
ranges.add(span);
|
||||
}
|
||||
|
||||
private static int textEnd(final String text) {
|
||||
return text.getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
private static int toOffset(final long value) {
|
||||
if (value <= 0L) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.min(Integer.MAX_VALUE, value);
|
||||
}
|
||||
|
||||
private record SpanPair(int start, int end) {
|
||||
private String key() {
|
||||
return start + ":" + end;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Collected {
|
||||
private final ArrayList<SpanPair> folding = new ArrayList<>();
|
||||
private final ArrayList<SpanPair> covered = new ArrayList<>();
|
||||
private final ArrayList<SpanPair> blocks = new ArrayList<>();
|
||||
private final ArrayList<SpanPair> declarations = new ArrayList<>();
|
||||
|
||||
private List<SpanPair> folding() {
|
||||
return folding;
|
||||
}
|
||||
|
||||
private void cover(final Span span) {
|
||||
addSpan(covered, span);
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> coveredKeys() {
|
||||
final LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
for (final SpanPair span : covered) {
|
||||
keys.add(span.key());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private List<SpanPair> blocks() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private List<SpanPair> declarations() {
|
||||
return declarations;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,32 @@ public final class PbsLexer {
|
||||
return lexer.lexInternal();
|
||||
}
|
||||
|
||||
public static boolean isKeyword(final String text) {
|
||||
return text != null && KEYWORDS.containsKey(text);
|
||||
}
|
||||
|
||||
public static boolean isIdentifierLexeme(final String text) {
|
||||
if (text == null || text.isEmpty() || isKeyword(text)) {
|
||||
return false;
|
||||
}
|
||||
int index = 0;
|
||||
final int first = text.codePointAt(0);
|
||||
if (!(first == '_' || Character.isAlphabetic(first))) {
|
||||
return false;
|
||||
}
|
||||
index += Character.charCount(first);
|
||||
while (index < text.length()) {
|
||||
final int codePoint = text.codePointAt(index);
|
||||
if (!(codePoint == '_'
|
||||
|| Character.isAlphabetic(codePoint)
|
||||
|| Character.isDigit(codePoint))) {
|
||||
return false;
|
||||
}
|
||||
index += Character.charCount(codePoint);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private ReadOnlyList<PbsToken> lexInternal() {
|
||||
while (!isAtEnd() || state != LexerState.DEFAULT) {
|
||||
switch (state) {
|
||||
|
||||
@ -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";
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
final class PbsAstWorkspaceSymbols {
|
||||
private PbsAstWorkspaceSymbols() {
|
||||
}
|
||||
|
||||
static List<PbsEditorialWorkspaceSymbol> list(
|
||||
final PbsAst.File ast,
|
||||
final String query) {
|
||||
if (ast == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<PbsEditorialWorkspaceSymbol> symbols = new ArrayList<>();
|
||||
for (final PbsEditorialDocumentSymbol topDecl : PbsAstDocumentSymbols.list(ast)) {
|
||||
flatten(topDecl, "", query, symbols);
|
||||
}
|
||||
return List.copyOf(symbols);
|
||||
}
|
||||
|
||||
private static void flatten(
|
||||
final PbsEditorialDocumentSymbol symbol,
|
||||
final String containerName,
|
||||
final String query,
|
||||
final ArrayList<PbsEditorialWorkspaceSymbol> symbols) {
|
||||
if (matches(symbol.name(), query)) {
|
||||
symbols.add(new PbsEditorialWorkspaceSymbol(
|
||||
symbol.name(),
|
||||
symbol.kind(),
|
||||
symbol.detail(),
|
||||
containerName,
|
||||
symbol.span()));
|
||||
}
|
||||
for (final PbsEditorialDocumentSymbol child : symbol.children()) {
|
||||
flatten(child, symbol.name(), query, symbols);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean matches(
|
||||
final String name,
|
||||
final String query) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
if (query == null || query.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
return name.toLowerCase(Locale.ROOT).contains(query.trim().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record PbsEditorialRename(
|
||||
String refusal,
|
||||
PbsEditorialLocation placeholder,
|
||||
List<PbsEditorialLocation> identity,
|
||||
List<PbsEditorialLocation> sites) {
|
||||
|
||||
public PbsEditorialRename {
|
||||
refusal = refusal == null ? "" : refusal;
|
||||
identity = identity == null ? List.of() : List.copyOf(identity);
|
||||
sites = sites == null ? List.of() : List.copyOf(sites);
|
||||
}
|
||||
|
||||
public static PbsEditorialRename refused(final String reason) {
|
||||
return new PbsEditorialRename(
|
||||
reason == null || reason.isBlank() ? "not renameable" : reason,
|
||||
null,
|
||||
List.of(),
|
||||
List.of());
|
||||
}
|
||||
|
||||
public static PbsEditorialRename applied(
|
||||
final PbsEditorialLocation placeholder,
|
||||
final List<PbsEditorialLocation> identity,
|
||||
final List<PbsEditorialLocation> sites) {
|
||||
return new PbsEditorialRename(
|
||||
"",
|
||||
Objects.requireNonNull(placeholder, "placeholder"),
|
||||
identity,
|
||||
sites);
|
||||
}
|
||||
|
||||
public boolean allowed() {
|
||||
return refusal.isBlank() && placeholder != null && !identity.isEmpty() && !sites.isEmpty();
|
||||
}
|
||||
}
|
||||
@ -74,6 +74,16 @@ public final class PbsEditorialSupportService {
|
||||
return context.resolveSymbolHover(token.lexeme(), tokenIndex, scopeState);
|
||||
}
|
||||
|
||||
public List<PbsEditorialDocumentSymbol> documentSymbols(final PbsAst.File ast) {
|
||||
return PbsAstDocumentSymbols.list(ast);
|
||||
}
|
||||
|
||||
public List<PbsEditorialWorkspaceSymbol> workspaceSymbols(
|
||||
final PbsAst.File ast,
|
||||
final String query) {
|
||||
return PbsAstWorkspaceSymbols.list(ast, query);
|
||||
}
|
||||
|
||||
public List<PbsEditorialLocation> definition(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
@ -145,6 +155,63 @@ public final class PbsEditorialSupportService {
|
||||
return List.copyOf(usages.values());
|
||||
}
|
||||
|
||||
public Optional<PbsEditorialLocation> prepareRename(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final int offset) {
|
||||
return prepareRename(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset);
|
||||
}
|
||||
|
||||
public Optional<PbsEditorialLocation> prepareRename(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final FESurfaceContext feSurfaceContext,
|
||||
final int offset) {
|
||||
final EditorialDocument cursor = new EditorialDocument(sourceText, ast, supplementalTopDecls, feSurfaceContext);
|
||||
return prepareRename(cursor, offset, List.of(cursor));
|
||||
}
|
||||
|
||||
public Optional<PbsEditorialLocation> prepareRename(
|
||||
final EditorialDocument cursorDocument,
|
||||
final int offset,
|
||||
final List<EditorialDocument> projectDocuments) {
|
||||
final PbsEditorialRename planned = planRename(cursorDocument, offset, projectDocuments, null);
|
||||
if (!planned.allowed()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(planned.placeholder());
|
||||
}
|
||||
|
||||
public PbsEditorialRename rename(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
return rename(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset, newName);
|
||||
}
|
||||
|
||||
public PbsEditorialRename rename(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final FESurfaceContext feSurfaceContext,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
final EditorialDocument cursor = new EditorialDocument(sourceText, ast, supplementalTopDecls, feSurfaceContext);
|
||||
return rename(cursor, offset, newName, List.of(cursor));
|
||||
}
|
||||
|
||||
public PbsEditorialRename rename(
|
||||
final EditorialDocument cursorDocument,
|
||||
final int offset,
|
||||
final String newName,
|
||||
final List<EditorialDocument> projectDocuments) {
|
||||
return planRename(cursorDocument, offset, projectDocuments, newName);
|
||||
}
|
||||
|
||||
public record EditorialDocument(
|
||||
String sourceText,
|
||||
PbsAst.File ast,
|
||||
@ -192,6 +259,76 @@ public final class PbsEditorialSupportService {
|
||||
return Optional.of(new PbsEditorialSignatureHelp(signatures, 0, callSite.activeParameter()));
|
||||
}
|
||||
|
||||
private PbsEditorialRename planRename(
|
||||
final EditorialDocument cursorDocument,
|
||||
final int offset,
|
||||
final List<EditorialDocument> projectDocuments,
|
||||
final String newName) {
|
||||
if (cursorDocument == null || cursorDocument.ast() == null) {
|
||||
return PbsEditorialRename.refused("not renameable");
|
||||
}
|
||||
final DocumentContext cursor = buildContext(
|
||||
cursorDocument.sourceText(),
|
||||
cursorDocument.ast(),
|
||||
cursorDocument.supplementalTopDecls(),
|
||||
cursorDocument.feSurfaceContext());
|
||||
final RenameCursor renameCursor = cursor.renameCursorAt(offset);
|
||||
if (renameCursor.refusal() != null) {
|
||||
return PbsEditorialRename.refused(renameCursor.refusal());
|
||||
}
|
||||
if (newName != null) {
|
||||
if (!PbsLexer.isIdentifierLexeme(newName)) {
|
||||
return PbsEditorialRename.refused("invalid identifier");
|
||||
}
|
||||
if (cursor.collides(newName, renameCursor)) {
|
||||
return PbsEditorialRename.refused("name collision");
|
||||
}
|
||||
}
|
||||
final List<EditorialDocument> documents = projectDocuments == null || projectDocuments.isEmpty()
|
||||
? List.of(cursorDocument)
|
||||
: projectDocuments;
|
||||
final LinkedHashMap<String, PbsEditorialLocation> sites = new LinkedHashMap<>();
|
||||
sites.putIfAbsent(locationKey(renameCursor.placeholder().span()), renameCursor.placeholder());
|
||||
final Set<String> identityKeys = locationKeys(renameCursor.identity());
|
||||
for (final EditorialDocument document : documents) {
|
||||
if (document == null || document.ast() == null) {
|
||||
continue;
|
||||
}
|
||||
final DocumentContext documentContext = buildContext(
|
||||
document.sourceText(),
|
||||
document.ast(),
|
||||
document.supplementalTopDecls(),
|
||||
document.feSurfaceContext());
|
||||
for (final PbsEditorialLocation site : documentContext.renameSitesMatching(
|
||||
identityKeys,
|
||||
renameCursor.originalName())) {
|
||||
sites.putIfAbsent(locationKey(site.span()), site);
|
||||
}
|
||||
final FileId documentFileId = document.ast().span() == null
|
||||
? FileId.none()
|
||||
: document.ast().span().getFileId();
|
||||
for (final PbsEditorialLocation location : renameCursor.identity()) {
|
||||
if (location == null || location.span() == null || location.span().isNone()) {
|
||||
continue;
|
||||
}
|
||||
if (location.span().length() != renameCursor.originalName().length()) {
|
||||
continue;
|
||||
}
|
||||
if (location.span().getFileId() == null || !location.span().getFileId().equals(documentFileId)) {
|
||||
continue;
|
||||
}
|
||||
sites.putIfAbsent(locationKey(location.span()), location);
|
||||
}
|
||||
}
|
||||
if (sites.isEmpty()) {
|
||||
return PbsEditorialRename.refused("not renameable");
|
||||
}
|
||||
return PbsEditorialRename.applied(
|
||||
renameCursor.placeholder(),
|
||||
renameCursor.identity(),
|
||||
List.copyOf(sites.values()));
|
||||
}
|
||||
|
||||
private DocumentContext buildContext(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
@ -1489,6 +1626,332 @@ public final class PbsEditorialSupportService {
|
||||
throw new IndexOutOfBoundsException("No significant token at index " + significantIndex);
|
||||
}
|
||||
|
||||
private RenameCursor renameCursorAt(final int offset) {
|
||||
final Integer tokenIndex = identifierTokenIndexAt(offset);
|
||||
if (tokenIndex == null) {
|
||||
return RenameCursor.refused("not an identifier");
|
||||
}
|
||||
final PbsToken token = token(tokenIndex);
|
||||
final String lexeme = token.lexeme();
|
||||
if (isImportAliasLexeme(lexeme)) {
|
||||
return RenameCursor.refused("import alias");
|
||||
}
|
||||
List<PbsEditorialLocation> identity = definitionAt(offset);
|
||||
if (identity.isEmpty()) {
|
||||
identity = localLocations(lexeme, offset);
|
||||
}
|
||||
if (identity.isEmpty()) {
|
||||
identity = parameterLocations(lexeme, offset);
|
||||
}
|
||||
if (isErrorCaseLabel(lexeme, offset, identity)) {
|
||||
return RenameCursor.refused("error case label");
|
||||
}
|
||||
if (identity.isEmpty()) {
|
||||
return RenameCursor.refused("not renameable");
|
||||
}
|
||||
final FileId fileId = ast.span() == null ? FileId.none() : ast.span().getFileId();
|
||||
final Span placeholderSpan = new Span(fileId, token.start(), token.end());
|
||||
if (!hasPhysicalSpan(placeholderSpan)) {
|
||||
return RenameCursor.refused("not renameable");
|
||||
}
|
||||
return new RenameCursor(
|
||||
lexeme,
|
||||
new PbsEditorialLocation(placeholderSpan),
|
||||
identity,
|
||||
null);
|
||||
}
|
||||
|
||||
private List<PbsEditorialLocation> renameSitesMatching(
|
||||
final Set<String> identityKeys,
|
||||
final String originalName) {
|
||||
if (identityKeys == null || identityKeys.isEmpty() || originalName == null || originalName.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
final FileId fileId = ast.span() == null ? FileId.none() : ast.span().getFileId();
|
||||
final LinkedHashMap<String, PbsEditorialLocation> sites = new LinkedHashMap<>();
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token.kind() != PbsTokenKind.IDENTIFIER || !originalName.equals(token.lexeme())) {
|
||||
continue;
|
||||
}
|
||||
if (isImportAliasLexeme(token.lexeme())) {
|
||||
continue;
|
||||
}
|
||||
if (!intersects(definitionAt(token.start()), identityKeys)) {
|
||||
continue;
|
||||
}
|
||||
final Span usageSpan = new Span(fileId, token.start(), token.end());
|
||||
if (!hasPhysicalSpan(usageSpan)) {
|
||||
continue;
|
||||
}
|
||||
sites.putIfAbsent(locationKey(usageSpan), new PbsEditorialLocation(usageSpan));
|
||||
}
|
||||
return List.copyOf(sites.values());
|
||||
}
|
||||
|
||||
private boolean isImportAliasLexeme(final String lexeme) {
|
||||
if (lexeme == null || lexeme.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
for (final var importDecl : ast.imports()) {
|
||||
for (final var item : importDecl.items()) {
|
||||
if (item.alias() == null || item.alias().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (lexeme.equals(item.alias()) && !item.alias().equals(item.name())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isErrorCaseLabel(
|
||||
final String lexeme,
|
||||
final int offset,
|
||||
final List<PbsEditorialLocation> identity) {
|
||||
if (lexeme == null || lexeme.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.ErrorDecl errorDecl)) {
|
||||
continue;
|
||||
}
|
||||
if (lexeme.equals(errorDecl.name()) || !errorDecl.cases().asList().contains(lexeme)) {
|
||||
continue;
|
||||
}
|
||||
if (errorDecl.span() != null && errorDecl.span().contains(offset)) {
|
||||
return true;
|
||||
}
|
||||
final String errorKey = locationKey(errorDecl.span());
|
||||
for (final PbsEditorialLocation location : identity) {
|
||||
if (location != null && location.span() != null && errorKey.equals(locationKey(location.span()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean collides(
|
||||
final String newName,
|
||||
final RenameCursor renameCursor) {
|
||||
if (newName == null || newName.equals(renameCursor.originalName())) {
|
||||
return false;
|
||||
}
|
||||
if (isLocalOrParameterIdentity(renameCursor)) {
|
||||
return localBindingNames((int) renameCursor.placeholder().span().getStart()).contains(newName);
|
||||
}
|
||||
if (isStructFieldIdentity(renameCursor)) {
|
||||
return structFieldNames(renameCursor).contains(newName);
|
||||
}
|
||||
if (isMethodIdentity(renameCursor)) {
|
||||
return methodNames(renameCursor).contains(newName);
|
||||
}
|
||||
if (isEnumCaseIdentity(renameCursor)) {
|
||||
return enumCaseNames(renameCursor).contains(newName);
|
||||
}
|
||||
return topLevelBindingNames().contains(newName);
|
||||
}
|
||||
|
||||
private boolean isLocalOrParameterIdentity(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.FunctionDecl functionDecl) || functionDecl.body() == null) {
|
||||
continue;
|
||||
}
|
||||
for (final var parameter : functionDecl.parameters()) {
|
||||
if (keys.contains(locationKey(parameter.span()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (containsLetNameSpan(functionDecl.body(), keys)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var method : structDecl.methods()) {
|
||||
if (method.body() == null) {
|
||||
continue;
|
||||
}
|
||||
for (final var parameter : method.parameters()) {
|
||||
if (keys.contains(locationKey(parameter.span()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (containsLetNameSpan(method.body(), keys)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsLetNameSpan(
|
||||
final PbsAst.Block block,
|
||||
final Set<String> keys) {
|
||||
if (block == null) {
|
||||
return false;
|
||||
}
|
||||
for (final var statement : block.statements()) {
|
||||
if (statement instanceof PbsAst.LetStatement letStatement
|
||||
&& letStatement.nameSpan() != null
|
||||
&& keys.contains(locationKey(letStatement.nameSpan()))) {
|
||||
return true;
|
||||
}
|
||||
if (statement instanceof PbsAst.IfStatement ifStatement) {
|
||||
if (containsLetNameSpan(ifStatement.thenBlock(), keys)
|
||||
|| containsLetNameSpan(ifStatement.elseBlock(), keys)) {
|
||||
return true;
|
||||
}
|
||||
} else if (statement instanceof PbsAst.ForStatement forStatement) {
|
||||
if (containsLetNameSpan(forStatement.body(), keys)) {
|
||||
return true;
|
||||
}
|
||||
} else if (statement instanceof PbsAst.WhileStatement whileStatement
|
||||
&& containsLetNameSpan(whileStatement.body(), keys)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Set<String> localBindingNames(final int offset) {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
final CallableFrame frame = callableFrameAt(offset);
|
||||
if (frame == null) {
|
||||
return Set.of();
|
||||
}
|
||||
names.addAll(frame.parameterNames());
|
||||
collectLetNames(frame.body(), names);
|
||||
return names;
|
||||
}
|
||||
|
||||
private void collectLetNames(
|
||||
final PbsAst.Block block,
|
||||
final Set<String> names) {
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
for (final var statement : block.statements()) {
|
||||
if (statement instanceof PbsAst.LetStatement letStatement) {
|
||||
names.add(letStatement.name());
|
||||
} else if (statement instanceof PbsAst.IfStatement ifStatement) {
|
||||
collectLetNames(ifStatement.thenBlock(), names);
|
||||
collectLetNames(ifStatement.elseBlock(), names);
|
||||
} else if (statement instanceof PbsAst.ForStatement forStatement) {
|
||||
names.add(forStatement.iteratorName());
|
||||
collectLetNames(forStatement.body(), names);
|
||||
} else if (statement instanceof PbsAst.WhileStatement whileStatement) {
|
||||
collectLetNames(whileStatement.body(), names);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStructFieldIdentity(final RenameCursor renameCursor) {
|
||||
return !structFieldNames(renameCursor).isEmpty()
|
||||
&& structFieldIdentityMatch(renameCursor);
|
||||
}
|
||||
|
||||
private boolean structFieldIdentityMatch(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var field : structDecl.fields()) {
|
||||
if (keys.contains(locationKey(field.span()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Set<String> structFieldNames(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var field : structDecl.fields()) {
|
||||
if (keys.contains(locationKey(field.span()))) {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (final var sibling : structDecl.fields()) {
|
||||
names.add(sibling.name());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
private boolean isMethodIdentity(final RenameCursor renameCursor) {
|
||||
return !methodNames(renameCursor).isEmpty();
|
||||
}
|
||||
|
||||
private Set<String> methodNames(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var method : structDecl.methods()) {
|
||||
if (keys.contains(locationKey(method.span()))) {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (final var sibling : structDecl.methods()) {
|
||||
names.add(sibling.name());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
private boolean isEnumCaseIdentity(final RenameCursor renameCursor) {
|
||||
return !enumCaseNames(renameCursor).isEmpty();
|
||||
}
|
||||
|
||||
private Set<String> enumCaseNames(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.EnumDecl enumDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var enumCase : enumDecl.cases()) {
|
||||
if (keys.contains(locationKey(enumCase.span()))) {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (final var sibling : enumDecl.cases()) {
|
||||
names.add(sibling.name());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
private Set<String> topLevelBindingNames() {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
|
||||
names.add(functionDecl.name());
|
||||
} else if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
|
||||
names.add(serviceDecl.name());
|
||||
} else if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
|
||||
names.add(globalDecl.name());
|
||||
} else if (topDecl instanceof PbsAst.ConstDecl constDecl) {
|
||||
names.add(constDecl.name());
|
||||
}
|
||||
}
|
||||
names.addAll(importedSymbols.keySet());
|
||||
return names;
|
||||
}
|
||||
|
||||
private PbsToken token(final int index) {
|
||||
return tokens.get(index);
|
||||
}
|
||||
@ -1697,6 +2160,17 @@ public final class PbsEditorialSupportService {
|
||||
String origin) {
|
||||
}
|
||||
|
||||
private record RenameCursor(
|
||||
String originalName,
|
||||
PbsEditorialLocation placeholder,
|
||||
List<PbsEditorialLocation> identity,
|
||||
String refusal) {
|
||||
|
||||
private static RenameCursor refused(final String reason) {
|
||||
return new RenameCursor("", null, List.of(), reason);
|
||||
}
|
||||
}
|
||||
|
||||
private record CallableFrame(
|
||||
ReadOnlyList<PbsAst.Parameter> parameters,
|
||||
PbsAst.Block body,
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import p.studio.compiler.source.Span;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record PbsEditorialWorkspaceSymbol(
|
||||
String name,
|
||||
PbsEditorialSymbolKind kind,
|
||||
String detail,
|
||||
String containerName,
|
||||
Span span) {
|
||||
|
||||
public PbsEditorialWorkspaceSymbol {
|
||||
name = Objects.requireNonNull(name, "name").trim();
|
||||
kind = Objects.requireNonNull(kind, "kind");
|
||||
detail = detail == null ? "" : detail;
|
||||
containerName = containerName == null ? "" : containerName.trim();
|
||||
span = Objects.requireNonNull(span, "span");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record PbsQuickFix(
|
||||
String diagnosticCode,
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
String newText,
|
||||
String title) {
|
||||
|
||||
public PbsQuickFix {
|
||||
diagnosticCode = requireText(diagnosticCode, "diagnosticCode");
|
||||
title = requireText(title, "title");
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
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.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public final class PbsQuickFixCollector {
|
||||
static final String REMOVE_DUPLICATE_TITLE = "Remove duplicate reserved attribute";
|
||||
static final String REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE = "Remove InitAllowed arguments";
|
||||
private static final String INIT_ALLOWED_REPLACEMENT = "[InitAllowed]";
|
||||
|
||||
private PbsQuickFixCollector() {
|
||||
}
|
||||
|
||||
public static List<PbsQuickFix> collect(
|
||||
final PbsAst.File ast,
|
||||
final boolean interfaceModule) {
|
||||
if (ast == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<PbsQuickFix> repairs = new ArrayList<>();
|
||||
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
|
||||
switch (topDecl) {
|
||||
case PbsAst.FunctionDecl functionDecl -> duplicateDoc(functionDecl.attributes(), repairs);
|
||||
case PbsAst.StructDecl structDecl -> {
|
||||
duplicateDoc(structDecl.attributes(), repairs);
|
||||
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
|
||||
duplicateDoc(method.attributes(), repairs);
|
||||
}
|
||||
}
|
||||
case PbsAst.ServiceDecl serviceDecl -> {
|
||||
duplicateDoc(serviceDecl.attributes(), repairs);
|
||||
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
|
||||
duplicateDoc(method.attributes(), repairs);
|
||||
}
|
||||
}
|
||||
case PbsAst.ContractDecl contractDecl -> {
|
||||
duplicateDoc(contractDecl.attributes(), repairs);
|
||||
if (interfaceModule) {
|
||||
for (final PbsAst.FunctionSignature signature : contractDecl.signatures()) {
|
||||
duplicateDoc(signature.attributes(), repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.HostDecl hostDecl -> {
|
||||
duplicateDoc(hostDecl.attributes(), repairs);
|
||||
if (interfaceModule) {
|
||||
for (final PbsAst.FunctionSignature signature : hostDecl.signatures()) {
|
||||
collectHostSignature(signature, repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.BuiltinTypeDecl builtinTypeDecl -> {
|
||||
if (interfaceModule) {
|
||||
duplicateDoc(builtinTypeDecl.attributes(), repairs);
|
||||
duplicateNamed(builtinTypeDecl.attributes(), "BuiltinType", repairs);
|
||||
for (final PbsAst.FunctionSignature signature : builtinTypeDecl.signatures()) {
|
||||
duplicateDoc(signature.attributes(), repairs);
|
||||
duplicateNamed(signature.attributes(), "IntrinsicCall", repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.ErrorDecl errorDecl -> duplicateDoc(errorDecl.attributes(), repairs);
|
||||
case PbsAst.EnumDecl enumDecl -> duplicateDoc(enumDecl.attributes(), repairs);
|
||||
case PbsAst.CallbackDecl callbackDecl -> duplicateDoc(callbackDecl.attributes(), repairs);
|
||||
case PbsAst.ConstDecl constDecl -> {
|
||||
duplicateDoc(constDecl.attributes(), repairs);
|
||||
duplicateNamed(constDecl.attributes(), "BuiltinConst", repairs);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
repairs.sort(Comparator
|
||||
.comparingInt(PbsQuickFix::startOffset)
|
||||
.thenComparingInt(PbsQuickFix::endOffset)
|
||||
.thenComparing(PbsQuickFix::diagnosticCode));
|
||||
return List.copyOf(repairs);
|
||||
}
|
||||
|
||||
private static void collectHostSignature(
|
||||
final PbsAst.FunctionSignature signature,
|
||||
final List<PbsQuickFix> repairs) {
|
||||
duplicateDoc(signature.attributes(), repairs);
|
||||
duplicateNamed(signature.attributes(), "Host", repairs);
|
||||
duplicateNamed(signature.attributes(), "InitAllowed", repairs);
|
||||
duplicateNamed(signature.attributes(), "AssetLowering", repairs);
|
||||
final List<PbsAst.Attribute> initAllowed = named(signature.attributes(), "InitAllowed");
|
||||
if (!initAllowed.isEmpty() && !initAllowed.getFirst().arguments().isEmpty()) {
|
||||
add(repairs, initAllowed.getFirst(), PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
|
||||
INIT_ALLOWED_REPLACEMENT, REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE);
|
||||
}
|
||||
}
|
||||
|
||||
private static void duplicateDoc(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final List<PbsQuickFix> repairs) {
|
||||
duplicateNamed(attributes, "Doc", repairs);
|
||||
}
|
||||
|
||||
private static void duplicateNamed(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final String name,
|
||||
final List<PbsQuickFix> repairs) {
|
||||
final List<PbsAst.Attribute> matches = named(attributes, name);
|
||||
for (int index = 1; index < matches.size(); index++) {
|
||||
add(repairs, matches.get(index), PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name(),
|
||||
"", REMOVE_DUPLICATE_TITLE);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<PbsAst.Attribute> named(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final String name) {
|
||||
final ArrayList<PbsAst.Attribute> matches = new ArrayList<>();
|
||||
if (attributes == null) {
|
||||
return matches;
|
||||
}
|
||||
for (final PbsAst.Attribute attribute : attributes) {
|
||||
if (name.equals(attribute.name())) {
|
||||
matches.add(attribute);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static void add(
|
||||
final List<PbsQuickFix> repairs,
|
||||
final PbsAst.Attribute attribute,
|
||||
final String code,
|
||||
final String newText,
|
||||
final String title) {
|
||||
if (attribute == null || attribute.span() == null || attribute.span().isNone()) {
|
||||
return;
|
||||
}
|
||||
final Span span = attribute.span();
|
||||
final long start = span.getStart();
|
||||
final long end = span.getEnd();
|
||||
if (start < 0 || end < start || end > Integer.MAX_VALUE) {
|
||||
return;
|
||||
}
|
||||
repairs.add(new PbsQuickFix(code, (int) start, (int) end, newText, title));
|
||||
}
|
||||
}
|
||||
@ -5,9 +5,15 @@ import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.JarEntry;
|
||||
|
||||
public final class ResourceStdlibEnvironmentResolver implements StdlibEnvironmentResolver {
|
||||
private static final String STDLIB_ROOT = "stdlib";
|
||||
@ -37,6 +43,73 @@ public final class ResourceStdlibEnvironmentResolver implements StdlibEnvironmen
|
||||
barrelSource.get()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyList<StdlibModuleSource> modules() {
|
||||
final String root = target.exportReferenceName().toLowerCase()
|
||||
+ "/" + STDLIB_ROOT + "/" + stdlibVersion;
|
||||
final ClassLoader classLoader = ResourceStdlibEnvironmentResolver.class.getClassLoader();
|
||||
if (classLoader == null) {
|
||||
return ReadOnlyList.empty();
|
||||
}
|
||||
final URL url = classLoader.getResource(root);
|
||||
if (url == null) {
|
||||
return ReadOnlyList.empty();
|
||||
}
|
||||
try {
|
||||
final List<List<String>> modulePaths = "jar".equals(url.getProtocol())
|
||||
? jarModulePaths(url, root)
|
||||
: fileModulePaths(url);
|
||||
final ArrayList<StdlibModuleSource> modules = new ArrayList<>();
|
||||
for (final List<String> modulePath : modulePaths) {
|
||||
if (modulePath.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
final String project = modulePath.getFirst();
|
||||
final ReadOnlyList<String> segments = ReadOnlyList.wrap(modulePath.subList(1, modulePath.size()));
|
||||
resolveModule(project, segments).ifPresent(modules::add);
|
||||
}
|
||||
return ReadOnlyList.wrap(modules);
|
||||
} catch (final Exception ignored) {
|
||||
return ReadOnlyList.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private List<List<String>> fileModulePaths(final URL url) throws Exception {
|
||||
final Path rootPath = Path.of(url.toURI());
|
||||
final ArrayList<List<String>> modulePaths = new ArrayList<>();
|
||||
try (var walk = Files.walk(rootPath)) {
|
||||
walk.filter(path -> "main.pbs".equals(String.valueOf(path.getFileName()))
|
||||
&& Files.isRegularFile(path.getParent().resolve("mod.barrel")))
|
||||
.forEach(path -> modulePaths.add(segments(rootPath.relativize(path.getParent()))));
|
||||
}
|
||||
return modulePaths;
|
||||
}
|
||||
|
||||
private List<List<String>> jarModulePaths(final URL url, final String root) throws IOException {
|
||||
final JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||
final var jarFile = connection.getJarFile();
|
||||
final String prefix = root + "/";
|
||||
final ArrayList<List<String>> modulePaths = new ArrayList<>();
|
||||
jarFile.stream()
|
||||
.map(JarEntry::getName)
|
||||
.filter(name -> name.startsWith(prefix) && name.endsWith("/main.pbs"))
|
||||
.forEach(name -> {
|
||||
final String directory = name.substring(prefix.length(), name.length() - "/main.pbs".length());
|
||||
if (!directory.isBlank() && jarFile.getEntry(prefix + directory + "/mod.barrel") != null) {
|
||||
modulePaths.add(List.of(directory.split("/")));
|
||||
}
|
||||
});
|
||||
return modulePaths;
|
||||
}
|
||||
|
||||
private static List<String> segments(final Path relative) {
|
||||
final ArrayList<String> segments = new ArrayList<>();
|
||||
for (final Path segment : relative) {
|
||||
segments.add(segment.toString());
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private String moduleBasePath(
|
||||
final String project,
|
||||
final ReadOnlyList<String> pathSegments) {
|
||||
|
||||
@ -6,5 +6,9 @@ import java.util.Optional;
|
||||
|
||||
public interface StdlibEnvironment {
|
||||
Optional<StdlibModuleSource> resolveModule(String project, ReadOnlyList<String> pathSegments);
|
||||
|
||||
default ReadOnlyList<StdlibModuleSource> modules() {
|
||||
return ReadOnlyList.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FESurfaceContext;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.IRBackend;
|
||||
import p.studio.compiler.models.SourceKind;
|
||||
import p.studio.compiler.pbs.PbsFrontendCompiler;
|
||||
import p.studio.compiler.pbs.PbsReservedMetadataExtractor;
|
||||
import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator;
|
||||
@ -14,9 +15,12 @@ import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.identifiers.ModuleId;
|
||||
import p.studio.compiler.source.tables.ModuleReference;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@ -127,11 +131,15 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
|
||||
final Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile = new LinkedHashMap<>();
|
||||
final Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile = new LinkedHashMap<>();
|
||||
final Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile = new LinkedHashMap<>();
|
||||
final Map<FileId, SourceKind> sourceKindByFile = new LinkedHashMap<>();
|
||||
for (final var entry : importedSemanticContexts.entrySet()) {
|
||||
supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls());
|
||||
}
|
||||
for (final var parsedSourceFile : assembly.parsedSourceFiles()) {
|
||||
astByFile.put(parsedSourceFile.fileId(), parsedSourceFile.ast());
|
||||
sourceKindByFile.put(
|
||||
parsedSourceFile.fileId(),
|
||||
parsedSourceFile.sourceKind() == null ? SourceKind.PROJECT : parsedSourceFile.sourceKind());
|
||||
final var importedSemanticContext = importedSemanticContexts.getOrDefault(
|
||||
parsedSourceFile.fileId(),
|
||||
PbsImportedSemanticContext.empty());
|
||||
@ -144,16 +152,81 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
|
||||
inlineHintsByFile.put(parsedSourceFile.fileId(), inlineHints);
|
||||
}
|
||||
}
|
||||
final Map<ModuleReference, Path> physicalModuleFiles = physicalModuleFiles(ctx, assembly);
|
||||
return new PbsSemanticReadSurface(
|
||||
Map.copyOf(astByFile),
|
||||
Map.copyOf(supplementalTopDeclsByFile),
|
||||
Map.copyOf(inlineHintsByFile));
|
||||
Map.copyOf(inlineHintsByFile),
|
||||
Map.copyOf(sourceKindByFile),
|
||||
physicalModuleFiles,
|
||||
moduleCompletionLabels(ctx, physicalModuleFiles));
|
||||
}
|
||||
|
||||
private static Map<ModuleReference, Path> physicalModuleFiles(
|
||||
final FrontendPhaseContext ctx,
|
||||
final PbsModuleAssembly assembly) {
|
||||
final Map<ModuleReference, Path> files = new LinkedHashMap<>();
|
||||
if (ctx == null || ctx.fileTable == null || assembly == null || assembly.parsedSourceFiles() == null) {
|
||||
return Map.of();
|
||||
}
|
||||
for (final PbsParsedSourceFile parsed : assembly.parsedSourceFiles()) {
|
||||
if (parsed.fileId() == null || parsed.fileId().isNone()
|
||||
|| parsed.moduleId() == null || parsed.moduleId().isNone()) {
|
||||
continue;
|
||||
}
|
||||
final var handle = ctx.fileTable.get(parsed.fileId());
|
||||
if (handle == null || handle.getCanonPath() == null) {
|
||||
continue;
|
||||
}
|
||||
final Path path = handle.getCanonPath().toAbsolutePath().normalize();
|
||||
if (!Files.isRegularFile(path)) {
|
||||
continue;
|
||||
}
|
||||
final ModuleReference reference = assembly.moduleTable().get(parsed.moduleId());
|
||||
if (reference == null) {
|
||||
continue;
|
||||
}
|
||||
files.merge(reference, path, (left, right) ->
|
||||
left.toString().compareTo(right.toString()) <= 0 ? left : right);
|
||||
}
|
||||
return Map.copyOf(files);
|
||||
}
|
||||
|
||||
private static List<String> moduleCompletionLabels(
|
||||
final FrontendPhaseContext ctx,
|
||||
final Map<ModuleReference, Path> physicalModuleFiles) {
|
||||
final var labels = new java.util.TreeSet<String>();
|
||||
for (final ModuleReference reference : physicalModuleFiles.keySet()) {
|
||||
labels.add(moduleLabel(reference.project(), reference.pathSegments()));
|
||||
}
|
||||
if (ctx != null) {
|
||||
final var environment = new ResourceStdlibEnvironmentResolver().resolve(ctx.stdlibVersion(), ctx.target());
|
||||
for (final var module : environment.modules()) {
|
||||
labels.add(moduleLabel(module.project(), module.pathSegments()));
|
||||
}
|
||||
}
|
||||
return List.copyOf(labels);
|
||||
}
|
||||
|
||||
private static String moduleLabel(
|
||||
final String project,
|
||||
final ReadOnlyList<String> pathSegments) {
|
||||
if (project == null || project.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
if (pathSegments == null || pathSegments.isEmpty()) {
|
||||
return "@" + project;
|
||||
}
|
||||
return "@" + project + ":" + String.join("/", pathSegments.asList());
|
||||
}
|
||||
|
||||
public record PbsSemanticReadSurface(
|
||||
Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile,
|
||||
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
|
||||
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile) {
|
||||
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile,
|
||||
Map<FileId, SourceKind> sourceKindByFile,
|
||||
Map<ModuleReference, Path> physicalModuleFileByReference,
|
||||
List<String> moduleCompletionLabels) {
|
||||
}
|
||||
|
||||
private IRBackend mergeCompiledSources(
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
package p.studio.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.AppMode;
|
||||
import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.BuildStack;
|
||||
import p.studio.compiler.models.ProjectDescriptor;
|
||||
import p.studio.compiler.models.SourceHandle;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.tables.FileTable;
|
||||
import p.studio.compiler.source.tables.ProjectTable;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PBSFrontendDocumentLinkTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void documentLinksUseTheAssembledPhysicalModuleFile() throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path sourceRoot = projectRoot.resolve("src");
|
||||
final Path sourceA = sourceRoot.resolve("a").resolve("source.pbs");
|
||||
final Path sourceB = sourceRoot.resolve("b").resolve("source.pbs");
|
||||
Files.createDirectories(sourceA.getParent());
|
||||
Files.createDirectories(sourceB.getParent());
|
||||
Files.writeString(sourceA, """
|
||||
fn target() -> int {
|
||||
return 1;
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
|
||||
final String caller = """
|
||||
import { target } from @app:a;
|
||||
import { Log } from @sdk:log;
|
||||
import { Missing } from @app:missing;
|
||||
|
||||
fn caller() -> int {
|
||||
return target();
|
||||
}
|
||||
""";
|
||||
Files.writeString(sourceB, caller);
|
||||
Files.writeString(sourceB.getParent().resolve("mod.barrel"), "pub fn caller() -> int;\n");
|
||||
|
||||
final var projectTable = new ProjectTable();
|
||||
final var fileTable = new FileTable(1);
|
||||
final var projectId = projectTable.register(ProjectDescriptor.builder()
|
||||
.rootPath(projectRoot)
|
||||
.name("app")
|
||||
.version("1.0.0")
|
||||
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
|
||||
.build());
|
||||
register(projectId, projectRoot, sourceA, fileTable);
|
||||
register(projectId, projectRoot, sourceA.getParent().resolve("mod.barrel"), fileTable);
|
||||
final FileId callerId = register(projectId, projectRoot, sourceB, fileTable);
|
||||
register(projectId, projectRoot, sourceB.getParent().resolve("mod.barrel"), fileTable);
|
||||
|
||||
final var phaseContext = new FrontendPhaseContext(
|
||||
projectTable,
|
||||
fileTable,
|
||||
new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
|
||||
AppMode.Game);
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var editorialContext = service.prepareEditorialContext(phaseContext, callerId).orElseThrow();
|
||||
final var links = service.documentLinks(
|
||||
new FrontendDocumentRequest(projectRoot, sourceB, caller),
|
||||
editorialContext);
|
||||
|
||||
assertEquals(1, links.size());
|
||||
assertEquals(sourceA.toAbsolutePath().normalize(), links.getFirst().path());
|
||||
final byte[] bytes = caller.getBytes(StandardCharsets.UTF_8);
|
||||
assertEquals("@app:a", new String(
|
||||
bytes,
|
||||
links.getFirst().startOffset(),
|
||||
links.getFirst().endOffset() - links.getFirst().startOffset(),
|
||||
StandardCharsets.UTF_8));
|
||||
assertTrue(service.documentLinksSupported());
|
||||
}
|
||||
|
||||
private static FileId register(
|
||||
final p.studio.compiler.source.identifiers.ProjectId projectId,
|
||||
final Path projectRoot,
|
||||
final Path file,
|
||||
final FileTable fileTable) throws IOException {
|
||||
final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class);
|
||||
return fileTable.register(new SourceHandle(
|
||||
projectId,
|
||||
projectRoot.relativize(file),
|
||||
file,
|
||||
attributes.size(),
|
||||
attributes.lastModifiedTime().toMillis(),
|
||||
SourceProviderFactory.filesystem()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
package p.studio.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.AppMode;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.BuildStack;
|
||||
import p.studio.compiler.models.ProjectDescriptor;
|
||||
import p.studio.compiler.models.SourceHandle;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendSymbolKind;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.compiler.source.tables.FileTable;
|
||||
import p.studio.compiler.source.tables.ProjectTable;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PBSFrontendModuleCompletionTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void moduleRefCompletionListsKnownModulesAndMemberCompletionStays() throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path sourceRoot = projectRoot.resolve("src");
|
||||
final Path moduleA = sourceRoot.resolve("a").resolve("source.pbs");
|
||||
final Path main = sourceRoot.resolve("main.pbs");
|
||||
Files.createDirectories(moduleA.getParent());
|
||||
Files.writeString(moduleA, """
|
||||
fn target() -> int {
|
||||
return 1;
|
||||
}
|
||||
""");
|
||||
Files.writeString(moduleA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
|
||||
final String caller = """
|
||||
import { Log } from @sdk:log;
|
||||
|
||||
declare struct Vec() {
|
||||
fn blend(dx: int, dy: int) -> int { return dx; }
|
||||
}
|
||||
|
||||
fn frame(vec: Vec) -> void {
|
||||
vec.blend(1, 2);
|
||||
}
|
||||
""";
|
||||
Files.writeString(main, caller);
|
||||
Files.writeString(sourceRoot.resolve("mod.barrel"), """
|
||||
pub struct Vec();
|
||||
pub fn frame(vec: Vec) -> void;
|
||||
""");
|
||||
|
||||
final var projectTable = new ProjectTable();
|
||||
final var fileTable = new FileTable(1);
|
||||
final ProjectId projectId = projectTable.register(ProjectDescriptor.builder()
|
||||
.rootPath(projectRoot)
|
||||
.name("app")
|
||||
.version("1.0.0")
|
||||
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
|
||||
.build());
|
||||
register(projectId, projectRoot, moduleA, fileTable);
|
||||
register(projectId, projectRoot, moduleA.getParent().resolve("mod.barrel"), fileTable);
|
||||
final FileId mainId = register(projectId, projectRoot, main, fileTable);
|
||||
register(projectId, projectRoot, sourceRoot.resolve("mod.barrel"), fileTable);
|
||||
|
||||
final var phaseContext = new FrontendPhaseContext(
|
||||
projectTable,
|
||||
fileTable,
|
||||
new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
|
||||
AppMode.Game);
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var editorialContext = service.prepareEditorialContext(phaseContext, mainId).orElseThrow();
|
||||
final var request = new FrontendDocumentRequest(projectRoot, main, caller);
|
||||
|
||||
final var moduleCompletion = service.completion(request, editorialContext, byteOffset(caller, "@sdk:log"));
|
||||
assertTrue(labels(moduleCompletion).contains("@sdk:log"));
|
||||
assertTrue(labels(moduleCompletion).contains("@app:a"));
|
||||
assertTrue(moduleCompletion.stream().allMatch(candidate -> candidate.kind() == FrontendSymbolKind.MODULE));
|
||||
|
||||
final int afterDot = caller.indexOf("vec.") + "vec.".length();
|
||||
final var memberCompletion = service.completion(
|
||||
request,
|
||||
editorialContext,
|
||||
caller.substring(0, afterDot).getBytes(StandardCharsets.UTF_8).length);
|
||||
assertTrue(labels(memberCompletion).contains("blend"), labels(memberCompletion).toString());
|
||||
assertFalse(labels(memberCompletion).contains("@sdk:log"));
|
||||
}
|
||||
|
||||
private static List<String> labels(final List<FrontendCompletionCandidate> candidates) {
|
||||
return candidates.stream().map(FrontendCompletionCandidate::label).toList();
|
||||
}
|
||||
|
||||
private static int byteOffset(final String source, final String marker) {
|
||||
final int index = source.indexOf(marker);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException(marker);
|
||||
}
|
||||
return source.substring(0, index + 1).getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
private static FileId register(
|
||||
final ProjectId projectId,
|
||||
final Path projectRoot,
|
||||
final Path file,
|
||||
final FileTable fileTable) throws IOException {
|
||||
final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class);
|
||||
return fileTable.register(new SourceHandle(
|
||||
projectId,
|
||||
projectRoot.relativize(file),
|
||||
file,
|
||||
attributes.size(),
|
||||
attributes.lastModifiedTime().toMillis(),
|
||||
SourceProviderFactory.filesystem()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.parser.PbsParser;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsCallHierarchyTest {
|
||||
private static final String SOURCE = """
|
||||
fn helper() -> int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
declare struct Box() {
|
||||
fn blend(dx: int) -> int {
|
||||
return dx;
|
||||
}
|
||||
}
|
||||
|
||||
fn main(box: Box) -> int {
|
||||
box.blend(1);
|
||||
return helper();
|
||||
}
|
||||
""";
|
||||
|
||||
@Test
|
||||
void functionAndMethodCallsComeFromTheAst() {
|
||||
final PbsAst.File ast = parse(SOURCE);
|
||||
final int mainOffset = byteOffset(SOURCE, "fn main");
|
||||
final var prepared = PbsCallHierarchy.prepare(ast, mainOffset);
|
||||
assertEquals(1, prepared.size());
|
||||
assertEquals("main", prepared.getFirst().name());
|
||||
assertEquals(false, prepared.getFirst().method());
|
||||
|
||||
final var outgoing = PbsCallHierarchy.outgoingCalls(ast, prepared.getFirst());
|
||||
assertTrue(outgoing.stream().anyMatch(call -> call.callee().name().equals("helper") && !call.callee().method()));
|
||||
assertTrue(outgoing.stream().anyMatch(call -> call.callee().name().equals("blend") && call.callee().method()));
|
||||
|
||||
final int helperOffset = byteOffset(SOURCE, "fn helper");
|
||||
final var helper = PbsCallHierarchy.prepare(ast, helperOffset).getFirst();
|
||||
final var incoming = PbsCallHierarchy.incomingCalls(ast, helper);
|
||||
assertEquals(1, incoming.size());
|
||||
assertEquals("main", incoming.getFirst().caller().name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aStructNameIsNotACallable() {
|
||||
final PbsAst.File ast = parse(SOURCE);
|
||||
assertTrue(PbsCallHierarchy.prepare(ast, byteOffset(SOURCE, "struct Box")).isEmpty());
|
||||
}
|
||||
|
||||
private static PbsAst.File parse(final String source) {
|
||||
return PbsParser.parse(
|
||||
PbsLexer.lex(source, FileId.none(), DiagnosticSink.empty()),
|
||||
FileId.none(),
|
||||
DiagnosticSink.empty());
|
||||
}
|
||||
|
||||
private static int byteOffset(final String source, final String marker) {
|
||||
final int index = source.indexOf(marker);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException(marker);
|
||||
}
|
||||
return source.substring(0, index + 1).getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsDocumentFormatterTest {
|
||||
|
||||
@Test
|
||||
void indentsOnlyWhereBraceOrParenNestingChanges() {
|
||||
final String source = """
|
||||
fn length(value: int) -> int {
|
||||
return value;
|
||||
}
|
||||
fn other() -> int {
|
||||
return 1;
|
||||
}
|
||||
""";
|
||||
|
||||
final String formatted = PbsDocumentFormatter.format(source);
|
||||
|
||||
assertEquals("""
|
||||
fn length(value: int) -> int {
|
||||
return value;
|
||||
}
|
||||
fn other() -> int {
|
||||
return 1;
|
||||
}
|
||||
""", formatted);
|
||||
assertTrue(formatted.indexOf("fn length") < formatted.indexOf("fn other"));
|
||||
assertFalse(formatted.contains("\nreturn value;\nreturn 1;"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void leavesDocTextBlockInteriorAndTrailingCommentUntouched() {
|
||||
final String source = """
|
||||
[Doc(markdown = \"\"\"
|
||||
hello
|
||||
\"\"\")]
|
||||
fn length() -> int { // keep
|
||||
return 1;
|
||||
}
|
||||
""";
|
||||
|
||||
final String formatted = PbsDocumentFormatter.format(source);
|
||||
|
||||
assertTrue(formatted.contains(" hello\n"));
|
||||
assertTrue(formatted.contains("fn length() -> int { // keep"));
|
||||
assertTrue(formatted.contains("\n return 1;\n"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.PBSFrontendLanguageService;
|
||||
import p.studio.compiler.PBSSemanticTokenProvider;
|
||||
import p.studio.compiler.messages.FESurfaceContext;
|
||||
import p.studio.compiler.models.FrontendSemanticToken;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.parser.PbsParser;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendEditorialContext;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsSemanticTokenOverlayTest {
|
||||
|
||||
@Test
|
||||
void resolvedFunctionUseReplacesTheIdentifierBaseline() {
|
||||
final String source = """
|
||||
fn helper() -> int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn main() -> int {
|
||||
return helper;
|
||||
}
|
||||
""";
|
||||
final PbsAst.File ast = PbsParser.parse(
|
||||
PbsLexer.lex(source, FileId.none(), DiagnosticSink.empty()),
|
||||
FileId.none(),
|
||||
DiagnosticSink.empty());
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var tokens = service.semanticTokens(
|
||||
new FrontendDocumentRequest(Path.of("."), Path.of("demo.pbs"), source),
|
||||
new FrontendEditorialContext(ast, ReadOnlyList.empty(), FESurfaceContext.empty()));
|
||||
|
||||
assertEquals(PbsSemanticKind.FUNCTION.semanticKey(), keyAt(source, tokens, "return helper"));
|
||||
assertTrue(tokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.KEYWORD.semanticKey())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void localBindingAndBrokenTextKeepTheBaselineToken() {
|
||||
final String source = """
|
||||
fn main() -> int {
|
||||
let value: int = 1;
|
||||
return value;
|
||||
}
|
||||
""";
|
||||
final PbsAst.File ast = PbsParser.parse(
|
||||
PbsLexer.lex(source, FileId.none(), DiagnosticSink.empty()),
|
||||
FileId.none(),
|
||||
DiagnosticSink.empty());
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var request = new FrontendDocumentRequest(Path.of("."), Path.of("demo.pbs"), source);
|
||||
final var overlaid = service.semanticTokens(
|
||||
request,
|
||||
new FrontendEditorialContext(ast, ReadOnlyList.empty(), FESurfaceContext.empty()));
|
||||
|
||||
assertEquals(PbsSemanticKind.IDENTIFIER.semanticKey(), keyAt(source, overlaid, "return value"));
|
||||
|
||||
final String broken = "fn main( {";
|
||||
final List<FrontendSemanticToken> baseline = new PBSSemanticTokenProvider().tokenize(broken);
|
||||
final List<FrontendSemanticToken> withoutContext = service.semanticTokens(
|
||||
new FrontendDocumentRequest(Path.of("."), Path.of("broken.pbs"), broken));
|
||||
assertEquals(baseline, withoutContext);
|
||||
assertTrue(withoutContext.stream().anyMatch(token ->
|
||||
token.semanticKey().equals(PbsSemanticKind.KEYWORD.semanticKey())));
|
||||
}
|
||||
|
||||
private static String keyAt(
|
||||
final String source,
|
||||
final List<FrontendSemanticToken> tokens,
|
||||
final String marker) {
|
||||
final int nameOffset = source.indexOf(marker) + marker.lastIndexOf(' ') + 1;
|
||||
final int byteOffset = source.substring(0, nameOffset).getBytes(StandardCharsets.UTF_8).length;
|
||||
return tokens.stream()
|
||||
.filter(token -> token.startOffset() == byteOffset)
|
||||
.map(FrontendSemanticToken::semanticKey)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.services.FrontendFoldingRange;
|
||||
import p.studio.compiler.services.FrontendSelectionRange;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsStructureRangesTest {
|
||||
private static final String SOURCE = """
|
||||
[Doc(markdown = \"\"\"
|
||||
hello
|
||||
\"\"\")]
|
||||
fn length(value: int) -> int {
|
||||
return value;
|
||||
}
|
||||
""";
|
||||
|
||||
@Test
|
||||
void foldingUsesDeclarationBodyParameterListAndDocText() {
|
||||
final var ranges = PbsStructureRanges.foldingRanges(SOURCE);
|
||||
final var slices = ranges.stream().map(range -> slice(SOURCE, range)).toList();
|
||||
|
||||
assertTrue(slices.stream().anyMatch(slice -> slice.contains("hello")), slices.toString());
|
||||
assertTrue(slices.stream().anyMatch(slice -> slice.equals("(value: int)")), slices.toString());
|
||||
assertTrue(slices.stream().anyMatch(slice -> slice.startsWith("{")), slices.toString());
|
||||
assertTrue(slices.stream().anyMatch(slice -> slice.contains("fn length")), slices.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectionGrowsFromIdentifierToBlockToDeclaration() {
|
||||
final int marker = SOURCE.indexOf("return value");
|
||||
assertTrue(marker >= 0, SOURCE);
|
||||
final int offset = marker + "return ".length();
|
||||
final var selections = PbsStructureRanges.selectionRanges(SOURCE, List.of(offset));
|
||||
assertEquals(1, selections.size(), "offset " + offset);
|
||||
final FrontendSelectionRange selection = selections.getFirst();
|
||||
|
||||
assertEquals("value", slice(SOURCE, selection.startOffset(), selection.endOffset()));
|
||||
assertNotNull(selection.parent());
|
||||
assertTrue(slice(SOURCE, selection.parent().startOffset(), selection.parent().endOffset()).startsWith("{"));
|
||||
assertNotNull(selection.parent().parent());
|
||||
assertTrue(slice(SOURCE, selection.parent().parent().startOffset(), selection.parent().parent().endOffset())
|
||||
.contains("fn length"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unmatchedBraceFoldsThroughTheEndOfTheText() {
|
||||
final String broken = """
|
||||
fn broken() -> void {
|
||||
let x: int = 1;
|
||||
""";
|
||||
final int textEnd = broken.getBytes(StandardCharsets.UTF_8).length;
|
||||
final var ranges = PbsStructureRanges.foldingRanges(broken);
|
||||
|
||||
assertTrue(ranges.stream().anyMatch(range ->
|
||||
range.endOffset() == textEnd && slice(broken, range).startsWith("{")));
|
||||
}
|
||||
|
||||
private static String slice(final String text, final FrontendFoldingRange range) {
|
||||
return slice(text, range.startOffset(), range.endOffset());
|
||||
}
|
||||
|
||||
private static String slice(final String text, final int start, final int end) {
|
||||
final byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
||||
return new String(bytes, start, end - start, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsLexerTest {
|
||||
@ -34,6 +35,13 @@ class PbsLexerTest {
|
||||
.kind());
|
||||
assertEquals(PbsTokenKind.EOF, tokens.getLast().kind());
|
||||
assertTrue(diagnostics.isEmpty(), "Lexer should not report diagnostics for valid input");
|
||||
assertTrue(PbsLexer.isKeyword("fn"));
|
||||
assertTrue(PbsLexer.isKeyword("step"));
|
||||
assertTrue(PbsLexer.isIdentifierLexeme("helper"));
|
||||
assertTrue(PbsLexer.isIdentifierLexeme("_value"));
|
||||
assertFalse(PbsLexer.isIdentifierLexeme("fn"));
|
||||
assertFalse(PbsLexer.isIdentifierLexeme("1ab"));
|
||||
assertFalse(PbsLexer.isIdentifierLexeme(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -434,6 +434,304 @@ final class PbsEditorialSupportServiceTest {
|
||||
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 shouldFlattenOutlineMembersForWorkspaceSearchAndFilterByName() {
|
||||
final var source = """
|
||||
import { Log } from @sdk:log;
|
||||
|
||||
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 var ast = parseOrdinary(source);
|
||||
final var all = service.workspaceSymbols(ast, "");
|
||||
assertEquals(List.of(
|
||||
"Vec",
|
||||
"x",
|
||||
"make",
|
||||
"blend",
|
||||
"helper",
|
||||
"helper",
|
||||
"frame"), all.stream().map(PbsEditorialWorkspaceSymbol::name).toList());
|
||||
assertEquals("", all.getFirst().containerName());
|
||||
assertEquals("Vec", all.stream().filter(symbol -> symbol.name().equals("blend")).findFirst().orElseThrow().containerName());
|
||||
assertEquals("Vec", all.stream().filter(symbol -> symbol.name().equals("x")).findFirst().orElseThrow().containerName());
|
||||
assertTrue(all.stream().noneMatch(symbol -> symbol.name().equals("Log") || symbol.name().equals("local")));
|
||||
|
||||
final var blendHits = service.workspaceSymbols(ast, "BLEND");
|
||||
assertEquals(List.of("blend"), blendHits.stream().map(PbsEditorialWorkspaceSymbol::name).toList());
|
||||
assertEquals("Vec", blendHits.getFirst().containerName());
|
||||
|
||||
final var helperHits = service.workspaceSymbols(ast, "helper");
|
||||
assertEquals(2, helperHits.size());
|
||||
assertNotEquals(helperHits.getFirst().detail(), helperHits.get(1).detail());
|
||||
|
||||
final var qualifiedMiss = service.workspaceSymbols(ast, "Vec.blend");
|
||||
assertTrue(qualifiedMiss.isEmpty());
|
||||
}
|
||||
|
||||
@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());
|
||||
final var workspaceSymbols = service.workspaceSymbols(ast, "");
|
||||
assertEquals(List.of("ok"), workspaceSymbols.stream().map(PbsEditorialWorkspaceSymbol::name).toList());
|
||||
}
|
||||
|
||||
@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()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameRewritesPhysicalFunctionSitesAndSkipsCommentsStringsAndCollisions() {
|
||||
final var source = """
|
||||
fn helper() -> int { return 1; }
|
||||
|
||||
fn helper(value: int) -> int { return value; }
|
||||
|
||||
fn frame() -> void {
|
||||
helper();
|
||||
helper(2);
|
||||
// helper
|
||||
let text = "helper";
|
||||
}
|
||||
""";
|
||||
final var ast = parseOrdinary(source);
|
||||
final var renamed = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "aid");
|
||||
assertTrue(renamed.allowed(), renamed.refusal());
|
||||
assertEquals(4, renamed.sites().size(), renamed.sites().toString());
|
||||
assertTrue(service.prepareRename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();")).isPresent());
|
||||
|
||||
final var keyword = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "fn");
|
||||
assertFalse(keyword.allowed());
|
||||
assertEquals("invalid identifier", keyword.refusal());
|
||||
|
||||
final var collision = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "frame");
|
||||
assertFalse(collision.allowed());
|
||||
assertEquals("name collision", collision.refusal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameAllowsLocalNameSpanAndRefusesAliasBuiltinAndErrorLabel() {
|
||||
final var source = """
|
||||
import { Log as Logger } from @sdk:log;
|
||||
|
||||
declare error Boom {
|
||||
Missing;
|
||||
}
|
||||
|
||||
fn frame() -> void {
|
||||
let total = 1;
|
||||
Logger.info("x");
|
||||
let n: int = total;
|
||||
n;
|
||||
}
|
||||
""";
|
||||
final var ast = parseOrdinary(source);
|
||||
final var supplemental = supplementalTopDecls("""
|
||||
declare service Log {
|
||||
fn info(message: str) -> void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
final var local = service.rename(
|
||||
source,
|
||||
ast,
|
||||
supplemental,
|
||||
source.indexOf("total ="),
|
||||
"sum");
|
||||
assertTrue(local.allowed(), local.refusal());
|
||||
assertEquals(2, local.sites().size(), local.sites().toString());
|
||||
|
||||
final var alias = service.prepareRename(
|
||||
source,
|
||||
ast,
|
||||
supplemental,
|
||||
source.indexOf("Logger.info"));
|
||||
assertTrue(alias.isEmpty());
|
||||
|
||||
final var builtin = service.prepareRename(
|
||||
source,
|
||||
ast,
|
||||
supplemental,
|
||||
source.indexOf("int = total"));
|
||||
assertTrue(builtin.isEmpty());
|
||||
|
||||
final var errorLabel = service.prepareRename(
|
||||
source,
|
||||
ast,
|
||||
supplemental,
|
||||
source.indexOf("Missing"));
|
||||
assertTrue(errorLabel.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameOfExportDoesNotRewriteDifferingAlias() {
|
||||
final var exporter = """
|
||||
fn helper() -> int { return 1; }
|
||||
""";
|
||||
final var importer = """
|
||||
import { helper as aid } from @sdk:log;
|
||||
|
||||
fn frame() -> void {
|
||||
aid();
|
||||
}
|
||||
""";
|
||||
final var exporterAst = parse(exporter, new FileId(1), PbsParser.ParseMode.ORDINARY);
|
||||
final var importerAst = parseOrdinary(importer);
|
||||
final var exporterDoc = new PbsEditorialSupportService.EditorialDocument(
|
||||
exporter,
|
||||
exporterAst,
|
||||
ReadOnlyList.empty(),
|
||||
p.studio.compiler.messages.FESurfaceContext.empty());
|
||||
final var importerDoc = new PbsEditorialSupportService.EditorialDocument(
|
||||
importer,
|
||||
importerAst,
|
||||
exporterAst.topDecls(),
|
||||
p.studio.compiler.messages.FESurfaceContext.empty());
|
||||
final var renamed = service.rename(
|
||||
exporterDoc,
|
||||
exporter.indexOf("helper"),
|
||||
"renamed",
|
||||
List.of(exporterDoc, importerDoc));
|
||||
assertTrue(renamed.allowed(), renamed.refusal());
|
||||
assertEquals(2, renamed.sites().size(), renamed.sites().toString());
|
||||
assertTrue(renamed.sites().stream().anyMatch(site ->
|
||||
site.span().getFileId().equals(exporterAst.span().getFileId())));
|
||||
assertTrue(renamed.sites().stream().anyMatch(site ->
|
||||
site.span().getFileId().equals(importerAst.span().getFileId())));
|
||||
final int aliasUse = importer.indexOf("aid();");
|
||||
assertTrue(renamed.sites().stream().noneMatch(site ->
|
||||
site.span().getFileId().equals(importerAst.span().getFileId())
|
||||
&& site.span().getStart() == aliasUse));
|
||||
}
|
||||
|
||||
private PbsEditorialResolvedSymbol requireHover(
|
||||
final String source,
|
||||
final PbsAst.File ast,
|
||||
@ -455,6 +753,26 @@ final class PbsEditorialSupportServiceTest {
|
||||
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) {
|
||||
return parse(source, new FileId(0), PbsParser.ParseMode.ORDINARY);
|
||||
}
|
||||
|
||||
@ -0,0 +1,234 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.models.SourceKind;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.parser.PbsParser;
|
||||
import p.studio.compiler.source.diagnostics.Diagnostic;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.tables.NameTable;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsQuickFixCollectorTest {
|
||||
|
||||
@Test
|
||||
void duplicateDocRepairsMatchValidatorSpansAndKeepTheFirstCopy() {
|
||||
final var parsed = parse("""
|
||||
[Doc(markdown = \"""
|
||||
First.
|
||||
\""")]
|
||||
[Doc(markdown = \"""
|
||||
Second.
|
||||
\""")]
|
||||
[Doc(markdown = \"""
|
||||
Third.
|
||||
\""")]
|
||||
fn duplicated() -> void { return; }
|
||||
|
||||
declare struct Player(x: int) {
|
||||
[Doc(markdown = \"""
|
||||
Kept.
|
||||
\""")]
|
||||
[Doc(markdown = \"""
|
||||
Extra method.
|
||||
\""")]
|
||||
fn update() -> void { return; }
|
||||
}
|
||||
""", SourceKind.PROJECT);
|
||||
|
||||
final var duplicates = diagnostics(parsed, PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name());
|
||||
assertEquals(3, duplicates.size(), parsed.diagnostics().stream().map(Diagnostic::getMessage).toList().toString());
|
||||
assertEquals(3, parsed.repairs().size());
|
||||
assertTrue(parsed.repairs().stream().allMatch(repair ->
|
||||
repair.newText().isEmpty()
|
||||
&& repair.title().equals(PbsQuickFixCollector.REMOVE_DUPLICATE_TITLE)
|
||||
&& repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name())));
|
||||
assertRepairsMatchDiagnostics(parsed);
|
||||
|
||||
final String edited = apply(parsed.source(), parsed.repairs());
|
||||
assertTrue(edited.contains("First."));
|
||||
assertTrue(edited.contains("Kept."));
|
||||
assertFalse(edited.contains("Second."));
|
||||
assertFalse(edited.contains("Third."));
|
||||
assertFalse(edited.contains("Extra method."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedAndEmptyDocProduceNoRepair() {
|
||||
final var parsed = parse("""
|
||||
[Doc(text = \"""
|
||||
Wrong argument.
|
||||
\""")]
|
||||
fn malformed() -> void { return; }
|
||||
|
||||
[Doc(markdown = \"""
|
||||
|
||||
\""")]
|
||||
fn empty() -> void { return; }
|
||||
|
||||
[InitAllowed(flag = 1)]
|
||||
fn frame() -> void { return; }
|
||||
""", SourceKind.PROJECT);
|
||||
|
||||
assertTrue(diagnostics(parsed, PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name()).size() >= 2);
|
||||
assertTrue(parsed.repairs().isEmpty(), parsed.repairs().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void initAllowedArgumentsAreRewrittenOnlyForInterfaceHostSignatures() {
|
||||
final var parsed = parse("""
|
||||
declare host Gfx {
|
||||
[Host(module = "gfx2d", name = "clear", version = 1)]
|
||||
[InitAllowed(flag = 1)]
|
||||
fn clear() -> void;
|
||||
|
||||
[Host(module = "gfx2d", name = "fill", version = 1)]
|
||||
[InitAllowed()]
|
||||
fn fill() -> void;
|
||||
|
||||
[Host(module = "gfx2d", name = "blit", version = 1)]
|
||||
[InitAllowed]
|
||||
fn blit() -> void;
|
||||
|
||||
[Host(module = "gfx2d")]
|
||||
fn broken() -> void;
|
||||
|
||||
[Host(module = "gfx2d", name = "copy", version = 1)]
|
||||
[Host(module = "gfx2d", name = "copy", version = 1)]
|
||||
fn copy() -> void;
|
||||
}
|
||||
""", SourceKind.SDK_INTERFACE);
|
||||
|
||||
final var argumentRepairs = parsed.repairs().stream()
|
||||
.filter(repair -> repair.newText().equals("[InitAllowed]"))
|
||||
.toList();
|
||||
assertEquals(1, argumentRepairs.size(), parsed.repairs().toString());
|
||||
assertEquals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), argumentRepairs.getFirst().diagnosticCode());
|
||||
assertEquals(PbsQuickFixCollector.REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE, argumentRepairs.getFirst().title());
|
||||
assertRepairsMatchDiagnostics(parsed);
|
||||
|
||||
final var duplicateRepairs = parsed.repairs().stream()
|
||||
.filter(repair -> repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name()))
|
||||
.toList();
|
||||
assertEquals(1, duplicateRepairs.size());
|
||||
assertTrue(duplicateRepairs.getFirst().newText().isEmpty());
|
||||
|
||||
final String edited = apply(parsed.source(), parsed.repairs());
|
||||
assertTrue(edited.contains("[InitAllowed]\n"));
|
||||
assertFalse(edited.contains("flag = 1"));
|
||||
assertTrue(edited.contains("[InitAllowed()]"));
|
||||
assertEquals(1, count(edited, "[Host(module = \"gfx2d\", name = \"copy\", version = 1)]"));
|
||||
|
||||
final var projectHost = parse("""
|
||||
declare host Gfx {
|
||||
[Host(module = "gfx2d", name = "clear", version = 1)]
|
||||
[InitAllowed(flag = 1)]
|
||||
fn clear() -> void;
|
||||
}
|
||||
""", SourceKind.PROJECT);
|
||||
assertTrue(projectHost.repairs().stream().noneMatch(repair -> repair.newText().equals("[InitAllowed]")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void builtinConstTypeIntrinsicAndAssetLoweringDuplicatesAreDeleted() {
|
||||
final var parsed = parse("""
|
||||
[BuiltinType(name = "Color", version = 1)]
|
||||
[BuiltinType(name = "Color", version = 1)]
|
||||
declare builtin type Color(
|
||||
pub raw: int
|
||||
) {
|
||||
[IntrinsicCall(name = "width", version = 1)]
|
||||
[IntrinsicCall(name = "width", version = 1)]
|
||||
fn width() -> int;
|
||||
}
|
||||
[BuiltinConst(target = "Color", name = "white", version = 1)]
|
||||
[BuiltinConst(target = "Color", name = "white", version = 1)]
|
||||
declare const WHITE: Color;
|
||||
declare host LowAssets {
|
||||
[Host(module = "asset", name = "load", version = 1)]
|
||||
[AssetLowering(param = 0)]
|
||||
[AssetLowering(param = 0)]
|
||||
fn load(addressable: int) -> void;
|
||||
}
|
||||
""", SourceKind.SDK_INTERFACE);
|
||||
|
||||
assertEquals(4, parsed.repairs().size(), parsed.repairs().toString());
|
||||
assertTrue(parsed.repairs().stream().allMatch(repair ->
|
||||
repair.newText().isEmpty()
|
||||
&& repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name())));
|
||||
assertRepairsMatchDiagnostics(parsed);
|
||||
final String edited = apply(parsed.source(), parsed.repairs());
|
||||
assertEquals(1, count(edited, "[BuiltinType(name = \"Color\", version = 1)]"));
|
||||
assertEquals(1, count(edited, "[IntrinsicCall(name = \"width\", version = 1)]"));
|
||||
assertEquals(1, count(edited, "[BuiltinConst(target = \"Color\", name = \"white\", version = 1)]"));
|
||||
assertEquals(1, count(edited, "[AssetLowering(param = 0)]"));
|
||||
}
|
||||
|
||||
private static Parsed parse(final String source, final SourceKind sourceKind) {
|
||||
final var diagnostics = DiagnosticSink.empty();
|
||||
final var fileId = new FileId(0);
|
||||
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
|
||||
final var parseMode = sourceKind == SourceKind.SDK_INTERFACE
|
||||
? PbsParser.ParseMode.INTERFACE_MODULE
|
||||
: PbsParser.ParseMode.ORDINARY;
|
||||
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics, parseMode);
|
||||
new PbsDeclarationSemanticsValidator(new NameTable()).validate(ast, sourceKind, diagnostics);
|
||||
return new Parsed(
|
||||
source,
|
||||
diagnostics,
|
||||
PbsQuickFixCollector.collect(ast, sourceKind == SourceKind.SDK_INTERFACE));
|
||||
}
|
||||
|
||||
private static List<Diagnostic> diagnostics(final Parsed parsed, final String code) {
|
||||
return parsed.diagnostics().stream().filter(diagnostic -> diagnostic.getCode().equals(code)).toList();
|
||||
}
|
||||
|
||||
private static void assertRepairsMatchDiagnostics(final Parsed parsed) {
|
||||
for (final PbsQuickFix repair : parsed.repairs()) {
|
||||
assertTrue(parsed.diagnostics().stream().anyMatch(diagnostic ->
|
||||
diagnostic.getCode().equals(repair.diagnosticCode())
|
||||
&& diagnostic.getSpan().getStart() == repair.startOffset()
|
||||
&& diagnostic.getSpan().getEnd() == repair.endOffset()),
|
||||
repair + " missing from " + parsed.diagnostics());
|
||||
}
|
||||
}
|
||||
|
||||
private static String apply(final String source, final List<PbsQuickFix> repairs) {
|
||||
final var ordered = repairs.stream()
|
||||
.sorted(Comparator.comparingInt(PbsQuickFix::startOffset).reversed())
|
||||
.toList();
|
||||
String current = source;
|
||||
for (final PbsQuickFix repair : ordered) {
|
||||
current = current.substring(0, repair.startOffset()) + repair.newText() + current.substring(repair.endOffset());
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private static int count(final String source, final String needle) {
|
||||
int count = 0;
|
||||
int from = 0;
|
||||
while (from <= source.length()) {
|
||||
final int found = source.indexOf(needle, from);
|
||||
if (found < 0) {
|
||||
return count;
|
||||
}
|
||||
count += 1;
|
||||
from = found + needle.length();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private record Parsed(
|
||||
String source,
|
||||
DiagnosticSink diagnostics,
|
||||
List<PbsQuickFix> repairs) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendCallHierarchyItem(
|
||||
String name,
|
||||
boolean method,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendCallHierarchyItem {
|
||||
name = Objects.requireNonNull(name, "name").trim();
|
||||
if (name.isEmpty()) {
|
||||
throw new IllegalArgumentException("name must not be blank");
|
||||
}
|
||||
if (startOffset < 0 || endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("call hierarchy range is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendCallSite(
|
||||
FrontendCallHierarchyItem item,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendCallSite {
|
||||
item = Objects.requireNonNull(item, "item");
|
||||
if (startOffset < 0 || endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("call site range is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendCodeAction(
|
||||
String title,
|
||||
String diagnosticCode,
|
||||
Path path,
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
String newText) {
|
||||
|
||||
public FrontendCodeAction {
|
||||
title = requireText(title, "title");
|
||||
diagnosticCode = requireText(diagnosticCode, "diagnosticCode");
|
||||
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");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendDocumentLink(
|
||||
Path path,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendDocumentLink {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
public record FrontendFoldingRange(
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendFoldingRange {
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,11 @@ import java.util.Optional;
|
||||
* Optional frontend-owned editor services.
|
||||
*/
|
||||
public interface FrontendLanguageService {
|
||||
default Optional<FrontendEditorialContext> prepareEditorialContext(
|
||||
final FrontendPhaseContext phaseContext) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
default Optional<FrontendEditorialContext> prepareEditorialContext(
|
||||
final FrontendPhaseContext phaseContext,
|
||||
final FileId fileId) {
|
||||
@ -21,6 +26,12 @@ public interface FrontendLanguageService {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendSemanticToken> semanticTokens(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return semanticTokens(request);
|
||||
}
|
||||
|
||||
default List<FrontendCompletionCandidate> completion(
|
||||
final FrontendDocumentRequest request,
|
||||
final int offset) {
|
||||
@ -85,4 +96,144 @@ public interface FrontendLanguageService {
|
||||
final int 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);
|
||||
}
|
||||
|
||||
default List<FrontendWorkspaceSymbol> workspaceSymbols(final FrontendWorkspaceRequest request) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendWorkspaceSymbol> workspaceSymbols(
|
||||
final FrontendWorkspaceRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return workspaceSymbols(request);
|
||||
}
|
||||
|
||||
default Optional<FrontendDefinitionLocation> prepareRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final int offset) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
default Optional<FrontendDefinitionLocation> prepareRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset) {
|
||||
return prepareRename(request, offset);
|
||||
}
|
||||
|
||||
default FrontendRenameResult rename(
|
||||
final FrontendDocumentRequest request,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
return FrontendRenameResult.unsupported();
|
||||
}
|
||||
|
||||
default FrontendRenameResult rename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
return rename(request, offset, newName);
|
||||
}
|
||||
|
||||
default boolean documentLinksSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default List<FrontendDocumentLink> documentLinks(final FrontendDocumentRequest request) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendDocumentLink> documentLinks(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return documentLinks(request);
|
||||
}
|
||||
|
||||
default boolean foldingRangesSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default List<FrontendFoldingRange> foldingRanges(final FrontendDocumentRequest request) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendFoldingRange> foldingRanges(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return foldingRanges(request);
|
||||
}
|
||||
|
||||
default boolean selectionRangesSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default List<FrontendSelectionRange> selectionRanges(
|
||||
final FrontendDocumentRequest request,
|
||||
final List<Integer> offsets) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendSelectionRange> selectionRanges(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final List<Integer> offsets) {
|
||||
return selectionRanges(request, offsets);
|
||||
}
|
||||
|
||||
default boolean callHierarchySupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default java.util.List<FrontendCallHierarchyItem> prepareCallHierarchy(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
default java.util.List<FrontendCallSite> incomingCalls(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final FrontendCallHierarchyItem target) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
default java.util.List<FrontendCallSite> outgoingCalls(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final FrontendCallHierarchyItem target) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
default boolean formattingSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default java.util.Optional<String> formatDocument(final FrontendDocumentRequest request) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
|
||||
default boolean codeActionsSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default List<FrontendCodeAction> codeActions(final FrontendDocumentRequest request) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendCodeAction> codeActions(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return codeActions(request);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendRenameEdit(
|
||||
Path path,
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
String newText) {
|
||||
|
||||
public FrontendRenameEdit {
|
||||
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");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendRenameResult(
|
||||
Status status,
|
||||
String message,
|
||||
List<FrontendRenameEdit> edits) {
|
||||
|
||||
public enum Status {
|
||||
UNSUPPORTED,
|
||||
REFUSED,
|
||||
APPLIED
|
||||
}
|
||||
|
||||
public FrontendRenameResult {
|
||||
status = Objects.requireNonNull(status, "status");
|
||||
message = message == null ? "" : message.trim();
|
||||
edits = edits == null ? List.of() : List.copyOf(edits);
|
||||
if (status == Status.APPLIED && edits.isEmpty()) {
|
||||
throw new IllegalArgumentException("applied rename must include at least one edit");
|
||||
}
|
||||
if (status != Status.APPLIED && !edits.isEmpty()) {
|
||||
throw new IllegalArgumentException("refused or unsupported rename must not include edits");
|
||||
}
|
||||
}
|
||||
|
||||
public static FrontendRenameResult unsupported() {
|
||||
return new FrontendRenameResult(Status.UNSUPPORTED, "unsupported", List.of());
|
||||
}
|
||||
|
||||
public static FrontendRenameResult refused(final String message) {
|
||||
final String reason = message == null || message.isBlank() ? "not renameable" : message.trim();
|
||||
return new FrontendRenameResult(Status.REFUSED, reason, List.of());
|
||||
}
|
||||
|
||||
public static FrontendRenameResult applied(final List<FrontendRenameEdit> edits) {
|
||||
return new FrontendRenameResult(Status.APPLIED, "", edits);
|
||||
}
|
||||
|
||||
public boolean applied() {
|
||||
return status == Status.APPLIED;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
public record FrontendSelectionRange(
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
FrontendSelectionRange parent) {
|
||||
|
||||
public FrontendSelectionRange {
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,5 +17,6 @@ public enum FrontendSymbolKind {
|
||||
ENUM,
|
||||
ERROR,
|
||||
GLOBAL,
|
||||
CONST
|
||||
CONST,
|
||||
MODULE
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendWorkspaceRequest(
|
||||
Path projectRoot,
|
||||
String query) {
|
||||
|
||||
public FrontendWorkspaceRequest {
|
||||
projectRoot = Objects.requireNonNull(projectRoot, "projectRoot").toAbsolutePath().normalize();
|
||||
query = query == null ? "" : query;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendWorkspaceSymbol(
|
||||
String name,
|
||||
FrontendSymbolKind kind,
|
||||
String detail,
|
||||
String containerName,
|
||||
Path path,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendWorkspaceSymbol {
|
||||
name = requireText(name, "name");
|
||||
kind = Objects.requireNonNull(kind, "kind");
|
||||
detail = detail == null ? "" : detail.trim();
|
||||
containerName = containerName == null ? "" : containerName.trim();
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,170 @@ class FrontendLanguageServiceTest {
|
||||
assertEquals(FrontendSignatureHelp.empty(), service.signatureHelp(request, 0));
|
||||
assertTrue(service.definition(request, 0).isEmpty());
|
||||
assertTrue(service.references(request, 0).isEmpty());
|
||||
assertTrue(service.documentSymbols(request).isEmpty());
|
||||
assertTrue(service.workspaceSymbols(new FrontendWorkspaceRequest(Path.of("."), "frame")).isEmpty());
|
||||
assertTrue(service.prepareEditorialContext(null).isEmpty());
|
||||
assertTrue(service.prepareRename(request, 0).isEmpty());
|
||||
assertEquals(FrontendRenameResult.Status.UNSUPPORTED, service.rename(request, 0, "renamed").status());
|
||||
assertTrue(service.rename(request, 0, "renamed").edits().isEmpty());
|
||||
assertFalse(service.documentLinksSupported());
|
||||
assertTrue(service.documentLinks(request).isEmpty());
|
||||
assertTrue(service.documentLinks(request, null).isEmpty());
|
||||
assertFalse(service.foldingRangesSupported());
|
||||
assertTrue(service.foldingRanges(request).isEmpty());
|
||||
assertTrue(service.foldingRanges(request, null).isEmpty());
|
||||
assertFalse(service.callHierarchySupported());
|
||||
assertTrue(service.prepareCallHierarchy(request, null, 0).isEmpty());
|
||||
assertFalse(service.formattingSupported());
|
||||
assertTrue(service.formatDocument(request).isEmpty());
|
||||
assertFalse(service.selectionRangesSupported());
|
||||
assertTrue(service.selectionRanges(request, List.of(0)).isEmpty());
|
||||
assertTrue(service.selectionRanges(request, null, List.of(0)).isEmpty());
|
||||
assertFalse(service.codeActionsSupported());
|
||||
assertTrue(service.codeActions(request).isEmpty());
|
||||
assertTrue(service.codeActions(request, null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void codeActionAllowsEmptyReplacementAndRejectsBlankIdentity() {
|
||||
final Path path = Path.of("src/main.pbs").toAbsolutePath();
|
||||
final var action = new FrontendCodeAction(
|
||||
" Remove duplicate reserved attribute ",
|
||||
" E_SEM_DUPLICATE_RESERVED_ATTRIBUTE ",
|
||||
path,
|
||||
4,
|
||||
12,
|
||||
"");
|
||||
assertEquals("Remove duplicate reserved attribute", action.title());
|
||||
assertEquals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", action.diagnosticCode());
|
||||
assertEquals("", action.newText());
|
||||
assertThrows(IllegalArgumentException.class, () -> new FrontendCodeAction(
|
||||
" ",
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
|
||||
path,
|
||||
0,
|
||||
1,
|
||||
""));
|
||||
assertThrows(NullPointerException.class, () -> new FrontendCodeAction(
|
||||
"Remove duplicate reserved attribute",
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
|
||||
path,
|
||||
0,
|
||||
1,
|
||||
null));
|
||||
}
|
||||
|
||||
@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
|
||||
void workspaceSymbolNormalizesOptionalTextAndRejectsBlankNameOrInvertedOffsets() {
|
||||
final Path path = Path.of("src/main.pbs").toAbsolutePath();
|
||||
final var symbol = new FrontendWorkspaceSymbol(
|
||||
" blend ",
|
||||
FrontendSymbolKind.METHOD,
|
||||
" (dx: int) -> int ",
|
||||
" Vec ",
|
||||
path,
|
||||
4,
|
||||
9);
|
||||
assertEquals("blend", symbol.name());
|
||||
assertEquals("(dx: int) -> int", symbol.detail());
|
||||
assertEquals("Vec", symbol.containerName());
|
||||
assertEquals(path.normalize(), symbol.path());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol(
|
||||
" ",
|
||||
FrontendSymbolKind.FUNCTION,
|
||||
"",
|
||||
"",
|
||||
path,
|
||||
0,
|
||||
1));
|
||||
assertThrows(NullPointerException.class, () -> new FrontendWorkspaceSymbol(
|
||||
"fn",
|
||||
FrontendSymbolKind.FUNCTION,
|
||||
"",
|
||||
"",
|
||||
null,
|
||||
0,
|
||||
1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol(
|
||||
"fn",
|
||||
FrontendSymbolKind.FUNCTION,
|
||||
"",
|
||||
"",
|
||||
path,
|
||||
-1,
|
||||
0));
|
||||
assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol(
|
||||
"fn",
|
||||
FrontendSymbolKind.FUNCTION,
|
||||
"",
|
||||
"",
|
||||
path,
|
||||
5,
|
||||
4));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineCallHierarchyItem(
|
||||
String name,
|
||||
boolean method,
|
||||
String uri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineCallHierarchyItem {
|
||||
name = Objects.requireNonNull(name, "name").trim();
|
||||
uri = Objects.requireNonNull(uri, "uri").trim();
|
||||
if (name.isEmpty() || uri.isEmpty()) {
|
||||
throw new IllegalArgumentException("call hierarchy item identity must not be blank");
|
||||
}
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("call hierarchy range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineCallSite(
|
||||
BaselineCallHierarchyItem item,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineCallSite {
|
||||
item = Objects.requireNonNull(item, "item");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("call site coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineCodeAction(
|
||||
String title,
|
||||
String diagnosticCode,
|
||||
String uri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter,
|
||||
String newText) {
|
||||
|
||||
public BaselineCodeAction {
|
||||
title = requireText(title, "title");
|
||||
diagnosticCode = requireText(diagnosticCode, "diagnosticCode");
|
||||
uri = requireText(uri, "uri");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("code action range coordinates must not be negative");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
public record BaselineCodeActionDiagnostic(
|
||||
String code,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineCodeActionDiagnostic {
|
||||
code = code == null ? "" : code.trim();
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("diagnostic range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineCodeActions(List<BaselineCodeAction> actions) {
|
||||
public BaselineCodeActions {
|
||||
actions = actions == null ? List.of() : List.copyOf(actions);
|
||||
}
|
||||
|
||||
public static BaselineCodeActions empty() {
|
||||
return new BaselineCodeActions(List.of());
|
||||
}
|
||||
}
|
||||
@ -16,5 +16,6 @@ public enum BaselineCompletionItemKind {
|
||||
ENUM,
|
||||
ERROR,
|
||||
GLOBAL,
|
||||
CONST
|
||||
CONST,
|
||||
MODULE
|
||||
}
|
||||
|
||||
@ -9,12 +9,14 @@ public record BaselineDocumentIssue(
|
||||
int endCharacter,
|
||||
BaselineIssueSeverity severity,
|
||||
String source,
|
||||
String message) {
|
||||
String message,
|
||||
String code) {
|
||||
|
||||
public BaselineDocumentIssue {
|
||||
severity = Objects.requireNonNull(severity, "severity");
|
||||
source = requireText(source, "source");
|
||||
message = requireText(message, "message");
|
||||
code = code == null ? "" : code.trim();
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineDocumentLink(
|
||||
String targetUri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineDocumentLink {
|
||||
targetUri = requireText(targetUri, "targetUri");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("document link 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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineDocumentLinks(List<BaselineDocumentLink> links) {
|
||||
public BaselineDocumentLinks {
|
||||
links = links == null ? List.of() : List.copyOf(links);
|
||||
}
|
||||
|
||||
public static BaselineDocumentLinks empty() {
|
||||
return new BaselineDocumentLinks(List.of());
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
public record BaselineFoldingRange(
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineFoldingRange {
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("folding range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineFoldingRanges(List<BaselineFoldingRange> ranges) {
|
||||
public BaselineFoldingRanges {
|
||||
ranges = ranges == null ? List.of() : List.copyOf(ranges);
|
||||
}
|
||||
|
||||
public static BaselineFoldingRanges empty() {
|
||||
return new BaselineFoldingRanges(List.of());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineFormatting(List<BaselineTextEdit> edits) {
|
||||
public BaselineFormatting {
|
||||
edits = edits == null ? List.of() : List.copyOf(edits);
|
||||
}
|
||||
|
||||
public static BaselineFormatting empty() {
|
||||
return new BaselineFormatting(List.of());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record BaselinePrepareRename(Optional<BaselineDefinitionLocation> placeholder) {
|
||||
|
||||
public BaselinePrepareRename {
|
||||
placeholder = placeholder == null ? Optional.empty() : placeholder;
|
||||
}
|
||||
|
||||
public static BaselinePrepareRename none() {
|
||||
return new BaselinePrepareRename(Optional.empty());
|
||||
}
|
||||
|
||||
public static BaselinePrepareRename of(final BaselineDefinitionLocation location) {
|
||||
return new BaselinePrepareRename(Optional.of(location));
|
||||
}
|
||||
|
||||
public boolean renameable() {
|
||||
return placeholder.isPresent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineRename(
|
||||
boolean applied,
|
||||
String message,
|
||||
List<BaselineRenameEdit> edits) {
|
||||
|
||||
public BaselineRename {
|
||||
message = message == null ? "" : message;
|
||||
edits = edits == null ? List.of() : List.copyOf(edits);
|
||||
}
|
||||
|
||||
public static BaselineRename unsupported() {
|
||||
return new BaselineRename(false, "unsupported", List.of());
|
||||
}
|
||||
|
||||
public static BaselineRename refused(final String message) {
|
||||
return new BaselineRename(false, message == null || message.isBlank() ? "not renameable" : message, List.of());
|
||||
}
|
||||
|
||||
public static BaselineRename applied(final List<BaselineRenameEdit> edits) {
|
||||
return new BaselineRename(true, "", edits);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineRenameEdit(
|
||||
String uri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter,
|
||||
String newText) {
|
||||
|
||||
public BaselineRenameEdit {
|
||||
uri = requireText(uri, "uri");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("rename range coordinates must not be negative");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
public record BaselineSelectionRange(
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter,
|
||||
BaselineSelectionRange parent) {
|
||||
|
||||
public BaselineSelectionRange {
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("selection range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineSelectionRanges(List<BaselineSelectionRange> ranges) {
|
||||
public BaselineSelectionRanges {
|
||||
ranges = ranges == null ? List.of() : List.copyOf(ranges);
|
||||
}
|
||||
|
||||
public static BaselineSelectionRanges empty() {
|
||||
return new BaselineSelectionRanges(List.of());
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,15 @@ public record BaselineServerDescription(
|
||||
boolean signatureHelpSupported,
|
||||
boolean definitionSupported,
|
||||
boolean referencesSupported,
|
||||
boolean documentSymbolsSupported,
|
||||
boolean workspaceSymbolsSupported,
|
||||
boolean renameSupported,
|
||||
boolean codeActionsSupported,
|
||||
boolean documentLinksSupported,
|
||||
boolean foldingRangesSupported,
|
||||
boolean selectionRangesSupported,
|
||||
boolean formattingSupported,
|
||||
boolean callHierarchySupported,
|
||||
List<String> semanticTokenTypes,
|
||||
List<BaselineSemanticHostProjection> semanticHostProjections,
|
||||
List<BaselineVisualTheme> visualThemes,
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineTextEdit(
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter,
|
||||
String newText) {
|
||||
|
||||
public BaselineTextEdit {
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("text edit coordinates must not be negative");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineWorkspaceSymbol(
|
||||
String name,
|
||||
BaselineDocumentSymbolKind kind,
|
||||
String detail,
|
||||
String containerName,
|
||||
String uri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineWorkspaceSymbol {
|
||||
name = requireText(name, "name");
|
||||
kind = Objects.requireNonNull(kind, "kind");
|
||||
detail = detail == null ? "" : detail;
|
||||
containerName = containerName == null ? "" : containerName;
|
||||
uri = requireText(uri, "uri");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("workspace symbol 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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineWorkspaceSymbols(
|
||||
List<BaselineWorkspaceSymbol> symbols) {
|
||||
|
||||
public BaselineWorkspaceSymbols {
|
||||
symbols = symbols == null ? List.of() : List.copyOf(symbols);
|
||||
}
|
||||
|
||||
public static BaselineWorkspaceSymbols empty() {
|
||||
return new BaselineWorkspaceSymbols(List.of());
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,27 @@ 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.BaselineDocumentSymbols;
|
||||
import p.studio.lsp.messages.BaselineReferences;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
||||
import p.studio.lsp.messages.BaselineFoldingRanges;
|
||||
import p.studio.lsp.messages.BaselineCallHierarchyItem;
|
||||
import p.studio.lsp.messages.BaselineCallSite;
|
||||
import p.studio.lsp.messages.BaselineFormatting;
|
||||
import p.studio.lsp.messages.BaselineSelectionRanges;
|
||||
import p.studio.lsp.messages.DocumentPosition;
|
||||
import p.studio.lsp.messages.BaselinePrepareRename;
|
||||
import p.studio.lsp.messages.BaselineRename;
|
||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||
import p.studio.lsp.messages.BaselineSignatureHelp;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface LanguageServiceBridge {
|
||||
BaselineServerDescription describeServer(LspProjectContext context);
|
||||
|
||||
@ -29,9 +44,79 @@ public interface LanguageServiceBridge {
|
||||
int character,
|
||||
boolean includeDeclaration);
|
||||
|
||||
BaselineDocumentSymbols documentSymbols(LspProjectContext context, String documentUri, String text);
|
||||
|
||||
BaselineWorkspaceSymbols workspaceSymbols(LspProjectContext context, String query);
|
||||
|
||||
BaselinePrepareRename prepareRename(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
int line,
|
||||
int character);
|
||||
|
||||
BaselineRename rename(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
int line,
|
||||
int character,
|
||||
String newName);
|
||||
|
||||
BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character);
|
||||
|
||||
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);
|
||||
|
||||
default BaselineDocumentLinks documentLinks(LspProjectContext context, String documentUri, String text) {
|
||||
return BaselineDocumentLinks.empty();
|
||||
}
|
||||
|
||||
default BaselineFoldingRanges foldingRanges(LspProjectContext context, String documentUri, String text) {
|
||||
return BaselineFoldingRanges.empty();
|
||||
}
|
||||
|
||||
default BaselineFormatting formatDocument(LspProjectContext context, String documentUri, String text) {
|
||||
return BaselineFormatting.empty();
|
||||
}
|
||||
|
||||
default java.util.List<BaselineCallHierarchyItem> prepareCallHierarchy(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
int line,
|
||||
int character) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
default java.util.List<BaselineCallSite> incomingCalls(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
BaselineCallHierarchyItem target) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
default java.util.List<BaselineCallSite> outgoingCalls(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
BaselineCallHierarchyItem target) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
default BaselineSelectionRanges selectionRanges(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
List<DocumentPosition> positions) {
|
||||
return BaselineSelectionRanges.empty();
|
||||
}
|
||||
|
||||
BaselineCodeActions codeActions(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
List<BaselineCodeActionDiagnostic> diagnostics);
|
||||
|
||||
String onSave(LspProjectContext context, String documentUri);
|
||||
}
|
||||
|
||||
@ -2,17 +2,30 @@ package p.studio.lsp.services.compiler;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import p.studio.compiler.FrontendRegistryService;
|
||||
import p.studio.compiler.exceptions.BuildException;
|
||||
import p.studio.compiler.messages.*;
|
||||
import p.studio.compiler.models.*;
|
||||
import p.studio.compiler.services.FrontendCallHierarchyItem;
|
||||
import p.studio.compiler.services.FrontendCallSite;
|
||||
import p.studio.compiler.services.FrontendCodeAction;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendDocumentLink;
|
||||
import p.studio.compiler.services.FrontendFoldingRange;
|
||||
import p.studio.compiler.services.FrontendSelectionRange;
|
||||
import p.studio.compiler.services.FrontendLanguageService;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendDocumentSymbol;
|
||||
import p.studio.compiler.services.FrontendEditorialContext;
|
||||
import p.studio.compiler.services.FrontendHover;
|
||||
import p.studio.compiler.services.FrontendProvider;
|
||||
import p.studio.compiler.services.FrontendSignature;
|
||||
import p.studio.compiler.services.FrontendSignatureHelp;
|
||||
import p.studio.compiler.services.FrontendSymbolKind;
|
||||
import p.studio.compiler.services.FrontendRenameEdit;
|
||||
import p.studio.compiler.services.FrontendRenameResult;
|
||||
import p.studio.compiler.services.FrontendWorkspaceRequest;
|
||||
import p.studio.compiler.services.FrontendWorkspaceSymbol;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.compiler.workspaces.AssetSurfaceContextLoader;
|
||||
@ -45,6 +58,26 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var frontend = frontendSpec(context);
|
||||
final var presentation = frontend.getSemanticPresentation();
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
final boolean languageServicePresent = languageService.isPresent();
|
||||
final boolean codeActionsSupported = languageService
|
||||
.map(FrontendLanguageService::codeActionsSupported)
|
||||
.orElse(false);
|
||||
final boolean documentLinksSupported = languageService
|
||||
.map(FrontendLanguageService::documentLinksSupported)
|
||||
.orElse(false);
|
||||
final boolean foldingRangesSupported = languageService
|
||||
.map(FrontendLanguageService::foldingRangesSupported)
|
||||
.orElse(false);
|
||||
final boolean selectionRangesSupported = languageService
|
||||
.map(FrontendLanguageService::selectionRangesSupported)
|
||||
.orElse(false);
|
||||
final boolean formattingSupported = languageService
|
||||
.map(FrontendLanguageService::formattingSupported)
|
||||
.orElse(false);
|
||||
final boolean callHierarchySupported = languageService
|
||||
.map(FrontendLanguageService::callHierarchySupported)
|
||||
.orElse(false);
|
||||
return new BaselineServerDescription(
|
||||
"Prometeu Studio LSP",
|
||||
"0.1.0",
|
||||
@ -52,8 +85,17 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
frontendProvider(context).languageService().isPresent(),
|
||||
frontendProvider(context).languageService().isPresent(),
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
codeActionsSupported,
|
||||
documentLinksSupported,
|
||||
foldingRangesSupported,
|
||||
selectionRangesSupported,
|
||||
formattingSupported,
|
||||
callHierarchySupported,
|
||||
presentation.semanticKeys(),
|
||||
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
|
||||
presentation.themes().stream().map(this::mapVisualTheme).toList(),
|
||||
@ -69,7 +111,11 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
final Path documentPath = normalizeDocumentPath(documentUri);
|
||||
final String effectiveText = StringUtils.isBlank(text) ? "" : text;
|
||||
final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText));
|
||||
return new BaselineDocumentAnalysis(mapDiagnostics(snapshot, documentPath, effectiveText));
|
||||
return new BaselineDocumentAnalysis(mapDiagnostics(
|
||||
snapshot,
|
||||
documentPath,
|
||||
effectiveText,
|
||||
context.languageId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -208,6 +254,466 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
.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
|
||||
public BaselineWorkspaceSymbols workspaceSymbols(
|
||||
final LspProjectContext context,
|
||||
final String query) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var provider = frontendProvider(context);
|
||||
final var languageService = provider.languageService();
|
||||
if (languageService.isEmpty()) {
|
||||
return BaselineWorkspaceSymbols.empty();
|
||||
}
|
||||
final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig(
|
||||
new BuilderPipelineConfig(
|
||||
false,
|
||||
context.projectRoot().toString(),
|
||||
"core-v1",
|
||||
SourceProviderFactory.overlayUtf8(Map.of())));
|
||||
BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty());
|
||||
final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext(
|
||||
pipelineContext.resolvedWorkspace.graph().projectTable(),
|
||||
pipelineContext.fileTable,
|
||||
pipelineContext.resolvedWorkspace.stack(),
|
||||
pipelineContext.resolvedWorkspace.stdlib(),
|
||||
pipelineContext.resolvedWorkspace.target(),
|
||||
HostAdmissionContext.permissiveDefault(),
|
||||
assetSurfaceContextLoader.load(pipelineContext.resolvedWorkspace.mainProject().getRootPath()));
|
||||
return languageService.orElseThrow()
|
||||
.prepareEditorialContext(frontendPhaseContext)
|
||||
.map(editorialContext -> new BaselineWorkspaceSymbols(mapWorkspaceSymbols(
|
||||
languageService.orElseThrow().workspaceSymbols(
|
||||
new FrontendWorkspaceRequest(context.projectRoot(), query),
|
||||
editorialContext))))
|
||||
.orElseGet(BaselineWorkspaceSymbols::empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselinePrepareRename prepareRename(
|
||||
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 BaselinePrepareRename.none();
|
||||
}
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.flatMap(document -> {
|
||||
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
|
||||
final FrontendDocumentRequest request = new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
normalizeDocumentPath(documentUri),
|
||||
document.text());
|
||||
return languageService.orElseThrow().prepareRename(
|
||||
request,
|
||||
document.editorialContext(),
|
||||
offset);
|
||||
})
|
||||
.map(location -> {
|
||||
final Path documentPath = normalizeDocumentPath(documentUri);
|
||||
final List<BaselineDefinitionLocation> mapped = mapDefinitionLocations(
|
||||
List.of(location),
|
||||
documentPath,
|
||||
text == null ? "" : text);
|
||||
if (mapped.isEmpty()) {
|
||||
return BaselinePrepareRename.none();
|
||||
}
|
||||
return BaselinePrepareRename.of(mapped.getFirst());
|
||||
})
|
||||
.orElseGet(BaselinePrepareRename::none);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineRename rename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final String newName) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var provider = frontendProvider(context);
|
||||
final var languageService = provider.languageService();
|
||||
if (languageService.isEmpty()) {
|
||||
return BaselineRename.unsupported();
|
||||
}
|
||||
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 FrontendRenameResult result = languageService.orElseThrow().rename(
|
||||
request,
|
||||
document.editorialContext(),
|
||||
offset,
|
||||
newName);
|
||||
if (!result.applied()) {
|
||||
if (result.status() == FrontendRenameResult.Status.UNSUPPORTED) {
|
||||
return BaselineRename.unsupported();
|
||||
}
|
||||
return BaselineRename.refused(result.message());
|
||||
}
|
||||
return BaselineRename.applied(mapRenameEdits(
|
||||
result.edits(),
|
||||
request.documentPath(),
|
||||
document.text()));
|
||||
})
|
||||
.orElseGet(() -> BaselineRename.refused("not renameable"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineDocumentLinks documentLinks(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
if (languageService.isEmpty() || !languageService.orElseThrow().documentLinksSupported()) {
|
||||
return BaselineDocumentLinks.empty();
|
||||
}
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.map(document -> {
|
||||
final FrontendDocumentRequest request = new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
normalizeDocumentPath(documentUri),
|
||||
document.text());
|
||||
return new BaselineDocumentLinks(mapDocumentLinks(
|
||||
languageService.orElseThrow().documentLinks(request, document.editorialContext()),
|
||||
document.text()));
|
||||
})
|
||||
.orElseGet(BaselineDocumentLinks::empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BaselineCallHierarchyItem> prepareCallHierarchy(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
final var languageService = callHierarchyService(context);
|
||||
if (languageService == null) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.map(document -> {
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(document.text());
|
||||
final int offset = mapper.byteOffsetOf(line, character);
|
||||
return languageService.prepareCallHierarchy(
|
||||
callRequest(context, documentUri, document.text()),
|
||||
document.editorialContext(),
|
||||
offset)
|
||||
.stream()
|
||||
.map(item -> toBaselineItem(item, documentUri, mapper))
|
||||
.toList();
|
||||
})
|
||||
.orElseGet(List::of);
|
||||
} catch (final BuildException ignored) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BaselineCallSite> incomingCalls(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final BaselineCallHierarchyItem target) {
|
||||
return directedCalls(context, documentUri, text, target, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BaselineCallSite> outgoingCalls(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final BaselineCallHierarchyItem target) {
|
||||
return directedCalls(context, documentUri, text, target, false);
|
||||
}
|
||||
|
||||
private List<BaselineCallSite> directedCalls(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final BaselineCallHierarchyItem target,
|
||||
final boolean incoming) {
|
||||
final var languageService = callHierarchyService(context);
|
||||
if (languageService == null || target == null) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.map(document -> {
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(document.text());
|
||||
final FrontendCallHierarchyItem frontendTarget = new FrontendCallHierarchyItem(
|
||||
target.name(),
|
||||
target.method(),
|
||||
mapper.byteOffsetOf(target.startLine(), target.startCharacter()),
|
||||
mapper.byteOffsetOf(target.endLine(), target.endCharacter()));
|
||||
final List<FrontendCallSite> calls = incoming
|
||||
? languageService.incomingCalls(
|
||||
callRequest(context, documentUri, document.text()),
|
||||
document.editorialContext(),
|
||||
frontendTarget)
|
||||
: languageService.outgoingCalls(
|
||||
callRequest(context, documentUri, document.text()),
|
||||
document.editorialContext(),
|
||||
frontendTarget);
|
||||
return calls.stream().map(call -> new BaselineCallSite(
|
||||
toBaselineItem(call.item(), documentUri, mapper),
|
||||
mapper.positionOf(call.startOffset()).line(),
|
||||
mapper.positionOf(call.startOffset()).character(),
|
||||
mapper.positionOf(call.endOffset()).line(),
|
||||
mapper.positionOf(call.endOffset()).character())).toList();
|
||||
})
|
||||
.orElseGet(List::of);
|
||||
} catch (final BuildException ignored) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private FrontendLanguageService callHierarchyService(final LspProjectContext context) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
if (languageService.isEmpty() || !languageService.orElseThrow().callHierarchySupported()) {
|
||||
return null;
|
||||
}
|
||||
return languageService.orElseThrow();
|
||||
}
|
||||
|
||||
private FrontendDocumentRequest callRequest(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
return new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
normalizeDocumentPath(documentUri),
|
||||
text == null ? "" : text);
|
||||
}
|
||||
|
||||
private BaselineCallHierarchyItem toBaselineItem(
|
||||
final FrontendCallHierarchyItem item,
|
||||
final String documentUri,
|
||||
final DocumentPositionMapper mapper) {
|
||||
final DocumentPosition start = mapper.positionOf(item.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(item.endOffset());
|
||||
return new BaselineCallHierarchyItem(
|
||||
item.name(),
|
||||
item.method(),
|
||||
documentUri,
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineFormatting formatDocument(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
if (languageService.isEmpty() || !languageService.orElseThrow().formattingSupported()) {
|
||||
return BaselineFormatting.empty();
|
||||
}
|
||||
final String effectiveText = text == null ? "" : text;
|
||||
final String formatted = languageService.orElseThrow().formatDocument(new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
normalizeDocumentPath(documentUri),
|
||||
effectiveText)).orElse(effectiveText);
|
||||
if (formatted.equals(effectiveText)) {
|
||||
return BaselineFormatting.empty();
|
||||
}
|
||||
final DocumentPosition end = new DocumentPositionMapper(effectiveText)
|
||||
.positionOf(effectiveText.getBytes(StandardCharsets.UTF_8).length);
|
||||
return new BaselineFormatting(List.of(new BaselineTextEdit(
|
||||
0,
|
||||
0,
|
||||
end.line(),
|
||||
end.character(),
|
||||
formatted)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineFoldingRanges foldingRanges(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
return structureRanges(context, documentUri, text, true, List.of()).folding();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSelectionRanges selectionRanges(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final List<DocumentPosition> positions) {
|
||||
return structureRanges(context, documentUri, text, false, positions == null ? List.of() : positions).selection();
|
||||
}
|
||||
|
||||
private StructureRanges structureRanges(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final boolean folding,
|
||||
final List<DocumentPosition> positions) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
if (languageService.isEmpty()) {
|
||||
return StructureRanges.empty();
|
||||
}
|
||||
final boolean supported = folding
|
||||
? languageService.orElseThrow().foldingRangesSupported()
|
||||
: languageService.orElseThrow().selectionRangesSupported();
|
||||
if (!supported) {
|
||||
return StructureRanges.empty();
|
||||
}
|
||||
final String effectiveText = text == null ? "" : text;
|
||||
final FrontendDocumentRequest request = new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
normalizeDocumentPath(documentUri),
|
||||
effectiveText);
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(effectiveText);
|
||||
if (folding) {
|
||||
return new StructureRanges(new BaselineFoldingRanges(mapFoldingRanges(
|
||||
languageService.orElseThrow().foldingRanges(request, null),
|
||||
mapper)), BaselineSelectionRanges.empty());
|
||||
}
|
||||
final ArrayList<Integer> offsets = new ArrayList<>();
|
||||
for (final DocumentPosition position : positions) {
|
||||
if (position == null) {
|
||||
offsets.add(0);
|
||||
continue;
|
||||
}
|
||||
offsets.add(mapper.byteOffsetOf(position.line(), position.character()));
|
||||
}
|
||||
return new StructureRanges(
|
||||
BaselineFoldingRanges.empty(),
|
||||
new BaselineSelectionRanges(mapSelectionRanges(
|
||||
languageService.orElseThrow().selectionRanges(request, null, offsets),
|
||||
mapper)));
|
||||
}
|
||||
|
||||
private List<BaselineFoldingRange> mapFoldingRanges(
|
||||
final List<FrontendFoldingRange> ranges,
|
||||
final DocumentPositionMapper mapper) {
|
||||
if (ranges == null || ranges.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<BaselineFoldingRange> mapped = new ArrayList<>();
|
||||
for (final FrontendFoldingRange range : ranges) {
|
||||
final DocumentPosition start = mapper.positionOf(range.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(range.endOffset());
|
||||
mapped.add(new BaselineFoldingRange(
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineSelectionRange> mapSelectionRanges(
|
||||
final List<FrontendSelectionRange> ranges,
|
||||
final DocumentPositionMapper mapper) {
|
||||
if (ranges == null || ranges.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<BaselineSelectionRange> mapped = new ArrayList<>();
|
||||
for (final FrontendSelectionRange range : ranges) {
|
||||
mapped.add(mapSelectionRange(range, mapper));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private BaselineSelectionRange mapSelectionRange(
|
||||
final FrontendSelectionRange range,
|
||||
final DocumentPositionMapper mapper) {
|
||||
if (range == null) {
|
||||
return null;
|
||||
}
|
||||
final DocumentPosition start = mapper.positionOf(range.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(range.endOffset());
|
||||
return new BaselineSelectionRange(
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character(),
|
||||
mapSelectionRange(range.parent(), mapper));
|
||||
}
|
||||
|
||||
private record StructureRanges(BaselineFoldingRanges folding, BaselineSelectionRanges selection) {
|
||||
private static StructureRanges empty() {
|
||||
return new StructureRanges(BaselineFoldingRanges.empty(), BaselineSelectionRanges.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final List<BaselineCodeActionDiagnostic> diagnostics) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
if (languageService.isEmpty() || !languageService.orElseThrow().codeActionsSupported()) {
|
||||
return BaselineCodeActions.empty();
|
||||
}
|
||||
if (diagnostics == null || diagnostics.isEmpty()) {
|
||||
return BaselineCodeActions.empty();
|
||||
}
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.map(document -> {
|
||||
final Path documentPath = normalizeDocumentPath(documentUri);
|
||||
final FrontendDocumentRequest request = new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
documentPath,
|
||||
document.text());
|
||||
final List<FrontendCodeAction> repairs = languageService.orElseThrow().codeActions(
|
||||
request,
|
||||
document.editorialContext());
|
||||
return new BaselineCodeActions(mapCodeActions(
|
||||
repairs,
|
||||
documentUri,
|
||||
documentPath,
|
||||
document.text(),
|
||||
diagnostics));
|
||||
})
|
||||
.orElseGet(BaselineCodeActions::empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext context,
|
||||
@ -253,7 +759,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
documentPath,
|
||||
effectiveText);
|
||||
final List<FrontendSemanticToken> tokens = provider.languageService()
|
||||
.map(languageService -> languageService.semanticTokens(request))
|
||||
.map(languageService -> semanticTokens(languageService, context, documentUri, documentPath, request))
|
||||
.orElseGet(List::of);
|
||||
final var positionMapper = new DocumentPositionMapper(effectiveText);
|
||||
final var semanticTokens = new ArrayList<BaselineSemanticToken>();
|
||||
@ -272,6 +778,26 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
return new BaselineSemanticTokens(presentation.semanticKeys(), semanticTokens);
|
||||
}
|
||||
|
||||
private List<FrontendSemanticToken> semanticTokens(
|
||||
final FrontendLanguageService languageService,
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final Path documentPath,
|
||||
final FrontendDocumentRequest request) {
|
||||
try {
|
||||
return editorialDocument(context, documentUri, request.documentText())
|
||||
.map(document -> languageService.semanticTokens(
|
||||
new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
documentPath,
|
||||
document.text()),
|
||||
document.editorialContext()))
|
||||
.orElseGet(() -> languageService.semanticTokens(request));
|
||||
} catch (final BuildException ignored) {
|
||||
return languageService.semanticTokens(request);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(
|
||||
final LspProjectContext context,
|
||||
@ -354,7 +880,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
private List<BaselineDocumentIssue> mapDiagnostics(
|
||||
final AnalysisSnapshot snapshot,
|
||||
final Path documentPath,
|
||||
final String text) {
|
||||
final String text,
|
||||
final String languageId) {
|
||||
final ArrayList<BaselineDocumentIssue> issues = new ArrayList<>();
|
||||
final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text);
|
||||
for (final BuildingIssue diagnostic : snapshot.diagnostics()) {
|
||||
@ -369,26 +896,29 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) {
|
||||
continue;
|
||||
}
|
||||
issues.add(toBaselineIssue(diagnostic, positionMapper));
|
||||
issues.add(toBaselineIssue(diagnostic, positionMapper, languageId));
|
||||
}
|
||||
return List.copyOf(issues);
|
||||
}
|
||||
|
||||
private BaselineDocumentIssue toBaselineIssue(
|
||||
final BuildingIssue diagnostic,
|
||||
final DocumentPositionMapper positionMapper) {
|
||||
final DocumentPositionMapper positionMapper,
|
||||
final String languageId) {
|
||||
final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart();
|
||||
final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd());
|
||||
final DocumentPosition start = positionMapper.positionOf(startOffset);
|
||||
final DocumentPosition end = positionMapper.positionOf(endOffset);
|
||||
final String code = diagnostic.getCode() == null ? "" : diagnostic.getCode().trim();
|
||||
return new BaselineDocumentIssue(
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character(),
|
||||
diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING,
|
||||
diagnostic.getCode() == null || diagnostic.getCode().isBlank() ? "Prometeu Studio" : diagnostic.getCode(),
|
||||
diagnostic.getMessage());
|
||||
languageId,
|
||||
diagnostic.getMessage(),
|
||||
code);
|
||||
}
|
||||
|
||||
private Path normalizeDocumentPath(final String documentUri) {
|
||||
@ -484,6 +1014,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
case ERROR -> BaselineCompletionItemKind.ERROR;
|
||||
case GLOBAL -> BaselineCompletionItemKind.GLOBAL;
|
||||
case CONST -> BaselineCompletionItemKind.CONST;
|
||||
case MODULE -> BaselineCompletionItemKind.MODULE;
|
||||
};
|
||||
}
|
||||
|
||||
@ -491,6 +1022,172 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
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<BaselineWorkspaceSymbol> mapWorkspaceSymbols(final List<FrontendWorkspaceSymbol> symbols) {
|
||||
if (symbols == null || symbols.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final LinkedHashMap<Path, DocumentPositionMapper> mappers = new LinkedHashMap<>();
|
||||
final ArrayList<BaselineWorkspaceSymbol> mapped = new ArrayList<>();
|
||||
for (final FrontendWorkspaceSymbol symbol : symbols) {
|
||||
final DocumentPositionMapper mapper = mappers.computeIfAbsent(symbol.path(), this::mapperForPath);
|
||||
if (mapper == null) {
|
||||
continue;
|
||||
}
|
||||
final DocumentPosition start = mapper.positionOf(symbol.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(symbol.endOffset());
|
||||
mapped.add(new BaselineWorkspaceSymbol(
|
||||
symbol.name(),
|
||||
BaselineDocumentSymbolKind.valueOf(symbol.kind().name()),
|
||||
symbol.detail(),
|
||||
symbol.containerName(),
|
||||
symbol.path().toUri().toString(),
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private DocumentPositionMapper mapperForPath(final Path path) {
|
||||
if (path == null || !Files.isRegularFile(path)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new DocumentPositionMapper(Files.readString(path, StandardCharsets.UTF_8));
|
||||
} catch (final Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<BaselineCodeAction> mapCodeActions(
|
||||
final List<FrontendCodeAction> repairs,
|
||||
final String documentUri,
|
||||
final Path documentPath,
|
||||
final String documentText,
|
||||
final List<BaselineCodeActionDiagnostic> diagnostics) {
|
||||
if (repairs == null || repairs.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(documentText == null ? "" : documentText);
|
||||
final ArrayList<BaselineCodeAction> mapped = new ArrayList<>();
|
||||
for (final FrontendCodeAction repair : repairs) {
|
||||
if (!repair.path().toAbsolutePath().normalize().equals(documentPath)) {
|
||||
continue;
|
||||
}
|
||||
final DocumentPosition start = mapper.positionOf(repair.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(repair.endOffset());
|
||||
final boolean matched = diagnostics.stream().anyMatch(diagnostic ->
|
||||
repair.diagnosticCode().equals(diagnostic.code())
|
||||
&& diagnostic.startLine() == start.line()
|
||||
&& diagnostic.startCharacter() == start.character()
|
||||
&& diagnostic.endLine() == end.line()
|
||||
&& diagnostic.endCharacter() == end.character());
|
||||
if (!matched) {
|
||||
continue;
|
||||
}
|
||||
mapped.add(new BaselineCodeAction(
|
||||
repair.title(),
|
||||
repair.diagnosticCode(),
|
||||
documentUri,
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character(),
|
||||
repair.newText()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineRenameEdit> mapRenameEdits(
|
||||
final List<FrontendRenameEdit> edits,
|
||||
final Path currentDocumentPath,
|
||||
final String currentDocumentText) {
|
||||
if (edits == null || edits.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<BaselineRenameEdit> mapped = new ArrayList<>();
|
||||
for (final FrontendRenameEdit edit : edits) {
|
||||
final String targetText = textForDefinitionTarget(edit.path(), currentDocumentPath, currentDocumentText);
|
||||
if (targetText == null) {
|
||||
continue;
|
||||
}
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(targetText);
|
||||
final DocumentPosition start = mapper.positionOf(edit.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(edit.endOffset());
|
||||
mapped.add(new BaselineRenameEdit(
|
||||
edit.path().toUri().toString(),
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character(),
|
||||
edit.newText()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineDocumentLink> mapDocumentLinks(
|
||||
final List<FrontendDocumentLink> links,
|
||||
final String currentDocumentText) {
|
||||
if (links == null || links.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(
|
||||
currentDocumentText == null ? "" : currentDocumentText);
|
||||
final ArrayList<BaselineDocumentLink> mapped = new ArrayList<>();
|
||||
for (final FrontendDocumentLink link : links) {
|
||||
final DocumentPosition start = mapper.positionOf(link.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(link.endOffset());
|
||||
mapped.add(new BaselineDocumentLink(
|
||||
link.path().toUri().toString(),
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineDefinitionLocation> mapDefinitionLocations(
|
||||
final List<FrontendDefinitionLocation> locations,
|
||||
final Path currentDocumentPath,
|
||||
|
||||
@ -29,7 +29,7 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
|
||||
compilerBridge,
|
||||
new Lsp4jProtocolMessageMapper(),
|
||||
null,
|
||||
new PrometeuWorkspaceService());
|
||||
null);
|
||||
}
|
||||
|
||||
PrometeuLanguageServer(
|
||||
@ -44,7 +44,9 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
|
||||
this.textDocumentService = textDocumentService != null
|
||||
? textDocumentService
|
||||
: new PrometeuTextDocumentService(project, languageServiceBridge, protocolMessageMapper);
|
||||
this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService");
|
||||
this.workspaceService = workspaceService != null
|
||||
? workspaceService
|
||||
: new PrometeuWorkspaceService(project, languageServiceBridge, protocolMessageMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
package p.studio.lsp.services.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
import p.studio.lsp.messages.BaselineCallHierarchyItem;
|
||||
import p.studio.lsp.messages.BaselineCallSite;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.DocumentPosition;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@ -127,6 +136,156 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
includeDeclaration)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior>> prepareRename(
|
||||
final PrepareRenameParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapPrepareRename(
|
||||
languageServiceBridge.prepareRename(
|
||||
project,
|
||||
uri,
|
||||
text,
|
||||
params.getPosition().getLine(),
|
||||
params.getPosition().getCharacter())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<WorkspaceEdit> rename(final RenameParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
final var result = languageServiceBridge.rename(
|
||||
project,
|
||||
uri,
|
||||
text,
|
||||
params.getPosition().getLine(),
|
||||
params.getPosition().getCharacter(),
|
||||
params.getNewName());
|
||||
if (!result.applied()) {
|
||||
final ResponseError error = new ResponseError(
|
||||
ResponseErrorCode.InvalidRequest,
|
||||
result.message() == null || result.message().isBlank() ? "not renameable" : result.message(),
|
||||
null);
|
||||
return CompletableFuture.failedFuture(new ResponseErrorException(error));
|
||||
}
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<CallHierarchyItem>> prepareCallHierarchy(final CallHierarchyPrepareParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapCallHierarchyItems(
|
||||
languageServiceBridge.prepareCallHierarchy(
|
||||
project,
|
||||
uri,
|
||||
text,
|
||||
params.getPosition().getLine(),
|
||||
params.getPosition().getCharacter())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<CallHierarchyIncomingCall>> callHierarchyIncomingCalls(
|
||||
final CallHierarchyIncomingCallsParams params) {
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapIncomingCalls(
|
||||
directedCallSites(params.getItem(), true)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<CallHierarchyOutgoingCall>> callHierarchyOutgoingCalls(
|
||||
final CallHierarchyOutgoingCallsParams params) {
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapOutgoingCalls(
|
||||
directedCallSites(params.getItem(), false)));
|
||||
}
|
||||
|
||||
private List<BaselineCallSite> directedCallSites(final CallHierarchyItem item, final boolean incoming) {
|
||||
if (item == null || item.getUri() == null || item.getSelectionRange() == null) {
|
||||
return List.of();
|
||||
}
|
||||
final String text = documentTextByUri.get(item.getUri());
|
||||
final BaselineCallHierarchyItem target = new BaselineCallHierarchyItem(
|
||||
item.getName(),
|
||||
item.getKind() == SymbolKind.Method,
|
||||
item.getUri(),
|
||||
item.getSelectionRange().getStart().getLine(),
|
||||
item.getSelectionRange().getStart().getCharacter(),
|
||||
item.getSelectionRange().getEnd().getLine(),
|
||||
item.getSelectionRange().getEnd().getCharacter());
|
||||
return incoming
|
||||
? languageServiceBridge.incomingCalls(project, item.getUri(), text, target)
|
||||
: languageServiceBridge.outgoingCalls(project, item.getUri(), text, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<? extends TextEdit>> formatting(final DocumentFormattingParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapFormatting(
|
||||
languageServiceBridge.formatDocument(project, uri, text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<FoldingRange>> foldingRange(final FoldingRangeRequestParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapFoldingRanges(
|
||||
languageServiceBridge.foldingRanges(project, uri, text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<SelectionRange>> selectionRange(final SelectionRangeParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
final ArrayList<DocumentPosition> positions = new ArrayList<>();
|
||||
if (params.getPositions() != null) {
|
||||
for (final Position position : params.getPositions()) {
|
||||
if (position == null) {
|
||||
continue;
|
||||
}
|
||||
positions.add(new DocumentPosition(position.getLine(), position.getCharacter()));
|
||||
}
|
||||
}
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapSelectionRanges(
|
||||
languageServiceBridge.selectionRanges(project, uri, text, positions)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<DocumentLink>> documentLink(final DocumentLinkParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapDocumentLinks(
|
||||
languageServiceBridge.documentLinks(project, uri, text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(final CodeActionParams params) {
|
||||
if (!quickFixRequested(params.getContext())) {
|
||||
return CompletableFuture.completedFuture(List.of());
|
||||
}
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
final List<Diagnostic> contextDiagnostics = params.getContext() == null
|
||||
|| params.getContext().getDiagnostics() == null
|
||||
? List.of()
|
||||
: params.getContext().getDiagnostics();
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapCodeActions(
|
||||
languageServiceBridge.codeActions(project, uri, text, baselineDiagnostics(contextDiagnostics)),
|
||||
contextDiagnostics));
|
||||
}
|
||||
|
||||
@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
|
||||
public CompletableFuture<SignatureHelp> signatureHelp(final SignatureHelpParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
@ -148,6 +307,52 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
languageServiceBridge.semanticTokens(project, uri, text)));
|
||||
}
|
||||
|
||||
private static boolean quickFixRequested(final CodeActionContext context) {
|
||||
if (context == null || context.getOnly() == null || context.getOnly().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (final String kind : context.getOnly()) {
|
||||
if (kind == null || kind.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
if (CodeActionKind.QuickFix.equals(kind) || CodeActionKind.QuickFix.startsWith(kind + ".")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<BaselineCodeActionDiagnostic> baselineDiagnostics(final List<Diagnostic> diagnostics) {
|
||||
final ArrayList<BaselineCodeActionDiagnostic> mapped = new ArrayList<>();
|
||||
for (final Diagnostic diagnostic : diagnostics) {
|
||||
if (diagnostic == null || diagnostic.getRange() == null
|
||||
|| diagnostic.getRange().getStart() == null
|
||||
|| diagnostic.getRange().getEnd() == null) {
|
||||
continue;
|
||||
}
|
||||
mapped.add(new BaselineCodeActionDiagnostic(
|
||||
diagnosticCode(diagnostic),
|
||||
diagnostic.getRange().getStart().getLine(),
|
||||
diagnostic.getRange().getStart().getCharacter(),
|
||||
diagnostic.getRange().getEnd().getLine(),
|
||||
diagnostic.getRange().getEnd().getCharacter()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private static String diagnosticCode(final Diagnostic diagnostic) {
|
||||
if (diagnostic.getCode() == null) {
|
||||
return "";
|
||||
}
|
||||
if (diagnostic.getCode().isLeft()) {
|
||||
return diagnostic.getCode().getLeft() == null ? "" : diagnostic.getCode().getLeft();
|
||||
}
|
||||
if (diagnostic.getCode().isRight() && diagnostic.getCode().getRight() != null) {
|
||||
return diagnostic.getCode().getRight().toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private void publishAnalysis(
|
||||
final String uri,
|
||||
final String text) {
|
||||
|
||||
@ -2,9 +2,36 @@ package p.studio.lsp.services.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.DidChangeConfigurationParams;
|
||||
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
|
||||
import org.eclipse.lsp4j.SymbolInformation;
|
||||
import org.eclipse.lsp4j.WorkspaceSymbol;
|
||||
import org.eclipse.lsp4j.WorkspaceSymbolParams;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.eclipse.lsp4j.services.WorkspaceService;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public final class PrometeuWorkspaceService implements WorkspaceService {
|
||||
private final LspProjectContext project;
|
||||
private final LanguageServiceBridge languageServiceBridge;
|
||||
private final ProtocolMessageMapper protocolMessageMapper;
|
||||
|
||||
public PrometeuWorkspaceService() {
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
public PrometeuWorkspaceService(
|
||||
final LspProjectContext project,
|
||||
final LanguageServiceBridge languageServiceBridge,
|
||||
final ProtocolMessageMapper protocolMessageMapper) {
|
||||
this.project = project;
|
||||
this.languageServiceBridge = languageServiceBridge;
|
||||
this.protocolMessageMapper = protocolMessageMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didChangeConfiguration(final DidChangeConfigurationParams params) {
|
||||
}
|
||||
@ -12,4 +39,15 @@ public final class PrometeuWorkspaceService implements WorkspaceService {
|
||||
@Override
|
||||
public void didChangeWatchedFiles(final DidChangeWatchedFilesParams params) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>>> symbol(
|
||||
final WorkspaceSymbolParams params) {
|
||||
if (project == null || languageServiceBridge == null || protocolMessageMapper == null) {
|
||||
return CompletableFuture.completedFuture(Either.forLeft(List.of()));
|
||||
}
|
||||
final String query = params == null || params.getQuery() == null ? "" : params.getQuery();
|
||||
return CompletableFuture.completedFuture(Either.forLeft(
|
||||
protocolMessageMapper.mapWorkspaceSymbols(languageServiceBridge.workspaceSymbols(project, query))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package p.studio.lsp.services.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
import p.studio.lsp.messages.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -38,6 +40,36 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
}
|
||||
capabilities.setDefinitionProvider(description.definitionSupported());
|
||||
capabilities.setReferencesProvider(description.referencesSupported());
|
||||
capabilities.setDocumentSymbolProvider(description.documentSymbolsSupported());
|
||||
capabilities.setWorkspaceSymbolProvider(description.workspaceSymbolsSupported());
|
||||
if (description.renameSupported()) {
|
||||
final RenameOptions renameOptions = new RenameOptions();
|
||||
renameOptions.setPrepareProvider(true);
|
||||
capabilities.setRenameProvider(renameOptions);
|
||||
}
|
||||
if (description.codeActionsSupported()) {
|
||||
final CodeActionOptions codeActionOptions = new CodeActionOptions();
|
||||
codeActionOptions.setResolveProvider(false);
|
||||
codeActionOptions.setCodeActionKinds(List.of(CodeActionKind.QuickFix));
|
||||
capabilities.setCodeActionProvider(codeActionOptions);
|
||||
}
|
||||
if (description.documentLinksSupported()) {
|
||||
final DocumentLinkOptions documentLinkOptions = new DocumentLinkOptions();
|
||||
documentLinkOptions.setResolveProvider(false);
|
||||
capabilities.setDocumentLinkProvider(documentLinkOptions);
|
||||
}
|
||||
if (description.foldingRangesSupported()) {
|
||||
capabilities.setFoldingRangeProvider(true);
|
||||
}
|
||||
if (description.selectionRangesSupported()) {
|
||||
capabilities.setSelectionRangeProvider(true);
|
||||
}
|
||||
if (description.formattingSupported()) {
|
||||
capabilities.setDocumentFormattingProvider(true);
|
||||
}
|
||||
if (description.callHierarchySupported()) {
|
||||
capabilities.setCallHierarchyProvider(true);
|
||||
}
|
||||
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
|
||||
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
|
||||
semanticTokens.setFull(true);
|
||||
@ -101,6 +133,271 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
final BaselinePrepareRename prepareRename) {
|
||||
if (prepareRename == null || prepareRename.placeholder().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
final BaselineDefinitionLocation location = prepareRename.placeholder().orElseThrow();
|
||||
return Either3.forFirst(new Range(
|
||||
new Position(location.startLine(), location.startCharacter()),
|
||||
new Position(location.endLine(), location.endCharacter())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkspaceEdit mapRename(final BaselineRename rename) {
|
||||
final WorkspaceEdit edit = new WorkspaceEdit();
|
||||
if (rename == null || !rename.applied() || rename.edits().isEmpty()) {
|
||||
edit.setChanges(Map.of());
|
||||
return edit;
|
||||
}
|
||||
final LinkedHashMap<String, List<TextEdit>> changes = new LinkedHashMap<>();
|
||||
for (final BaselineRenameEdit renameEdit : rename.edits()) {
|
||||
changes.computeIfAbsent(renameEdit.uri(), ignored -> new ArrayList<>()).add(new TextEdit(
|
||||
new Range(
|
||||
new Position(renameEdit.startLine(), renameEdit.startCharacter()),
|
||||
new Position(renameEdit.endLine(), renameEdit.endCharacter())),
|
||||
renameEdit.newText()));
|
||||
}
|
||||
edit.setChanges(changes);
|
||||
return edit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DocumentLink> mapDocumentLinks(final BaselineDocumentLinks links) {
|
||||
if (links == null || links.links().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<DocumentLink> mapped = new ArrayList<>();
|
||||
for (final BaselineDocumentLink link : links.links()) {
|
||||
final DocumentLink documentLink = new DocumentLink();
|
||||
documentLink.setRange(new Range(
|
||||
new Position(link.startLine(), link.startCharacter()),
|
||||
new Position(link.endLine(), link.endCharacter())));
|
||||
documentLink.setTarget(link.targetUri());
|
||||
mapped.add(documentLink);
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CallHierarchyItem> mapCallHierarchyItems(final List<BaselineCallHierarchyItem> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return items.stream().map(this::mapCallHierarchyItem).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CallHierarchyIncomingCall> mapIncomingCalls(final List<BaselineCallSite> calls) {
|
||||
if (calls == null || calls.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return calls.stream().map(call -> new CallHierarchyIncomingCall(
|
||||
mapCallHierarchyItem(call.item()),
|
||||
List.of(callRange(call)))).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CallHierarchyOutgoingCall> mapOutgoingCalls(final List<BaselineCallSite> calls) {
|
||||
if (calls == null || calls.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return calls.stream().map(call -> new CallHierarchyOutgoingCall(
|
||||
mapCallHierarchyItem(call.item()),
|
||||
List.of(callRange(call)))).toList();
|
||||
}
|
||||
|
||||
private CallHierarchyItem mapCallHierarchyItem(final BaselineCallHierarchyItem item) {
|
||||
final CallHierarchyItem mapped = new CallHierarchyItem();
|
||||
mapped.setName(item.name());
|
||||
mapped.setKind(item.method() ? SymbolKind.Method : SymbolKind.Function);
|
||||
mapped.setUri(item.uri());
|
||||
final Range range = new Range(
|
||||
new Position(item.startLine(), item.startCharacter()),
|
||||
new Position(item.endLine(), item.endCharacter()));
|
||||
mapped.setRange(range);
|
||||
mapped.setSelectionRange(range);
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private Range callRange(final BaselineCallSite call) {
|
||||
return new Range(
|
||||
new Position(call.startLine(), call.startCharacter()),
|
||||
new Position(call.endLine(), call.endCharacter()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TextEdit> mapFormatting(final BaselineFormatting formatting) {
|
||||
if (formatting == null || formatting.edits().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<TextEdit> mapped = new ArrayList<>();
|
||||
for (final BaselineTextEdit edit : formatting.edits()) {
|
||||
mapped.add(new TextEdit(
|
||||
new Range(
|
||||
new Position(edit.startLine(), edit.startCharacter()),
|
||||
new Position(edit.endLine(), edit.endCharacter())),
|
||||
edit.newText()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FoldingRange> mapFoldingRanges(final BaselineFoldingRanges ranges) {
|
||||
if (ranges == null || ranges.ranges().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<FoldingRange> mapped = new ArrayList<>();
|
||||
for (final BaselineFoldingRange range : ranges.ranges()) {
|
||||
final FoldingRange foldingRange = new FoldingRange();
|
||||
foldingRange.setStartLine(range.startLine());
|
||||
foldingRange.setStartCharacter(range.startCharacter());
|
||||
foldingRange.setEndLine(range.endLine());
|
||||
foldingRange.setEndCharacter(range.endCharacter());
|
||||
mapped.add(foldingRange);
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SelectionRange> mapSelectionRanges(final BaselineSelectionRanges ranges) {
|
||||
if (ranges == null || ranges.ranges().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<SelectionRange> mapped = new ArrayList<>();
|
||||
for (final BaselineSelectionRange range : ranges.ranges()) {
|
||||
mapped.add(mapSelectionRange(range));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private SelectionRange mapSelectionRange(final BaselineSelectionRange range) {
|
||||
if (range == null) {
|
||||
return null;
|
||||
}
|
||||
final SelectionRange selectionRange = new SelectionRange();
|
||||
selectionRange.setRange(new Range(
|
||||
new Position(range.startLine(), range.startCharacter()),
|
||||
new Position(range.endLine(), range.endCharacter())));
|
||||
selectionRange.setParent(mapSelectionRange(range.parent()));
|
||||
return selectionRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Either<Command, CodeAction>> mapCodeActions(
|
||||
final BaselineCodeActions actions,
|
||||
final List<Diagnostic> contextDiagnostics) {
|
||||
if (actions == null || actions.actions().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<Either<Command, CodeAction>> mapped = new ArrayList<>();
|
||||
for (final BaselineCodeAction action : actions.actions()) {
|
||||
final Range range = new Range(
|
||||
new Position(action.startLine(), action.startCharacter()),
|
||||
new Position(action.endLine(), action.endCharacter()));
|
||||
final TextEdit textEdit = new TextEdit(range, action.newText());
|
||||
final WorkspaceEdit edit = new WorkspaceEdit();
|
||||
edit.setChanges(Map.of(action.uri(), List.of(textEdit)));
|
||||
final CodeAction codeAction = new CodeAction();
|
||||
codeAction.setTitle(action.title());
|
||||
codeAction.setKind(CodeActionKind.QuickFix);
|
||||
codeAction.setEdit(edit);
|
||||
codeAction.setDiagnostics(matchingDiagnostics(contextDiagnostics, action.diagnosticCode(), range));
|
||||
mapped.add(Either.forRight(codeAction));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SymbolInformation> mapWorkspaceSymbols(final BaselineWorkspaceSymbols symbols) {
|
||||
if (symbols == null || symbols.symbols().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<SymbolInformation> mapped = new ArrayList<>();
|
||||
for (final BaselineWorkspaceSymbol symbol : symbols.symbols()) {
|
||||
final SymbolInformation information = new SymbolInformation();
|
||||
information.setName(symbol.name());
|
||||
information.setKind(toLspWorkspaceSymbolKind(symbol.kind(), symbol.containerName()));
|
||||
information.setContainerName(symbol.containerName());
|
||||
information.setLocation(new Location(
|
||||
symbol.uri(),
|
||||
new Range(
|
||||
new Position(symbol.startLine(), symbol.startCharacter()),
|
||||
new Position(symbol.endLine(), symbol.endCharacter()))));
|
||||
mapped.add(information);
|
||||
}
|
||||
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 toLspWorkspaceSymbolKind(
|
||||
final BaselineDocumentSymbolKind kind,
|
||||
final String containerName) {
|
||||
if (containerName != null
|
||||
&& !containerName.isBlank()
|
||||
&& (kind == BaselineDocumentSymbolKind.ENUM || kind == BaselineDocumentSymbolKind.ERROR)) {
|
||||
return SymbolKind.EnumMember;
|
||||
}
|
||||
return toLspSymbolKind(kind, null);
|
||||
}
|
||||
|
||||
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) {
|
||||
return locations.stream()
|
||||
.map(location -> new Location(
|
||||
@ -196,6 +493,9 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
diagnostic.setSeverity(mapSeverity(issue.severity()));
|
||||
diagnostic.setSource(issue.source());
|
||||
diagnostic.setMessage(issue.message());
|
||||
if (!issue.code().isBlank()) {
|
||||
diagnostic.setCode(Either.forLeft(issue.code()));
|
||||
}
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
@ -229,10 +529,51 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
case ENUM -> CompletionItemKind.Enum;
|
||||
case ERROR -> CompletionItemKind.EnumMember;
|
||||
case GLOBAL, CONST -> CompletionItemKind.Constant;
|
||||
case MODULE -> CompletionItemKind.Module;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private List<Diagnostic> matchingDiagnostics(
|
||||
final List<Diagnostic> contextDiagnostics,
|
||||
final String diagnosticCode,
|
||||
final Range range) {
|
||||
if (contextDiagnostics == null || contextDiagnostics.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<Diagnostic> matched = new ArrayList<>();
|
||||
for (final Diagnostic diagnostic : contextDiagnostics) {
|
||||
if (diagnostic == null || diagnostic.getRange() == null) {
|
||||
continue;
|
||||
}
|
||||
if (!diagnosticCode.equals(diagnosticCodeOf(diagnostic)) || !sameRange(diagnostic.getRange(), range)) {
|
||||
continue;
|
||||
}
|
||||
matched.add(diagnostic);
|
||||
}
|
||||
return List.copyOf(matched);
|
||||
}
|
||||
|
||||
private String diagnosticCodeOf(final Diagnostic diagnostic) {
|
||||
if (diagnostic.getCode() == null) {
|
||||
return "";
|
||||
}
|
||||
if (diagnostic.getCode().isLeft()) {
|
||||
return diagnostic.getCode().getLeft() == null ? "" : diagnostic.getCode().getLeft().trim();
|
||||
}
|
||||
if (diagnostic.getCode().isRight() && diagnostic.getCode().getRight() != null) {
|
||||
return diagnostic.getCode().getRight().toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private boolean sameRange(final Range left, final Range right) {
|
||||
return left.getStart().getLine() == right.getStart().getLine()
|
||||
&& left.getStart().getCharacter() == right.getStart().getCharacter()
|
||||
&& left.getEnd().getLine() == right.getEnd().getLine()
|
||||
&& left.getEnd().getCharacter() == right.getEnd().getCharacter();
|
||||
}
|
||||
|
||||
private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) {
|
||||
return switch (severity) {
|
||||
case INFORMATION -> DiagnosticSeverity.Information;
|
||||
|
||||
@ -1,13 +1,42 @@
|
||||
package p.studio.lsp.services.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.CodeAction;
|
||||
import org.eclipse.lsp4j.Command;
|
||||
import org.eclipse.lsp4j.Hover;
|
||||
import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.MessageParams;
|
||||
import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
||||
import org.eclipse.lsp4j.SignatureHelp;
|
||||
import org.eclipse.lsp4j.SemanticTokens;
|
||||
import org.eclipse.lsp4j.Diagnostic;
|
||||
import org.eclipse.lsp4j.DocumentLink;
|
||||
import org.eclipse.lsp4j.DocumentSymbol;
|
||||
import org.eclipse.lsp4j.CallHierarchyIncomingCall;
|
||||
import org.eclipse.lsp4j.CallHierarchyItem;
|
||||
import org.eclipse.lsp4j.CallHierarchyOutgoingCall;
|
||||
import org.eclipse.lsp4j.FoldingRange;
|
||||
import org.eclipse.lsp4j.TextEdit;
|
||||
import org.eclipse.lsp4j.SelectionRange;
|
||||
import org.eclipse.lsp4j.Location;
|
||||
import org.eclipse.lsp4j.Range;
|
||||
import org.eclipse.lsp4j.SymbolInformation;
|
||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
import org.eclipse.lsp4j.PrepareRenameDefaultBehavior;
|
||||
import org.eclipse.lsp4j.PrepareRenameResult;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
||||
import p.studio.lsp.messages.BaselineFoldingRanges;
|
||||
import p.studio.lsp.messages.BaselineCallHierarchyItem;
|
||||
import p.studio.lsp.messages.BaselineCallSite;
|
||||
import p.studio.lsp.messages.BaselineFormatting;
|
||||
import p.studio.lsp.messages.BaselineSelectionRanges;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbols;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselinePrepareRename;
|
||||
import p.studio.lsp.messages.BaselineRename;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineDefinition;
|
||||
import p.studio.lsp.messages.BaselineReferences;
|
||||
@ -32,6 +61,45 @@ public interface ProtocolMessageMapper {
|
||||
|
||||
List<Location> mapReferences(BaselineReferences references);
|
||||
|
||||
List<DocumentSymbol> mapDocumentSymbols(BaselineDocumentSymbols symbols);
|
||||
|
||||
List<SymbolInformation> mapWorkspaceSymbols(BaselineWorkspaceSymbols symbols);
|
||||
|
||||
Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
BaselinePrepareRename prepareRename);
|
||||
|
||||
WorkspaceEdit mapRename(BaselineRename rename);
|
||||
|
||||
List<Either<Command, CodeAction>> mapCodeActions(BaselineCodeActions actions, List<Diagnostic> contextDiagnostics);
|
||||
|
||||
default List<DocumentLink> mapDocumentLinks(BaselineDocumentLinks links) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FoldingRange> mapFoldingRanges(BaselineFoldingRanges ranges) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<TextEdit> mapFormatting(BaselineFormatting formatting) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<CallHierarchyItem> mapCallHierarchyItems(List<BaselineCallHierarchyItem> items) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<CallHierarchyIncomingCall> mapIncomingCalls(List<BaselineCallSite> calls) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<CallHierarchyOutgoingCall> mapOutgoingCalls(List<BaselineCallSite> calls) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<SelectionRange> mapSelectionRanges(BaselineSelectionRanges ranges) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
CompletionList mapCompletion(BaselineCompletion completion);
|
||||
|
||||
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.lsp.services.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.FrontendSpec;
|
||||
@ -8,15 +9,21 @@ import p.studio.compiler.models.IRBackend;
|
||||
import p.studio.compiler.services.FrontendPhaseService;
|
||||
import p.studio.compiler.services.FrontendProvider;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||
import p.studio.lsp.messages.DocumentPosition;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlySet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CompilerLanguageServiceBridgeTest {
|
||||
@ -51,9 +58,30 @@ class CompilerLanguageServiceBridgeTest {
|
||||
final var references = bridge.references(context, documentUri, "fn main() {}", 0, 0, true);
|
||||
assertTrue(references.locations().isEmpty());
|
||||
|
||||
final var documentSymbols = bridge.documentSymbols(context, documentUri, "fn main() {}");
|
||||
assertTrue(documentSymbols.symbols().isEmpty());
|
||||
|
||||
final var workspaceSymbols = bridge.workspaceSymbols(context, "frame");
|
||||
assertTrue(workspaceSymbols.symbols().isEmpty());
|
||||
|
||||
final var prepareRename = bridge.prepareRename(context, documentUri, "fn main() {}", 0, 0);
|
||||
assertFalse(prepareRename.renameable());
|
||||
final var rename = bridge.rename(context, documentUri, "fn main() {}", 0, 0, "other");
|
||||
assertFalse(rename.applied());
|
||||
assertTrue(rename.edits().isEmpty());
|
||||
|
||||
final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}");
|
||||
assertTrue(semanticTokens.legend().isEmpty());
|
||||
assertTrue(semanticTokens.tokens().isEmpty());
|
||||
|
||||
assertTrue(bridge.codeActions(context, documentUri, "fn main() {}", List.of(
|
||||
new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1)))
|
||||
.actions().isEmpty());
|
||||
assertTrue(bridge.documentLinks(context, documentUri, "fn main() {}").links().isEmpty());
|
||||
assertTrue(bridge.formatDocument(context, documentUri, "fn main() {}").edits().isEmpty());
|
||||
assertTrue(bridge.foldingRanges(context, documentUri, "fn main() {}").ranges().isEmpty());
|
||||
assertTrue(bridge.selectionRanges(context, documentUri, "fn main() {}", List.of(new DocumentPosition(0, 0)))
|
||||
.ranges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -80,6 +108,9 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.severity() == BaselineIssueSeverity.ERROR));
|
||||
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.startLine() == 1));
|
||||
assertTrue(analysis.issues().stream().noneMatch(issue -> issue.message().contains("Compiler-backed baseline")));
|
||||
assertTrue(analysis.issues().stream().allMatch(issue -> issue.source().equals("pbs")));
|
||||
assertTrue(analysis.issues().stream().noneMatch(issue -> issue.source().equals(issue.code())));
|
||||
assertTrue(analysis.issues().stream().anyMatch(issue -> !issue.code().isBlank()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -141,6 +172,112 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-builtin-type")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void semanticTokenOverlayReplacesAResolvedUseAndKeepsBrokenText(@TempDir final Path tempDir) throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path source = projectRoot.resolve("src").resolve("main.pbs");
|
||||
Files.createDirectories(source.getParent());
|
||||
Files.writeString(projectRoot.resolve("prometeu.json"), """
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"language": "pbs",
|
||||
"stdlib": "1",
|
||||
"target": "Game",
|
||||
"dependencies": []
|
||||
}
|
||||
""");
|
||||
final String text = """
|
||||
fn helper() -> int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn main() -> int {
|
||||
return helper;
|
||||
}
|
||||
""";
|
||||
Files.writeString(source, text);
|
||||
Files.writeString(source.getParent().resolve("mod.barrel"), """
|
||||
pub fn helper() -> int;
|
||||
pub fn main() -> int;
|
||||
""");
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final Path realSource = source.toRealPath();
|
||||
final var tokens = bridge.semanticTokens(
|
||||
new LspProjectContext("app", "pbs", projectRoot.toRealPath()),
|
||||
realSource.toUri().toString(),
|
||||
text);
|
||||
final int helperUse = text.indexOf("return helper") + "return ".length();
|
||||
assertTrue(tokens.tokens().stream().anyMatch(token ->
|
||||
token.semanticKey().equals("pbs-function")
|
||||
&& token.line() == new DocumentPositionMapper(text).positionOf(helperUse).line()
|
||||
&& token.startCharacter() == new DocumentPositionMapper(text).positionOf(helperUse).character()));
|
||||
|
||||
final String broken = "fn main( {";
|
||||
final var brokenTokens = bridge.semanticTokens(
|
||||
new LspProjectContext("app", "pbs", projectRoot.toRealPath()),
|
||||
realSource.toUri().toString(),
|
||||
broken);
|
||||
assertTrue(brokenTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-keyword")));
|
||||
assertTrue(brokenTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-punctuation")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callHierarchyUsesAstCallsAndRefusesAStruct(@TempDir final Path tempDir) throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path source = projectRoot.resolve("src").resolve("main.pbs");
|
||||
Files.createDirectories(source.getParent());
|
||||
Files.writeString(projectRoot.resolve("prometeu.json"), """
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"language": "pbs",
|
||||
"stdlib": "1",
|
||||
"target": "Game",
|
||||
"dependencies": []
|
||||
}
|
||||
""");
|
||||
final String text = """
|
||||
fn helper() -> int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn main() -> int {
|
||||
return helper();
|
||||
}
|
||||
""";
|
||||
Files.writeString(source, text);
|
||||
Files.writeString(source.getParent().resolve("mod.barrel"), """
|
||||
pub fn helper() -> int;
|
||||
pub fn main() -> int;
|
||||
""");
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final LspProjectContext context = new LspProjectContext("app", "pbs", projectRoot.toRealPath());
|
||||
final String uri = source.toRealPath().toUri().toString();
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(text);
|
||||
final var mainPosition = mapper.positionOf(text.indexOf("fn main") + 1);
|
||||
final var prepared = bridge.prepareCallHierarchy(
|
||||
context,
|
||||
uri,
|
||||
text,
|
||||
mainPosition.line(),
|
||||
mainPosition.character());
|
||||
assertEquals(1, prepared.size());
|
||||
assertEquals("main", prepared.getFirst().name());
|
||||
final var outgoing = bridge.outgoingCalls(context, uri, text, prepared.getFirst());
|
||||
assertTrue(outgoing.stream().anyMatch(call -> call.item().name().equals("helper")));
|
||||
final var structPosition = mapper.positionOf(text.indexOf("fn helper") + 1);
|
||||
final var helper = bridge.prepareCallHierarchy(
|
||||
context,
|
||||
uri,
|
||||
text,
|
||||
structPosition.line(),
|
||||
structPosition.character());
|
||||
assertEquals("helper", helper.getFirst().name());
|
||||
assertTrue(bridge.incomingCalls(context, uri, text, helper.getFirst()).stream()
|
||||
.anyMatch(call -> call.item().name().equals("main")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
@ -474,6 +611,261 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(builtinReferences.locations().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameRewritesPhysicalFunctionSitesAndRefusesStdlib() {
|
||||
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;
|
||||
|
||||
fn helper() -> int { return 42; }
|
||||
|
||||
fn frame() -> void {
|
||||
helper();
|
||||
helper();
|
||||
Gfx.clear(5);
|
||||
let n: int = 1;
|
||||
// helper
|
||||
let text = "helper";
|
||||
}
|
||||
""";
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay);
|
||||
final String documentUri = documentPath.toUri().toString();
|
||||
|
||||
final var helperUse = mapper.positionOf(overlay.indexOf("helper();"));
|
||||
final var prepared = bridge.prepareRename(
|
||||
context,
|
||||
documentUri,
|
||||
overlay,
|
||||
helperUse.line(),
|
||||
helperUse.character());
|
||||
assertTrue(prepared.renameable(), prepared.toString());
|
||||
|
||||
final var renamed = bridge.rename(
|
||||
context,
|
||||
documentUri,
|
||||
overlay,
|
||||
helperUse.line(),
|
||||
helperUse.character(),
|
||||
"aid");
|
||||
assertTrue(renamed.applied(), renamed.message());
|
||||
assertEquals(3, renamed.edits().size(), renamed.edits().toString());
|
||||
assertTrue(renamed.edits().stream().allMatch(edit -> edit.newText().equals("aid")));
|
||||
assertTrue(renamed.edits().stream().allMatch(edit -> edit.uri().equals(documentUri)));
|
||||
|
||||
final var clearUse = mapper.positionOf(overlay.indexOf("clear"));
|
||||
final var stdlibPrepared = bridge.prepareRename(
|
||||
context,
|
||||
documentUri,
|
||||
overlay,
|
||||
clearUse.line(),
|
||||
clearUse.character());
|
||||
assertFalse(stdlibPrepared.renameable());
|
||||
final var stdlibRename = bridge.rename(
|
||||
context,
|
||||
documentUri,
|
||||
overlay,
|
||||
clearUse.line(),
|
||||
clearUse.character(),
|
||||
"wipe");
|
||||
assertFalse(stdlibRename.applied());
|
||||
assertTrue(stdlibRename.edits().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void quickFixDeletesTheDuplicateDocDiagnosticAndIgnoresOtherCodes() {
|
||||
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 = """
|
||||
[Doc(markdown = \"""
|
||||
First.
|
||||
\""")]
|
||||
[Doc(markdown = \"""
|
||||
Second.
|
||||
\""")]
|
||||
fn duplicated() -> void { return; }
|
||||
|
||||
fn frame() -> void { return; }
|
||||
""";
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
|
||||
final String documentUri = documentPath.toUri().toString();
|
||||
|
||||
final var analysis = bridge.analyzeDocument(context, documentUri, overlay);
|
||||
final var duplicates = analysis.issues().stream()
|
||||
.filter(issue -> issue.code().equals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE"))
|
||||
.toList();
|
||||
assertEquals(1, duplicates.size(), analysis.issues().toString());
|
||||
final var duplicate = duplicates.getFirst();
|
||||
assertFalse(duplicate.code().isBlank());
|
||||
final var diagnostic = new BaselineCodeActionDiagnostic(
|
||||
duplicate.code(),
|
||||
duplicate.startLine(),
|
||||
duplicate.startCharacter(),
|
||||
duplicate.endLine(),
|
||||
duplicate.endCharacter());
|
||||
|
||||
final var actions = bridge.codeActions(context, documentUri, overlay, List.of(diagnostic));
|
||||
assertEquals(1, actions.actions().size(), actions.toString());
|
||||
final var action = actions.actions().getFirst();
|
||||
assertEquals("Remove duplicate reserved attribute", action.title());
|
||||
assertEquals(documentUri, action.uri());
|
||||
assertEquals("", action.newText());
|
||||
assertEquals(diagnostic.startLine(), action.startLine());
|
||||
assertEquals(diagnostic.startCharacter(), action.startCharacter());
|
||||
assertEquals(diagnostic.endLine(), action.endLine());
|
||||
assertEquals(diagnostic.endCharacter(), action.endCharacter());
|
||||
final String removed = sourceSlice(overlay, action.startLine(), action.startCharacter(), action.endLine(), action.endCharacter());
|
||||
assertTrue(removed.contains("Second."), removed);
|
||||
assertFalse(removed.contains("First."), removed);
|
||||
|
||||
final var wrongCode = new BaselineCodeActionDiagnostic(
|
||||
"E_SEM_MALFORMED_RESERVED_ATTRIBUTE",
|
||||
diagnostic.startLine(),
|
||||
diagnostic.startCharacter(),
|
||||
diagnostic.endLine(),
|
||||
diagnostic.endCharacter());
|
||||
assertTrue(bridge.codeActions(context, documentUri, overlay, List.of(wrongCode)).actions().isEmpty());
|
||||
assertTrue(bridge.codeActions(context, documentUri, overlay, List.of()).actions().isEmpty());
|
||||
|
||||
final String malformed = """
|
||||
[Doc(text = \"""
|
||||
Wrong argument.
|
||||
\""")]
|
||||
fn frame() -> void { return; }
|
||||
""";
|
||||
final var malformedAnalysis = bridge.analyzeDocument(context, documentUri, malformed);
|
||||
final var malformedDiagnostics = malformedAnalysis.issues().stream()
|
||||
.map(issue -> new BaselineCodeActionDiagnostic(
|
||||
issue.code(),
|
||||
issue.startLine(),
|
||||
issue.startCharacter(),
|
||||
issue.endLine(),
|
||||
issue.endCharacter()))
|
||||
.toList();
|
||||
assertTrue(bridge.codeActions(context, documentUri, malformed, malformedDiagnostics).actions().isEmpty());
|
||||
}
|
||||
|
||||
private static String sourceSlice(
|
||||
final String source,
|
||||
final int startLine,
|
||||
final int startCharacter,
|
||||
final int endLine,
|
||||
final int endCharacter) {
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(source);
|
||||
final byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
|
||||
final int start = mapper.byteOffsetOf(startLine, startCharacter);
|
||||
final int end = mapper.byteOffsetOf(endLine, endCharacter);
|
||||
return new String(bytes, start, Math.max(0, end - start), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@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
|
||||
void workspaceSymbolsSearchPhysicalNamedDeclarationsWithoutAnOpenDocument() {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
.resolve("test-projects")
|
||||
.resolve("main")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final var context = new LspProjectContext("main", "pbs", projectRoot);
|
||||
|
||||
final var frameHits = bridge.workspaceSymbols(context, "frame");
|
||||
assertTrue(frameHits.symbols().stream().anyMatch(symbol ->
|
||||
symbol.name().equals("frame") && symbol.containerName().isEmpty()));
|
||||
assertTrue(frameHits.symbols().stream().allMatch(symbol ->
|
||||
symbol.uri().contains("main.pbs")));
|
||||
|
||||
final var caseInsensitive = bridge.workspaceSymbols(context, "BLA");
|
||||
assertTrue(caseInsensitive.symbols().stream().anyMatch(symbol ->
|
||||
symbol.name().equals("Bla") && symbol.containerName().isEmpty()));
|
||||
|
||||
final var all = bridge.workspaceSymbols(context, "");
|
||||
assertTrue(all.symbols().stream().anyMatch(symbol ->
|
||||
symbol.name().equals("a") && symbol.containerName().equals("Bla")));
|
||||
assertTrue(all.symbols().stream().anyMatch(symbol -> symbol.name().equals("init")));
|
||||
assertTrue(all.symbols().stream().noneMatch(symbol ->
|
||||
symbol.name().equals("Gfx")
|
||||
|| symbol.name().equals("Log")
|
||||
|| symbol.name().equals("Composer")));
|
||||
assertTrue(isSortedByNameThenUriThenOffset(all.symbols()));
|
||||
|
||||
final var stdlibMiss = bridge.workspaceSymbols(context, "Gfx");
|
||||
assertTrue(stdlibMiss.symbols().isEmpty());
|
||||
}
|
||||
|
||||
private boolean isSortedByNameThenUriThenOffset(
|
||||
final List<p.studio.lsp.messages.BaselineWorkspaceSymbol> symbols) {
|
||||
for (int index = 1; index < symbols.size(); index += 1) {
|
||||
final var previous = symbols.get(index - 1);
|
||||
final var current = symbols.get(index);
|
||||
final int nameOrder = String.CASE_INSENSITIVE_ORDER.compare(previous.name(), current.name());
|
||||
if (nameOrder > 0) {
|
||||
return false;
|
||||
}
|
||||
if (nameOrder == 0) {
|
||||
final int uriOrder = previous.uri().compareTo(current.uri());
|
||||
if (uriOrder > 0) {
|
||||
return false;
|
||||
}
|
||||
if (uriOrder == 0 && previous.startLine() > current.startLine()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Test
|
||||
void describeServerPublishesFrontendVisualThemes() {
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
@ -486,6 +878,15 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(description.signatureHelpSupported());
|
||||
assertTrue(description.definitionSupported());
|
||||
assertTrue(description.referencesSupported());
|
||||
assertTrue(description.documentSymbolsSupported());
|
||||
assertTrue(description.workspaceSymbolsSupported());
|
||||
assertTrue(description.renameSupported());
|
||||
assertTrue(description.codeActionsSupported());
|
||||
assertTrue(description.documentLinksSupported());
|
||||
assertTrue(description.foldingRangesSupported());
|
||||
assertTrue(description.selectionRangesSupported());
|
||||
assertTrue(description.formattingSupported());
|
||||
assertTrue(description.callHierarchySupported());
|
||||
assertEquals(1, description.semanticHostProjections().size());
|
||||
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
|
||||
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
|
||||
@ -499,6 +900,77 @@ class CompilerLanguageServiceBridgeTest {
|
||||
&& tokenStyle.foreground().equals("#7fb8ff")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksSkipVirtualStdlibImports() throws IOException {
|
||||
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 text = Files.readString(documentPath);
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
|
||||
final var links = bridge.documentLinks(
|
||||
new LspProjectContext("main", "pbs", projectRoot),
|
||||
documentPath.toUri().toString(),
|
||||
text);
|
||||
|
||||
assertTrue(links.links().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksPointAtThePhysicalModuleFile(@TempDir final Path tempDir) throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path sourceA = projectRoot.resolve("src").resolve("a").resolve("source.pbs");
|
||||
final Path sourceB = projectRoot.resolve("src").resolve("b").resolve("source.pbs");
|
||||
Files.createDirectories(sourceA.getParent());
|
||||
Files.createDirectories(sourceB.getParent());
|
||||
Files.writeString(projectRoot.resolve("prometeu.json"), """
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"language": "pbs",
|
||||
"stdlib": "1",
|
||||
"target": "Game",
|
||||
"dependencies": []
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA, """
|
||||
fn target() -> int {
|
||||
return 1;
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
|
||||
final String caller = """
|
||||
import { target } from @app:a;
|
||||
import { Log } from @sdk:log;
|
||||
import { Missing } from @app:missing;
|
||||
|
||||
fn caller() -> int {
|
||||
return target();
|
||||
}
|
||||
""";
|
||||
Files.writeString(sourceB, caller);
|
||||
Files.writeString(sourceB.getParent().resolve("mod.barrel"), "pub fn caller() -> int;\n");
|
||||
final Path realRoot = projectRoot.toRealPath();
|
||||
final Path realA = sourceA.toRealPath();
|
||||
final Path realB = sourceB.toRealPath();
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
|
||||
final var links = bridge.documentLinks(
|
||||
new LspProjectContext("app", "pbs", realRoot),
|
||||
realB.toUri().toString(),
|
||||
caller);
|
||||
|
||||
assertEquals(1, links.links().size());
|
||||
final var link = links.links().getFirst();
|
||||
assertEquals(realA.toUri().toString(), link.targetUri());
|
||||
final int start = new DocumentPositionMapper(caller).byteOffsetOf(link.startLine(), link.startCharacter());
|
||||
final int end = new DocumentPositionMapper(caller).byteOffsetOf(link.endLine(), link.endCharacter());
|
||||
assertEquals("@app:a", new String(caller.getBytes(StandardCharsets.UTF_8), start, end - start, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private Path findRepoRoot(final Path start) {
|
||||
var current = start;
|
||||
while (current != null) {
|
||||
|
||||
@ -101,6 +101,15 @@ class PrometeuLanguageServerTest {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
List.of(),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -169,6 +178,42 @@ class PrometeuLanguageServerTest {
|
||||
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
|
||||
public p.studio.lsp.messages.BaselineWorkspaceSymbols workspaceSymbols(
|
||||
final LspProjectContext project,
|
||||
final String query) {
|
||||
return p.studio.lsp.messages.BaselineWorkspaceSymbols.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
|
||||
final LspProjectContext project,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
return p.studio.lsp.messages.BaselinePrepareRename.none();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineRename rename(
|
||||
final LspProjectContext project,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final String newName) {
|
||||
return p.studio.lsp.messages.BaselineRename.unsupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext project,
|
||||
@ -184,6 +229,15 @@ class PrometeuLanguageServerTest {
|
||||
return new BaselineSemanticTokens(List.of(), List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
|
||||
return p.studio.lsp.messages.BaselineCodeActions.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(final LspProjectContext project, final String documentUri) {
|
||||
return "saved";
|
||||
@ -226,6 +280,39 @@ class PrometeuLanguageServerTest {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.eclipse.lsp4j.DocumentSymbol> mapDocumentSymbols(
|
||||
final p.studio.lsp.messages.BaselineDocumentSymbols symbols) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.eclipse.lsp4j.SymbolInformation> mapWorkspaceSymbols(
|
||||
final p.studio.lsp.messages.BaselineWorkspaceSymbols symbols) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
|
||||
org.eclipse.lsp4j.Range,
|
||||
org.eclipse.lsp4j.PrepareRenameResult,
|
||||
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
|
||||
final p.studio.lsp.messages.BaselineCodeActions actions,
|
||||
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@ -4,6 +4,8 @@ import org.eclipse.lsp4j.CompletionItem;
|
||||
import org.eclipse.lsp4j.CompletionList;
|
||||
import org.eclipse.lsp4j.CompletionParams;
|
||||
import org.eclipse.lsp4j.DefinitionParams;
|
||||
import org.eclipse.lsp4j.DocumentSymbol;
|
||||
import org.eclipse.lsp4j.DocumentSymbolParams;
|
||||
import org.eclipse.lsp4j.ReferenceContext;
|
||||
import org.eclipse.lsp4j.ReferenceParams;
|
||||
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.BaselineDefinition;
|
||||
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.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineHover;
|
||||
@ -102,6 +107,13 @@ final class PrometeuTextDocumentServiceTest {
|
||||
assertEquals(1, bridge.lastReferencesLine);
|
||||
assertEquals(2, bridge.lastReferencesCharacter);
|
||||
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 {
|
||||
@ -116,6 +128,7 @@ final class PrometeuTextDocumentServiceTest {
|
||||
private int lastReferencesLine;
|
||||
private int lastReferencesCharacter;
|
||||
private boolean lastIncludeDeclaration;
|
||||
private String lastDocumentSymbolsText;
|
||||
private String lastSignatureHelpText;
|
||||
private int lastSignatureHelpLine;
|
||||
private int lastSignatureHelpCharacter;
|
||||
@ -183,6 +196,55 @@ final class PrometeuTextDocumentServiceTest {
|
||||
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
|
||||
public p.studio.lsp.messages.BaselineWorkspaceSymbols workspaceSymbols(
|
||||
final LspProjectContext context,
|
||||
final String query) {
|
||||
return p.studio.lsp.messages.BaselineWorkspaceSymbols.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
return p.studio.lsp.messages.BaselinePrepareRename.none();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineRename rename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final String newName) {
|
||||
return p.studio.lsp.messages.BaselineRename.unsupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext context,
|
||||
@ -202,6 +264,15 @@ final class PrometeuTextDocumentServiceTest {
|
||||
return new BaselineSemanticTokens(List.of(), List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
|
||||
return p.studio.lsp.messages.BaselineCodeActions.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(final LspProjectContext context, final String documentUri) {
|
||||
return "saved";
|
||||
@ -213,6 +284,7 @@ final class PrometeuTextDocumentServiceTest {
|
||||
private final SignatureHelp signatureHelp = new SignatureHelp();
|
||||
private final List<Location> definitionLocations = List.of();
|
||||
private final List<Location> referenceLocations = List.of();
|
||||
private final List<DocumentSymbol> documentSymbols = List.of(new DocumentSymbol());
|
||||
private BaselineSignatureHelp mappedSignatureHelp;
|
||||
|
||||
@Override
|
||||
@ -250,6 +322,38 @@ final class PrometeuTextDocumentServiceTest {
|
||||
return referenceLocations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DocumentSymbol> mapDocumentSymbols(final BaselineDocumentSymbols symbols) {
|
||||
return documentSymbols;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<org.eclipse.lsp4j.SymbolInformation> mapWorkspaceSymbols(
|
||||
final p.studio.lsp.messages.BaselineWorkspaceSymbols symbols) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
|
||||
org.eclipse.lsp4j.Range,
|
||||
org.eclipse.lsp4j.PrepareRenameResult,
|
||||
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
|
||||
return new org.eclipse.lsp4j.WorkspaceEdit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
|
||||
final p.studio.lsp.messages.BaselineCodeActions actions,
|
||||
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
return completionList;
|
||||
|
||||
@ -0,0 +1,280 @@
|
||||
package p.studio.lsp.services.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.SymbolInformation;
|
||||
import org.eclipse.lsp4j.WorkspaceSymbolParams;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineDefinition;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbols;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbolKind;
|
||||
import p.studio.lsp.messages.BaselineHover;
|
||||
import p.studio.lsp.messages.BaselineReferences;
|
||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
import p.studio.lsp.messages.BaselineSignatureHelp;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbol;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class PrometeuWorkspaceServiceTest {
|
||||
|
||||
@Test
|
||||
void symbolDelegatesQueryWithoutADocumentUri() {
|
||||
final var bridge = new RecordingBridge();
|
||||
final var mapper = new RecordingMapper();
|
||||
final var project = new LspProjectContext("main", "pbs", Path.of("."));
|
||||
final var service = new PrometeuWorkspaceService(project, bridge, mapper);
|
||||
|
||||
final var result = service.symbol(new WorkspaceSymbolParams("blend")).join();
|
||||
|
||||
assertTrue(result.isLeft());
|
||||
assertSame(mapper.workspaceSymbols, result.getLeft());
|
||||
assertEquals("blend", bridge.lastQuery);
|
||||
assertSame(project, bridge.lastContext);
|
||||
}
|
||||
|
||||
private static final class RecordingBridge implements LanguageServiceBridge {
|
||||
private LspProjectContext lastContext;
|
||||
private String lastQuery;
|
||||
|
||||
@Override
|
||||
public BaselineServerDescription describeServer(final LspProjectContext context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineDocumentAnalysis analyzeDocument(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineCompletion completion(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineHover hover(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineDefinition definition(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineReferences references(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final boolean includeDeclaration) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineDocumentSymbols documentSymbols(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineWorkspaceSymbols workspaceSymbols(
|
||||
final LspProjectContext context,
|
||||
final String query) {
|
||||
lastContext = context;
|
||||
lastQuery = query;
|
||||
return new BaselineWorkspaceSymbols(List.of(new BaselineWorkspaceSymbol(
|
||||
"blend",
|
||||
BaselineDocumentSymbolKind.METHOD,
|
||||
"",
|
||||
"Vec",
|
||||
"file:///tmp/demo.pbs",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSemanticTokens semanticTokens(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineRename rename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final String newName) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(final LspProjectContext context, final String documentUri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingMapper implements ProtocolMessageMapper {
|
||||
private final List<SymbolInformation> workspaceSymbols = List.of(new SymbolInformation());
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.InitializeResult mapInitializeResult(final BaselineServerDescription description) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.PublishDiagnosticsParams mapDiagnostics(
|
||||
final String uri,
|
||||
final BaselineDocumentAnalysis analysis) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.PublishDiagnosticsParams emptyDiagnostics(final String uri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.Hover mapHover(final BaselineHover hover) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<org.eclipse.lsp4j.Location> mapDefinition(final BaselineDefinition definition) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<org.eclipse.lsp4j.Location> mapReferences(final BaselineReferences references) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<org.eclipse.lsp4j.DocumentSymbol> mapDocumentSymbols(final BaselineDocumentSymbols symbols) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SymbolInformation> mapWorkspaceSymbols(final BaselineWorkspaceSymbols symbols) {
|
||||
return workspaceSymbols;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
|
||||
org.eclipse.lsp4j.Range,
|
||||
org.eclipse.lsp4j.PrepareRenameResult,
|
||||
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
|
||||
final p.studio.lsp.messages.BaselineCodeActions actions,
|
||||
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.SignatureHelp mapSignatureHelp(final BaselineSignatureHelp signatureHelp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.MessageParams mapInfoMessage(final String message) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.MessageParams mapErrorMessage(final String message) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.MessageParams mapWarningMessage(final String message) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,25 @@
|
||||
package p.studio.lsp.services.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.CodeActionKind;
|
||||
import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.SymbolKind;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.lsp.messages.BaselineCodeAction;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentIssue;
|
||||
import p.studio.lsp.messages.BaselineDocumentLink;
|
||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||
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.BaselineDocumentSymbol;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbolKind;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbols;
|
||||
import p.studio.lsp.messages.BaselineReferences;
|
||||
import p.studio.lsp.messages.BaselineEditorPalette;
|
||||
import p.studio.lsp.messages.BaselineSemanticHostProjection;
|
||||
@ -24,6 +36,7 @@ import java.util.Map;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class Lsp4jProtocolMessageMapperTest {
|
||||
@ -40,6 +53,15 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -62,6 +84,19 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(List.of("(", ","), result.getCapabilities().getSignatureHelpProvider().getTriggerCharacters());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getDefinitionProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentSymbolProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getWorkspaceSymbolProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getRenameProvider().getRight().getPrepareProvider());
|
||||
assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds());
|
||||
assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider());
|
||||
assertEquals(Boolean.FALSE, result.getCapabilities().getDocumentLinkProvider().getResolveProvider());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getFoldingRangeProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getSelectionRangeProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentFormattingProvider().getLeft());
|
||||
assertNull(result.getCapabilities().getDocumentRangeFormattingProvider());
|
||||
assertNull(result.getCapabilities().getDocumentOnTypeFormattingProvider());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getCallHierarchyProvider().getLeft());
|
||||
assertNull(result.getCapabilities().getTypeHierarchyProvider());
|
||||
|
||||
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
|
||||
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
|
||||
@ -135,4 +170,202 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(3, locations.getFirst().getRange().getStart().getCharacter());
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void workspaceSymbolsMapToFlatSymbolInformation() {
|
||||
final var mapper = new Lsp4jProtocolMessageMapper();
|
||||
final var mapped = mapper.mapWorkspaceSymbols(new p.studio.lsp.messages.BaselineWorkspaceSymbols(List.of(
|
||||
new p.studio.lsp.messages.BaselineWorkspaceSymbol(
|
||||
"blend",
|
||||
BaselineDocumentSymbolKind.METHOD,
|
||||
"(dx: int, dy: int) -> int",
|
||||
"Vec",
|
||||
"file:///tmp/demo.pbs",
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
7),
|
||||
new p.studio.lsp.messages.BaselineWorkspaceSymbol(
|
||||
"Idle",
|
||||
BaselineDocumentSymbolKind.ENUM,
|
||||
"0",
|
||||
"Mode",
|
||||
"file:///tmp/demo.pbs",
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
23))));
|
||||
|
||||
assertEquals(2, mapped.size());
|
||||
assertEquals("blend", mapped.getFirst().getName());
|
||||
assertEquals("Vec", mapped.getFirst().getContainerName());
|
||||
assertEquals(SymbolKind.Method, mapped.getFirst().getKind());
|
||||
assertEquals("file:///tmp/demo.pbs", mapped.getFirst().getLocation().getUri());
|
||||
assertEquals(2, mapped.getFirst().getLocation().getRange().getStart().getCharacter());
|
||||
assertEquals("Idle", mapped.get(1).getName());
|
||||
assertEquals(SymbolKind.EnumMember, mapped.get(1).getKind());
|
||||
assertTrue(mapper.mapWorkspaceSymbols(p.studio.lsp.messages.BaselineWorkspaceSymbols.empty()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void diagnosticsPublishStableCodeAndQuickFixesAreEditsNotCommands() {
|
||||
final var mapper = new Lsp4jProtocolMessageMapper();
|
||||
final var published = mapper.mapDiagnostics("file:///tmp/demo.pbs", new BaselineDocumentAnalysis(List.of(
|
||||
new BaselineDocumentIssue(
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
18,
|
||||
BaselineIssueSeverity.ERROR,
|
||||
"pbs",
|
||||
"Duplicate Doc attribute on the same declaration surface",
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE"))));
|
||||
final var diagnostic = published.getDiagnostics().getFirst();
|
||||
assertEquals("pbs", diagnostic.getSource());
|
||||
assertEquals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", diagnostic.getCode().getLeft());
|
||||
assertNull(diagnostic.getData());
|
||||
assertTrue(diagnostic.getRelatedInformation() == null || diagnostic.getRelatedInformation().isEmpty());
|
||||
|
||||
final var mapped = mapper.mapCodeActions(new BaselineCodeActions(List.of(new BaselineCodeAction(
|
||||
"Remove duplicate reserved attribute",
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
|
||||
"file:///tmp/demo.pbs",
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
18,
|
||||
""))), List.of(diagnostic));
|
||||
|
||||
assertEquals(1, mapped.size());
|
||||
assertTrue(mapped.getFirst().isRight());
|
||||
assertFalse(mapped.getFirst().isLeft());
|
||||
final var action = mapped.getFirst().getRight();
|
||||
assertEquals(CodeActionKind.QuickFix, action.getKind());
|
||||
assertNull(action.getCommand());
|
||||
assertEquals("", action.getEdit().getChanges().get("file:///tmp/demo.pbs").getFirst().getNewText());
|
||||
assertEquals(1, action.getDiagnostics().size());
|
||||
assertEquals(diagnostic, action.getDiagnostics().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksMapRangeAndTargetWithoutResolve() {
|
||||
final var mapper = new Lsp4jProtocolMessageMapper();
|
||||
final var unsupported = mapper.mapInitializeResult(serverDescription(false));
|
||||
assertNull(unsupported.getCapabilities().getDocumentLinkProvider());
|
||||
assertNull(unsupported.getCapabilities().getFoldingRangeProvider());
|
||||
assertNull(unsupported.getCapabilities().getSelectionRangeProvider());
|
||||
|
||||
final var mapped = mapper.mapDocumentLinks(new BaselineDocumentLinks(List.of(
|
||||
new BaselineDocumentLink("file:///tmp/a/source.pbs", 0, 19, 0, 25))));
|
||||
assertEquals(1, mapped.size());
|
||||
assertEquals("file:///tmp/a/source.pbs", mapped.getFirst().getTarget());
|
||||
assertEquals(0, mapped.getFirst().getRange().getStart().getLine());
|
||||
assertEquals(19, mapped.getFirst().getRange().getStart().getCharacter());
|
||||
assertEquals(25, mapped.getFirst().getRange().getEnd().getCharacter());
|
||||
assertNull(mapped.getFirst().getData());
|
||||
assertTrue(mapper.mapDocumentLinks(BaselineDocumentLinks.empty()).isEmpty());
|
||||
}
|
||||
|
||||
private static BaselineServerDescription serverDescription(final boolean documentLinksSupported) {
|
||||
return new BaselineServerDescription(
|
||||
"demo",
|
||||
"1",
|
||||
"pbs",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
documentLinksSupported,
|
||||
documentLinksSupported,
|
||||
documentLinksSupported,
|
||||
documentLinksSupported,
|
||||
documentLinksSupported,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
List.of(new BaselineSemanticHostProjectionEntry(
|
||||
"demo-keyword",
|
||||
"keyword",
|
||||
List.of(),
|
||||
"variable",
|
||||
List.of())))),
|
||||
List.of(new BaselineVisualTheme(
|
||||
"demo-default",
|
||||
"Demo Default",
|
||||
new BaselineEditorPalette("#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777"),
|
||||
List.of(new BaselineTokenStyle("demo-keyword", "#888888", false, false, false)))),
|
||||
"demo-default");
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -22,10 +24,12 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
final class StudioRuntimeHandshakeServiceTest {
|
||||
@Test
|
||||
void connectsPublishesRuntimeHandshakeAndStreamsLogEvents() throws Exception {
|
||||
try (final ServerSocket serverSocket = new ServerSocket(0)) {
|
||||
try (final ServerSocket serverSocket = new ServerSocket()) {
|
||||
serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0));
|
||||
final CountDownLatch clientStarted = new CountDownLatch(1);
|
||||
final CountDownLatch eventDelivered = new CountDownLatch(1);
|
||||
final Thread serverThread = new Thread(() -> serveSuccessfulHandshake(serverSocket, clientStarted, eventDelivered));
|
||||
serverThread.setDaemon(true);
|
||||
serverThread.start();
|
||||
|
||||
final StudioExecutionSessionService session = new StudioExecutionSessionService();
|
||||
@ -36,7 +40,7 @@ final class StudioRuntimeHandshakeServiceTest {
|
||||
final StudioRuntimeHandshakeService service = new StudioRuntimeHandshakeService(new ObjectMapper(), backgroundTasks);
|
||||
final StudioRuntimeHandshakeResult result = service.connect(session, settings);
|
||||
|
||||
assertTrue(result.success());
|
||||
assertTrue(result.success(), result.failureMessage());
|
||||
assertEquals(StudioExecutionState.RUNNING, session.snapshot().state());
|
||||
assertTrue(clientStarted.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(eventDelivered.await(2, TimeUnit.SECONDS));
|
||||
@ -90,6 +94,9 @@ final class StudioRuntimeHandshakeServiceTest {
|
||||
writer.flush();
|
||||
eventDelivered.countDown();
|
||||
} catch (Exception exception) {
|
||||
if (serverSocket.isClosed()) {
|
||||
return;
|
||||
}
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user