Compare commits

...

18 Commits

Author SHA1 Message Date
7011209675 Merge pull request 'dev/pbs-lsp-remaining-editor-surface' (#31) from dev/pbs-lsp-remaining-editor-surface into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 63.92% (20214/31624) * Branch Coverage: 54.09% (8188/15137) * Lines of Code: 31624 * Cyclomatic Complexity: 13003 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 701
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #31
2026-09-22 09:00:21 +00:00
274ce28e8d
clean up and fixes
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 63.92% (20214/31624) * Branch Coverage: 54.09% (8188/15137) * Lines of Code: 31624 * Cyclomatic Complexity: 13003 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 701
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
2026-09-22 09:53:21 +01:00
1fcecdb04d
implements PLN-0137 (7/7) call-hierarchy
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, and type hierarchy stays unadvertised.

Housekeep DSC-0066 with LSN-0074.
2026-09-22 09:20:28 +01:00
e456bb8d7a
implements PLN-0137 (6/7) module-ref-completion
Completion inside a ModuleRef lists project and stdlib modules the
current resolver can already name. Completion after a dot stays the
existing member list. Items remain eager, resolveProvider stays false,
and there is no snippet or ranking score.
2026-09-22 09:13:40 +01:00
202c785f94
implements PLN-0137 (5/7) semantic-token-overlay
Keep the lexical token provider as the baseline. When hover resolution
names a symbol and an existing semantic key matches it, replace the
weaker identifier token. A failed resolution, a broken file, or a
symbol without an existing key keeps the provider token. No new
semantic key is added.
2026-09-22 09:09:43 +01:00
7629a73e0a
implements PLN-0137 (4/7) document-formatting
Advertise full-document formatting only. The formatter reprints the
token stream and changes indentation by four spaces where brace or
parenthesis nesting changes. Doc text-block interiors stay as written,
comments stay on their current line, and declarations are not reordered.
Range formatting and on-type formatting stay off.
2026-09-22 08:50:18 +01:00
ce2e3e4cd3
implements PLN-0137 (3/7) folding-and-selection-ranges
Advertise folding and selection only when the frontend supports them.
Folding uses recovered spans for 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 the
identifier to the argument or parameter list, the block, and the
declaration.
2026-09-22 08:45:31 +01:00
3320f222bb
implements PLN-0137 (2/7) document-links
Advertise document links only when the frontend supports them.
A link covers the ModuleRef span when module assembly already
bound that module to a regular file. Virtual stdlib and unresolved
modules produce no link.
2026-09-22 08:34:13 +01:00
099857b636
implements PLN-0137 (1/7) diagnostic-source
Lock DSC-0066 on Q1-A and Q2-A. Retire AGD-0048 through AGD-0056
into AGD-0069, DEC-0058, and PLN-0137.

Published editor diagnostics use the bound frontend language id as
source. For PBS that is pbs. The stable compiler code stays in code
and is not copied into source. Related locations, phase, and repair
payload stay unpublished.
2026-09-22 08:11:06 +01:00
20adc196d9 Merge pull request 'implements PLN-0136' (#30) from dev/pbs-lsp-code-actions-and-quick-fixes into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 63.75% (19407/30442) * Branch Coverage: 54.17% (7740/14289) * Lines of Code: 30442 * Cyclomatic Complexity: 12365 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 685
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #30
2026-09-22 06:51:07 +00:00
ae891262e1
implements PLN-0136
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 63.75% (19407/30442) * Branch Coverage: 54.17% (7740/14289) * Lines of Code: 30442 * Cyclomatic Complexity: 12365 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 685
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Add PBS quick fixes as frontend-owned repairs transported by the LSP.
Duplicate reserved attributes are deleted at the extra span. Interface
InitAllowed attributes with arguments become [InitAllowed]. The server
matches Diagnostic.code plus range, does not parse messages, and returns
an empty list when the capability or the fix is absent.

Housekeep DSC-0044 with LSN-0073.
2026-09-22 07:43:05 +01:00
6f45f4f205 Merge pull request 'implements PLN-0135' (#29) from dev/pbs-lsp-rename-symbol into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 63.56% (19159/30145) * Branch Coverage: 54.14% (7612/14060) * Lines of Code: 30145 * Cyclomatic Complexity: 12214 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 678
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #29
2026-09-22 05:54:40 +00:00
65d2d40310
implements PLN-0135
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 63.56% (19159/30145) * Branch Coverage: 54.14% (7612/14060) * Lines of Code: 30145 * Cyclomatic Complexity: 12214 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 678
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Add PBS rename as a validated WorkspaceEdit of physical identifier
sites on FrontendLanguageService. Identity is definition; locals and
params rename from their name span. Refuse aliases, virtual stdlib,
builtins, error labels, keywords, and collisions. Advertise
renameProvider with prepareProvider; refused cursors are LSP errors,
not empty success.

Housekeep DSC-0043 with LSN-0072.
2026-09-22 06:40:18 +01:00
fa21a029ae Merge pull request 'implements PLN-0134' (#28) from dev/pbs-lsp-workspace-symbols into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 63.37% (18783/29640) * Branch Coverage: 53.99% (7369/13648) * Lines of Code: 29640 * Cyclomatic Complexity: 11938 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 674
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #28
2026-09-21 16:27:42 +00:00
12e2a4edcc
implements PLN-0134
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 63.37% (18783/29640) * Branch Coverage: 53.99% (7369/13648) * Lines of Code: 29640 * Cyclomatic Complexity: 11938 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 674
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Add PBS workspace symbols as a physical flatten of the outline
named-declaration set on FrontendLanguageService. Regular files
only; virtual stdlib, barrels, imports, locals, and compile-only
frontends stay empty. Advertise workspaceSymbolProvider with flat
SymbolInformation. workspace/symbol does not require an open
document.

Housekeep DSC-0042 with LSN-0071.
2026-09-21 17:18:00 +01:00
78d10cf03c Merge pull request 'implements PLN-0133' (#27) from dev/pbs-lsp-document-symbols-and-outline into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 62.84% (18504/29445) * Branch Coverage: 53.33% (7215/13528) * Lines of Code: 29445 * Cyclomatic Complexity: 11850 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 669
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #27
2026-09-21 09:23:52 +00:00
5d3d48e802
implements PLN-0133
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 62.84% (18504/29445) * Branch Coverage: 53.33% (7215/13528) * Lines of Code: 29445 * Cyclomatic Complexity: 11850 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 669
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Add PBS document symbols as an AST-backed hierarchical outline of
the open .pbs file on FrontendLanguageService. Recovered syntax is
enough; unnamed InvalidDecl is omitted; barrels, imports, locals,
and compile-only frontends stay empty. Advertise documentSymbolProvider
with hierarchical DocumentSymbol.

Housekeep DSC-0041 with LSN-0070.
2026-09-21 10:16:14 +01:00
331ad752d1 Merge pull request 'implements PLN-0132' (#26) from dev/pbs-lsp-find-references into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 62.51% (18192/29102) * Branch Coverage: 53.15% (7093/13345) * Lines of Code: 29102 * Cyclomatic Complexity: 11715 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 663
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #26
2026-09-21 08:00:40 +00:00
97 changed files with 8190 additions and 839 deletions

View File

@ -1,4 +1,5 @@
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":54,"PLN":133,"LSN":70,"CLSN":1}}
{"type":"meta","next_id":{"DSC":67,"AGD":70,"DEC":59,"PLN":138,"LSN":75,"CLSN":1}}
{"type":"discussion","id":"DSC-0066","status":"done","ticket":"pbs-lsp-remaining-editor-surface","title":"PBS LSP Remaining Editor Surface","created_at":"2026-09-22","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0074","file":"discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/LSN-0074-pbs-remaining-editor-surface-is-frontend-owned.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
{"type":"discussion","id":"DSC-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
{"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
{"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
@ -11,19 +12,19 @@
{"type":"discussion","id":"DSC-0056","status":"done","ticket":"multi-frontend-remove-pbs-branches","title":"Generalizar o contrato LSP/editorial para frontends","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0058","file":"discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
{"type":"discussion","id":"DSC-0055","status":"done","ticket":"multi-frontend-compiler-vs-language-services","title":"Separar compilacao de servicos editoriais de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","lsp","editor","frontend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0056","file":"discussion/lessons/DSC-0055-multi-frontend-compiler-vs-language-services/LSN-0056-compile-first-frontends-with-optional-editorial-capabilities.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
{"type":"discussion","id":"DSC-0054","status":"done","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0055","file":"discussion/lessons/DSC-0054-multi-frontend-provider-contract/LSN-0055-static-frontend-providers-before-plugin-architecture.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
{"type":"discussion","id":"DSC-0053","status":"open","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0052","status":"open","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[{"id":"AGD-0055","file":"AGD-0055-pbs-lsp-document-links.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0051","status":"open","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[{"id":"AGD-0054","file":"AGD-0054-pbs-lsp-diagnostics-ux.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0050","status":"open","ticket":"pbs-lsp-folding-and-selection-ranges","title":"PBS LSP Folding and Selection Ranges","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","folding","selection-range"],"agendas":[{"id":"AGD-0053","file":"AGD-0053-pbs-lsp-folding-and-selection-ranges.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0049","status":"open","ticket":"pbs-lsp-completion-depth","title":"PBS LSP Completion Depth","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","completion"],"agendas":[{"id":"AGD-0052","file":"AGD-0052-pbs-lsp-completion-depth.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0048","status":"open","ticket":"pbs-lsp-performance-cache-and-snapshots","title":"PBS LSP Performance Cache and Snapshots","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","performance","caching"],"agendas":[{"id":"AGD-0051","file":"AGD-0051-pbs-lsp-performance-cache-and-snapshots.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0047","status":"open","ticket":"pbs-lsp-semantic-tokens-semantic-classification","title":"PBS LSP Semantic Token Classification","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","semantic-tokens"],"agendas":[{"id":"AGD-0050","file":"AGD-0050-pbs-lsp-semantic-token-classification.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0046","status":"open","ticket":"pbs-lsp-import-assistance","title":"PBS LSP Import Assistance","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","imports","completion"],"agendas":[{"id":"AGD-0049","file":"AGD-0049-pbs-lsp-import-assistance.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0045","status":"open","ticket":"pbs-lsp-formatting","title":"PBS LSP Formatting","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","formatting"],"agendas":[{"id":"AGD-0048","file":"AGD-0048-pbs-lsp-formatting.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0044","status":"open","ticket":"pbs-lsp-code-actions","title":"PBS LSP Code Actions and Quick Fixes","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","code-actions","quick-fix"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-pbs-lsp-code-actions-and-quick-fixes.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0043","status":"open","ticket":"pbs-lsp-rename-symbol","title":"PBS LSP Rename Symbol","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","rename"],"agendas":[{"id":"AGD-0046","file":"AGD-0046-pbs-lsp-rename-symbol.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0042","status":"open","ticket":"pbs-lsp-workspace-symbols","title":"PBS LSP Workspace Symbols","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","workspace-symbols"],"agendas":[{"id":"AGD-0045","file":"AGD-0045-pbs-lsp-workspace-symbols.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0041","status":"open","ticket":"pbs-lsp-document-symbols-outline","title":"PBS LSP Document Symbols and Outline","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","outline","document-symbols"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-pbs-lsp-document-symbols-and-outline.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0053","status":"done","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0052","status":"done","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0051","status":"done","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0050","status":"done","ticket":"pbs-lsp-folding-and-selection-ranges","title":"PBS LSP Folding and Selection Ranges","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","folding","selection-range"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0049","status":"done","ticket":"pbs-lsp-completion-depth","title":"PBS LSP Completion Depth","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","completion"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0048","status":"done","ticket":"pbs-lsp-performance-cache-and-snapshots","title":"PBS LSP Performance Cache and Snapshots","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","performance","caching"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0047","status":"done","ticket":"pbs-lsp-semantic-tokens-semantic-classification","title":"PBS LSP Semantic Token Classification","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","semantic-tokens"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0046","status":"done","ticket":"pbs-lsp-import-assistance","title":"PBS LSP Import Assistance","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","imports","completion"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0045","status":"done","ticket":"pbs-lsp-formatting","title":"PBS LSP Formatting","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","formatting"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0044","status":"done","ticket":"pbs-lsp-code-actions","title":"PBS LSP Code Actions and Quick Fixes","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","code-actions","quick-fix"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0073","file":"discussion/lessons/DSC-0044-pbs-lsp-code-actions/LSN-0073-pbs-quick-fixes-are-frontend-repairs-transported-by-the-lsp.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
{"type":"discussion","id":"DSC-0043","status":"done","ticket":"pbs-lsp-rename-symbol","title":"PBS LSP Rename Symbol","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","rename"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0072","file":"discussion/lessons/DSC-0043-pbs-lsp-rename-symbol/LSN-0072-pbs-rename-is-a-validated-physical-workspace-edit.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
{"type":"discussion","id":"DSC-0042","status":"done","ticket":"pbs-lsp-workspace-symbols","title":"PBS LSP Workspace Symbols","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","workspace-symbols"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0071","file":"discussion/lessons/DSC-0042-pbs-lsp-workspace-symbols/LSN-0071-pbs-workspace-symbols-are-a-physical-named-declaration-search.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
{"type":"discussion","id":"DSC-0041","status":"done","ticket":"pbs-lsp-document-symbols-outline","title":"PBS LSP Document Symbols and Outline","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","outline","document-symbols"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0070","file":"discussion/lessons/DSC-0041-pbs-lsp-document-symbols-outline/LSN-0070-pbs-outline-is-an-ast-backed-document-symbol-tree.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"pbs-lsp-find-references","title":"PBS LSP Find References","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","references"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0069","file":"discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
{"type":"discussion","id":"DSC-0039","status":"done","ticket":"pbs-lsp-go-to-definition","title":"PBS LSP Go to Definition","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","definition"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0068","file":"discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
{"type":"discussion","id":"DSC-0038","status":"done","ticket":"studio-packer-rgba8888-asset-pipeline","title":"Studio and Packer RGBA8888 Asset Pipeline Alignment","created_at":"2026-05-23","updated_at":"2026-07-14","tags":["studio","packer","assets","glyph-bank","palette","rgba8888","runtime-alignment"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0053","file":"discussion/lessons/DSC-0038-studio-packer-rgba8888-asset-pipeline/LSN-0053-rgba8888-is-the-canonical-studio-packer-palette-contract.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14"}]}

View File

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

View File

@ -0,0 +1,149 @@
---
id: LSN-0071
ticket: pbs-lsp-workspace-symbols
title: PBS workspace symbols are a physical named-declaration search
created: 2026-09-21
tags: [studio, lsp, vscode, compiler-pbs, editor, workspace-symbols]
---
# PBS workspace symbols are a physical named-declaration search
## Original Problem
PBS already had compiler-backed completion, hover, signature help, go-to-definition (`LSN-0068`), find-references (`LSN-0069`), and an AST-backed outline of the open `.pbs` file (`LSN-0070`). VS Code workspace search (`Ctrl+T` / `workspace/symbol`) stayed empty.
The risk was treating workspace symbols as a second outline, inventing a `SymbolId` index, waiting for snapshot cache (`AGD-0051`), or returning stdlib/SDK hits whose declarations are still virtual paths (`/virtual/stdlib`). Workspace symbols is a jump. Outline is not.
## Consolidated Decision
PBS Workspace Symbols is the LSP projection of a **flat named-declaration search** over **regular filesystem files** of the current analysis, flattening the **same outline set** and using the **same physical-file destination policy** as definition.
Durable locks from `DEC-0055` (AGD-0045 Q1 A, Q2 A, Q3 A, Q4 A, Q5 A, Q6 first-wave exclusions):
1. Emit only declarations whose `fileTable` handle is a regular filesystem file. Virtual stdlib/SDK, including `/virtual/stdlib`, is omitted. Do not invent virtual documents, `untitled:` buffers, or synthetic URIs.
2. `workspace/symbol` does not require an open text document. The server obtains the project analysis surface (`astByFile` / `projectSurface`) from `analyze()`. Missing surface → empty list.
3. The set is the flatten, file by file, of the named-declaration tree locked by outline (`LSN-0070`):
- `fn` → Function;
- `declare struct` → Struct plus fields, `ctor`s, methods;
- `declare service` → Service plus methods;
- `declare contract` → Contract plus signatures;
- `declare host` → Host plus signatures;
- `declare builtin type` → Builtin type plus projection fields and signatures;
- `declare enum` → Enum plus cases;
- `declare error` → Error plus labels;
- `declare callback` → Callback;
- `declare global` / `const` → Global / Const;
- `implements Contract for Owner``contractName`, kind CONTRACT, `detail` = owner, plus methods.
Locals, parameters, `this`, imports, and comments stay out. Private fields stay in. Overloads stay sibling hits. Unnamed `InvalidDecl` is omitted. Named decls survive later semantic errors.
4. First wave is `.pbs` whose recovered root is `PbsAst.File`. `mod.barrel` (`BarrelFile`) stays empty. Presence, name, kind, `detail`, and spans come from recovered AST. Do not invent nodes from text search.
5. Query is a case-insensitive substring of `name` only. Blank query returns the full first-wave corpus. Do not match `detail`, path, `containerName`, or qualified `Vec.blend`. No fuzzy ranking, no `top N`. After the filter, order is stable by name, then path, then start offset.
6. `containerName` is the parent name for members and empty for top-level declarations. `detail` may repeat the outline AST signature. Attributes such as `[Init]` do not enter the name.
7. Keep the capability on aggregated `FrontendLanguageService`. Add `workspaceSymbols` (default empty) plus a flat DTO with `name`, `kind`, optional `detail`, `containerName`, absolute path, and offsets. Do not reuse pathless `FrontendDocumentSymbol`. PBS flattens in `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. No Workspace Symbol SPI, `SymbolId`, `RefIndex`, or snapshot cache.
8. Advertise `workspaceSymbolProvider` when the frontend exposes a language service. Payload is flat `SymbolInformation`. Do not emit hierarchical `DocumentSymbol` on this request. Compile-only frontends stay empty. VS Code stays a thin client.
Spec 23 §8.3 now also states: workspace symbols are a flat search of named declarations in compiler-known physical source files of the current analysis; recovered syntax is enough; a missing analysis surface is empty; the request must not require an open document; a declaration without a physical file is omitted; this is not outline, folding, a persistent index, or navigation to virtual locations.
## Final Implementation
| Layer | What landed |
|---|---|
| PBS editorial | `PbsAstWorkspaceSymbols` flattens `PbsAstDocumentSymbols` and filters by name. `PbsEditorialSupportService.workspaceSymbols(ast, query)` delegates to that flatten. |
| Generic contract | `FrontendWorkspaceRequest` (`projectRoot`, `query`). `FrontendWorkspaceSymbol` carries path. `FrontendLanguageService.workspaceSymbols` defaults to `List.of()`. `prepareEditorialContext(FrontendPhaseContext)` defaults to empty so a workspace request can obtain project surface without a `FileId`. |
| PBS mapping | Project-level `prepareEditorialContext` attaches `fileTable` + `astByFile`. Regular `PbsAst.File` entries are flattened; non-regular handles are skipped. Results sort by name, path, offset. |
| LSP | `workspaceSymbolProvider`, `workspace/symbol` on `PrometeuWorkspaceService` (no document URI). `CompilerLanguageServiceBridge` analyzes the project with an empty overlay map. Mapper emits flat `SymbolInformation`. Nested enum/error hits with a container project to `EnumMember`. Compile-only frontends stay empty. |
| VS Code | Unchanged thin client. |
`workspace/symbol` in this wave reads the project on disk. Unsaved editor overlays are not shared into the search. Empty is honest until a later overlay/snapshot decision.
`mod.barrel` is still `BarrelFile`, not `PbsAst.File`. Empty barrel hits are the honest first-wave result.
Error case labels still have no per-label span. Flattened children reuse the parent `ErrorDecl` span, same as outline.
## Examples
Workspace search of a project file is the flatten of that file's outline, not imports or locals:
```pbs
import { Log } from @sdk:log;
fn helper() -> int { return 42; }
fn frame() -> void {
let local = 1;
helper(local);
}
```
Query `frame``frame`. Query `helper``helper`. Query empty → `helper`, `frame` (plus other physical decls), sorted. Not `Log`, not `local`.
Members keep a container; overloads stay siblings:
```pbs
declare struct Vec(x: int) {
ctor make(x: int) { return; }
fn blend(dx: int, dy: int) -> int { return dx; }
}
fn helper() -> int { return 42; }
fn helper(value: int) -> int { return value; }
```
Query `blend` → method `blend` with `containerName` `Vec`. Query `helper` → two hits. Query `Vec.blend` → empty (this wave matches `name` only).
Stdlib remains hover-only while the handle is virtual:
```pbs
import { Gfx } from @sdk:gfx;
Gfx.clear(0);
```
Query `Gfx` is empty. F12 on `Gfx.clear` is already empty (`LSN-0068`). Workspace search does not reopen that jump.
Recovered parse keeps the named decl:
```pbs
declare ;
fn ok() -> int { return 1; }
```
Query empty → `ok`. The unnamed `InvalidDecl` is omitted.
`[Init] fn init()` still names the symbol `init`.
## Pitfalls
Do not implement workspace symbols as workspace text search in `lsp-v1` or in the VS Code extension. It will lie about overloads, members, and imports.
Do not reuse `FrontendDocumentSymbol`. Outline is pathless because the host already has the document URI. Workspace symbols is a jump and must carry a path.
Do not require an open editor buffer to answer `workspace/symbol`. Analyze the project and walk `astByFile`.
Do not include `/virtual/stdlib` so Ctrl+T on `Gfx` “does something”. Empty is the honest result until those sources are regular files.
Do not wait for snapshot cache. References already walks the current analysis request-locally. Snapshots (`AGD-0051`) can make the walk cheaper without changing this contract.
Do not add a `SymbolId` index “for rename later”. Outline already named the declarations; definition already named the destination.
Do not match `containerName`, path, or `Vec.blend` in this wave. The query is a substring of `name`.
Do not emit hierarchical `DocumentSymbol` on `workspace/symbol`. That payload is outline.
Do not treat unsaved overlay absence as a missing feature of this contract. First-wave analysis is the project on disk.
Do not split a Workspace Symbol SPI “for future languages”. PBS is a real consumer of `FrontendLanguageService` (`LSN-0067`).
Do not treat this lesson as closing rename, snapshots, folding, document links, or hierarchy.
## References
- Agenda: `AGD-0045` (Q1 A, Q2 A, Q3 A, Q4 A, Q5 A, Q6 first-wave exclusions)
- Decision: `DEC-0055`
- Plan: `PLN-0134`
- Spec 23 §8.3 — optional workspace symbols; physical-file-or-omit; no open document required
- `LSN-0070` — outline set this feature flattens
- `LSN-0068` — physical-file destination this feature reuses
- `LSN-0069` — project `astByFile` walk already exists for references
- `LSN-0058` — generic `FrontendLanguageService` for LSP
- `LSN-0047` — protocol stays in `lsp-v1`; compiler owns semantics
- `LSN-0067` — no SPI without a real in-repo consumer
- Still open: `AGD-0046` rename, `AGD-0051` snapshots, `AGD-0053` folding, `AGD-0055` document links, `AGD-0056` hierarchy

View File

@ -0,0 +1,128 @@
---
id: LSN-0072
ticket: pbs-lsp-rename-symbol
title: PBS rename is a validated physical workspace edit
created: 2026-09-22
tags: [studio, lsp, vscode, compiler-pbs, editor, rename]
---
# PBS rename is a validated physical workspace edit
## Original Problem
PBS already had compiler-backed completion, hover, signature help, go-to-definition (`LSN-0068`), find-references (`LSN-0069`), outline (`LSN-0070`), and workspace symbols (`LSN-0071`). Users still could not rename a symbol through the editor. Manual text edits miss uses or rewrite unrelated overloads, comments, strings, and same-name declarations.
The risk was a file-local rename, a lexeme replace, a new `SymbolId` / `RefIndex`, snapshot cache (`AGD-0051`) as a prerequisite, virtual stdlib documents, or a successful empty `WorkspaceEdit` that pretends the symbol changed.
## Consolidated Decision
PBS Rename Symbol is the LSP projection of a **validated `WorkspaceEdit`** over **physical identifier spans** of the **same identity definition already returns**.
Durable locks from `DEC-0056` (AGD-0046 Q1Q6 = A):
1. Identity is the physical declaration-location set from definition. Do not text-search. Do not add `SymbolId` or a persistent `RefIndex`.
2. Sites are identifier tokens whose definition intersects that identity, plus identifier-sized declaration name spans. Overloads of the same name in the same owner rewrite together. Members rewrite only that member. Comments, strings, and homonyms of another identity stay.
3. Walk regular `fileTable` files of the current `analyze()` surface. Omit `/virtual/stdlib` and other non-regular handles. Every edit location must be a compiler-known physical source file.
4. Refuse when physical identity is empty: builtins, virtual stdlib/SDK, unresolved names, keywords, comments, strings, `this`, `Self`. Refuse `mod.barrel`. Refuse `declare error` case labels that have no per-label span.
5. Exception: a local or parameter name span is renameable even when definition at that offset is empty. Identity becomes that name span; uses are identifiers whose definition intersects it.
6. Refuse when the cursor lexeme is an import alias (`item.alias()` distinct from `item.name()`), including uses of that alias. Do not follow alias definition and rewrite the export. Export rename happens only when the cursor writes the original name. Export rewrite must not change `as Alias` tokens or uses that spell a differing alias.
7. `prepareRename` validates the cursor and returns the identifier range, or null. `rename` also validates the new name: PBS identifier, not a keyword, no collision in the same binding space (top-level `fn`/`service`/`global`/`const`, local/param in the same scope, field in the same struct, method in the same owner, case in the same enum), ignoring rewritten sites and the overload set. Failure is an LSP error. Never return a successful empty `WorkspaceEdit`. The server does not write files and does not rename paths.
8. Keep the capability on aggregated `FrontendLanguageService`. `prepareRename` defaults empty; `rename` defaults unsupported. PBS maps in `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. No Rename SPI. Advertise `renameProvider` with `prepareProvider`. Compile-only stays unsupported. VS Code stays a thin client.
Spec 23 §8.3 now also states: rename is a validated workspace edit of identifier spans in compiler-known physical source files; identity is definition; local/param name spans remain renameable; alias, virtual, and error-label cursors refuse; invalid names and collisions are protocol errors, not empty success; the server does not write files; `prepareProvider` is advertised when rename is exposed.
## Final Implementation
| Layer | What landed |
|---|---|
| PBS editorial | `PbsEditorialSupportService.planRename` builds identity from definition or local/param name span, refuses aliases and error labels, walks `IDENTIFIER` tokens whose definition intersects identity and whose lexeme is the original name, then validates identifier/keyword/collision. |
| Generic contract | `FrontendLanguageService.prepareRename` defaults empty. `rename` defaults `FrontendRenameResult.unsupported()`. `FrontendRenameEdit` is path + offsets + `newText`. Applied results reject an empty edit list. |
| PBS mapping | `PBSFrontendLanguageService` reuses `projectDocuments` and `toFrontendLocation`. Empty physical identity is refuse, even if editorial sites exist on virtual handles. |
| LSP | `renameSupported`, `prepareRename`, `rename`. Mapper advertises `RenameOptions.prepareProvider`. Refused rename is `ResponseErrorException`, not an empty `WorkspaceEdit`. Compile-only does not advertise. |
| VS Code | Unchanged thin client. |
Stdlib and SDK modules remain virtual (`/virtual/stdlib/...`). F2 on `Gfx.clear` is refused, matching empty F12 / Shift+F12. Hover is unchanged.
`let total` at the binding itself still has empty definition. Rename uses the name span as identity so F2 on the binding works.
## Examples
Same-file function, including overloads and the declaring identifier. Comment and string stay:
```pbs
fn helper() -> int { return 1; }
fn helper(value: int) -> int { return value; }
fn frame() -> void {
helper();
helper(2);
// helper
let text = "helper";
}
```
F2 on `helper``aid` rewrites both declarations and both calls.
Local name span:
```pbs
fn frame() -> void {
let total = 1;
return total;
}
```
F2 on `let total` or on the use rewrites both identifier sites.
Alias is refused. Export rename keeps the local alias:
```pbs
import { Log as Logger } from @sdk:log;
Logger.info("a");
```
F2 on `Logger` or `Logger.info` is refused. F2 on a physical `Log` declaration rewrites the original name and `import { Log as Logger }`, and leaves `Logger` / `Logger.info`.
Builtins and virtual stdlib stay hover-only:
```pbs
let n: int = 1;
Gfx.clear(5);
```
Invalid new names (`fn`, `1ab`) and collisions (`helper` → existing `frame`) fail with an LSP error.
## Pitfalls
Do not implement rename as workspace text search in `lsp-v1` or in the VS Code extension. It will rewrite comments, strings, overloads of another identity, and import aliases.
Do not follow definition through an import alias. `Logger` currently resolves to `Log`'s span; a blind inverse-of-definition rename would rewrite the export.
Do not return a successful empty `WorkspaceEdit` for a refused cursor. The editor will treat that as “renamed”.
Do not skip the local/param name-span exception. F2 on `let total` is the common case, and definition there is empty.
Do not rewrite `declare error` case labels by using the parent `ErrorDecl` span. Those labels have no per-label span.
Do not wait for snapshot cache. Rename reuses the request-local walk already used by references.
Do not treat empty/refused stdlib rename as a missing feature. Virtual paths are not regular files.
Do not split a Rename SPI “for future languages”. PBS is a real consumer of `FrontendLanguageService` (`LSN-0067`).
Do not treat this lesson as closing snapshots, folding, document links, hierarchy, code actions, formatting, file rename, or local-alias rename.
## References
- Agenda: `AGD-0046` (Q1Q6 = A)
- Decision: `DEC-0056`
- Plan: `PLN-0135`
- Spec 23 §8.3 — optional rename; physical-file-or-empty; alias refuse; error instead of empty success; `prepareProvider`
- Spec: `docs/specs/compiler-languages/pbs/3. Core Syntax Specification.md` §4.34.4
- `LSN-0068` — definition identity this feature writes
- `LSN-0069` — physical inverse that supplies usage sites
- `LSN-0051` — wave 1 deferred rename
- `LSN-0058` — generic `FrontendLanguageService` for LSP
- `LSN-0047` — protocol stays in `lsp-v1`; compiler owns semantics
- `LSN-0067` — no SPI without a real in-repo consumer
- Still open: `AGD-0051` snapshots, `AGD-0053` folding, `AGD-0055` document links, `AGD-0056` hierarchy

View File

@ -0,0 +1,130 @@
---
id: LSN-0073
ticket: pbs-lsp-code-actions
title: PBS quick fixes are frontend repairs transported by the LSP
created: 2026-09-22
tags: [studio, lsp, vscode, compiler-pbs, editor, code-actions, quick-fix]
---
# PBS quick fixes are frontend repairs transported by the LSP
## Original Problem
PBS diagnostics already reach the editor, but the server did not advertise `codeActionProvider`. A user could see a duplicate attribute or a bad `InitAllowed` and still had to repair it by hand.
The tempting shortcut was a code-to-edit table in the LSP. That shortcut does not work. Compiler `Diagnostic` values carry a stable code, phase, template, placeholders, span, and related locations, but the reserved-attribute reports use the short constructor: empty placeholders, and the English sentence is the only fine discriminator. `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` covers Doc without `markdown`, a wrong payload, empty markdown, an unknown argument, `InitAllowed` with arguments, an out-of-range `AssetLowering`, and a bad `Host` shape. A switch on the code cannot pick a safe edit without reading the message. Diagnostic wording is rendering, not identity. Code actions are an optional editorial capability: the frontend owns the meaning, and the LSP transports it.
## Consolidated Decision
PBS quick fix is one `WorkspaceEdit` tied to one diagnostic of the requested physical document. The frontend produces the repair from the syntax tree. The LSP does not invent it.
Durable locks from `DEC-0057` (AGD-0047: scope A, producer B, residence B, first wave C):
1. This wave is quick fix only. No organize-imports, source action, refactor, function or struct stub, code lens, or server command.
2. The server does not write files. The client applies the edit.
3. Each repair names the stable diagnostic code, the primary span of that diagnostic, and exactly one local text edit. The LSP must not keep a code-to-edit table and must not read the diagnostic message to build the edit.
4. The repair is computed on `textDocument/codeAction` from the current document analysis. It is not a required field of compiler `Diagnostic`, and it is not published inside `publishDiagnostics`, including `data`.
5. `publishDiagnostics` carries the stable compiler code on `Diagnostic.code` so the client can echo it. Source, severity, message, and related information stay as they were. Those presentation questions remain with `AGD-0054`.
6. A repair is returned only when the request context contains a diagnostic with the same code and the same primary range. An empty diagnostic list returns an empty action list. An `only` filter that does not include quick fix returns an empty action list.
7. Each item is a QuickFix `CodeAction`, not a `Command`. The edit touches one file, the requested physical document, and contains one `TextEdit`. There is no lazy resolve.
8. A path that is not a compiler-known regular file, including `/virtual/stdlib`, gets no edit.
9. If the frontend does not expose the capability, the server does not advertise `codeActionProvider`. A request that arrives anyway returns an empty list, not a protocol error. Absence of a fix is not a refused cursor.
10. PBS exposes the capability. Another frontend does not gain it merely by having a language service.
11. The first wave is two shapes, not three diagnostic codes.
- For every extra reserved attribute the declaration validator already reports as `E_SEM_DUPLICATE_RESERVED_ATTRIBUTE`, delete exactly that occurrence's span. Do not delete the first occurrence, the declaration, or neighboring whitespace.
- For an `InitAllowed` attribute with a non-empty argument list on a host signature, the shape the validator reports as `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` when the file is an interface module, replace that attribute span with `[InitAllowed]`. Do not emit that repair for any other attribute that shares the code. `[InitAllowed()]` is not diagnosed today and gets no repair.
12. Nothing else gets a quick fix in this wave. In particular: inserting `[Doc(markdown = """""")]` or any other Doc rewrite, inserting an empty or incomplete `[Host]` or `[BuiltinType]`, `W_SEM_IGNORED_VALUE`, unresolved imports, an invalid reserved-attribute target, parse errors, unresolved overloads, and method stubs.
`AGD-0048` still owns formatting. `AGD-0049` still owns import assistance and organize-imports. `AGD-0054` still owns range, related locations, and diagnostic presentation. The PBS diagnostics spec does not gain a mandatory remediation field.
Spec 23 §8.3 now states the shared contract: code actions are quick fixes only; the frontend produces each repair from the document syntax; tooling transports it and does not parse the message or switch on codes; the published diagnostic carries the stable code; a repair is returned only for a matching context diagnostic; the edit is one physical file; the server does not write files, return a command, or require resolve; a missing capability is an empty list, not an error; virtual documents get no edit.
## Final Implementation
| Layer | What landed |
|---|---|
| PBS repairs | `PbsQuickFixCollector` walks the same attribute surfaces as `PbsDeclarationSemanticsValidator`, including the interface-module gate. Duplicate reserved attributes become an empty replacement of the extra span. The first `InitAllowed` with arguments on an interface host signature becomes `[InitAllowed]`. |
| Source kind | `PbsSemanticReadSurface.sourceKindByFile` records the kind used to parse each file. A missing kind means project rules, so a project `declare host` does not pretend the validator emitted the interface-only `InitAllowed` diagnostic. |
| Generic contract | `FrontendLanguageService.codeActionsSupported` defaults false. `codeActions` defaults empty. `FrontendCodeAction` is title, diagnostic code, path, offsets, and `newText`. Empty `newText` is a deletion and is allowed. |
| PBS mapping | `PBSFrontendLanguageService` exposes the capability and returns repairs only for the requested regular file. `/virtual/stdlib` fails `Files.isRegularFile` and produces nothing. |
| LSP | `BaselineDocumentIssue.code` is copied onto `Diagnostic.code`. `data` stays unset. The bridge drops repairs whose code and range do not match a context diagnostic. `PrometeuTextDocumentService.codeAction` honors the quick-fix `only` filter and never fails the request for “no fix”. |
| Protocol | `codeActionProvider` is `CodeActionOptions` with kind `quickfix` and `resolveProvider` false, and only when the frontend reports support. Mapped actions are `CodeAction` values with a `WorkspaceEdit`. They are not `Command`s. |
Titles are `Remove duplicate reserved attribute` and `Remove InitAllowed arguments`.
## Examples
Duplicate `Doc` on a project file. The first copy stays. Each later copy is deleted at its own span:
```pbs
[Doc(markdown = """
First.
""")]
[Doc(markdown = """
Second.
""")]
fn duplicated() -> void { return; }
```
The quick fix removes the second attribute and leaves `First.`. A context diagnostic with the same range but code `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` produces no action. An empty diagnostic list produces no action.
Malformed or empty Doc is visible and not repairable here:
```pbs
[Doc(text = """
Wrong argument.
""")]
fn malformed() -> void { return; }
```
`InitAllowed` with arguments is repaired only on an interface-module host signature:
```pbs
declare host Gfx {
[Host(module = "gfx2d", name = "clear", version = 1)]
[InitAllowed(flag = 1)]
fn clear() -> void;
[Host(module = "gfx2d", name = "fill", version = 1)]
[InitAllowed()]
fn fill() -> void;
}
```
`clear` becomes `[InitAllowed]`. `fill` is already valid to the validator, so `[InitAllowed()]` is left alone. The same `InitAllowed(flag = 1)` inside a project file does not get that repair, because the validator does not emit the malformed-attribute diagnostic there.
The same collector deletes the extra copy of `BuiltinType`, `IntrinsicCall`, `BuiltinConst`, `Host`, and `AssetLowering` when the validator reports `E_SEM_DUPLICATE_RESERVED_ATTRIBUTE` for that surface. A malformed `Host` that merely shares the malformed-attribute code gets no edit.
## Pitfalls
Do not choose the edit in `prometeu-lsp` by switching on `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` or by parsing the English message. The code is too coarse, and the message is not the identity.
Do not store the repair on compiler `Diagnostic` or in `Diagnostic.data`. Compilation and quick fix stay different surfaces. The published code exists so the client can echo it back.
Do not match on `Diagnostic.source` as if it were the long-term identity field. Today the bridge still copies the code into `source` for the existing presentation. The code-action match uses `Diagnostic.code`. `AGD-0054` may change `source` later.
Do not delete the first duplicate, and do not trim the whitespace around the deleted span. That cleanup is formatting and belongs to `AGD-0048`.
Do not insert `[Doc(markdown = """""")]`, an empty `[Host]`, or an empty `[BuiltinType]`. Those edits trade one diagnostic for another, or invent a declaration.
Do not treat `W_SEM_IGNORED_VALUE` as a source edit. Discard exists in lowering, not as PBS syntax.
Do not offer the `InitAllowed` rewrite for `[InitAllowed()]` or for a project file. The validator does not diagnose those shapes as “arguments not allowed”, so there is no diagnostic to attach.
Do not return a protocol error when there is no fix. Rename refuses a bad cursor with an error. Quick fix answers “nothing to do” with an empty list.
Do not advertise `codeActionProvider` for every frontend that has a language service. The default capability is unsupported.
Do not treat this lesson as closing formatting, import assistance, diagnostic presentation, snapshots, folding, document links, or hierarchy.
## References
- Agenda: `AGD-0047` (scope A, producer B, residence B, first wave C)
- Decision: `DEC-0057`
- Plan: `PLN-0136`
- Spec 23 §8.3 — optional code actions; frontend-owned repair; match by code and range; empty list when unsupported
- Spec: `docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md` — wording is not identity; remediation stays outside the v1 diagnostic minimum
- `LSN-0056` — code actions are an optional editorial capability
- `LSN-0058` — generic `FrontendLanguageService` for the LSP
- `LSN-0067` — no new editorial SPI without a real consumer; PBS implements the existing language service
- Still open: `AGD-0048` formatting, `AGD-0049` import assistance, `AGD-0054` diagnostics UX, `AGD-0051` snapshots, `AGD-0056` hierarchy

View File

@ -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`

View File

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

View File

@ -1,62 +0,0 @@
---
id: AGD-0045
ticket: pbs-lsp-workspace-symbols
title: PBS LSP Workspace Symbols
status: open
created: 2026-07-15
resolved:
decision:
tags: [studio, lsp, vscode, compiler-pbs, editor, workspace-symbols]
---
## Pain
Domain owner: `studio/lsp`
VS Code cannot search PBS symbols across the workspace, so users cannot quickly jump to functions, services, structs, constants, or stdlib-exposed APIs by name.
## Context
Document-level editorial features exist, but the LSP does not announce workspaceSymbolProvider and the compiler pipeline does not expose a project-wide symbol index designed for editor queries.
## Open Questions
- [ ] Should workspace symbols include only project files or also imported stdlib and SDK surfaces?
- [ ] What symbol kinds and ranking should be returned for duplicate names or overloads?
- [ ] Should the symbol index be cached from analysis snapshots or rebuilt per request in the first implementation?
## Options
### Option A - Project source index only
- **Approach:** Index project-owned declarations from analysis snapshots and expose `workspace/symbol`.
- **Pro:** Clear ownership and physical locations.
- **Con:** Does not help users discover stdlib/SDK symbols.
- **Maintainability:** Strong as a first wave.
### Option B - Project plus stdlib index
- **Approach:** Include project declarations and imported stdlib/SDK surfaces in the workspace symbol result set.
- **Pro:** Better discovery, especially for new users.
- **Con:** Requires a navigation policy for stdlib source or virtual targets.
- **Maintainability:** Good if stdlib target handling is solved; awkward otherwise.
## Tradeoffs
Workspace symbols are partly navigation and partly discovery. Including stdlib too early can create broken jumps if target locations are not defined.
## Recommendation
Prefer Option A first, then add stdlib once definition has a target policy.
## Discussion
This should follow document symbols and the semantic location/index work.
## Resolution
Ainda em aberto.
## Next Step
Decide whether first-wave workspace search includes stdlib or project-only symbols.

View File

@ -1,62 +0,0 @@
---
id: AGD-0046
ticket: pbs-lsp-rename-symbol
title: PBS LSP Rename Symbol
status: open
created: 2026-07-15
resolved:
decision:
tags: [studio, lsp, vscode, compiler-pbs, editor, rename]
---
## Pain
Domain owner: `studio/lsp`
PBS users cannot safely rename a symbol through the editor, so they must edit text manually and risk missing references or changing unrelated overloads and same-name declarations.
## Context
Rename requires the same semantic identity and reference machinery needed by definition/references, plus workspace edits and conflict detection. The current LSP does not announce renameProvider.
## Open Questions
- [ ] Which symbols should be renameable in the first wave, and which should be protected such as stdlib, generated, host, or builtin declarations?
- [ ] How should rename handle overloads, member methods, imports, and labels with the same text?
- [ ] What validation should run before returning a WorkspaceEdit?
## Options
### Option A - Local file rename
- **Approach:** Rename only references in the current file using local semantic resolution.
- **Pro:** Smaller blast radius and easier conflict handling.
- **Con:** Incomplete for imported symbols and project-wide APIs.
- **Maintainability:** Acceptable only if explicitly presented as limited.
### Option B - Workspace semantic rename
- **Approach:** Use a project-wide reference index and return a `WorkspaceEdit` for all usage and declaration sites.
- **Pro:** Matches user expectations for editor rename.
- **Con:** Requires reliable references, collision checks, and write-safety across files.
- **Maintainability:** Strong once references exist.
## Tradeoffs
Rename is high trust. A partial or text-based rename can corrupt code silently, so scope must be explicit and conservative.
## Recommendation
Prefer Option B after semantic references are implemented. If delivered earlier, ship only a clearly limited local rename.
## Discussion
Do not implement rename before definition/references are resolved.
## Resolution
Ainda em aberto.
## Next Step
Decide whether rename waits for workspace references or ships a limited local-only wave.

View File

@ -1,62 +0,0 @@
---
id: AGD-0047
ticket: pbs-lsp-code-actions
title: PBS LSP Code Actions and Quick Fixes
status: open
created: 2026-07-15
resolved:
decision:
tags: [studio, lsp, vscode, compiler-pbs, editor, code-actions, quick-fix]
---
## Pain
Domain owner: `studio/lsp`
PBS diagnostics are visible, but users do not get actionable quick fixes for common compiler/editor problems, making simple repairs slower than necessary.
## Context
The LSP maps diagnostics but does not announce codeActionProvider. Compiler diagnostics already carry codes and ranges, but not every diagnostic has enough structured repair data to generate safe edits.
## Open Questions
- [ ] Which diagnostics should receive quick fixes first?
- [ ] Should code actions be produced from diagnostic codes only, or from structured compiler repair hints?
- [ ] How should imports, reserved attributes, missing methods, and invalid Doc shapes be prioritized?
## Options
### Option A - Diagnostic-code quick fixes
- **Approach:** Map known diagnostic codes to simple edits in the LSP layer.
- **Pro:** Fast for a small set of obvious repairs.
- **Con:** Couples fixes to diagnostic text/ranges and can become brittle.
- **Maintainability:** Moderate if limited to trivial edits.
### Option B - Compiler repair hints
- **Approach:** Extend diagnostics with structured repair metadata and let the LSP map hints to code actions.
- **Pro:** Keeps product logic close to compiler knowledge.
- **Con:** Requires a new diagnostic/repair contract.
- **Maintainability:** Strong for non-trivial fixes and future frontends.
## Tradeoffs
Quick fixes are only useful if safe. Simple code-based fixes can ship first, but imports and generated stubs need stronger compiler context.
## Recommendation
Prefer Option B as the target design, with Option A allowed only for a small bootstrap set of unambiguous repairs.
## Discussion
This should follow diagnostics UX and import assistance for the first high-value fixes.
## Resolution
Ainda em aberto.
## Next Step
Pick the first three diagnostic codes eligible for safe quick fixes.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -276,8 +276,32 @@ When a frontend exposes definition or navigation, each returned location MUST id
When a frontend exposes find-references, each returned usage or declaration location MUST identify a compiler-known physical source file. A cursor offset whose definition response is empty MUST produce an empty references response. Host `includeDeclaration` MAY add the canonical declaration location only when that declaration is a compiler-known physical source file; it MUST NOT invent a virtual, untitled, or synthetic location.
When a frontend exposes document symbols or outline, the result MUST be the named declaration tree of the requested document. Presence, hierarchy, and ranges MUST come from that document's recovered syntax tree. Semantic success MUST NOT be required. Semantic enrichment MUST NOT drop a named declaration that the syntax tree still exposes. A missing syntax tree MUST produce an empty list. Document symbols MUST NOT be a workspace symbol index, folding ranges, or a navigation list of foreign files.
When a frontend exposes workspace symbols, the result MUST be a flat search of named declarations whose locations identify compiler-known physical source files of the current analysis. Presence, names, kinds, details, and ranges MUST come from the recovered syntax of those files. Semantic success MUST NOT be required. Semantic enrichment MUST NOT drop a named declaration that the syntax tree still exposes. A missing analysis surface MUST produce an empty list. The workspace-symbol request MUST NOT require an open text document. A declaration without a compiler-known physical file MUST be omitted. Tooling MUST NOT require virtual documents, untitled buffers, or synthetic URIs for this capability. Workspace symbols MUST NOT be a document outline, folding ranges, a persistent symbol index, or a navigation list of virtual locations.
When a frontend exposes rename, the result MUST be a validated workspace edit of identifier spans whose locations identify compiler-known physical source files. Identity MUST be the physical declaration locations that definition returns at the cursor. A local or parameter name span MUST still be renameable when definition at that offset is empty. Each rewritten location MUST identify a compiler-known physical source file. A cursor that is not renameable, including builtins, virtual paths, unresolved names, keywords, comments, strings, import aliases and uses of those aliases, error case labels without their own span, and barrel files, MUST produce a protocol error or a null prepareRename result. Tooling MUST NOT return a successful empty workspace edit for a refused cursor or an invalid new name. The new name MUST be a language identifier that is not a keyword and MUST NOT collide in the same binding space, ignoring the sites being rewritten and the overload set being renamed. The server MUST NOT write files and MUST NOT rename file paths. Host prepareRename MUST advertise a prepareProvider when rename is exposed. Tooling MUST NOT require virtual documents, untitled buffers, or synthetic URIs for this capability.
When a frontend exposes code actions, the result MUST be quick fixes only: one workspace edit tied to a diagnostic of the requested physical document. The frontend MUST produce each repair from that document's syntax. Each repair MUST name the stable diagnostic code, the primary span, and exactly one local text edit. Tooling MUST transport that repair. Tooling MUST NOT choose the edit by parsing the diagnostic message or by a diagnostic-code table. A repair MUST NOT be a required field of compiler diagnostics and MUST NOT be published inside diagnostic data. The published diagnostic MUST carry the stable code so a later code-action request can match it. A repair MUST be returned only when the request context contains a diagnostic with the same code and the same primary range. An empty diagnostic context MUST produce an empty list. The edit MUST change only the requested physical source file. The server MUST NOT write files, MUST NOT return a command, and MUST NOT require a resolve round trip. A missing code-action capability MUST NOT advertise code actions and MUST produce an empty list rather than a protocol error. Virtual, untitled, or synthetic documents MUST NOT receive an edit. Code actions MUST NOT include organize-imports, source actions, refactors, or generated stubs unless a future accepted decision says so.
Compiler diagnostics and editor diagnostics are distinct ownership surfaces. Diagnostics returned by `analyze`, `compile`, and `build` are compiler contract output. Editor diagnostics MAY reuse compiler analysis results, live overlays, caches, or cancellation-aware tooling state, but a frontend MUST NOT be required to provide editor diagnostics in order to compile.
When tooling publishes an editor diagnostic, `source` MUST be the `languageId` of the bound frontend project. `code` MUST be the stable compiler diagnostic code when one exists, and MUST be empty when the compiler diagnostic has none. Tooling MUST NOT copy that code into `source`. Tooling MUST NOT publish related locations, compiler phase, or a repair payload on the editor diagnostic.
When a frontend exposes document links, each link MUST cover the span of a module reference whose resolved destination is a compiler-known regular file. A module reference that does not resolve to a regular file MUST produce no link. Tooling MUST NOT target virtual, untitled, or synthetic URIs, including virtual stdlib documents. Document links MUST NOT replace definition. A missing document-link capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
When a frontend exposes folding ranges, the ranges MUST come from recovered syntax spans for top-level `fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, and `host` declarations, their bodies, and `Doc` text blocks. A parameter list MUST fold from the parenthesis pair. A brace, parenthesis, or text-block delimiter that the recovered tree does not cover MUST still produce a token range, including an unmatched opener through the end of the text. Folding ranges MUST NOT be document symbols.
When a frontend exposes selection ranges, the chain at a cursor MUST run from the inside outward in this order: identifier, argument or parameter list, block or `Doc` text block, declaration. A layer that does not contain the cursor MUST be omitted. A missing folding or selection capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
When a frontend exposes call hierarchy, incoming and outgoing calls MUST be callsites found by walking call expressions in the syntax tree. The references index MUST NOT be used as the call graph. A cursor that is not on a function, method, or constructor MUST produce an empty prepare result. Type hierarchy MUST NOT be advertised. A missing call-hierarchy capability MUST NOT be advertised and MUST produce an empty list rather than a protocol error.
When a frontend exposes completion inside a module reference, the candidates MUST be project and stdlib modules the current module resolver can already name. Completion outside a module reference MUST keep its existing member and general candidates. Completion items MUST stay eager. `resolveProvider` MUST stay false. Snippets and a new ranking score MUST NOT be added.
When a frontend exposes semantic tokens, the lexical token provider remains the baseline. An identifier token MAY be replaced with an existing semantic key when the same resolution used by hover names that symbol and an existing key matches it. Resolution failure, a broken file, or a symbol with no existing key MUST keep the baseline token. This overlay MUST NOT add a semantic-token key.
When a frontend exposes formatting, it MUST be full-document formatting only. Range formatting and on-type formatting MUST NOT be advertised. The formatter MUST reprint the existing token stream. It MUST NOT reorder declarations, join lines, or split lines. Indentation MUST be four spaces and MUST change only where brace or parenthesis nesting changes. The interior of a `Doc` text block MUST be copied unchanged. A comment MUST stay on the line of the token it already follows. A missing formatting capability MUST NOT be advertised and MUST produce an empty edit list rather than a protocol error.
`FrontendSpec` remains the source of static frontend-owned presentation metadata such as semantic vocabularies, host projections, and visual themes. Producing semantic tokens for a live document is an optional editor-facing capability; the existence of static presentation metadata MUST NOT imply that every frontend can provide live semantic-token results.
The frontend registry MUST resolve providers by `languageId`. A lookup for an unknown `languageId` MUST fail explicitly with a diagnostic-friendly error. Unknown languages MUST NOT silently fall back to PBS or to any other frontend.

View File

@ -6,16 +6,36 @@ import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.FrontendSemanticToken;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsEditorialCompletionCandidate;
import p.studio.compiler.pbs.semantics.PbsEditorialDocumentSymbol;
import p.studio.compiler.pbs.semantics.PbsEditorialLocation;
import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol;
import p.studio.compiler.pbs.semantics.PbsEditorialSignatureHelp;
import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind;
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService;
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocument;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.semantics.PbsEditorialRename;
import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol;
import p.studio.compiler.pbs.PbsCallHierarchy;
import p.studio.compiler.pbs.PbsDocumentFormatter;
import p.studio.compiler.pbs.PbsSemanticTokenOverlay;
import p.studio.compiler.pbs.PbsStructureRanges;
import p.studio.compiler.pbs.semantics.PbsQuickFix;
import p.studio.compiler.pbs.semantics.PbsQuickFixCollector;
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
import p.studio.compiler.services.FrontendCallHierarchyItem;
import p.studio.compiler.services.FrontendCallSite;
import p.studio.compiler.services.FrontendCodeAction;
import p.studio.compiler.services.FrontendCompletionCandidate;
import p.studio.compiler.services.FrontendSymbolKind;
import p.studio.compiler.services.FrontendDocumentLink;
import p.studio.compiler.services.FrontendFoldingRange;
import p.studio.compiler.services.FrontendSelectionRange;
import p.studio.compiler.services.FrontendDefinitionLocation;
import p.studio.compiler.services.FrontendDocumentRequest;
import p.studio.compiler.services.FrontendDocumentSymbol;
import p.studio.compiler.services.FrontendRenameEdit;
import p.studio.compiler.services.FrontendRenameResult;
import p.studio.compiler.services.FrontendDocumentation;
import p.studio.compiler.services.FrontendEditorialContext;
import p.studio.compiler.services.FrontendHover;
@ -23,16 +43,20 @@ import p.studio.compiler.services.FrontendLanguageService;
import p.studio.compiler.services.FrontendSignature;
import p.studio.compiler.services.FrontendSignatureHelp;
import p.studio.compiler.services.FrontendSymbolKind;
import p.studio.compiler.services.FrontendWorkspaceRequest;
import p.studio.compiler.services.FrontendWorkspaceSymbol;
import p.studio.compiler.services.PBSFrontendPhaseService;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.tables.FileTableReader;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
@ -53,6 +77,23 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
this.semanticTokenProvider = Objects.requireNonNull(semanticTokenProvider, "semanticTokenProvider");
}
@Override
public Optional<FrontendEditorialContext> prepareEditorialContext(final FrontendPhaseContext phaseContext) {
if (phaseContext == null) {
return Optional.empty();
}
final var semanticReadSurface = PBSFrontendPhaseService.semanticReadSurface(
phaseContext,
DiagnosticSink.empty(),
BuildingIssueSink.empty());
return Optional.of(new FrontendEditorialContext(
null,
ReadOnlyList.empty(),
phaseContext.feSurfaceContext(),
phaseContext.fileTable,
semanticReadSurface));
}
@Override
public Optional<FrontendEditorialContext> prepareEditorialContext(
final FrontendPhaseContext phaseContext,
@ -98,6 +139,10 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return List.of();
}
if (insideModuleRef(ast, offset)
&& editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface) {
return moduleCompletions(surface.moduleCompletionLabels());
}
@SuppressWarnings("unchecked")
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
@ -267,15 +312,404 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return List.copyOf(mapped.values());
}
@Override
public List<FrontendDocumentSymbol> documentSymbols(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
if (editorialContext == null || !(editorialContext.syntaxTree() instanceof PbsAst.File ast)) {
return List.of();
}
return editorialSupportService.documentSymbols(ast).stream()
.map(this::toFrontendDocumentSymbol)
.toList();
}
@Override
public List<FrontendWorkspaceSymbol> workspaceSymbols(
final FrontendWorkspaceRequest request,
final FrontendEditorialContext editorialContext) {
if (request == null
|| editorialContext == null
|| !(editorialContext.sourceFiles() instanceof FileTableReader fileTable)
|| !(editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface)
|| surface.astByFile() == null
|| surface.astByFile().isEmpty()) {
return List.of();
}
final ArrayList<FrontendWorkspaceSymbol> symbols = new ArrayList<>();
for (final var entry : surface.astByFile().entrySet()) {
final Path path = pathForRegularSource(entry.getKey(), fileTable);
if (path == null) {
continue;
}
final PbsAst.File fileAst = entry.getValue();
if (fileAst == null) {
continue;
}
for (final PbsEditorialWorkspaceSymbol symbol : editorialSupportService.workspaceSymbols(
fileAst,
request.query())) {
toFrontendWorkspaceSymbol(symbol, path).ifPresent(symbols::add);
}
}
symbols.sort(Comparator
.comparing(FrontendWorkspaceSymbol::name, String.CASE_INSENSITIVE_ORDER)
.thenComparing(symbol -> symbol.path().toString())
.thenComparingInt(FrontendWorkspaceSymbol::startOffset));
return List.copyOf(symbols);
}
@Override
public Optional<FrontendDefinitionLocation> prepareRename(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset) {
final PbsEditorialRename planned = editorialRename(request, editorialContext, offset, null);
if (planned == null || !planned.allowed()) {
return Optional.empty();
}
if (!(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
return Optional.empty();
}
if (physicalIdentity(planned, fileTable).isEmpty()) {
return Optional.empty();
}
return toFrontendLocation(planned.placeholder(), fileTable);
}
@Override
public FrontendRenameResult rename(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset,
final String newName) {
if (newName == null) {
return FrontendRenameResult.refused("invalid identifier");
}
final PbsEditorialRename planned = editorialRename(request, editorialContext, offset, newName);
if (planned == null) {
return FrontendRenameResult.refused("not renameable");
}
if (!planned.allowed()) {
return FrontendRenameResult.refused(planned.refusal());
}
if (!(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
return FrontendRenameResult.refused("not renameable");
}
if (physicalIdentity(planned, fileTable).isEmpty()) {
return FrontendRenameResult.refused("not renameable");
}
final LinkedHashMap<String, FrontendRenameEdit> edits = new LinkedHashMap<>();
for (final PbsEditorialLocation site : planned.sites()) {
toFrontendLocation(site, fileTable).ifPresent(location ->
edits.putIfAbsent(
frontendLocationKey(location),
new FrontendRenameEdit(
location.path(),
location.startOffset(),
location.endOffset(),
newName)));
}
if (edits.isEmpty()) {
return FrontendRenameResult.refused("not renameable");
}
return FrontendRenameResult.applied(List.copyOf(edits.values()));
}
@Override
public boolean documentLinksSupported() {
return true;
}
@Override
public List<FrontendDocumentLink> documentLinks(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
if (request == null
|| editorialContext == null
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|| !(editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface)
|| surface.physicalModuleFileByReference() == null
|| ast.imports() == null) {
return List.of();
}
final ArrayList<FrontendDocumentLink> links = new ArrayList<>();
for (final PbsAst.ImportDecl importDecl : ast.imports()) {
if (importDecl == null || importDecl.moduleRef() == null) {
continue;
}
final PbsAst.ModuleRef moduleRef = importDecl.moduleRef();
final Span span = moduleRef.span();
if (span == null || span.isNone() || moduleRef.project() == null || moduleRef.project().isBlank()) {
continue;
}
final Path target = surface.physicalModuleFileByReference().get(
new ModuleReference(moduleRef.project(), moduleRef.pathSegments()));
if (target == null) {
continue;
}
links.add(new FrontendDocumentLink(
target,
toOffset(span.getStart()),
toOffset(span.getEnd())));
}
return List.copyOf(links);
}
@Override
public boolean foldingRangesSupported() {
return true;
}
@Override
public List<FrontendFoldingRange> foldingRanges(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
return PbsStructureRanges.foldingRanges(request == null || request.documentText() == null
? ""
: request.documentText());
}
@Override
public boolean selectionRangesSupported() {
return true;
}
@Override
public List<FrontendSelectionRange> selectionRanges(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final List<Integer> offsets) {
return PbsStructureRanges.selectionRanges(
request == null || request.documentText() == null ? "" : request.documentText(),
offsets);
}
@Override
public boolean callHierarchySupported() {
return true;
}
@Override
public List<FrontendCallHierarchyItem> prepareCallHierarchy(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset) {
final PbsAst.File ast = callHierarchyAst(editorialContext);
if (ast == null) {
return List.of();
}
return PbsCallHierarchy.prepare(ast, offset).stream().map(this::toCallItem).toList();
}
@Override
public List<FrontendCallSite> incomingCalls(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final FrontendCallHierarchyItem target) {
return callSites(callHierarchyAst(editorialContext), target, true);
}
@Override
public List<FrontendCallSite> outgoingCalls(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final FrontendCallHierarchyItem target) {
return callSites(callHierarchyAst(editorialContext), target, false);
}
private List<FrontendCallSite> callSites(
final PbsAst.File ast,
final FrontendCallHierarchyItem target,
final boolean incoming) {
if (ast == null || target == null) {
return List.of();
}
final PbsCallHierarchy.Callable callable = new PbsCallHierarchy.Callable(
target.name(),
target.method(),
target.startOffset(),
target.endOffset());
final var calls = incoming
? PbsCallHierarchy.incomingCalls(ast, callable)
: PbsCallHierarchy.outgoingCalls(ast, callable);
return calls.stream()
.map(call -> new FrontendCallSite(
toCallItem(incoming ? call.caller() : call.callee()),
call.startOffset(),
call.endOffset()))
.toList();
}
private static PbsAst.File callHierarchyAst(final FrontendEditorialContext editorialContext) {
if (editorialContext == null || !(editorialContext.syntaxTree() instanceof PbsAst.File ast)) {
return null;
}
return ast;
}
private FrontendCallHierarchyItem toCallItem(final PbsCallHierarchy.Callable callable) {
return new FrontendCallHierarchyItem(
callable.name(),
callable.method(),
callable.startOffset(),
callable.endOffset());
}
@Override
public boolean formattingSupported() {
return true;
}
@Override
public Optional<String> formatDocument(final FrontendDocumentRequest request) {
return Optional.of(PbsDocumentFormatter.format(
request == null || request.documentText() == null ? "" : request.documentText()));
}
@Override
public boolean codeActionsSupported() {
return true;
}
@Override
public List<FrontendCodeAction> codeActions(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
if (request == null
|| editorialContext == null
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|| !(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
return List.of();
}
final FileId fileId = ast.span() == null ? null : ast.span().getFileId();
final Path path = pathForRegularSource(fileId, fileTable);
if (path == null || !path.equals(request.documentPath().toAbsolutePath().normalize())) {
return List.of();
}
final boolean interfaceModule = editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface
&& surface.sourceKindByFile() != null
&& surface.sourceKindByFile().get(fileId) == SourceKind.SDK_INTERFACE;
final ArrayList<FrontendCodeAction> actions = new ArrayList<>();
for (final PbsQuickFix repair : PbsQuickFixCollector.collect(ast, interfaceModule)) {
actions.add(new FrontendCodeAction(
repair.title(),
repair.diagnosticCode(),
path,
repair.startOffset(),
repair.endOffset(),
repair.newText()));
}
return List.copyOf(actions);
}
private PbsEditorialRename editorialRename(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset,
final String newName) {
if (request == null
|| editorialContext == null
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|| !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList<?> supplementalTopDecls)
|| !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext)
|| !(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
return null;
}
@SuppressWarnings("unchecked")
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
final EditorialDocument cursorDocument = new EditorialDocument(
request.documentText(),
ast,
typedSupplementalTopDecls,
feSurfaceContext);
return editorialSupportService.rename(
cursorDocument,
offset,
newName,
projectDocuments(request, editorialContext, cursorDocument, fileTable));
}
private List<FrontendDefinitionLocation> physicalIdentity(
final PbsEditorialRename planned,
final FileTableReader fileTable) {
final ArrayList<FrontendDefinitionLocation> locations = new ArrayList<>();
for (final PbsEditorialLocation location : planned.identity()) {
toFrontendLocation(location, fileTable).ifPresent(locations::add);
}
return List.copyOf(locations);
}
@Override
public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
return semanticTokens(request == null ? "" : request.documentText());
return semanticTokens(request, null);
}
@Override
public List<FrontendSemanticToken> semanticTokens(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
final String text = request == null || request.documentText() == null ? "" : request.documentText();
final List<FrontendSemanticToken> baseline = semanticTokenProvider.tokenize(text);
if (editorialContext == null
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|| !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList<?> supplementalTopDecls)
|| !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext)) {
return baseline;
}
@SuppressWarnings("unchecked")
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
return PbsSemanticTokenOverlay.apply(
baseline,
text,
ast,
typedSupplementalTopDecls,
feSurfaceContext,
editorialSupportService);
}
public List<FrontendSemanticToken> semanticTokens(final String documentText) {
return semanticTokenProvider.tokenize(documentText == null ? "" : documentText);
}
private static boolean insideModuleRef(final PbsAst.File ast, final int offset) {
if (ast == null || ast.imports() == null || offset < 0) {
return false;
}
for (final PbsAst.ImportDecl importDecl : ast.imports()) {
if (importDecl == null || importDecl.moduleRef() == null || importDecl.moduleRef().span() == null) {
continue;
}
final long start = importDecl.moduleRef().span().getStart();
final long end = importDecl.moduleRef().span().getEnd();
if (end > start && start <= offset && offset <= end) {
return true;
}
}
return false;
}
private static List<FrontendCompletionCandidate> moduleCompletions(final List<String> labels) {
if (labels == null || labels.isEmpty()) {
return List.of();
}
final ArrayList<FrontendCompletionCandidate> candidates = new ArrayList<>();
for (final String label : labels) {
if (label == null || label.isBlank()) {
continue;
}
candidates.add(new FrontendCompletionCandidate(
label,
FrontendSymbolKind.MODULE,
"module",
"",
""));
}
return List.copyOf(candidates);
}
private FrontendCompletionCandidate toFrontendCompletionCandidate(final PbsEditorialCompletionCandidate candidate) {
return new FrontendCompletionCandidate(
candidate.label(),
@ -314,6 +748,36 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return FrontendSymbolKind.valueOf(kind.name());
}
private Optional<FrontendWorkspaceSymbol> toFrontendWorkspaceSymbol(
final PbsEditorialWorkspaceSymbol symbol,
final Path path) {
if (symbol == null || path == null || symbol.span() == null || symbol.span().isNone()) {
return Optional.empty();
}
return Optional.of(new FrontendWorkspaceSymbol(
symbol.name(),
toFrontendSymbolKind(symbol.kind()),
symbol.detail(),
symbol.containerName(),
path,
toOffset(symbol.span().getStart()),
toOffset(symbol.span().getEnd())));
}
private FrontendDocumentSymbol toFrontendDocumentSymbol(final PbsEditorialDocumentSymbol symbol) {
final int start = toOffset(symbol.span().getStart());
final int end = toOffset(symbol.span().getEnd());
return new FrontendDocumentSymbol(
symbol.name(),
toFrontendSymbolKind(symbol.kind()),
symbol.detail(),
start,
end,
start,
end,
symbol.children().stream().map(this::toFrontendDocumentSymbol).toList());
}
private List<EditorialDocument> projectDocuments(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
@ -348,6 +812,15 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
return List.copyOf(documents);
}
private Path pathForRegularSource(
final FileId fileId,
final FileTableReader fileTable) {
if (!isRegularSource(fileId, fileTable)) {
return null;
}
return fileTable.get(fileId).getCanonPath().toAbsolutePath().normalize();
}
private boolean isRegularSource(
final FileId fileId,
final FileTableReader fileTable) {

View File

@ -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);
}
}

View File

@ -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) {
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -46,6 +46,32 @@ public final class PbsLexer {
return lexer.lexInternal();
}
public static boolean isKeyword(final String text) {
return text != null && KEYWORDS.containsKey(text);
}
public static boolean isIdentifierLexeme(final String text) {
if (text == null || text.isEmpty() || isKeyword(text)) {
return false;
}
int index = 0;
final int first = text.codePointAt(0);
if (!(first == '_' || Character.isAlphabetic(first))) {
return false;
}
index += Character.charCount(first);
while (index < text.length()) {
final int codePoint = text.codePointAt(index);
if (!(codePoint == '_'
|| Character.isAlphabetic(codePoint)
|| Character.isDigit(codePoint))) {
return false;
}
index += Character.charCount(codePoint);
}
return true;
}
private ReadOnlyList<PbsToken> lexInternal() {
while (!isAtEnd() || state != LexerState.DEFAULT) {
switch (state) {

View File

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

View File

@ -0,0 +1,55 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
final class PbsAstWorkspaceSymbols {
private PbsAstWorkspaceSymbols() {
}
static List<PbsEditorialWorkspaceSymbol> list(
final PbsAst.File ast,
final String query) {
if (ast == null) {
return List.of();
}
final ArrayList<PbsEditorialWorkspaceSymbol> symbols = new ArrayList<>();
for (final PbsEditorialDocumentSymbol topDecl : PbsAstDocumentSymbols.list(ast)) {
flatten(topDecl, "", query, symbols);
}
return List.copyOf(symbols);
}
private static void flatten(
final PbsEditorialDocumentSymbol symbol,
final String containerName,
final String query,
final ArrayList<PbsEditorialWorkspaceSymbol> symbols) {
if (matches(symbol.name(), query)) {
symbols.add(new PbsEditorialWorkspaceSymbol(
symbol.name(),
symbol.kind(),
symbol.detail(),
containerName,
symbol.span()));
}
for (final PbsEditorialDocumentSymbol child : symbol.children()) {
flatten(child, symbol.name(), query, symbols);
}
}
static boolean matches(
final String name,
final String query) {
if (name == null || name.isBlank()) {
return false;
}
if (query == null || query.isBlank()) {
return true;
}
return name.toLowerCase(Locale.ROOT).contains(query.trim().toLowerCase(Locale.ROOT));
}
}

View File

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

View File

@ -0,0 +1,40 @@
package p.studio.compiler.pbs.semantics;
import java.util.List;
import java.util.Objects;
public record PbsEditorialRename(
String refusal,
PbsEditorialLocation placeholder,
List<PbsEditorialLocation> identity,
List<PbsEditorialLocation> sites) {
public PbsEditorialRename {
refusal = refusal == null ? "" : refusal;
identity = identity == null ? List.of() : List.copyOf(identity);
sites = sites == null ? List.of() : List.copyOf(sites);
}
public static PbsEditorialRename refused(final String reason) {
return new PbsEditorialRename(
reason == null || reason.isBlank() ? "not renameable" : reason,
null,
List.of(),
List.of());
}
public static PbsEditorialRename applied(
final PbsEditorialLocation placeholder,
final List<PbsEditorialLocation> identity,
final List<PbsEditorialLocation> sites) {
return new PbsEditorialRename(
"",
Objects.requireNonNull(placeholder, "placeholder"),
identity,
sites);
}
public boolean allowed() {
return refusal.isBlank() && placeholder != null && !identity.isEmpty() && !sites.isEmpty();
}
}

View File

@ -74,6 +74,16 @@ public final class PbsEditorialSupportService {
return context.resolveSymbolHover(token.lexeme(), tokenIndex, scopeState);
}
public List<PbsEditorialDocumentSymbol> documentSymbols(final PbsAst.File ast) {
return PbsAstDocumentSymbols.list(ast);
}
public List<PbsEditorialWorkspaceSymbol> workspaceSymbols(
final PbsAst.File ast,
final String query) {
return PbsAstWorkspaceSymbols.list(ast, query);
}
public List<PbsEditorialLocation> definition(
final String sourceText,
final PbsAst.File ast,
@ -145,6 +155,63 @@ public final class PbsEditorialSupportService {
return List.copyOf(usages.values());
}
public Optional<PbsEditorialLocation> prepareRename(
final String sourceText,
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final int offset) {
return prepareRename(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset);
}
public Optional<PbsEditorialLocation> prepareRename(
final String sourceText,
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final FESurfaceContext feSurfaceContext,
final int offset) {
final EditorialDocument cursor = new EditorialDocument(sourceText, ast, supplementalTopDecls, feSurfaceContext);
return prepareRename(cursor, offset, List.of(cursor));
}
public Optional<PbsEditorialLocation> prepareRename(
final EditorialDocument cursorDocument,
final int offset,
final List<EditorialDocument> projectDocuments) {
final PbsEditorialRename planned = planRename(cursorDocument, offset, projectDocuments, null);
if (!planned.allowed()) {
return Optional.empty();
}
return Optional.of(planned.placeholder());
}
public PbsEditorialRename rename(
final String sourceText,
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final int offset,
final String newName) {
return rename(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset, newName);
}
public PbsEditorialRename rename(
final String sourceText,
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final FESurfaceContext feSurfaceContext,
final int offset,
final String newName) {
final EditorialDocument cursor = new EditorialDocument(sourceText, ast, supplementalTopDecls, feSurfaceContext);
return rename(cursor, offset, newName, List.of(cursor));
}
public PbsEditorialRename rename(
final EditorialDocument cursorDocument,
final int offset,
final String newName,
final List<EditorialDocument> projectDocuments) {
return planRename(cursorDocument, offset, projectDocuments, newName);
}
public record EditorialDocument(
String sourceText,
PbsAst.File ast,
@ -192,6 +259,76 @@ public final class PbsEditorialSupportService {
return Optional.of(new PbsEditorialSignatureHelp(signatures, 0, callSite.activeParameter()));
}
private PbsEditorialRename planRename(
final EditorialDocument cursorDocument,
final int offset,
final List<EditorialDocument> projectDocuments,
final String newName) {
if (cursorDocument == null || cursorDocument.ast() == null) {
return PbsEditorialRename.refused("not renameable");
}
final DocumentContext cursor = buildContext(
cursorDocument.sourceText(),
cursorDocument.ast(),
cursorDocument.supplementalTopDecls(),
cursorDocument.feSurfaceContext());
final RenameCursor renameCursor = cursor.renameCursorAt(offset);
if (renameCursor.refusal() != null) {
return PbsEditorialRename.refused(renameCursor.refusal());
}
if (newName != null) {
if (!PbsLexer.isIdentifierLexeme(newName)) {
return PbsEditorialRename.refused("invalid identifier");
}
if (cursor.collides(newName, renameCursor)) {
return PbsEditorialRename.refused("name collision");
}
}
final List<EditorialDocument> documents = projectDocuments == null || projectDocuments.isEmpty()
? List.of(cursorDocument)
: projectDocuments;
final LinkedHashMap<String, PbsEditorialLocation> sites = new LinkedHashMap<>();
sites.putIfAbsent(locationKey(renameCursor.placeholder().span()), renameCursor.placeholder());
final Set<String> identityKeys = locationKeys(renameCursor.identity());
for (final EditorialDocument document : documents) {
if (document == null || document.ast() == null) {
continue;
}
final DocumentContext documentContext = buildContext(
document.sourceText(),
document.ast(),
document.supplementalTopDecls(),
document.feSurfaceContext());
for (final PbsEditorialLocation site : documentContext.renameSitesMatching(
identityKeys,
renameCursor.originalName())) {
sites.putIfAbsent(locationKey(site.span()), site);
}
final FileId documentFileId = document.ast().span() == null
? FileId.none()
: document.ast().span().getFileId();
for (final PbsEditorialLocation location : renameCursor.identity()) {
if (location == null || location.span() == null || location.span().isNone()) {
continue;
}
if (location.span().length() != renameCursor.originalName().length()) {
continue;
}
if (location.span().getFileId() == null || !location.span().getFileId().equals(documentFileId)) {
continue;
}
sites.putIfAbsent(locationKey(location.span()), location);
}
}
if (sites.isEmpty()) {
return PbsEditorialRename.refused("not renameable");
}
return PbsEditorialRename.applied(
renameCursor.placeholder(),
renameCursor.identity(),
List.copyOf(sites.values()));
}
private DocumentContext buildContext(
final String sourceText,
final PbsAst.File ast,
@ -1489,6 +1626,332 @@ public final class PbsEditorialSupportService {
throw new IndexOutOfBoundsException("No significant token at index " + significantIndex);
}
private RenameCursor renameCursorAt(final int offset) {
final Integer tokenIndex = identifierTokenIndexAt(offset);
if (tokenIndex == null) {
return RenameCursor.refused("not an identifier");
}
final PbsToken token = token(tokenIndex);
final String lexeme = token.lexeme();
if (isImportAliasLexeme(lexeme)) {
return RenameCursor.refused("import alias");
}
List<PbsEditorialLocation> identity = definitionAt(offset);
if (identity.isEmpty()) {
identity = localLocations(lexeme, offset);
}
if (identity.isEmpty()) {
identity = parameterLocations(lexeme, offset);
}
if (isErrorCaseLabel(lexeme, offset, identity)) {
return RenameCursor.refused("error case label");
}
if (identity.isEmpty()) {
return RenameCursor.refused("not renameable");
}
final FileId fileId = ast.span() == null ? FileId.none() : ast.span().getFileId();
final Span placeholderSpan = new Span(fileId, token.start(), token.end());
if (!hasPhysicalSpan(placeholderSpan)) {
return RenameCursor.refused("not renameable");
}
return new RenameCursor(
lexeme,
new PbsEditorialLocation(placeholderSpan),
identity,
null);
}
private List<PbsEditorialLocation> renameSitesMatching(
final Set<String> identityKeys,
final String originalName) {
if (identityKeys == null || identityKeys.isEmpty() || originalName == null || originalName.isBlank()) {
return List.of();
}
final FileId fileId = ast.span() == null ? FileId.none() : ast.span().getFileId();
final LinkedHashMap<String, PbsEditorialLocation> sites = new LinkedHashMap<>();
for (final PbsToken token : tokens) {
if (token.kind() != PbsTokenKind.IDENTIFIER || !originalName.equals(token.lexeme())) {
continue;
}
if (isImportAliasLexeme(token.lexeme())) {
continue;
}
if (!intersects(definitionAt(token.start()), identityKeys)) {
continue;
}
final Span usageSpan = new Span(fileId, token.start(), token.end());
if (!hasPhysicalSpan(usageSpan)) {
continue;
}
sites.putIfAbsent(locationKey(usageSpan), new PbsEditorialLocation(usageSpan));
}
return List.copyOf(sites.values());
}
private boolean isImportAliasLexeme(final String lexeme) {
if (lexeme == null || lexeme.isBlank()) {
return false;
}
for (final var importDecl : ast.imports()) {
for (final var item : importDecl.items()) {
if (item.alias() == null || item.alias().isBlank()) {
continue;
}
if (lexeme.equals(item.alias()) && !item.alias().equals(item.name())) {
return true;
}
}
}
return false;
}
private boolean isErrorCaseLabel(
final String lexeme,
final int offset,
final List<PbsEditorialLocation> identity) {
if (lexeme == null || lexeme.isBlank()) {
return false;
}
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.ErrorDecl errorDecl)) {
continue;
}
if (lexeme.equals(errorDecl.name()) || !errorDecl.cases().asList().contains(lexeme)) {
continue;
}
if (errorDecl.span() != null && errorDecl.span().contains(offset)) {
return true;
}
final String errorKey = locationKey(errorDecl.span());
for (final PbsEditorialLocation location : identity) {
if (location != null && location.span() != null && errorKey.equals(locationKey(location.span()))) {
return true;
}
}
}
return false;
}
private boolean collides(
final String newName,
final RenameCursor renameCursor) {
if (newName == null || newName.equals(renameCursor.originalName())) {
return false;
}
if (isLocalOrParameterIdentity(renameCursor)) {
return localBindingNames((int) renameCursor.placeholder().span().getStart()).contains(newName);
}
if (isStructFieldIdentity(renameCursor)) {
return structFieldNames(renameCursor).contains(newName);
}
if (isMethodIdentity(renameCursor)) {
return methodNames(renameCursor).contains(newName);
}
if (isEnumCaseIdentity(renameCursor)) {
return enumCaseNames(renameCursor).contains(newName);
}
return topLevelBindingNames().contains(newName);
}
private boolean isLocalOrParameterIdentity(final RenameCursor renameCursor) {
final Set<String> keys = locationKeys(renameCursor.identity());
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.FunctionDecl functionDecl) || functionDecl.body() == null) {
continue;
}
for (final var parameter : functionDecl.parameters()) {
if (keys.contains(locationKey(parameter.span()))) {
return true;
}
}
if (containsLetNameSpan(functionDecl.body(), keys)) {
return true;
}
}
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
continue;
}
for (final var method : structDecl.methods()) {
if (method.body() == null) {
continue;
}
for (final var parameter : method.parameters()) {
if (keys.contains(locationKey(parameter.span()))) {
return true;
}
}
if (containsLetNameSpan(method.body(), keys)) {
return true;
}
}
}
return false;
}
private boolean containsLetNameSpan(
final PbsAst.Block block,
final Set<String> keys) {
if (block == null) {
return false;
}
for (final var statement : block.statements()) {
if (statement instanceof PbsAst.LetStatement letStatement
&& letStatement.nameSpan() != null
&& keys.contains(locationKey(letStatement.nameSpan()))) {
return true;
}
if (statement instanceof PbsAst.IfStatement ifStatement) {
if (containsLetNameSpan(ifStatement.thenBlock(), keys)
|| containsLetNameSpan(ifStatement.elseBlock(), keys)) {
return true;
}
} else if (statement instanceof PbsAst.ForStatement forStatement) {
if (containsLetNameSpan(forStatement.body(), keys)) {
return true;
}
} else if (statement instanceof PbsAst.WhileStatement whileStatement
&& containsLetNameSpan(whileStatement.body(), keys)) {
return true;
}
}
return false;
}
private Set<String> localBindingNames(final int offset) {
final LinkedHashSet<String> names = new LinkedHashSet<>();
final CallableFrame frame = callableFrameAt(offset);
if (frame == null) {
return Set.of();
}
names.addAll(frame.parameterNames());
collectLetNames(frame.body(), names);
return names;
}
private void collectLetNames(
final PbsAst.Block block,
final Set<String> names) {
if (block == null) {
return;
}
for (final var statement : block.statements()) {
if (statement instanceof PbsAst.LetStatement letStatement) {
names.add(letStatement.name());
} else if (statement instanceof PbsAst.IfStatement ifStatement) {
collectLetNames(ifStatement.thenBlock(), names);
collectLetNames(ifStatement.elseBlock(), names);
} else if (statement instanceof PbsAst.ForStatement forStatement) {
names.add(forStatement.iteratorName());
collectLetNames(forStatement.body(), names);
} else if (statement instanceof PbsAst.WhileStatement whileStatement) {
collectLetNames(whileStatement.body(), names);
}
}
}
private boolean isStructFieldIdentity(final RenameCursor renameCursor) {
return !structFieldNames(renameCursor).isEmpty()
&& structFieldIdentityMatch(renameCursor);
}
private boolean structFieldIdentityMatch(final RenameCursor renameCursor) {
final Set<String> keys = locationKeys(renameCursor.identity());
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
continue;
}
for (final var field : structDecl.fields()) {
if (keys.contains(locationKey(field.span()))) {
return true;
}
}
}
return false;
}
private Set<String> structFieldNames(final RenameCursor renameCursor) {
final Set<String> keys = locationKeys(renameCursor.identity());
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
continue;
}
for (final var field : structDecl.fields()) {
if (keys.contains(locationKey(field.span()))) {
final LinkedHashSet<String> names = new LinkedHashSet<>();
for (final var sibling : structDecl.fields()) {
names.add(sibling.name());
}
return names;
}
}
}
return Set.of();
}
private boolean isMethodIdentity(final RenameCursor renameCursor) {
return !methodNames(renameCursor).isEmpty();
}
private Set<String> methodNames(final RenameCursor renameCursor) {
final Set<String> keys = locationKeys(renameCursor.identity());
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
continue;
}
for (final var method : structDecl.methods()) {
if (keys.contains(locationKey(method.span()))) {
final LinkedHashSet<String> names = new LinkedHashSet<>();
for (final var sibling : structDecl.methods()) {
names.add(sibling.name());
}
return names;
}
}
}
return Set.of();
}
private boolean isEnumCaseIdentity(final RenameCursor renameCursor) {
return !enumCaseNames(renameCursor).isEmpty();
}
private Set<String> enumCaseNames(final RenameCursor renameCursor) {
final Set<String> keys = locationKeys(renameCursor.identity());
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.EnumDecl enumDecl)) {
continue;
}
for (final var enumCase : enumDecl.cases()) {
if (keys.contains(locationKey(enumCase.span()))) {
final LinkedHashSet<String> names = new LinkedHashSet<>();
for (final var sibling : enumDecl.cases()) {
names.add(sibling.name());
}
return names;
}
}
}
return Set.of();
}
private Set<String> topLevelBindingNames() {
final LinkedHashSet<String> names = new LinkedHashSet<>();
for (final var topDecl : ast.topDecls()) {
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
names.add(functionDecl.name());
} else if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
names.add(serviceDecl.name());
} else if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
names.add(globalDecl.name());
} else if (topDecl instanceof PbsAst.ConstDecl constDecl) {
names.add(constDecl.name());
}
}
names.addAll(importedSymbols.keySet());
return names;
}
private PbsToken token(final int index) {
return tokens.get(index);
}
@ -1697,6 +2160,17 @@ public final class PbsEditorialSupportService {
String origin) {
}
private record RenameCursor(
String originalName,
PbsEditorialLocation placeholder,
List<PbsEditorialLocation> identity,
String refusal) {
private static RenameCursor refused(final String reason) {
return new RenameCursor("", null, List.of(), reason);
}
}
private record CallableFrame(
ReadOnlyList<PbsAst.Parameter> parameters,
PbsAst.Block body,

View File

@ -0,0 +1,21 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.source.Span;
import java.util.Objects;
public record PbsEditorialWorkspaceSymbol(
String name,
PbsEditorialSymbolKind kind,
String detail,
String containerName,
Span span) {
public PbsEditorialWorkspaceSymbol {
name = Objects.requireNonNull(name, "name").trim();
kind = Objects.requireNonNull(kind, "kind");
detail = detail == null ? "" : detail;
containerName = containerName == null ? "" : containerName.trim();
span = Objects.requireNonNull(span, "span");
}
}

View File

@ -0,0 +1,33 @@
package p.studio.compiler.pbs.semantics;
import java.util.Objects;
public record PbsQuickFix(
String diagnosticCode,
int startOffset,
int endOffset,
String newText,
String title) {
public PbsQuickFix {
diagnosticCode = requireText(diagnosticCode, "diagnosticCode");
title = requireText(title, "title");
if (startOffset < 0) {
throw new IllegalArgumentException("startOffset must not be negative");
}
if (endOffset < startOffset) {
throw new IllegalArgumentException("endOffset must not be before startOffset");
}
newText = Objects.requireNonNull(newText, "newText");
}
private static String requireText(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}

View File

@ -0,0 +1,148 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.Span;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class PbsQuickFixCollector {
static final String REMOVE_DUPLICATE_TITLE = "Remove duplicate reserved attribute";
static final String REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE = "Remove InitAllowed arguments";
private static final String INIT_ALLOWED_REPLACEMENT = "[InitAllowed]";
private PbsQuickFixCollector() {
}
public static List<PbsQuickFix> collect(
final PbsAst.File ast,
final boolean interfaceModule) {
if (ast == null) {
return List.of();
}
final ArrayList<PbsQuickFix> repairs = new ArrayList<>();
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
switch (topDecl) {
case PbsAst.FunctionDecl functionDecl -> duplicateDoc(functionDecl.attributes(), repairs);
case PbsAst.StructDecl structDecl -> {
duplicateDoc(structDecl.attributes(), repairs);
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
duplicateDoc(method.attributes(), repairs);
}
}
case PbsAst.ServiceDecl serviceDecl -> {
duplicateDoc(serviceDecl.attributes(), repairs);
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
duplicateDoc(method.attributes(), repairs);
}
}
case PbsAst.ContractDecl contractDecl -> {
duplicateDoc(contractDecl.attributes(), repairs);
if (interfaceModule) {
for (final PbsAst.FunctionSignature signature : contractDecl.signatures()) {
duplicateDoc(signature.attributes(), repairs);
}
}
}
case PbsAst.HostDecl hostDecl -> {
duplicateDoc(hostDecl.attributes(), repairs);
if (interfaceModule) {
for (final PbsAst.FunctionSignature signature : hostDecl.signatures()) {
collectHostSignature(signature, repairs);
}
}
}
case PbsAst.BuiltinTypeDecl builtinTypeDecl -> {
if (interfaceModule) {
duplicateDoc(builtinTypeDecl.attributes(), repairs);
duplicateNamed(builtinTypeDecl.attributes(), "BuiltinType", repairs);
for (final PbsAst.FunctionSignature signature : builtinTypeDecl.signatures()) {
duplicateDoc(signature.attributes(), repairs);
duplicateNamed(signature.attributes(), "IntrinsicCall", repairs);
}
}
}
case PbsAst.ErrorDecl errorDecl -> duplicateDoc(errorDecl.attributes(), repairs);
case PbsAst.EnumDecl enumDecl -> duplicateDoc(enumDecl.attributes(), repairs);
case PbsAst.CallbackDecl callbackDecl -> duplicateDoc(callbackDecl.attributes(), repairs);
case PbsAst.ConstDecl constDecl -> {
duplicateDoc(constDecl.attributes(), repairs);
duplicateNamed(constDecl.attributes(), "BuiltinConst", repairs);
}
default -> {
}
}
}
repairs.sort(Comparator
.comparingInt(PbsQuickFix::startOffset)
.thenComparingInt(PbsQuickFix::endOffset)
.thenComparing(PbsQuickFix::diagnosticCode));
return List.copyOf(repairs);
}
private static void collectHostSignature(
final PbsAst.FunctionSignature signature,
final List<PbsQuickFix> repairs) {
duplicateDoc(signature.attributes(), repairs);
duplicateNamed(signature.attributes(), "Host", repairs);
duplicateNamed(signature.attributes(), "InitAllowed", repairs);
duplicateNamed(signature.attributes(), "AssetLowering", repairs);
final List<PbsAst.Attribute> initAllowed = named(signature.attributes(), "InitAllowed");
if (!initAllowed.isEmpty() && !initAllowed.getFirst().arguments().isEmpty()) {
add(repairs, initAllowed.getFirst(), PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
INIT_ALLOWED_REPLACEMENT, REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE);
}
}
private static void duplicateDoc(
final ReadOnlyList<PbsAst.Attribute> attributes,
final List<PbsQuickFix> repairs) {
duplicateNamed(attributes, "Doc", repairs);
}
private static void duplicateNamed(
final ReadOnlyList<PbsAst.Attribute> attributes,
final String name,
final List<PbsQuickFix> repairs) {
final List<PbsAst.Attribute> matches = named(attributes, name);
for (int index = 1; index < matches.size(); index++) {
add(repairs, matches.get(index), PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name(),
"", REMOVE_DUPLICATE_TITLE);
}
}
private static List<PbsAst.Attribute> named(
final ReadOnlyList<PbsAst.Attribute> attributes,
final String name) {
final ArrayList<PbsAst.Attribute> matches = new ArrayList<>();
if (attributes == null) {
return matches;
}
for (final PbsAst.Attribute attribute : attributes) {
if (name.equals(attribute.name())) {
matches.add(attribute);
}
}
return matches;
}
private static void add(
final List<PbsQuickFix> repairs,
final PbsAst.Attribute attribute,
final String code,
final String newText,
final String title) {
if (attribute == null || attribute.span() == null || attribute.span().isNone()) {
return;
}
final Span span = attribute.span();
final long start = span.getStart();
final long end = span.getEnd();
if (start < 0 || end < start || end > Integer.MAX_VALUE) {
return;
}
repairs.add(new PbsQuickFix(code, (int) start, (int) end, newText, title));
}
}

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -5,6 +5,7 @@ import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.PbsReservedMetadataExtractor;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator;
@ -14,9 +15,12 @@ import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@Slf4j
@ -127,11 +131,15 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
final Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile = new LinkedHashMap<>();
final Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile = new LinkedHashMap<>();
final Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile = new LinkedHashMap<>();
final Map<FileId, SourceKind> sourceKindByFile = new LinkedHashMap<>();
for (final var entry : importedSemanticContexts.entrySet()) {
supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls());
}
for (final var parsedSourceFile : assembly.parsedSourceFiles()) {
astByFile.put(parsedSourceFile.fileId(), parsedSourceFile.ast());
sourceKindByFile.put(
parsedSourceFile.fileId(),
parsedSourceFile.sourceKind() == null ? SourceKind.PROJECT : parsedSourceFile.sourceKind());
final var importedSemanticContext = importedSemanticContexts.getOrDefault(
parsedSourceFile.fileId(),
PbsImportedSemanticContext.empty());
@ -144,16 +152,81 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
inlineHintsByFile.put(parsedSourceFile.fileId(), inlineHints);
}
}
final Map<ModuleReference, Path> physicalModuleFiles = physicalModuleFiles(ctx, assembly);
return new PbsSemanticReadSurface(
Map.copyOf(astByFile),
Map.copyOf(supplementalTopDeclsByFile),
Map.copyOf(inlineHintsByFile));
Map.copyOf(inlineHintsByFile),
Map.copyOf(sourceKindByFile),
physicalModuleFiles,
moduleCompletionLabels(ctx, physicalModuleFiles));
}
private static Map<ModuleReference, Path> physicalModuleFiles(
final FrontendPhaseContext ctx,
final PbsModuleAssembly assembly) {
final Map<ModuleReference, Path> files = new LinkedHashMap<>();
if (ctx == null || ctx.fileTable == null || assembly == null || assembly.parsedSourceFiles() == null) {
return Map.of();
}
for (final PbsParsedSourceFile parsed : assembly.parsedSourceFiles()) {
if (parsed.fileId() == null || parsed.fileId().isNone()
|| parsed.moduleId() == null || parsed.moduleId().isNone()) {
continue;
}
final var handle = ctx.fileTable.get(parsed.fileId());
if (handle == null || handle.getCanonPath() == null) {
continue;
}
final Path path = handle.getCanonPath().toAbsolutePath().normalize();
if (!Files.isRegularFile(path)) {
continue;
}
final ModuleReference reference = assembly.moduleTable().get(parsed.moduleId());
if (reference == null) {
continue;
}
files.merge(reference, path, (left, right) ->
left.toString().compareTo(right.toString()) <= 0 ? left : right);
}
return Map.copyOf(files);
}
private static List<String> moduleCompletionLabels(
final FrontendPhaseContext ctx,
final Map<ModuleReference, Path> physicalModuleFiles) {
final var labels = new java.util.TreeSet<String>();
for (final ModuleReference reference : physicalModuleFiles.keySet()) {
labels.add(moduleLabel(reference.project(), reference.pathSegments()));
}
if (ctx != null) {
final var environment = new ResourceStdlibEnvironmentResolver().resolve(ctx.stdlibVersion(), ctx.target());
for (final var module : environment.modules()) {
labels.add(moduleLabel(module.project(), module.pathSegments()));
}
}
return List.copyOf(labels);
}
private static String moduleLabel(
final String project,
final ReadOnlyList<String> pathSegments) {
if (project == null || project.isBlank()) {
return "";
}
if (pathSegments == null || pathSegments.isEmpty()) {
return "@" + project;
}
return "@" + project + ":" + String.join("/", pathSegments.asList());
}
public record PbsSemanticReadSurface(
Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile,
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile) {
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile,
Map<FileId, SourceKind> sourceKindByFile,
Map<ModuleReference, Path> physicalModuleFileByReference,
List<String> moduleCompletionLabels) {
}
private IRBackend mergeCompiledSources(

View File

@ -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()));
}
}

View File

@ -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()));
}
}

View File

@ -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;
}
}

View File

@ -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"));
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -8,6 +8,7 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsLexerTest {
@ -34,6 +35,13 @@ class PbsLexerTest {
.kind());
assertEquals(PbsTokenKind.EOF, tokens.getLast().kind());
assertTrue(diagnostics.isEmpty(), "Lexer should not report diagnostics for valid input");
assertTrue(PbsLexer.isKeyword("fn"));
assertTrue(PbsLexer.isKeyword("step"));
assertTrue(PbsLexer.isIdentifierLexeme("helper"));
assertTrue(PbsLexer.isIdentifierLexeme("_value"));
assertFalse(PbsLexer.isIdentifierLexeme("fn"));
assertFalse(PbsLexer.isIdentifierLexeme("1ab"));
assertFalse(PbsLexer.isIdentifierLexeme(""));
}
@Test

View File

@ -434,6 +434,304 @@ final class PbsEditorialSupportServiceTest {
return locations;
}
@Test
void shouldBuildAstBackedOutlineHierarchyAndKeepOverloadsAsSiblings() {
final var source = """
import { Log } from @sdk:log;
[Init]
fn init() -> void { return; }
fn helper(a: int) -> int { return a; }
fn helper(a: int, b: int) -> int { return a; }
declare struct Point(x: int, pub y: int) {
ctor make(x: int) { return; }
fn run() -> void { return; }
}
declare contract TickLike { fn tick(dt: int) -> void; }
declare service Physics { fn tick() -> void { return; } }
declare error Io { NotFound; Busy; }
declare enum Mode(Idle = 0, Run = 1);
declare callback OnClick(x: int) -> result<Io> int;
declare global STATE: int = 10;
declare const LIMIT: int = 10;
implements TickLike for Point using s { fn tick(dt: int) -> void { return; } }
fn frame() -> void {
let local = 1;
helper(local);
}
""";
final var ast = parseOrdinary(source);
final var symbols = service.documentSymbols(ast);
assertEquals(List.of(
"init",
"helper",
"helper",
"Point",
"TickLike",
"Physics",
"Io",
"Mode",
"OnClick",
"STATE",
"LIMIT",
"TickLike",
"frame"), names(symbols));
assertTrue(symbols.stream().noneMatch(symbol -> symbol.name().equals("Log") || symbol.name().equals("local")));
assertTrue(symbols.getFirst().name().equals("init"));
assertFalse(symbols.getFirst().name().contains("Init"));
assertEquals("(a: int) -> int", symbols.get(1).detail());
assertEquals("(a: int, b: int) -> int", symbols.get(2).detail());
final var point = requireNamed(symbols, "Point");
assertEquals(PbsEditorialSymbolKind.STRUCT, point.kind());
assertEquals(List.of("x", "y", "make", "run"), names(point.children()));
assertEquals(PbsEditorialSymbolKind.FIELD, point.children().getFirst().kind());
assertEquals("int", point.children().getFirst().detail());
assertEquals(PbsEditorialSymbolKind.CONSTRUCTOR, point.children().get(2).kind());
assertEquals("(x: int)", point.children().get(2).detail());
assertEquals(PbsEditorialSymbolKind.METHOD, point.children().get(3).kind());
final var contract = symbols.get(4);
assertEquals(PbsEditorialSymbolKind.CONTRACT, contract.kind());
assertEquals(List.of("tick"), names(contract.children()));
assertEquals("(dt: int) -> void", contract.children().getFirst().detail());
final var physics = requireNamed(symbols, "Physics");
assertEquals(PbsEditorialSymbolKind.SERVICE, physics.kind());
assertEquals(List.of("tick"), names(physics.children()));
final var error = requireNamed(symbols, "Io");
assertEquals(PbsEditorialSymbolKind.ERROR, error.kind());
assertEquals(List.of("NotFound", "Busy"), names(error.children()));
final var mode = requireNamed(symbols, "Mode");
assertEquals(PbsEditorialSymbolKind.ENUM, mode.kind());
assertEquals(List.of("Idle", "Run"), names(mode.children()));
assertEquals("0", mode.children().getFirst().detail());
final var callback = requireNamed(symbols, "OnClick");
assertEquals(PbsEditorialSymbolKind.CALLBACK, callback.kind());
assertEquals("(x: int) -> result<Io> int", callback.detail());
assertEquals(PbsEditorialSymbolKind.GLOBAL, requireNamed(symbols, "STATE").kind());
assertEquals("int", requireNamed(symbols, "STATE").detail());
assertEquals(PbsEditorialSymbolKind.CONST, requireNamed(symbols, "LIMIT").kind());
final var implementsSymbol = symbols.get(11);
assertEquals("TickLike", implementsSymbol.name());
assertEquals("Point", implementsSymbol.detail());
assertEquals(PbsEditorialSymbolKind.CONTRACT, implementsSymbol.kind());
assertEquals(List.of("tick"), names(implementsSymbol.children()));
}
@Test
void shouldFlattenOutlineMembersForWorkspaceSearchAndFilterByName() {
final var source = """
import { Log } from @sdk:log;
declare struct Vec(x: int) {
ctor make(x: int) { return; }
fn blend(dx: int, dy: int) -> int { return dx; }
}
fn helper() -> int { return 42; }
fn helper(value: int) -> int { return value; }
fn frame() -> void {
let local = 1;
helper(local);
}
""";
final var ast = parseOrdinary(source);
final var all = service.workspaceSymbols(ast, "");
assertEquals(List.of(
"Vec",
"x",
"make",
"blend",
"helper",
"helper",
"frame"), all.stream().map(PbsEditorialWorkspaceSymbol::name).toList());
assertEquals("", all.getFirst().containerName());
assertEquals("Vec", all.stream().filter(symbol -> symbol.name().equals("blend")).findFirst().orElseThrow().containerName());
assertEquals("Vec", all.stream().filter(symbol -> symbol.name().equals("x")).findFirst().orElseThrow().containerName());
assertTrue(all.stream().noneMatch(symbol -> symbol.name().equals("Log") || symbol.name().equals("local")));
final var blendHits = service.workspaceSymbols(ast, "BLEND");
assertEquals(List.of("blend"), blendHits.stream().map(PbsEditorialWorkspaceSymbol::name).toList());
assertEquals("Vec", blendHits.getFirst().containerName());
final var helperHits = service.workspaceSymbols(ast, "helper");
assertEquals(2, helperHits.size());
assertNotEquals(helperHits.getFirst().detail(), helperHits.get(1).detail());
final var qualifiedMiss = service.workspaceSymbols(ast, "Vec.blend");
assertTrue(qualifiedMiss.isEmpty());
}
@Test
void shouldOmitInvalidDeclarationsAndKeepNamedDeclsFromRecoveredAst() {
final var source = """
declare ;
fn ok() -> int { return 1; }
""";
final var ast = parseAllowingErrors(source);
final var symbols = service.documentSymbols(ast);
assertEquals(List.of("ok"), names(symbols));
assertEquals(PbsEditorialSymbolKind.FUNCTION, symbols.getFirst().kind());
final var workspaceSymbols = service.workspaceSymbols(ast, "");
assertEquals(List.of("ok"), workspaceSymbols.stream().map(PbsEditorialWorkspaceSymbol::name).toList());
}
@Test
void shouldOutlineHostAndBuiltinTypeDeclarationsFromInterfaceModules() {
final var source = """
declare host Gfx {
fn clear(color: int) -> void;
}
declare builtin type Color(
pub r: int
) {
fn pack() -> int;
}
""";
final var ast = parseInterface(source, new FileId(0));
final var symbols = service.documentSymbols(ast);
assertEquals(List.of("Gfx", "Color"), names(symbols));
assertEquals(PbsEditorialSymbolKind.HOST, symbols.getFirst().kind());
assertEquals(List.of("clear"), names(symbols.getFirst().children()));
assertEquals(PbsEditorialSymbolKind.BUILTIN_TYPE, symbols.get(1).kind());
assertEquals(List.of("r", "pack"), names(symbols.get(1).children()));
}
@Test
void renameRewritesPhysicalFunctionSitesAndSkipsCommentsStringsAndCollisions() {
final var source = """
fn helper() -> int { return 1; }
fn helper(value: int) -> int { return value; }
fn frame() -> void {
helper();
helper(2);
// helper
let text = "helper";
}
""";
final var ast = parseOrdinary(source);
final var renamed = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "aid");
assertTrue(renamed.allowed(), renamed.refusal());
assertEquals(4, renamed.sites().size(), renamed.sites().toString());
assertTrue(service.prepareRename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();")).isPresent());
final var keyword = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "fn");
assertFalse(keyword.allowed());
assertEquals("invalid identifier", keyword.refusal());
final var collision = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "frame");
assertFalse(collision.allowed());
assertEquals("name collision", collision.refusal());
}
@Test
void renameAllowsLocalNameSpanAndRefusesAliasBuiltinAndErrorLabel() {
final var source = """
import { Log as Logger } from @sdk:log;
declare error Boom {
Missing;
}
fn frame() -> void {
let total = 1;
Logger.info("x");
let n: int = total;
n;
}
""";
final var ast = parseOrdinary(source);
final var supplemental = supplementalTopDecls("""
declare service Log {
fn info(message: str) -> void {
return;
}
}
""");
final var local = service.rename(
source,
ast,
supplemental,
source.indexOf("total ="),
"sum");
assertTrue(local.allowed(), local.refusal());
assertEquals(2, local.sites().size(), local.sites().toString());
final var alias = service.prepareRename(
source,
ast,
supplemental,
source.indexOf("Logger.info"));
assertTrue(alias.isEmpty());
final var builtin = service.prepareRename(
source,
ast,
supplemental,
source.indexOf("int = total"));
assertTrue(builtin.isEmpty());
final var errorLabel = service.prepareRename(
source,
ast,
supplemental,
source.indexOf("Missing"));
assertTrue(errorLabel.isEmpty());
}
@Test
void renameOfExportDoesNotRewriteDifferingAlias() {
final var exporter = """
fn helper() -> int { return 1; }
""";
final var importer = """
import { helper as aid } from @sdk:log;
fn frame() -> void {
aid();
}
""";
final var exporterAst = parse(exporter, new FileId(1), PbsParser.ParseMode.ORDINARY);
final var importerAst = parseOrdinary(importer);
final var exporterDoc = new PbsEditorialSupportService.EditorialDocument(
exporter,
exporterAst,
ReadOnlyList.empty(),
p.studio.compiler.messages.FESurfaceContext.empty());
final var importerDoc = new PbsEditorialSupportService.EditorialDocument(
importer,
importerAst,
exporterAst.topDecls(),
p.studio.compiler.messages.FESurfaceContext.empty());
final var renamed = service.rename(
exporterDoc,
exporter.indexOf("helper"),
"renamed",
List.of(exporterDoc, importerDoc));
assertTrue(renamed.allowed(), renamed.refusal());
assertEquals(2, renamed.sites().size(), renamed.sites().toString());
assertTrue(renamed.sites().stream().anyMatch(site ->
site.span().getFileId().equals(exporterAst.span().getFileId())));
assertTrue(renamed.sites().stream().anyMatch(site ->
site.span().getFileId().equals(importerAst.span().getFileId())));
final int aliasUse = importer.indexOf("aid();");
assertTrue(renamed.sites().stream().noneMatch(site ->
site.span().getFileId().equals(importerAst.span().getFileId())
&& site.span().getStart() == aliasUse));
}
private PbsEditorialResolvedSymbol requireHover(
final String source,
final PbsAst.File ast,
@ -455,6 +753,26 @@ final class PbsEditorialSupportServiceTest {
return ReadOnlyList.wrap(topDecls);
}
private List<String> names(final List<PbsEditorialDocumentSymbol> symbols) {
return symbols.stream().map(PbsEditorialDocumentSymbol::name).toList();
}
private PbsEditorialDocumentSymbol requireNamed(
final List<PbsEditorialDocumentSymbol> symbols,
final String name) {
return symbols.stream()
.filter(symbol -> symbol.name().equals(name))
.findFirst()
.orElseThrow(() -> new AssertionError("missing symbol: " + name + " in " + names(symbols)));
}
private PbsAst.File parseAllowingErrors(final String source) {
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
return PbsParser.parse(tokens, fileId, diagnostics, PbsParser.ParseMode.ORDINARY);
}
private PbsAst.File parseOrdinary(final String source) {
return parse(source, new FileId(0), PbsParser.ParseMode.ORDINARY);
}

View File

@ -0,0 +1,234 @@
package p.studio.compiler.pbs.semantics;
import org.junit.jupiter.api.Test;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.source.diagnostics.Diagnostic;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.tables.NameTable;
import java.util.Comparator;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsQuickFixCollectorTest {
@Test
void duplicateDocRepairsMatchValidatorSpansAndKeepTheFirstCopy() {
final var parsed = parse("""
[Doc(markdown = \"""
First.
\""")]
[Doc(markdown = \"""
Second.
\""")]
[Doc(markdown = \"""
Third.
\""")]
fn duplicated() -> void { return; }
declare struct Player(x: int) {
[Doc(markdown = \"""
Kept.
\""")]
[Doc(markdown = \"""
Extra method.
\""")]
fn update() -> void { return; }
}
""", SourceKind.PROJECT);
final var duplicates = diagnostics(parsed, PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name());
assertEquals(3, duplicates.size(), parsed.diagnostics().stream().map(Diagnostic::getMessage).toList().toString());
assertEquals(3, parsed.repairs().size());
assertTrue(parsed.repairs().stream().allMatch(repair ->
repair.newText().isEmpty()
&& repair.title().equals(PbsQuickFixCollector.REMOVE_DUPLICATE_TITLE)
&& repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name())));
assertRepairsMatchDiagnostics(parsed);
final String edited = apply(parsed.source(), parsed.repairs());
assertTrue(edited.contains("First."));
assertTrue(edited.contains("Kept."));
assertFalse(edited.contains("Second."));
assertFalse(edited.contains("Third."));
assertFalse(edited.contains("Extra method."));
}
@Test
void malformedAndEmptyDocProduceNoRepair() {
final var parsed = parse("""
[Doc(text = \"""
Wrong argument.
\""")]
fn malformed() -> void { return; }
[Doc(markdown = \"""
\""")]
fn empty() -> void { return; }
[InitAllowed(flag = 1)]
fn frame() -> void { return; }
""", SourceKind.PROJECT);
assertTrue(diagnostics(parsed, PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name()).size() >= 2);
assertTrue(parsed.repairs().isEmpty(), parsed.repairs().toString());
}
@Test
void initAllowedArgumentsAreRewrittenOnlyForInterfaceHostSignatures() {
final var parsed = parse("""
declare host Gfx {
[Host(module = "gfx2d", name = "clear", version = 1)]
[InitAllowed(flag = 1)]
fn clear() -> void;
[Host(module = "gfx2d", name = "fill", version = 1)]
[InitAllowed()]
fn fill() -> void;
[Host(module = "gfx2d", name = "blit", version = 1)]
[InitAllowed]
fn blit() -> void;
[Host(module = "gfx2d")]
fn broken() -> void;
[Host(module = "gfx2d", name = "copy", version = 1)]
[Host(module = "gfx2d", name = "copy", version = 1)]
fn copy() -> void;
}
""", SourceKind.SDK_INTERFACE);
final var argumentRepairs = parsed.repairs().stream()
.filter(repair -> repair.newText().equals("[InitAllowed]"))
.toList();
assertEquals(1, argumentRepairs.size(), parsed.repairs().toString());
assertEquals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), argumentRepairs.getFirst().diagnosticCode());
assertEquals(PbsQuickFixCollector.REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE, argumentRepairs.getFirst().title());
assertRepairsMatchDiagnostics(parsed);
final var duplicateRepairs = parsed.repairs().stream()
.filter(repair -> repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name()))
.toList();
assertEquals(1, duplicateRepairs.size());
assertTrue(duplicateRepairs.getFirst().newText().isEmpty());
final String edited = apply(parsed.source(), parsed.repairs());
assertTrue(edited.contains("[InitAllowed]\n"));
assertFalse(edited.contains("flag = 1"));
assertTrue(edited.contains("[InitAllowed()]"));
assertEquals(1, count(edited, "[Host(module = \"gfx2d\", name = \"copy\", version = 1)]"));
final var projectHost = parse("""
declare host Gfx {
[Host(module = "gfx2d", name = "clear", version = 1)]
[InitAllowed(flag = 1)]
fn clear() -> void;
}
""", SourceKind.PROJECT);
assertTrue(projectHost.repairs().stream().noneMatch(repair -> repair.newText().equals("[InitAllowed]")));
}
@Test
void builtinConstTypeIntrinsicAndAssetLoweringDuplicatesAreDeleted() {
final var parsed = parse("""
[BuiltinType(name = "Color", version = 1)]
[BuiltinType(name = "Color", version = 1)]
declare builtin type Color(
pub raw: int
) {
[IntrinsicCall(name = "width", version = 1)]
[IntrinsicCall(name = "width", version = 1)]
fn width() -> int;
}
[BuiltinConst(target = "Color", name = "white", version = 1)]
[BuiltinConst(target = "Color", name = "white", version = 1)]
declare const WHITE: Color;
declare host LowAssets {
[Host(module = "asset", name = "load", version = 1)]
[AssetLowering(param = 0)]
[AssetLowering(param = 0)]
fn load(addressable: int) -> void;
}
""", SourceKind.SDK_INTERFACE);
assertEquals(4, parsed.repairs().size(), parsed.repairs().toString());
assertTrue(parsed.repairs().stream().allMatch(repair ->
repair.newText().isEmpty()
&& repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name())));
assertRepairsMatchDiagnostics(parsed);
final String edited = apply(parsed.source(), parsed.repairs());
assertEquals(1, count(edited, "[BuiltinType(name = \"Color\", version = 1)]"));
assertEquals(1, count(edited, "[IntrinsicCall(name = \"width\", version = 1)]"));
assertEquals(1, count(edited, "[BuiltinConst(target = \"Color\", name = \"white\", version = 1)]"));
assertEquals(1, count(edited, "[AssetLowering(param = 0)]"));
}
private static Parsed parse(final String source, final SourceKind sourceKind) {
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
final var parseMode = sourceKind == SourceKind.SDK_INTERFACE
? PbsParser.ParseMode.INTERFACE_MODULE
: PbsParser.ParseMode.ORDINARY;
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics, parseMode);
new PbsDeclarationSemanticsValidator(new NameTable()).validate(ast, sourceKind, diagnostics);
return new Parsed(
source,
diagnostics,
PbsQuickFixCollector.collect(ast, sourceKind == SourceKind.SDK_INTERFACE));
}
private static List<Diagnostic> diagnostics(final Parsed parsed, final String code) {
return parsed.diagnostics().stream().filter(diagnostic -> diagnostic.getCode().equals(code)).toList();
}
private static void assertRepairsMatchDiagnostics(final Parsed parsed) {
for (final PbsQuickFix repair : parsed.repairs()) {
assertTrue(parsed.diagnostics().stream().anyMatch(diagnostic ->
diagnostic.getCode().equals(repair.diagnosticCode())
&& diagnostic.getSpan().getStart() == repair.startOffset()
&& diagnostic.getSpan().getEnd() == repair.endOffset()),
repair + " missing from " + parsed.diagnostics());
}
}
private static String apply(final String source, final List<PbsQuickFix> repairs) {
final var ordered = repairs.stream()
.sorted(Comparator.comparingInt(PbsQuickFix::startOffset).reversed())
.toList();
String current = source;
for (final PbsQuickFix repair : ordered) {
current = current.substring(0, repair.startOffset()) + repair.newText() + current.substring(repair.endOffset());
}
return current;
}
private static int count(final String source, final String needle) {
int count = 0;
int from = 0;
while (from <= source.length()) {
final int found = source.indexOf(needle, from);
if (found < 0) {
return count;
}
count += 1;
from = found + needle.length();
}
return count;
}
private record Parsed(
String source,
DiagnosticSink diagnostics,
List<PbsQuickFix> repairs) {
}
}

View File

@ -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");
}
}
}

View File

@ -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");
}
}
}

View File

@ -0,0 +1,36 @@
package p.studio.compiler.services;
import java.nio.file.Path;
import java.util.Objects;
public record FrontendCodeAction(
String title,
String diagnosticCode,
Path path,
int startOffset,
int endOffset,
String newText) {
public FrontendCodeAction {
title = requireText(title, "title");
diagnosticCode = requireText(diagnosticCode, "diagnosticCode");
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
if (startOffset < 0) {
throw new IllegalArgumentException("startOffset must not be negative");
}
if (endOffset < startOffset) {
throw new IllegalArgumentException("endOffset must not be before startOffset");
}
newText = Objects.requireNonNull(newText, "newText");
}
private static String requireText(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}

View File

@ -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");
}
}
}

View File

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

View File

@ -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");
}
}
}

View File

@ -11,6 +11,11 @@ import java.util.Optional;
* Optional frontend-owned editor services.
*/
public interface FrontendLanguageService {
default Optional<FrontendEditorialContext> prepareEditorialContext(
final FrontendPhaseContext phaseContext) {
return Optional.empty();
}
default Optional<FrontendEditorialContext> prepareEditorialContext(
final FrontendPhaseContext phaseContext,
final FileId fileId) {
@ -21,6 +26,12 @@ public interface FrontendLanguageService {
return List.of();
}
default List<FrontendSemanticToken> semanticTokens(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
return semanticTokens(request);
}
default List<FrontendCompletionCandidate> completion(
final FrontendDocumentRequest request,
final int offset) {
@ -85,4 +96,144 @@ public interface FrontendLanguageService {
final int offset) {
return references(request, offset);
}
default List<FrontendDocumentSymbol> documentSymbols(final FrontendDocumentRequest request) {
return List.of();
}
default List<FrontendDocumentSymbol> documentSymbols(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
return documentSymbols(request);
}
default List<FrontendWorkspaceSymbol> workspaceSymbols(final FrontendWorkspaceRequest request) {
return List.of();
}
default List<FrontendWorkspaceSymbol> workspaceSymbols(
final FrontendWorkspaceRequest request,
final FrontendEditorialContext editorialContext) {
return workspaceSymbols(request);
}
default Optional<FrontendDefinitionLocation> prepareRename(
final FrontendDocumentRequest request,
final int offset) {
return Optional.empty();
}
default Optional<FrontendDefinitionLocation> prepareRename(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset) {
return prepareRename(request, offset);
}
default FrontendRenameResult rename(
final FrontendDocumentRequest request,
final int offset,
final String newName) {
return FrontendRenameResult.unsupported();
}
default FrontendRenameResult rename(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset,
final String newName) {
return rename(request, offset, newName);
}
default boolean documentLinksSupported() {
return false;
}
default List<FrontendDocumentLink> documentLinks(final FrontendDocumentRequest request) {
return List.of();
}
default List<FrontendDocumentLink> documentLinks(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
return documentLinks(request);
}
default boolean foldingRangesSupported() {
return false;
}
default List<FrontendFoldingRange> foldingRanges(final FrontendDocumentRequest request) {
return List.of();
}
default List<FrontendFoldingRange> foldingRanges(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
return foldingRanges(request);
}
default boolean selectionRangesSupported() {
return false;
}
default List<FrontendSelectionRange> selectionRanges(
final FrontendDocumentRequest request,
final List<Integer> offsets) {
return List.of();
}
default List<FrontendSelectionRange> selectionRanges(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final List<Integer> offsets) {
return selectionRanges(request, offsets);
}
default boolean callHierarchySupported() {
return false;
}
default java.util.List<FrontendCallHierarchyItem> prepareCallHierarchy(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset) {
return java.util.List.of();
}
default java.util.List<FrontendCallSite> incomingCalls(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final FrontendCallHierarchyItem target) {
return java.util.List.of();
}
default java.util.List<FrontendCallSite> outgoingCalls(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final FrontendCallHierarchyItem target) {
return java.util.List.of();
}
default boolean formattingSupported() {
return false;
}
default java.util.Optional<String> formatDocument(final FrontendDocumentRequest request) {
return java.util.Optional.empty();
}
default boolean codeActionsSupported() {
return false;
}
default List<FrontendCodeAction> codeActions(final FrontendDocumentRequest request) {
return List.of();
}
default List<FrontendCodeAction> codeActions(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext) {
return codeActions(request);
}
}

View File

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

View File

@ -0,0 +1,45 @@
package p.studio.compiler.services;
import java.util.List;
import java.util.Objects;
public record FrontendRenameResult(
Status status,
String message,
List<FrontendRenameEdit> edits) {
public enum Status {
UNSUPPORTED,
REFUSED,
APPLIED
}
public FrontendRenameResult {
status = Objects.requireNonNull(status, "status");
message = message == null ? "" : message.trim();
edits = edits == null ? List.of() : List.copyOf(edits);
if (status == Status.APPLIED && edits.isEmpty()) {
throw new IllegalArgumentException("applied rename must include at least one edit");
}
if (status != Status.APPLIED && !edits.isEmpty()) {
throw new IllegalArgumentException("refused or unsupported rename must not include edits");
}
}
public static FrontendRenameResult unsupported() {
return new FrontendRenameResult(Status.UNSUPPORTED, "unsupported", List.of());
}
public static FrontendRenameResult refused(final String message) {
final String reason = message == null || message.isBlank() ? "not renameable" : message.trim();
return new FrontendRenameResult(Status.REFUSED, reason, List.of());
}
public static FrontendRenameResult applied(final List<FrontendRenameEdit> edits) {
return new FrontendRenameResult(Status.APPLIED, "", edits);
}
public boolean applied() {
return status == Status.APPLIED;
}
}

View File

@ -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");
}
}
}

View File

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

View File

@ -0,0 +1,14 @@
package p.studio.compiler.services;
import java.nio.file.Path;
import java.util.Objects;
public record FrontendWorkspaceRequest(
Path projectRoot,
String query) {
public FrontendWorkspaceRequest {
projectRoot = Objects.requireNonNull(projectRoot, "projectRoot").toAbsolutePath().normalize();
query = query == null ? "" : query;
}
}

View File

@ -0,0 +1,38 @@
package p.studio.compiler.services;
import java.nio.file.Path;
import java.util.Objects;
public record FrontendWorkspaceSymbol(
String name,
FrontendSymbolKind kind,
String detail,
String containerName,
Path path,
int startOffset,
int endOffset) {
public FrontendWorkspaceSymbol {
name = requireText(name, "name");
kind = Objects.requireNonNull(kind, "kind");
detail = detail == null ? "" : detail.trim();
containerName = containerName == null ? "" : containerName.trim();
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
if (startOffset < 0) {
throw new IllegalArgumentException("startOffset must not be negative");
}
if (endOffset < startOffset) {
throw new IllegalArgumentException("endOffset must not be before startOffset");
}
}
private static String requireText(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}

View File

@ -24,6 +24,170 @@ class FrontendLanguageServiceTest {
assertEquals(FrontendSignatureHelp.empty(), service.signatureHelp(request, 0));
assertTrue(service.definition(request, 0).isEmpty());
assertTrue(service.references(request, 0).isEmpty());
assertTrue(service.documentSymbols(request).isEmpty());
assertTrue(service.workspaceSymbols(new FrontendWorkspaceRequest(Path.of("."), "frame")).isEmpty());
assertTrue(service.prepareEditorialContext(null).isEmpty());
assertTrue(service.prepareRename(request, 0).isEmpty());
assertEquals(FrontendRenameResult.Status.UNSUPPORTED, service.rename(request, 0, "renamed").status());
assertTrue(service.rename(request, 0, "renamed").edits().isEmpty());
assertFalse(service.documentLinksSupported());
assertTrue(service.documentLinks(request).isEmpty());
assertTrue(service.documentLinks(request, null).isEmpty());
assertFalse(service.foldingRangesSupported());
assertTrue(service.foldingRanges(request).isEmpty());
assertTrue(service.foldingRanges(request, null).isEmpty());
assertFalse(service.callHierarchySupported());
assertTrue(service.prepareCallHierarchy(request, null, 0).isEmpty());
assertFalse(service.formattingSupported());
assertTrue(service.formatDocument(request).isEmpty());
assertFalse(service.selectionRangesSupported());
assertTrue(service.selectionRanges(request, List.of(0)).isEmpty());
assertTrue(service.selectionRanges(request, null, List.of(0)).isEmpty());
assertFalse(service.codeActionsSupported());
assertTrue(service.codeActions(request).isEmpty());
assertTrue(service.codeActions(request, null).isEmpty());
}
@Test
void codeActionAllowsEmptyReplacementAndRejectsBlankIdentity() {
final Path path = Path.of("src/main.pbs").toAbsolutePath();
final var action = new FrontendCodeAction(
" Remove duplicate reserved attribute ",
" E_SEM_DUPLICATE_RESERVED_ATTRIBUTE ",
path,
4,
12,
"");
assertEquals("Remove duplicate reserved attribute", action.title());
assertEquals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", action.diagnosticCode());
assertEquals("", action.newText());
assertThrows(IllegalArgumentException.class, () -> new FrontendCodeAction(
" ",
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
path,
0,
1,
""));
assertThrows(NullPointerException.class, () -> new FrontendCodeAction(
"Remove duplicate reserved attribute",
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
path,
0,
1,
null));
}
@Test
void documentSymbolRejectsBlankNameAndInvertedRangesAndCopiesChildren() {
final var child = new FrontendDocumentSymbol(
"x",
FrontendSymbolKind.FIELD,
"int",
10,
16,
10,
16,
List.of());
final var symbol = new FrontendDocumentSymbol(
" Point ",
FrontendSymbolKind.STRUCT,
" ",
0,
20,
8,
13,
List.of(child));
assertEquals("Point", symbol.name());
assertEquals("", symbol.detail());
assertEquals(1, symbol.children().size());
assertEquals("x", symbol.children().getFirst().name());
assertThrows(UnsupportedOperationException.class, () -> symbol.children().add(child));
assertThrows(IllegalArgumentException.class, () -> new FrontendDocumentSymbol(
" ",
FrontendSymbolKind.FUNCTION,
"",
0,
1,
0,
1,
List.of()));
assertThrows(IllegalArgumentException.class, () -> new FrontendDocumentSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
-1,
1,
0,
1,
List.of()));
assertThrows(IllegalArgumentException.class, () -> new FrontendDocumentSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
5,
4,
0,
1,
List.of()));
assertThrows(IllegalArgumentException.class, () -> new FrontendDocumentSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
0,
1,
3,
2,
List.of()));
}
@Test
void workspaceSymbolNormalizesOptionalTextAndRejectsBlankNameOrInvertedOffsets() {
final Path path = Path.of("src/main.pbs").toAbsolutePath();
final var symbol = new FrontendWorkspaceSymbol(
" blend ",
FrontendSymbolKind.METHOD,
" (dx: int) -> int ",
" Vec ",
path,
4,
9);
assertEquals("blend", symbol.name());
assertEquals("(dx: int) -> int", symbol.detail());
assertEquals("Vec", symbol.containerName());
assertEquals(path.normalize(), symbol.path());
assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol(
" ",
FrontendSymbolKind.FUNCTION,
"",
"",
path,
0,
1));
assertThrows(NullPointerException.class, () -> new FrontendWorkspaceSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
"",
null,
0,
1));
assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
"",
path,
-1,
0));
assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol(
"fn",
FrontendSymbolKind.FUNCTION,
"",
"",
path,
5,
4));
}
@Test

View File

@ -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");
}
}
}

View File

@ -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");
}
}
}

View File

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

View File

@ -0,0 +1,16 @@
package p.studio.lsp.messages;
public record BaselineCodeActionDiagnostic(
String code,
int startLine,
int startCharacter,
int endLine,
int endCharacter) {
public BaselineCodeActionDiagnostic {
code = code == null ? "" : code.trim();
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
throw new IllegalArgumentException("diagnostic range coordinates must not be negative");
}
}
}

View File

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

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

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

View File

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

View File

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

View File

@ -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");
}
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,22 @@
package p.studio.lsp.messages;
import java.util.Optional;
public record BaselinePrepareRename(Optional<BaselineDefinitionLocation> placeholder) {
public BaselinePrepareRename {
placeholder = placeholder == null ? Optional.empty() : placeholder;
}
public static BaselinePrepareRename none() {
return new BaselinePrepareRename(Optional.empty());
}
public static BaselinePrepareRename of(final BaselineDefinitionLocation location) {
return new BaselinePrepareRename(Optional.of(location));
}
public boolean renameable() {
return placeholder.isPresent();
}
}

View File

@ -0,0 +1,26 @@
package p.studio.lsp.messages;
import java.util.List;
public record BaselineRename(
boolean applied,
String message,
List<BaselineRenameEdit> edits) {
public BaselineRename {
message = message == null ? "" : message;
edits = edits == null ? List.of() : List.copyOf(edits);
}
public static BaselineRename unsupported() {
return new BaselineRename(false, "unsupported", List.of());
}
public static BaselineRename refused(final String message) {
return new BaselineRename(false, message == null || message.isBlank() ? "not renameable" : message, List.of());
}
public static BaselineRename applied(final List<BaselineRenameEdit> edits) {
return new BaselineRename(true, "", edits);
}
}

View File

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

View File

@ -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");
}
}
}

View File

@ -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());
}
}

View File

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

View File

@ -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");
}
}

View File

@ -0,0 +1,36 @@
package p.studio.lsp.messages;
import java.util.Objects;
public record BaselineWorkspaceSymbol(
String name,
BaselineDocumentSymbolKind kind,
String detail,
String containerName,
String uri,
int startLine,
int startCharacter,
int endLine,
int endCharacter) {
public BaselineWorkspaceSymbol {
name = requireText(name, "name");
kind = Objects.requireNonNull(kind, "kind");
detail = detail == null ? "" : detail;
containerName = containerName == null ? "" : containerName;
uri = requireText(uri, "uri");
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
throw new IllegalArgumentException("workspace symbol range coordinates must not be negative");
}
}
private static String requireText(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}

View File

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

View File

@ -4,12 +4,27 @@ import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineReferences;
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
import p.studio.lsp.messages.BaselineCodeActions;
import p.studio.lsp.messages.BaselineDocumentLinks;
import p.studio.lsp.messages.BaselineFoldingRanges;
import p.studio.lsp.messages.BaselineCallHierarchyItem;
import p.studio.lsp.messages.BaselineCallSite;
import p.studio.lsp.messages.BaselineFormatting;
import p.studio.lsp.messages.BaselineSelectionRanges;
import p.studio.lsp.messages.DocumentPosition;
import p.studio.lsp.messages.BaselinePrepareRename;
import p.studio.lsp.messages.BaselineRename;
import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineSignatureHelp;
import p.studio.lsp.messages.BaselineServerDescription;
import p.studio.lsp.messages.LspProjectContext;
import java.util.List;
public interface LanguageServiceBridge {
BaselineServerDescription describeServer(LspProjectContext context);
@ -29,9 +44,79 @@ public interface LanguageServiceBridge {
int character,
boolean includeDeclaration);
BaselineDocumentSymbols documentSymbols(LspProjectContext context, String documentUri, String text);
BaselineWorkspaceSymbols workspaceSymbols(LspProjectContext context, String query);
BaselinePrepareRename prepareRename(
LspProjectContext context,
String documentUri,
String text,
int line,
int character);
BaselineRename rename(
LspProjectContext context,
String documentUri,
String text,
int line,
int character,
String newName);
BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character);
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);
default BaselineDocumentLinks documentLinks(LspProjectContext context, String documentUri, String text) {
return BaselineDocumentLinks.empty();
}
default BaselineFoldingRanges foldingRanges(LspProjectContext context, String documentUri, String text) {
return BaselineFoldingRanges.empty();
}
default BaselineFormatting formatDocument(LspProjectContext context, String documentUri, String text) {
return BaselineFormatting.empty();
}
default java.util.List<BaselineCallHierarchyItem> prepareCallHierarchy(
LspProjectContext context,
String documentUri,
String text,
int line,
int character) {
return java.util.List.of();
}
default java.util.List<BaselineCallSite> incomingCalls(
LspProjectContext context,
String documentUri,
String text,
BaselineCallHierarchyItem target) {
return java.util.List.of();
}
default java.util.List<BaselineCallSite> outgoingCalls(
LspProjectContext context,
String documentUri,
String text,
BaselineCallHierarchyItem target) {
return java.util.List.of();
}
default BaselineSelectionRanges selectionRanges(
LspProjectContext context,
String documentUri,
String text,
List<DocumentPosition> positions) {
return BaselineSelectionRanges.empty();
}
BaselineCodeActions codeActions(
LspProjectContext context,
String documentUri,
String text,
List<BaselineCodeActionDiagnostic> diagnostics);
String onSave(LspProjectContext context, String documentUri);
}

View File

@ -2,17 +2,30 @@ package p.studio.lsp.services.compiler;
import org.apache.commons.lang3.StringUtils;
import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.exceptions.BuildException;
import p.studio.compiler.messages.*;
import p.studio.compiler.models.*;
import p.studio.compiler.services.FrontendCallHierarchyItem;
import p.studio.compiler.services.FrontendCallSite;
import p.studio.compiler.services.FrontendCodeAction;
import p.studio.compiler.services.FrontendCompletionCandidate;
import p.studio.compiler.services.FrontendDocumentLink;
import p.studio.compiler.services.FrontendFoldingRange;
import p.studio.compiler.services.FrontendSelectionRange;
import p.studio.compiler.services.FrontendLanguageService;
import p.studio.compiler.services.FrontendDefinitionLocation;
import p.studio.compiler.services.FrontendDocumentRequest;
import p.studio.compiler.services.FrontendDocumentSymbol;
import p.studio.compiler.services.FrontendEditorialContext;
import p.studio.compiler.services.FrontendHover;
import p.studio.compiler.services.FrontendProvider;
import p.studio.compiler.services.FrontendSignature;
import p.studio.compiler.services.FrontendSignatureHelp;
import p.studio.compiler.services.FrontendSymbolKind;
import p.studio.compiler.services.FrontendRenameEdit;
import p.studio.compiler.services.FrontendRenameResult;
import p.studio.compiler.services.FrontendWorkspaceRequest;
import p.studio.compiler.services.FrontendWorkspaceSymbol;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.utilities.SourceProviderFactory;
import p.studio.compiler.workspaces.AssetSurfaceContextLoader;
@ -45,6 +58,26 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
Objects.requireNonNull(context, "context");
final var frontend = frontendSpec(context);
final var presentation = frontend.getSemanticPresentation();
final var languageService = frontendProvider(context).languageService();
final boolean languageServicePresent = languageService.isPresent();
final boolean codeActionsSupported = languageService
.map(FrontendLanguageService::codeActionsSupported)
.orElse(false);
final boolean documentLinksSupported = languageService
.map(FrontendLanguageService::documentLinksSupported)
.orElse(false);
final boolean foldingRangesSupported = languageService
.map(FrontendLanguageService::foldingRangesSupported)
.orElse(false);
final boolean selectionRangesSupported = languageService
.map(FrontendLanguageService::selectionRangesSupported)
.orElse(false);
final boolean formattingSupported = languageService
.map(FrontendLanguageService::formattingSupported)
.orElse(false);
final boolean callHierarchySupported = languageService
.map(FrontendLanguageService::callHierarchySupported)
.orElse(false);
return new BaselineServerDescription(
"Prometeu Studio LSP",
"0.1.0",
@ -52,8 +85,17 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
true,
true,
true,
frontendProvider(context).languageService().isPresent(),
frontendProvider(context).languageService().isPresent(),
languageServicePresent,
languageServicePresent,
languageServicePresent,
languageServicePresent,
languageServicePresent,
codeActionsSupported,
documentLinksSupported,
foldingRangesSupported,
selectionRangesSupported,
formattingSupported,
callHierarchySupported,
presentation.semanticKeys(),
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
presentation.themes().stream().map(this::mapVisualTheme).toList(),
@ -69,7 +111,11 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
final Path documentPath = normalizeDocumentPath(documentUri);
final String effectiveText = StringUtils.isBlank(text) ? "" : text;
final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText));
return new BaselineDocumentAnalysis(mapDiagnostics(snapshot, documentPath, effectiveText));
return new BaselineDocumentAnalysis(mapDiagnostics(
snapshot,
documentPath,
effectiveText,
context.languageId()));
}
@Override
@ -208,6 +254,466 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
.orElseGet(BaselineReferences::empty);
}
@Override
public BaselineDocumentSymbols documentSymbols(
final LspProjectContext context,
final String documentUri,
final String text) {
Objects.requireNonNull(context, "context");
final var provider = frontendProvider(context);
final var languageService = provider.languageService();
if (languageService.isEmpty()) {
return BaselineDocumentSymbols.empty();
}
return editorialDocument(context, documentUri, text)
.map(document -> {
final FrontendDocumentRequest request = new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
document.text());
return new BaselineDocumentSymbols(mapDocumentSymbols(
languageService.orElseThrow().documentSymbols(request, document.editorialContext()),
document.text()));
})
.orElseGet(BaselineDocumentSymbols::empty);
}
@Override
public BaselineWorkspaceSymbols workspaceSymbols(
final LspProjectContext context,
final String query) {
Objects.requireNonNull(context, "context");
final var provider = frontendProvider(context);
final var languageService = provider.languageService();
if (languageService.isEmpty()) {
return BaselineWorkspaceSymbols.empty();
}
final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig(
new BuilderPipelineConfig(
false,
context.projectRoot().toString(),
"core-v1",
SourceProviderFactory.overlayUtf8(Map.of())));
BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty());
final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext(
pipelineContext.resolvedWorkspace.graph().projectTable(),
pipelineContext.fileTable,
pipelineContext.resolvedWorkspace.stack(),
pipelineContext.resolvedWorkspace.stdlib(),
pipelineContext.resolvedWorkspace.target(),
HostAdmissionContext.permissiveDefault(),
assetSurfaceContextLoader.load(pipelineContext.resolvedWorkspace.mainProject().getRootPath()));
return languageService.orElseThrow()
.prepareEditorialContext(frontendPhaseContext)
.map(editorialContext -> new BaselineWorkspaceSymbols(mapWorkspaceSymbols(
languageService.orElseThrow().workspaceSymbols(
new FrontendWorkspaceRequest(context.projectRoot(), query),
editorialContext))))
.orElseGet(BaselineWorkspaceSymbols::empty);
}
@Override
public BaselinePrepareRename prepareRename(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
Objects.requireNonNull(context, "context");
final var provider = frontendProvider(context);
final var languageService = provider.languageService();
if (languageService.isEmpty()) {
return BaselinePrepareRename.none();
}
return editorialDocument(context, documentUri, text)
.flatMap(document -> {
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
final FrontendDocumentRequest request = new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
document.text());
return languageService.orElseThrow().prepareRename(
request,
document.editorialContext(),
offset);
})
.map(location -> {
final Path documentPath = normalizeDocumentPath(documentUri);
final List<BaselineDefinitionLocation> mapped = mapDefinitionLocations(
List.of(location),
documentPath,
text == null ? "" : text);
if (mapped.isEmpty()) {
return BaselinePrepareRename.none();
}
return BaselinePrepareRename.of(mapped.getFirst());
})
.orElseGet(BaselinePrepareRename::none);
}
@Override
public BaselineRename rename(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character,
final String newName) {
Objects.requireNonNull(context, "context");
final var provider = frontendProvider(context);
final var languageService = provider.languageService();
if (languageService.isEmpty()) {
return BaselineRename.unsupported();
}
return editorialDocument(context, documentUri, text)
.map(document -> {
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
final FrontendDocumentRequest request = new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
document.text());
final FrontendRenameResult result = languageService.orElseThrow().rename(
request,
document.editorialContext(),
offset,
newName);
if (!result.applied()) {
if (result.status() == FrontendRenameResult.Status.UNSUPPORTED) {
return BaselineRename.unsupported();
}
return BaselineRename.refused(result.message());
}
return BaselineRename.applied(mapRenameEdits(
result.edits(),
request.documentPath(),
document.text()));
})
.orElseGet(() -> BaselineRename.refused("not renameable"));
}
@Override
public BaselineDocumentLinks documentLinks(
final LspProjectContext context,
final String documentUri,
final String text) {
Objects.requireNonNull(context, "context");
final var languageService = frontendProvider(context).languageService();
if (languageService.isEmpty() || !languageService.orElseThrow().documentLinksSupported()) {
return BaselineDocumentLinks.empty();
}
return editorialDocument(context, documentUri, text)
.map(document -> {
final FrontendDocumentRequest request = new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
document.text());
return new BaselineDocumentLinks(mapDocumentLinks(
languageService.orElseThrow().documentLinks(request, document.editorialContext()),
document.text()));
})
.orElseGet(BaselineDocumentLinks::empty);
}
@Override
public List<BaselineCallHierarchyItem> prepareCallHierarchy(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
final var languageService = callHierarchyService(context);
if (languageService == null) {
return List.of();
}
try {
return editorialDocument(context, documentUri, text)
.map(document -> {
final DocumentPositionMapper mapper = new DocumentPositionMapper(document.text());
final int offset = mapper.byteOffsetOf(line, character);
return languageService.prepareCallHierarchy(
callRequest(context, documentUri, document.text()),
document.editorialContext(),
offset)
.stream()
.map(item -> toBaselineItem(item, documentUri, mapper))
.toList();
})
.orElseGet(List::of);
} catch (final BuildException ignored) {
return List.of();
}
}
@Override
public List<BaselineCallSite> incomingCalls(
final LspProjectContext context,
final String documentUri,
final String text,
final BaselineCallHierarchyItem target) {
return directedCalls(context, documentUri, text, target, true);
}
@Override
public List<BaselineCallSite> outgoingCalls(
final LspProjectContext context,
final String documentUri,
final String text,
final BaselineCallHierarchyItem target) {
return directedCalls(context, documentUri, text, target, false);
}
private List<BaselineCallSite> directedCalls(
final LspProjectContext context,
final String documentUri,
final String text,
final BaselineCallHierarchyItem target,
final boolean incoming) {
final var languageService = callHierarchyService(context);
if (languageService == null || target == null) {
return List.of();
}
try {
return editorialDocument(context, documentUri, text)
.map(document -> {
final DocumentPositionMapper mapper = new DocumentPositionMapper(document.text());
final FrontendCallHierarchyItem frontendTarget = new FrontendCallHierarchyItem(
target.name(),
target.method(),
mapper.byteOffsetOf(target.startLine(), target.startCharacter()),
mapper.byteOffsetOf(target.endLine(), target.endCharacter()));
final List<FrontendCallSite> calls = incoming
? languageService.incomingCalls(
callRequest(context, documentUri, document.text()),
document.editorialContext(),
frontendTarget)
: languageService.outgoingCalls(
callRequest(context, documentUri, document.text()),
document.editorialContext(),
frontendTarget);
return calls.stream().map(call -> new BaselineCallSite(
toBaselineItem(call.item(), documentUri, mapper),
mapper.positionOf(call.startOffset()).line(),
mapper.positionOf(call.startOffset()).character(),
mapper.positionOf(call.endOffset()).line(),
mapper.positionOf(call.endOffset()).character())).toList();
})
.orElseGet(List::of);
} catch (final BuildException ignored) {
return List.of();
}
}
private FrontendLanguageService callHierarchyService(final LspProjectContext context) {
Objects.requireNonNull(context, "context");
final var languageService = frontendProvider(context).languageService();
if (languageService.isEmpty() || !languageService.orElseThrow().callHierarchySupported()) {
return null;
}
return languageService.orElseThrow();
}
private FrontendDocumentRequest callRequest(
final LspProjectContext context,
final String documentUri,
final String text) {
return new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
text == null ? "" : text);
}
private BaselineCallHierarchyItem toBaselineItem(
final FrontendCallHierarchyItem item,
final String documentUri,
final DocumentPositionMapper mapper) {
final DocumentPosition start = mapper.positionOf(item.startOffset());
final DocumentPosition end = mapper.positionOf(item.endOffset());
return new BaselineCallHierarchyItem(
item.name(),
item.method(),
documentUri,
start.line(),
start.character(),
end.line(),
end.character());
}
@Override
public BaselineFormatting formatDocument(
final LspProjectContext context,
final String documentUri,
final String text) {
Objects.requireNonNull(context, "context");
final var languageService = frontendProvider(context).languageService();
if (languageService.isEmpty() || !languageService.orElseThrow().formattingSupported()) {
return BaselineFormatting.empty();
}
final String effectiveText = text == null ? "" : text;
final String formatted = languageService.orElseThrow().formatDocument(new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
effectiveText)).orElse(effectiveText);
if (formatted.equals(effectiveText)) {
return BaselineFormatting.empty();
}
final DocumentPosition end = new DocumentPositionMapper(effectiveText)
.positionOf(effectiveText.getBytes(StandardCharsets.UTF_8).length);
return new BaselineFormatting(List.of(new BaselineTextEdit(
0,
0,
end.line(),
end.character(),
formatted)));
}
@Override
public BaselineFoldingRanges foldingRanges(
final LspProjectContext context,
final String documentUri,
final String text) {
return structureRanges(context, documentUri, text, true, List.of()).folding();
}
@Override
public BaselineSelectionRanges selectionRanges(
final LspProjectContext context,
final String documentUri,
final String text,
final List<DocumentPosition> positions) {
return structureRanges(context, documentUri, text, false, positions == null ? List.of() : positions).selection();
}
private StructureRanges structureRanges(
final LspProjectContext context,
final String documentUri,
final String text,
final boolean folding,
final List<DocumentPosition> positions) {
Objects.requireNonNull(context, "context");
final var languageService = frontendProvider(context).languageService();
if (languageService.isEmpty()) {
return StructureRanges.empty();
}
final boolean supported = folding
? languageService.orElseThrow().foldingRangesSupported()
: languageService.orElseThrow().selectionRangesSupported();
if (!supported) {
return StructureRanges.empty();
}
final String effectiveText = text == null ? "" : text;
final FrontendDocumentRequest request = new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
effectiveText);
final DocumentPositionMapper mapper = new DocumentPositionMapper(effectiveText);
if (folding) {
return new StructureRanges(new BaselineFoldingRanges(mapFoldingRanges(
languageService.orElseThrow().foldingRanges(request, null),
mapper)), BaselineSelectionRanges.empty());
}
final ArrayList<Integer> offsets = new ArrayList<>();
for (final DocumentPosition position : positions) {
if (position == null) {
offsets.add(0);
continue;
}
offsets.add(mapper.byteOffsetOf(position.line(), position.character()));
}
return new StructureRanges(
BaselineFoldingRanges.empty(),
new BaselineSelectionRanges(mapSelectionRanges(
languageService.orElseThrow().selectionRanges(request, null, offsets),
mapper)));
}
private List<BaselineFoldingRange> mapFoldingRanges(
final List<FrontendFoldingRange> ranges,
final DocumentPositionMapper mapper) {
if (ranges == null || ranges.isEmpty()) {
return List.of();
}
final ArrayList<BaselineFoldingRange> mapped = new ArrayList<>();
for (final FrontendFoldingRange range : ranges) {
final DocumentPosition start = mapper.positionOf(range.startOffset());
final DocumentPosition end = mapper.positionOf(range.endOffset());
mapped.add(new BaselineFoldingRange(
start.line(),
start.character(),
end.line(),
end.character()));
}
return List.copyOf(mapped);
}
private List<BaselineSelectionRange> mapSelectionRanges(
final List<FrontendSelectionRange> ranges,
final DocumentPositionMapper mapper) {
if (ranges == null || ranges.isEmpty()) {
return List.of();
}
final ArrayList<BaselineSelectionRange> mapped = new ArrayList<>();
for (final FrontendSelectionRange range : ranges) {
mapped.add(mapSelectionRange(range, mapper));
}
return List.copyOf(mapped);
}
private BaselineSelectionRange mapSelectionRange(
final FrontendSelectionRange range,
final DocumentPositionMapper mapper) {
if (range == null) {
return null;
}
final DocumentPosition start = mapper.positionOf(range.startOffset());
final DocumentPosition end = mapper.positionOf(range.endOffset());
return new BaselineSelectionRange(
start.line(),
start.character(),
end.line(),
end.character(),
mapSelectionRange(range.parent(), mapper));
}
private record StructureRanges(BaselineFoldingRanges folding, BaselineSelectionRanges selection) {
private static StructureRanges empty() {
return new StructureRanges(BaselineFoldingRanges.empty(), BaselineSelectionRanges.empty());
}
}
@Override
public BaselineCodeActions codeActions(
final LspProjectContext context,
final String documentUri,
final String text,
final List<BaselineCodeActionDiagnostic> diagnostics) {
Objects.requireNonNull(context, "context");
final var languageService = frontendProvider(context).languageService();
if (languageService.isEmpty() || !languageService.orElseThrow().codeActionsSupported()) {
return BaselineCodeActions.empty();
}
if (diagnostics == null || diagnostics.isEmpty()) {
return BaselineCodeActions.empty();
}
return editorialDocument(context, documentUri, text)
.map(document -> {
final Path documentPath = normalizeDocumentPath(documentUri);
final FrontendDocumentRequest request = new FrontendDocumentRequest(
context.projectRoot(),
documentPath,
document.text());
final List<FrontendCodeAction> repairs = languageService.orElseThrow().codeActions(
request,
document.editorialContext());
return new BaselineCodeActions(mapCodeActions(
repairs,
documentUri,
documentPath,
document.text(),
diagnostics));
})
.orElseGet(BaselineCodeActions::empty);
}
@Override
public BaselineSignatureHelp signatureHelp(
final LspProjectContext context,
@ -253,7 +759,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
documentPath,
effectiveText);
final List<FrontendSemanticToken> tokens = provider.languageService()
.map(languageService -> languageService.semanticTokens(request))
.map(languageService -> semanticTokens(languageService, context, documentUri, documentPath, request))
.orElseGet(List::of);
final var positionMapper = new DocumentPositionMapper(effectiveText);
final var semanticTokens = new ArrayList<BaselineSemanticToken>();
@ -272,6 +778,26 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
return new BaselineSemanticTokens(presentation.semanticKeys(), semanticTokens);
}
private List<FrontendSemanticToken> semanticTokens(
final FrontendLanguageService languageService,
final LspProjectContext context,
final String documentUri,
final Path documentPath,
final FrontendDocumentRequest request) {
try {
return editorialDocument(context, documentUri, request.documentText())
.map(document -> languageService.semanticTokens(
new FrontendDocumentRequest(
context.projectRoot(),
documentPath,
document.text()),
document.editorialContext()))
.orElseGet(() -> languageService.semanticTokens(request));
} catch (final BuildException ignored) {
return languageService.semanticTokens(request);
}
}
@Override
public String onSave(
final LspProjectContext context,
@ -354,7 +880,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
private List<BaselineDocumentIssue> mapDiagnostics(
final AnalysisSnapshot snapshot,
final Path documentPath,
final String text) {
final String text,
final String languageId) {
final ArrayList<BaselineDocumentIssue> issues = new ArrayList<>();
final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text);
for (final BuildingIssue diagnostic : snapshot.diagnostics()) {
@ -369,26 +896,29 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) {
continue;
}
issues.add(toBaselineIssue(diagnostic, positionMapper));
issues.add(toBaselineIssue(diagnostic, positionMapper, languageId));
}
return List.copyOf(issues);
}
private BaselineDocumentIssue toBaselineIssue(
final BuildingIssue diagnostic,
final DocumentPositionMapper positionMapper) {
final DocumentPositionMapper positionMapper,
final String languageId) {
final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart();
final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd());
final DocumentPosition start = positionMapper.positionOf(startOffset);
final DocumentPosition end = positionMapper.positionOf(endOffset);
final String code = diagnostic.getCode() == null ? "" : diagnostic.getCode().trim();
return new BaselineDocumentIssue(
start.line(),
start.character(),
end.line(),
end.character(),
diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING,
diagnostic.getCode() == null || diagnostic.getCode().isBlank() ? "Prometeu Studio" : diagnostic.getCode(),
diagnostic.getMessage());
languageId,
diagnostic.getMessage(),
code);
}
private Path normalizeDocumentPath(final String documentUri) {
@ -484,6 +1014,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
case ERROR -> BaselineCompletionItemKind.ERROR;
case GLOBAL -> BaselineCompletionItemKind.GLOBAL;
case CONST -> BaselineCompletionItemKind.CONST;
case MODULE -> BaselineCompletionItemKind.MODULE;
};
}
@ -491,6 +1022,172 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
return location.path() + ":" + location.startOffset() + ":" + location.endOffset();
}
private List<BaselineDocumentSymbol> mapDocumentSymbols(
final List<FrontendDocumentSymbol> symbols,
final String documentText) {
if (symbols == null || symbols.isEmpty()) {
return List.of();
}
final DocumentPositionMapper mapper = new DocumentPositionMapper(documentText == null ? "" : documentText);
final ArrayList<BaselineDocumentSymbol> mapped = new ArrayList<>();
for (final FrontendDocumentSymbol symbol : symbols) {
mapped.add(mapDocumentSymbol(symbol, mapper));
}
return List.copyOf(mapped);
}
private BaselineDocumentSymbol mapDocumentSymbol(
final FrontendDocumentSymbol symbol,
final DocumentPositionMapper mapper) {
final DocumentPosition rangeStart = mapper.positionOf(symbol.rangeStart());
final DocumentPosition rangeEnd = mapper.positionOf(symbol.rangeEnd());
final DocumentPosition selectionStart = mapper.positionOf(symbol.selectionStart());
final DocumentPosition selectionEnd = mapper.positionOf(symbol.selectionEnd());
final ArrayList<BaselineDocumentSymbol> children = new ArrayList<>();
for (final FrontendDocumentSymbol child : symbol.children()) {
children.add(mapDocumentSymbol(child, mapper));
}
return new BaselineDocumentSymbol(
symbol.name(),
BaselineDocumentSymbolKind.valueOf(symbol.kind().name()),
symbol.detail(),
rangeStart.line(),
rangeStart.character(),
rangeEnd.line(),
rangeEnd.character(),
selectionStart.line(),
selectionStart.character(),
selectionEnd.line(),
selectionEnd.character(),
children);
}
private List<BaselineWorkspaceSymbol> mapWorkspaceSymbols(final List<FrontendWorkspaceSymbol> symbols) {
if (symbols == null || symbols.isEmpty()) {
return List.of();
}
final LinkedHashMap<Path, DocumentPositionMapper> mappers = new LinkedHashMap<>();
final ArrayList<BaselineWorkspaceSymbol> mapped = new ArrayList<>();
for (final FrontendWorkspaceSymbol symbol : symbols) {
final DocumentPositionMapper mapper = mappers.computeIfAbsent(symbol.path(), this::mapperForPath);
if (mapper == null) {
continue;
}
final DocumentPosition start = mapper.positionOf(symbol.startOffset());
final DocumentPosition end = mapper.positionOf(symbol.endOffset());
mapped.add(new BaselineWorkspaceSymbol(
symbol.name(),
BaselineDocumentSymbolKind.valueOf(symbol.kind().name()),
symbol.detail(),
symbol.containerName(),
symbol.path().toUri().toString(),
start.line(),
start.character(),
end.line(),
end.character()));
}
return List.copyOf(mapped);
}
private DocumentPositionMapper mapperForPath(final Path path) {
if (path == null || !Files.isRegularFile(path)) {
return null;
}
try {
return new DocumentPositionMapper(Files.readString(path, StandardCharsets.UTF_8));
} catch (final Exception ignored) {
return null;
}
}
private List<BaselineCodeAction> mapCodeActions(
final List<FrontendCodeAction> repairs,
final String documentUri,
final Path documentPath,
final String documentText,
final List<BaselineCodeActionDiagnostic> diagnostics) {
if (repairs == null || repairs.isEmpty()) {
return List.of();
}
final DocumentPositionMapper mapper = new DocumentPositionMapper(documentText == null ? "" : documentText);
final ArrayList<BaselineCodeAction> mapped = new ArrayList<>();
for (final FrontendCodeAction repair : repairs) {
if (!repair.path().toAbsolutePath().normalize().equals(documentPath)) {
continue;
}
final DocumentPosition start = mapper.positionOf(repair.startOffset());
final DocumentPosition end = mapper.positionOf(repair.endOffset());
final boolean matched = diagnostics.stream().anyMatch(diagnostic ->
repair.diagnosticCode().equals(diagnostic.code())
&& diagnostic.startLine() == start.line()
&& diagnostic.startCharacter() == start.character()
&& diagnostic.endLine() == end.line()
&& diagnostic.endCharacter() == end.character());
if (!matched) {
continue;
}
mapped.add(new BaselineCodeAction(
repair.title(),
repair.diagnosticCode(),
documentUri,
start.line(),
start.character(),
end.line(),
end.character(),
repair.newText()));
}
return List.copyOf(mapped);
}
private List<BaselineRenameEdit> mapRenameEdits(
final List<FrontendRenameEdit> edits,
final Path currentDocumentPath,
final String currentDocumentText) {
if (edits == null || edits.isEmpty()) {
return List.of();
}
final ArrayList<BaselineRenameEdit> mapped = new ArrayList<>();
for (final FrontendRenameEdit edit : edits) {
final String targetText = textForDefinitionTarget(edit.path(), currentDocumentPath, currentDocumentText);
if (targetText == null) {
continue;
}
final DocumentPositionMapper mapper = new DocumentPositionMapper(targetText);
final DocumentPosition start = mapper.positionOf(edit.startOffset());
final DocumentPosition end = mapper.positionOf(edit.endOffset());
mapped.add(new BaselineRenameEdit(
edit.path().toUri().toString(),
start.line(),
start.character(),
end.line(),
end.character(),
edit.newText()));
}
return List.copyOf(mapped);
}
private List<BaselineDocumentLink> mapDocumentLinks(
final List<FrontendDocumentLink> links,
final String currentDocumentText) {
if (links == null || links.isEmpty()) {
return List.of();
}
final DocumentPositionMapper mapper = new DocumentPositionMapper(
currentDocumentText == null ? "" : currentDocumentText);
final ArrayList<BaselineDocumentLink> mapped = new ArrayList<>();
for (final FrontendDocumentLink link : links) {
final DocumentPosition start = mapper.positionOf(link.startOffset());
final DocumentPosition end = mapper.positionOf(link.endOffset());
mapped.add(new BaselineDocumentLink(
link.path().toUri().toString(),
start.line(),
start.character(),
end.line(),
end.character()));
}
return List.copyOf(mapped);
}
private List<BaselineDefinitionLocation> mapDefinitionLocations(
final List<FrontendDefinitionLocation> locations,
final Path currentDocumentPath,

View File

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

View File

@ -1,13 +1,22 @@
package p.studio.lsp.services.protocol;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService;
import p.studio.lsp.messages.BaselineCallHierarchyItem;
import p.studio.lsp.messages.BaselineCallSite;
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
import p.studio.lsp.messages.DocumentPosition;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
@ -127,6 +136,156 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
includeDeclaration)));
}
@Override
public CompletableFuture<Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior>> prepareRename(
final PrepareRenameParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
return CompletableFuture.completedFuture(protocolMessageMapper.mapPrepareRename(
languageServiceBridge.prepareRename(
project,
uri,
text,
params.getPosition().getLine(),
params.getPosition().getCharacter())));
}
@Override
public CompletableFuture<WorkspaceEdit> rename(final RenameParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
final var result = languageServiceBridge.rename(
project,
uri,
text,
params.getPosition().getLine(),
params.getPosition().getCharacter(),
params.getNewName());
if (!result.applied()) {
final ResponseError error = new ResponseError(
ResponseErrorCode.InvalidRequest,
result.message() == null || result.message().isBlank() ? "not renameable" : result.message(),
null);
return CompletableFuture.failedFuture(new ResponseErrorException(error));
}
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
}
@Override
public CompletableFuture<List<CallHierarchyItem>> prepareCallHierarchy(final CallHierarchyPrepareParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
return CompletableFuture.completedFuture(protocolMessageMapper.mapCallHierarchyItems(
languageServiceBridge.prepareCallHierarchy(
project,
uri,
text,
params.getPosition().getLine(),
params.getPosition().getCharacter())));
}
@Override
public CompletableFuture<List<CallHierarchyIncomingCall>> callHierarchyIncomingCalls(
final CallHierarchyIncomingCallsParams params) {
return CompletableFuture.completedFuture(protocolMessageMapper.mapIncomingCalls(
directedCallSites(params.getItem(), true)));
}
@Override
public CompletableFuture<List<CallHierarchyOutgoingCall>> callHierarchyOutgoingCalls(
final CallHierarchyOutgoingCallsParams params) {
return CompletableFuture.completedFuture(protocolMessageMapper.mapOutgoingCalls(
directedCallSites(params.getItem(), false)));
}
private List<BaselineCallSite> directedCallSites(final CallHierarchyItem item, final boolean incoming) {
if (item == null || item.getUri() == null || item.getSelectionRange() == null) {
return List.of();
}
final String text = documentTextByUri.get(item.getUri());
final BaselineCallHierarchyItem target = new BaselineCallHierarchyItem(
item.getName(),
item.getKind() == SymbolKind.Method,
item.getUri(),
item.getSelectionRange().getStart().getLine(),
item.getSelectionRange().getStart().getCharacter(),
item.getSelectionRange().getEnd().getLine(),
item.getSelectionRange().getEnd().getCharacter());
return incoming
? languageServiceBridge.incomingCalls(project, item.getUri(), text, target)
: languageServiceBridge.outgoingCalls(project, item.getUri(), text, target);
}
@Override
public CompletableFuture<List<? extends TextEdit>> formatting(final DocumentFormattingParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
return CompletableFuture.completedFuture(protocolMessageMapper.mapFormatting(
languageServiceBridge.formatDocument(project, uri, text)));
}
@Override
public CompletableFuture<List<FoldingRange>> foldingRange(final FoldingRangeRequestParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
return CompletableFuture.completedFuture(protocolMessageMapper.mapFoldingRanges(
languageServiceBridge.foldingRanges(project, uri, text)));
}
@Override
public CompletableFuture<List<SelectionRange>> selectionRange(final SelectionRangeParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
final ArrayList<DocumentPosition> positions = new ArrayList<>();
if (params.getPositions() != null) {
for (final Position position : params.getPositions()) {
if (position == null) {
continue;
}
positions.add(new DocumentPosition(position.getLine(), position.getCharacter()));
}
}
return CompletableFuture.completedFuture(protocolMessageMapper.mapSelectionRanges(
languageServiceBridge.selectionRanges(project, uri, text, positions)));
}
@Override
public CompletableFuture<List<DocumentLink>> documentLink(final DocumentLinkParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
return CompletableFuture.completedFuture(protocolMessageMapper.mapDocumentLinks(
languageServiceBridge.documentLinks(project, uri, text)));
}
@Override
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(final CodeActionParams params) {
if (!quickFixRequested(params.getContext())) {
return CompletableFuture.completedFuture(List.of());
}
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
final List<Diagnostic> contextDiagnostics = params.getContext() == null
|| params.getContext().getDiagnostics() == null
? List.of()
: params.getContext().getDiagnostics();
return CompletableFuture.completedFuture(protocolMessageMapper.mapCodeActions(
languageServiceBridge.codeActions(project, uri, text, baselineDiagnostics(contextDiagnostics)),
contextDiagnostics));
}
@Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
final DocumentSymbolParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
final List<Either<SymbolInformation, DocumentSymbol>> symbols = new ArrayList<>();
for (final DocumentSymbol symbol : protocolMessageMapper.mapDocumentSymbols(
languageServiceBridge.documentSymbols(project, uri, text))) {
symbols.add(Either.forRight(symbol));
}
return CompletableFuture.completedFuture(List.copyOf(symbols));
}
@Override
public CompletableFuture<SignatureHelp> signatureHelp(final SignatureHelpParams params) {
final String uri = params.getTextDocument().getUri();
@ -148,6 +307,52 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
languageServiceBridge.semanticTokens(project, uri, text)));
}
private static boolean quickFixRequested(final CodeActionContext context) {
if (context == null || context.getOnly() == null || context.getOnly().isEmpty()) {
return true;
}
for (final String kind : context.getOnly()) {
if (kind == null || kind.isBlank()) {
return true;
}
if (CodeActionKind.QuickFix.equals(kind) || CodeActionKind.QuickFix.startsWith(kind + ".")) {
return true;
}
}
return false;
}
private static List<BaselineCodeActionDiagnostic> baselineDiagnostics(final List<Diagnostic> diagnostics) {
final ArrayList<BaselineCodeActionDiagnostic> mapped = new ArrayList<>();
for (final Diagnostic diagnostic : diagnostics) {
if (diagnostic == null || diagnostic.getRange() == null
|| diagnostic.getRange().getStart() == null
|| diagnostic.getRange().getEnd() == null) {
continue;
}
mapped.add(new BaselineCodeActionDiagnostic(
diagnosticCode(diagnostic),
diagnostic.getRange().getStart().getLine(),
diagnostic.getRange().getStart().getCharacter(),
diagnostic.getRange().getEnd().getLine(),
diagnostic.getRange().getEnd().getCharacter()));
}
return List.copyOf(mapped);
}
private static String diagnosticCode(final Diagnostic diagnostic) {
if (diagnostic.getCode() == null) {
return "";
}
if (diagnostic.getCode().isLeft()) {
return diagnostic.getCode().getLeft() == null ? "" : diagnostic.getCode().getLeft();
}
if (diagnostic.getCode().isRight() && diagnostic.getCode().getRight() != null) {
return diagnostic.getCode().getRight().toString();
}
return "";
}
private void publishAnalysis(
final String uri,
final String text) {

View File

@ -2,9 +2,36 @@ package p.studio.lsp.services.protocol;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.WorkspaceService;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public final class PrometeuWorkspaceService implements WorkspaceService {
private final LspProjectContext project;
private final LanguageServiceBridge languageServiceBridge;
private final ProtocolMessageMapper protocolMessageMapper;
public PrometeuWorkspaceService() {
this(null, null, null);
}
public PrometeuWorkspaceService(
final LspProjectContext project,
final LanguageServiceBridge languageServiceBridge,
final ProtocolMessageMapper protocolMessageMapper) {
this.project = project;
this.languageServiceBridge = languageServiceBridge;
this.protocolMessageMapper = protocolMessageMapper;
}
@Override
public void didChangeConfiguration(final DidChangeConfigurationParams params) {
}
@ -12,4 +39,15 @@ public final class PrometeuWorkspaceService implements WorkspaceService {
@Override
public void didChangeWatchedFiles(final DidChangeWatchedFilesParams params) {
}
@Override
public CompletableFuture<Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>>> symbol(
final WorkspaceSymbolParams params) {
if (project == null || languageServiceBridge == null || protocolMessageMapper == null) {
return CompletableFuture.completedFuture(Either.forLeft(List.of()));
}
final String query = params == null || params.getQuery() == null ? "" : params.getQuery();
return CompletableFuture.completedFuture(Either.forLeft(
protocolMessageMapper.mapWorkspaceSymbols(languageServiceBridge.workspaceSymbols(project, query))));
}
}

View File

@ -1,6 +1,8 @@
package p.studio.lsp.services.protocol.mapping;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
import p.studio.lsp.messages.*;
import java.util.ArrayList;
@ -38,6 +40,36 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
}
capabilities.setDefinitionProvider(description.definitionSupported());
capabilities.setReferencesProvider(description.referencesSupported());
capabilities.setDocumentSymbolProvider(description.documentSymbolsSupported());
capabilities.setWorkspaceSymbolProvider(description.workspaceSymbolsSupported());
if (description.renameSupported()) {
final RenameOptions renameOptions = new RenameOptions();
renameOptions.setPrepareProvider(true);
capabilities.setRenameProvider(renameOptions);
}
if (description.codeActionsSupported()) {
final CodeActionOptions codeActionOptions = new CodeActionOptions();
codeActionOptions.setResolveProvider(false);
codeActionOptions.setCodeActionKinds(List.of(CodeActionKind.QuickFix));
capabilities.setCodeActionProvider(codeActionOptions);
}
if (description.documentLinksSupported()) {
final DocumentLinkOptions documentLinkOptions = new DocumentLinkOptions();
documentLinkOptions.setResolveProvider(false);
capabilities.setDocumentLinkProvider(documentLinkOptions);
}
if (description.foldingRangesSupported()) {
capabilities.setFoldingRangeProvider(true);
}
if (description.selectionRangesSupported()) {
capabilities.setSelectionRangeProvider(true);
}
if (description.formattingSupported()) {
capabilities.setDocumentFormattingProvider(true);
}
if (description.callHierarchySupported()) {
capabilities.setCallHierarchyProvider(true);
}
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
semanticTokens.setFull(true);
@ -101,6 +133,271 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return mapLocations(references.locations());
}
@Override
public List<DocumentSymbol> mapDocumentSymbols(final BaselineDocumentSymbols symbols) {
if (symbols == null || symbols.symbols().isEmpty()) {
return List.of();
}
final ArrayList<DocumentSymbol> mapped = new ArrayList<>();
for (final BaselineDocumentSymbol symbol : symbols.symbols()) {
mapped.add(mapDocumentSymbol(symbol, null));
}
return List.copyOf(mapped);
}
@Override
public Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior> mapPrepareRename(
final BaselinePrepareRename prepareRename) {
if (prepareRename == null || prepareRename.placeholder().isEmpty()) {
return null;
}
final BaselineDefinitionLocation location = prepareRename.placeholder().orElseThrow();
return Either3.forFirst(new Range(
new Position(location.startLine(), location.startCharacter()),
new Position(location.endLine(), location.endCharacter())));
}
@Override
public WorkspaceEdit mapRename(final BaselineRename rename) {
final WorkspaceEdit edit = new WorkspaceEdit();
if (rename == null || !rename.applied() || rename.edits().isEmpty()) {
edit.setChanges(Map.of());
return edit;
}
final LinkedHashMap<String, List<TextEdit>> changes = new LinkedHashMap<>();
for (final BaselineRenameEdit renameEdit : rename.edits()) {
changes.computeIfAbsent(renameEdit.uri(), ignored -> new ArrayList<>()).add(new TextEdit(
new Range(
new Position(renameEdit.startLine(), renameEdit.startCharacter()),
new Position(renameEdit.endLine(), renameEdit.endCharacter())),
renameEdit.newText()));
}
edit.setChanges(changes);
return edit;
}
@Override
public List<DocumentLink> mapDocumentLinks(final BaselineDocumentLinks links) {
if (links == null || links.links().isEmpty()) {
return List.of();
}
final ArrayList<DocumentLink> mapped = new ArrayList<>();
for (final BaselineDocumentLink link : links.links()) {
final DocumentLink documentLink = new DocumentLink();
documentLink.setRange(new Range(
new Position(link.startLine(), link.startCharacter()),
new Position(link.endLine(), link.endCharacter())));
documentLink.setTarget(link.targetUri());
mapped.add(documentLink);
}
return List.copyOf(mapped);
}
@Override
public List<CallHierarchyItem> mapCallHierarchyItems(final List<BaselineCallHierarchyItem> items) {
if (items == null || items.isEmpty()) {
return List.of();
}
return items.stream().map(this::mapCallHierarchyItem).toList();
}
@Override
public List<CallHierarchyIncomingCall> mapIncomingCalls(final List<BaselineCallSite> calls) {
if (calls == null || calls.isEmpty()) {
return List.of();
}
return calls.stream().map(call -> new CallHierarchyIncomingCall(
mapCallHierarchyItem(call.item()),
List.of(callRange(call)))).toList();
}
@Override
public List<CallHierarchyOutgoingCall> mapOutgoingCalls(final List<BaselineCallSite> calls) {
if (calls == null || calls.isEmpty()) {
return List.of();
}
return calls.stream().map(call -> new CallHierarchyOutgoingCall(
mapCallHierarchyItem(call.item()),
List.of(callRange(call)))).toList();
}
private CallHierarchyItem mapCallHierarchyItem(final BaselineCallHierarchyItem item) {
final CallHierarchyItem mapped = new CallHierarchyItem();
mapped.setName(item.name());
mapped.setKind(item.method() ? SymbolKind.Method : SymbolKind.Function);
mapped.setUri(item.uri());
final Range range = new Range(
new Position(item.startLine(), item.startCharacter()),
new Position(item.endLine(), item.endCharacter()));
mapped.setRange(range);
mapped.setSelectionRange(range);
return mapped;
}
private Range callRange(final BaselineCallSite call) {
return new Range(
new Position(call.startLine(), call.startCharacter()),
new Position(call.endLine(), call.endCharacter()));
}
@Override
public List<TextEdit> mapFormatting(final BaselineFormatting formatting) {
if (formatting == null || formatting.edits().isEmpty()) {
return List.of();
}
final ArrayList<TextEdit> mapped = new ArrayList<>();
for (final BaselineTextEdit edit : formatting.edits()) {
mapped.add(new TextEdit(
new Range(
new Position(edit.startLine(), edit.startCharacter()),
new Position(edit.endLine(), edit.endCharacter())),
edit.newText()));
}
return List.copyOf(mapped);
}
@Override
public List<FoldingRange> mapFoldingRanges(final BaselineFoldingRanges ranges) {
if (ranges == null || ranges.ranges().isEmpty()) {
return List.of();
}
final ArrayList<FoldingRange> mapped = new ArrayList<>();
for (final BaselineFoldingRange range : ranges.ranges()) {
final FoldingRange foldingRange = new FoldingRange();
foldingRange.setStartLine(range.startLine());
foldingRange.setStartCharacter(range.startCharacter());
foldingRange.setEndLine(range.endLine());
foldingRange.setEndCharacter(range.endCharacter());
mapped.add(foldingRange);
}
return List.copyOf(mapped);
}
@Override
public List<SelectionRange> mapSelectionRanges(final BaselineSelectionRanges ranges) {
if (ranges == null || ranges.ranges().isEmpty()) {
return List.of();
}
final ArrayList<SelectionRange> mapped = new ArrayList<>();
for (final BaselineSelectionRange range : ranges.ranges()) {
mapped.add(mapSelectionRange(range));
}
return List.copyOf(mapped);
}
private SelectionRange mapSelectionRange(final BaselineSelectionRange range) {
if (range == null) {
return null;
}
final SelectionRange selectionRange = new SelectionRange();
selectionRange.setRange(new Range(
new Position(range.startLine(), range.startCharacter()),
new Position(range.endLine(), range.endCharacter())));
selectionRange.setParent(mapSelectionRange(range.parent()));
return selectionRange;
}
@Override
public List<Either<Command, CodeAction>> mapCodeActions(
final BaselineCodeActions actions,
final List<Diagnostic> contextDiagnostics) {
if (actions == null || actions.actions().isEmpty()) {
return List.of();
}
final ArrayList<Either<Command, CodeAction>> mapped = new ArrayList<>();
for (final BaselineCodeAction action : actions.actions()) {
final Range range = new Range(
new Position(action.startLine(), action.startCharacter()),
new Position(action.endLine(), action.endCharacter()));
final TextEdit textEdit = new TextEdit(range, action.newText());
final WorkspaceEdit edit = new WorkspaceEdit();
edit.setChanges(Map.of(action.uri(), List.of(textEdit)));
final CodeAction codeAction = new CodeAction();
codeAction.setTitle(action.title());
codeAction.setKind(CodeActionKind.QuickFix);
codeAction.setEdit(edit);
codeAction.setDiagnostics(matchingDiagnostics(contextDiagnostics, action.diagnosticCode(), range));
mapped.add(Either.forRight(codeAction));
}
return List.copyOf(mapped);
}
@Override
public List<SymbolInformation> mapWorkspaceSymbols(final BaselineWorkspaceSymbols symbols) {
if (symbols == null || symbols.symbols().isEmpty()) {
return List.of();
}
final ArrayList<SymbolInformation> mapped = new ArrayList<>();
for (final BaselineWorkspaceSymbol symbol : symbols.symbols()) {
final SymbolInformation information = new SymbolInformation();
information.setName(symbol.name());
information.setKind(toLspWorkspaceSymbolKind(symbol.kind(), symbol.containerName()));
information.setContainerName(symbol.containerName());
information.setLocation(new Location(
symbol.uri(),
new Range(
new Position(symbol.startLine(), symbol.startCharacter()),
new Position(symbol.endLine(), symbol.endCharacter()))));
mapped.add(information);
}
return List.copyOf(mapped);
}
private DocumentSymbol mapDocumentSymbol(
final BaselineDocumentSymbol symbol,
final BaselineDocumentSymbolKind parentKind) {
final DocumentSymbol mapped = new DocumentSymbol();
mapped.setName(symbol.name());
mapped.setDetail(symbol.detail());
mapped.setKind(toLspSymbolKind(symbol.kind(), parentKind));
mapped.setRange(new Range(
new Position(symbol.startLine(), symbol.startCharacter()),
new Position(symbol.endLine(), symbol.endCharacter())));
mapped.setSelectionRange(new Range(
new Position(symbol.selectionStartLine(), symbol.selectionStartCharacter()),
new Position(symbol.selectionEndLine(), symbol.selectionEndCharacter())));
final ArrayList<DocumentSymbol> children = new ArrayList<>();
for (final BaselineDocumentSymbol child : symbol.children()) {
children.add(mapDocumentSymbol(child, symbol.kind()));
}
mapped.setChildren(children);
return mapped;
}
private SymbolKind toLspWorkspaceSymbolKind(
final BaselineDocumentSymbolKind kind,
final String containerName) {
if (containerName != null
&& !containerName.isBlank()
&& (kind == BaselineDocumentSymbolKind.ENUM || kind == BaselineDocumentSymbolKind.ERROR)) {
return SymbolKind.EnumMember;
}
return toLspSymbolKind(kind, null);
}
private SymbolKind toLspSymbolKind(
final BaselineDocumentSymbolKind kind,
final BaselineDocumentSymbolKind parentKind) {
if (parentKind == BaselineDocumentSymbolKind.ENUM || parentKind == BaselineDocumentSymbolKind.ERROR) {
if (kind == BaselineDocumentSymbolKind.ENUM || kind == BaselineDocumentSymbolKind.ERROR) {
return SymbolKind.EnumMember;
}
}
return switch (kind) {
case FUNCTION -> SymbolKind.Function;
case METHOD -> SymbolKind.Method;
case CONSTRUCTOR -> SymbolKind.Constructor;
case FIELD -> SymbolKind.Field;
case STRUCT, BUILTIN_TYPE -> SymbolKind.Struct;
case SERVICE, HOST -> SymbolKind.Class;
case CONTRACT, CALLBACK -> SymbolKind.Interface;
case ENUM, ERROR -> SymbolKind.Enum;
case GLOBAL, LOCAL, PARAMETER -> SymbolKind.Variable;
case CONST -> SymbolKind.Constant;
case KEYWORD -> SymbolKind.Key;
};
}
private List<Location> mapLocations(final List<BaselineDefinitionLocation> locations) {
return locations.stream()
.map(location -> new Location(
@ -196,6 +493,9 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
diagnostic.setSeverity(mapSeverity(issue.severity()));
diagnostic.setSource(issue.source());
diagnostic.setMessage(issue.message());
if (!issue.code().isBlank()) {
diagnostic.setCode(Either.forLeft(issue.code()));
}
return diagnostic;
}
@ -229,10 +529,51 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
case ENUM -> CompletionItemKind.Enum;
case ERROR -> CompletionItemKind.EnumMember;
case GLOBAL, CONST -> CompletionItemKind.Constant;
case MODULE -> CompletionItemKind.Module;
};
}
private List<Diagnostic> matchingDiagnostics(
final List<Diagnostic> contextDiagnostics,
final String diagnosticCode,
final Range range) {
if (contextDiagnostics == null || contextDiagnostics.isEmpty()) {
return List.of();
}
final ArrayList<Diagnostic> matched = new ArrayList<>();
for (final Diagnostic diagnostic : contextDiagnostics) {
if (diagnostic == null || diagnostic.getRange() == null) {
continue;
}
if (!diagnosticCode.equals(diagnosticCodeOf(diagnostic)) || !sameRange(diagnostic.getRange(), range)) {
continue;
}
matched.add(diagnostic);
}
return List.copyOf(matched);
}
private String diagnosticCodeOf(final Diagnostic diagnostic) {
if (diagnostic.getCode() == null) {
return "";
}
if (diagnostic.getCode().isLeft()) {
return diagnostic.getCode().getLeft() == null ? "" : diagnostic.getCode().getLeft().trim();
}
if (diagnostic.getCode().isRight() && diagnostic.getCode().getRight() != null) {
return diagnostic.getCode().getRight().toString();
}
return "";
}
private boolean sameRange(final Range left, final Range right) {
return left.getStart().getLine() == right.getStart().getLine()
&& left.getStart().getCharacter() == right.getStart().getCharacter()
&& left.getEnd().getLine() == right.getEnd().getLine()
&& left.getEnd().getCharacter() == right.getEnd().getCharacter();
}
private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) {
return switch (severity) {
case INFORMATION -> DiagnosticSeverity.Information;

View File

@ -1,13 +1,42 @@
package p.studio.lsp.services.protocol.mapping;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DocumentLink;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.CallHierarchyIncomingCall;
import org.eclipse.lsp4j.CallHierarchyItem;
import org.eclipse.lsp4j.CallHierarchyOutgoingCall;
import org.eclipse.lsp4j.FoldingRange;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.SelectionRange;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
import org.eclipse.lsp4j.PrepareRenameDefaultBehavior;
import org.eclipse.lsp4j.PrepareRenameResult;
import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineDocumentLinks;
import p.studio.lsp.messages.BaselineFoldingRanges;
import p.studio.lsp.messages.BaselineCallHierarchyItem;
import p.studio.lsp.messages.BaselineCallSite;
import p.studio.lsp.messages.BaselineFormatting;
import p.studio.lsp.messages.BaselineSelectionRanges;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
import p.studio.lsp.messages.BaselineCodeActions;
import p.studio.lsp.messages.BaselinePrepareRename;
import p.studio.lsp.messages.BaselineRename;
import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineReferences;
@ -32,6 +61,45 @@ public interface ProtocolMessageMapper {
List<Location> mapReferences(BaselineReferences references);
List<DocumentSymbol> mapDocumentSymbols(BaselineDocumentSymbols symbols);
List<SymbolInformation> mapWorkspaceSymbols(BaselineWorkspaceSymbols symbols);
Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior> mapPrepareRename(
BaselinePrepareRename prepareRename);
WorkspaceEdit mapRename(BaselineRename rename);
List<Either<Command, CodeAction>> mapCodeActions(BaselineCodeActions actions, List<Diagnostic> contextDiagnostics);
default List<DocumentLink> mapDocumentLinks(BaselineDocumentLinks links) {
return List.of();
}
default List<FoldingRange> mapFoldingRanges(BaselineFoldingRanges ranges) {
return List.of();
}
default List<TextEdit> mapFormatting(BaselineFormatting formatting) {
return List.of();
}
default List<CallHierarchyItem> mapCallHierarchyItems(List<BaselineCallHierarchyItem> items) {
return List.of();
}
default List<CallHierarchyIncomingCall> mapIncomingCalls(List<BaselineCallSite> calls) {
return List.of();
}
default List<CallHierarchyOutgoingCall> mapOutgoingCalls(List<BaselineCallSite> calls) {
return List.of();
}
default List<SelectionRange> mapSelectionRanges(BaselineSelectionRanges ranges) {
return List.of();
}
CompletionList mapCompletion(BaselineCompletion completion);
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);

View File

@ -1,6 +1,7 @@
package p.studio.lsp.services.compiler;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.FrontendSpec;
@ -8,15 +9,21 @@ import p.studio.compiler.models.IRBackend;
import p.studio.compiler.services.FrontendPhaseService;
import p.studio.compiler.services.FrontendProvider;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
import p.studio.lsp.messages.BaselineIssueSeverity;
import p.studio.lsp.messages.DocumentPosition;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlySet;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CompilerLanguageServiceBridgeTest {
@ -51,9 +58,30 @@ class CompilerLanguageServiceBridgeTest {
final var references = bridge.references(context, documentUri, "fn main() {}", 0, 0, true);
assertTrue(references.locations().isEmpty());
final var documentSymbols = bridge.documentSymbols(context, documentUri, "fn main() {}");
assertTrue(documentSymbols.symbols().isEmpty());
final var workspaceSymbols = bridge.workspaceSymbols(context, "frame");
assertTrue(workspaceSymbols.symbols().isEmpty());
final var prepareRename = bridge.prepareRename(context, documentUri, "fn main() {}", 0, 0);
assertFalse(prepareRename.renameable());
final var rename = bridge.rename(context, documentUri, "fn main() {}", 0, 0, "other");
assertFalse(rename.applied());
assertTrue(rename.edits().isEmpty());
final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}");
assertTrue(semanticTokens.legend().isEmpty());
assertTrue(semanticTokens.tokens().isEmpty());
assertTrue(bridge.codeActions(context, documentUri, "fn main() {}", List.of(
new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1)))
.actions().isEmpty());
assertTrue(bridge.documentLinks(context, documentUri, "fn main() {}").links().isEmpty());
assertTrue(bridge.formatDocument(context, documentUri, "fn main() {}").edits().isEmpty());
assertTrue(bridge.foldingRanges(context, documentUri, "fn main() {}").ranges().isEmpty());
assertTrue(bridge.selectionRanges(context, documentUri, "fn main() {}", List.of(new DocumentPosition(0, 0)))
.ranges().isEmpty());
}
@Test
@ -80,6 +108,9 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.severity() == BaselineIssueSeverity.ERROR));
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.startLine() == 1));
assertTrue(analysis.issues().stream().noneMatch(issue -> issue.message().contains("Compiler-backed baseline")));
assertTrue(analysis.issues().stream().allMatch(issue -> issue.source().equals("pbs")));
assertTrue(analysis.issues().stream().noneMatch(issue -> issue.source().equals(issue.code())));
assertTrue(analysis.issues().stream().anyMatch(issue -> !issue.code().isBlank()));
}
@Test
@ -141,6 +172,112 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-builtin-type")));
}
@Test
void semanticTokenOverlayReplacesAResolvedUseAndKeepsBrokenText(@TempDir final Path tempDir) throws IOException {
final Path projectRoot = tempDir.resolve("app");
final Path source = projectRoot.resolve("src").resolve("main.pbs");
Files.createDirectories(source.getParent());
Files.writeString(projectRoot.resolve("prometeu.json"), """
{
"name": "app",
"version": "1.0.0",
"language": "pbs",
"stdlib": "1",
"target": "Game",
"dependencies": []
}
""");
final String text = """
fn helper() -> int {
return 1;
}
fn main() -> int {
return helper;
}
""";
Files.writeString(source, text);
Files.writeString(source.getParent().resolve("mod.barrel"), """
pub fn helper() -> int;
pub fn main() -> int;
""");
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final Path realSource = source.toRealPath();
final var tokens = bridge.semanticTokens(
new LspProjectContext("app", "pbs", projectRoot.toRealPath()),
realSource.toUri().toString(),
text);
final int helperUse = text.indexOf("return helper") + "return ".length();
assertTrue(tokens.tokens().stream().anyMatch(token ->
token.semanticKey().equals("pbs-function")
&& token.line() == new DocumentPositionMapper(text).positionOf(helperUse).line()
&& token.startCharacter() == new DocumentPositionMapper(text).positionOf(helperUse).character()));
final String broken = "fn main( {";
final var brokenTokens = bridge.semanticTokens(
new LspProjectContext("app", "pbs", projectRoot.toRealPath()),
realSource.toUri().toString(),
broken);
assertTrue(brokenTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-keyword")));
assertTrue(brokenTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-punctuation")));
}
@Test
void callHierarchyUsesAstCallsAndRefusesAStruct(@TempDir final Path tempDir) throws IOException {
final Path projectRoot = tempDir.resolve("app");
final Path source = projectRoot.resolve("src").resolve("main.pbs");
Files.createDirectories(source.getParent());
Files.writeString(projectRoot.resolve("prometeu.json"), """
{
"name": "app",
"version": "1.0.0",
"language": "pbs",
"stdlib": "1",
"target": "Game",
"dependencies": []
}
""");
final String text = """
fn helper() -> int {
return 1;
}
fn main() -> int {
return helper();
}
""";
Files.writeString(source, text);
Files.writeString(source.getParent().resolve("mod.barrel"), """
pub fn helper() -> int;
pub fn main() -> int;
""");
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final LspProjectContext context = new LspProjectContext("app", "pbs", projectRoot.toRealPath());
final String uri = source.toRealPath().toUri().toString();
final DocumentPositionMapper mapper = new DocumentPositionMapper(text);
final var mainPosition = mapper.positionOf(text.indexOf("fn main") + 1);
final var prepared = bridge.prepareCallHierarchy(
context,
uri,
text,
mainPosition.line(),
mainPosition.character());
assertEquals(1, prepared.size());
assertEquals("main", prepared.getFirst().name());
final var outgoing = bridge.outgoingCalls(context, uri, text, prepared.getFirst());
assertTrue(outgoing.stream().anyMatch(call -> call.item().name().equals("helper")));
final var structPosition = mapper.positionOf(text.indexOf("fn helper") + 1);
final var helper = bridge.prepareCallHierarchy(
context,
uri,
text,
structPosition.line(),
structPosition.character());
assertEquals("helper", helper.getFirst().name());
assertTrue(bridge.incomingCalls(context, uri, text, helper.getFirst()).stream()
.anyMatch(call -> call.item().name().equals("main")));
}
@Test
void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
@ -474,6 +611,261 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(builtinReferences.locations().isEmpty());
}
@Test
void renameRewritesPhysicalFunctionSitesAndRefusesStdlib() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
.resolve("test-projects")
.resolve("main")
.toAbsolutePath()
.normalize();
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
final String overlay = """
import { Gfx } from @sdk:gfx;
fn helper() -> int { return 42; }
fn frame() -> void {
helper();
helper();
Gfx.clear(5);
let n: int = 1;
// helper
let text = "helper";
}
""";
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay);
final String documentUri = documentPath.toUri().toString();
final var helperUse = mapper.positionOf(overlay.indexOf("helper();"));
final var prepared = bridge.prepareRename(
context,
documentUri,
overlay,
helperUse.line(),
helperUse.character());
assertTrue(prepared.renameable(), prepared.toString());
final var renamed = bridge.rename(
context,
documentUri,
overlay,
helperUse.line(),
helperUse.character(),
"aid");
assertTrue(renamed.applied(), renamed.message());
assertEquals(3, renamed.edits().size(), renamed.edits().toString());
assertTrue(renamed.edits().stream().allMatch(edit -> edit.newText().equals("aid")));
assertTrue(renamed.edits().stream().allMatch(edit -> edit.uri().equals(documentUri)));
final var clearUse = mapper.positionOf(overlay.indexOf("clear"));
final var stdlibPrepared = bridge.prepareRename(
context,
documentUri,
overlay,
clearUse.line(),
clearUse.character());
assertFalse(stdlibPrepared.renameable());
final var stdlibRename = bridge.rename(
context,
documentUri,
overlay,
clearUse.line(),
clearUse.character(),
"wipe");
assertFalse(stdlibRename.applied());
assertTrue(stdlibRename.edits().isEmpty());
}
@Test
void quickFixDeletesTheDuplicateDocDiagnosticAndIgnoresOtherCodes() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
.resolve("test-projects")
.resolve("main")
.toAbsolutePath()
.normalize();
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
final String overlay = """
[Doc(markdown = \"""
First.
\""")]
[Doc(markdown = \"""
Second.
\""")]
fn duplicated() -> void { return; }
fn frame() -> void { return; }
""";
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
final String documentUri = documentPath.toUri().toString();
final var analysis = bridge.analyzeDocument(context, documentUri, overlay);
final var duplicates = analysis.issues().stream()
.filter(issue -> issue.code().equals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE"))
.toList();
assertEquals(1, duplicates.size(), analysis.issues().toString());
final var duplicate = duplicates.getFirst();
assertFalse(duplicate.code().isBlank());
final var diagnostic = new BaselineCodeActionDiagnostic(
duplicate.code(),
duplicate.startLine(),
duplicate.startCharacter(),
duplicate.endLine(),
duplicate.endCharacter());
final var actions = bridge.codeActions(context, documentUri, overlay, List.of(diagnostic));
assertEquals(1, actions.actions().size(), actions.toString());
final var action = actions.actions().getFirst();
assertEquals("Remove duplicate reserved attribute", action.title());
assertEquals(documentUri, action.uri());
assertEquals("", action.newText());
assertEquals(diagnostic.startLine(), action.startLine());
assertEquals(diagnostic.startCharacter(), action.startCharacter());
assertEquals(diagnostic.endLine(), action.endLine());
assertEquals(diagnostic.endCharacter(), action.endCharacter());
final String removed = sourceSlice(overlay, action.startLine(), action.startCharacter(), action.endLine(), action.endCharacter());
assertTrue(removed.contains("Second."), removed);
assertFalse(removed.contains("First."), removed);
final var wrongCode = new BaselineCodeActionDiagnostic(
"E_SEM_MALFORMED_RESERVED_ATTRIBUTE",
diagnostic.startLine(),
diagnostic.startCharacter(),
diagnostic.endLine(),
diagnostic.endCharacter());
assertTrue(bridge.codeActions(context, documentUri, overlay, List.of(wrongCode)).actions().isEmpty());
assertTrue(bridge.codeActions(context, documentUri, overlay, List.of()).actions().isEmpty());
final String malformed = """
[Doc(text = \"""
Wrong argument.
\""")]
fn frame() -> void { return; }
""";
final var malformedAnalysis = bridge.analyzeDocument(context, documentUri, malformed);
final var malformedDiagnostics = malformedAnalysis.issues().stream()
.map(issue -> new BaselineCodeActionDiagnostic(
issue.code(),
issue.startLine(),
issue.startCharacter(),
issue.endLine(),
issue.endCharacter()))
.toList();
assertTrue(bridge.codeActions(context, documentUri, malformed, malformedDiagnostics).actions().isEmpty());
}
private static String sourceSlice(
final String source,
final int startLine,
final int startCharacter,
final int endLine,
final int endCharacter) {
final DocumentPositionMapper mapper = new DocumentPositionMapper(source);
final byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
final int start = mapper.byteOffsetOf(startLine, startCharacter);
final int end = mapper.byteOffsetOf(endLine, endCharacter);
return new String(bytes, start, Math.max(0, end - start), StandardCharsets.UTF_8);
}
@Test
void documentSymbolsUseAstHierarchyOfTheOpenPbsFile() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
.resolve("test-projects")
.resolve("main")
.toAbsolutePath()
.normalize();
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
final String overlay = """
import { Gfx } from @sdk:gfx;
declare struct Vec(x: int) {
ctor make(x: int) { return; }
fn blend(dx: int, dy: int) -> int { return dx; }
}
fn helper() -> int { return 42; }
fn helper(value: int) -> int { return value; }
fn frame() -> void {
let local = 1;
helper(local);
}
""";
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final var symbols = bridge.documentSymbols(
new LspProjectContext("main", "pbs", projectRoot),
documentPath.toUri().toString(),
overlay).symbols();
assertEquals(List.of("Vec", "helper", "helper", "frame"), symbols.stream().map(symbol -> symbol.name()).toList());
assertTrue(symbols.stream().noneMatch(symbol -> symbol.name().equals("Gfx") || symbol.name().equals("local")));
final var vec = symbols.getFirst();
assertEquals(p.studio.lsp.messages.BaselineDocumentSymbolKind.STRUCT, vec.kind());
assertEquals(List.of("x", "make", "blend"), vec.children().stream().map(symbol -> symbol.name()).toList());
assertEquals("(dx: int, dy: int) -> int", vec.children().get(2).detail());
assertEquals("helper", symbols.get(1).name());
assertEquals("helper", symbols.get(2).name());
assertNotEquals(symbols.get(1).detail(), symbols.get(2).detail());
}
@Test
void workspaceSymbolsSearchPhysicalNamedDeclarationsWithoutAnOpenDocument() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
.resolve("test-projects")
.resolve("main")
.toAbsolutePath()
.normalize();
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final var context = new LspProjectContext("main", "pbs", projectRoot);
final var frameHits = bridge.workspaceSymbols(context, "frame");
assertTrue(frameHits.symbols().stream().anyMatch(symbol ->
symbol.name().equals("frame") && symbol.containerName().isEmpty()));
assertTrue(frameHits.symbols().stream().allMatch(symbol ->
symbol.uri().contains("main.pbs")));
final var caseInsensitive = bridge.workspaceSymbols(context, "BLA");
assertTrue(caseInsensitive.symbols().stream().anyMatch(symbol ->
symbol.name().equals("Bla") && symbol.containerName().isEmpty()));
final var all = bridge.workspaceSymbols(context, "");
assertTrue(all.symbols().stream().anyMatch(symbol ->
symbol.name().equals("a") && symbol.containerName().equals("Bla")));
assertTrue(all.symbols().stream().anyMatch(symbol -> symbol.name().equals("init")));
assertTrue(all.symbols().stream().noneMatch(symbol ->
symbol.name().equals("Gfx")
|| symbol.name().equals("Log")
|| symbol.name().equals("Composer")));
assertTrue(isSortedByNameThenUriThenOffset(all.symbols()));
final var stdlibMiss = bridge.workspaceSymbols(context, "Gfx");
assertTrue(stdlibMiss.symbols().isEmpty());
}
private boolean isSortedByNameThenUriThenOffset(
final List<p.studio.lsp.messages.BaselineWorkspaceSymbol> symbols) {
for (int index = 1; index < symbols.size(); index += 1) {
final var previous = symbols.get(index - 1);
final var current = symbols.get(index);
final int nameOrder = String.CASE_INSENSITIVE_ORDER.compare(previous.name(), current.name());
if (nameOrder > 0) {
return false;
}
if (nameOrder == 0) {
final int uriOrder = previous.uri().compareTo(current.uri());
if (uriOrder > 0) {
return false;
}
if (uriOrder == 0 && previous.startLine() > current.startLine()) {
return false;
}
}
}
return true;
}
@Test
void describeServerPublishesFrontendVisualThemes() {
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
@ -486,6 +878,15 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(description.signatureHelpSupported());
assertTrue(description.definitionSupported());
assertTrue(description.referencesSupported());
assertTrue(description.documentSymbolsSupported());
assertTrue(description.workspaceSymbolsSupported());
assertTrue(description.renameSupported());
assertTrue(description.codeActionsSupported());
assertTrue(description.documentLinksSupported());
assertTrue(description.foldingRangesSupported());
assertTrue(description.selectionRangesSupported());
assertTrue(description.formattingSupported());
assertTrue(description.callHierarchySupported());
assertEquals(1, description.semanticHostProjections().size());
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
@ -499,6 +900,77 @@ class CompilerLanguageServiceBridgeTest {
&& tokenStyle.foreground().equals("#7fb8ff")));
}
@Test
void documentLinksSkipVirtualStdlibImports() throws IOException {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
.resolve("test-projects")
.resolve("main")
.toAbsolutePath()
.normalize();
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
final String text = Files.readString(documentPath);
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final var links = bridge.documentLinks(
new LspProjectContext("main", "pbs", projectRoot),
documentPath.toUri().toString(),
text);
assertTrue(links.links().isEmpty());
}
@Test
void documentLinksPointAtThePhysicalModuleFile(@TempDir final Path tempDir) throws IOException {
final Path projectRoot = tempDir.resolve("app");
final Path sourceA = projectRoot.resolve("src").resolve("a").resolve("source.pbs");
final Path sourceB = projectRoot.resolve("src").resolve("b").resolve("source.pbs");
Files.createDirectories(sourceA.getParent());
Files.createDirectories(sourceB.getParent());
Files.writeString(projectRoot.resolve("prometeu.json"), """
{
"name": "app",
"version": "1.0.0",
"language": "pbs",
"stdlib": "1",
"target": "Game",
"dependencies": []
}
""");
Files.writeString(sourceA, """
fn target() -> int {
return 1;
}
""");
Files.writeString(sourceA.getParent().resolve("mod.barrel"), "pub fn target() -> int;\n");
final String caller = """
import { target } from @app:a;
import { Log } from @sdk:log;
import { Missing } from @app:missing;
fn caller() -> int {
return target();
}
""";
Files.writeString(sourceB, caller);
Files.writeString(sourceB.getParent().resolve("mod.barrel"), "pub fn caller() -> int;\n");
final Path realRoot = projectRoot.toRealPath();
final Path realA = sourceA.toRealPath();
final Path realB = sourceB.toRealPath();
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final var links = bridge.documentLinks(
new LspProjectContext("app", "pbs", realRoot),
realB.toUri().toString(),
caller);
assertEquals(1, links.links().size());
final var link = links.links().getFirst();
assertEquals(realA.toUri().toString(), link.targetUri());
final int start = new DocumentPositionMapper(caller).byteOffsetOf(link.startLine(), link.startCharacter());
final int end = new DocumentPositionMapper(caller).byteOffsetOf(link.endLine(), link.endCharacter());
assertEquals("@app:a", new String(caller.getBytes(StandardCharsets.UTF_8), start, end - start, StandardCharsets.UTF_8));
}
private Path findRepoRoot(final Path start) {
var current = start;
while (current != null) {

View File

@ -101,6 +101,15 @@ class PrometeuLanguageServerTest {
true,
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
false,
List.of(),
List.of(new BaselineSemanticHostProjection(
"vscode",
@ -169,6 +178,42 @@ class PrometeuLanguageServerTest {
return p.studio.lsp.messages.BaselineReferences.empty();
}
@Override
public p.studio.lsp.messages.BaselineDocumentSymbols documentSymbols(
final LspProjectContext project,
final String documentUri,
final String text) {
return p.studio.lsp.messages.BaselineDocumentSymbols.empty();
}
@Override
public p.studio.lsp.messages.BaselineWorkspaceSymbols workspaceSymbols(
final LspProjectContext project,
final String query) {
return p.studio.lsp.messages.BaselineWorkspaceSymbols.empty();
}
@Override
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
final LspProjectContext project,
final String documentUri,
final String text,
final int line,
final int character) {
return p.studio.lsp.messages.BaselinePrepareRename.none();
}
@Override
public p.studio.lsp.messages.BaselineRename rename(
final LspProjectContext project,
final String documentUri,
final String text,
final int line,
final int character,
final String newName) {
return p.studio.lsp.messages.BaselineRename.unsupported();
}
@Override
public BaselineSignatureHelp signatureHelp(
final LspProjectContext project,
@ -184,6 +229,15 @@ class PrometeuLanguageServerTest {
return new BaselineSemanticTokens(List.of(), List.of());
}
@Override
public p.studio.lsp.messages.BaselineCodeActions codeActions(
final LspProjectContext context,
final String documentUri,
final String text,
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
return p.studio.lsp.messages.BaselineCodeActions.empty();
}
@Override
public String onSave(final LspProjectContext project, final String documentUri) {
return "saved";
@ -226,6 +280,39 @@ class PrometeuLanguageServerTest {
throw new UnsupportedOperationException();
}
@Override
public java.util.List<org.eclipse.lsp4j.DocumentSymbol> mapDocumentSymbols(
final p.studio.lsp.messages.BaselineDocumentSymbols symbols) {
throw new UnsupportedOperationException();
}
@Override
public java.util.List<org.eclipse.lsp4j.SymbolInformation> mapWorkspaceSymbols(
final p.studio.lsp.messages.BaselineWorkspaceSymbols symbols) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
org.eclipse.lsp4j.Range,
org.eclipse.lsp4j.PrepareRenameResult,
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
throw new UnsupportedOperationException();
}
@Override
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
final p.studio.lsp.messages.BaselineCodeActions actions,
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
throw new UnsupportedOperationException();

View File

@ -4,6 +4,8 @@ import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.DefinitionParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.ReferenceContext;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
@ -22,6 +24,9 @@ import p.studio.lsp.messages.BaselineCompletionItem;
import p.studio.lsp.messages.BaselineCompletionItemKind;
import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineDefinitionLocation;
import p.studio.lsp.messages.BaselineDocumentSymbol;
import p.studio.lsp.messages.BaselineDocumentSymbolKind;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineReferences;
import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineHover;
@ -102,6 +107,13 @@ final class PrometeuTextDocumentServiceTest {
assertEquals(1, bridge.lastReferencesLine);
assertEquals(2, bridge.lastReferencesCharacter);
assertTrue(bridge.lastIncludeDeclaration);
final var documentSymbols = service.documentSymbol(new DocumentSymbolParams(
new TextDocumentIdentifier(uri))).join();
assertEquals(1, documentSymbols.size());
assertTrue(documentSymbols.getFirst().isRight());
assertSame(mapper.documentSymbols.getFirst(), documentSymbols.getFirst().getRight());
assertEquals(text, bridge.lastDocumentSymbolsText);
}
private static final class RecordingBridge implements LanguageServiceBridge {
@ -116,6 +128,7 @@ final class PrometeuTextDocumentServiceTest {
private int lastReferencesLine;
private int lastReferencesCharacter;
private boolean lastIncludeDeclaration;
private String lastDocumentSymbolsText;
private String lastSignatureHelpText;
private int lastSignatureHelpLine;
private int lastSignatureHelpCharacter;
@ -183,6 +196,55 @@ final class PrometeuTextDocumentServiceTest {
return new BaselineReferences(List.of(new BaselineDefinitionLocation(documentUri, 0, 0, 0, 6)));
}
@Override
public BaselineDocumentSymbols documentSymbols(
final LspProjectContext context,
final String documentUri,
final String text) {
lastDocumentSymbolsText = text;
return new BaselineDocumentSymbols(List.of(new BaselineDocumentSymbol(
"frame",
BaselineDocumentSymbolKind.FUNCTION,
"",
0,
0,
2,
1,
0,
3,
0,
8,
List.of())));
}
@Override
public p.studio.lsp.messages.BaselineWorkspaceSymbols workspaceSymbols(
final LspProjectContext context,
final String query) {
return p.studio.lsp.messages.BaselineWorkspaceSymbols.empty();
}
@Override
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
return p.studio.lsp.messages.BaselinePrepareRename.none();
}
@Override
public p.studio.lsp.messages.BaselineRename rename(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character,
final String newName) {
return p.studio.lsp.messages.BaselineRename.unsupported();
}
@Override
public BaselineSignatureHelp signatureHelp(
final LspProjectContext context,
@ -202,6 +264,15 @@ final class PrometeuTextDocumentServiceTest {
return new BaselineSemanticTokens(List.of(), List.of());
}
@Override
public p.studio.lsp.messages.BaselineCodeActions codeActions(
final LspProjectContext context,
final String documentUri,
final String text,
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
return p.studio.lsp.messages.BaselineCodeActions.empty();
}
@Override
public String onSave(final LspProjectContext context, final String documentUri) {
return "saved";
@ -213,6 +284,7 @@ final class PrometeuTextDocumentServiceTest {
private final SignatureHelp signatureHelp = new SignatureHelp();
private final List<Location> definitionLocations = List.of();
private final List<Location> referenceLocations = List.of();
private final List<DocumentSymbol> documentSymbols = List.of(new DocumentSymbol());
private BaselineSignatureHelp mappedSignatureHelp;
@Override
@ -250,6 +322,38 @@ final class PrometeuTextDocumentServiceTest {
return referenceLocations;
}
@Override
public List<DocumentSymbol> mapDocumentSymbols(final BaselineDocumentSymbols symbols) {
return documentSymbols;
}
@Override
public List<org.eclipse.lsp4j.SymbolInformation> mapWorkspaceSymbols(
final p.studio.lsp.messages.BaselineWorkspaceSymbols symbols) {
return List.of();
}
@Override
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
org.eclipse.lsp4j.Range,
org.eclipse.lsp4j.PrepareRenameResult,
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
return null;
}
@Override
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
return new org.eclipse.lsp4j.WorkspaceEdit();
}
@Override
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
final p.studio.lsp.messages.BaselineCodeActions actions,
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
return java.util.List.of();
}
@Override
public CompletionList mapCompletion(final BaselineCompletion completion) {
return completionList;

View File

@ -0,0 +1,280 @@
package p.studio.lsp.services.protocol;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineDocumentSymbolKind;
import p.studio.lsp.messages.BaselineHover;
import p.studio.lsp.messages.BaselineReferences;
import p.studio.lsp.messages.BaselineSemanticTokens;
import p.studio.lsp.messages.BaselineServerDescription;
import p.studio.lsp.messages.BaselineSignatureHelp;
import p.studio.lsp.messages.BaselineWorkspaceSymbol;
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class PrometeuWorkspaceServiceTest {
@Test
void symbolDelegatesQueryWithoutADocumentUri() {
final var bridge = new RecordingBridge();
final var mapper = new RecordingMapper();
final var project = new LspProjectContext("main", "pbs", Path.of("."));
final var service = new PrometeuWorkspaceService(project, bridge, mapper);
final var result = service.symbol(new WorkspaceSymbolParams("blend")).join();
assertTrue(result.isLeft());
assertSame(mapper.workspaceSymbols, result.getLeft());
assertEquals("blend", bridge.lastQuery);
assertSame(project, bridge.lastContext);
}
private static final class RecordingBridge implements LanguageServiceBridge {
private LspProjectContext lastContext;
private String lastQuery;
@Override
public BaselineServerDescription describeServer(final LspProjectContext context) {
throw new UnsupportedOperationException();
}
@Override
public BaselineDocumentAnalysis analyzeDocument(
final LspProjectContext context,
final String documentUri,
final String text) {
throw new UnsupportedOperationException();
}
@Override
public BaselineCompletion completion(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
throw new UnsupportedOperationException();
}
@Override
public BaselineHover hover(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
throw new UnsupportedOperationException();
}
@Override
public BaselineDefinition definition(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
throw new UnsupportedOperationException();
}
@Override
public BaselineReferences references(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character,
final boolean includeDeclaration) {
throw new UnsupportedOperationException();
}
@Override
public BaselineDocumentSymbols documentSymbols(
final LspProjectContext context,
final String documentUri,
final String text) {
throw new UnsupportedOperationException();
}
@Override
public BaselineWorkspaceSymbols workspaceSymbols(
final LspProjectContext context,
final String query) {
lastContext = context;
lastQuery = query;
return new BaselineWorkspaceSymbols(List.of(new BaselineWorkspaceSymbol(
"blend",
BaselineDocumentSymbolKind.METHOD,
"",
"Vec",
"file:///tmp/demo.pbs",
0,
0,
0,
5)));
}
@Override
public BaselineSignatureHelp signatureHelp(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
throw new UnsupportedOperationException();
}
@Override
public BaselineSemanticTokens semanticTokens(
final LspProjectContext context,
final String documentUri,
final String text) {
throw new UnsupportedOperationException();
}
@Override
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
throw new UnsupportedOperationException();
}
@Override
public p.studio.lsp.messages.BaselineRename rename(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character,
final String newName) {
throw new UnsupportedOperationException();
}
@Override
public p.studio.lsp.messages.BaselineCodeActions codeActions(
final LspProjectContext context,
final String documentUri,
final String text,
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
throw new UnsupportedOperationException();
}
@Override
public String onSave(final LspProjectContext context, final String documentUri) {
throw new UnsupportedOperationException();
}
}
private static final class RecordingMapper implements ProtocolMessageMapper {
private final List<SymbolInformation> workspaceSymbols = List.of(new SymbolInformation());
@Override
public org.eclipse.lsp4j.InitializeResult mapInitializeResult(final BaselineServerDescription description) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.PublishDiagnosticsParams mapDiagnostics(
final String uri,
final BaselineDocumentAnalysis analysis) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.PublishDiagnosticsParams emptyDiagnostics(final String uri) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.Hover mapHover(final BaselineHover hover) {
throw new UnsupportedOperationException();
}
@Override
public List<org.eclipse.lsp4j.Location> mapDefinition(final BaselineDefinition definition) {
throw new UnsupportedOperationException();
}
@Override
public List<org.eclipse.lsp4j.Location> mapReferences(final BaselineReferences references) {
throw new UnsupportedOperationException();
}
@Override
public List<org.eclipse.lsp4j.DocumentSymbol> mapDocumentSymbols(final BaselineDocumentSymbols symbols) {
throw new UnsupportedOperationException();
}
@Override
public List<SymbolInformation> mapWorkspaceSymbols(final BaselineWorkspaceSymbols symbols) {
return workspaceSymbols;
}
@Override
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
org.eclipse.lsp4j.Range,
org.eclipse.lsp4j.PrepareRenameResult,
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
throw new UnsupportedOperationException();
}
@Override
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
final p.studio.lsp.messages.BaselineCodeActions actions,
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.SignatureHelp mapSignatureHelp(final BaselineSignatureHelp signatureHelp) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.MessageParams mapInfoMessage(final String message) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.MessageParams mapErrorMessage(final String message) {
throw new UnsupportedOperationException();
}
@Override
public org.eclipse.lsp4j.MessageParams mapWarningMessage(final String message) {
throw new UnsupportedOperationException();
}
}
}

View File

@ -1,13 +1,25 @@
package p.studio.lsp.services.protocol.mapping;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.BaselineCodeAction;
import p.studio.lsp.messages.BaselineCodeActions;
import p.studio.lsp.messages.BaselineCompletion;
import p.studio.lsp.messages.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineDocumentIssue;
import p.studio.lsp.messages.BaselineDocumentLink;
import p.studio.lsp.messages.BaselineDocumentLinks;
import p.studio.lsp.messages.BaselineIssueSeverity;
import p.studio.lsp.messages.BaselineCompletionItem;
import p.studio.lsp.messages.BaselineCompletionItemKind;
import p.studio.lsp.messages.BaselineDefinition;
import p.studio.lsp.messages.BaselineDefinitionLocation;
import p.studio.lsp.messages.BaselineDocumentSymbol;
import p.studio.lsp.messages.BaselineDocumentSymbolKind;
import p.studio.lsp.messages.BaselineDocumentSymbols;
import p.studio.lsp.messages.BaselineReferences;
import p.studio.lsp.messages.BaselineEditorPalette;
import p.studio.lsp.messages.BaselineSemanticHostProjection;
@ -24,6 +36,7 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class Lsp4jProtocolMessageMapperTest {
@ -40,6 +53,15 @@ final class Lsp4jProtocolMessageMapperTest {
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection(
"vscode",
@ -62,6 +84,19 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals(List.of("(", ","), result.getCapabilities().getSignatureHelpProvider().getTriggerCharacters());
assertEquals(Boolean.TRUE, result.getCapabilities().getDefinitionProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentSymbolProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getWorkspaceSymbolProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getRenameProvider().getRight().getPrepareProvider());
assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds());
assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider());
assertEquals(Boolean.FALSE, result.getCapabilities().getDocumentLinkProvider().getResolveProvider());
assertEquals(Boolean.TRUE, result.getCapabilities().getFoldingRangeProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getSelectionRangeProvider().getLeft());
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentFormattingProvider().getLeft());
assertNull(result.getCapabilities().getDocumentRangeFormattingProvider());
assertNull(result.getCapabilities().getDocumentOnTypeFormattingProvider());
assertEquals(Boolean.TRUE, result.getCapabilities().getCallHierarchyProvider().getLeft());
assertNull(result.getCapabilities().getTypeHierarchyProvider());
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
@ -135,4 +170,202 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals(3, locations.getFirst().getRange().getStart().getCharacter());
assertTrue(mapper.mapReferences(BaselineReferences.empty()).isEmpty());
}
@Test
void documentSymbolsMapToHierarchicalDocumentSymbolTrees() {
final var mapper = new Lsp4jProtocolMessageMapper();
final var mapped = mapper.mapDocumentSymbols(new BaselineDocumentSymbols(List.of(
new BaselineDocumentSymbol(
"Mode",
BaselineDocumentSymbolKind.ENUM,
"",
0,
0,
0,
28,
0,
14,
0,
18,
List.of(new BaselineDocumentSymbol(
"Idle",
BaselineDocumentSymbolKind.ENUM,
"0",
0,
19,
0,
23,
0,
19,
0,
23,
List.of()))),
new BaselineDocumentSymbol(
"Point",
BaselineDocumentSymbolKind.STRUCT,
"",
1,
0,
4,
1,
1,
15,
1,
20,
List.of(new BaselineDocumentSymbol(
"x",
BaselineDocumentSymbolKind.FIELD,
"int",
1,
21,
1,
27,
1,
21,
1,
22,
List.of()))))));
assertEquals(2, mapped.size());
assertEquals("Mode", mapped.getFirst().getName());
assertEquals(SymbolKind.Enum, mapped.getFirst().getKind());
assertEquals("Idle", mapped.getFirst().getChildren().getFirst().getName());
assertEquals(SymbolKind.EnumMember, mapped.getFirst().getChildren().getFirst().getKind());
assertEquals("Point", mapped.get(1).getName());
assertEquals(SymbolKind.Struct, mapped.get(1).getKind());
assertEquals("x", mapped.get(1).getChildren().getFirst().getName());
assertEquals(SymbolKind.Field, mapped.get(1).getChildren().getFirst().getKind());
assertTrue(mapper.mapDocumentSymbols(BaselineDocumentSymbols.empty()).isEmpty());
}
@Test
void workspaceSymbolsMapToFlatSymbolInformation() {
final var mapper = new Lsp4jProtocolMessageMapper();
final var mapped = mapper.mapWorkspaceSymbols(new p.studio.lsp.messages.BaselineWorkspaceSymbols(List.of(
new p.studio.lsp.messages.BaselineWorkspaceSymbol(
"blend",
BaselineDocumentSymbolKind.METHOD,
"(dx: int, dy: int) -> int",
"Vec",
"file:///tmp/demo.pbs",
3,
2,
3,
7),
new p.studio.lsp.messages.BaselineWorkspaceSymbol(
"Idle",
BaselineDocumentSymbolKind.ENUM,
"0",
"Mode",
"file:///tmp/demo.pbs",
0,
19,
0,
23))));
assertEquals(2, mapped.size());
assertEquals("blend", mapped.getFirst().getName());
assertEquals("Vec", mapped.getFirst().getContainerName());
assertEquals(SymbolKind.Method, mapped.getFirst().getKind());
assertEquals("file:///tmp/demo.pbs", mapped.getFirst().getLocation().getUri());
assertEquals(2, mapped.getFirst().getLocation().getRange().getStart().getCharacter());
assertEquals("Idle", mapped.get(1).getName());
assertEquals(SymbolKind.EnumMember, mapped.get(1).getKind());
assertTrue(mapper.mapWorkspaceSymbols(p.studio.lsp.messages.BaselineWorkspaceSymbols.empty()).isEmpty());
}
@Test
void diagnosticsPublishStableCodeAndQuickFixesAreEditsNotCommands() {
final var mapper = new Lsp4jProtocolMessageMapper();
final var published = mapper.mapDiagnostics("file:///tmp/demo.pbs", new BaselineDocumentAnalysis(List.of(
new BaselineDocumentIssue(
1,
2,
1,
18,
BaselineIssueSeverity.ERROR,
"pbs",
"Duplicate Doc attribute on the same declaration surface",
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE"))));
final var diagnostic = published.getDiagnostics().getFirst();
assertEquals("pbs", diagnostic.getSource());
assertEquals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", diagnostic.getCode().getLeft());
assertNull(diagnostic.getData());
assertTrue(diagnostic.getRelatedInformation() == null || diagnostic.getRelatedInformation().isEmpty());
final var mapped = mapper.mapCodeActions(new BaselineCodeActions(List.of(new BaselineCodeAction(
"Remove duplicate reserved attribute",
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
"file:///tmp/demo.pbs",
1,
2,
1,
18,
""))), List.of(diagnostic));
assertEquals(1, mapped.size());
assertTrue(mapped.getFirst().isRight());
assertFalse(mapped.getFirst().isLeft());
final var action = mapped.getFirst().getRight();
assertEquals(CodeActionKind.QuickFix, action.getKind());
assertNull(action.getCommand());
assertEquals("", action.getEdit().getChanges().get("file:///tmp/demo.pbs").getFirst().getNewText());
assertEquals(1, action.getDiagnostics().size());
assertEquals(diagnostic, action.getDiagnostics().getFirst());
}
@Test
void documentLinksMapRangeAndTargetWithoutResolve() {
final var mapper = new Lsp4jProtocolMessageMapper();
final var unsupported = mapper.mapInitializeResult(serverDescription(false));
assertNull(unsupported.getCapabilities().getDocumentLinkProvider());
assertNull(unsupported.getCapabilities().getFoldingRangeProvider());
assertNull(unsupported.getCapabilities().getSelectionRangeProvider());
final var mapped = mapper.mapDocumentLinks(new BaselineDocumentLinks(List.of(
new BaselineDocumentLink("file:///tmp/a/source.pbs", 0, 19, 0, 25))));
assertEquals(1, mapped.size());
assertEquals("file:///tmp/a/source.pbs", mapped.getFirst().getTarget());
assertEquals(0, mapped.getFirst().getRange().getStart().getLine());
assertEquals(19, mapped.getFirst().getRange().getStart().getCharacter());
assertEquals(25, mapped.getFirst().getRange().getEnd().getCharacter());
assertNull(mapped.getFirst().getData());
assertTrue(mapper.mapDocumentLinks(BaselineDocumentLinks.empty()).isEmpty());
}
private static BaselineServerDescription serverDescription(final boolean documentLinksSupported) {
return new BaselineServerDescription(
"demo",
"1",
"pbs",
true,
true,
true,
true,
true,
true,
true,
true,
true,
documentLinksSupported,
documentLinksSupported,
documentLinksSupported,
documentLinksSupported,
documentLinksSupported,
List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection(
"vscode",
List.of(new BaselineSemanticHostProjectionEntry(
"demo-keyword",
"keyword",
List.of(),
"variable",
List.of())))),
List.of(new BaselineVisualTheme(
"demo-default",
"Demo Default",
new BaselineEditorPalette("#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777"),
List.of(new BaselineTokenStyle("demo-keyword", "#888888", false, false, false)))),
"demo-default");
}
}

View File

@ -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);
}
}