Compare commits

..

No commits in common. "master" and "dev/pbs-lsp-document-symbols-and-outline" have entirely different histories.

89 changed files with 775 additions and 7009 deletions

View File

@ -1,5 +1,4 @@
{"type":"meta","next_id":{"DSC":67,"AGD":70,"DEC":59,"PLN":138,"LSN":75,"CLSN":1}} {"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":55,"PLN":134,"LSN":71,"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-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
{"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
{"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
@ -12,18 +11,18 @@
{"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-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-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-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":"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-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":"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-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":"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-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":"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-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":"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-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":"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-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":"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-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":"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-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":"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-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":"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-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":"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-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":"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-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":"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-0041","status":"done","ticket":"pbs-lsp-document-symbols-outline","title":"PBS LSP Document Symbols and Outline","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","outline","document-symbols"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0070","file":"discussion/lessons/DSC-0041-pbs-lsp-document-symbols-outline/LSN-0070-pbs-outline-is-an-ast-backed-document-symbol-tree.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"pbs-lsp-find-references","title":"PBS LSP Find References","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","references"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0069","file":"discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"pbs-lsp-find-references","title":"PBS LSP Find References","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","references"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0069","file":"discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
{"type":"discussion","id":"DSC-0039","status":"done","ticket":"pbs-lsp-go-to-definition","title":"PBS LSP Go to Definition","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","definition"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0068","file":"discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]} {"type":"discussion","id":"DSC-0039","status":"done","ticket":"pbs-lsp-go-to-definition","title":"PBS LSP Go to Definition","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","definition"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0068","file":"discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}

View File

@ -1,149 +0,0 @@
---
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

View File

@ -1,128 +0,0 @@
---
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 Q1Q6 = 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` (Q1Q6 = 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.34.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

View File

@ -1,130 +0,0 @@
---
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

View File

@ -1,70 +0,0 @@
---
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`

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -0,0 +1,62 @@
---
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.

View File

@ -278,30 +278,8 @@ When a frontend exposes find-references, each returned usage or declaration loca
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 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. 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. `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. 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.

View File

@ -13,29 +13,11 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSignatureHelp;
import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind; import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind;
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService; import p.studio.compiler.pbs.semantics.PbsEditorialSupportService;
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocument; 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.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.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.FrontendDefinitionLocation;
import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendDocumentRequest;
import p.studio.compiler.services.FrontendDocumentSymbol; 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.FrontendDocumentation;
import p.studio.compiler.services.FrontendEditorialContext; import p.studio.compiler.services.FrontendEditorialContext;
import p.studio.compiler.services.FrontendHover; import p.studio.compiler.services.FrontendHover;
@ -43,20 +25,16 @@ import p.studio.compiler.services.FrontendLanguageService;
import p.studio.compiler.services.FrontendSignature; import p.studio.compiler.services.FrontendSignature;
import p.studio.compiler.services.FrontendSignatureHelp; import p.studio.compiler.services.FrontendSignatureHelp;
import p.studio.compiler.services.FrontendSymbolKind; 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.services.PBSFrontendPhaseService;
import p.studio.compiler.source.Span; import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.tables.FileTableReader; import p.studio.compiler.source.tables.FileTableReader;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.utilities.structures.ReadOnlyList; import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -77,23 +55,6 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
this.semanticTokenProvider = Objects.requireNonNull(semanticTokenProvider, "semanticTokenProvider"); 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 @Override
public Optional<FrontendEditorialContext> prepareEditorialContext( public Optional<FrontendEditorialContext> prepareEditorialContext(
final FrontendPhaseContext phaseContext, final FrontendPhaseContext phaseContext,
@ -139,10 +100,6 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return List.of(); return List.of();
} }
if (insideModuleRef(ast, offset)
&& editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface) {
return moduleCompletions(surface.moduleCompletionLabels());
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls = final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls; (ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
@ -324,392 +281,15 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
.toList(); .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 @Override
public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) { public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
return semanticTokens(request, null); return semanticTokens(request == null ? "" : request.documentText());
}
@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) { public List<FrontendSemanticToken> semanticTokens(final String documentText) {
return semanticTokenProvider.tokenize(documentText == null ? "" : 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) { private FrontendCompletionCandidate toFrontendCompletionCandidate(final PbsEditorialCompletionCandidate candidate) {
return new FrontendCompletionCandidate( return new FrontendCompletionCandidate(
candidate.label(), candidate.label(),
@ -748,22 +328,6 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return FrontendSymbolKind.valueOf(kind.name()); 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) { private FrontendDocumentSymbol toFrontendDocumentSymbol(final PbsEditorialDocumentSymbol symbol) {
final int start = toOffset(symbol.span().getStart()); final int start = toOffset(symbol.span().getStart());
final int end = toOffset(symbol.span().getEnd()); final int end = toOffset(symbol.span().getEnd());
@ -812,15 +376,6 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return List.copyOf(documents); 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( private boolean isRegularSource(
final FileId fileId, final FileId fileId,
final FileTableReader fileTable) { final FileTableReader fileTable) {

View File

@ -1,335 +0,0 @@
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);
}
}

View File

@ -1,179 +0,0 @@
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) {
}
}

View File

@ -1,100 +0,0 @@
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();
}
}

View File

@ -1,380 +0,0 @@
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;
}
}
}

View File

@ -46,32 +46,6 @@ public final class PbsLexer {
return lexer.lexInternal(); 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() { private ReadOnlyList<PbsToken> lexInternal() {
while (!isAtEnd() || state != LexerState.DEFAULT) { while (!isAtEnd() || state != LexerState.DEFAULT) {
switch (state) { switch (state) {

View File

@ -1,55 +0,0 @@
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));
}
}

View File

@ -1,40 +0,0 @@
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();
}
}

View File

@ -78,12 +78,6 @@ public final class PbsEditorialSupportService {
return PbsAstDocumentSymbols.list(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( public List<PbsEditorialLocation> definition(
final String sourceText, final String sourceText,
final PbsAst.File ast, final PbsAst.File ast,
@ -155,63 +149,6 @@ public final class PbsEditorialSupportService {
return List.copyOf(usages.values()); 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( public record EditorialDocument(
String sourceText, String sourceText,
PbsAst.File ast, PbsAst.File ast,
@ -259,76 +196,6 @@ public final class PbsEditorialSupportService {
return Optional.of(new PbsEditorialSignatureHelp(signatures, 0, callSite.activeParameter())); 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( private DocumentContext buildContext(
final String sourceText, final String sourceText,
final PbsAst.File ast, final PbsAst.File ast,
@ -1626,332 +1493,6 @@ public final class PbsEditorialSupportService {
throw new IndexOutOfBoundsException("No significant token at index " + significantIndex); 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) { private PbsToken token(final int index) {
return tokens.get(index); return tokens.get(index);
} }
@ -2160,17 +1701,6 @@ public final class PbsEditorialSupportService {
String origin) { 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( private record CallableFrame(
ReadOnlyList<PbsAst.Parameter> parameters, ReadOnlyList<PbsAst.Parameter> parameters,
PbsAst.Block body, PbsAst.Block body,

View File

@ -1,21 +0,0 @@
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");
}
}

View File

@ -1,33 +0,0 @@
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;
}
}

View File

@ -1,148 +0,0 @@
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));
}
}

View File

@ -5,15 +5,9 @@ import p.studio.utilities.structures.ReadOnlyList;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets; 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.List;
import java.util.Optional; import java.util.Optional;
import java.util.jar.JarEntry;
public final class ResourceStdlibEnvironmentResolver implements StdlibEnvironmentResolver { public final class ResourceStdlibEnvironmentResolver implements StdlibEnvironmentResolver {
private static final String STDLIB_ROOT = "stdlib"; private static final String STDLIB_ROOT = "stdlib";
@ -43,73 +37,6 @@ public final class ResourceStdlibEnvironmentResolver implements StdlibEnvironmen
barrelSource.get())); 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( private String moduleBasePath(
final String project, final String project,
final ReadOnlyList<String> pathSegments) { final ReadOnlyList<String> pathSegments) {

View File

@ -6,9 +6,5 @@ import java.util.Optional;
public interface StdlibEnvironment { public interface StdlibEnvironment {
Optional<StdlibModuleSource> resolveModule(String project, ReadOnlyList<String> pathSegments); Optional<StdlibModuleSource> resolveModule(String project, ReadOnlyList<String> pathSegments);
default ReadOnlyList<StdlibModuleSource> modules() {
return ReadOnlyList.empty();
}
} }

View File

@ -5,7 +5,6 @@ import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FESurfaceContext; import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.FrontendPhaseContext; import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.IRBackend; import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.PbsFrontendCompiler; import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.PbsReservedMetadataExtractor; import p.studio.compiler.pbs.PbsReservedMetadataExtractor;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator; import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator;
@ -15,12 +14,9 @@ import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver;
import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId; import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList; import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*; import java.util.*;
@Slf4j @Slf4j
@ -131,15 +127,11 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
final Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile = new LinkedHashMap<>(); 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<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile = new LinkedHashMap<>();
final Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile = new LinkedHashMap<>(); final Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile = new LinkedHashMap<>();
final Map<FileId, SourceKind> sourceKindByFile = new LinkedHashMap<>();
for (final var entry : importedSemanticContexts.entrySet()) { for (final var entry : importedSemanticContexts.entrySet()) {
supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls()); supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls());
} }
for (final var parsedSourceFile : assembly.parsedSourceFiles()) { for (final var parsedSourceFile : assembly.parsedSourceFiles()) {
astByFile.put(parsedSourceFile.fileId(), parsedSourceFile.ast()); astByFile.put(parsedSourceFile.fileId(), parsedSourceFile.ast());
sourceKindByFile.put(
parsedSourceFile.fileId(),
parsedSourceFile.sourceKind() == null ? SourceKind.PROJECT : parsedSourceFile.sourceKind());
final var importedSemanticContext = importedSemanticContexts.getOrDefault( final var importedSemanticContext = importedSemanticContexts.getOrDefault(
parsedSourceFile.fileId(), parsedSourceFile.fileId(),
PbsImportedSemanticContext.empty()); PbsImportedSemanticContext.empty());
@ -152,81 +144,16 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
inlineHintsByFile.put(parsedSourceFile.fileId(), inlineHints); inlineHintsByFile.put(parsedSourceFile.fileId(), inlineHints);
} }
} }
final Map<ModuleReference, Path> physicalModuleFiles = physicalModuleFiles(ctx, assembly);
return new PbsSemanticReadSurface( return new PbsSemanticReadSurface(
Map.copyOf(astByFile), Map.copyOf(astByFile),
Map.copyOf(supplementalTopDeclsByFile), 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( public record PbsSemanticReadSurface(
Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile, Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile,
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile, 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( private IRBackend mergeCompiledSources(

View File

@ -1,109 +0,0 @@
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()));
}
}

View File

@ -1,127 +0,0 @@
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()));
}
}

View File

@ -1,73 +0,0 @@
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;
}
}

View File

@ -1,53 +0,0 @@
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"));
}
}

View File

@ -1,91 +0,0 @@
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();
}
}

View File

@ -1,73 +0,0 @@
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);
}
}

View File

@ -8,7 +8,6 @@ import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsLexerTest { class PbsLexerTest {
@ -35,13 +34,6 @@ class PbsLexerTest {
.kind()); .kind());
assertEquals(PbsTokenKind.EOF, tokens.getLast().kind()); assertEquals(PbsTokenKind.EOF, tokens.getLast().kind());
assertTrue(diagnostics.isEmpty(), "Lexer should not report diagnostics for valid input"); 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 @Test

View File

@ -526,51 +526,6 @@ final class PbsEditorialSupportServiceTest {
assertEquals(List.of("tick"), names(implementsSymbol.children())); 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 @Test
void shouldOmitInvalidDeclarationsAndKeepNamedDeclsFromRecoveredAst() { void shouldOmitInvalidDeclarationsAndKeepNamedDeclsFromRecoveredAst() {
final var source = """ final var source = """
@ -581,8 +536,6 @@ final class PbsEditorialSupportServiceTest {
final var symbols = service.documentSymbols(ast); final var symbols = service.documentSymbols(ast);
assertEquals(List.of("ok"), names(symbols)); assertEquals(List.of("ok"), names(symbols));
assertEquals(PbsEditorialSymbolKind.FUNCTION, symbols.getFirst().kind()); assertEquals(PbsEditorialSymbolKind.FUNCTION, symbols.getFirst().kind());
final var workspaceSymbols = service.workspaceSymbols(ast, "");
assertEquals(List.of("ok"), workspaceSymbols.stream().map(PbsEditorialWorkspaceSymbol::name).toList());
} }
@Test @Test
@ -606,132 +559,6 @@ final class PbsEditorialSupportServiceTest {
assertEquals(List.of("r", "pack"), names(symbols.get(1).children())); 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( private PbsEditorialResolvedSymbol requireHover(
final String source, final String source,
final PbsAst.File ast, final PbsAst.File ast,

View File

@ -1,234 +0,0 @@
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) {
}
}

View File

@ -1,20 +0,0 @@
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");
}
}
}

View File

@ -1,16 +0,0 @@
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");
}
}
}

View File

@ -1,36 +0,0 @@
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;
}
}

View File

@ -1,20 +0,0 @@
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");
}
}
}

View File

@ -1,15 +0,0 @@
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");
}
}
}

View File

@ -11,11 +11,6 @@ import java.util.Optional;
* Optional frontend-owned editor services. * Optional frontend-owned editor services.
*/ */
public interface FrontendLanguageService { public interface FrontendLanguageService {
default Optional<FrontendEditorialContext> prepareEditorialContext(
final FrontendPhaseContext phaseContext) {
return Optional.empty();
}
default Optional<FrontendEditorialContext> prepareEditorialContext( default Optional<FrontendEditorialContext> prepareEditorialContext(
final FrontendPhaseContext phaseContext, final FrontendPhaseContext phaseContext,
final FileId fileId) { final FileId fileId) {
@ -26,12 +21,6 @@ public interface FrontendLanguageService {
return List.of(); return List.of();
} }
default List<FrontendSemanticToken> semanticTokens(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
return semanticTokens(request);
}
default List<FrontendCompletionCandidate> completion( default List<FrontendCompletionCandidate> completion(
final FrontendDocumentRequest request, final FrontendDocumentRequest request,
final int offset) { final int offset) {
@ -106,134 +95,4 @@ public interface FrontendLanguageService {
final FrontendEditorialContext editorialContext) { final FrontendEditorialContext editorialContext) {
return documentSymbols(request); 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);
}
} }

View File

@ -1,22 +0,0 @@
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");
}
}

View File

@ -1,45 +0,0 @@
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;
}
}

View File

@ -1,16 +0,0 @@
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");
}
}
}

View File

@ -17,6 +17,5 @@ public enum FrontendSymbolKind {
ENUM, ENUM,
ERROR, ERROR,
GLOBAL, GLOBAL,
CONST, CONST
MODULE
} }

View File

@ -1,14 +0,0 @@
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;
}
}

View File

@ -1,38 +0,0 @@
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;
}
}

View File

@ -25,56 +25,6 @@ class FrontendLanguageServiceTest {
assertTrue(service.definition(request, 0).isEmpty()); assertTrue(service.definition(request, 0).isEmpty());
assertTrue(service.references(request, 0).isEmpty()); assertTrue(service.references(request, 0).isEmpty());
assertTrue(service.documentSymbols(request).isEmpty()); 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 @Test
@ -140,56 +90,6 @@ class FrontendLanguageServiceTest {
List.of())); 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 @Test
void definitionLocationRejectsBlankPathAndInvertedOffsets() { void definitionLocationRejectsBlankPathAndInvertedOffsets() {
final Path path = Path.of("src/main.pbs").toAbsolutePath(); final Path path = Path.of("src/main.pbs").toAbsolutePath();

View File

@ -1,24 +0,0 @@
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");
}
}
}

View File

@ -1,18 +0,0 @@
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");
}
}
}

View File

@ -1,34 +0,0 @@
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;
}
}

View File

@ -1,16 +0,0 @@
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");
}
}
}

View File

@ -1,13 +0,0 @@
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());
}
}

View File

@ -16,6 +16,5 @@ public enum BaselineCompletionItemKind {
ENUM, ENUM,
ERROR, ERROR,
GLOBAL, GLOBAL,
CONST, CONST
MODULE
} }

View File

@ -9,14 +9,12 @@ public record BaselineDocumentIssue(
int endCharacter, int endCharacter,
BaselineIssueSeverity severity, BaselineIssueSeverity severity,
String source, String source,
String message, String message) {
String code) {
public BaselineDocumentIssue { public BaselineDocumentIssue {
severity = Objects.requireNonNull(severity, "severity"); severity = Objects.requireNonNull(severity, "severity");
source = requireText(source, "source"); source = requireText(source, "source");
message = requireText(message, "message"); message = requireText(message, "message");
code = code == null ? "" : code.trim();
} }
private static String requireText( private static String requireText(

View File

@ -1,28 +0,0 @@
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;
}
}

View File

@ -1,13 +0,0 @@
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());
}
}

View File

@ -1,14 +0,0 @@
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");
}
}
}

View File

@ -1,13 +0,0 @@
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());
}
}

View File

@ -1,13 +0,0 @@
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());
}
}

View File

@ -1,22 +0,0 @@
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();
}
}

View File

@ -1,26 +0,0 @@
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);
}
}

View File

@ -1,30 +0,0 @@
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;
}
}

View File

@ -1,15 +0,0 @@
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");
}
}
}

View File

@ -1,13 +0,0 @@
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());
}
}

View File

@ -13,14 +13,6 @@ public record BaselineServerDescription(
boolean definitionSupported, boolean definitionSupported,
boolean referencesSupported, boolean referencesSupported,
boolean documentSymbolsSupported, boolean documentSymbolsSupported,
boolean workspaceSymbolsSupported,
boolean renameSupported,
boolean codeActionsSupported,
boolean documentLinksSupported,
boolean foldingRangesSupported,
boolean selectionRangesSupported,
boolean formattingSupported,
boolean callHierarchySupported,
List<String> semanticTokenTypes, List<String> semanticTokenTypes,
List<BaselineSemanticHostProjection> semanticHostProjections, List<BaselineSemanticHostProjection> semanticHostProjections,
List<BaselineVisualTheme> visualThemes, List<BaselineVisualTheme> visualThemes,

View File

@ -1,18 +0,0 @@
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");
}
}

View File

@ -1,36 +0,0 @@
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;
}
}

View File

@ -1,15 +0,0 @@
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());
}
}

View File

@ -6,25 +6,11 @@ import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineHover; import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineDocumentSymbols; import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineReferences; 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.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineSignatureHelp; import p.studio.lsp.messages.BaselineSignatureHelp;
import p.studio.lsp.messages.BaselineServerDescription; import p.studio.lsp.messages.BaselineServerDescription;
import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.messages.LspProjectContext;
import java.util.List;
public interface LanguageServiceBridge { public interface LanguageServiceBridge {
BaselineServerDescription describeServer(LspProjectContext context); BaselineServerDescription describeServer(LspProjectContext context);
@ -46,77 +32,9 @@ public interface LanguageServiceBridge {
BaselineDocumentSymbols documentSymbols(LspProjectContext context, String documentUri, String text); 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); BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character);
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text); BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);
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); String onSave(LspProjectContext context, String documentUri);
} }

View File

@ -2,17 +2,9 @@ package p.studio.lsp.services.compiler;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import p.studio.compiler.FrontendRegistryService; import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.exceptions.BuildException;
import p.studio.compiler.messages.*; import p.studio.compiler.messages.*;
import p.studio.compiler.models.*; 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.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.FrontendDefinitionLocation;
import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendDocumentRequest;
import p.studio.compiler.services.FrontendDocumentSymbol; import p.studio.compiler.services.FrontendDocumentSymbol;
@ -22,10 +14,6 @@ import p.studio.compiler.services.FrontendProvider;
import p.studio.compiler.services.FrontendSignature; import p.studio.compiler.services.FrontendSignature;
import p.studio.compiler.services.FrontendSignatureHelp; import p.studio.compiler.services.FrontendSignatureHelp;
import p.studio.compiler.services.FrontendSymbolKind; 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.source.identifiers.FileId;
import p.studio.compiler.utilities.SourceProviderFactory; import p.studio.compiler.utilities.SourceProviderFactory;
import p.studio.compiler.workspaces.AssetSurfaceContextLoader; import p.studio.compiler.workspaces.AssetSurfaceContextLoader;
@ -58,26 +46,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
Objects.requireNonNull(context, "context"); Objects.requireNonNull(context, "context");
final var frontend = frontendSpec(context); final var frontend = frontendSpec(context);
final var presentation = frontend.getSemanticPresentation(); final var presentation = frontend.getSemanticPresentation();
final var languageService = frontendProvider(context).languageService(); final boolean languageServicePresent = frontendProvider(context).languageService().isPresent();
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( return new BaselineServerDescription(
"Prometeu Studio LSP", "Prometeu Studio LSP",
"0.1.0", "0.1.0",
@ -88,14 +57,6 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
languageServicePresent, languageServicePresent,
languageServicePresent, languageServicePresent,
languageServicePresent, languageServicePresent,
languageServicePresent,
languageServicePresent,
codeActionsSupported,
documentLinksSupported,
foldingRangesSupported,
selectionRangesSupported,
formattingSupported,
callHierarchySupported,
presentation.semanticKeys(), presentation.semanticKeys(),
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
presentation.themes().stream().map(this::mapVisualTheme).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(),
@ -111,11 +72,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
final Path documentPath = normalizeDocumentPath(documentUri); final Path documentPath = normalizeDocumentPath(documentUri);
final String effectiveText = StringUtils.isBlank(text) ? "" : text; final String effectiveText = StringUtils.isBlank(text) ? "" : text;
final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText)); final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText));
return new BaselineDocumentAnalysis(mapDiagnostics( return new BaselineDocumentAnalysis(mapDiagnostics(snapshot, documentPath, effectiveText));
snapshot,
documentPath,
effectiveText,
context.languageId()));
} }
@Override @Override
@ -278,442 +235,6 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
.orElseGet(BaselineDocumentSymbols::empty); .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 @Override
public BaselineSignatureHelp signatureHelp( public BaselineSignatureHelp signatureHelp(
final LspProjectContext context, final LspProjectContext context,
@ -759,7 +280,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
documentPath, documentPath,
effectiveText); effectiveText);
final List<FrontendSemanticToken> tokens = provider.languageService() final List<FrontendSemanticToken> tokens = provider.languageService()
.map(languageService -> semanticTokens(languageService, context, documentUri, documentPath, request)) .map(languageService -> languageService.semanticTokens(request))
.orElseGet(List::of); .orElseGet(List::of);
final var positionMapper = new DocumentPositionMapper(effectiveText); final var positionMapper = new DocumentPositionMapper(effectiveText);
final var semanticTokens = new ArrayList<BaselineSemanticToken>(); final var semanticTokens = new ArrayList<BaselineSemanticToken>();
@ -778,26 +299,6 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
return new BaselineSemanticTokens(presentation.semanticKeys(), semanticTokens); 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 @Override
public String onSave( public String onSave(
final LspProjectContext context, final LspProjectContext context,
@ -880,8 +381,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
private List<BaselineDocumentIssue> mapDiagnostics( private List<BaselineDocumentIssue> mapDiagnostics(
final AnalysisSnapshot snapshot, final AnalysisSnapshot snapshot,
final Path documentPath, final Path documentPath,
final String text, final String text) {
final String languageId) {
final ArrayList<BaselineDocumentIssue> issues = new ArrayList<>(); final ArrayList<BaselineDocumentIssue> issues = new ArrayList<>();
final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text); final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text);
for (final BuildingIssue diagnostic : snapshot.diagnostics()) { for (final BuildingIssue diagnostic : snapshot.diagnostics()) {
@ -896,29 +396,26 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) { if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) {
continue; continue;
} }
issues.add(toBaselineIssue(diagnostic, positionMapper, languageId)); issues.add(toBaselineIssue(diagnostic, positionMapper));
} }
return List.copyOf(issues); return List.copyOf(issues);
} }
private BaselineDocumentIssue toBaselineIssue( private BaselineDocumentIssue toBaselineIssue(
final BuildingIssue diagnostic, final BuildingIssue diagnostic,
final DocumentPositionMapper positionMapper, final DocumentPositionMapper positionMapper) {
final String languageId) {
final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart(); final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart();
final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd()); final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd());
final DocumentPosition start = positionMapper.positionOf(startOffset); final DocumentPosition start = positionMapper.positionOf(startOffset);
final DocumentPosition end = positionMapper.positionOf(endOffset); final DocumentPosition end = positionMapper.positionOf(endOffset);
final String code = diagnostic.getCode() == null ? "" : diagnostic.getCode().trim();
return new BaselineDocumentIssue( return new BaselineDocumentIssue(
start.line(), start.line(),
start.character(), start.character(),
end.line(), end.line(),
end.character(), end.character(),
diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING, diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING,
languageId, diagnostic.getCode() == null || diagnostic.getCode().isBlank() ? "Prometeu Studio" : diagnostic.getCode(),
diagnostic.getMessage(), diagnostic.getMessage());
code);
} }
private Path normalizeDocumentPath(final String documentUri) { private Path normalizeDocumentPath(final String documentUri) {
@ -1014,7 +511,6 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
case ERROR -> BaselineCompletionItemKind.ERROR; case ERROR -> BaselineCompletionItemKind.ERROR;
case GLOBAL -> BaselineCompletionItemKind.GLOBAL; case GLOBAL -> BaselineCompletionItemKind.GLOBAL;
case CONST -> BaselineCompletionItemKind.CONST; case CONST -> BaselineCompletionItemKind.CONST;
case MODULE -> BaselineCompletionItemKind.MODULE;
}; };
} }
@ -1062,132 +558,6 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
children); 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( private List<BaselineDefinitionLocation> mapDefinitionLocations(
final List<FrontendDefinitionLocation> locations, final List<FrontendDefinitionLocation> locations,
final Path currentDocumentPath, final Path currentDocumentPath,

View File

@ -29,7 +29,7 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
compilerBridge, compilerBridge,
new Lsp4jProtocolMessageMapper(), new Lsp4jProtocolMessageMapper(),
null, null,
null); new PrometeuWorkspaceService());
} }
PrometeuLanguageServer( PrometeuLanguageServer(
@ -44,9 +44,7 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
this.textDocumentService = textDocumentService != null this.textDocumentService = textDocumentService != null
? textDocumentService ? textDocumentService
: new PrometeuTextDocumentService(project, languageServiceBridge, protocolMessageMapper); : new PrometeuTextDocumentService(project, languageServiceBridge, protocolMessageMapper);
this.workspaceService = workspaceService != null this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService");
? workspaceService
: new PrometeuWorkspaceService(project, languageServiceBridge, protocolMessageMapper);
} }
@Override @Override

View File

@ -1,17 +1,9 @@
package p.studio.lsp.services.protocol; package p.studio.lsp.services.protocol;
import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.Either; 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.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService; 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.messages.LspProjectContext;
import p.studio.lsp.services.LanguageServiceBridge; import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
@ -136,143 +128,6 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
includeDeclaration))); 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 @Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol( public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
final DocumentSymbolParams params) { final DocumentSymbolParams params) {
@ -307,52 +162,6 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
languageServiceBridge.semanticTokens(project, uri, text))); 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( private void publishAnalysis(
final String uri, final String uri,
final String text) { final String text) {

View File

@ -2,36 +2,9 @@ package p.studio.lsp.services.protocol;
import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams; 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 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 { 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 @Override
public void didChangeConfiguration(final DidChangeConfigurationParams params) { public void didChangeConfiguration(final DidChangeConfigurationParams params) {
} }
@ -39,15 +12,4 @@ public final class PrometeuWorkspaceService implements WorkspaceService {
@Override @Override
public void didChangeWatchedFiles(final DidChangeWatchedFilesParams params) { 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))));
}
} }

View File

@ -1,8 +1,6 @@
package p.studio.lsp.services.protocol.mapping; package p.studio.lsp.services.protocol.mapping;
import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
import p.studio.lsp.messages.*; import p.studio.lsp.messages.*;
import java.util.ArrayList; import java.util.ArrayList;
@ -41,35 +39,6 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
capabilities.setDefinitionProvider(description.definitionSupported()); capabilities.setDefinitionProvider(description.definitionSupported());
capabilities.setReferencesProvider(description.referencesSupported()); capabilities.setReferencesProvider(description.referencesSupported());
capabilities.setDocumentSymbolProvider(description.documentSymbolsSupported()); 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(); final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
semanticTokens.setFull(true); semanticTokens.setFull(true);
@ -145,204 +114,6 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return List.copyOf(mapped); 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( private DocumentSymbol mapDocumentSymbol(
final BaselineDocumentSymbol symbol, final BaselineDocumentSymbol symbol,
final BaselineDocumentSymbolKind parentKind) { final BaselineDocumentSymbolKind parentKind) {
@ -364,17 +135,6 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return mapped; 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( private SymbolKind toLspSymbolKind(
final BaselineDocumentSymbolKind kind, final BaselineDocumentSymbolKind kind,
final BaselineDocumentSymbolKind parentKind) { final BaselineDocumentSymbolKind parentKind) {
@ -493,9 +253,6 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
diagnostic.setSeverity(mapSeverity(issue.severity())); diagnostic.setSeverity(mapSeverity(issue.severity()));
diagnostic.setSource(issue.source()); diagnostic.setSource(issue.source());
diagnostic.setMessage(issue.message()); diagnostic.setMessage(issue.message());
if (!issue.code().isBlank()) {
diagnostic.setCode(Either.forLeft(issue.code()));
}
return diagnostic; return diagnostic;
} }
@ -529,51 +286,10 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
case ENUM -> CompletionItemKind.Enum; case ENUM -> CompletionItemKind.Enum;
case ERROR -> CompletionItemKind.EnumMember; case ERROR -> CompletionItemKind.EnumMember;
case GLOBAL, CONST -> CompletionItemKind.Constant; 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) { private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) {
return switch (severity) { return switch (severity) {
case INFORMATION -> DiagnosticSeverity.Information; case INFORMATION -> DiagnosticSeverity.Information;

View File

@ -1,42 +1,15 @@
package p.studio.lsp.services.protocol.mapping; 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.Hover;
import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SemanticTokens; import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DocumentLink;
import org.eclipse.lsp4j.DocumentSymbol; 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.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.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.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.BaselineCompletion;
import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineReferences; import p.studio.lsp.messages.BaselineReferences;
@ -63,43 +36,6 @@ public interface ProtocolMessageMapper {
List<DocumentSymbol> mapDocumentSymbols(BaselineDocumentSymbols symbols); 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); CompletionList mapCompletion(BaselineCompletion completion);
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp); SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);

View File

@ -1,7 +1,6 @@
package p.studio.lsp.services.compiler; package p.studio.lsp.services.compiler;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.BuildingIssueSink; import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FrontendPhaseContext; import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.FrontendSpec; import p.studio.compiler.models.FrontendSpec;
@ -9,15 +8,11 @@ import p.studio.compiler.models.IRBackend;
import p.studio.compiler.services.FrontendPhaseService; import p.studio.compiler.services.FrontendPhaseService;
import p.studio.compiler.services.FrontendProvider; import p.studio.compiler.services.FrontendProvider;
import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
import p.studio.lsp.messages.BaselineIssueSeverity; import p.studio.lsp.messages.BaselineIssueSeverity;
import p.studio.lsp.messages.DocumentPosition;
import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.messages.LspProjectContext;
import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlySet; import p.studio.utilities.structures.ReadOnlySet;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
@ -61,27 +56,9 @@ class CompilerLanguageServiceBridgeTest {
final var documentSymbols = bridge.documentSymbols(context, documentUri, "fn main() {}"); final var documentSymbols = bridge.documentSymbols(context, documentUri, "fn main() {}");
assertTrue(documentSymbols.symbols().isEmpty()); 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() {}"); final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}");
assertTrue(semanticTokens.legend().isEmpty()); assertTrue(semanticTokens.legend().isEmpty());
assertTrue(semanticTokens.tokens().isEmpty()); assertTrue(semanticTokens.tokens().isEmpty());
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 @Test
@ -108,9 +85,6 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.severity() == BaselineIssueSeverity.ERROR)); assertTrue(analysis.issues().stream().anyMatch(issue -> issue.severity() == BaselineIssueSeverity.ERROR));
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.startLine() == 1)); 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().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 @Test
@ -172,112 +146,6 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-builtin-type"))); 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 @Test
void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() { void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()) final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
@ -611,164 +479,6 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(builtinReferences.locations().isEmpty()); 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 @Test
void documentSymbolsUseAstHierarchyOfTheOpenPbsFile() { void documentSymbolsUseAstHierarchyOfTheOpenPbsFile() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()) final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
@ -810,62 +520,6 @@ class CompilerLanguageServiceBridgeTest {
assertNotEquals(symbols.get(1).detail(), symbols.get(2).detail()); 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 @Test
void describeServerPublishesFrontendVisualThemes() { void describeServerPublishesFrontendVisualThemes() {
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
@ -879,14 +533,6 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(description.definitionSupported()); assertTrue(description.definitionSupported());
assertTrue(description.referencesSupported()); assertTrue(description.referencesSupported());
assertTrue(description.documentSymbolsSupported()); 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(1, description.semanticHostProjections().size());
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
@ -900,77 +546,6 @@ class CompilerLanguageServiceBridgeTest {
&& tokenStyle.foreground().equals("#7fb8ff"))); && 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) { private Path findRepoRoot(final Path start) {
var current = start; var current = start;
while (current != null) { while (current != null) {

View File

@ -102,14 +102,6 @@ class PrometeuLanguageServerTest {
true, true,
true, true,
true, true,
true,
true,
false,
false,
false,
false,
false,
false,
List.of(), List.of(),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",
@ -186,34 +178,6 @@ class PrometeuLanguageServerTest {
return p.studio.lsp.messages.BaselineDocumentSymbols.empty(); 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 @Override
public BaselineSignatureHelp signatureHelp( public BaselineSignatureHelp signatureHelp(
final LspProjectContext project, final LspProjectContext project,
@ -229,15 +193,6 @@ class PrometeuLanguageServerTest {
return new BaselineSemanticTokens(List.of(), List.of()); 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 @Override
public String onSave(final LspProjectContext project, final String documentUri) { public String onSave(final LspProjectContext project, final String documentUri) {
return "saved"; return "saved";
@ -286,33 +241,6 @@ class PrometeuLanguageServerTest {
throw new UnsupportedOperationException(); 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 @Override
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) { public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View File

@ -217,34 +217,6 @@ final class PrometeuTextDocumentServiceTest {
List.of()))); 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 @Override
public BaselineSignatureHelp signatureHelp( public BaselineSignatureHelp signatureHelp(
final LspProjectContext context, final LspProjectContext context,
@ -264,15 +236,6 @@ final class PrometeuTextDocumentServiceTest {
return new BaselineSemanticTokens(List.of(), List.of()); 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 @Override
public String onSave(final LspProjectContext context, final String documentUri) { public String onSave(final LspProjectContext context, final String documentUri) {
return "saved"; return "saved";
@ -327,33 +290,6 @@ final class PrometeuTextDocumentServiceTest {
return documentSymbols; 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 @Override
public CompletionList mapCompletion(final BaselineCompletion completion) { public CompletionList mapCompletion(final BaselineCompletion completion) {
return completionList; return completionList;

View File

@ -1,280 +0,0 @@
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();
}
}
}

View File

@ -1,18 +1,10 @@
package p.studio.lsp.services.protocol.mapping; package p.studio.lsp.services.protocol.mapping;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.BaselineCodeAction;
import p.studio.lsp.messages.BaselineCodeActions;
import p.studio.lsp.messages.BaselineCompletion; 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.BaselineCompletionItem;
import p.studio.lsp.messages.BaselineCompletionItemKind; import p.studio.lsp.messages.BaselineCompletionItemKind;
import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineDefinition;
@ -36,7 +28,6 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
final class Lsp4jProtocolMessageMapperTest { final class Lsp4jProtocolMessageMapperTest {
@ -54,14 +45,6 @@ final class Lsp4jProtocolMessageMapperTest {
true, true,
true, true,
true, true,
true,
true,
true,
true,
true,
true,
true,
true,
List.of("demo-keyword"), List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",
@ -85,18 +68,6 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals(Boolean.TRUE, result.getCapabilities().getDefinitionProvider().getLeft()); assertEquals(Boolean.TRUE, result.getCapabilities().getDefinitionProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft()); assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentSymbolProvider().getLeft()); 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 experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
@ -237,135 +208,4 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals(SymbolKind.Field, mapped.get(1).getChildren().getFirst().getKind()); assertEquals(SymbolKind.Field, mapped.get(1).getChildren().getFirst().getKind());
assertTrue(mapper.mapDocumentSymbols(BaselineDocumentSymbols.empty()).isEmpty()); 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");
}
} }

View File

@ -10,8 +10,6 @@ import java.io.BufferedReader;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -24,12 +22,10 @@ import static org.junit.jupiter.api.Assertions.*;
final class StudioRuntimeHandshakeServiceTest { final class StudioRuntimeHandshakeServiceTest {
@Test @Test
void connectsPublishesRuntimeHandshakeAndStreamsLogEvents() throws Exception { void connectsPublishesRuntimeHandshakeAndStreamsLogEvents() throws Exception {
try (final ServerSocket serverSocket = new ServerSocket()) { try (final ServerSocket serverSocket = new ServerSocket(0)) {
serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0));
final CountDownLatch clientStarted = new CountDownLatch(1); final CountDownLatch clientStarted = new CountDownLatch(1);
final CountDownLatch eventDelivered = new CountDownLatch(1); final CountDownLatch eventDelivered = new CountDownLatch(1);
final Thread serverThread = new Thread(() -> serveSuccessfulHandshake(serverSocket, clientStarted, eventDelivered)); final Thread serverThread = new Thread(() -> serveSuccessfulHandshake(serverSocket, clientStarted, eventDelivered));
serverThread.setDaemon(true);
serverThread.start(); serverThread.start();
final StudioExecutionSessionService session = new StudioExecutionSessionService(); final StudioExecutionSessionService session = new StudioExecutionSessionService();
@ -40,7 +36,7 @@ final class StudioRuntimeHandshakeServiceTest {
final StudioRuntimeHandshakeService service = new StudioRuntimeHandshakeService(new ObjectMapper(), backgroundTasks); final StudioRuntimeHandshakeService service = new StudioRuntimeHandshakeService(new ObjectMapper(), backgroundTasks);
final StudioRuntimeHandshakeResult result = service.connect(session, settings); final StudioRuntimeHandshakeResult result = service.connect(session, settings);
assertTrue(result.success(), result.failureMessage()); assertTrue(result.success());
assertEquals(StudioExecutionState.RUNNING, session.snapshot().state()); assertEquals(StudioExecutionState.RUNNING, session.snapshot().state());
assertTrue(clientStarted.await(2, TimeUnit.SECONDS)); assertTrue(clientStarted.await(2, TimeUnit.SECONDS));
assertTrue(eventDelivered.await(2, TimeUnit.SECONDS)); assertTrue(eventDelivered.await(2, TimeUnit.SECONDS));
@ -94,9 +90,6 @@ final class StudioRuntimeHandshakeServiceTest {
writer.flush(); writer.flush();
eventDelivered.countDown(); eventDelivered.countDown();
} catch (Exception exception) { } catch (Exception exception) {
if (serverSocket.isClosed()) {
return;
}
throw new RuntimeException(exception); throw new RuntimeException(exception);
} }
} }