From 099857b63605152737c61334fd49fdac9f8f69b7 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 08:11:06 +0100 Subject: [PATCH 1/8] 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. --- discussion/index.ndjson | 21 +-- .../agendas/AGD-0048-pbs-lsp-formatting.md | 4 +- .../AGD-0049-pbs-lsp-import-assistance.md | 4 +- ...0-pbs-lsp-semantic-token-classification.md | 4 +- ...pbs-lsp-performance-cache-and-snapshots.md | 4 +- .../AGD-0052-pbs-lsp-completion-depth.md | 4 +- ...53-pbs-lsp-folding-and-selection-ranges.md | 4 +- .../AGD-0054-pbs-lsp-diagnostics-ux.md | 4 +- .../AGD-0055-pbs-lsp-document-links.md | 4 +- ...GD-0056-pbs-lsp-call-and-type-hierarchy.md | 4 +- ...D-0069-pbs-lsp-remaining-editor-surface.md | 127 ++++++++++++++++++ ...C-0058-pbs-lsp-remaining-editor-surface.md | 78 +++++++++++ ...N-0137-pbs-lsp-remaining-editor-surface.md | 126 +++++++++++++++++ ...ler Pipeline Entry Points Specification.md | 2 + .../CompilerLanguageServiceBridge.java | 16 ++- .../CompilerLanguageServiceBridgeTest.java | 3 + .../Lsp4jProtocolMessageMapperTest.java | 4 +- 17 files changed, 379 insertions(+), 34 deletions(-) create mode 100644 discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md create mode 100644 discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md create mode 100644 discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index ab63d2a0..a214584d 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,5 @@ -{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":58,"PLN":137,"LSN":74,"CLSN":1}} +{"type":"meta","next_id":{"DSC":67,"AGD":70,"DEC":59,"PLN":138,"LSN":74,"CLSN":1}} +{"type":"discussion","id":"DSC-0066","status":"in_progress","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":[{"id":"AGD-0069","file":"AGD-0069-pbs-lsp-remaining-editor-surface.md","status":"accepted","created_at":"2026-09-22","updated_at":"2026-09-22"}],"decisions":[{"id":"DEC-0058","file":"DEC-0058-pbs-lsp-remaining-editor-surface.md","status":"accepted","created_at":"2026-09-22","updated_at":"2026-09-22","ref_agenda":"AGD-0069"}],"plans":[{"id":"PLN-0137","file":"PLN-0137-pbs-lsp-remaining-editor-surface.md","status":"in_progress","created_at":"2026-09-22","updated_at":"2026-09-22","ref_decisions":["DEC-0058"]}],"lessons":[]} {"type":"discussion","id":"DSC-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} @@ -11,15 +12,15 @@ {"type":"discussion","id":"DSC-0056","status":"done","ticket":"multi-frontend-remove-pbs-branches","title":"Generalizar o contrato LSP/editorial para frontends","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0058","file":"discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0055","status":"done","ticket":"multi-frontend-compiler-vs-language-services","title":"Separar compilacao de servicos editoriais de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","lsp","editor","frontend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0056","file":"discussion/lessons/DSC-0055-multi-frontend-compiler-vs-language-services/LSN-0056-compile-first-frontends-with-optional-editorial-capabilities.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0054","status":"done","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0055","file":"discussion/lessons/DSC-0054-multi-frontend-provider-contract/LSN-0055-static-frontend-providers-before-plugin-architecture.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} -{"type":"discussion","id":"DSC-0053","status":"open","ticket":"pbs-lsp-call-and-type-hierarchy","title":"PBS LSP Call and Type Hierarchy","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","call-hierarchy","type-hierarchy"],"agendas":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0052","status":"open","ticket":"pbs-lsp-document-links","title":"PBS LSP Document Links","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","document-links","imports"],"agendas":[{"id":"AGD-0055","file":"AGD-0055-pbs-lsp-document-links.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0051","status":"open","ticket":"pbs-lsp-diagnostics-ux","title":"PBS LSP Diagnostics UX","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","diagnostics","ux"],"agendas":[{"id":"AGD-0054","file":"AGD-0054-pbs-lsp-diagnostics-ux.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0050","status":"open","ticket":"pbs-lsp-folding-and-selection-ranges","title":"PBS LSP Folding and Selection Ranges","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","folding","selection-range"],"agendas":[{"id":"AGD-0053","file":"AGD-0053-pbs-lsp-folding-and-selection-ranges.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0049","status":"open","ticket":"pbs-lsp-completion-depth","title":"PBS LSP Completion Depth","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","completion"],"agendas":[{"id":"AGD-0052","file":"AGD-0052-pbs-lsp-completion-depth.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0048","status":"open","ticket":"pbs-lsp-performance-cache-and-snapshots","title":"PBS LSP Performance Cache and Snapshots","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","performance","caching"],"agendas":[{"id":"AGD-0051","file":"AGD-0051-pbs-lsp-performance-cache-and-snapshots.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0047","status":"open","ticket":"pbs-lsp-semantic-tokens-semantic-classification","title":"PBS LSP Semantic Token Classification","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","semantic-tokens"],"agendas":[{"id":"AGD-0050","file":"AGD-0050-pbs-lsp-semantic-token-classification.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0046","status":"open","ticket":"pbs-lsp-import-assistance","title":"PBS LSP Import Assistance","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","imports","completion"],"agendas":[{"id":"AGD-0049","file":"AGD-0049-pbs-lsp-import-assistance.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0045","status":"open","ticket":"pbs-lsp-formatting","title":"PBS LSP Formatting","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","formatting"],"agendas":[{"id":"AGD-0048","file":"AGD-0048-pbs-lsp-formatting.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0053","status":"abandoned","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":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0052","status":"abandoned","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":[{"id":"AGD-0055","file":"AGD-0055-pbs-lsp-document-links.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0051","status":"abandoned","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":[{"id":"AGD-0054","file":"AGD-0054-pbs-lsp-diagnostics-ux.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0050","status":"abandoned","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":[{"id":"AGD-0053","file":"AGD-0053-pbs-lsp-folding-and-selection-ranges.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0049","status":"abandoned","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":[{"id":"AGD-0052","file":"AGD-0052-pbs-lsp-completion-depth.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0048","status":"abandoned","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":[{"id":"AGD-0051","file":"AGD-0051-pbs-lsp-performance-cache-and-snapshots.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0047","status":"abandoned","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":[{"id":"AGD-0050","file":"AGD-0050-pbs-lsp-semantic-token-classification.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0046","status":"abandoned","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":[{"id":"AGD-0049","file":"AGD-0049-pbs-lsp-import-assistance.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0045","status":"abandoned","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":[{"id":"AGD-0048","file":"AGD-0048-pbs-lsp-formatting.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"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"}]} diff --git a/discussion/workflow/agendas/AGD-0048-pbs-lsp-formatting.md b/discussion/workflow/agendas/AGD-0048-pbs-lsp-formatting.md index a6020d49..461fdb7d 100644 --- a/discussion/workflow/agendas/AGD-0048-pbs-lsp-formatting.md +++ b/discussion/workflow/agendas/AGD-0048-pbs-lsp-formatting.md @@ -2,7 +2,7 @@ id: AGD-0048 ticket: pbs-lsp-formatting title: PBS LSP Formatting -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ Formatter decision should define canonical style before implementation. ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0049-pbs-lsp-import-assistance.md b/discussion/workflow/agendas/AGD-0049-pbs-lsp-import-assistance.md index cd6617eb..ca78f160 100644 --- a/discussion/workflow/agendas/AGD-0049-pbs-lsp-import-assistance.md +++ b/discussion/workflow/agendas/AGD-0049-pbs-lsp-import-assistance.md @@ -2,7 +2,7 @@ id: AGD-0049 ticket: pbs-lsp-import-assistance title: PBS LSP Import Assistance -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ This should be coordinated with code actions and formatter. ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0050-pbs-lsp-semantic-token-classification.md b/discussion/workflow/agendas/AGD-0050-pbs-lsp-semantic-token-classification.md index 0f31bab5..9dca0b3a 100644 --- a/discussion/workflow/agendas/AGD-0050-pbs-lsp-semantic-token-classification.md +++ b/discussion/workflow/agendas/AGD-0050-pbs-lsp-semantic-token-classification.md @@ -2,7 +2,7 @@ id: AGD-0050 ticket: pbs-lsp-semantic-tokens-semantic-classification title: PBS LSP Semantic Token Classification -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ This can proceed independently but benefits from LSP snapshot/cache work. ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0051-pbs-lsp-performance-cache-and-snapshots.md b/discussion/workflow/agendas/AGD-0051-pbs-lsp-performance-cache-and-snapshots.md index c6fbe030..a663f47f 100644 --- a/discussion/workflow/agendas/AGD-0051-pbs-lsp-performance-cache-and-snapshots.md +++ b/discussion/workflow/agendas/AGD-0051-pbs-lsp-performance-cache-and-snapshots.md @@ -2,7 +2,7 @@ id: AGD-0051 ticket: pbs-lsp-performance-cache-and-snapshots title: PBS LSP Performance Cache and Snapshots -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ Define snapshot lifecycle, invalidation triggers, and cancellation policy. ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0052-pbs-lsp-completion-depth.md b/discussion/workflow/agendas/AGD-0052-pbs-lsp-completion-depth.md index 2fc44b82..bbe5c87f 100644 --- a/discussion/workflow/agendas/AGD-0052-pbs-lsp-completion-depth.md +++ b/discussion/workflow/agendas/AGD-0052-pbs-lsp-completion-depth.md @@ -2,7 +2,7 @@ id: AGD-0052 ticket: pbs-lsp-completion-depth title: PBS LSP Completion Depth -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ Define completion contexts and ranking before adding snippets or resolve. ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0053-pbs-lsp-folding-and-selection-ranges.md b/discussion/workflow/agendas/AGD-0053-pbs-lsp-folding-and-selection-ranges.md index 440507eb..09bbdf39 100644 --- a/discussion/workflow/agendas/AGD-0053-pbs-lsp-folding-and-selection-ranges.md +++ b/discussion/workflow/agendas/AGD-0053-pbs-lsp-folding-and-selection-ranges.md @@ -2,7 +2,7 @@ id: AGD-0053 ticket: pbs-lsp-folding-and-selection-ranges title: PBS LSP Folding and Selection Ranges -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ This is an editor UX feature with low semantic dependency and can be planned ind ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0054-pbs-lsp-diagnostics-ux.md b/discussion/workflow/agendas/AGD-0054-pbs-lsp-diagnostics-ux.md index f77d11d2..5326f68f 100644 --- a/discussion/workflow/agendas/AGD-0054-pbs-lsp-diagnostics-ux.md +++ b/discussion/workflow/agendas/AGD-0054-pbs-lsp-diagnostics-ux.md @@ -2,7 +2,7 @@ id: AGD-0054 ticket: pbs-lsp-diagnostics-ux title: PBS LSP Diagnostics UX -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ This agenda should inform code actions before quick fixes become broad. ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0055-pbs-lsp-document-links.md b/discussion/workflow/agendas/AGD-0055-pbs-lsp-document-links.md index bb040af5..bd450330 100644 --- a/discussion/workflow/agendas/AGD-0055-pbs-lsp-document-links.md +++ b/discussion/workflow/agendas/AGD-0055-pbs-lsp-document-links.md @@ -2,7 +2,7 @@ id: AGD-0055 ticket: pbs-lsp-document-links title: PBS LSP Document Links -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ Coordinate stdlib target handling with go-to-definition. ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0056-pbs-lsp-call-and-type-hierarchy.md b/discussion/workflow/agendas/AGD-0056-pbs-lsp-call-and-type-hierarchy.md index 2676afc5..45c87ba8 100644 --- a/discussion/workflow/agendas/AGD-0056-pbs-lsp-call-and-type-hierarchy.md +++ b/discussion/workflow/agendas/AGD-0056-pbs-lsp-call-and-type-hierarchy.md @@ -2,7 +2,7 @@ id: AGD-0056 ticket: pbs-lsp-call-and-type-hierarchy title: PBS LSP Call and Type Hierarchy -status: open +status: abandoned created: 2026-07-15 resolved: decision: @@ -55,7 +55,7 @@ This should wait until definition and references are complete. ## Resolution -Ainda em aberto. +Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. ## Next Step diff --git a/discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md b/discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md new file mode 100644 index 00000000..cf0fb179 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md @@ -0,0 +1,127 @@ +--- +id: AGD-0069 +ticket: pbs-lsp-remaining-editor-surface +title: PBS LSP Remaining Editor Surface +status: accepted +created: 2026-09-22 +resolved: +decision: +tags: [studio, lsp, vscode, compiler-pbs, editor] +--- + +## Pain + +Domain owner: `studio` + +Nove agendas de LSP (AGD-0048 a AGD-0056, discussões DSC-0045 a DSC-0053) continuavam abertas como se cada tema partisse do zero. O servidor já anuncia hover, completion, signature help, definition, references, document symbols, workspace symbols, rename, quick fix e semantic tokens. Várias perguntas dessas agendas já foram respondidas pelo código, ou pedem trabalho cuja premissa deixou de existir. Decidir uma a uma repetiria debate vencido e produziria nove decisions para uma superfície só. + +## Context + +Esta agenda substitui, sem herdar o texto como normativo: + +| Agenda aposentada | Discussão | O que o código já fez com o tema | +| --- | --- | --- | +| AGD-0048 formatting | DSC-0045 | Não há `documentFormattingProvider`, `rangeFormatting` nem `onTypeFormatting`. | +| AGD-0049 import assistance | DSC-0046 | Não há auto-import nem organize imports. Imports existem como `ImportDecl` / `ModuleRef`. | +| AGD-0050 semantic tokens | DSC-0047 | Tokens full-document já saem de `PBSSemanticTokenProvider`. | +| AGD-0051 cache e snapshots | DSC-0048 | Cada pedido reconstrói análise. References e rename já existem sem cache. | +| AGD-0052 completion depth | DSC-0049 | Completion já existe, eager, gatilho `.`, `resolveProvider` falso. | +| AGD-0053 folding e selection | DSC-0050 | Não há folding nem selection range. Outline não é folding. | +| AGD-0054 diagnostics UX | DSC-0051 | Diagnóstico já publica range, severity, message e code. Quick fix já casa code + range. | +| AGD-0055 document links | DSC-0052 | Não há `documentLinkProvider`. Definition já recusa alvo que não seja arquivo físico. | +| AGD-0056 call e type hierarchy | DSC-0053 | Não há call hierarchy nem type hierarchy. References não são grafo de chamadas. | + +O que o initialize anuncia hoje, em `Lsp4jProtocolMessageMapper`: sync full, hover, completion, signature help, definition, references, document symbol, workspace symbol, rename com prepare, code action só `quickfix`, semantic tokens full. Nada além disso. + +Fatos que esta agenda trata como fechados, porque o código já os fixou: + +1. Análise de LSP é local ao pedido. `CompilerLanguageServiceBridge.analyzeDocument` monta um `AnalysisSnapshot` novo com overlay do documento aberto. Não há cache de projeto, cancelamento nem reanálise em background. References, rename e workspace symbols já foram entregues em cima desse modelo. Cache não é pré-requisito de mais nenhuma feature desta onda. +2. Semantic tokens não estão por fazer. `PBSSemanticTokenProvider` lexa o arquivo, classifica token léxico direto e classifica identificador por token vizinho, mapa de declaração do próprio arquivo e kinds importados da stdlib. A legenda é `PbsSemanticKind`. Não existem keys de field, local ou parameter. Arquivo quebrado continua tokenizável porque o lexer não depende do sucesso semântico. +3. Completion não está por fazer. `PbsEditorialSupportService.completion` faz duas coisas: se o token anterior é `.`, membros do tipo; senão keywords, parâmetros, locals, símbolos importados e top-level do arquivo. Documentação volta no próprio item. Não há snippets, não há `completionItem/resolve`, não há score de ranking. +4. Diagnóstico não carrega remediação. Spec 23 §8.3 e o quick fix já entregue exigem que o reparo não seja campo do diagnóstico nem venha de parse da mensagem. O mapper copia range, severity, message e code. O campo `source` está errado: `toBaselineIssue` grava o code estável também em `source` (ou `"Prometeu Studio"` quando o code vem vazio). Não há `relatedInformation`. +5. Não há um code de diagnóstico cujo significado seja "nome não importado". Os codes de nome não resolvido que existem são casos pontuais (`E_SEM_CONST_UNRESOLVED_REFERENCE`, `E_SEM_APPLY_UNRESOLVED_OVERLOAD`, `E_SEM_ASSIGN_TARGET_UNRESOLVED`, `E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE`). Inserir import em cima deles mistura typo com import faltando. +6. Definition, references e rename só apontam arquivo físico. Alvo virtual de stdlib não é destino de navegação. +7. A política de indentação em `LSN-0033` pertence ao editor embutido removido. Não é estilo canônico de PBS e não manda no formatter do LSP. +8. Outline, workspace symbols, rename e quick fix não absorvem folding, links, hierarquia nem organize imports. + +Fora desta onda, de propósito: + +- cache de snapshot, cancelamento e reanálise em background; +- type hierarchy, implementações de contract e grafo de host/intrinsic como feature separada; +- `completionItem/resolve`, snippets e modelo de ranking; +- formatting por range ou on-type; +- organize imports e quick fix de import faltando; +- `relatedInformation`, fase do compilador dentro do diagnóstico, e campo de remediação; +- keys novas de semantic token; +- links para assets, addressables, docs ou URI virtual de stdlib. + +## Open Questions + +- [x] **Q1. Qual pacote o plano único executa?** Travada em 2026-09-22: **A**. + - **A.** Os sete passos da recomendação, nesta ordem, um commit cada. Inclui formatter e call hierarchy. + - **B.** Os mesmos passos sem o 7 (call hierarchy fica fora). + - **C.** Os mesmos passos sem o 4 (formatter fica fora até existir estilo aceito). + - **D.** Só os passos que não escolhem estilo nem grafo novo: 1, 2, 3 e 6. + +- [x] **Q2. Se o passo 4 entrar, qual estilo o formatter aplica?** Travada em 2026-09-22: **A**. + - **A.** Reimpressão do token stream. Não reordena declarações. Não junta nem parte linhas para embelezar. Indentação de 4 espaços só onde o aninhamento de chaves, parênteses ou bloco muda. Interior de text block `Doc` intocado. Comentário permanece na linha do token a que já estava preso. Documento inteiro apenas. + - **B.** Não travar estilo. O passo 4 sai do plano mesmo se Q1 escolher A ou B. + +## Options + +### Option A - Uma onda, um plano, commits por passo + +- **Approach:** Uma decision e um plan. O plan copia os passos abaixo que Q1/Q2 deixarem vivos. Cada passo é um commit na mesma branch. Lesson e housekeep só depois do último passo. +- **Pro:** O código atual vira o piso. Tema vencido não vira decision. A série continua um commit por entrega, sem nove discussões. +- **Con:** Formatter e call hierarchy ainda são maiores que links ou o rótulo de `source`. +- **Maintainability:** Forte se cada passo não reabrir cache, type hierarchy ou auto-import. + +### Option B - Só o que já não tem escolha de produto + +- **Approach:** Mesma discussão única, mas o plano fica em diagnóstico `source`, document links físicos, folding/selection e completion de module ref. +- **Pro:** Evita travar estilo e evita um grafo de chamadas novo. +- **Con:** Formatting e call hierarchy continuam sem dono depois de aposentar as agendas velhas. +- **Maintainability:** Boa, e menor. + +## Tradeoffs + +Juntar as nove agendas num plano só é seguro porque o piso já está no código. O risco é enfiar de volta, como "passo pequeno", cache, contrato novo de diagnóstico ou auto-import. Esses três não cabem: o primeiro perdeu a premissa, o segundo quebra o quick fix já aceito, o terceiro não tem diagnóstico estável para se pendurar. + +Formatter é o único passo que ainda escolhe estilo. Por isso ele é um passo isolado: recusar Q2 tira só esse commit. Call hierarchy não pode reusar o índice de references; references são usos da mesma identidade, não chamadas. Ou entra como caminhada de `CallExpr` no último commit, ou fica de fora. + +## Recommendation + +Aceitar Option A com Q1-A e Q2-A. + +Forma do plano, obrigatória: + +- uma decision e um plan desta discussão; +- branch `dev/pbs-lsp-remaining-editor-surface`, não uma branch por passo e não commit em `master`; +- cada passo vivo vira um commit `implements PLN-NNNN (k/N) `; +- sem push; +- spec 23 §8.3 só ganha parágrafo do passo que a superfície realmente passa a anunciar; +- lesson e `discussion housekeep` só no commit do último passo. + +Passos, nesta ordem. Passo que Q1 ou Q2 tirar não é renumerado no meio da implementação: o plan lista só os que sobreviverem, em ordem. + +1. **Diagnostic source.** `source` passa a ser o language id do frontend (`pbs`). `code` continua o code estável. Sem `relatedInformation`, sem fase, sem data de reparo. +2. **Document links.** Link só no span do `ModuleRef` de um import cujo destino já resolve para arquivo físico regular. Sem destino físico, sem link. Stdlib virtual, asset e doc ficam de fora. +3. **Folding e selection range.** Span de AST quando a árvore recuperada tem o nó: declaração top-level (`fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, `host`), corpo, lista de parâmetros e text block `Doc`. Fallback de token só para chave, parêntese ou text block que a árvore não cobre. Selection, do interno para o externo: identificador, lista de argumento ou parâmetro, bloco ou text block, declaração. Lista vazia quando não houver range. Não reutilizar document symbols. +4. **Formatting de documento inteiro.** Só se Q2 travar estilo. Sem range e sem on-type. Preserva comentário e interior de text block `Doc`. Não é pretty-printer de AST. +5. **Overlay semântico em cima do token atual.** Não substituir `PBSSemanticTokenProvider`. Onde a mesma resolução de hover já nomeia o símbolo, trocar o kind fraco por um `PbsSemanticKind` que já existe. Falha, arquivo quebrado ou símbolo desconhecido mantém o token de hoje. Nenhuma key nova. +6. **Completion de module ref.** No cursor dentro de `ModuleRef`, candidatos que o resolver atual já sabe nomear, módulo de projeto e stdlib. Continua eager. `resolveProvider` continua falso. Sem snippets e sem score novo. Fora de module ref, o completion atual não muda. +7. **Call hierarchy.** Incoming e outgoing de função e método por caminhada de callsite na AST, não pelo índice de references. Cursor que não é callable devolve vazio. Sem type hierarchy e sem cache. + +## Discussion + +As agendas AGD-0048 a AGD-0056 foram aposentadas a pedido, neste consolidado, antes de qualquer accept. O texto delas não é decisão. A CLI `discussion` não tem comando de abandon; a aposentadoria ficou no índice e no frontmatter, com a lacuna registrada para um comando futuro. + +Não aceitar esta agenda até Q1 e Q2 terem resposta explícita. A recomendação é o pacote, não um convite para preencher o resto na implementação. + +## Resolution + +Q1-A e Q2-A travados pelo usuário em 2026-09-22. O pacote da recomendação é a escolha que a decision torna normativa. + +## Next Step + +Decision aceita e plan desta discussão. Implementar os sete passos em série, um commit cada. diff --git a/discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md b/discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md new file mode 100644 index 00000000..2627e0af --- /dev/null +++ b/discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md @@ -0,0 +1,78 @@ +--- +id: DEC-0058 +ticket: pbs-lsp-remaining-editor-surface +title: PBS LSP remaining editor surface +status: accepted +created: 2026-09-22 +ref_agenda: AGD-0069 +tags: [studio, lsp, vscode, compiler-pbs, editor] +--- + +## Context + +AGD-0069 consolidou AGD-0048 a AGD-0056. Essas agendas foram aposentadas e não são normativas. O usuário travou Q1-A e Q2-A em 2026-09-22. + +O LSP já anuncia hover, completion, signature help, definition, references, document symbols, workspace symbols, rename, quick fix e semantic tokens full. Cada pedido reconstrói a análise. Definition, references e rename só devolvem arquivo físico. Quick fix é um `WorkspaceEdit` produzido pelo frontend, casado por code e range, sem remediação dentro do diagnóstico. `PBSSemanticTokenProvider` já classifica o documento a partir do lexer. Completion já cobre membro depois de `.` e o contexto geral, com documentação no próprio item e sem `resolve`. + +O que falta é a superfície desta decision. O campo `source` do diagnóstico publicado está errado: hoje recebe o code estável, ou `"Prometeu Studio"` quando o code vem vazio. + +## Decision + +A discussão DSC-0066 tem uma decision e um plan. O plan executa sete passos, nesta ordem, um commit cada, na branch `dev/pbs-lsp-remaining-editor-surface`. Lesson e housekeep acontecem só no commit do último passo. Não há push nesta decision. + +O frontend produz o resultado editorial. O LSP transporta. O LSP MUST NOT inventar reparo, link, range, formatação, token ou chamada que o frontend não devolveu. Capacidade ausente MUST NOT ser anunciada e MUST devolver lista vazia, não erro de protocolo. + +### 1. Diagnostic source + +O `source` de um diagnóstico editorial publicado MUST ser o `languageId` do `LspProjectContext` daquele pedido. Numa sessão PBS esse valor é `pbs`. O `code` MUST continuar o code estável do diagnóstico do compilador, vazio quando o compilador não tem code. O LSP MUST NOT copiar o code para `source`. O LSP MUST NOT publicar `relatedInformation`, fase do compilador ou payload de reparo nesse diagnóstico. + +### 2. Document links + +O servidor MUST anunciar `documentLink` só para esta capacidade. Cada link MUST cobrir o span do `ModuleRef` de um import cujo destino o resolver atual já entrega como arquivo físico regular. Sem arquivo físico, MUST NOT haver link. Stdlib virtual, asset, addressable, doc e URI sintético MUST NOT ser alvo. O link MUST NOT substituir definition. + +### 3. Folding e selection range + +O servidor MUST anunciar folding range e selection range. Range de declaração top-level (`fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, `host`), corpo, lista de parâmetros e text block `Doc` MUST vir do span da árvore recuperada quando o nó existe. Chave, parêntese ou text block que a árvore não cobre MUST usar fallback de token. Selection, do interno para o externo, MUST ser: identificador, lista de argumento ou parâmetro, bloco ou text block, declaração. Sem range, a resposta MUST ser lista vazia. Document symbols MUST NOT ser reutilizados como folding. + +### 4. Formatting + +O servidor MUST anunciar formatting só de documento inteiro. Range formatting e on-type formatting MUST NOT ser anunciados. O formatter MUST reimprimir o token stream. MUST NOT reordenar declarações. MUST NOT juntar ou partir linhas para embelezar. Indentação MUST ser 4 espaços, aplicada só onde o aninhamento de chaves, parênteses ou bloco muda. O interior de text block `Doc` MUST permanecer intocado. Comentário MUST permanecer na linha do token a que já estava preso. Pretty-printer de AST MUST NOT ser o formatter. `LSN-0033` MUST NOT definir esse estilo. + +### 5. Overlay semântico + +`PBSSemanticTokenProvider` MUST continuar sendo a base lexical. Onde a mesma resolução usada pelo hover nomeia o símbolo e existe um `PbsSemanticKind` para esse símbolo, o token MUST trocar o kind fraco por esse kind existente. Falha de resolução, arquivo quebrado ou símbolo sem kind existente MUST manter o token que o provider emitiu. Esta onda MUST NOT criar key nova de semantic token. + +### 6. Completion de module ref + +Com o cursor dentro de um `ModuleRef`, completion MUST oferecer módulos de projeto e de stdlib que o resolver atual já sabe nomear. Fora de `ModuleRef`, o completion atual MUST permanecer. A resposta MUST continuar eager. `resolveProvider` MUST continuar falso. Snippets e score novo de ranking MUST NOT entrar. + +### 7. Call hierarchy + +O servidor MUST anunciar call hierarchy para função e método. Incoming e outgoing MUST ser uma caminhada de callsite na AST. O índice de references MUST NOT ser tratado como grafo de chamadas. Cursor que não está num callable MUST devolver vazio. Type hierarchy, implementações de contract e grafo separado de host ou intrinsic MUST NOT entrar. + +### Fora desta decision + +Esta decision MUST NOT pedir cache de snapshot, cancelamento, reanálise em background, organize imports, quick fix de import faltando, `completionItem/resolve`, formatting por range ou on-type, `relatedInformation`, ou links que não sejam o `ModuleRef` físico do passo 2. + +## Rationale + +Q1-A mantém os sete passos porque cada um cobre um buraco real e o piso já está implementado. Q2-A isola o estilo no formatter para ele não vazar para os outros passos. Cache deixou de ser pré-requisito quando references e rename saíram em análise local ao pedido. Auto-import não tem diagnóstico cujo significado seja "nome não importado". Type hierarchy não compartilha modelo com call hierarchy. + +## Implications + +Os passos 2, 3, 4 e 7 acrescentam capacidade anunciada. Os passos 1, 5 e 6 corrigem ou estendem capacidade que o servidor já anuncia. Nenhum passo reabre o contrato de quick fix nem o de definition física. Spec 23 §8.3 ganha o parágrafo do passo no commit daquele passo, não antes. + +## Propagation Targets + +- specs: `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` §8.3, um parágrafo por passo, no commit desse passo. +- plans: um plan de DSC-0066 com os sete passos e um commit cada. +- code: `prometeu-compiler` onde o frontend produz o resultado; `prometeu-lsp` onde o protocolo transporta. `source` do diagnóstico usa `LspProjectContext.languageId`. +- tests: bridge e, quando a capacidade é nova, o anúncio no mapper. Arquivo quebrado continua coberto em token e folding. +- docs: nenhum outro. A lesson em inglês só no housekeep do último passo. + +## References + +- Agenda: AGD-0069 +- Discussões aposentadas: DSC-0045 a DSC-0053, AGD-0048 a AGD-0056 +- Spec: `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` §8.3 +- Lessons de piso: LSN-0068, LSN-0069, LSN-0070, LSN-0071, LSN-0072, LSN-0073 diff --git a/discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md b/discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md new file mode 100644 index 00000000..46fa8731 --- /dev/null +++ b/discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md @@ -0,0 +1,126 @@ +--- +id: PLN-0137 +ticket: pbs-lsp-remaining-editor-surface +title: PBS LSP remaining editor surface +status: in_progress +created: 2026-09-22 +ref_decisions: [DEC-0058] +tags: [studio, lsp, vscode, compiler-pbs, editor] +--- + +## Briefing + +DEC-0058 locks one PBS LSP wave on top of the editor surface that already ships. Seven steps run in order on `dev/pbs-lsp-remaining-editor-surface`. Each step is one commit. The frontend produces the editorial result. The LSP transports it and does not invent a second policy. + +## Objective + +Land diagnostic source, physical import document links, folding and selection ranges, full-document formatting, a semantic-token overlay, module-ref completion, and function/method call hierarchy. Leave snapshot cache, type hierarchy, auto-import, and resolve completion out. + +## Dependencies + +Shipped and unchanged by this plan: request-local analysis, physical definition and references, rename, document symbols, workspace symbols, quick fix (LSN-0073), `PBSSemanticTokenProvider`, and eager completion in `PbsEditorialSupportService`. Spec 23 §8.3 is the only normative doc updated, one paragraph in the commit of the step that introduces that rule. + +## Scope + +Local to studio LSP and the PBS frontend. Shared compiler entry points do not gain a cache or a new pipeline phase. New LSP capabilities follow the code-action pattern: `FrontendLanguageService` defaults to unsupported, PBS overrides support, `BaselineServerDescription` advertises only that flag, and a missing flag is an empty list. + +### Step 1. Diagnostic source + +In `CompilerLanguageServiceBridge.toBaselineIssue`, set `BaselineDocumentIssue.source` from `LspProjectContext.languageId`. Keep `code` as the trimmed compiler code, empty when the compiler code is null or blank. Do not special-case `"Prometeu Studio"`. `Lsp4jProtocolMessageMapper.mapDiagnostic` already copies `source` and `code` and must stay without `relatedInformation` or `data`. + +Spec 23 §8.3 gains one paragraph: published editor diagnostic `source` is the bound frontend `languageId`; `code` is the stable compiler code or empty; tooling must not copy the code into `source` and must not publish related locations, phase, or a repair payload. + +### Step 2. Document links + +Add a frontend document-link result: span of a `ModuleRef` plus a physical file target. Default on `FrontendLanguageService` is an empty list and `documentLinksSupported` false. PBS returns a link only when the resolver the compiler already uses yields a regular file. No link for virtual stdlib, assets, docs, or an unresolved module. Do not add a second module resolver. + +LSP: baseline message, `documentLinkProvider`, `PrometeuTextDocumentService.documentLink`, mapper. Unsupported stays unadvertised and returns an empty list. + +### Step 3. Folding and selection ranges + +Frontend produces both lists from the recovered AST when the node has a span: top-level `fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, and `host`; their bodies; parameter lists; `Doc` text blocks. Token fallback covers only a brace, parenthesis, or text-block delimiter the tree does not cover. Selection order, inner to outer: identifier, argument or parameter list, block or text block, declaration. Empty when there is nothing. Do not build either list from document symbols. + +LSP advertises `foldingRangeProvider` and `selectionRangeProvider` only when the frontend flags are true. + +### Step 4. Full-document formatting + +Frontend formatter reprints the token stream. It does not reorder declarations, join lines, or split lines to beautify. Indent is four spaces and changes only where brace, parenthesis, or block nesting changes. `Doc` text-block interiors are copied unchanged. A comment stays on the token line it already belongs to. No AST pretty-printer. No range formatting. No on-type formatting. Do not read `.studio/setup.json` or LSN-0033. + +LSP advertises `documentFormattingProvider` only. + +### Step 5. Semantic-token overlay + +Keep `PBSSemanticTokenProvider` as the lexical baseline. After that pass, where the same resolution hover uses names a symbol and an existing `PbsSemanticKind` matches it, replace the weaker key. On resolution failure, a broken file, or a symbol with no existing key, keep the provider token. Add no `PbsSemanticKind` constant. The server already advertises semantic tokens; do not add a second provider. + +### Step 6. Module-ref completion + +When the completion offset sits inside a `ModuleRef`, return project and stdlib modules the current resolver can already name. Every other offset keeps today's member or general completion. Items stay eager. `CompletionOptions.resolveProvider` stays false. No snippets. No new rank score. No new trigger-character policy beyond what module-ref detection needs inside the existing completion request. + +### Step 7. Call hierarchy + +Frontend walks `CallExpr` callsites for incoming and outgoing calls of functions and methods. Do not query the references index. A cursor that is not on a callable returns empty. No type hierarchy and no separate host or intrinsic graph. + +LSP advertises call hierarchy only. This commit also writes the English lesson, runs `discussion housekeep DSC-0066 --lesson `, and `discussion validate`. + +## Non-Goals + +- Project snapshot cache, cancellation, and background reanalysis. +- Type hierarchy, contract implementation hierarchy, and a separate host or intrinsic graph. +- `completionItem/resolve`, snippets, and a ranking model. +- Range formatting and on-type formatting. +- Organize imports and a missing-import quick fix. +- `relatedInformation`, compiler phase on the diagnostic, and repair data. +- New semantic-token keys. +- Document links for assets, addressables, docs, or virtual stdlib URIs. +- Reusing document symbols as folding ranges. +- Pushing the branch. + +## Execution Method + +1. Work on `dev/pbs-lsp-remaining-editor-surface`. Do not commit on `master`. +2. Mark this plan `in_progress` before step 1 code. +3. Implement one step, then run the tests listed for that step. +4. Commit that step before starting the next. Subject: `implements PLN-0137 (k/7) `. +5. Stage `discussion`, `docs`, `prometeu-compiler`, and `prometeu-lsp` as the step touches them. Do not stage `AGENTS.md`. +6. Spec 23 §8.3 is edited in the same commit as the step whose rule it states. +7. Steps are not reordered and are not parallel. A later step may use the capability flags introduced earlier; it must not reopen a closed step. +8. The seventh commit includes the lesson and housekeep. Earlier commits keep DEC-0058 and this plan in the tree. + +Commit slugs: + +1. `diagnostic-source` +2. `document-links` +3. `folding-and-selection-ranges` +4. `document-formatting` +5. `semantic-token-overlay` +6. `module-ref-completion` +7. `call-hierarchy` + +## Acceptance Criteria + +- Step 1: every issue from `analyzeDocument` on a PBS project has `source` equal to `pbs`, `code` is the stable compiler code, and `source` is not that code. No related information is mapped. +- Step 2: a `ModuleRef` that resolves to a regular file becomes one document link on that span. Virtual stdlib and unresolved modules produce no link. The provider is advertised only when supported. +- Step 3: a well-formed declaration, body, parameter list, and `Doc` text block fold from AST spans. An unmatched brace still produces a token fallback range. Selection nests in the decided order. Document symbols are unchanged. +- Step 4: formatting a document does not change `Doc` text-block interiors, does not move a comment off its token line, does not reorder declarations, and only changes indent by four spaces at nesting changes. Range and on-type providers stay off. +- Step 5: an identifier hover can resolve changes to an existing semantic key; a broken file still returns the lexical tokens; `PbsSemanticKind` gains no constant. +- Step 6: completion inside a `ModuleRef` lists resolver-known modules; completion after `.` is unchanged; `resolveProvider` is false. +- Step 7: incoming and outgoing calls report function and method callsites found in the AST. A non-callable cursor is empty. Type hierarchy is not advertised. `discussion validate` passes after housekeep, and the lesson states the seven rules. + +## Tests + +- `CompilerLanguageServiceBridgeTest` for diagnostic source, and for each new bridge method as it lands. +- `Lsp4jProtocolMessageMapperTest` for capability advertisement of each new provider, and for the absence of range formatting, on-type formatting, resolve, related information, and type hierarchy. +- PBS frontend tests for links, folding, selection, formatting, the token overlay, module-ref completion, and call hierarchy. Include one broken-file case for tokens and one unmatched-delimiter case for folding. +- Do not require a running VS Code instance. Bridge and mapper tests are the protocol gate. + +## Affected Artifacts + +- `discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md` +- `discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md` +- `discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md` +- `discussion/index.ndjson` +- `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` +- `prometeu-compiler/prometeu-frontend-api` (`FrontendLanguageService` and new result types) +- `prometeu-compiler/frontends/prometeu-frontend-pbs` (PBS implementations) +- `prometeu-lsp/prometeu-lsp-v1` (bridge, baseline messages, text document service, mapper) +- Lesson, only in the step 7 commit: `discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/` diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index 02dcd454..e1ff4659 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -286,6 +286,8 @@ When a frontend exposes code actions, the result MUST be quick fixes only: one w Compiler diagnostics and editor diagnostics are distinct ownership surfaces. Diagnostics returned by `analyze`, `compile`, and `build` are compiler contract output. Editor diagnostics MAY reuse compiler analysis results, live overlays, caches, or cancellation-aware tooling state, but a frontend MUST NOT be required to provide editor diagnostics in order to compile. +When tooling publishes an editor diagnostic, `source` MUST be the `languageId` of the bound frontend project. `code` MUST be the stable compiler diagnostic code when one exists, and MUST be empty when the compiler diagnostic has none. Tooling MUST NOT copy that code into `source`. Tooling MUST NOT publish related locations, compiler phase, or a repair payload on the editor diagnostic. + `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. diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index afd7e869..308ddbda 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -85,7 +85,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 @@ -541,7 +545,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg private List mapDiagnostics( final AnalysisSnapshot snapshot, final Path documentPath, - final String text) { + final String text, + final String languageId) { final ArrayList issues = new ArrayList<>(); final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text); for (final BuildingIssue diagnostic : snapshot.diagnostics()) { @@ -556,14 +561,15 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) { continue; } - issues.add(toBaselineIssue(diagnostic, positionMapper)); + issues.add(toBaselineIssue(diagnostic, positionMapper, languageId)); } return List.copyOf(issues); } private BaselineDocumentIssue toBaselineIssue( final BuildingIssue diagnostic, - final DocumentPositionMapper positionMapper) { + final DocumentPositionMapper positionMapper, + final String languageId) { final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart(); final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd()); final DocumentPosition start = positionMapper.positionOf(startOffset); @@ -575,7 +581,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg end.line(), end.character(), diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING, - code.isBlank() ? "Prometeu Studio" : code, + languageId, diagnostic.getMessage(), code); } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index b2e665fe..6b84d3b9 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -100,6 +100,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 diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index d8a73cf4..603c8c9b 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -269,12 +269,14 @@ final class Lsp4jProtocolMessageMapperTest { 1, 18, BaselineIssueSeverity.ERROR, - "E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", + "pbs", "Duplicate Doc attribute on the same declaration surface", "E_SEM_DUPLICATE_RESERVED_ATTRIBUTE")))); final var diagnostic = published.getDiagnostics().getFirst(); + assertEquals("pbs", diagnostic.getSource()); assertEquals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", diagnostic.getCode().getLeft()); assertNull(diagnostic.getData()); + assertTrue(diagnostic.getRelatedInformation() == null || diagnostic.getRelatedInformation().isEmpty()); final var mapped = mapper.mapCodeActions(new BaselineCodeActions(List.of(new BaselineCodeAction( "Remove duplicate reserved attribute", From 3320f222bbf474b1997928748f7501b6045243ae Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 08:34:13 +0100 Subject: [PATCH 2/8] 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. --- ...ler Pipeline Entry Points Specification.md | 2 + .../compiler/PBSFrontendLanguageService.java | 42 +++++++ .../services/PBSFrontendPhaseService.java | 39 ++++++- .../compiler/PBSFrontendDocumentLinkTest.java | 109 ++++++++++++++++++ .../services/FrontendDocumentLink.java | 20 ++++ .../services/FrontendLanguageService.java | 14 +++ .../services/FrontendLanguageServiceTest.java | 3 + .../lsp/messages/BaselineDocumentLink.java | 28 +++++ .../lsp/messages/BaselineDocumentLinks.java | 13 +++ .../messages/BaselineServerDescription.java | 1 + .../lsp/services/LanguageServiceBridge.java | 5 + .../CompilerLanguageServiceBridge.java | 50 ++++++++ .../protocol/PrometeuTextDocumentService.java | 8 ++ .../mapping/Lsp4jProtocolMessageMapper.java | 22 ++++ .../mapping/ProtocolMessageMapper.java | 6 + .../CompilerLanguageServiceBridgeTest.java | 75 ++++++++++++ .../protocol/PrometeuLanguageServerTest.java | 1 + .../Lsp4jProtocolMessageMapperTest.java | 53 +++++++++ 18 files changed, 489 insertions(+), 2 deletions(-) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/PBSFrontendDocumentLinkTest.java create mode 100644 prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentLink.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentLink.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentLinks.java diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index e1ff4659..7e2850c5 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -288,6 +288,8 @@ Compiler diagnostics and editor diagnostics are distinct ownership surfaces. Dia 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. + `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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index 3fed0117..8333ede3 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -21,6 +21,7 @@ import p.studio.compiler.pbs.semantics.PbsQuickFixCollector; import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface; import p.studio.compiler.services.FrontendCodeAction; import p.studio.compiler.services.FrontendCompletionCandidate; +import p.studio.compiler.services.FrontendDocumentLink; import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendDocumentSymbol; @@ -40,6 +41,7 @@ import p.studio.compiler.source.Span; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.source.tables.FileTableReader; +import p.studio.compiler.source.tables.ModuleReference; import p.studio.utilities.structures.ReadOnlyList; import java.nio.file.Files; @@ -401,6 +403,46 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return FrontendRenameResult.applied(List.copyOf(edits.values())); } + @Override + public boolean documentLinksSupported() { + return true; + } + + @Override + public List 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 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 codeActionsSupported() { return true; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 7268c097..afa5edca 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -15,9 +15,12 @@ import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.source.identifiers.ModuleId; +import p.studio.compiler.source.tables.ModuleReference; import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.structures.ReadOnlyList; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; @Slf4j @@ -153,14 +156,46 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { Map.copyOf(astByFile), Map.copyOf(supplementalTopDeclsByFile), Map.copyOf(inlineHintsByFile), - Map.copyOf(sourceKindByFile)); + Map.copyOf(sourceKindByFile), + physicalModuleFiles(ctx, assembly)); + } + + private static Map physicalModuleFiles( + final FrontendPhaseContext ctx, + final PbsModuleAssembly assembly) { + final Map 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); } public record PbsSemanticReadSurface( Map astByFile, Map> supplementalTopDeclsByFile, Map> inlineHintsByFile, - Map sourceKindByFile) { + Map sourceKindByFile, + Map physicalModuleFileByReference) { } private IRBackend mergeCompiledSources( diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/PBSFrontendDocumentLinkTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/PBSFrontendDocumentLinkTest.java new file mode 100644 index 00000000..79ef48c4 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/PBSFrontendDocumentLinkTest.java @@ -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())); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentLink.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentLink.java new file mode 100644 index 00000000..4f7433e3 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendDocumentLink.java @@ -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"); + } + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index 99670e2c..5934fd1e 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -139,6 +139,20 @@ public interface FrontendLanguageService { return rename(request, offset, newName); } + default boolean documentLinksSupported() { + return false; + } + + default List documentLinks(final FrontendDocumentRequest request) { + return List.of(); + } + + default List documentLinks( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext) { + return documentLinks(request); + } + default boolean codeActionsSupported() { return false; } diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java index db48db2e..a5c5a7d5 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java @@ -30,6 +30,9 @@ class FrontendLanguageServiceTest { assertTrue(service.prepareRename(request, 0).isEmpty()); assertEquals(FrontendRenameResult.Status.UNSUPPORTED, service.rename(request, 0, "renamed").status()); assertTrue(service.rename(request, 0, "renamed").edits().isEmpty()); + assertFalse(service.documentLinksSupported()); + assertTrue(service.documentLinks(request).isEmpty()); + assertTrue(service.documentLinks(request, null).isEmpty()); assertFalse(service.codeActionsSupported()); assertTrue(service.codeActions(request).isEmpty()); assertTrue(service.codeActions(request, null).isEmpty()); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentLink.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentLink.java new file mode 100644 index 00000000..a158ff9c --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentLink.java @@ -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; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentLinks.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentLinks.java new file mode 100644 index 00000000..595619b1 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentLinks.java @@ -0,0 +1,13 @@ +package p.studio.lsp.messages; + +import java.util.List; + +public record BaselineDocumentLinks(List links) { + public BaselineDocumentLinks { + links = links == null ? List.of() : List.copyOf(links); + } + + public static BaselineDocumentLinks empty() { + return new BaselineDocumentLinks(List.of()); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index cb592b39..85e3c003 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -16,6 +16,7 @@ public record BaselineServerDescription( boolean workspaceSymbolsSupported, boolean renameSupported, boolean codeActionsSupported, + boolean documentLinksSupported, List semanticTokenTypes, List semanticHostProjections, List visualThemes, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index 2949eace..838c073a 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -9,6 +9,7 @@ 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.BaselinePrepareRename; import p.studio.lsp.messages.BaselineRename; import p.studio.lsp.messages.BaselineSemanticTokens; @@ -60,6 +61,10 @@ public interface LanguageServiceBridge { BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text); + default BaselineDocumentLinks documentLinks(LspProjectContext context, String documentUri, String text) { + return BaselineDocumentLinks.empty(); + } + BaselineCodeActions codeActions( LspProjectContext context, String documentUri, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index 308ddbda..f34faec2 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -6,6 +6,7 @@ import p.studio.compiler.messages.*; import p.studio.compiler.models.*; import p.studio.compiler.services.FrontendCodeAction; import p.studio.compiler.services.FrontendCompletionCandidate; +import p.studio.compiler.services.FrontendDocumentLink; import p.studio.compiler.services.FrontendLanguageService; import p.studio.compiler.services.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDocumentRequest; @@ -57,6 +58,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg final boolean codeActionsSupported = languageService .map(FrontendLanguageService::codeActionsSupported) .orElse(false); + final boolean documentLinksSupported = languageService + .map(FrontendLanguageService::documentLinksSupported) + .orElse(false); return new BaselineServerDescription( "Prometeu Studio LSP", "0.1.0", @@ -70,6 +74,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg languageServicePresent, languageServicePresent, codeActionsSupported, + documentLinksSupported, presentation.semanticKeys(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(), @@ -365,6 +370,29 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg .orElseGet(() -> BaselineRename.refused("not renameable")); } + @Override + public BaselineDocumentLinks documentLinks( + final LspProjectContext context, + final String documentUri, + final String text) { + Objects.requireNonNull(context, "context"); + final var languageService = frontendProvider(context).languageService(); + if (languageService.isEmpty() || !languageService.orElseThrow().documentLinksSupported()) { + return BaselineDocumentLinks.empty(); + } + return editorialDocument(context, documentUri, text) + .map(document -> { + final FrontendDocumentRequest request = new FrontendDocumentRequest( + context.projectRoot(), + normalizeDocumentPath(documentUri), + document.text()); + return new BaselineDocumentLinks(mapDocumentLinks( + languageService.orElseThrow().documentLinks(request, document.editorialContext()), + document.text())); + }) + .orElseGet(BaselineDocumentLinks::empty); + } + @Override public BaselineCodeActions codeActions( final LspProjectContext context, @@ -830,6 +858,28 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg return List.copyOf(mapped); } + private List mapDocumentLinks( + final List links, + final String currentDocumentText) { + if (links == null || links.isEmpty()) { + return List.of(); + } + final DocumentPositionMapper mapper = new DocumentPositionMapper( + currentDocumentText == null ? "" : currentDocumentText); + final ArrayList 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 mapDefinitionLocations( final List locations, final Path currentDocumentPath, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java index fcafd5dc..4d4cad03 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -168,6 +168,14 @@ public final class PrometeuTextDocumentService implements TextDocumentService { return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result)); } + @Override + public CompletableFuture> 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>> codeAction(final CodeActionParams params) { if (!quickFixRequested(params.getContext())) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index 482be900..b3608e2e 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -53,6 +53,11 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { codeActionOptions.setCodeActionKinds(List.of(CodeActionKind.QuickFix)); capabilities.setCodeActionProvider(codeActionOptions); } + if (description.documentLinksSupported()) { + final DocumentLinkOptions documentLinkOptions = new DocumentLinkOptions(); + documentLinkOptions.setResolveProvider(false); + capabilities.setDocumentLinkProvider(documentLinkOptions); + } final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setFull(true); @@ -159,6 +164,23 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { return edit; } + @Override + public List mapDocumentLinks(final BaselineDocumentLinks links) { + if (links == null || links.links().isEmpty()) { + return List.of(); + } + final ArrayList 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> mapCodeActions( final BaselineCodeActions actions, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index f1a466a2..51bb2f84 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -9,6 +9,7 @@ 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.Location; import org.eclipse.lsp4j.Range; @@ -19,6 +20,7 @@ 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.BaselineDocumentSymbols; import p.studio.lsp.messages.BaselineWorkspaceSymbols; import p.studio.lsp.messages.BaselineCodeActions; @@ -59,6 +61,10 @@ public interface ProtocolMessageMapper { List> mapCodeActions(BaselineCodeActions actions, List contextDiagnostics); + default List mapDocumentLinks(BaselineDocumentLinks links) { + return List.of(); + } + CompletionList mapCompletion(BaselineCompletion completion); SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 6b84d3b9..3f00e44b 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -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; @@ -14,6 +15,7 @@ import p.studio.lsp.messages.LspProjectContext; import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.structures.ReadOnlySet; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -74,6 +76,7 @@ class CompilerLanguageServiceBridgeTest { assertTrue(bridge.codeActions(context, documentUri, "fn main() {}", List.of( new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1))) .actions().isEmpty()); + assertTrue(bridge.documentLinks(context, documentUri, "fn main() {}").links().isEmpty()); } @Test @@ -768,6 +771,7 @@ class CompilerLanguageServiceBridgeTest { assertTrue(description.workspaceSymbolsSupported()); assertTrue(description.renameSupported()); assertTrue(description.codeActionsSupported()); + assertTrue(description.documentLinksSupported()); assertEquals(1, description.semanticHostProjections().size()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() @@ -781,6 +785,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) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index 1b7ab0e3..0f93d292 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -105,6 +105,7 @@ class PrometeuLanguageServerTest { true, true, false, + false, List.of(), List.of(new BaselineSemanticHostProjection( "vscode", diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index 603c8c9b..85ce742d 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -10,6 +10,8 @@ import p.studio.lsp.messages.BaselineCodeActions; import p.studio.lsp.messages.BaselineCompletion; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineDocumentIssue; +import p.studio.lsp.messages.BaselineDocumentLink; +import p.studio.lsp.messages.BaselineDocumentLinks; import p.studio.lsp.messages.BaselineIssueSeverity; import p.studio.lsp.messages.BaselineCompletionItem; import p.studio.lsp.messages.BaselineCompletionItemKind; @@ -55,6 +57,7 @@ final class Lsp4jProtocolMessageMapperTest { true, true, true, + true, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", @@ -82,6 +85,7 @@ final class Lsp4jProtocolMessageMapperTest { assertEquals(Boolean.TRUE, result.getCapabilities().getRenameProvider().getRight().getPrepareProvider()); assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds()); assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider()); + assertEquals(Boolean.FALSE, result.getCapabilities().getDocumentLinkProvider().getResolveProvider()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); @@ -298,4 +302,53 @@ final class Lsp4jProtocolMessageMapperTest { assertEquals(1, action.getDiagnostics().size()); assertEquals(diagnostic, action.getDiagnostics().getFirst()); } + + @Test + void documentLinksMapRangeAndTargetWithoutResolve() { + final var mapper = new Lsp4jProtocolMessageMapper(); + final var unsupported = mapper.mapInitializeResult(serverDescription(false)); + assertNull(unsupported.getCapabilities().getDocumentLinkProvider()); + + 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, + 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"); + } } From ce2e3e4cd3c227413d7ca265ecdc5f3cef5cffbd Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 08:45:31 +0100 Subject: [PATCH 3/8] 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. --- ...ler Pipeline Entry Points Specification.md | 4 + .../compiler/PBSFrontendLanguageService.java | 32 ++ .../compiler/pbs/PbsStructureRanges.java | 380 ++++++++++++++++++ .../compiler/pbs/PbsStructureRangesTest.java | 73 ++++ .../services/FrontendFoldingRange.java | 15 + .../services/FrontendLanguageService.java | 31 ++ .../services/FrontendSelectionRange.java | 16 + .../services/FrontendLanguageServiceTest.java | 6 + .../lsp/messages/BaselineFoldingRange.java | 14 + .../lsp/messages/BaselineFoldingRanges.java | 13 + .../lsp/messages/BaselineSelectionRange.java | 15 + .../lsp/messages/BaselineSelectionRanges.java | 13 + .../messages/BaselineServerDescription.java | 2 + .../lsp/services/LanguageServiceBridge.java | 15 + .../CompilerLanguageServiceBridge.java | 124 ++++++ .../protocol/PrometeuTextDocumentService.java | 26 ++ .../mapping/Lsp4jProtocolMessageMapper.java | 47 +++ .../mapping/ProtocolMessageMapper.java | 12 + .../CompilerLanguageServiceBridgeTest.java | 6 + .../protocol/PrometeuLanguageServerTest.java | 2 + .../Lsp4jProtocolMessageMapperTest.java | 8 + 21 files changed, 854 insertions(+) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsStructureRanges.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsStructureRangesTest.java create mode 100644 prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendFoldingRange.java create mode 100644 prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSelectionRange.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRange.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRanges.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRange.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRanges.java diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index 7e2850c5..ab924de8 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -290,6 +290,10 @@ When tooling publishes an editor diagnostic, `source` MUST be the `languageId` o 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. + `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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index 8333ede3..14439297 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -16,12 +16,15 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocum import p.studio.compiler.models.SourceKind; import p.studio.compiler.pbs.semantics.PbsEditorialRename; import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol; +import p.studio.compiler.pbs.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.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.FrontendDefinitionLocation; import p.studio.compiler.services.FrontendDocumentRequest; import p.studio.compiler.services.FrontendDocumentSymbol; @@ -443,6 +446,35 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return List.copyOf(links); } + @Override + public boolean foldingRangesSupported() { + return true; + } + + @Override + public List 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 selectionRanges( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final List offsets) { + return PbsStructureRanges.selectionRanges( + request == null || request.documentText() == null ? "" : request.documentText(), + offsets); + } + @Override public boolean codeActionsSupported() { return true; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsStructureRanges.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsStructureRanges.java new file mode 100644 index 00000000..e5d6c127 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsStructureRanges.java @@ -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 foldingRanges(final String documentText) { + final String text = documentText == null ? "" : documentText; + final ReadOnlyList 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 selectionRanges( + final String documentText, + final List offsets) { + final String text = documentText == null ? "" : documentText; + final ReadOnlyList 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 delimiters = delimiters(tokens, textEnd(text)); + final ArrayList 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 foldingRanges( + final String text, + final PbsAst.File ast, + final ReadOnlyList tokens) { + final Collected collected = collect(ast); + final LinkedHashSet emitted = new LinkedHashSet<>(); + final ArrayList 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 tokens, + final Collected collected, + final List 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 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 delimiters( + final ReadOnlyList tokens, + final int textEnd) { + final ArrayList stack = new ArrayList<>(); + final ArrayList 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 identifierSpans(final ReadOnlyList tokens) { + final ArrayList 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 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 spans, final Span span) { + if (span == null) { + return; + } + addPair(spans, toOffset(span.getStart()), toOffset(span.getEnd())); + } + + private static void addPair(final List 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 ranges, + final LinkedHashSet 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 folding = new ArrayList<>(); + private final ArrayList covered = new ArrayList<>(); + private final ArrayList blocks = new ArrayList<>(); + private final ArrayList declarations = new ArrayList<>(); + + private List folding() { + return folding; + } + + private void cover(final Span span) { + addSpan(covered, span); + } + + private LinkedHashSet coveredKeys() { + final LinkedHashSet keys = new LinkedHashSet<>(); + for (final SpanPair span : covered) { + keys.add(span.key()); + } + return keys; + } + + private List blocks() { + return blocks; + } + + private List declarations() { + return declarations; + } + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsStructureRangesTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsStructureRangesTest.java new file mode 100644 index 00000000..7f2737ec --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsStructureRangesTest.java @@ -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); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendFoldingRange.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendFoldingRange.java new file mode 100644 index 00000000..1e0d977f --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendFoldingRange.java @@ -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"); + } + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index 5934fd1e..48a33e00 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -153,6 +153,37 @@ public interface FrontendLanguageService { return documentLinks(request); } + default boolean foldingRangesSupported() { + return false; + } + + default List foldingRanges(final FrontendDocumentRequest request) { + return List.of(); + } + + default List foldingRanges( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext) { + return foldingRanges(request); + } + + default boolean selectionRangesSupported() { + return false; + } + + default List selectionRanges( + final FrontendDocumentRequest request, + final List offsets) { + return List.of(); + } + + default List selectionRanges( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final List offsets) { + return selectionRanges(request, offsets); + } + default boolean codeActionsSupported() { return false; } diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSelectionRange.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSelectionRange.java new file mode 100644 index 00000000..15750bc9 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSelectionRange.java @@ -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"); + } + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java index a5c5a7d5..dfb2d63d 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java @@ -33,6 +33,12 @@ class FrontendLanguageServiceTest { 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.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()); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRange.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRange.java new file mode 100644 index 00000000..877122e5 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRange.java @@ -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"); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRanges.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRanges.java new file mode 100644 index 00000000..0d759b9a --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFoldingRanges.java @@ -0,0 +1,13 @@ +package p.studio.lsp.messages; + +import java.util.List; + +public record BaselineFoldingRanges(List ranges) { + public BaselineFoldingRanges { + ranges = ranges == null ? List.of() : List.copyOf(ranges); + } + + public static BaselineFoldingRanges empty() { + return new BaselineFoldingRanges(List.of()); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRange.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRange.java new file mode 100644 index 00000000..09aac25c --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRange.java @@ -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"); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRanges.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRanges.java new file mode 100644 index 00000000..94cc6abc --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSelectionRanges.java @@ -0,0 +1,13 @@ +package p.studio.lsp.messages; + +import java.util.List; + +public record BaselineSelectionRanges(List ranges) { + public BaselineSelectionRanges { + ranges = ranges == null ? List.of() : List.copyOf(ranges); + } + + public static BaselineSelectionRanges empty() { + return new BaselineSelectionRanges(List.of()); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index 85e3c003..ef9eba20 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -17,6 +17,8 @@ public record BaselineServerDescription( boolean renameSupported, boolean codeActionsSupported, boolean documentLinksSupported, + boolean foldingRangesSupported, + boolean selectionRangesSupported, List semanticTokenTypes, List semanticHostProjections, List visualThemes, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index 838c073a..f704f8df 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -10,6 +10,9 @@ 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.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; @@ -65,6 +68,18 @@ public interface LanguageServiceBridge { return BaselineDocumentLinks.empty(); } + default BaselineFoldingRanges foldingRanges(LspProjectContext context, String documentUri, String text) { + return BaselineFoldingRanges.empty(); + } + + default BaselineSelectionRanges selectionRanges( + LspProjectContext context, + String documentUri, + String text, + List positions) { + return BaselineSelectionRanges.empty(); + } + BaselineCodeActions codeActions( LspProjectContext context, String documentUri, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index f34faec2..cf85b739 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -7,6 +7,8 @@ import p.studio.compiler.models.*; 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; @@ -61,6 +63,12 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg 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); return new BaselineServerDescription( "Prometeu Studio LSP", "0.1.0", @@ -75,6 +83,8 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg languageServicePresent, codeActionsSupported, documentLinksSupported, + foldingRangesSupported, + selectionRangesSupported, presentation.semanticKeys(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(), @@ -393,6 +403,120 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg .orElseGet(BaselineDocumentLinks::empty); } + @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 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 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 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 mapFoldingRanges( + final List ranges, + final DocumentPositionMapper mapper) { + if (ranges == null || ranges.isEmpty()) { + return List.of(); + } + final ArrayList 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 mapSelectionRanges( + final List ranges, + final DocumentPositionMapper mapper) { + if (ranges == null || ranges.isEmpty()) { + return List.of(); + } + final ArrayList 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, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java index 4d4cad03..a8b8ea89 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -9,6 +9,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; import p.studio.lsp.messages.BaselineCodeActionDiagnostic; +import p.studio.lsp.messages.DocumentPosition; import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.services.LanguageServiceBridge; import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; @@ -168,6 +169,31 @@ public final class PrometeuTextDocumentService implements TextDocumentService { return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result)); } + @Override + public CompletableFuture> 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> selectionRange(final SelectionRangeParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.get(uri); + final ArrayList 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> documentLink(final DocumentLinkParams params) { final String uri = params.getTextDocument().getUri(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index b3608e2e..e1bdf41c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -58,6 +58,12 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { documentLinkOptions.setResolveProvider(false); capabilities.setDocumentLinkProvider(documentLinkOptions); } + if (description.foldingRangesSupported()) { + capabilities.setFoldingRangeProvider(true); + } + if (description.selectionRangesSupported()) { + capabilities.setSelectionRangeProvider(true); + } final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setFull(true); @@ -181,6 +187,47 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { return List.copyOf(mapped); } + @Override + public List mapFoldingRanges(final BaselineFoldingRanges ranges) { + if (ranges == null || ranges.ranges().isEmpty()) { + return List.of(); + } + final ArrayList 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 mapSelectionRanges(final BaselineSelectionRanges ranges) { + if (ranges == null || ranges.ranges().isEmpty()) { + return List.of(); + } + final ArrayList 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> mapCodeActions( final BaselineCodeActions actions, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index 51bb2f84..b7de3e24 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -11,6 +11,8 @@ import org.eclipse.lsp4j.SemanticTokens; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DocumentLink; import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.SelectionRange; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolInformation; @@ -21,6 +23,8 @@ 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.BaselineSelectionRanges; import p.studio.lsp.messages.BaselineDocumentSymbols; import p.studio.lsp.messages.BaselineWorkspaceSymbols; import p.studio.lsp.messages.BaselineCodeActions; @@ -65,6 +69,14 @@ public interface ProtocolMessageMapper { return List.of(); } + default List mapFoldingRanges(BaselineFoldingRanges ranges) { + return List.of(); + } + + default List mapSelectionRanges(BaselineSelectionRanges ranges) { + return List.of(); + } + CompletionList mapCompletion(BaselineCompletion completion); SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 3f00e44b..a9983601 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -11,6 +11,7 @@ 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; @@ -77,6 +78,9 @@ class CompilerLanguageServiceBridgeTest { new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1))) .actions().isEmpty()); assertTrue(bridge.documentLinks(context, documentUri, "fn main() {}").links().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 @@ -772,6 +776,8 @@ class CompilerLanguageServiceBridgeTest { assertTrue(description.renameSupported()); assertTrue(description.codeActionsSupported()); assertTrue(description.documentLinksSupported()); + assertTrue(description.foldingRangesSupported()); + assertTrue(description.selectionRangesSupported()); assertEquals(1, description.semanticHostProjections().size()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index 0f93d292..97d4aaee 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -106,6 +106,8 @@ class PrometeuLanguageServerTest { true, false, false, + false, + false, List.of(), List.of(new BaselineSemanticHostProjection( "vscode", diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index 85ce742d..284cee4b 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -58,6 +58,8 @@ final class Lsp4jProtocolMessageMapperTest { true, true, true, + true, + true, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", @@ -86,6 +88,8 @@ final class Lsp4jProtocolMessageMapperTest { 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()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); @@ -308,6 +312,8 @@ final class Lsp4jProtocolMessageMapperTest { 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)))); @@ -335,6 +341,8 @@ final class Lsp4jProtocolMessageMapperTest { true, true, documentLinksSupported, + documentLinksSupported, + documentLinksSupported, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", From 7629a73e0a1e7290d7b68811f39a3b938e5309cc Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 08:50:18 +0100 Subject: [PATCH 4/8] 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. --- ...ler Pipeline Entry Points Specification.md | 2 + .../compiler/PBSFrontendLanguageService.java | 12 ++ .../compiler/pbs/PbsDocumentFormatter.java | 179 ++++++++++++++++++ .../pbs/PbsDocumentFormatterTest.java | 53 ++++++ .../services/FrontendLanguageService.java | 8 + .../services/FrontendLanguageServiceTest.java | 2 + .../lsp/messages/BaselineFormatting.java | 13 ++ .../messages/BaselineServerDescription.java | 1 + .../studio/lsp/messages/BaselineTextEdit.java | 18 ++ .../lsp/services/LanguageServiceBridge.java | 5 + .../CompilerLanguageServiceBridge.java | 32 ++++ .../protocol/PrometeuTextDocumentService.java | 8 + .../mapping/Lsp4jProtocolMessageMapper.java | 19 ++ .../mapping/ProtocolMessageMapper.java | 6 + .../CompilerLanguageServiceBridgeTest.java | 2 + .../protocol/PrometeuLanguageServerTest.java | 1 + .../Lsp4jProtocolMessageMapperTest.java | 5 + 17 files changed, 366 insertions(+) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsDocumentFormatter.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDocumentFormatterTest.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFormatting.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineTextEdit.java diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index ab924de8..13ebaece 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -294,6 +294,8 @@ When a frontend exposes folding ranges, the ranges MUST come from recovered synt 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 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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index 14439297..b8d1ca1e 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -16,6 +16,7 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocum import p.studio.compiler.models.SourceKind; import p.studio.compiler.pbs.semantics.PbsEditorialRename; import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol; +import p.studio.compiler.pbs.PbsDocumentFormatter; import p.studio.compiler.pbs.PbsStructureRanges; import p.studio.compiler.pbs.semantics.PbsQuickFix; import p.studio.compiler.pbs.semantics.PbsQuickFixCollector; @@ -475,6 +476,17 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService offsets); } + @Override + public boolean formattingSupported() { + return true; + } + + @Override + public Optional formatDocument(final FrontendDocumentRequest request) { + return Optional.of(PbsDocumentFormatter.format( + request == null || request.documentText() == null ? "" : request.documentText())); + } + @Override public boolean codeActionsSupported() { return true; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsDocumentFormatter.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsDocumentFormatter.java new file mode 100644 index 00000000..502f360b --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsDocumentFormatter.java @@ -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 tokens = PbsLexer.lex(text, FileId.none(), DiagnosticSink.empty()); + final ArrayList 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 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 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 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 splitLines(final String text) { + final ArrayList 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) { + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDocumentFormatterTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDocumentFormatterTest.java new file mode 100644 index 00000000..a74768fc --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDocumentFormatterTest.java @@ -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")); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index 48a33e00..cc10e7f8 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -184,6 +184,14 @@ public interface FrontendLanguageService { return selectionRanges(request, offsets); } + default boolean formattingSupported() { + return false; + } + + default java.util.Optional formatDocument(final FrontendDocumentRequest request) { + return java.util.Optional.empty(); + } + default boolean codeActionsSupported() { return false; } diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java index dfb2d63d..52cf4c0a 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java @@ -36,6 +36,8 @@ class FrontendLanguageServiceTest { assertFalse(service.foldingRangesSupported()); assertTrue(service.foldingRanges(request).isEmpty()); assertTrue(service.foldingRanges(request, null).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()); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFormatting.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFormatting.java new file mode 100644 index 00000000..8a0ef588 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineFormatting.java @@ -0,0 +1,13 @@ +package p.studio.lsp.messages; + +import java.util.List; + +public record BaselineFormatting(List edits) { + public BaselineFormatting { + edits = edits == null ? List.of() : List.copyOf(edits); + } + + public static BaselineFormatting empty() { + return new BaselineFormatting(List.of()); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index ef9eba20..93fab3a1 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -19,6 +19,7 @@ public record BaselineServerDescription( boolean documentLinksSupported, boolean foldingRangesSupported, boolean selectionRangesSupported, + boolean formattingSupported, List semanticTokenTypes, List semanticHostProjections, List visualThemes, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineTextEdit.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineTextEdit.java new file mode 100644 index 00000000..194cae4a --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineTextEdit.java @@ -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"); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index f704f8df..0f6bd3ad 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -11,6 +11,7 @@ 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.BaselineFormatting; import p.studio.lsp.messages.BaselineSelectionRanges; import p.studio.lsp.messages.DocumentPosition; import p.studio.lsp.messages.BaselinePrepareRename; @@ -72,6 +73,10 @@ public interface LanguageServiceBridge { return BaselineFoldingRanges.empty(); } + default BaselineFormatting formatDocument(LspProjectContext context, String documentUri, String text) { + return BaselineFormatting.empty(); + } + default BaselineSelectionRanges selectionRanges( LspProjectContext context, String documentUri, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index cf85b739..9815b19d 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -69,6 +69,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg final boolean selectionRangesSupported = languageService .map(FrontendLanguageService::selectionRangesSupported) .orElse(false); + final boolean formattingSupported = languageService + .map(FrontendLanguageService::formattingSupported) + .orElse(false); return new BaselineServerDescription( "Prometeu Studio LSP", "0.1.0", @@ -85,6 +88,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg documentLinksSupported, foldingRangesSupported, selectionRangesSupported, + formattingSupported, presentation.semanticKeys(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(), @@ -403,6 +407,34 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg .orElseGet(BaselineDocumentLinks::empty); } + @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, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java index a8b8ea89..5f56dafa 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -169,6 +169,14 @@ public final class PrometeuTextDocumentService implements TextDocumentService { return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result)); } + @Override + public CompletableFuture> 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> foldingRange(final FoldingRangeRequestParams params) { final String uri = params.getTextDocument().getUri(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index e1bdf41c..95dd0311 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -64,6 +64,9 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { if (description.selectionRangesSupported()) { capabilities.setSelectionRangeProvider(true); } + if (description.formattingSupported()) { + capabilities.setDocumentFormattingProvider(true); + } final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setFull(true); @@ -187,6 +190,22 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { return List.copyOf(mapped); } + @Override + public List mapFormatting(final BaselineFormatting formatting) { + if (formatting == null || formatting.edits().isEmpty()) { + return List.of(); + } + final ArrayList 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 mapFoldingRanges(final BaselineFoldingRanges ranges) { if (ranges == null || ranges.ranges().isEmpty()) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index b7de3e24..3e530bf4 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -12,6 +12,7 @@ import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DocumentLink; import org.eclipse.lsp4j.DocumentSymbol; 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; @@ -24,6 +25,7 @@ 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.BaselineFormatting; import p.studio.lsp.messages.BaselineSelectionRanges; import p.studio.lsp.messages.BaselineDocumentSymbols; import p.studio.lsp.messages.BaselineWorkspaceSymbols; @@ -73,6 +75,10 @@ public interface ProtocolMessageMapper { return List.of(); } + default List mapFormatting(BaselineFormatting formatting) { + return List.of(); + } + default List mapSelectionRanges(BaselineSelectionRanges ranges) { return List.of(); } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index a9983601..285103fe 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -78,6 +78,7 @@ class CompilerLanguageServiceBridgeTest { 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()); @@ -778,6 +779,7 @@ class CompilerLanguageServiceBridgeTest { assertTrue(description.documentLinksSupported()); assertTrue(description.foldingRangesSupported()); assertTrue(description.selectionRangesSupported()); + assertTrue(description.formattingSupported()); assertEquals(1, description.semanticHostProjections().size()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index 97d4aaee..68be0806 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -108,6 +108,7 @@ class PrometeuLanguageServerTest { false, false, false, + false, List.of(), List.of(new BaselineSemanticHostProjection( "vscode", diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index 284cee4b..8238ea69 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -60,6 +60,7 @@ final class Lsp4jProtocolMessageMapperTest { true, true, true, + true, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", @@ -90,6 +91,9 @@ final class Lsp4jProtocolMessageMapperTest { 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()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); @@ -343,6 +347,7 @@ final class Lsp4jProtocolMessageMapperTest { documentLinksSupported, documentLinksSupported, documentLinksSupported, + documentLinksSupported, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", From 202c785f94b60b2f0240a08a0c0c5242a9e061fa Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 09:09:43 +0100 Subject: [PATCH 5/8] 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. --- ...ler Pipeline Entry Points Specification.md | 2 + .../compiler/PBSFrontendLanguageService.java | 27 ++++- .../compiler/pbs/PbsSemanticTokenOverlay.java | 100 ++++++++++++++++++ .../pbs/PbsSemanticTokenOverlayTest.java | 91 ++++++++++++++++ .../services/FrontendLanguageService.java | 6 ++ .../CompilerLanguageServiceBridge.java | 23 +++- .../CompilerLanguageServiceBridgeTest.java | 50 +++++++++ 7 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticTokenOverlay.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticTokenOverlayTest.java diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index 13ebaece..84eba27a 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -294,6 +294,8 @@ When a frontend exposes folding ranges, the ranges MUST come from recovered synt 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 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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index b8d1ca1e..9b674375 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -17,6 +17,7 @@ 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.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; @@ -563,7 +564,31 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService @Override public List semanticTokens(final FrontendDocumentRequest request) { - return semanticTokens(request == null ? "" : request.documentText()); + return semanticTokens(request, null); + } + + @Override + public List semanticTokens( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext) { + final String text = request == null || request.documentText() == null ? "" : request.documentText(); + final List 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 typedSupplementalTopDecls = + (ReadOnlyList) supplementalTopDecls; + return PbsSemanticTokenOverlay.apply( + baseline, + text, + ast, + typedSupplementalTopDecls, + feSurfaceContext, + editorialSupportService); } public List semanticTokens(final String documentText) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticTokenOverlay.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticTokenOverlay.java new file mode 100644 index 00000000..bcc70047 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticTokenOverlay.java @@ -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 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 apply( + final List baseline, + final String documentText, + final PbsAst.File ast, + final ReadOnlyList 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 supplementals = supplementalTopDecls == null + ? ReadOnlyList.empty() + : supplementalTopDecls; + final FESurfaceContext surface = feSurfaceContext == null ? FESurfaceContext.empty() : feSurfaceContext; + final ArrayList 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 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(); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticTokenOverlayTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticTokenOverlayTest.java new file mode 100644 index 00000000..0960695c --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticTokenOverlayTest.java @@ -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 baseline = new PBSSemanticTokenProvider().tokenize(broken); + final List 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 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(); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index cc10e7f8..50e4db4f 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -26,6 +26,12 @@ public interface FrontendLanguageService { return List.of(); } + default List semanticTokens( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext) { + return semanticTokens(request); + } + default List completion( final FrontendDocumentRequest request, final int offset) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index 9815b19d..dd54c7fb 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -2,6 +2,7 @@ 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.FrontendCodeAction; @@ -628,7 +629,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg documentPath, effectiveText); final List 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(); @@ -647,6 +648,26 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg return new BaselineSemanticTokens(presentation.semanticKeys(), semanticTokens); } + private List 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, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 285103fe..68cf5649 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -172,6 +172,56 @@ 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 completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() { final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()) From e456bb8d7a9c246cadcfced1ecfc8dea3d912516 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 09:13:40 +0100 Subject: [PATCH 6/8] 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. --- ...ler Pipeline Entry Points Specification.md | 2 + .../compiler/PBSFrontendLanguageService.java | 41 ++++++ .../ResourceStdlibEnvironmentResolver.java | 73 ++++++++++ .../pbs/stdlib/StdlibEnvironment.java | 4 + .../services/PBSFrontendPhaseService.java | 35 ++++- .../PBSFrontendModuleCompletionTest.java | 127 ++++++++++++++++++ .../compiler/services/FrontendSymbolKind.java | 3 +- .../messages/BaselineCompletionItemKind.java | 3 +- .../CompilerLanguageServiceBridge.java | 1 + .../mapping/Lsp4jProtocolMessageMapper.java | 1 + 10 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/PBSFrontendModuleCompletionTest.java diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index 84eba27a..da97f4d6 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -294,6 +294,8 @@ When a frontend exposes folding ranges, the ranges MUST come from recovered synt 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 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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index 9b674375..194cd5f1 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -24,6 +24,7 @@ import p.studio.compiler.pbs.semantics.PbsQuickFixCollector; import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface; 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; @@ -135,6 +136,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 typedSupplementalTopDecls = (ReadOnlyList) supplementalTopDecls; @@ -595,6 +600,42 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService 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 moduleCompletions(final List labels) { + if (labels == null || labels.isEmpty()) { + return List.of(); + } + final ArrayList 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(), diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/ResourceStdlibEnvironmentResolver.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/ResourceStdlibEnvironmentResolver.java index 982fb845..e439e147 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/ResourceStdlibEnvironmentResolver.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/ResourceStdlibEnvironmentResolver.java @@ -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 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> modulePaths = "jar".equals(url.getProtocol()) + ? jarModulePaths(url, root) + : fileModulePaths(url); + final ArrayList modules = new ArrayList<>(); + for (final List modulePath : modulePaths) { + if (modulePath.isEmpty()) { + continue; + } + final String project = modulePath.getFirst(); + final ReadOnlyList 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> fileModulePaths(final URL url) throws Exception { + final Path rootPath = Path.of(url.toURI()); + final ArrayList> 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> 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> 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 segments(final Path relative) { + final ArrayList segments = new ArrayList<>(); + for (final Path segment : relative) { + segments.add(segment.toString()); + } + return segments; + } + private String moduleBasePath( final String project, final ReadOnlyList pathSegments) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/StdlibEnvironment.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/StdlibEnvironment.java index 88c72513..a27c9216 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/StdlibEnvironment.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/StdlibEnvironment.java @@ -6,5 +6,9 @@ import java.util.Optional; public interface StdlibEnvironment { Optional resolveModule(String project, ReadOnlyList pathSegments); + + default ReadOnlyList modules() { + return ReadOnlyList.empty(); + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index afa5edca..b0ed3b90 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -152,12 +152,14 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { inlineHintsByFile.put(parsedSourceFile.fileId(), inlineHints); } } + final Map physicalModuleFiles = physicalModuleFiles(ctx, assembly); return new PbsSemanticReadSurface( Map.copyOf(astByFile), Map.copyOf(supplementalTopDeclsByFile), Map.copyOf(inlineHintsByFile), Map.copyOf(sourceKindByFile), - physicalModuleFiles(ctx, assembly)); + physicalModuleFiles, + moduleCompletionLabels(ctx, physicalModuleFiles)); } private static Map physicalModuleFiles( @@ -190,12 +192,41 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { return Map.copyOf(files); } + private static List moduleCompletionLabels( + final FrontendPhaseContext ctx, + final Map physicalModuleFiles) { + final var labels = new java.util.TreeSet(); + 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 pathSegments) { + if (project == null || project.isBlank()) { + return ""; + } + if (pathSegments == null || pathSegments.isEmpty()) { + return "@" + project; + } + return "@" + project + ":" + String.join("/", pathSegments.asList()); + } + public record PbsSemanticReadSurface( Map astByFile, Map> supplementalTopDeclsByFile, Map> inlineHintsByFile, Map sourceKindByFile, - Map physicalModuleFileByReference) { + Map physicalModuleFileByReference, + List moduleCompletionLabels) { } private IRBackend mergeCompiledSources( diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/PBSFrontendModuleCompletionTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/PBSFrontendModuleCompletionTest.java new file mode 100644 index 00000000..56268c60 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/PBSFrontendModuleCompletionTest.java @@ -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 labels(final List 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())); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSymbolKind.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSymbolKind.java index cfec8cda..b90f5b31 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSymbolKind.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendSymbolKind.java @@ -17,5 +17,6 @@ public enum FrontendSymbolKind { ENUM, ERROR, GLOBAL, - CONST + CONST, + MODULE } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java index 28da0724..5794c8a4 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java @@ -16,5 +16,6 @@ public enum BaselineCompletionItemKind { ENUM, ERROR, GLOBAL, - CONST + CONST, + MODULE } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index dd54c7fb..e311c457 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -884,6 +884,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg case ERROR -> BaselineCompletionItemKind.ERROR; case GLOBAL -> BaselineCompletionItemKind.GLOBAL; case CONST -> BaselineCompletionItemKind.CONST; + case MODULE -> BaselineCompletionItemKind.MODULE; }; } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index 95dd0311..18b10959 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -479,6 +479,7 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { case ENUM -> CompletionItemKind.Enum; case ERROR -> CompletionItemKind.EnumMember; case GLOBAL, CONST -> CompletionItemKind.Constant; + case MODULE -> CompletionItemKind.Module; }; } From 1fcecdb04d6e6442bd32eed4d08232fcfe34cd79 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 09:20:28 +0100 Subject: [PATCH 7/8] 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. --- discussion/index.ndjson | 4 +- ...aining-editor-surface-is-frontend-owned.md | 70 ++++ ...D-0069-pbs-lsp-remaining-editor-surface.md | 127 ------- ...C-0058-pbs-lsp-remaining-editor-surface.md | 78 ---- ...N-0137-pbs-lsp-remaining-editor-surface.md | 126 ------- ...ler Pipeline Entry Points Specification.md | 2 + .../compiler/PBSFrontendLanguageService.java | 74 ++++ .../studio/compiler/pbs/PbsCallHierarchy.java | 335 ++++++++++++++++++ .../compiler/pbs/PbsCallHierarchyTest.java | 73 ++++ .../services/FrontendCallHierarchyItem.java | 20 ++ .../compiler/services/FrontendCallSite.java | 16 + .../services/FrontendLanguageService.java | 25 ++ .../services/FrontendLanguageServiceTest.java | 2 + .../messages/BaselineCallHierarchyItem.java | 24 ++ .../studio/lsp/messages/BaselineCallSite.java | 18 + .../messages/BaselineServerDescription.java | 1 + .../lsp/services/LanguageServiceBridge.java | 27 ++ .../CompilerLanguageServiceBridge.java | 130 +++++++ .../protocol/PrometeuTextDocumentService.java | 47 +++ .../mapping/Lsp4jProtocolMessageMapper.java | 50 +++ .../mapping/ProtocolMessageMapper.java | 17 + .../CompilerLanguageServiceBridgeTest.java | 57 +++ .../protocol/PrometeuLanguageServerTest.java | 1 + .../Lsp4jProtocolMessageMapperTest.java | 4 + 24 files changed, 995 insertions(+), 333 deletions(-) create mode 100644 discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/LSN-0074-pbs-remaining-editor-surface-is-frontend-owned.md delete mode 100644 discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md delete mode 100644 discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md delete mode 100644 discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsCallHierarchy.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsCallHierarchyTest.java create mode 100644 prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCallHierarchyItem.java create mode 100644 prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCallSite.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCallHierarchyItem.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCallSite.java diff --git a/discussion/index.ndjson b/discussion/index.ndjson index a214584d..04f61c98 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ -{"type":"meta","next_id":{"DSC":67,"AGD":70,"DEC":59,"PLN":138,"LSN":74,"CLSN":1}} -{"type":"discussion","id":"DSC-0066","status":"in_progress","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":[{"id":"AGD-0069","file":"AGD-0069-pbs-lsp-remaining-editor-surface.md","status":"accepted","created_at":"2026-09-22","updated_at":"2026-09-22"}],"decisions":[{"id":"DEC-0058","file":"DEC-0058-pbs-lsp-remaining-editor-surface.md","status":"accepted","created_at":"2026-09-22","updated_at":"2026-09-22","ref_agenda":"AGD-0069"}],"plans":[{"id":"PLN-0137","file":"PLN-0137-pbs-lsp-remaining-editor-surface.md","status":"in_progress","created_at":"2026-09-22","updated_at":"2026-09-22","ref_decisions":["DEC-0058"]}],"lessons":[]} +{"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"}]} diff --git a/discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/LSN-0074-pbs-remaining-editor-surface-is-frontend-owned.md b/discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/LSN-0074-pbs-remaining-editor-surface-is-frontend-owned.md new file mode 100644 index 00000000..35277673 --- /dev/null +++ b/discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/LSN-0074-pbs-remaining-editor-surface-is-frontend-owned.md @@ -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` diff --git a/discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md b/discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md deleted file mode 100644 index cf0fb179..00000000 --- a/discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -id: AGD-0069 -ticket: pbs-lsp-remaining-editor-surface -title: PBS LSP Remaining Editor Surface -status: accepted -created: 2026-09-22 -resolved: -decision: -tags: [studio, lsp, vscode, compiler-pbs, editor] ---- - -## Pain - -Domain owner: `studio` - -Nove agendas de LSP (AGD-0048 a AGD-0056, discussões DSC-0045 a DSC-0053) continuavam abertas como se cada tema partisse do zero. O servidor já anuncia hover, completion, signature help, definition, references, document symbols, workspace symbols, rename, quick fix e semantic tokens. Várias perguntas dessas agendas já foram respondidas pelo código, ou pedem trabalho cuja premissa deixou de existir. Decidir uma a uma repetiria debate vencido e produziria nove decisions para uma superfície só. - -## Context - -Esta agenda substitui, sem herdar o texto como normativo: - -| Agenda aposentada | Discussão | O que o código já fez com o tema | -| --- | --- | --- | -| AGD-0048 formatting | DSC-0045 | Não há `documentFormattingProvider`, `rangeFormatting` nem `onTypeFormatting`. | -| AGD-0049 import assistance | DSC-0046 | Não há auto-import nem organize imports. Imports existem como `ImportDecl` / `ModuleRef`. | -| AGD-0050 semantic tokens | DSC-0047 | Tokens full-document já saem de `PBSSemanticTokenProvider`. | -| AGD-0051 cache e snapshots | DSC-0048 | Cada pedido reconstrói análise. References e rename já existem sem cache. | -| AGD-0052 completion depth | DSC-0049 | Completion já existe, eager, gatilho `.`, `resolveProvider` falso. | -| AGD-0053 folding e selection | DSC-0050 | Não há folding nem selection range. Outline não é folding. | -| AGD-0054 diagnostics UX | DSC-0051 | Diagnóstico já publica range, severity, message e code. Quick fix já casa code + range. | -| AGD-0055 document links | DSC-0052 | Não há `documentLinkProvider`. Definition já recusa alvo que não seja arquivo físico. | -| AGD-0056 call e type hierarchy | DSC-0053 | Não há call hierarchy nem type hierarchy. References não são grafo de chamadas. | - -O que o initialize anuncia hoje, em `Lsp4jProtocolMessageMapper`: sync full, hover, completion, signature help, definition, references, document symbol, workspace symbol, rename com prepare, code action só `quickfix`, semantic tokens full. Nada além disso. - -Fatos que esta agenda trata como fechados, porque o código já os fixou: - -1. Análise de LSP é local ao pedido. `CompilerLanguageServiceBridge.analyzeDocument` monta um `AnalysisSnapshot` novo com overlay do documento aberto. Não há cache de projeto, cancelamento nem reanálise em background. References, rename e workspace symbols já foram entregues em cima desse modelo. Cache não é pré-requisito de mais nenhuma feature desta onda. -2. Semantic tokens não estão por fazer. `PBSSemanticTokenProvider` lexa o arquivo, classifica token léxico direto e classifica identificador por token vizinho, mapa de declaração do próprio arquivo e kinds importados da stdlib. A legenda é `PbsSemanticKind`. Não existem keys de field, local ou parameter. Arquivo quebrado continua tokenizável porque o lexer não depende do sucesso semântico. -3. Completion não está por fazer. `PbsEditorialSupportService.completion` faz duas coisas: se o token anterior é `.`, membros do tipo; senão keywords, parâmetros, locals, símbolos importados e top-level do arquivo. Documentação volta no próprio item. Não há snippets, não há `completionItem/resolve`, não há score de ranking. -4. Diagnóstico não carrega remediação. Spec 23 §8.3 e o quick fix já entregue exigem que o reparo não seja campo do diagnóstico nem venha de parse da mensagem. O mapper copia range, severity, message e code. O campo `source` está errado: `toBaselineIssue` grava o code estável também em `source` (ou `"Prometeu Studio"` quando o code vem vazio). Não há `relatedInformation`. -5. Não há um code de diagnóstico cujo significado seja "nome não importado". Os codes de nome não resolvido que existem são casos pontuais (`E_SEM_CONST_UNRESOLVED_REFERENCE`, `E_SEM_APPLY_UNRESOLVED_OVERLOAD`, `E_SEM_ASSIGN_TARGET_UNRESOLVED`, `E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE`). Inserir import em cima deles mistura typo com import faltando. -6. Definition, references e rename só apontam arquivo físico. Alvo virtual de stdlib não é destino de navegação. -7. A política de indentação em `LSN-0033` pertence ao editor embutido removido. Não é estilo canônico de PBS e não manda no formatter do LSP. -8. Outline, workspace symbols, rename e quick fix não absorvem folding, links, hierarquia nem organize imports. - -Fora desta onda, de propósito: - -- cache de snapshot, cancelamento e reanálise em background; -- type hierarchy, implementações de contract e grafo de host/intrinsic como feature separada; -- `completionItem/resolve`, snippets e modelo de ranking; -- formatting por range ou on-type; -- organize imports e quick fix de import faltando; -- `relatedInformation`, fase do compilador dentro do diagnóstico, e campo de remediação; -- keys novas de semantic token; -- links para assets, addressables, docs ou URI virtual de stdlib. - -## Open Questions - -- [x] **Q1. Qual pacote o plano único executa?** Travada em 2026-09-22: **A**. - - **A.** Os sete passos da recomendação, nesta ordem, um commit cada. Inclui formatter e call hierarchy. - - **B.** Os mesmos passos sem o 7 (call hierarchy fica fora). - - **C.** Os mesmos passos sem o 4 (formatter fica fora até existir estilo aceito). - - **D.** Só os passos que não escolhem estilo nem grafo novo: 1, 2, 3 e 6. - -- [x] **Q2. Se o passo 4 entrar, qual estilo o formatter aplica?** Travada em 2026-09-22: **A**. - - **A.** Reimpressão do token stream. Não reordena declarações. Não junta nem parte linhas para embelezar. Indentação de 4 espaços só onde o aninhamento de chaves, parênteses ou bloco muda. Interior de text block `Doc` intocado. Comentário permanece na linha do token a que já estava preso. Documento inteiro apenas. - - **B.** Não travar estilo. O passo 4 sai do plano mesmo se Q1 escolher A ou B. - -## Options - -### Option A - Uma onda, um plano, commits por passo - -- **Approach:** Uma decision e um plan. O plan copia os passos abaixo que Q1/Q2 deixarem vivos. Cada passo é um commit na mesma branch. Lesson e housekeep só depois do último passo. -- **Pro:** O código atual vira o piso. Tema vencido não vira decision. A série continua um commit por entrega, sem nove discussões. -- **Con:** Formatter e call hierarchy ainda são maiores que links ou o rótulo de `source`. -- **Maintainability:** Forte se cada passo não reabrir cache, type hierarchy ou auto-import. - -### Option B - Só o que já não tem escolha de produto - -- **Approach:** Mesma discussão única, mas o plano fica em diagnóstico `source`, document links físicos, folding/selection e completion de module ref. -- **Pro:** Evita travar estilo e evita um grafo de chamadas novo. -- **Con:** Formatting e call hierarchy continuam sem dono depois de aposentar as agendas velhas. -- **Maintainability:** Boa, e menor. - -## Tradeoffs - -Juntar as nove agendas num plano só é seguro porque o piso já está no código. O risco é enfiar de volta, como "passo pequeno", cache, contrato novo de diagnóstico ou auto-import. Esses três não cabem: o primeiro perdeu a premissa, o segundo quebra o quick fix já aceito, o terceiro não tem diagnóstico estável para se pendurar. - -Formatter é o único passo que ainda escolhe estilo. Por isso ele é um passo isolado: recusar Q2 tira só esse commit. Call hierarchy não pode reusar o índice de references; references são usos da mesma identidade, não chamadas. Ou entra como caminhada de `CallExpr` no último commit, ou fica de fora. - -## Recommendation - -Aceitar Option A com Q1-A e Q2-A. - -Forma do plano, obrigatória: - -- uma decision e um plan desta discussão; -- branch `dev/pbs-lsp-remaining-editor-surface`, não uma branch por passo e não commit em `master`; -- cada passo vivo vira um commit `implements PLN-NNNN (k/N) `; -- sem push; -- spec 23 §8.3 só ganha parágrafo do passo que a superfície realmente passa a anunciar; -- lesson e `discussion housekeep` só no commit do último passo. - -Passos, nesta ordem. Passo que Q1 ou Q2 tirar não é renumerado no meio da implementação: o plan lista só os que sobreviverem, em ordem. - -1. **Diagnostic source.** `source` passa a ser o language id do frontend (`pbs`). `code` continua o code estável. Sem `relatedInformation`, sem fase, sem data de reparo. -2. **Document links.** Link só no span do `ModuleRef` de um import cujo destino já resolve para arquivo físico regular. Sem destino físico, sem link. Stdlib virtual, asset e doc ficam de fora. -3. **Folding e selection range.** Span de AST quando a árvore recuperada tem o nó: declaração top-level (`fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, `host`), corpo, lista de parâmetros e text block `Doc`. Fallback de token só para chave, parêntese ou text block que a árvore não cobre. Selection, do interno para o externo: identificador, lista de argumento ou parâmetro, bloco ou text block, declaração. Lista vazia quando não houver range. Não reutilizar document symbols. -4. **Formatting de documento inteiro.** Só se Q2 travar estilo. Sem range e sem on-type. Preserva comentário e interior de text block `Doc`. Não é pretty-printer de AST. -5. **Overlay semântico em cima do token atual.** Não substituir `PBSSemanticTokenProvider`. Onde a mesma resolução de hover já nomeia o símbolo, trocar o kind fraco por um `PbsSemanticKind` que já existe. Falha, arquivo quebrado ou símbolo desconhecido mantém o token de hoje. Nenhuma key nova. -6. **Completion de module ref.** No cursor dentro de `ModuleRef`, candidatos que o resolver atual já sabe nomear, módulo de projeto e stdlib. Continua eager. `resolveProvider` continua falso. Sem snippets e sem score novo. Fora de module ref, o completion atual não muda. -7. **Call hierarchy.** Incoming e outgoing de função e método por caminhada de callsite na AST, não pelo índice de references. Cursor que não é callable devolve vazio. Sem type hierarchy e sem cache. - -## Discussion - -As agendas AGD-0048 a AGD-0056 foram aposentadas a pedido, neste consolidado, antes de qualquer accept. O texto delas não é decisão. A CLI `discussion` não tem comando de abandon; a aposentadoria ficou no índice e no frontmatter, com a lacuna registrada para um comando futuro. - -Não aceitar esta agenda até Q1 e Q2 terem resposta explícita. A recomendação é o pacote, não um convite para preencher o resto na implementação. - -## Resolution - -Q1-A e Q2-A travados pelo usuário em 2026-09-22. O pacote da recomendação é a escolha que a decision torna normativa. - -## Next Step - -Decision aceita e plan desta discussão. Implementar os sete passos em série, um commit cada. diff --git a/discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md b/discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md deleted file mode 100644 index 2627e0af..00000000 --- a/discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: DEC-0058 -ticket: pbs-lsp-remaining-editor-surface -title: PBS LSP remaining editor surface -status: accepted -created: 2026-09-22 -ref_agenda: AGD-0069 -tags: [studio, lsp, vscode, compiler-pbs, editor] ---- - -## Context - -AGD-0069 consolidou AGD-0048 a AGD-0056. Essas agendas foram aposentadas e não são normativas. O usuário travou Q1-A e Q2-A em 2026-09-22. - -O LSP já anuncia hover, completion, signature help, definition, references, document symbols, workspace symbols, rename, quick fix e semantic tokens full. Cada pedido reconstrói a análise. Definition, references e rename só devolvem arquivo físico. Quick fix é um `WorkspaceEdit` produzido pelo frontend, casado por code e range, sem remediação dentro do diagnóstico. `PBSSemanticTokenProvider` já classifica o documento a partir do lexer. Completion já cobre membro depois de `.` e o contexto geral, com documentação no próprio item e sem `resolve`. - -O que falta é a superfície desta decision. O campo `source` do diagnóstico publicado está errado: hoje recebe o code estável, ou `"Prometeu Studio"` quando o code vem vazio. - -## Decision - -A discussão DSC-0066 tem uma decision e um plan. O plan executa sete passos, nesta ordem, um commit cada, na branch `dev/pbs-lsp-remaining-editor-surface`. Lesson e housekeep acontecem só no commit do último passo. Não há push nesta decision. - -O frontend produz o resultado editorial. O LSP transporta. O LSP MUST NOT inventar reparo, link, range, formatação, token ou chamada que o frontend não devolveu. Capacidade ausente MUST NOT ser anunciada e MUST devolver lista vazia, não erro de protocolo. - -### 1. Diagnostic source - -O `source` de um diagnóstico editorial publicado MUST ser o `languageId` do `LspProjectContext` daquele pedido. Numa sessão PBS esse valor é `pbs`. O `code` MUST continuar o code estável do diagnóstico do compilador, vazio quando o compilador não tem code. O LSP MUST NOT copiar o code para `source`. O LSP MUST NOT publicar `relatedInformation`, fase do compilador ou payload de reparo nesse diagnóstico. - -### 2. Document links - -O servidor MUST anunciar `documentLink` só para esta capacidade. Cada link MUST cobrir o span do `ModuleRef` de um import cujo destino o resolver atual já entrega como arquivo físico regular. Sem arquivo físico, MUST NOT haver link. Stdlib virtual, asset, addressable, doc e URI sintético MUST NOT ser alvo. O link MUST NOT substituir definition. - -### 3. Folding e selection range - -O servidor MUST anunciar folding range e selection range. Range de declaração top-level (`fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, `host`), corpo, lista de parâmetros e text block `Doc` MUST vir do span da árvore recuperada quando o nó existe. Chave, parêntese ou text block que a árvore não cobre MUST usar fallback de token. Selection, do interno para o externo, MUST ser: identificador, lista de argumento ou parâmetro, bloco ou text block, declaração. Sem range, a resposta MUST ser lista vazia. Document symbols MUST NOT ser reutilizados como folding. - -### 4. Formatting - -O servidor MUST anunciar formatting só de documento inteiro. Range formatting e on-type formatting MUST NOT ser anunciados. O formatter MUST reimprimir o token stream. MUST NOT reordenar declarações. MUST NOT juntar ou partir linhas para embelezar. Indentação MUST ser 4 espaços, aplicada só onde o aninhamento de chaves, parênteses ou bloco muda. O interior de text block `Doc` MUST permanecer intocado. Comentário MUST permanecer na linha do token a que já estava preso. Pretty-printer de AST MUST NOT ser o formatter. `LSN-0033` MUST NOT definir esse estilo. - -### 5. Overlay semântico - -`PBSSemanticTokenProvider` MUST continuar sendo a base lexical. Onde a mesma resolução usada pelo hover nomeia o símbolo e existe um `PbsSemanticKind` para esse símbolo, o token MUST trocar o kind fraco por esse kind existente. Falha de resolução, arquivo quebrado ou símbolo sem kind existente MUST manter o token que o provider emitiu. Esta onda MUST NOT criar key nova de semantic token. - -### 6. Completion de module ref - -Com o cursor dentro de um `ModuleRef`, completion MUST oferecer módulos de projeto e de stdlib que o resolver atual já sabe nomear. Fora de `ModuleRef`, o completion atual MUST permanecer. A resposta MUST continuar eager. `resolveProvider` MUST continuar falso. Snippets e score novo de ranking MUST NOT entrar. - -### 7. Call hierarchy - -O servidor MUST anunciar call hierarchy para função e método. Incoming e outgoing MUST ser uma caminhada de callsite na AST. O índice de references MUST NOT ser tratado como grafo de chamadas. Cursor que não está num callable MUST devolver vazio. Type hierarchy, implementações de contract e grafo separado de host ou intrinsic MUST NOT entrar. - -### Fora desta decision - -Esta decision MUST NOT pedir cache de snapshot, cancelamento, reanálise em background, organize imports, quick fix de import faltando, `completionItem/resolve`, formatting por range ou on-type, `relatedInformation`, ou links que não sejam o `ModuleRef` físico do passo 2. - -## Rationale - -Q1-A mantém os sete passos porque cada um cobre um buraco real e o piso já está implementado. Q2-A isola o estilo no formatter para ele não vazar para os outros passos. Cache deixou de ser pré-requisito quando references e rename saíram em análise local ao pedido. Auto-import não tem diagnóstico cujo significado seja "nome não importado". Type hierarchy não compartilha modelo com call hierarchy. - -## Implications - -Os passos 2, 3, 4 e 7 acrescentam capacidade anunciada. Os passos 1, 5 e 6 corrigem ou estendem capacidade que o servidor já anuncia. Nenhum passo reabre o contrato de quick fix nem o de definition física. Spec 23 §8.3 ganha o parágrafo do passo no commit daquele passo, não antes. - -## Propagation Targets - -- specs: `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` §8.3, um parágrafo por passo, no commit desse passo. -- plans: um plan de DSC-0066 com os sete passos e um commit cada. -- code: `prometeu-compiler` onde o frontend produz o resultado; `prometeu-lsp` onde o protocolo transporta. `source` do diagnóstico usa `LspProjectContext.languageId`. -- tests: bridge e, quando a capacidade é nova, o anúncio no mapper. Arquivo quebrado continua coberto em token e folding. -- docs: nenhum outro. A lesson em inglês só no housekeep do último passo. - -## References - -- Agenda: AGD-0069 -- Discussões aposentadas: DSC-0045 a DSC-0053, AGD-0048 a AGD-0056 -- Spec: `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` §8.3 -- Lessons de piso: LSN-0068, LSN-0069, LSN-0070, LSN-0071, LSN-0072, LSN-0073 diff --git a/discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md b/discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md deleted file mode 100644 index 46fa8731..00000000 --- a/discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -id: PLN-0137 -ticket: pbs-lsp-remaining-editor-surface -title: PBS LSP remaining editor surface -status: in_progress -created: 2026-09-22 -ref_decisions: [DEC-0058] -tags: [studio, lsp, vscode, compiler-pbs, editor] ---- - -## Briefing - -DEC-0058 locks one PBS LSP wave on top of the editor surface that already ships. Seven steps run in order on `dev/pbs-lsp-remaining-editor-surface`. Each step is one commit. The frontend produces the editorial result. The LSP transports it and does not invent a second policy. - -## Objective - -Land diagnostic source, physical import document links, folding and selection ranges, full-document formatting, a semantic-token overlay, module-ref completion, and function/method call hierarchy. Leave snapshot cache, type hierarchy, auto-import, and resolve completion out. - -## Dependencies - -Shipped and unchanged by this plan: request-local analysis, physical definition and references, rename, document symbols, workspace symbols, quick fix (LSN-0073), `PBSSemanticTokenProvider`, and eager completion in `PbsEditorialSupportService`. Spec 23 §8.3 is the only normative doc updated, one paragraph in the commit of the step that introduces that rule. - -## Scope - -Local to studio LSP and the PBS frontend. Shared compiler entry points do not gain a cache or a new pipeline phase. New LSP capabilities follow the code-action pattern: `FrontendLanguageService` defaults to unsupported, PBS overrides support, `BaselineServerDescription` advertises only that flag, and a missing flag is an empty list. - -### Step 1. Diagnostic source - -In `CompilerLanguageServiceBridge.toBaselineIssue`, set `BaselineDocumentIssue.source` from `LspProjectContext.languageId`. Keep `code` as the trimmed compiler code, empty when the compiler code is null or blank. Do not special-case `"Prometeu Studio"`. `Lsp4jProtocolMessageMapper.mapDiagnostic` already copies `source` and `code` and must stay without `relatedInformation` or `data`. - -Spec 23 §8.3 gains one paragraph: published editor diagnostic `source` is the bound frontend `languageId`; `code` is the stable compiler code or empty; tooling must not copy the code into `source` and must not publish related locations, phase, or a repair payload. - -### Step 2. Document links - -Add a frontend document-link result: span of a `ModuleRef` plus a physical file target. Default on `FrontendLanguageService` is an empty list and `documentLinksSupported` false. PBS returns a link only when the resolver the compiler already uses yields a regular file. No link for virtual stdlib, assets, docs, or an unresolved module. Do not add a second module resolver. - -LSP: baseline message, `documentLinkProvider`, `PrometeuTextDocumentService.documentLink`, mapper. Unsupported stays unadvertised and returns an empty list. - -### Step 3. Folding and selection ranges - -Frontend produces both lists from the recovered AST when the node has a span: top-level `fn`, `struct`, `service`, `contract`, `enum`, `error`, `callback`, and `host`; their bodies; parameter lists; `Doc` text blocks. Token fallback covers only a brace, parenthesis, or text-block delimiter the tree does not cover. Selection order, inner to outer: identifier, argument or parameter list, block or text block, declaration. Empty when there is nothing. Do not build either list from document symbols. - -LSP advertises `foldingRangeProvider` and `selectionRangeProvider` only when the frontend flags are true. - -### Step 4. Full-document formatting - -Frontend formatter reprints the token stream. It does not reorder declarations, join lines, or split lines to beautify. Indent is four spaces and changes only where brace, parenthesis, or block nesting changes. `Doc` text-block interiors are copied unchanged. A comment stays on the token line it already belongs to. No AST pretty-printer. No range formatting. No on-type formatting. Do not read `.studio/setup.json` or LSN-0033. - -LSP advertises `documentFormattingProvider` only. - -### Step 5. Semantic-token overlay - -Keep `PBSSemanticTokenProvider` as the lexical baseline. After that pass, where the same resolution hover uses names a symbol and an existing `PbsSemanticKind` matches it, replace the weaker key. On resolution failure, a broken file, or a symbol with no existing key, keep the provider token. Add no `PbsSemanticKind` constant. The server already advertises semantic tokens; do not add a second provider. - -### Step 6. Module-ref completion - -When the completion offset sits inside a `ModuleRef`, return project and stdlib modules the current resolver can already name. Every other offset keeps today's member or general completion. Items stay eager. `CompletionOptions.resolveProvider` stays false. No snippets. No new rank score. No new trigger-character policy beyond what module-ref detection needs inside the existing completion request. - -### Step 7. Call hierarchy - -Frontend walks `CallExpr` callsites for incoming and outgoing calls of functions and methods. Do not query the references index. A cursor that is not on a callable returns empty. No type hierarchy and no separate host or intrinsic graph. - -LSP advertises call hierarchy only. This commit also writes the English lesson, runs `discussion housekeep DSC-0066 --lesson `, and `discussion validate`. - -## Non-Goals - -- Project snapshot cache, cancellation, and background reanalysis. -- Type hierarchy, contract implementation hierarchy, and a separate host or intrinsic graph. -- `completionItem/resolve`, snippets, and a ranking model. -- Range formatting and on-type formatting. -- Organize imports and a missing-import quick fix. -- `relatedInformation`, compiler phase on the diagnostic, and repair data. -- New semantic-token keys. -- Document links for assets, addressables, docs, or virtual stdlib URIs. -- Reusing document symbols as folding ranges. -- Pushing the branch. - -## Execution Method - -1. Work on `dev/pbs-lsp-remaining-editor-surface`. Do not commit on `master`. -2. Mark this plan `in_progress` before step 1 code. -3. Implement one step, then run the tests listed for that step. -4. Commit that step before starting the next. Subject: `implements PLN-0137 (k/7) `. -5. Stage `discussion`, `docs`, `prometeu-compiler`, and `prometeu-lsp` as the step touches them. Do not stage `AGENTS.md`. -6. Spec 23 §8.3 is edited in the same commit as the step whose rule it states. -7. Steps are not reordered and are not parallel. A later step may use the capability flags introduced earlier; it must not reopen a closed step. -8. The seventh commit includes the lesson and housekeep. Earlier commits keep DEC-0058 and this plan in the tree. - -Commit slugs: - -1. `diagnostic-source` -2. `document-links` -3. `folding-and-selection-ranges` -4. `document-formatting` -5. `semantic-token-overlay` -6. `module-ref-completion` -7. `call-hierarchy` - -## Acceptance Criteria - -- Step 1: every issue from `analyzeDocument` on a PBS project has `source` equal to `pbs`, `code` is the stable compiler code, and `source` is not that code. No related information is mapped. -- Step 2: a `ModuleRef` that resolves to a regular file becomes one document link on that span. Virtual stdlib and unresolved modules produce no link. The provider is advertised only when supported. -- Step 3: a well-formed declaration, body, parameter list, and `Doc` text block fold from AST spans. An unmatched brace still produces a token fallback range. Selection nests in the decided order. Document symbols are unchanged. -- Step 4: formatting a document does not change `Doc` text-block interiors, does not move a comment off its token line, does not reorder declarations, and only changes indent by four spaces at nesting changes. Range and on-type providers stay off. -- Step 5: an identifier hover can resolve changes to an existing semantic key; a broken file still returns the lexical tokens; `PbsSemanticKind` gains no constant. -- Step 6: completion inside a `ModuleRef` lists resolver-known modules; completion after `.` is unchanged; `resolveProvider` is false. -- Step 7: incoming and outgoing calls report function and method callsites found in the AST. A non-callable cursor is empty. Type hierarchy is not advertised. `discussion validate` passes after housekeep, and the lesson states the seven rules. - -## Tests - -- `CompilerLanguageServiceBridgeTest` for diagnostic source, and for each new bridge method as it lands. -- `Lsp4jProtocolMessageMapperTest` for capability advertisement of each new provider, and for the absence of range formatting, on-type formatting, resolve, related information, and type hierarchy. -- PBS frontend tests for links, folding, selection, formatting, the token overlay, module-ref completion, and call hierarchy. Include one broken-file case for tokens and one unmatched-delimiter case for folding. -- Do not require a running VS Code instance. Bridge and mapper tests are the protocol gate. - -## Affected Artifacts - -- `discussion/workflow/agendas/AGD-0069-pbs-lsp-remaining-editor-surface.md` -- `discussion/workflow/decisions/DEC-0058-pbs-lsp-remaining-editor-surface.md` -- `discussion/workflow/plans/PLN-0137-pbs-lsp-remaining-editor-surface.md` -- `discussion/index.ndjson` -- `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` -- `prometeu-compiler/prometeu-frontend-api` (`FrontendLanguageService` and new result types) -- `prometeu-compiler/frontends/prometeu-frontend-pbs` (PBS implementations) -- `prometeu-lsp/prometeu-lsp-v1` (bridge, baseline messages, text document service, mapper) -- Lesson, only in the step 7 commit: `discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/` diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index da97f4d6..21e9899e 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -294,6 +294,8 @@ When a frontend exposes folding ranges, the ranges MUST come from recovered synt 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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index 194cd5f1..1019f284 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -16,12 +16,15 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocum import p.studio.compiler.models.SourceKind; import p.studio.compiler.pbs.semantics.PbsEditorialRename; import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol; +import p.studio.compiler.pbs.PbsCallHierarchy; import p.studio.compiler.pbs.PbsDocumentFormatter; import p.studio.compiler.pbs.PbsSemanticTokenOverlay; import p.studio.compiler.pbs.PbsStructureRanges; import p.studio.compiler.pbs.semantics.PbsQuickFix; import p.studio.compiler.pbs.semantics.PbsQuickFixCollector; import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface; +import p.studio.compiler.services.FrontendCallHierarchyItem; +import p.studio.compiler.services.FrontendCallSite; import p.studio.compiler.services.FrontendCodeAction; import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendSymbolKind; @@ -482,6 +485,77 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService offsets); } + @Override + public boolean callHierarchySupported() { + return true; + } + + @Override + public List 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 incomingCalls( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final FrontendCallHierarchyItem target) { + return callSites(callHierarchyAst(editorialContext), target, true); + } + + @Override + public List outgoingCalls( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final FrontendCallHierarchyItem target) { + return callSites(callHierarchyAst(editorialContext), target, false); + } + + private List 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; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsCallHierarchy.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsCallHierarchy.java new file mode 100644 index 00000000..61cf27b9 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsCallHierarchy.java @@ -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 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 incomingCalls(final PbsAst.File ast, final Callable target) { + if (target == null) { + return List.of(); + } + final ArrayList calls = new ArrayList<>(); + for (final CallSite call : calls(ast)) { + if (sameCallable(call.callee(), target)) { + calls.add(call); + } + } + return List.copyOf(calls); + } + + public static List outgoingCalls(final PbsAst.File ast, final Callable target) { + if (target == null) { + return List.of(); + } + final ArrayList 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 callables(final PbsAst.File ast) { + final ArrayList 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 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 calls(final PbsAst.File ast) { + final List callables = callables(ast); + final ArrayList calls = new ArrayList<>(); + for (final Callable caller : callables) { + final PbsAst.Block body = body(ast, caller); + if (body == null) { + continue; + } + final ArrayList 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 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 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 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 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); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsCallHierarchyTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsCallHierarchyTest.java new file mode 100644 index 00000000..54a31d69 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsCallHierarchyTest.java @@ -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; + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCallHierarchyItem.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCallHierarchyItem.java new file mode 100644 index 00000000..a9ac07d8 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCallHierarchyItem.java @@ -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"); + } + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCallSite.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCallSite.java new file mode 100644 index 00000000..213d9d1d --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendCallSite.java @@ -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"); + } + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index 50e4db4f..8cad357f 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -190,6 +190,31 @@ public interface FrontendLanguageService { return selectionRanges(request, offsets); } + default boolean callHierarchySupported() { + return false; + } + + default java.util.List prepareCallHierarchy( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final int offset) { + return java.util.List.of(); + } + + default java.util.List incomingCalls( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final FrontendCallHierarchyItem target) { + return java.util.List.of(); + } + + default java.util.List outgoingCalls( + final FrontendDocumentRequest request, + final FrontendEditorialContext editorialContext, + final FrontendCallHierarchyItem target) { + return java.util.List.of(); + } + default boolean formattingSupported() { return false; } diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java index 52cf4c0a..a501a4ca 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java @@ -36,6 +36,8 @@ class FrontendLanguageServiceTest { 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()); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCallHierarchyItem.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCallHierarchyItem.java new file mode 100644 index 00000000..bdc7a4ba --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCallHierarchyItem.java @@ -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"); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCallSite.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCallSite.java new file mode 100644 index 00000000..6ab21ff7 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCallSite.java @@ -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"); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index 93fab3a1..0aba519e 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -20,6 +20,7 @@ public record BaselineServerDescription( boolean foldingRangesSupported, boolean selectionRangesSupported, boolean formattingSupported, + boolean callHierarchySupported, List semanticTokenTypes, List semanticHostProjections, List visualThemes, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index 0f6bd3ad..2bb906f9 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -11,6 +11,8 @@ 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; @@ -77,6 +79,31 @@ public interface LanguageServiceBridge { return BaselineFormatting.empty(); } + default java.util.List prepareCallHierarchy( + LspProjectContext context, + String documentUri, + String text, + int line, + int character) { + return java.util.List.of(); + } + + default java.util.List incomingCalls( + LspProjectContext context, + String documentUri, + String text, + BaselineCallHierarchyItem target) { + return java.util.List.of(); + } + + default java.util.List outgoingCalls( + LspProjectContext context, + String documentUri, + String text, + BaselineCallHierarchyItem target) { + return java.util.List.of(); + } + default BaselineSelectionRanges selectionRanges( LspProjectContext context, String documentUri, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index e311c457..fa04159c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -5,6 +5,8 @@ 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; @@ -73,6 +75,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg 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", @@ -90,6 +95,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg foldingRangesSupported, selectionRangesSupported, formattingSupported, + callHierarchySupported, presentation.semanticKeys(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(), @@ -408,6 +414,130 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg .orElseGet(BaselineDocumentLinks::empty); } + @Override + public List 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 incomingCalls( + final LspProjectContext context, + final String documentUri, + final String text, + final BaselineCallHierarchyItem target) { + return directedCalls(context, documentUri, text, target, true); + } + + @Override + public List outgoingCalls( + final LspProjectContext context, + final String documentUri, + final String text, + final BaselineCallHierarchyItem target) { + return directedCalls(context, documentUri, text, target, false); + } + + private List 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 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, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java index 5f56dafa..fed40076 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -8,6 +8,8 @@ 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; @@ -169,6 +171,51 @@ public final class PrometeuTextDocumentService implements TextDocumentService { return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result)); } + @Override + public CompletableFuture> 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> callHierarchyIncomingCalls( + final CallHierarchyIncomingCallsParams params) { + return CompletableFuture.completedFuture(protocolMessageMapper.mapIncomingCalls( + directedCallSites(params.getItem(), true))); + } + + @Override + public CompletableFuture> callHierarchyOutgoingCalls( + final CallHierarchyOutgoingCallsParams params) { + return CompletableFuture.completedFuture(protocolMessageMapper.mapOutgoingCalls( + directedCallSites(params.getItem(), false))); + } + + private List 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> formatting(final DocumentFormattingParams params) { final String uri = params.getTextDocument().getUri(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index 18b10959..f8f9d9eb 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -67,6 +67,9 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { 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); @@ -190,6 +193,53 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { return List.copyOf(mapped); } + @Override + public List mapCallHierarchyItems(final List items) { + if (items == null || items.isEmpty()) { + return List.of(); + } + return items.stream().map(this::mapCallHierarchyItem).toList(); + } + + @Override + public List mapIncomingCalls(final List 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 mapOutgoingCalls(final List 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 mapFormatting(final BaselineFormatting formatting) { if (formatting == null || formatting.edits().isEmpty()) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index 3e530bf4..cf84b230 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -11,6 +11,9 @@ 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; @@ -25,6 +28,8 @@ 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; @@ -79,6 +84,18 @@ public interface ProtocolMessageMapper { return List.of(); } + default List mapCallHierarchyItems(List items) { + return List.of(); + } + + default List mapIncomingCalls(List calls) { + return List.of(); + } + + default List mapOutgoingCalls(List calls) { + return List.of(); + } + default List mapSelectionRanges(BaselineSelectionRanges ranges) { return List.of(); } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 68cf5649..1029eb96 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -222,6 +222,62 @@ class CompilerLanguageServiceBridgeTest { 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()) @@ -830,6 +886,7 @@ class CompilerLanguageServiceBridgeTest { 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() diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index 68be0806..db513485 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -109,6 +109,7 @@ class PrometeuLanguageServerTest { false, false, false, + false, List.of(), List.of(new BaselineSemanticHostProjection( "vscode", diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index 8238ea69..0ab4fd84 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -61,6 +61,7 @@ final class Lsp4jProtocolMessageMapperTest { true, true, true, + true, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", @@ -94,6 +95,8 @@ final class Lsp4jProtocolMessageMapperTest { 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")); @@ -348,6 +351,7 @@ final class Lsp4jProtocolMessageMapperTest { documentLinksSupported, documentLinksSupported, documentLinksSupported, + documentLinksSupported, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", From 274ce28e8dba792306eea649f40b9bcb9f17e887 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 22 Sep 2026 09:53:21 +0100 Subject: [PATCH 8/8] clean up and fixes --- discussion/index.ndjson | 18 +++--- .../agendas/AGD-0048-pbs-lsp-formatting.md | 62 ------------------- .../AGD-0049-pbs-lsp-import-assistance.md | 62 ------------------- ...0-pbs-lsp-semantic-token-classification.md | 62 ------------------- ...pbs-lsp-performance-cache-and-snapshots.md | 62 ------------------- .../AGD-0052-pbs-lsp-completion-depth.md | 62 ------------------- ...53-pbs-lsp-folding-and-selection-ranges.md | 62 ------------------- .../AGD-0054-pbs-lsp-diagnostics-ux.md | 62 ------------------- .../AGD-0055-pbs-lsp-document-links.md | 62 ------------------- ...GD-0056-pbs-lsp-call-and-type-hierarchy.md | 62 ------------------- .../StudioRuntimeHandshakeServiceTest.java | 11 +++- 11 files changed, 18 insertions(+), 569 deletions(-) delete mode 100644 discussion/workflow/agendas/AGD-0048-pbs-lsp-formatting.md delete mode 100644 discussion/workflow/agendas/AGD-0049-pbs-lsp-import-assistance.md delete mode 100644 discussion/workflow/agendas/AGD-0050-pbs-lsp-semantic-token-classification.md delete mode 100644 discussion/workflow/agendas/AGD-0051-pbs-lsp-performance-cache-and-snapshots.md delete mode 100644 discussion/workflow/agendas/AGD-0052-pbs-lsp-completion-depth.md delete mode 100644 discussion/workflow/agendas/AGD-0053-pbs-lsp-folding-and-selection-ranges.md delete mode 100644 discussion/workflow/agendas/AGD-0054-pbs-lsp-diagnostics-ux.md delete mode 100644 discussion/workflow/agendas/AGD-0055-pbs-lsp-document-links.md delete mode 100644 discussion/workflow/agendas/AGD-0056-pbs-lsp-call-and-type-hierarchy.md diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 04f61c98..f006b70d 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -12,15 +12,15 @@ {"type":"discussion","id":"DSC-0056","status":"done","ticket":"multi-frontend-remove-pbs-branches","title":"Generalizar o contrato LSP/editorial para frontends","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0058","file":"discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0055","status":"done","ticket":"multi-frontend-compiler-vs-language-services","title":"Separar compilacao de servicos editoriais de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","lsp","editor","frontend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0056","file":"discussion/lessons/DSC-0055-multi-frontend-compiler-vs-language-services/LSN-0056-compile-first-frontends-with-optional-editorial-capabilities.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} {"type":"discussion","id":"DSC-0054","status":"done","ticket":"multi-frontend-provider-contract","title":"Introduzir provider completo de frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","registry","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0055","file":"discussion/lessons/DSC-0054-multi-frontend-provider-contract/LSN-0055-static-frontend-providers-before-plugin-architecture.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]} -{"type":"discussion","id":"DSC-0053","status":"abandoned","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":[{"id":"AGD-0056","file":"AGD-0056-pbs-lsp-call-and-type-hierarchy.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0052","status":"abandoned","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":[{"id":"AGD-0055","file":"AGD-0055-pbs-lsp-document-links.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0051","status":"abandoned","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":[{"id":"AGD-0054","file":"AGD-0054-pbs-lsp-diagnostics-ux.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0050","status":"abandoned","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":[{"id":"AGD-0053","file":"AGD-0053-pbs-lsp-folding-and-selection-ranges.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0049","status":"abandoned","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":[{"id":"AGD-0052","file":"AGD-0052-pbs-lsp-completion-depth.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0048","status":"abandoned","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":[{"id":"AGD-0051","file":"AGD-0051-pbs-lsp-performance-cache-and-snapshots.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0047","status":"abandoned","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":[{"id":"AGD-0050","file":"AGD-0050-pbs-lsp-semantic-token-classification.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0046","status":"abandoned","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":[{"id":"AGD-0049","file":"AGD-0049-pbs-lsp-import-assistance.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0045","status":"abandoned","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":[{"id":"AGD-0048","file":"AGD-0048-pbs-lsp-formatting.md","status":"abandoned","created_at":"2026-07-15","updated_at":"2026-09-22","_override_reason":"Merged into DSC-0066 / AGD-0069 on 2026-09-22. Separate agendas were stale against the shipped LSP. Explicit user request to consolidate before any decision."}],"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"}]} diff --git a/discussion/workflow/agendas/AGD-0048-pbs-lsp-formatting.md b/discussion/workflow/agendas/AGD-0048-pbs-lsp-formatting.md deleted file mode 100644 index 461fdb7d..00000000 --- a/discussion/workflow/agendas/AGD-0048-pbs-lsp-formatting.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0048 -ticket: pbs-lsp-formatting -title: PBS LSP Formatting -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -Decide PBS canonical formatting rules for attributes, braces, imports, params, and text blocks. diff --git a/discussion/workflow/agendas/AGD-0049-pbs-lsp-import-assistance.md b/discussion/workflow/agendas/AGD-0049-pbs-lsp-import-assistance.md deleted file mode 100644 index ca78f160..00000000 --- a/discussion/workflow/agendas/AGD-0049-pbs-lsp-import-assistance.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0049 -ticket: pbs-lsp-import-assistance -title: PBS LSP Import Assistance -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -Decide first-wave import sources: stdlib only, project modules only, or both. diff --git a/discussion/workflow/agendas/AGD-0050-pbs-lsp-semantic-token-classification.md b/discussion/workflow/agendas/AGD-0050-pbs-lsp-semantic-token-classification.md deleted file mode 100644 index 9dca0b3a..00000000 --- a/discussion/workflow/agendas/AGD-0050-pbs-lsp-semantic-token-classification.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0050 -ticket: pbs-lsp-semantic-tokens-semantic-classification -title: PBS LSP Semantic Token Classification -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -Choose semantic overlay categories for the first pass. diff --git a/discussion/workflow/agendas/AGD-0051-pbs-lsp-performance-cache-and-snapshots.md b/discussion/workflow/agendas/AGD-0051-pbs-lsp-performance-cache-and-snapshots.md deleted file mode 100644 index a663f47f..00000000 --- a/discussion/workflow/agendas/AGD-0051-pbs-lsp-performance-cache-and-snapshots.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0051 -ticket: pbs-lsp-performance-cache-and-snapshots -title: PBS LSP Performance Cache and Snapshots -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -Decide whether snapshot caching is a prerequisite for references/rename or can be staged later. diff --git a/discussion/workflow/agendas/AGD-0052-pbs-lsp-completion-depth.md b/discussion/workflow/agendas/AGD-0052-pbs-lsp-completion-depth.md deleted file mode 100644 index bbe5c87f..00000000 --- a/discussion/workflow/agendas/AGD-0052-pbs-lsp-completion-depth.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0052 -ticket: pbs-lsp-completion-depth -title: PBS LSP Completion Depth -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -List first-wave completion contexts beyond member access. diff --git a/discussion/workflow/agendas/AGD-0053-pbs-lsp-folding-and-selection-ranges.md b/discussion/workflow/agendas/AGD-0053-pbs-lsp-folding-and-selection-ranges.md deleted file mode 100644 index 09bbdf39..00000000 --- a/discussion/workflow/agendas/AGD-0053-pbs-lsp-folding-and-selection-ranges.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0053 -ticket: pbs-lsp-folding-and-selection-ranges -title: PBS LSP Folding and Selection Ranges -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -Decide first-wave foldable constructs and selection hierarchy. diff --git a/discussion/workflow/agendas/AGD-0054-pbs-lsp-diagnostics-ux.md b/discussion/workflow/agendas/AGD-0054-pbs-lsp-diagnostics-ux.md deleted file mode 100644 index 5326f68f..00000000 --- a/discussion/workflow/agendas/AGD-0054-pbs-lsp-diagnostics-ux.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0054 -ticket: pbs-lsp-diagnostics-ux -title: PBS LSP Diagnostics UX -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -Inventory high-frequency diagnostics and identify which need better ranges, related information, or repair hints. diff --git a/discussion/workflow/agendas/AGD-0055-pbs-lsp-document-links.md b/discussion/workflow/agendas/AGD-0055-pbs-lsp-document-links.md deleted file mode 100644 index bd450330..00000000 --- a/discussion/workflow/agendas/AGD-0055-pbs-lsp-document-links.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0055 -ticket: pbs-lsp-document-links -title: PBS LSP Document Links -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -Decide first-wave link targets and how stdlib/module URIs are represented. diff --git a/discussion/workflow/agendas/AGD-0056-pbs-lsp-call-and-type-hierarchy.md b/discussion/workflow/agendas/AGD-0056-pbs-lsp-call-and-type-hierarchy.md deleted file mode 100644 index 45c87ba8..00000000 --- a/discussion/workflow/agendas/AGD-0056-pbs-lsp-call-and-type-hierarchy.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0056 -ticket: pbs-lsp-call-and-type-hierarchy -title: PBS LSP Call and Type Hierarchy -status: abandoned -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 - -Aposentada em 2026-09-22. O tema foi consolidado em [AGD-0069](AGD-0069-pbs-lsp-remaining-editor-surface.md) / DSC-0066. Este texto não é normativo. - -## Next Step - -Decide whether to split into two discussions before moving to decisions. diff --git a/prometeu-studio/src/test/java/p/studio/debug/runtime/StudioRuntimeHandshakeServiceTest.java b/prometeu-studio/src/test/java/p/studio/debug/runtime/StudioRuntimeHandshakeServiceTest.java index 825fcdda..6cb3b3b1 100644 --- a/prometeu-studio/src/test/java/p/studio/debug/runtime/StudioRuntimeHandshakeServiceTest.java +++ b/prometeu-studio/src/test/java/p/studio/debug/runtime/StudioRuntimeHandshakeServiceTest.java @@ -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); } }