Compare commits
10 Commits
dev/pbs-ls
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7011209675 | |||
| 274ce28e8d | |||
| 1fcecdb04d | |||
| e456bb8d7a | |||
| 202c785f94 | |||
| 7629a73e0a | |||
| ce2e3e4cd3 | |||
| 3320f222bb | |||
| 099857b636 | |||
| 20adc196d9 |
@ -1,4 +1,5 @@
|
||||
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":58,"PLN":137,"LSN":74,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":67,"AGD":70,"DEC":59,"PLN":138,"LSN":75,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0066","status":"done","ticket":"pbs-lsp-remaining-editor-surface","title":"PBS LSP Remaining Editor Surface","created_at":"2026-09-22","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0074","file":"discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/LSN-0074-pbs-remaining-editor-surface-is-frontend-owned.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
|
||||
{"type":"discussion","id":"DSC-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
{"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
{"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
@ -11,15 +12,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-0054","status":"done","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0055","file":"discussion/lessons/DSC-0054-multi-frontend-provider-contract/LSN-0055-static-frontend-providers-before-plugin-architecture.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
|
||||
{"type":"discussion","id":"DSC-0053","status":"open","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0052","status":"open","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[{"id":"AGD-0055","file":"AGD-0055-pbs-lsp-document-links.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0051","status":"open","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[{"id":"AGD-0054","file":"AGD-0054-pbs-lsp-diagnostics-ux.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0050","status":"open","ticket":"pbs-lsp-folding-and-selection-ranges","title":"PBS LSP Folding and Selection Ranges","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","folding","selection-range"],"agendas":[{"id":"AGD-0053","file":"AGD-0053-pbs-lsp-folding-and-selection-ranges.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0049","status":"open","ticket":"pbs-lsp-completion-depth","title":"PBS LSP Completion Depth","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","completion"],"agendas":[{"id":"AGD-0052","file":"AGD-0052-pbs-lsp-completion-depth.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0048","status":"open","ticket":"pbs-lsp-performance-cache-and-snapshots","title":"PBS LSP Performance Cache and Snapshots","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","performance","caching"],"agendas":[{"id":"AGD-0051","file":"AGD-0051-pbs-lsp-performance-cache-and-snapshots.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0047","status":"open","ticket":"pbs-lsp-semantic-tokens-semantic-classification","title":"PBS LSP Semantic Token Classification","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","semantic-tokens"],"agendas":[{"id":"AGD-0050","file":"AGD-0050-pbs-lsp-semantic-token-classification.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0046","status":"open","ticket":"pbs-lsp-import-assistance","title":"PBS LSP Import Assistance","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","imports","completion"],"agendas":[{"id":"AGD-0049","file":"AGD-0049-pbs-lsp-import-assistance.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0045","status":"open","ticket":"pbs-lsp-formatting","title":"PBS LSP Formatting","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","formatting"],"agendas":[{"id":"AGD-0048","file":"AGD-0048-pbs-lsp-formatting.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0053","status":"done","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0052","status":"done","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0051","status":"done","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0050","status":"done","ticket":"pbs-lsp-folding-and-selection-ranges","title":"PBS LSP Folding and Selection Ranges","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","folding","selection-range"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0049","status":"done","ticket":"pbs-lsp-completion-depth","title":"PBS LSP Completion Depth","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","completion"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0048","status":"done","ticket":"pbs-lsp-performance-cache-and-snapshots","title":"PBS LSP Performance Cache and Snapshots","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","performance","caching"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0047","status":"done","ticket":"pbs-lsp-semantic-tokens-semantic-classification","title":"PBS LSP Semantic Token Classification","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","semantic-tokens"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0046","status":"done","ticket":"pbs-lsp-import-assistance","title":"PBS LSP Import Assistance","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","imports","completion"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0045","status":"done","ticket":"pbs-lsp-formatting","title":"PBS LSP Formatting","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","formatting"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0044","status":"done","ticket":"pbs-lsp-code-actions","title":"PBS LSP Code Actions and Quick Fixes","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","code-actions","quick-fix"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0073","file":"discussion/lessons/DSC-0044-pbs-lsp-code-actions/LSN-0073-pbs-quick-fixes-are-frontend-repairs-transported-by-the-lsp.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
|
||||
{"type":"discussion","id":"DSC-0043","status":"done","ticket":"pbs-lsp-rename-symbol","title":"PBS LSP Rename Symbol","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","rename"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0072","file":"discussion/lessons/DSC-0043-pbs-lsp-rename-symbol/LSN-0072-pbs-rename-is-a-validated-physical-workspace-edit.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
|
||||
{"type":"discussion","id":"DSC-0042","status":"done","ticket":"pbs-lsp-workspace-symbols","title":"PBS LSP Workspace Symbols","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","workspace-symbols"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0071","file":"discussion/lessons/DSC-0042-pbs-lsp-workspace-symbols/LSN-0071-pbs-workspace-symbols-are-a-physical-named-declaration-search.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
---
|
||||
id: LSN-0074
|
||||
ticket: pbs-lsp-remaining-editor-surface
|
||||
title: PBS remaining editor surface is frontend-owned and transported by the LSP
|
||||
created: 2026-09-22
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor]
|
||||
---
|
||||
|
||||
# PBS remaining editor surface is frontend-owned and transported by the LSP
|
||||
|
||||
## Original Problem
|
||||
|
||||
Nine open LSP agendas still described formatting, imports, semantic tokens, snapshots, completion, folding, diagnostics, document links, and call hierarchy as if none of that surface existed. By the time those agendas were still open, the server already advertised hover, completion, signature help, definition, references, outline, workspace symbols, rename, quick fix, and full semantic tokens. Each request still rebuilt analysis. References and rename had already shipped without a snapshot cache. There was no diagnostic whose meaning was "this name is not imported."
|
||||
|
||||
Treating the nine agendas as nine decisions would have reimplemented finished work and locked stale premises.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
`DEC-0058` retired that split. One plan, `PLN-0137`, landed seven steps. The frontend produces the editorial result. The LSP transports it and does not invent a second policy. A missing capability is not advertised and returns an empty list, not a protocol error.
|
||||
|
||||
The wave explicitly does not add a project snapshot cache, type hierarchy, `completionItem/resolve`, snippets, a ranking score, range or on-type formatting, organize imports, a missing-import quick fix, `relatedInformation`, or a new semantic-token key.
|
||||
|
||||
1. Published editor diagnostic `source` is the bound frontend `languageId`. For PBS that is `pbs`. `code` stays the stable compiler code. The code is not copied into `source`.
|
||||
2. A document link covers only the span of a `ModuleRef` whose module assembly already resolved to a regular file. Virtual stdlib and unresolved modules produce no link.
|
||||
3. Folding comes from recovered spans of the listed declarations, their bodies, parameter lists, and `Doc` text blocks. A brace, parenthesis, or text block the tree does not cover still folds, including an unmatched opener through the end of the text. Selection grows outward from identifier, to argument or parameter list, to block or `Doc` text block, to declaration.
|
||||
4. Formatting is full-document only. It reprints the token stream, indents by four spaces where brace or parenthesis nesting changes, leaves `Doc` text-block interiors untouched, and does not reorder declarations or move comments off their line.
|
||||
5. Semantic tokens keep `PBSSemanticTokenProvider` as the baseline. Hover resolution may replace an identifier token only when an existing `PbsSemanticKind` matches the symbol. Failure, a broken file, or a symbol with no existing key keeps the baseline token.
|
||||
6. Completion inside a `ModuleRef` lists project and stdlib modules the current resolver can already name. Every other offset keeps the existing member or general completion. Items stay eager and `resolveProvider` stays false.
|
||||
7. Call hierarchy walks `CallExpr` nodes for functions and methods. It does not use the references index. A cursor that is not on a callable is empty. Type hierarchy is not advertised.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
`CompilerLanguageServiceBridge` reads `LspProjectContext.languageId` into `BaselineDocumentIssue.source`. Document links read the physical module map that module assembly already built; stdlib files under `/virtual/stdlib` are not regular files, so they are not links. Folding, selection, and formatting reparse the open buffer with the existing lexer and parser. They do not require a successful project analysis. The semantic-token overlay runs only when editorial context is available and otherwise returns the provider tokens, including when project discovery fails. Module completion labels are the project modules from that same physical map plus the modules `ResourceStdlibEnvironmentResolver` can already resolve. Call hierarchy collects function, method, and constructor declarations from the recovered AST and matches `CallExpr` callees by name and call shape.
|
||||
|
||||
Spec 23 §8.3 states each of these rules. The nine earlier agendas, `AGD-0048` through `AGD-0056`, were abandoned and are not normative.
|
||||
|
||||
## Examples
|
||||
|
||||
A project import `@app:a` that resolves to `src/a/source.pbs` is one document link on that module-reference span. `@sdk:log` is not, because the stdlib file is virtual.
|
||||
|
||||
```pbs
|
||||
fn helper() -> int { return 1; }
|
||||
fn main() -> int { return helper; }
|
||||
```
|
||||
|
||||
The use of `helper` without a call is an identifier in the lexical pass. Hover resolves it as a function, so the overlay key is `pbs-function`. A local such as `value` has no existing key, so it stays `pbs-identifier`.
|
||||
|
||||
```pbs
|
||||
fn helper() -> int { return 1; }
|
||||
fn main() -> int { return helper(); }
|
||||
```
|
||||
|
||||
Prepare on `main` returns that function. Outgoing calls include `helper`. Incoming calls of `helper` include `main`. A cursor on a struct name returns no item.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Do not copy the diagnostic code into `source`. The editor then shows the code twice and loses the language id.
|
||||
- Do not build document links to `/virtual/stdlib`. Definition already refuses that destination.
|
||||
- Do not treat document symbols as folding ranges. Outline and folding are different payloads.
|
||||
- Do not pretty-print from the AST. Comments and `Doc` text-block interiors are not fully modeled as trivia on the tree.
|
||||
- Do not add `pbs-local`, `pbs-parameter`, or `pbs-field` in the overlay. Those symbols stay on the baseline token until a later decision adds the key.
|
||||
- Do not implement call hierarchy by inverting the references index. A reference is a use of a name, not a call.
|
||||
- Do not start a snapshot cache because a later feature feels slow. This wave deliberately kept request-local analysis.
|
||||
|
||||
## References
|
||||
|
||||
- `DEC-0058` and `PLN-0137`, removed by housekeeping after this lesson
|
||||
- `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` §8.3
|
||||
- Earlier surface: `LSN-0068`, `LSN-0069`, `LSN-0070`, `LSN-0071`, `LSN-0072`, `LSN-0073`
|
||||
- Abandoned agendas: `AGD-0048` through `AGD-0056`
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0048
|
||||
ticket: pbs-lsp-formatting
|
||||
title: PBS LSP Formatting
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, formatting]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS formatting is not automated, so style drifts across files, especially around attributes, services, blocks, imports, and multi-line Doc text blocks.
|
||||
|
||||
## Context
|
||||
|
||||
The LSP does not announce documentFormattingProvider. PBS has a parser and AST, but formatting must preserve documentation text block content while normalizing surrounding syntax and indentation.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should formatting be AST-based, token-based, or hybrid to preserve comments and text blocks?
|
||||
- [ ] What canonical style should PBS use for attributes, braces, imports, params, and text blocks?
|
||||
- [ ] Should the first wave support full document formatting only, or also range/on-type formatting?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Token-preserving formatter
|
||||
|
||||
- **Approach:** Format from tokens and trivia, preserving comments and documentation text block contents.
|
||||
- **Pro:** Safer for active editing and text blocks.
|
||||
- **Con:** Harder to enforce deep structural layout.
|
||||
- **Maintainability:** Good if PBS formatting remains mostly syntactic.
|
||||
|
||||
### Option B - AST pretty-printer
|
||||
|
||||
- **Approach:** Reprint code from the AST using canonical style rules.
|
||||
- **Pro:** Produces consistent output.
|
||||
- **Con:** Risks losing comments/trivia and cannot handle malformed files well.
|
||||
- **Maintainability:** Strong only after AST/trivia ownership is explicit.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Formatting must not damage `Doc` text blocks. That makes trivia preservation a first-class requirement rather than an implementation detail.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option A or a hybrid token/AST formatter. Avoid a pure AST pretty-printer until comments/trivia are modeled.
|
||||
|
||||
## Discussion
|
||||
|
||||
Formatter decision should define canonical style before implementation.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide PBS canonical formatting rules for attributes, braces, imports, params, and text blocks.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0049
|
||||
ticket: pbs-lsp-import-assistance
|
||||
title: PBS LSP Import Assistance
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, imports, completion]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS users do not get import-aware assistance, so discovering exported symbols and adding the right import from barrels, stdlib, or project modules remains manual.
|
||||
|
||||
## Context
|
||||
|
||||
Completion already exists for local/editorial contexts, and stdlib imports are understood by the compiler pipeline. The LSP does not yet provide auto-import completion, organize imports, or import quick fixes.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should auto-import target project modules, stdlib modules, or both in the first wave?
|
||||
- [ ] How should barrels and explicit module imports be ranked and rendered in completion?
|
||||
- [ ] Should organize imports be a code action, source action, formatter responsibility, or separate feature?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Import completion only
|
||||
|
||||
- **Approach:** Suggest symbols from known modules and include import detail, but do not edit imports automatically.
|
||||
- **Pro:** Low risk and improves discovery.
|
||||
- **Con:** Still leaves manual import edits.
|
||||
- **Maintainability:** Good as a stepping stone.
|
||||
|
||||
### Option B - Auto-import and organize imports
|
||||
|
||||
- **Approach:** Completion/code actions insert missing imports and source actions sort/remove unused imports.
|
||||
- **Pro:** Matches modern editor expectations.
|
||||
- **Con:** Requires robust module export indexing and edit placement rules.
|
||||
- **Maintainability:** Strong if built on a proper import graph.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Auto-import depends on knowing canonical exports and choosing an edit location. Doing that ad hoc in completion would create formatting and duplicate import issues.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B as the target, but start with import-aware suggestions plus a limited missing-import quick fix.
|
||||
|
||||
## Discussion
|
||||
|
||||
This should be coordinated with code actions and formatter.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide first-wave import sources: stdlib only, project modules only, or both.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0050
|
||||
ticket: pbs-lsp-semantic-tokens-semantic-classification
|
||||
title: PBS LSP Semantic Token Classification
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, semantic-tokens]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS semantic highlighting exists, but parts of the token classification can remain lexical or superficial, which limits visual accuracy for same-looking identifiers with different semantic roles.
|
||||
|
||||
## Context
|
||||
|
||||
The current LSP announces full semantic tokens and maps frontend semantic presentation to VS Code. The compiler has semantic read surfaces that can classify symbols more accurately than raw tokenization.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which token categories must become semantic identity-based first?
|
||||
- [ ] Should semantic tokens be produced from compiler semantic surfaces, parser AST, lexer tokens, or layered output?
|
||||
- [ ] How should tokens behave in files with syntax or semantic errors during active editing?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Lexical tokens with semantic overlays
|
||||
|
||||
- **Approach:** Keep lexer-based tokenization as baseline and overlay semantic categories where resolution succeeds.
|
||||
- **Pro:** Stable during broken edits and incremental to improve.
|
||||
- **Con:** Some unresolved identifiers remain visually generic.
|
||||
- **Maintainability:** Strong because fallback behavior is explicit.
|
||||
|
||||
### Option B - Fully semantic token stream
|
||||
|
||||
- **Approach:** Generate all semantic tokens from compiler semantic surfaces.
|
||||
- **Pro:** Maximum accuracy when analysis succeeds.
|
||||
- **Con:** Highlighting may disappear or degrade sharply on incomplete code.
|
||||
- **Maintainability:** Risky unless snapshot/error recovery is excellent.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Highlighting must be resilient while the user is typing. A perfect semantic pass that fails on partial code is worse than a layered model.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option A: lexical baseline plus semantic overlays from resolved symbols.
|
||||
|
||||
## Discussion
|
||||
|
||||
This can proceed independently but benefits from LSP snapshot/cache work.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Choose semantic overlay categories for the first pass.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0051
|
||||
ticket: pbs-lsp-performance-cache-and-snapshots
|
||||
title: PBS LSP Performance Cache and Snapshots
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, performance, caching]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
Several PBS LSP requests rebuild or reanalyze substantial project state, which can become slow or jittery as projects grow and as richer features are added.
|
||||
|
||||
## Context
|
||||
|
||||
The current bridge favors correctness and simplicity by invoking compiler pipeline analysis for diagnostics and editorial documents. A complete LSP needs snapshot caching, invalidation, cancellation, and predictable latency.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] What is the canonical project/document snapshot boundary for LSP requests?
|
||||
- [ ] Which requests can reuse semantic read surfaces safely across document changes?
|
||||
- [ ] How should cancellation, stale responses, and background reanalysis be handled?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Request-local analysis
|
||||
|
||||
- **Approach:** Keep each LSP request self-contained and rebuild analysis as needed.
|
||||
- **Pro:** Simple, deterministic, and less stateful.
|
||||
- **Con:** Latency grows with project size and feature richness.
|
||||
- **Maintainability:** Good for correctness, weak for interactive performance.
|
||||
|
||||
### Option B - Project snapshot cache
|
||||
|
||||
- **Approach:** Maintain per-project analysis snapshots with document overlays, invalidation, and cancellation-aware request handling.
|
||||
- **Pro:** Required for responsive definition, references, rename, and semantic tokens.
|
||||
- **Con:** Introduces lifecycle complexity and stale-result risks.
|
||||
- **Maintainability:** Strong if snapshot ownership and invalidation are explicit.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
A complete LSP needs shared state, but shared state must not leak stale analysis into edits. The decision is about the boundary, not just caching.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B before implementing workspace-wide features like references and rename.
|
||||
|
||||
## Discussion
|
||||
|
||||
Define snapshot lifecycle, invalidation triggers, and cancellation policy.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide whether snapshot caching is a prerequisite for references/rename or can be staged later.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0052
|
||||
ticket: pbs-lsp-completion-depth
|
||||
title: PBS LSP Completion Depth
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, completion]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS completion works for core cases, but a complete daily-use experience needs ranking, resolve details, snippets, overload handling, context-aware suggestions, and richer documentation.
|
||||
|
||||
## Context
|
||||
|
||||
The current LSP announces completion with trigger character '.' and returns detail/documentation for resolved compiler candidates. It does not use resolveProvider, snippets, broad trigger contexts, or advanced ranking.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which completion contexts should be supported first beyond member access?
|
||||
- [ ] Should completion use resolveProvider for expensive documentation/details or return everything eagerly?
|
||||
- [ ] How should overloads, snippets, imports, keywords, and stdlib candidates be ranked?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Eager rich completion
|
||||
|
||||
- **Approach:** Return labels, details, docs, and snippets directly in every completion response.
|
||||
- **Pro:** Simple client behavior and immediate docs.
|
||||
- **Con:** Can become expensive for broad/global completion.
|
||||
- **Maintainability:** Good while candidate sets remain small.
|
||||
|
||||
### Option B - Completion resolve provider
|
||||
|
||||
- **Approach:** Return lightweight candidates first and populate expensive docs/details through `completionItem/resolve`.
|
||||
- **Pro:** Scales better for large candidate sets and import-aware suggestions.
|
||||
- **Con:** Requires client/server resolve plumbing and stable candidate identities.
|
||||
- **Maintainability:** Strong for a full LSP.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
The current eager model is fine for scoped completions, but global/import-aware completion may need resolve to avoid latency.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer a staged approach: keep eager for current contexts, design resolve before broad workspace/import completion.
|
||||
|
||||
## Discussion
|
||||
|
||||
Define completion contexts and ranking before adding snippets or resolve.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
List first-wave completion contexts beyond member access.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0053
|
||||
ticket: pbs-lsp-folding-and-selection-ranges
|
||||
title: PBS LSP Folding and Selection Ranges
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, folding, selection-range]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS users do not get structural folding or smart selection ranges, so navigating and selecting nested functions, blocks, services, attributes, and Doc text blocks is more manual than it should be.
|
||||
|
||||
## Context
|
||||
|
||||
The parser knows syntactic spans for declarations and blocks. The LSP currently does not announce foldingRangeProvider or selectionRangeProvider.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should folding ranges be computed from AST spans, token pairs, or a hybrid that handles malformed files?
|
||||
- [ ] Which constructs should fold in the first wave: imports, declarations, blocks, attributes, text blocks, or all of them?
|
||||
- [ ] What selection range hierarchy should VS Code expose for identifiers, calls, params, blocks, and declarations?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Parser span ranges
|
||||
|
||||
- **Approach:** Use AST spans for declarations, blocks, parameter lists, and text blocks.
|
||||
- **Pro:** Accurate for well-formed code and easy to map to LSP ranges.
|
||||
- **Con:** Can degrade when parsing fails mid-file.
|
||||
- **Maintainability:** Good because parser owns structure.
|
||||
|
||||
### Option B - Token pair scanning
|
||||
|
||||
- **Approach:** Compute ranges from brace/paren/text-block tokens without relying on full AST validity.
|
||||
- **Pro:** More resilient during malformed edits.
|
||||
- **Con:** Less semantically meaningful and can produce noisy ranges.
|
||||
- **Maintainability:** Good only as fallback.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Folding and selection should keep working while typing. AST ranges are higher quality, but token fallback may be needed for broken files.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer a hybrid: AST ranges first, token fallback for unmatched/incomplete constructs.
|
||||
|
||||
## Discussion
|
||||
|
||||
This is an editor UX feature with low semantic dependency and can be planned independently.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide first-wave foldable constructs and selection hierarchy.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0054
|
||||
ticket: pbs-lsp-diagnostics-ux
|
||||
title: PBS LSP Diagnostics UX
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, diagnostics, ux]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS diagnostics reach VS Code, but the editor experience can still be hard to act on when ranges, related information, severity, and phase context are too generic.
|
||||
|
||||
## Context
|
||||
|
||||
The LSP maps compiler diagnostics to LSP diagnostics. Compiler diagnostics include phase/code/message/range in several paths, but not all editor-friendly fields are consistently exposed.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which diagnostics need better ranges or related information first?
|
||||
- [ ] Should diagnostic source include compiler phase, frontend, or both?
|
||||
- [ ] What structured diagnostic metadata is needed for future quick fixes without coupling UI to text messages?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Mapper-only UX improvements
|
||||
|
||||
- **Approach:** Improve LSP diagnostic mapping using existing diagnostic fields.
|
||||
- **Pro:** Quick improvements to source, severity, and display consistency.
|
||||
- **Con:** Cannot add missing related spans or repair metadata that compiler diagnostics do not expose.
|
||||
- **Maintainability:** Good for presentation-only changes.
|
||||
|
||||
### Option B - Structured diagnostic contract
|
||||
|
||||
- **Approach:** Extend compiler diagnostics with editor-facing metadata such as phase, related locations, stable quick-fix hints, and refined ranges.
|
||||
- **Pro:** Enables better diagnostics and future code actions without parsing message text.
|
||||
- **Con:** Requires compiler/core diagnostic API changes.
|
||||
- **Maintainability:** Strong as the foundation for quick fixes and richer UX.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Some improvements belong in the LSP mapper, but the most valuable changes require structured information at diagnostic creation time.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B as the target, with mapper-only cleanup allowed for low-risk display fixes.
|
||||
|
||||
## Discussion
|
||||
|
||||
This agenda should inform code actions before quick fixes become broad.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Inventory high-frequency diagnostics and identify which need better ranges, related information, or repair hints.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0055
|
||||
ticket: pbs-lsp-document-links
|
||||
title: PBS LSP Document Links
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, document-links, imports]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS import/module references and asset-like surfaces are plain text in VS Code, so users cannot click through to imported modules or related project resources.
|
||||
|
||||
## Context
|
||||
|
||||
The compiler understands module references and project files. The LSP does not announce documentLinkProvider, and the VS Code extension delegates all language behavior to the server.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which references should become document links first: imports, barrels, stdlib modules, assets/addressables, or docs?
|
||||
- [ ] How should virtual or stdlib resources be represented as link targets?
|
||||
- [ ] Should document links overlap with go-to-definition or remain a separate lightweight navigation feature?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Import/module links only
|
||||
|
||||
- **Approach:** Expose `documentLink` for module references such as `@sdk:gfx` and barrel paths.
|
||||
- **Pro:** Narrow, useful, and mostly aligned with existing module resolution.
|
||||
- **Con:** Does not cover assets/addressables or documentation links.
|
||||
- **Maintainability:** Strong first wave.
|
||||
|
||||
### Option B - General resource links
|
||||
|
||||
- **Approach:** Link imports, stdlib modules, assets, addressables, and possibly documentation/resource surfaces.
|
||||
- **Pro:** Richer navigation across project resources.
|
||||
- **Con:** Requires resource-specific target policy and may overlap with definition.
|
||||
- **Maintainability:** Good only after resource ownership is defined.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Document links should remain lightweight. If a link requires semantic identity and symbol resolution, it may belong in go-to-definition instead.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option A first: imports and module references only.
|
||||
|
||||
## Discussion
|
||||
|
||||
Coordinate stdlib target handling with go-to-definition.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide first-wave link targets and how stdlib/module URIs are represented.
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0056
|
||||
ticket: pbs-lsp-call-and-type-hierarchy
|
||||
title: PBS LSP Call and Type Hierarchy
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, call-hierarchy, type-hierarchy]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS users cannot inspect call relationships or type/member relationships from the editor, which limits understanding of larger projects after basic navigation exists.
|
||||
|
||||
## Context
|
||||
|
||||
Call hierarchy and type hierarchy are advanced LSP features. They depend on reliable semantic identity, references, callsite classification, and type ownership surfaces, so they should probably follow definition/references rather than precede them.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Should call hierarchy and type hierarchy be one discussion or split after dependencies are clearer?
|
||||
- [ ] Which relationships are valuable for PBS: function call graph, service method calls, struct methods, contract implementations, host/intrinsic calls, or all of them?
|
||||
- [ ] What prerequisite symbol/reference infrastructure must be completed before this feature is safe to implement?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Call hierarchy only
|
||||
|
||||
- **Approach:** Implement incoming/outgoing calls for functions and methods after callsite references are stable.
|
||||
- **Pro:** Directly useful and builds on executable call resolution.
|
||||
- **Con:** Does not cover type ownership, contracts, or implementations.
|
||||
- **Maintainability:** Strong if based on the reference/callsite index.
|
||||
|
||||
### Option B - Split call hierarchy and type hierarchy
|
||||
|
||||
- **Approach:** Treat call graph and type/member/contract hierarchy as separate decisions and implementations.
|
||||
- **Pro:** Avoids mixing two different semantic models.
|
||||
- **Con:** Creates more workflow artifacts and sequencing work.
|
||||
- **Maintainability:** Strong; each feature can evolve on its own contract.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Call hierarchy and type hierarchy share LSP vocabulary but not the same compiler data. Coupling them too early would make the first implementation harder to reason about.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B: split after this agenda confirms dependencies, likely implementing call hierarchy first.
|
||||
|
||||
## Discussion
|
||||
|
||||
This should wait until definition and references are complete.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide whether to split into two discussions before moving to decisions.
|
||||
@ -286,6 +286,22 @@ When a frontend exposes code actions, the result MUST be quick fixes only: one w
|
||||
|
||||
Compiler diagnostics and editor diagnostics are distinct ownership surfaces. Diagnostics returned by `analyze`, `compile`, and `build` are compiler contract output. Editor diagnostics MAY reuse compiler analysis results, live overlays, caches, or cancellation-aware tooling state, but a frontend MUST NOT be required to provide editor diagnostics in order to compile.
|
||||
|
||||
When tooling publishes an editor diagnostic, `source` MUST be the `languageId` of the bound frontend project. `code` MUST be the stable compiler diagnostic code when one exists, and MUST be empty when the compiler diagnostic has none. Tooling MUST NOT copy that code into `source`. Tooling MUST NOT publish related locations, compiler phase, or a repair payload on the editor diagnostic.
|
||||
|
||||
When a frontend exposes document links, each link MUST cover the span of a module reference whose resolved destination is a compiler-known regular file. A module reference that does not resolve to a regular file MUST produce no link. Tooling MUST NOT target virtual, untitled, or synthetic URIs, including virtual stdlib documents. Document links MUST NOT replace definition. A missing document-link capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
|
||||
|
||||
When a frontend exposes folding ranges, the ranges MUST come from recovered syntax spans for top-level `fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, and `host` declarations, their bodies, and `Doc` text blocks. A parameter list MUST fold from the parenthesis pair. A brace, parenthesis, or text-block delimiter that the recovered tree does not cover MUST still produce a token range, including an unmatched opener through the end of the text. Folding ranges MUST NOT be document symbols.
|
||||
|
||||
When a frontend exposes selection ranges, the chain at a cursor MUST run from the inside outward in this order: identifier, argument or parameter list, block or `Doc` text block, declaration. A layer that does not contain the cursor MUST be omitted. A missing folding or selection capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
|
||||
|
||||
When a frontend exposes call hierarchy, incoming and outgoing calls MUST be callsites found by walking call expressions in the syntax tree. The references index MUST NOT be used as the call graph. A cursor that is not on a function, method, or constructor MUST produce an empty prepare result. Type hierarchy MUST NOT be advertised. A missing call-hierarchy capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
|
||||
|
||||
When a frontend exposes completion inside a module reference, the candidates MUST be project and stdlib modules the current module resolver can already name. Completion outside a module reference MUST keep its existing member and general candidates. Completion items MUST stay eager. `resolveProvider` MUST stay false. Snippets and a new ranking score MUST NOT be added.
|
||||
|
||||
When a frontend exposes semantic tokens, the lexical token provider remains the baseline. An identifier token MAY be replaced with an existing semantic key when the same resolution used by hover names that symbol and an existing key matches it. Resolution failure, a broken file, or a symbol with no existing key MUST keep the baseline token. This overlay MUST NOT add a semantic-token key.
|
||||
|
||||
When a frontend exposes formatting, it MUST be full-document formatting only. Range formatting and on-type formatting MUST NOT be advertised. The formatter MUST reprint the existing token stream. It MUST NOT reorder declarations, join lines, or split lines. Indentation MUST be four spaces and MUST change only where brace or parenthesis nesting changes. The interior of a `Doc` text block MUST be copied unchanged. A comment MUST stay on the line of the token it already follows. A missing formatting capability MUST NOT be advertised and MUST produce an empty edit list rather than a protocol error.
|
||||
|
||||
`FrontendSpec` remains the source of static frontend-owned presentation metadata such as semantic vocabularies, host projections, and visual themes. Producing semantic tokens for a live document is an optional editor-facing capability; the existence of static presentation metadata MUST NOT imply that every frontend can provide live semantic-token results.
|
||||
|
||||
The frontend registry MUST resolve providers by `languageId`. A lookup for an unknown `languageId` MUST fail explicitly with a diagnostic-friendly error. Unknown languages MUST NOT silently fall back to PBS or to any other frontend.
|
||||
|
||||
@ -16,11 +16,21 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocum
|
||||
import p.studio.compiler.models.SourceKind;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialRename;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol;
|
||||
import p.studio.compiler.pbs.PbsCallHierarchy;
|
||||
import p.studio.compiler.pbs.PbsDocumentFormatter;
|
||||
import p.studio.compiler.pbs.PbsSemanticTokenOverlay;
|
||||
import p.studio.compiler.pbs.PbsStructureRanges;
|
||||
import p.studio.compiler.pbs.semantics.PbsQuickFix;
|
||||
import p.studio.compiler.pbs.semantics.PbsQuickFixCollector;
|
||||
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
|
||||
import p.studio.compiler.services.FrontendCallHierarchyItem;
|
||||
import p.studio.compiler.services.FrontendCallSite;
|
||||
import p.studio.compiler.services.FrontendCodeAction;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendSymbolKind;
|
||||
import p.studio.compiler.services.FrontendDocumentLink;
|
||||
import p.studio.compiler.services.FrontendFoldingRange;
|
||||
import p.studio.compiler.services.FrontendSelectionRange;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendDocumentSymbol;
|
||||
@ -40,6 +50,7 @@ import p.studio.compiler.source.Span;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.tables.FileTableReader;
|
||||
import p.studio.compiler.source.tables.ModuleReference;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Files;
|
||||
@ -128,6 +139,10 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (insideModuleRef(ast, offset)
|
||||
&& editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface) {
|
||||
return moduleCompletions(surface.moduleCompletionLabels());
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
|
||||
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
|
||||
@ -401,6 +416,157 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
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;
|
||||
@ -477,13 +643,73 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
|
||||
@Override
|
||||
public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
|
||||
return semanticTokens(request == null ? "" : request.documentText());
|
||||
return semanticTokens(request, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendSemanticToken> semanticTokens(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
final String text = request == null || request.documentText() == null ? "" : request.documentText();
|
||||
final List<FrontendSemanticToken> baseline = semanticTokenProvider.tokenize(text);
|
||||
if (editorialContext == null
|
||||
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|
||||
|| !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList<?> supplementalTopDecls)
|
||||
|| !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext)) {
|
||||
return baseline;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
|
||||
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
|
||||
return PbsSemanticTokenOverlay.apply(
|
||||
baseline,
|
||||
text,
|
||||
ast,
|
||||
typedSupplementalTopDecls,
|
||||
feSurfaceContext,
|
||||
editorialSupportService);
|
||||
}
|
||||
|
||||
public List<FrontendSemanticToken> semanticTokens(final String documentText) {
|
||||
return semanticTokenProvider.tokenize(documentText == null ? "" : documentText);
|
||||
}
|
||||
|
||||
private static boolean insideModuleRef(final PbsAst.File ast, final int offset) {
|
||||
if (ast == null || ast.imports() == null || offset < 0) {
|
||||
return false;
|
||||
}
|
||||
for (final PbsAst.ImportDecl importDecl : ast.imports()) {
|
||||
if (importDecl == null || importDecl.moduleRef() == null || importDecl.moduleRef().span() == null) {
|
||||
continue;
|
||||
}
|
||||
final long start = importDecl.moduleRef().span().getStart();
|
||||
final long end = importDecl.moduleRef().span().getEnd();
|
||||
if (end > start && start <= offset && offset <= end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<FrontendCompletionCandidate> moduleCompletions(final List<String> labels) {
|
||||
if (labels == null || labels.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<FrontendCompletionCandidate> candidates = new ArrayList<>();
|
||||
for (final String label : labels) {
|
||||
if (label == null || label.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
candidates.add(new FrontendCompletionCandidate(
|
||||
label,
|
||||
FrontendSymbolKind.MODULE,
|
||||
"module",
|
||||
"",
|
||||
""));
|
||||
}
|
||||
return List.copyOf(candidates);
|
||||
}
|
||||
|
||||
private FrontendCompletionCandidate toFrontendCompletionCandidate(final PbsEditorialCompletionCandidate candidate) {
|
||||
return new FrontendCompletionCandidate(
|
||||
candidate.label(),
|
||||
|
||||
@ -0,0 +1,335 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.source.Span;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class PbsCallHierarchy {
|
||||
private PbsCallHierarchy() {
|
||||
}
|
||||
|
||||
public record Callable(
|
||||
String name,
|
||||
boolean method,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
}
|
||||
|
||||
public record CallSite(
|
||||
Callable caller,
|
||||
Callable callee,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
}
|
||||
|
||||
public static List<Callable> prepare(final PbsAst.File ast, final int offset) {
|
||||
Callable best = null;
|
||||
for (final Callable callable : callables(ast)) {
|
||||
if (callable.startOffset() > offset || offset >= callable.endOffset()) {
|
||||
continue;
|
||||
}
|
||||
if (best == null || callable.endOffset() - callable.startOffset() < best.endOffset() - best.startOffset()) {
|
||||
best = callable;
|
||||
}
|
||||
}
|
||||
return best == null ? List.of() : List.of(best);
|
||||
}
|
||||
|
||||
public static List<CallSite> incomingCalls(final PbsAst.File ast, final Callable target) {
|
||||
if (target == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<CallSite> calls = new ArrayList<>();
|
||||
for (final CallSite call : calls(ast)) {
|
||||
if (sameCallable(call.callee(), target)) {
|
||||
calls.add(call);
|
||||
}
|
||||
}
|
||||
return List.copyOf(calls);
|
||||
}
|
||||
|
||||
public static List<CallSite> outgoingCalls(final PbsAst.File ast, final Callable target) {
|
||||
if (target == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<CallSite> calls = new ArrayList<>();
|
||||
for (final CallSite call : calls(ast)) {
|
||||
if (sameCallable(call.caller(), target)) {
|
||||
calls.add(call);
|
||||
}
|
||||
}
|
||||
return List.copyOf(calls);
|
||||
}
|
||||
|
||||
private static boolean sameCallable(final Callable left, final Callable right) {
|
||||
return left != null
|
||||
&& right != null
|
||||
&& left.method() == right.method()
|
||||
&& left.name().equals(right.name())
|
||||
&& left.startOffset() == right.startOffset()
|
||||
&& left.endOffset() == right.endOffset();
|
||||
}
|
||||
|
||||
private static List<Callable> callables(final PbsAst.File ast) {
|
||||
final ArrayList<Callable> callables = new ArrayList<>();
|
||||
if (ast == null || ast.topDecls() == null) {
|
||||
return callables;
|
||||
}
|
||||
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
|
||||
collectCallables(topDecl, callables);
|
||||
}
|
||||
return callables;
|
||||
}
|
||||
|
||||
private static void collectCallables(final PbsAst.TopDecl topDecl, final List<Callable> callables) {
|
||||
switch (topDecl) {
|
||||
case null -> {
|
||||
}
|
||||
case PbsAst.FunctionDecl functionDecl -> callables.add(callable(functionDecl.name(), false, functionDecl.span()));
|
||||
case PbsAst.StructDecl structDecl -> {
|
||||
if (structDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
|
||||
if (method != null) {
|
||||
callables.add(callable(method.name(), true, method.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (structDecl.ctors() != null) {
|
||||
for (final PbsAst.CtorDecl ctor : structDecl.ctors()) {
|
||||
if (ctor != null) {
|
||||
callables.add(callable(ctor.name(), true, ctor.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.ServiceDecl serviceDecl -> {
|
||||
if (serviceDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
|
||||
if (method != null) {
|
||||
callables.add(callable(method.name(), true, method.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<CallSite> calls(final PbsAst.File ast) {
|
||||
final List<Callable> callables = callables(ast);
|
||||
final ArrayList<CallSite> calls = new ArrayList<>();
|
||||
for (final Callable caller : callables) {
|
||||
final PbsAst.Block body = body(ast, caller);
|
||||
if (body == null) {
|
||||
continue;
|
||||
}
|
||||
final ArrayList<PbsAst.CallExpr> found = new ArrayList<>();
|
||||
collectCalls(body, found);
|
||||
for (final PbsAst.CallExpr call : found) {
|
||||
final Callable callee = resolveCallee(call, callables);
|
||||
final int[] span = offsets(call.span());
|
||||
if (callee == null || span == null) {
|
||||
continue;
|
||||
}
|
||||
calls.add(new CallSite(caller, callee, span[0], span[1]));
|
||||
}
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
private static Callable resolveCallee(final PbsAst.CallExpr call, final List<Callable> callables) {
|
||||
if (call == null || call.callee() == null) {
|
||||
return null;
|
||||
}
|
||||
final boolean method = call.callee() instanceof PbsAst.MemberExpr;
|
||||
final String name = switch (call.callee()) {
|
||||
case PbsAst.IdentifierExpr identifier -> identifier.name();
|
||||
case PbsAst.MemberExpr member -> member.memberName();
|
||||
default -> null;
|
||||
};
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
for (final Callable callable : callables) {
|
||||
if (callable.method() == method && callable.name().equals(name)) {
|
||||
return callable;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PbsAst.Block body(final PbsAst.File ast, final Callable caller) {
|
||||
for (final Callable callable : callables(ast)) {
|
||||
if (!sameCallable(callable, caller)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return findBody(ast, caller);
|
||||
}
|
||||
|
||||
private static PbsAst.Block findBody(final PbsAst.File ast, final Callable caller) {
|
||||
if (ast.topDecls() == null) {
|
||||
return null;
|
||||
}
|
||||
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
|
||||
final PbsAst.Block body = findBody(topDecl, caller);
|
||||
if (body != null) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PbsAst.Block findBody(final PbsAst.TopDecl topDecl, final Callable caller) {
|
||||
switch (topDecl) {
|
||||
case null -> {
|
||||
return null;
|
||||
}
|
||||
case PbsAst.FunctionDecl functionDecl -> {
|
||||
if (matches(functionDecl.name(), false, functionDecl.span(), caller)) {
|
||||
return functionDecl.body();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case PbsAst.StructDecl structDecl -> {
|
||||
if (structDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
|
||||
if (method != null && matches(method.name(), true, method.span(), caller)) {
|
||||
return method.body();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (structDecl.ctors() != null) {
|
||||
for (final PbsAst.CtorDecl ctor : structDecl.ctors()) {
|
||||
if (ctor != null && matches(ctor.name(), true, ctor.span(), caller)) {
|
||||
return ctor.body();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case PbsAst.ServiceDecl serviceDecl -> {
|
||||
if (serviceDecl.methods() == null) {
|
||||
return null;
|
||||
}
|
||||
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
|
||||
if (method != null && matches(method.name(), true, method.span(), caller)) {
|
||||
return method.body();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectCalls(final PbsAst.Block block, final List<PbsAst.CallExpr> calls) {
|
||||
if (block == null || block.statements() == null) {
|
||||
return;
|
||||
}
|
||||
for (final PbsAst.Statement statement : block.statements()) {
|
||||
collectCalls(statement, calls);
|
||||
}
|
||||
collectCalls(block.tailExpression(), calls);
|
||||
}
|
||||
|
||||
private static void collectCalls(final PbsAst.Statement statement, final List<PbsAst.CallExpr> calls) {
|
||||
switch (statement) {
|
||||
case null -> {
|
||||
}
|
||||
case PbsAst.LetStatement letStatement -> collectCalls(letStatement.initializer(), calls);
|
||||
case PbsAst.ReturnStatement returnStatement -> collectCalls(returnStatement.value(), calls);
|
||||
case PbsAst.AssignStatement assignStatement -> collectCalls(assignStatement.value(), calls);
|
||||
case PbsAst.ExpressionStatement expressionStatement -> collectCalls(expressionStatement.expression(), calls);
|
||||
case PbsAst.IfStatement ifStatement -> {
|
||||
collectCalls(ifStatement.condition(), calls);
|
||||
collectCalls(ifStatement.thenBlock(), calls);
|
||||
collectCalls(ifStatement.elseBlock(), calls);
|
||||
collectCalls(ifStatement.elseIf(), calls);
|
||||
}
|
||||
case PbsAst.ForStatement forStatement -> {
|
||||
collectCalls(forStatement.fromExpression(), calls);
|
||||
collectCalls(forStatement.untilExpression(), calls);
|
||||
collectCalls(forStatement.stepExpression(), calls);
|
||||
collectCalls(forStatement.body(), calls);
|
||||
}
|
||||
case PbsAst.WhileStatement whileStatement -> {
|
||||
collectCalls(whileStatement.condition(), calls);
|
||||
collectCalls(whileStatement.body(), calls);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectCalls(final PbsAst.Expression expression, final List<PbsAst.CallExpr> calls) {
|
||||
switch (expression) {
|
||||
case null -> {
|
||||
}
|
||||
case PbsAst.CallExpr callExpr -> {
|
||||
calls.add(callExpr);
|
||||
collectCalls(callExpr.callee(), calls);
|
||||
if (callExpr.arguments() != null) {
|
||||
for (final PbsAst.Expression argument : callExpr.arguments()) {
|
||||
collectCalls(argument, calls);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.MemberExpr memberExpr -> collectCalls(memberExpr.receiver(), calls);
|
||||
case PbsAst.BinaryExpr binaryExpr -> {
|
||||
collectCalls(binaryExpr.left(), calls);
|
||||
collectCalls(binaryExpr.right(), calls);
|
||||
}
|
||||
case PbsAst.UnaryExpr unaryExpr -> collectCalls(unaryExpr.expression(), calls);
|
||||
case PbsAst.GroupExpr groupExpr -> collectCalls(groupExpr.expression(), calls);
|
||||
case PbsAst.IfExpr ifExpr -> {
|
||||
collectCalls(ifExpr.condition(), calls);
|
||||
collectCalls(ifExpr.thenBlock(), calls);
|
||||
collectCalls(ifExpr.elseExpression(), calls);
|
||||
}
|
||||
case PbsAst.BlockExpr blockExpr -> collectCalls(blockExpr.block(), calls);
|
||||
case PbsAst.ApplyExpr applyExpr -> {
|
||||
collectCalls(applyExpr.callee(), calls);
|
||||
collectCalls(applyExpr.argument(), calls);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matches(
|
||||
final String name,
|
||||
final boolean method,
|
||||
final Span span,
|
||||
final Callable caller) {
|
||||
final Callable callable = callable(name, method, span);
|
||||
return callable != null && sameCallable(callable, caller);
|
||||
}
|
||||
|
||||
private static Callable callable(final String name, final boolean method, final Span span) {
|
||||
final int[] offsets = offsets(span);
|
||||
if (name == null || name.isBlank() || offsets == null) {
|
||||
return null;
|
||||
}
|
||||
return new Callable(name, method, offsets[0], offsets[1]);
|
||||
}
|
||||
|
||||
private static int[] offsets(final Span span) {
|
||||
if (span == null || span.getEnd() <= span.getStart()) {
|
||||
return null;
|
||||
}
|
||||
return new int[] {toOffset(span.getStart()), toOffset(span.getEnd())};
|
||||
}
|
||||
|
||||
private static int toOffset(final long value) {
|
||||
if (value <= 0L) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.min(Integer.MAX_VALUE, value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.lexer.PbsToken;
|
||||
import p.studio.compiler.pbs.lexer.PbsTokenKind;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class PbsDocumentFormatter {
|
||||
private static final int INDENT_SPACES = 4;
|
||||
|
||||
private PbsDocumentFormatter() {
|
||||
}
|
||||
|
||||
public static String format(final String documentText) {
|
||||
final String text = documentText == null ? "" : documentText;
|
||||
if (text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
final ReadOnlyList<PbsToken> tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty());
|
||||
final ArrayList<SourceLine> lines = splitLines(text);
|
||||
final StringBuilder formatted = new StringBuilder(text.length());
|
||||
int depth = 0;
|
||||
for (final SourceLine line : lines) {
|
||||
if (isDocInterior(line, tokens)) {
|
||||
formatted.append(line.content()).append(line.ending());
|
||||
continue;
|
||||
}
|
||||
final int leadingClosers = leadingClosers(line, tokens);
|
||||
final int indent = Math.max(0, depth - leadingClosers);
|
||||
formatted.append(reindent(line.content(), indent)).append(line.ending());
|
||||
depth = depthAfter(line, tokens, depth);
|
||||
}
|
||||
return formatted.toString();
|
||||
}
|
||||
|
||||
private static boolean isDocInterior(final SourceLine line, final ReadOnlyList<PbsToken> tokens) {
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token.kind() != PbsTokenKind.DOC_TEXT_BLOCK) {
|
||||
continue;
|
||||
}
|
||||
if (line.startByte() > token.start() && line.endByte() <= token.end()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int leadingClosers(final SourceLine line, final ReadOnlyList<PbsToken> tokens) {
|
||||
int closers = 0;
|
||||
boolean seenCode = false;
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token.start() < line.startByte() || token.start() >= line.endByte()) {
|
||||
continue;
|
||||
}
|
||||
if (token.kind() == PbsTokenKind.COMMENT) {
|
||||
continue;
|
||||
}
|
||||
if (!seenCode && isCloser(token.kind())) {
|
||||
closers += 1;
|
||||
continue;
|
||||
}
|
||||
seenCode = true;
|
||||
}
|
||||
return closers;
|
||||
}
|
||||
|
||||
private static int depthAfter(
|
||||
final SourceLine line,
|
||||
final ReadOnlyList<PbsToken> tokens,
|
||||
final int depth) {
|
||||
int next = depth;
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token.start() < line.startByte() || token.start() >= line.endByte()) {
|
||||
continue;
|
||||
}
|
||||
if (isOpener(token.kind())) {
|
||||
next += 1;
|
||||
} else if (isCloser(token.kind())) {
|
||||
next = Math.max(0, next - 1);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private static boolean isOpener(final PbsTokenKind kind) {
|
||||
return kind == PbsTokenKind.LEFT_BRACE || kind == PbsTokenKind.LEFT_PAREN;
|
||||
}
|
||||
|
||||
private static boolean isCloser(final PbsTokenKind kind) {
|
||||
return kind == PbsTokenKind.RIGHT_BRACE || kind == PbsTokenKind.RIGHT_PAREN;
|
||||
}
|
||||
|
||||
private static String reindent(final String content, final int indent) {
|
||||
int index = 0;
|
||||
while (index < content.length()) {
|
||||
final char character = content.charAt(index);
|
||||
if (character != ' ' && character != '\t') {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if (index == content.length()) {
|
||||
return content;
|
||||
}
|
||||
return " ".repeat(indent * INDENT_SPACES) + content.substring(index);
|
||||
}
|
||||
|
||||
private static ArrayList<SourceLine> splitLines(final String text) {
|
||||
final ArrayList<SourceLine> lines = new ArrayList<>();
|
||||
final byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
||||
int charIndex = 0;
|
||||
int byteIndex = 0;
|
||||
int lineCharStart = 0;
|
||||
int lineByteStart = 0;
|
||||
while (charIndex < text.length()) {
|
||||
final int codePoint = text.codePointAt(charIndex);
|
||||
final int charWidth = Character.charCount(codePoint);
|
||||
final int byteWidth = utf8Length(codePoint);
|
||||
if (codePoint == '\n') {
|
||||
lines.add(new SourceLine(
|
||||
text.substring(lineCharStart, charIndex),
|
||||
"\n",
|
||||
lineByteStart,
|
||||
byteIndex));
|
||||
charIndex += charWidth;
|
||||
byteIndex += byteWidth;
|
||||
lineCharStart = charIndex;
|
||||
lineByteStart = byteIndex;
|
||||
continue;
|
||||
}
|
||||
if (codePoint == '\r' && charIndex + charWidth < text.length() && text.codePointAt(charIndex + charWidth) == '\n') {
|
||||
lines.add(new SourceLine(
|
||||
text.substring(lineCharStart, charIndex),
|
||||
"\r\n",
|
||||
lineByteStart,
|
||||
byteIndex));
|
||||
charIndex += charWidth + 1;
|
||||
byteIndex += byteWidth + 1;
|
||||
lineCharStart = charIndex;
|
||||
lineByteStart = byteIndex;
|
||||
continue;
|
||||
}
|
||||
charIndex += charWidth;
|
||||
byteIndex += byteWidth;
|
||||
}
|
||||
if (lineCharStart < text.length()) {
|
||||
lines.add(new SourceLine(
|
||||
text.substring(lineCharStart),
|
||||
"",
|
||||
lineByteStart,
|
||||
bytes.length));
|
||||
}
|
||||
if (lines.isEmpty()) {
|
||||
lines.add(new SourceLine(text, "", 0, bytes.length));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static int utf8Length(final int codePoint) {
|
||||
if (codePoint <= 0x7F) {
|
||||
return 1;
|
||||
}
|
||||
if (codePoint <= 0x7FF) {
|
||||
return 2;
|
||||
}
|
||||
if (codePoint <= 0xFFFF) {
|
||||
return 3;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
private record SourceLine(String content, String ending, int startByte, int endByte) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import p.studio.compiler.messages.FESurfaceContext;
|
||||
import p.studio.compiler.models.FrontendSemanticToken;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class PbsSemanticTokenOverlay {
|
||||
private static final Set<String> LEXICAL_KEYS = Set.of(
|
||||
PbsSemanticKind.COMMENT.semanticKey(),
|
||||
PbsSemanticKind.STRING.semanticKey(),
|
||||
PbsSemanticKind.NUMBER.semanticKey(),
|
||||
PbsSemanticKind.LITERAL.semanticKey(),
|
||||
PbsSemanticKind.KEYWORD.semanticKey(),
|
||||
PbsSemanticKind.OPERATOR.semanticKey(),
|
||||
PbsSemanticKind.PUNCTUATION.semanticKey());
|
||||
|
||||
private PbsSemanticTokenOverlay() {
|
||||
}
|
||||
|
||||
public static List<FrontendSemanticToken> apply(
|
||||
final List<FrontendSemanticToken> baseline,
|
||||
final String documentText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final FESurfaceContext feSurfaceContext,
|
||||
final PbsEditorialSupportService editorialSupportService) {
|
||||
if (baseline == null || baseline.isEmpty() || ast == null || editorialSupportService == null) {
|
||||
return baseline == null ? List.of() : List.copyOf(baseline);
|
||||
}
|
||||
final String text = documentText == null ? "" : documentText;
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementals = supplementalTopDecls == null
|
||||
? ReadOnlyList.empty()
|
||||
: supplementalTopDecls;
|
||||
final FESurfaceContext surface = feSurfaceContext == null ? FESurfaceContext.empty() : feSurfaceContext;
|
||||
final ArrayList<FrontendSemanticToken> overlaid = new ArrayList<>(baseline.size());
|
||||
for (final FrontendSemanticToken token : baseline) {
|
||||
overlaid.add(overlay(token, text, ast, supplementals, surface, editorialSupportService));
|
||||
}
|
||||
return List.copyOf(overlaid);
|
||||
}
|
||||
|
||||
private static FrontendSemanticToken overlay(
|
||||
final FrontendSemanticToken token,
|
||||
final String text,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementals,
|
||||
final FESurfaceContext surface,
|
||||
final PbsEditorialSupportService editorialSupportService) {
|
||||
if (token == null || LEXICAL_KEYS.contains(token.semanticKey())) {
|
||||
return token;
|
||||
}
|
||||
try {
|
||||
final String resolvedKey = editorialSupportService.hover(
|
||||
text,
|
||||
ast,
|
||||
supplementals,
|
||||
surface,
|
||||
token.startOffset())
|
||||
.map(PbsSemanticTokenOverlay::semanticKey)
|
||||
.orElse(null);
|
||||
if (resolvedKey == null || resolvedKey.equals(token.semanticKey())) {
|
||||
return token;
|
||||
}
|
||||
return new FrontendSemanticToken(token.startOffset(), token.endOffset(), resolvedKey);
|
||||
} catch (final RuntimeException ignored) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
private static String semanticKey(final PbsEditorialResolvedSymbol symbol) {
|
||||
if (symbol == null || symbol.kind() == null) {
|
||||
return null;
|
||||
}
|
||||
final PbsSemanticKind kind = switch (symbol.kind()) {
|
||||
case FUNCTION -> PbsSemanticKind.FUNCTION;
|
||||
case METHOD -> PbsSemanticKind.METHOD;
|
||||
case CONSTRUCTOR -> PbsSemanticKind.CONSTRUCTOR;
|
||||
case STRUCT -> PbsSemanticKind.STRUCT;
|
||||
case BUILTIN_TYPE -> PbsSemanticKind.BUILTIN_TYPE;
|
||||
case SERVICE -> PbsSemanticKind.SERVICE;
|
||||
case HOST -> PbsSemanticKind.HOST;
|
||||
case CONTRACT -> PbsSemanticKind.CONTRACT;
|
||||
case CALLBACK -> PbsSemanticKind.CALLBACK;
|
||||
case ENUM -> PbsSemanticKind.ENUM;
|
||||
case ERROR -> PbsSemanticKind.ERROR;
|
||||
case GLOBAL -> PbsSemanticKind.GLOBAL;
|
||||
case CONST -> PbsSemanticKind.CONST;
|
||||
case KEYWORD, LOCAL, PARAMETER, FIELD -> null;
|
||||
};
|
||||
return kind == null ? null : kind.semanticKey();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,380 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.lexer.PbsToken;
|
||||
import p.studio.compiler.pbs.lexer.PbsTokenKind;
|
||||
import p.studio.compiler.pbs.parser.PbsParser;
|
||||
import p.studio.compiler.services.FrontendFoldingRange;
|
||||
import p.studio.compiler.services.FrontendSelectionRange;
|
||||
import p.studio.compiler.source.Span;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
public final class PbsStructureRanges {
|
||||
private PbsStructureRanges() {
|
||||
}
|
||||
|
||||
public static List<FrontendFoldingRange> foldingRanges(final String documentText) {
|
||||
final String text = documentText == null ? "" : documentText;
|
||||
final ReadOnlyList<PbsToken> tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty());
|
||||
final PbsAst.File ast = PbsParser.parse(tokens, FileId.none(), DiagnosticSink.empty());
|
||||
return foldingRanges(text, ast, tokens);
|
||||
}
|
||||
|
||||
public static List<FrontendSelectionRange> selectionRanges(
|
||||
final String documentText,
|
||||
final List<Integer> offsets) {
|
||||
final String text = documentText == null ? "" : documentText;
|
||||
final ReadOnlyList<PbsToken> tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty());
|
||||
final PbsAst.File ast = PbsParser.parse(tokens, FileId.none(), DiagnosticSink.empty());
|
||||
if (offsets == null || offsets.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final Collected collected = collect(ast);
|
||||
final List<SpanPair> delimiters = delimiters(tokens, textEnd(text));
|
||||
final ArrayList<FrontendSelectionRange> ranges = new ArrayList<>();
|
||||
for (final Integer offset : offsets) {
|
||||
if (offset == null || offset < 0) {
|
||||
continue;
|
||||
}
|
||||
final FrontendSelectionRange range = selectionAt(offset, tokens, collected, delimiters);
|
||||
if (range != null) {
|
||||
ranges.add(range);
|
||||
}
|
||||
}
|
||||
return List.copyOf(ranges);
|
||||
}
|
||||
|
||||
private static List<FrontendFoldingRange> foldingRanges(
|
||||
final String text,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsToken> tokens) {
|
||||
final Collected collected = collect(ast);
|
||||
final LinkedHashSet<String> emitted = new LinkedHashSet<>();
|
||||
final ArrayList<SpanPair> ranges = new ArrayList<>();
|
||||
for (final SpanPair span : collected.folding()) {
|
||||
addRange(ranges, emitted, span);
|
||||
}
|
||||
for (final SpanPair delimiter : delimiters(tokens, textEnd(text))) {
|
||||
if (collected.coveredKeys().contains(delimiter.key())) {
|
||||
continue;
|
||||
}
|
||||
addRange(ranges, emitted, delimiter);
|
||||
}
|
||||
ranges.sort(Comparator.comparingInt(SpanPair::start).thenComparingInt(SpanPair::end));
|
||||
return ranges.stream()
|
||||
.map(span -> new FrontendFoldingRange(span.start(), span.end()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static FrontendSelectionRange selectionAt(
|
||||
final int offset,
|
||||
final ReadOnlyList<PbsToken> tokens,
|
||||
final Collected collected,
|
||||
final List<SpanPair> delimiters) {
|
||||
final SpanPair identifier = smallest(identifierSpans(tokens), offset);
|
||||
final SpanPair parameters = smallest(delimiters, offset);
|
||||
final SpanPair block = smallest(collected.blocks(), offset);
|
||||
final SpanPair declaration = smallest(collected.declarations(), offset);
|
||||
FrontendSelectionRange node = null;
|
||||
SpanPair previous = null;
|
||||
for (final SpanPair layer : new SpanPair[] {declaration, block, parameters, identifier}) {
|
||||
if (layer == null) {
|
||||
continue;
|
||||
}
|
||||
if (previous != null && previous.start() == layer.start() && previous.end() == layer.end()) {
|
||||
continue;
|
||||
}
|
||||
node = new FrontendSelectionRange(layer.start(), layer.end(), node);
|
||||
previous = layer;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private static Collected collect(final PbsAst.File ast) {
|
||||
final Collected collected = new Collected();
|
||||
if (ast == null || ast.topDecls() == null) {
|
||||
return collected;
|
||||
}
|
||||
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
|
||||
visitTop(topDecl, true, collected);
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
private static void visitTop(
|
||||
final PbsAst.TopDecl topDecl,
|
||||
final boolean topLevel,
|
||||
final Collected collected) {
|
||||
if (topDecl == null) {
|
||||
return;
|
||||
}
|
||||
if (topLevel && isListedDeclaration(topDecl)) {
|
||||
addSpan(collected.folding(), topDecl.span());
|
||||
}
|
||||
if (isListedDeclaration(topDecl) || topDecl instanceof PbsAst.FunctionDecl) {
|
||||
addSpan(collected.declarations(), topDecl.span());
|
||||
}
|
||||
switch (topDecl) {
|
||||
case PbsAst.FunctionDecl functionDecl -> visitFunction(functionDecl, collected);
|
||||
case PbsAst.StructDecl structDecl -> {
|
||||
visitAttributes(structDecl.attributes(), collected);
|
||||
if (structDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
|
||||
visitTop(method, false, collected);
|
||||
}
|
||||
}
|
||||
if (structDecl.ctors() != null) {
|
||||
for (final PbsAst.CtorDecl ctor : structDecl.ctors()) {
|
||||
visitCtor(ctor, collected);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.ServiceDecl serviceDecl -> {
|
||||
visitAttributes(serviceDecl.attributes(), collected);
|
||||
if (serviceDecl.methods() != null) {
|
||||
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
|
||||
visitTop(method, false, collected);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.ContractDecl contractDecl -> visitAttributes(contractDecl.attributes(), collected);
|
||||
case PbsAst.HostDecl hostDecl -> visitAttributes(hostDecl.attributes(), collected);
|
||||
case PbsAst.EnumDecl enumDecl -> visitAttributes(enumDecl.attributes(), collected);
|
||||
case PbsAst.ErrorDecl errorDecl -> visitAttributes(errorDecl.attributes(), collected);
|
||||
case PbsAst.CallbackDecl callbackDecl -> visitAttributes(callbackDecl.attributes(), collected);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void visitFunction(
|
||||
final PbsAst.FunctionDecl functionDecl,
|
||||
final Collected collected) {
|
||||
visitAttributes(functionDecl.attributes(), collected);
|
||||
coverBlock(functionDecl.body(), true, collected);
|
||||
}
|
||||
|
||||
private static void visitCtor(
|
||||
final PbsAst.CtorDecl ctor,
|
||||
final Collected collected) {
|
||||
if (ctor == null) {
|
||||
return;
|
||||
}
|
||||
addSpan(collected.declarations(), ctor.span());
|
||||
coverBlock(ctor.body(), true, collected);
|
||||
}
|
||||
|
||||
private static void visitAttributes(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final Collected collected) {
|
||||
if (attributes == null) {
|
||||
return;
|
||||
}
|
||||
for (final PbsAst.Attribute attribute : attributes) {
|
||||
if (attribute == null || attribute.arguments() == null) {
|
||||
continue;
|
||||
}
|
||||
for (final PbsAst.AttributeArgument argument : attribute.arguments()) {
|
||||
if (argument != null && argument.value() instanceof PbsAst.AttributeDocTextBlockValue doc) {
|
||||
addSpan(collected.folding(), doc.span());
|
||||
addSpan(collected.blocks(), doc.span());
|
||||
collected.cover(doc.span());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void coverBlock(
|
||||
final PbsAst.Block block,
|
||||
final boolean emit,
|
||||
final Collected collected) {
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
if (emit) {
|
||||
addSpan(collected.folding(), block.span());
|
||||
}
|
||||
addSpan(collected.blocks(), block.span());
|
||||
collected.cover(block.span());
|
||||
if (block.statements() == null) {
|
||||
return;
|
||||
}
|
||||
for (final PbsAst.Statement statement : block.statements()) {
|
||||
coverStatement(statement, collected);
|
||||
}
|
||||
}
|
||||
|
||||
private static void coverStatement(
|
||||
final PbsAst.Statement statement,
|
||||
final Collected collected) {
|
||||
switch (statement) {
|
||||
case null -> {
|
||||
}
|
||||
case PbsAst.IfStatement ifStatement -> {
|
||||
coverBlock(ifStatement.thenBlock(), false, collected);
|
||||
coverBlock(ifStatement.elseBlock(), false, collected);
|
||||
coverStatement(ifStatement.elseIf(), collected);
|
||||
}
|
||||
case PbsAst.ForStatement forStatement -> coverBlock(forStatement.body(), false, collected);
|
||||
case PbsAst.WhileStatement whileStatement -> coverBlock(whileStatement.body(), false, collected);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isListedDeclaration(final PbsAst.TopDecl topDecl) {
|
||||
return topDecl instanceof PbsAst.FunctionDecl
|
||||
|| topDecl instanceof PbsAst.StructDecl
|
||||
|| topDecl instanceof PbsAst.ServiceDecl
|
||||
|| topDecl instanceof PbsAst.ContractDecl
|
||||
|| topDecl instanceof PbsAst.HostDecl
|
||||
|| topDecl instanceof PbsAst.EnumDecl
|
||||
|| topDecl instanceof PbsAst.ErrorDecl
|
||||
|| topDecl instanceof PbsAst.CallbackDecl;
|
||||
}
|
||||
|
||||
private static List<SpanPair> delimiters(
|
||||
final ReadOnlyList<PbsToken> tokens,
|
||||
final int textEnd) {
|
||||
final ArrayList<PbsToken> stack = new ArrayList<>();
|
||||
final ArrayList<SpanPair> pairs = new ArrayList<>();
|
||||
if (tokens == null) {
|
||||
return pairs;
|
||||
}
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
if (token.kind() == PbsTokenKind.LEFT_BRACE || token.kind() == PbsTokenKind.LEFT_PAREN) {
|
||||
stack.add(token);
|
||||
continue;
|
||||
}
|
||||
if (token.kind() == PbsTokenKind.DOC_TEXT_BLOCK) {
|
||||
addPair(pairs, token.start(), token.end());
|
||||
continue;
|
||||
}
|
||||
if (token.kind() == PbsTokenKind.RIGHT_BRACE || token.kind() == PbsTokenKind.RIGHT_PAREN) {
|
||||
final PbsTokenKind openKind = token.kind() == PbsTokenKind.RIGHT_BRACE
|
||||
? PbsTokenKind.LEFT_BRACE
|
||||
: PbsTokenKind.LEFT_PAREN;
|
||||
if (!stack.isEmpty() && stack.getLast().kind() == openKind) {
|
||||
final PbsToken open = stack.removeLast();
|
||||
addPair(pairs, open.start(), token.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final PbsToken open : stack) {
|
||||
addPair(pairs, open.start(), textEnd);
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
private static List<SpanPair> identifierSpans(final ReadOnlyList<PbsToken> tokens) {
|
||||
final ArrayList<SpanPair> spans = new ArrayList<>();
|
||||
if (tokens == null) {
|
||||
return spans;
|
||||
}
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token != null && token.kind() == PbsTokenKind.IDENTIFIER) {
|
||||
addPair(spans, token.start(), token.end());
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
private static SpanPair smallest(final List<SpanPair> spans, final int offset) {
|
||||
SpanPair best = null;
|
||||
for (final SpanPair span : spans) {
|
||||
if (span.start() > offset || offset >= span.end()) {
|
||||
continue;
|
||||
}
|
||||
if (best == null
|
||||
|| span.end() - span.start() < best.end() - best.start()
|
||||
|| (span.end() - span.start() == best.end() - best.start() && span.start() > best.start())) {
|
||||
best = span;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static void addSpan(final List<SpanPair> spans, final Span span) {
|
||||
if (span == null) {
|
||||
return;
|
||||
}
|
||||
addPair(spans, toOffset(span.getStart()), toOffset(span.getEnd()));
|
||||
}
|
||||
|
||||
private static void addPair(final List<SpanPair> spans, final int start, final int end) {
|
||||
if (start < 0 || end <= start) {
|
||||
return;
|
||||
}
|
||||
spans.add(new SpanPair(start, end));
|
||||
}
|
||||
|
||||
private static void addRange(
|
||||
final List<SpanPair> ranges,
|
||||
final LinkedHashSet<String> emitted,
|
||||
final SpanPair span) {
|
||||
if (span == null || !emitted.add(span.key())) {
|
||||
return;
|
||||
}
|
||||
ranges.add(span);
|
||||
}
|
||||
|
||||
private static int textEnd(final String text) {
|
||||
return text.getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
private static int toOffset(final long value) {
|
||||
if (value <= 0L) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.min(Integer.MAX_VALUE, value);
|
||||
}
|
||||
|
||||
private record SpanPair(int start, int end) {
|
||||
private String key() {
|
||||
return start + ":" + end;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Collected {
|
||||
private final ArrayList<SpanPair> folding = new ArrayList<>();
|
||||
private final ArrayList<SpanPair> covered = new ArrayList<>();
|
||||
private final ArrayList<SpanPair> blocks = new ArrayList<>();
|
||||
private final ArrayList<SpanPair> declarations = new ArrayList<>();
|
||||
|
||||
private List<SpanPair> folding() {
|
||||
return folding;
|
||||
}
|
||||
|
||||
private void cover(final Span span) {
|
||||
addSpan(covered, span);
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> coveredKeys() {
|
||||
final LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
for (final SpanPair span : covered) {
|
||||
keys.add(span.key());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private List<SpanPair> blocks() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private List<SpanPair> declarations() {
|
||||
return declarations;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,9 +5,15 @@ import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.JarEntry;
|
||||
|
||||
public final class ResourceStdlibEnvironmentResolver implements StdlibEnvironmentResolver {
|
||||
private static final String STDLIB_ROOT = "stdlib";
|
||||
@ -37,6 +43,73 @@ public final class ResourceStdlibEnvironmentResolver implements StdlibEnvironmen
|
||||
barrelSource.get()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyList<StdlibModuleSource> modules() {
|
||||
final String root = target.exportReferenceName().toLowerCase()
|
||||
+ "/" + STDLIB_ROOT + "/" + stdlibVersion;
|
||||
final ClassLoader classLoader = ResourceStdlibEnvironmentResolver.class.getClassLoader();
|
||||
if (classLoader == null) {
|
||||
return ReadOnlyList.empty();
|
||||
}
|
||||
final URL url = classLoader.getResource(root);
|
||||
if (url == null) {
|
||||
return ReadOnlyList.empty();
|
||||
}
|
||||
try {
|
||||
final List<List<String>> modulePaths = "jar".equals(url.getProtocol())
|
||||
? jarModulePaths(url, root)
|
||||
: fileModulePaths(url);
|
||||
final ArrayList<StdlibModuleSource> modules = new ArrayList<>();
|
||||
for (final List<String> modulePath : modulePaths) {
|
||||
if (modulePath.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
final String project = modulePath.getFirst();
|
||||
final ReadOnlyList<String> segments = ReadOnlyList.wrap(modulePath.subList(1, modulePath.size()));
|
||||
resolveModule(project, segments).ifPresent(modules::add);
|
||||
}
|
||||
return ReadOnlyList.wrap(modules);
|
||||
} catch (final Exception ignored) {
|
||||
return ReadOnlyList.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private List<List<String>> fileModulePaths(final URL url) throws Exception {
|
||||
final Path rootPath = Path.of(url.toURI());
|
||||
final ArrayList<List<String>> modulePaths = new ArrayList<>();
|
||||
try (var walk = Files.walk(rootPath)) {
|
||||
walk.filter(path -> "main.pbs".equals(String.valueOf(path.getFileName()))
|
||||
&& Files.isRegularFile(path.getParent().resolve("mod.barrel")))
|
||||
.forEach(path -> modulePaths.add(segments(rootPath.relativize(path.getParent()))));
|
||||
}
|
||||
return modulePaths;
|
||||
}
|
||||
|
||||
private List<List<String>> jarModulePaths(final URL url, final String root) throws IOException {
|
||||
final JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||
final var jarFile = connection.getJarFile();
|
||||
final String prefix = root + "/";
|
||||
final ArrayList<List<String>> modulePaths = new ArrayList<>();
|
||||
jarFile.stream()
|
||||
.map(JarEntry::getName)
|
||||
.filter(name -> name.startsWith(prefix) && name.endsWith("/main.pbs"))
|
||||
.forEach(name -> {
|
||||
final String directory = name.substring(prefix.length(), name.length() - "/main.pbs".length());
|
||||
if (!directory.isBlank() && jarFile.getEntry(prefix + directory + "/mod.barrel") != null) {
|
||||
modulePaths.add(List.of(directory.split("/")));
|
||||
}
|
||||
});
|
||||
return modulePaths;
|
||||
}
|
||||
|
||||
private static List<String> segments(final Path relative) {
|
||||
final ArrayList<String> segments = new ArrayList<>();
|
||||
for (final Path segment : relative) {
|
||||
segments.add(segment.toString());
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private String moduleBasePath(
|
||||
final String project,
|
||||
final ReadOnlyList<String> pathSegments) {
|
||||
|
||||
@ -6,5 +6,9 @@ import java.util.Optional;
|
||||
|
||||
public interface StdlibEnvironment {
|
||||
Optional<StdlibModuleSource> resolveModule(String project, ReadOnlyList<String> pathSegments);
|
||||
|
||||
default ReadOnlyList<StdlibModuleSource> modules() {
|
||||
return ReadOnlyList.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,9 +15,12 @@ import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.identifiers.ModuleId;
|
||||
import p.studio.compiler.source.tables.ModuleReference;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@ -149,18 +152,81 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
|
||||
inlineHintsByFile.put(parsedSourceFile.fileId(), inlineHints);
|
||||
}
|
||||
}
|
||||
final Map<ModuleReference, Path> physicalModuleFiles = physicalModuleFiles(ctx, assembly);
|
||||
return new PbsSemanticReadSurface(
|
||||
Map.copyOf(astByFile),
|
||||
Map.copyOf(supplementalTopDeclsByFile),
|
||||
Map.copyOf(inlineHintsByFile),
|
||||
Map.copyOf(sourceKindByFile));
|
||||
Map.copyOf(sourceKindByFile),
|
||||
physicalModuleFiles,
|
||||
moduleCompletionLabels(ctx, physicalModuleFiles));
|
||||
}
|
||||
|
||||
private static Map<ModuleReference, Path> physicalModuleFiles(
|
||||
final FrontendPhaseContext ctx,
|
||||
final PbsModuleAssembly assembly) {
|
||||
final Map<ModuleReference, Path> files = new LinkedHashMap<>();
|
||||
if (ctx == null || ctx.fileTable == null || assembly == null || assembly.parsedSourceFiles() == null) {
|
||||
return Map.of();
|
||||
}
|
||||
for (final PbsParsedSourceFile parsed : assembly.parsedSourceFiles()) {
|
||||
if (parsed.fileId() == null || parsed.fileId().isNone()
|
||||
|| parsed.moduleId() == null || parsed.moduleId().isNone()) {
|
||||
continue;
|
||||
}
|
||||
final var handle = ctx.fileTable.get(parsed.fileId());
|
||||
if (handle == null || handle.getCanonPath() == null) {
|
||||
continue;
|
||||
}
|
||||
final Path path = handle.getCanonPath().toAbsolutePath().normalize();
|
||||
if (!Files.isRegularFile(path)) {
|
||||
continue;
|
||||
}
|
||||
final ModuleReference reference = assembly.moduleTable().get(parsed.moduleId());
|
||||
if (reference == null) {
|
||||
continue;
|
||||
}
|
||||
files.merge(reference, path, (left, right) ->
|
||||
left.toString().compareTo(right.toString()) <= 0 ? left : right);
|
||||
}
|
||||
return Map.copyOf(files);
|
||||
}
|
||||
|
||||
private static List<String> moduleCompletionLabels(
|
||||
final FrontendPhaseContext ctx,
|
||||
final Map<ModuleReference, Path> physicalModuleFiles) {
|
||||
final var labels = new java.util.TreeSet<String>();
|
||||
for (final ModuleReference reference : physicalModuleFiles.keySet()) {
|
||||
labels.add(moduleLabel(reference.project(), reference.pathSegments()));
|
||||
}
|
||||
if (ctx != null) {
|
||||
final var environment = new ResourceStdlibEnvironmentResolver().resolve(ctx.stdlibVersion(), ctx.target());
|
||||
for (final var module : environment.modules()) {
|
||||
labels.add(moduleLabel(module.project(), module.pathSegments()));
|
||||
}
|
||||
}
|
||||
return List.copyOf(labels);
|
||||
}
|
||||
|
||||
private static String moduleLabel(
|
||||
final String project,
|
||||
final ReadOnlyList<String> pathSegments) {
|
||||
if (project == null || project.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
if (pathSegments == null || pathSegments.isEmpty()) {
|
||||
return "@" + project;
|
||||
}
|
||||
return "@" + project + ":" + String.join("/", pathSegments.asList());
|
||||
}
|
||||
|
||||
public record PbsSemanticReadSurface(
|
||||
Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile,
|
||||
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
|
||||
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile,
|
||||
Map<FileId, SourceKind> sourceKindByFile) {
|
||||
Map<FileId, SourceKind> sourceKindByFile,
|
||||
Map<ModuleReference, Path> physicalModuleFileByReference,
|
||||
List<String> moduleCompletionLabels) {
|
||||
}
|
||||
|
||||
private IRBackend mergeCompiledSources(
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
package p.studio.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.AppMode;
|
||||
import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.BuildStack;
|
||||
import p.studio.compiler.models.ProjectDescriptor;
|
||||
import p.studio.compiler.models.SourceHandle;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.tables.FileTable;
|
||||
import p.studio.compiler.source.tables.ProjectTable;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PBSFrontendDocumentLinkTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void documentLinksUseTheAssembledPhysicalModuleFile() throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path sourceRoot = projectRoot.resolve("src");
|
||||
final Path sourceA = sourceRoot.resolve("a").resolve("source.pbs");
|
||||
final Path sourceB = sourceRoot.resolve("b").resolve("source.pbs");
|
||||
Files.createDirectories(sourceA.getParent());
|
||||
Files.createDirectories(sourceB.getParent());
|
||||
Files.writeString(sourceA, """
|
||||
fn target() -> int {
|
||||
return 1;
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
|
||||
final String caller = """
|
||||
import { target } from @app:a;
|
||||
import { Log } from @sdk:log;
|
||||
import { Missing } from @app:missing;
|
||||
|
||||
fn caller() -> int {
|
||||
return target();
|
||||
}
|
||||
""";
|
||||
Files.writeString(sourceB, caller);
|
||||
Files.writeString(sourceB.getParent().resolve("mod.barrel"), "pub fn caller() -> int;\n");
|
||||
|
||||
final var projectTable = new ProjectTable();
|
||||
final var fileTable = new FileTable(1);
|
||||
final var projectId = projectTable.register(ProjectDescriptor.builder()
|
||||
.rootPath(projectRoot)
|
||||
.name("app")
|
||||
.version("1.0.0")
|
||||
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
|
||||
.build());
|
||||
register(projectId, projectRoot, sourceA, fileTable);
|
||||
register(projectId, projectRoot, sourceA.getParent().resolve("mod.barrel"), fileTable);
|
||||
final FileId callerId = register(projectId, projectRoot, sourceB, fileTable);
|
||||
register(projectId, projectRoot, sourceB.getParent().resolve("mod.barrel"), fileTable);
|
||||
|
||||
final var phaseContext = new FrontendPhaseContext(
|
||||
projectTable,
|
||||
fileTable,
|
||||
new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
|
||||
AppMode.Game);
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var editorialContext = service.prepareEditorialContext(phaseContext, callerId).orElseThrow();
|
||||
final var links = service.documentLinks(
|
||||
new FrontendDocumentRequest(projectRoot, sourceB, caller),
|
||||
editorialContext);
|
||||
|
||||
assertEquals(1, links.size());
|
||||
assertEquals(sourceA.toAbsolutePath().normalize(), links.getFirst().path());
|
||||
final byte[] bytes = caller.getBytes(StandardCharsets.UTF_8);
|
||||
assertEquals("@app:a", new String(
|
||||
bytes,
|
||||
links.getFirst().startOffset(),
|
||||
links.getFirst().endOffset() - links.getFirst().startOffset(),
|
||||
StandardCharsets.UTF_8));
|
||||
assertTrue(service.documentLinksSupported());
|
||||
}
|
||||
|
||||
private static FileId register(
|
||||
final p.studio.compiler.source.identifiers.ProjectId projectId,
|
||||
final Path projectRoot,
|
||||
final Path file,
|
||||
final FileTable fileTable) throws IOException {
|
||||
final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class);
|
||||
return fileTable.register(new SourceHandle(
|
||||
projectId,
|
||||
projectRoot.relativize(file),
|
||||
file,
|
||||
attributes.size(),
|
||||
attributes.lastModifiedTime().toMillis(),
|
||||
SourceProviderFactory.filesystem()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
package p.studio.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.AppMode;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.BuildStack;
|
||||
import p.studio.compiler.models.ProjectDescriptor;
|
||||
import p.studio.compiler.models.SourceHandle;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendSymbolKind;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.compiler.source.tables.FileTable;
|
||||
import p.studio.compiler.source.tables.ProjectTable;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PBSFrontendModuleCompletionTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void moduleRefCompletionListsKnownModulesAndMemberCompletionStays() throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path sourceRoot = projectRoot.resolve("src");
|
||||
final Path moduleA = sourceRoot.resolve("a").resolve("source.pbs");
|
||||
final Path main = sourceRoot.resolve("main.pbs");
|
||||
Files.createDirectories(moduleA.getParent());
|
||||
Files.writeString(moduleA, """
|
||||
fn target() -> int {
|
||||
return 1;
|
||||
}
|
||||
""");
|
||||
Files.writeString(moduleA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
|
||||
final String caller = """
|
||||
import { Log } from @sdk:log;
|
||||
|
||||
declare struct Vec() {
|
||||
fn blend(dx: int, dy: int) -> int { return dx; }
|
||||
}
|
||||
|
||||
fn frame(vec: Vec) -> void {
|
||||
vec.blend(1, 2);
|
||||
}
|
||||
""";
|
||||
Files.writeString(main, caller);
|
||||
Files.writeString(sourceRoot.resolve("mod.barrel"), """
|
||||
pub struct Vec();
|
||||
pub fn frame(vec: Vec) -> void;
|
||||
""");
|
||||
|
||||
final var projectTable = new ProjectTable();
|
||||
final var fileTable = new FileTable(1);
|
||||
final ProjectId projectId = projectTable.register(ProjectDescriptor.builder()
|
||||
.rootPath(projectRoot)
|
||||
.name("app")
|
||||
.version("1.0.0")
|
||||
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
|
||||
.build());
|
||||
register(projectId, projectRoot, moduleA, fileTable);
|
||||
register(projectId, projectRoot, moduleA.getParent().resolve("mod.barrel"), fileTable);
|
||||
final FileId mainId = register(projectId, projectRoot, main, fileTable);
|
||||
register(projectId, projectRoot, sourceRoot.resolve("mod.barrel"), fileTable);
|
||||
|
||||
final var phaseContext = new FrontendPhaseContext(
|
||||
projectTable,
|
||||
fileTable,
|
||||
new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
|
||||
AppMode.Game);
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var editorialContext = service.prepareEditorialContext(phaseContext, mainId).orElseThrow();
|
||||
final var request = new FrontendDocumentRequest(projectRoot, main, caller);
|
||||
|
||||
final var moduleCompletion = service.completion(request, editorialContext, byteOffset(caller, "@sdk:log"));
|
||||
assertTrue(labels(moduleCompletion).contains("@sdk:log"));
|
||||
assertTrue(labels(moduleCompletion).contains("@app:a"));
|
||||
assertTrue(moduleCompletion.stream().allMatch(candidate -> candidate.kind() == FrontendSymbolKind.MODULE));
|
||||
|
||||
final int afterDot = caller.indexOf("vec.") + "vec.".length();
|
||||
final var memberCompletion = service.completion(
|
||||
request,
|
||||
editorialContext,
|
||||
caller.substring(0, afterDot).getBytes(StandardCharsets.UTF_8).length);
|
||||
assertTrue(labels(memberCompletion).contains("blend"), labels(memberCompletion).toString());
|
||||
assertFalse(labels(memberCompletion).contains("@sdk:log"));
|
||||
}
|
||||
|
||||
private static List<String> labels(final List<FrontendCompletionCandidate> candidates) {
|
||||
return candidates.stream().map(FrontendCompletionCandidate::label).toList();
|
||||
}
|
||||
|
||||
private static int byteOffset(final String source, final String marker) {
|
||||
final int index = source.indexOf(marker);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException(marker);
|
||||
}
|
||||
return source.substring(0, index + 1).getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
private static FileId register(
|
||||
final ProjectId projectId,
|
||||
final Path projectRoot,
|
||||
final Path file,
|
||||
final FileTable fileTable) throws IOException {
|
||||
final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class);
|
||||
return fileTable.register(new SourceHandle(
|
||||
projectId,
|
||||
projectRoot.relativize(file),
|
||||
file,
|
||||
attributes.size(),
|
||||
attributes.lastModifiedTime().toMillis(),
|
||||
SourceProviderFactory.filesystem()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.parser.PbsParser;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsCallHierarchyTest {
|
||||
private static final String SOURCE = """
|
||||
fn helper() -> int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
declare struct Box() {
|
||||
fn blend(dx: int) -> int {
|
||||
return dx;
|
||||
}
|
||||
}
|
||||
|
||||
fn main(box: Box) -> int {
|
||||
box.blend(1);
|
||||
return helper();
|
||||
}
|
||||
""";
|
||||
|
||||
@Test
|
||||
void functionAndMethodCallsComeFromTheAst() {
|
||||
final PbsAst.File ast = parse(SOURCE);
|
||||
final int mainOffset = byteOffset(SOURCE, "fn main");
|
||||
final var prepared = PbsCallHierarchy.prepare(ast, mainOffset);
|
||||
assertEquals(1, prepared.size());
|
||||
assertEquals("main", prepared.getFirst().name());
|
||||
assertEquals(false, prepared.getFirst().method());
|
||||
|
||||
final var outgoing = PbsCallHierarchy.outgoingCalls(ast, prepared.getFirst());
|
||||
assertTrue(outgoing.stream().anyMatch(call -> call.callee().name().equals("helper") && !call.callee().method()));
|
||||
assertTrue(outgoing.stream().anyMatch(call -> call.callee().name().equals("blend") && call.callee().method()));
|
||||
|
||||
final int helperOffset = byteOffset(SOURCE, "fn helper");
|
||||
final var helper = PbsCallHierarchy.prepare(ast, helperOffset).getFirst();
|
||||
final var incoming = PbsCallHierarchy.incomingCalls(ast, helper);
|
||||
assertEquals(1, incoming.size());
|
||||
assertEquals("main", incoming.getFirst().caller().name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aStructNameIsNotACallable() {
|
||||
final PbsAst.File ast = parse(SOURCE);
|
||||
assertTrue(PbsCallHierarchy.prepare(ast, byteOffset(SOURCE, "struct Box")).isEmpty());
|
||||
}
|
||||
|
||||
private static PbsAst.File parse(final String source) {
|
||||
return PbsParser.parse(
|
||||
PbsLexer.lex(source, FileId.none(), DiagnosticSink.empty()),
|
||||
FileId.none(),
|
||||
DiagnosticSink.empty());
|
||||
}
|
||||
|
||||
private static int byteOffset(final String source, final String marker) {
|
||||
final int index = source.indexOf(marker);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException(marker);
|
||||
}
|
||||
return source.substring(0, index + 1).getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsDocumentFormatterTest {
|
||||
|
||||
@Test
|
||||
void indentsOnlyWhereBraceOrParenNestingChanges() {
|
||||
final String source = """
|
||||
fn length(value: int) -> int {
|
||||
return value;
|
||||
}
|
||||
fn other() -> int {
|
||||
return 1;
|
||||
}
|
||||
""";
|
||||
|
||||
final String formatted = PbsDocumentFormatter.format(source);
|
||||
|
||||
assertEquals("""
|
||||
fn length(value: int) -> int {
|
||||
return value;
|
||||
}
|
||||
fn other() -> int {
|
||||
return 1;
|
||||
}
|
||||
""", formatted);
|
||||
assertTrue(formatted.indexOf("fn length") < formatted.indexOf("fn other"));
|
||||
assertFalse(formatted.contains("\nreturn value;\nreturn 1;"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void leavesDocTextBlockInteriorAndTrailingCommentUntouched() {
|
||||
final String source = """
|
||||
[Doc(markdown = \"\"\"
|
||||
hello
|
||||
\"\"\")]
|
||||
fn length() -> int { // keep
|
||||
return 1;
|
||||
}
|
||||
""";
|
||||
|
||||
final String formatted = PbsDocumentFormatter.format(source);
|
||||
|
||||
assertTrue(formatted.contains(" hello\n"));
|
||||
assertTrue(formatted.contains("fn length() -> int { // keep"));
|
||||
assertTrue(formatted.contains("\n return 1;\n"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.PBSFrontendLanguageService;
|
||||
import p.studio.compiler.PBSSemanticTokenProvider;
|
||||
import p.studio.compiler.messages.FESurfaceContext;
|
||||
import p.studio.compiler.models.FrontendSemanticToken;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.parser.PbsParser;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendEditorialContext;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsSemanticTokenOverlayTest {
|
||||
|
||||
@Test
|
||||
void resolvedFunctionUseReplacesTheIdentifierBaseline() {
|
||||
final String source = """
|
||||
fn helper() -> int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn main() -> int {
|
||||
return helper;
|
||||
}
|
||||
""";
|
||||
final PbsAst.File ast = PbsParser.parse(
|
||||
PbsLexer.lex(source, FileId.none(), DiagnosticSink.empty()),
|
||||
FileId.none(),
|
||||
DiagnosticSink.empty());
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var tokens = service.semanticTokens(
|
||||
new FrontendDocumentRequest(Path.of("."), Path.of("demo.pbs"), source),
|
||||
new FrontendEditorialContext(ast, ReadOnlyList.empty(), FESurfaceContext.empty()));
|
||||
|
||||
assertEquals(PbsSemanticKind.FUNCTION.semanticKey(), keyAt(source, tokens, "return helper"));
|
||||
assertTrue(tokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.KEYWORD.semanticKey())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void localBindingAndBrokenTextKeepTheBaselineToken() {
|
||||
final String source = """
|
||||
fn main() -> int {
|
||||
let value: int = 1;
|
||||
return value;
|
||||
}
|
||||
""";
|
||||
final PbsAst.File ast = PbsParser.parse(
|
||||
PbsLexer.lex(source, FileId.none(), DiagnosticSink.empty()),
|
||||
FileId.none(),
|
||||
DiagnosticSink.empty());
|
||||
final var service = new PBSFrontendLanguageService();
|
||||
final var request = new FrontendDocumentRequest(Path.of("."), Path.of("demo.pbs"), source);
|
||||
final var overlaid = service.semanticTokens(
|
||||
request,
|
||||
new FrontendEditorialContext(ast, ReadOnlyList.empty(), FESurfaceContext.empty()));
|
||||
|
||||
assertEquals(PbsSemanticKind.IDENTIFIER.semanticKey(), keyAt(source, overlaid, "return value"));
|
||||
|
||||
final String broken = "fn main( {";
|
||||
final List<FrontendSemanticToken> baseline = new PBSSemanticTokenProvider().tokenize(broken);
|
||||
final List<FrontendSemanticToken> withoutContext = service.semanticTokens(
|
||||
new FrontendDocumentRequest(Path.of("."), Path.of("broken.pbs"), broken));
|
||||
assertEquals(baseline, withoutContext);
|
||||
assertTrue(withoutContext.stream().anyMatch(token ->
|
||||
token.semanticKey().equals(PbsSemanticKind.KEYWORD.semanticKey())));
|
||||
}
|
||||
|
||||
private static String keyAt(
|
||||
final String source,
|
||||
final List<FrontendSemanticToken> tokens,
|
||||
final String marker) {
|
||||
final int nameOffset = source.indexOf(marker) + marker.lastIndexOf(' ') + 1;
|
||||
final int byteOffset = source.substring(0, nameOffset).getBytes(StandardCharsets.UTF_8).length;
|
||||
return tokens.stream()
|
||||
.filter(token -> token.startOffset() == byteOffset)
|
||||
.map(FrontendSemanticToken::semanticKey)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package p.studio.compiler.pbs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.services.FrontendFoldingRange;
|
||||
import p.studio.compiler.services.FrontendSelectionRange;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsStructureRangesTest {
|
||||
private static final String SOURCE = """
|
||||
[Doc(markdown = \"\"\"
|
||||
hello
|
||||
\"\"\")]
|
||||
fn length(value: int) -> int {
|
||||
return value;
|
||||
}
|
||||
""";
|
||||
|
||||
@Test
|
||||
void foldingUsesDeclarationBodyParameterListAndDocText() {
|
||||
final var ranges = PbsStructureRanges.foldingRanges(SOURCE);
|
||||
final var slices = ranges.stream().map(range -> slice(SOURCE, range)).toList();
|
||||
|
||||
assertTrue(slices.stream().anyMatch(slice -> slice.contains("hello")), slices.toString());
|
||||
assertTrue(slices.stream().anyMatch(slice -> slice.equals("(value: int)")), slices.toString());
|
||||
assertTrue(slices.stream().anyMatch(slice -> slice.startsWith("{")), slices.toString());
|
||||
assertTrue(slices.stream().anyMatch(slice -> slice.contains("fn length")), slices.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectionGrowsFromIdentifierToBlockToDeclaration() {
|
||||
final int marker = SOURCE.indexOf("return value");
|
||||
assertTrue(marker >= 0, SOURCE);
|
||||
final int offset = marker + "return ".length();
|
||||
final var selections = PbsStructureRanges.selectionRanges(SOURCE, List.of(offset));
|
||||
assertEquals(1, selections.size(), "offset " + offset);
|
||||
final FrontendSelectionRange selection = selections.getFirst();
|
||||
|
||||
assertEquals("value", slice(SOURCE, selection.startOffset(), selection.endOffset()));
|
||||
assertNotNull(selection.parent());
|
||||
assertTrue(slice(SOURCE, selection.parent().startOffset(), selection.parent().endOffset()).startsWith("{"));
|
||||
assertNotNull(selection.parent().parent());
|
||||
assertTrue(slice(SOURCE, selection.parent().parent().startOffset(), selection.parent().parent().endOffset())
|
||||
.contains("fn length"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unmatchedBraceFoldsThroughTheEndOfTheText() {
|
||||
final String broken = """
|
||||
fn broken() -> void {
|
||||
let x: int = 1;
|
||||
""";
|
||||
final int textEnd = broken.getBytes(StandardCharsets.UTF_8).length;
|
||||
final var ranges = PbsStructureRanges.foldingRanges(broken);
|
||||
|
||||
assertTrue(ranges.stream().anyMatch(range ->
|
||||
range.endOffset() == textEnd && slice(broken, range).startsWith("{")));
|
||||
}
|
||||
|
||||
private static String slice(final String text, final FrontendFoldingRange range) {
|
||||
return slice(text, range.startOffset(), range.endOffset());
|
||||
}
|
||||
|
||||
private static String slice(final String text, final int start, final int end) {
|
||||
final byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
||||
return new String(bytes, start, end - start, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendCallHierarchyItem(
|
||||
String name,
|
||||
boolean method,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendCallHierarchyItem {
|
||||
name = Objects.requireNonNull(name, "name").trim();
|
||||
if (name.isEmpty()) {
|
||||
throw new IllegalArgumentException("name must not be blank");
|
||||
}
|
||||
if (startOffset < 0 || endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("call hierarchy range is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendCallSite(
|
||||
FrontendCallHierarchyItem item,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendCallSite {
|
||||
item = Objects.requireNonNull(item, "item");
|
||||
if (startOffset < 0 || endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("call site range is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendDocumentLink(
|
||||
Path path,
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendDocumentLink {
|
||||
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
public record FrontendFoldingRange(
|
||||
int startOffset,
|
||||
int endOffset) {
|
||||
|
||||
public FrontendFoldingRange {
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,12 @@ public interface FrontendLanguageService {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendSemanticToken> semanticTokens(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return semanticTokens(request);
|
||||
}
|
||||
|
||||
default List<FrontendCompletionCandidate> completion(
|
||||
final FrontendDocumentRequest request,
|
||||
final int offset) {
|
||||
@ -139,6 +145,84 @@ public interface FrontendLanguageService {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
public record FrontendSelectionRange(
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
FrontendSelectionRange parent) {
|
||||
|
||||
public FrontendSelectionRange {
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,5 +17,6 @@ public enum FrontendSymbolKind {
|
||||
ENUM,
|
||||
ERROR,
|
||||
GLOBAL,
|
||||
CONST
|
||||
CONST,
|
||||
MODULE
|
||||
}
|
||||
|
||||
@ -30,6 +30,19 @@ class FrontendLanguageServiceTest {
|
||||
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());
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineCallHierarchyItem(
|
||||
String name,
|
||||
boolean method,
|
||||
String uri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineCallHierarchyItem {
|
||||
name = Objects.requireNonNull(name, "name").trim();
|
||||
uri = Objects.requireNonNull(uri, "uri").trim();
|
||||
if (name.isEmpty() || uri.isEmpty()) {
|
||||
throw new IllegalArgumentException("call hierarchy item identity must not be blank");
|
||||
}
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("call hierarchy range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineCallSite(
|
||||
BaselineCallHierarchyItem item,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineCallSite {
|
||||
item = Objects.requireNonNull(item, "item");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("call site coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,5 +16,6 @@ public enum BaselineCompletionItemKind {
|
||||
ENUM,
|
||||
ERROR,
|
||||
GLOBAL,
|
||||
CONST
|
||||
CONST,
|
||||
MODULE
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineDocumentLink(
|
||||
String targetUri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineDocumentLink {
|
||||
targetUri = requireText(targetUri, "targetUri");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("document link range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
final String value,
|
||||
final String field) {
|
||||
final String candidate = Objects.requireNonNull(value, field).trim();
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineDocumentLinks(List<BaselineDocumentLink> links) {
|
||||
public BaselineDocumentLinks {
|
||||
links = links == null ? List.of() : List.copyOf(links);
|
||||
}
|
||||
|
||||
public static BaselineDocumentLinks empty() {
|
||||
return new BaselineDocumentLinks(List.of());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
public record BaselineFoldingRange(
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineFoldingRange {
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("folding range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineFoldingRanges(List<BaselineFoldingRange> ranges) {
|
||||
public BaselineFoldingRanges {
|
||||
ranges = ranges == null ? List.of() : List.copyOf(ranges);
|
||||
}
|
||||
|
||||
public static BaselineFoldingRanges empty() {
|
||||
return new BaselineFoldingRanges(List.of());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineFormatting(List<BaselineTextEdit> edits) {
|
||||
public BaselineFormatting {
|
||||
edits = edits == null ? List.of() : List.copyOf(edits);
|
||||
}
|
||||
|
||||
public static BaselineFormatting empty() {
|
||||
return new BaselineFormatting(List.of());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
public record BaselineSelectionRange(
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter,
|
||||
BaselineSelectionRange parent) {
|
||||
|
||||
public BaselineSelectionRange {
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("selection range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineSelectionRanges(List<BaselineSelectionRange> ranges) {
|
||||
public BaselineSelectionRanges {
|
||||
ranges = ranges == null ? List.of() : List.copyOf(ranges);
|
||||
}
|
||||
|
||||
public static BaselineSelectionRanges empty() {
|
||||
return new BaselineSelectionRanges(List.of());
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,11 @@ public record BaselineServerDescription(
|
||||
boolean workspaceSymbolsSupported,
|
||||
boolean renameSupported,
|
||||
boolean codeActionsSupported,
|
||||
boolean documentLinksSupported,
|
||||
boolean foldingRangesSupported,
|
||||
boolean selectionRangesSupported,
|
||||
boolean formattingSupported,
|
||||
boolean callHierarchySupported,
|
||||
List<String> semanticTokenTypes,
|
||||
List<BaselineSemanticHostProjection> semanticHostProjections,
|
||||
List<BaselineVisualTheme> visualThemes,
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineTextEdit(
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter,
|
||||
String newText) {
|
||||
|
||||
public BaselineTextEdit {
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("text edit coordinates must not be negative");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,13 @@ 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;
|
||||
@ -60,6 +67,51 @@ public interface LanguageServiceBridge {
|
||||
|
||||
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,
|
||||
|
||||
@ -2,10 +2,16 @@ package p.studio.lsp.services.compiler;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import p.studio.compiler.FrontendRegistryService;
|
||||
import p.studio.compiler.exceptions.BuildException;
|
||||
import p.studio.compiler.messages.*;
|
||||
import p.studio.compiler.models.*;
|
||||
import p.studio.compiler.services.FrontendCallHierarchyItem;
|
||||
import p.studio.compiler.services.FrontendCallSite;
|
||||
import p.studio.compiler.services.FrontendCodeAction;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendDocumentLink;
|
||||
import p.studio.compiler.services.FrontendFoldingRange;
|
||||
import p.studio.compiler.services.FrontendSelectionRange;
|
||||
import p.studio.compiler.services.FrontendLanguageService;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
@ -57,6 +63,21 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
final boolean codeActionsSupported = languageService
|
||||
.map(FrontendLanguageService::codeActionsSupported)
|
||||
.orElse(false);
|
||||
final boolean documentLinksSupported = languageService
|
||||
.map(FrontendLanguageService::documentLinksSupported)
|
||||
.orElse(false);
|
||||
final boolean foldingRangesSupported = languageService
|
||||
.map(FrontendLanguageService::foldingRangesSupported)
|
||||
.orElse(false);
|
||||
final boolean selectionRangesSupported = languageService
|
||||
.map(FrontendLanguageService::selectionRangesSupported)
|
||||
.orElse(false);
|
||||
final boolean formattingSupported = languageService
|
||||
.map(FrontendLanguageService::formattingSupported)
|
||||
.orElse(false);
|
||||
final boolean callHierarchySupported = languageService
|
||||
.map(FrontendLanguageService::callHierarchySupported)
|
||||
.orElse(false);
|
||||
return new BaselineServerDescription(
|
||||
"Prometeu Studio LSP",
|
||||
"0.1.0",
|
||||
@ -70,6 +91,11 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
codeActionsSupported,
|
||||
documentLinksSupported,
|
||||
foldingRangesSupported,
|
||||
selectionRangesSupported,
|
||||
formattingSupported,
|
||||
callHierarchySupported,
|
||||
presentation.semanticKeys(),
|
||||
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
|
||||
presentation.themes().stream().map(this::mapVisualTheme).toList(),
|
||||
@ -85,7 +111,11 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
final Path documentPath = normalizeDocumentPath(documentUri);
|
||||
final String effectiveText = StringUtils.isBlank(text) ? "" : text;
|
||||
final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText));
|
||||
return new BaselineDocumentAnalysis(mapDiagnostics(snapshot, documentPath, effectiveText));
|
||||
return new BaselineDocumentAnalysis(mapDiagnostics(
|
||||
snapshot,
|
||||
documentPath,
|
||||
effectiveText,
|
||||
context.languageId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -361,6 +391,295 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
.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,
|
||||
@ -440,7 +759,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
documentPath,
|
||||
effectiveText);
|
||||
final List<FrontendSemanticToken> tokens = provider.languageService()
|
||||
.map(languageService -> languageService.semanticTokens(request))
|
||||
.map(languageService -> semanticTokens(languageService, context, documentUri, documentPath, request))
|
||||
.orElseGet(List::of);
|
||||
final var positionMapper = new DocumentPositionMapper(effectiveText);
|
||||
final var semanticTokens = new ArrayList<BaselineSemanticToken>();
|
||||
@ -459,6 +778,26 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
return new BaselineSemanticTokens(presentation.semanticKeys(), semanticTokens);
|
||||
}
|
||||
|
||||
private List<FrontendSemanticToken> semanticTokens(
|
||||
final FrontendLanguageService languageService,
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final Path documentPath,
|
||||
final FrontendDocumentRequest request) {
|
||||
try {
|
||||
return editorialDocument(context, documentUri, request.documentText())
|
||||
.map(document -> languageService.semanticTokens(
|
||||
new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
documentPath,
|
||||
document.text()),
|
||||
document.editorialContext()))
|
||||
.orElseGet(() -> languageService.semanticTokens(request));
|
||||
} catch (final BuildException ignored) {
|
||||
return languageService.semanticTokens(request);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(
|
||||
final LspProjectContext context,
|
||||
@ -541,7 +880,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
private List<BaselineDocumentIssue> mapDiagnostics(
|
||||
final AnalysisSnapshot snapshot,
|
||||
final Path documentPath,
|
||||
final String text) {
|
||||
final String text,
|
||||
final String languageId) {
|
||||
final ArrayList<BaselineDocumentIssue> issues = new ArrayList<>();
|
||||
final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text);
|
||||
for (final BuildingIssue diagnostic : snapshot.diagnostics()) {
|
||||
@ -556,14 +896,15 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) {
|
||||
continue;
|
||||
}
|
||||
issues.add(toBaselineIssue(diagnostic, positionMapper));
|
||||
issues.add(toBaselineIssue(diagnostic, positionMapper, languageId));
|
||||
}
|
||||
return List.copyOf(issues);
|
||||
}
|
||||
|
||||
private BaselineDocumentIssue toBaselineIssue(
|
||||
final BuildingIssue diagnostic,
|
||||
final DocumentPositionMapper positionMapper) {
|
||||
final DocumentPositionMapper positionMapper,
|
||||
final String languageId) {
|
||||
final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart();
|
||||
final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd());
|
||||
final DocumentPosition start = positionMapper.positionOf(startOffset);
|
||||
@ -575,7 +916,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
end.line(),
|
||||
end.character(),
|
||||
diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING,
|
||||
code.isBlank() ? "Prometeu Studio" : code,
|
||||
languageId,
|
||||
diagnostic.getMessage(),
|
||||
code);
|
||||
}
|
||||
@ -673,6 +1014,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
case ERROR -> BaselineCompletionItemKind.ERROR;
|
||||
case GLOBAL -> BaselineCompletionItemKind.GLOBAL;
|
||||
case CONST -> BaselineCompletionItemKind.CONST;
|
||||
case MODULE -> BaselineCompletionItemKind.MODULE;
|
||||
};
|
||||
}
|
||||
|
||||
@ -824,6 +1166,28 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineDocumentLink> mapDocumentLinks(
|
||||
final List<FrontendDocumentLink> links,
|
||||
final String currentDocumentText) {
|
||||
if (links == null || links.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(
|
||||
currentDocumentText == null ? "" : currentDocumentText);
|
||||
final ArrayList<BaselineDocumentLink> mapped = new ArrayList<>();
|
||||
for (final FrontendDocumentLink link : links) {
|
||||
final DocumentPosition start = mapper.positionOf(link.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(link.endOffset());
|
||||
mapped.add(new BaselineDocumentLink(
|
||||
link.path().toUri().toString(),
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineDefinitionLocation> mapDefinitionLocations(
|
||||
final List<FrontendDefinitionLocation> locations,
|
||||
final Path currentDocumentPath,
|
||||
|
||||
@ -8,7 +8,10 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
import p.studio.lsp.messages.BaselineCallHierarchyItem;
|
||||
import p.studio.lsp.messages.BaselineCallSite;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.DocumentPosition;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
||||
@ -168,6 +171,92 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
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())) {
|
||||
|
||||
@ -53,6 +53,23 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
codeActionOptions.setCodeActionKinds(List.of(CodeActionKind.QuickFix));
|
||||
capabilities.setCodeActionProvider(codeActionOptions);
|
||||
}
|
||||
if (description.documentLinksSupported()) {
|
||||
final DocumentLinkOptions documentLinkOptions = new DocumentLinkOptions();
|
||||
documentLinkOptions.setResolveProvider(false);
|
||||
capabilities.setDocumentLinkProvider(documentLinkOptions);
|
||||
}
|
||||
if (description.foldingRangesSupported()) {
|
||||
capabilities.setFoldingRangeProvider(true);
|
||||
}
|
||||
if (description.selectionRangesSupported()) {
|
||||
capabilities.setSelectionRangeProvider(true);
|
||||
}
|
||||
if (description.formattingSupported()) {
|
||||
capabilities.setDocumentFormattingProvider(true);
|
||||
}
|
||||
if (description.callHierarchySupported()) {
|
||||
capabilities.setCallHierarchyProvider(true);
|
||||
}
|
||||
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
|
||||
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
|
||||
semanticTokens.setFull(true);
|
||||
@ -159,6 +176,127 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
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,
|
||||
@ -391,6 +529,7 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
case ENUM -> CompletionItemKind.Enum;
|
||||
case ERROR -> CompletionItemKind.EnumMember;
|
||||
case GLOBAL, CONST -> CompletionItemKind.Constant;
|
||||
case MODULE -> CompletionItemKind.Module;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,14 @@ import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
||||
import org.eclipse.lsp4j.SignatureHelp;
|
||||
import org.eclipse.lsp4j.SemanticTokens;
|
||||
import org.eclipse.lsp4j.Diagnostic;
|
||||
import org.eclipse.lsp4j.DocumentLink;
|
||||
import org.eclipse.lsp4j.DocumentSymbol;
|
||||
import org.eclipse.lsp4j.CallHierarchyIncomingCall;
|
||||
import org.eclipse.lsp4j.CallHierarchyItem;
|
||||
import org.eclipse.lsp4j.CallHierarchyOutgoingCall;
|
||||
import org.eclipse.lsp4j.FoldingRange;
|
||||
import org.eclipse.lsp4j.TextEdit;
|
||||
import org.eclipse.lsp4j.SelectionRange;
|
||||
import org.eclipse.lsp4j.Location;
|
||||
import org.eclipse.lsp4j.Range;
|
||||
import org.eclipse.lsp4j.SymbolInformation;
|
||||
@ -19,6 +26,12 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
import org.eclipse.lsp4j.PrepareRenameDefaultBehavior;
|
||||
import org.eclipse.lsp4j.PrepareRenameResult;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
||||
import p.studio.lsp.messages.BaselineFoldingRanges;
|
||||
import p.studio.lsp.messages.BaselineCallHierarchyItem;
|
||||
import p.studio.lsp.messages.BaselineCallSite;
|
||||
import p.studio.lsp.messages.BaselineFormatting;
|
||||
import p.studio.lsp.messages.BaselineSelectionRanges;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbols;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
@ -59,6 +72,34 @@ public interface ProtocolMessageMapper {
|
||||
|
||||
List<Either<Command, CodeAction>> mapCodeActions(BaselineCodeActions actions, List<Diagnostic> contextDiagnostics);
|
||||
|
||||
default List<DocumentLink> mapDocumentLinks(BaselineDocumentLinks links) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FoldingRange> mapFoldingRanges(BaselineFoldingRanges ranges) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<TextEdit> mapFormatting(BaselineFormatting formatting) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<CallHierarchyItem> mapCallHierarchyItems(List<BaselineCallHierarchyItem> items) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<CallHierarchyIncomingCall> mapIncomingCalls(List<BaselineCallSite> calls) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<CallHierarchyOutgoingCall> mapOutgoingCalls(List<BaselineCallSite> calls) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<SelectionRange> mapSelectionRanges(BaselineSelectionRanges ranges) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
CompletionList mapCompletion(BaselineCompletion completion);
|
||||
|
||||
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.lsp.services.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.FrontendSpec;
|
||||
@ -10,10 +11,12 @@ import p.studio.compiler.services.FrontendProvider;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||
import p.studio.lsp.messages.DocumentPosition;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlySet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@ -74,6 +77,11 @@ class CompilerLanguageServiceBridgeTest {
|
||||
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
|
||||
@ -100,6 +108,9 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.severity() == BaselineIssueSeverity.ERROR));
|
||||
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.startLine() == 1));
|
||||
assertTrue(analysis.issues().stream().noneMatch(issue -> issue.message().contains("Compiler-backed baseline")));
|
||||
assertTrue(analysis.issues().stream().allMatch(issue -> issue.source().equals("pbs")));
|
||||
assertTrue(analysis.issues().stream().noneMatch(issue -> issue.source().equals(issue.code())));
|
||||
assertTrue(analysis.issues().stream().anyMatch(issue -> !issue.code().isBlank()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -161,6 +172,112 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-builtin-type")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void semanticTokenOverlayReplacesAResolvedUseAndKeepsBrokenText(@TempDir final Path tempDir) throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path source = projectRoot.resolve("src").resolve("main.pbs");
|
||||
Files.createDirectories(source.getParent());
|
||||
Files.writeString(projectRoot.resolve("prometeu.json"), """
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"language": "pbs",
|
||||
"stdlib": "1",
|
||||
"target": "Game",
|
||||
"dependencies": []
|
||||
}
|
||||
""");
|
||||
final String text = """
|
||||
fn helper() -> int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn main() -> int {
|
||||
return helper;
|
||||
}
|
||||
""";
|
||||
Files.writeString(source, text);
|
||||
Files.writeString(source.getParent().resolve("mod.barrel"), """
|
||||
pub fn helper() -> int;
|
||||
pub fn main() -> int;
|
||||
""");
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final Path realSource = source.toRealPath();
|
||||
final var tokens = bridge.semanticTokens(
|
||||
new LspProjectContext("app", "pbs", projectRoot.toRealPath()),
|
||||
realSource.toUri().toString(),
|
||||
text);
|
||||
final int helperUse = text.indexOf("return helper") + "return ".length();
|
||||
assertTrue(tokens.tokens().stream().anyMatch(token ->
|
||||
token.semanticKey().equals("pbs-function")
|
||||
&& token.line() == new DocumentPositionMapper(text).positionOf(helperUse).line()
|
||||
&& token.startCharacter() == new DocumentPositionMapper(text).positionOf(helperUse).character()));
|
||||
|
||||
final String broken = "fn main( {";
|
||||
final var brokenTokens = bridge.semanticTokens(
|
||||
new LspProjectContext("app", "pbs", projectRoot.toRealPath()),
|
||||
realSource.toUri().toString(),
|
||||
broken);
|
||||
assertTrue(brokenTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-keyword")));
|
||||
assertTrue(brokenTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-punctuation")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callHierarchyUsesAstCallsAndRefusesAStruct(@TempDir final Path tempDir) throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path source = projectRoot.resolve("src").resolve("main.pbs");
|
||||
Files.createDirectories(source.getParent());
|
||||
Files.writeString(projectRoot.resolve("prometeu.json"), """
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"language": "pbs",
|
||||
"stdlib": "1",
|
||||
"target": "Game",
|
||||
"dependencies": []
|
||||
}
|
||||
""");
|
||||
final String text = """
|
||||
fn helper() -> int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn main() -> int {
|
||||
return helper();
|
||||
}
|
||||
""";
|
||||
Files.writeString(source, text);
|
||||
Files.writeString(source.getParent().resolve("mod.barrel"), """
|
||||
pub fn helper() -> int;
|
||||
pub fn main() -> int;
|
||||
""");
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final LspProjectContext context = new LspProjectContext("app", "pbs", projectRoot.toRealPath());
|
||||
final String uri = source.toRealPath().toUri().toString();
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(text);
|
||||
final var mainPosition = mapper.positionOf(text.indexOf("fn main") + 1);
|
||||
final var prepared = bridge.prepareCallHierarchy(
|
||||
context,
|
||||
uri,
|
||||
text,
|
||||
mainPosition.line(),
|
||||
mainPosition.character());
|
||||
assertEquals(1, prepared.size());
|
||||
assertEquals("main", prepared.getFirst().name());
|
||||
final var outgoing = bridge.outgoingCalls(context, uri, text, prepared.getFirst());
|
||||
assertTrue(outgoing.stream().anyMatch(call -> call.item().name().equals("helper")));
|
||||
final var structPosition = mapper.positionOf(text.indexOf("fn helper") + 1);
|
||||
final var helper = bridge.prepareCallHierarchy(
|
||||
context,
|
||||
uri,
|
||||
text,
|
||||
structPosition.line(),
|
||||
structPosition.character());
|
||||
assertEquals("helper", helper.getFirst().name());
|
||||
assertTrue(bridge.incomingCalls(context, uri, text, helper.getFirst()).stream()
|
||||
.anyMatch(call -> call.item().name().equals("main")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
@ -765,6 +882,11 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(description.workspaceSymbolsSupported());
|
||||
assertTrue(description.renameSupported());
|
||||
assertTrue(description.codeActionsSupported());
|
||||
assertTrue(description.documentLinksSupported());
|
||||
assertTrue(description.foldingRangesSupported());
|
||||
assertTrue(description.selectionRangesSupported());
|
||||
assertTrue(description.formattingSupported());
|
||||
assertTrue(description.callHierarchySupported());
|
||||
assertEquals(1, description.semanticHostProjections().size());
|
||||
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
|
||||
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
|
||||
@ -778,6 +900,77 @@ class CompilerLanguageServiceBridgeTest {
|
||||
&& tokenStyle.foreground().equals("#7fb8ff")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksSkipVirtualStdlibImports() throws IOException {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
.resolve("test-projects")
|
||||
.resolve("main")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
|
||||
final String text = Files.readString(documentPath);
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
|
||||
final var links = bridge.documentLinks(
|
||||
new LspProjectContext("main", "pbs", projectRoot),
|
||||
documentPath.toUri().toString(),
|
||||
text);
|
||||
|
||||
assertTrue(links.links().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksPointAtThePhysicalModuleFile(@TempDir final Path tempDir) throws IOException {
|
||||
final Path projectRoot = tempDir.resolve("app");
|
||||
final Path sourceA = projectRoot.resolve("src").resolve("a").resolve("source.pbs");
|
||||
final Path sourceB = projectRoot.resolve("src").resolve("b").resolve("source.pbs");
|
||||
Files.createDirectories(sourceA.getParent());
|
||||
Files.createDirectories(sourceB.getParent());
|
||||
Files.writeString(projectRoot.resolve("prometeu.json"), """
|
||||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"language": "pbs",
|
||||
"stdlib": "1",
|
||||
"target": "Game",
|
||||
"dependencies": []
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA, """
|
||||
fn target() -> int {
|
||||
return 1;
|
||||
}
|
||||
""");
|
||||
Files.writeString(sourceA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
|
||||
final String caller = """
|
||||
import { target } from @app:a;
|
||||
import { Log } from @sdk:log;
|
||||
import { Missing } from @app:missing;
|
||||
|
||||
fn caller() -> int {
|
||||
return target();
|
||||
}
|
||||
""";
|
||||
Files.writeString(sourceB, caller);
|
||||
Files.writeString(sourceB.getParent().resolve("mod.barrel"), "pub fn caller() -> int;\n");
|
||||
final Path realRoot = projectRoot.toRealPath();
|
||||
final Path realA = sourceA.toRealPath();
|
||||
final Path realB = sourceB.toRealPath();
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
|
||||
final var links = bridge.documentLinks(
|
||||
new LspProjectContext("app", "pbs", realRoot),
|
||||
realB.toUri().toString(),
|
||||
caller);
|
||||
|
||||
assertEquals(1, links.links().size());
|
||||
final var link = links.links().getFirst();
|
||||
assertEquals(realA.toUri().toString(), link.targetUri());
|
||||
final int start = new DocumentPositionMapper(caller).byteOffsetOf(link.startLine(), link.startCharacter());
|
||||
final int end = new DocumentPositionMapper(caller).byteOffsetOf(link.endLine(), link.endCharacter());
|
||||
assertEquals("@app:a", new String(caller.getBytes(StandardCharsets.UTF_8), start, end - start, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private Path findRepoRoot(final Path start) {
|
||||
var current = start;
|
||||
while (current != null) {
|
||||
|
||||
@ -105,6 +105,11 @@ class PrometeuLanguageServerTest {
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
List.of(),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
|
||||
@ -10,6 +10,8 @@ import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentIssue;
|
||||
import p.studio.lsp.messages.BaselineDocumentLink;
|
||||
import p.studio.lsp.messages.BaselineDocumentLinks;
|
||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||
import p.studio.lsp.messages.BaselineCompletionItem;
|
||||
import p.studio.lsp.messages.BaselineCompletionItemKind;
|
||||
@ -55,6 +57,11 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -82,6 +89,14 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getRenameProvider().getRight().getPrepareProvider());
|
||||
assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds());
|
||||
assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider());
|
||||
assertEquals(Boolean.FALSE, result.getCapabilities().getDocumentLinkProvider().getResolveProvider());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getFoldingRangeProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getSelectionRangeProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentFormattingProvider().getLeft());
|
||||
assertNull(result.getCapabilities().getDocumentRangeFormattingProvider());
|
||||
assertNull(result.getCapabilities().getDocumentOnTypeFormattingProvider());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getCallHierarchyProvider().getLeft());
|
||||
assertNull(result.getCapabilities().getTypeHierarchyProvider());
|
||||
|
||||
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
|
||||
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
|
||||
@ -269,12 +284,14 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
1,
|
||||
18,
|
||||
BaselineIssueSeverity.ERROR,
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
|
||||
"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",
|
||||
@ -296,4 +313,59 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(1, action.getDiagnostics().size());
|
||||
assertEquals(diagnostic, action.getDiagnostics().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentLinksMapRangeAndTargetWithoutResolve() {
|
||||
final var mapper = new Lsp4jProtocolMessageMapper();
|
||||
final var unsupported = mapper.mapInitializeResult(serverDescription(false));
|
||||
assertNull(unsupported.getCapabilities().getDocumentLinkProvider());
|
||||
assertNull(unsupported.getCapabilities().getFoldingRangeProvider());
|
||||
assertNull(unsupported.getCapabilities().getSelectionRangeProvider());
|
||||
|
||||
final var mapped = mapper.mapDocumentLinks(new BaselineDocumentLinks(List.of(
|
||||
new BaselineDocumentLink("file:///tmp/a/source.pbs", 0, 19, 0, 25))));
|
||||
assertEquals(1, mapped.size());
|
||||
assertEquals("file:///tmp/a/source.pbs", mapped.getFirst().getTarget());
|
||||
assertEquals(0, mapped.getFirst().getRange().getStart().getLine());
|
||||
assertEquals(19, mapped.getFirst().getRange().getStart().getCharacter());
|
||||
assertEquals(25, mapped.getFirst().getRange().getEnd().getCharacter());
|
||||
assertNull(mapped.getFirst().getData());
|
||||
assertTrue(mapper.mapDocumentLinks(BaselineDocumentLinks.empty()).isEmpty());
|
||||
}
|
||||
|
||||
private static BaselineServerDescription serverDescription(final boolean documentLinksSupported) {
|
||||
return new BaselineServerDescription(
|
||||
"demo",
|
||||
"1",
|
||||
"pbs",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
documentLinksSupported,
|
||||
documentLinksSupported,
|
||||
documentLinksSupported,
|
||||
documentLinksSupported,
|
||||
documentLinksSupported,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
List.of(new BaselineSemanticHostProjectionEntry(
|
||||
"demo-keyword",
|
||||
"keyword",
|
||||
List.of(),
|
||||
"variable",
|
||||
List.of())))),
|
||||
List.of(new BaselineVisualTheme(
|
||||
"demo-default",
|
||||
"Demo Default",
|
||||
new BaselineEditorPalette("#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777"),
|
||||
List.of(new BaselineTokenStyle("demo-keyword", "#888888", false, false, false)))),
|
||||
"demo-default");
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -22,10 +24,12 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
final class StudioRuntimeHandshakeServiceTest {
|
||||
@Test
|
||||
void connectsPublishesRuntimeHandshakeAndStreamsLogEvents() throws Exception {
|
||||
try (final ServerSocket serverSocket = new ServerSocket(0)) {
|
||||
try (final ServerSocket serverSocket = new ServerSocket()) {
|
||||
serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0));
|
||||
final CountDownLatch clientStarted = new CountDownLatch(1);
|
||||
final CountDownLatch eventDelivered = new CountDownLatch(1);
|
||||
final Thread serverThread = new Thread(() -> serveSuccessfulHandshake(serverSocket, clientStarted, eventDelivered));
|
||||
serverThread.setDaemon(true);
|
||||
serverThread.start();
|
||||
|
||||
final StudioExecutionSessionService session = new StudioExecutionSessionService();
|
||||
@ -36,7 +40,7 @@ final class StudioRuntimeHandshakeServiceTest {
|
||||
final StudioRuntimeHandshakeService service = new StudioRuntimeHandshakeService(new ObjectMapper(), backgroundTasks);
|
||||
final StudioRuntimeHandshakeResult result = service.connect(session, settings);
|
||||
|
||||
assertTrue(result.success());
|
||||
assertTrue(result.success(), result.failureMessage());
|
||||
assertEquals(StudioExecutionState.RUNNING, session.snapshot().state());
|
||||
assertTrue(clientStarted.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(eventDelivered.await(2, TimeUnit.SECONDS));
|
||||
@ -90,6 +94,9 @@ final class StudioRuntimeHandshakeServiceTest {
|
||||
writer.flush();
|
||||
eventDelivered.countDown();
|
||||
} catch (Exception exception) {
|
||||
if (serverSocket.isClosed()) {
|
||||
return;
|
||||
}
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user