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.
This commit is contained in:
bQUARKz 2026-09-22 09:20:28 +01:00
parent e456bb8d7a
commit 1fcecdb04d
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
24 changed files with 995 additions and 333 deletions

View File

@ -1,5 +1,5 @@
{"type":"meta","next_id":{"DSC":67,"AGD":70,"DEC":59,"PLN":138,"LSN":74,"CLSN":1}} {"type":"meta","next_id":{"DSC":67,"AGD":70,"DEC":59,"PLN":138,"LSN":75,"CLSN":1}}
{"type":"discussion","id":"DSC-0066","status":"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-0066","status":"done","ticket":"pbs-lsp-remaining-editor-surface","title":"PBS LSP Remaining Editor Surface","created_at":"2026-09-22","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0074","file":"discussion/lessons/DSC-0066-pbs-lsp-remaining-editor-surface/LSN-0074-pbs-remaining-editor-surface-is-frontend-owned.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
{"type":"discussion","id":"DSC-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
{"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
{"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]} {"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}

View File

@ -0,0 +1,70 @@
---
id: LSN-0074
ticket: pbs-lsp-remaining-editor-surface
title: PBS remaining editor surface is frontend-owned and transported by the LSP
created: 2026-09-22
tags: [studio, lsp, vscode, compiler-pbs, editor]
---
# PBS remaining editor surface is frontend-owned and transported by the LSP
## Original Problem
Nine open LSP agendas still described formatting, imports, semantic tokens, snapshots, completion, folding, diagnostics, document links, and call hierarchy as if none of that surface existed. By the time those agendas were still open, the server already advertised hover, completion, signature help, definition, references, outline, workspace symbols, rename, quick fix, and full semantic tokens. Each request still rebuilt analysis. References and rename had already shipped without a snapshot cache. There was no diagnostic whose meaning was "this name is not imported."
Treating the nine agendas as nine decisions would have reimplemented finished work and locked stale premises.
## Consolidated Decision
`DEC-0058` retired that split. One plan, `PLN-0137`, landed seven steps. The frontend produces the editorial result. The LSP transports it and does not invent a second policy. A missing capability is not advertised and returns an empty list, not a protocol error.
The wave explicitly does not add a project snapshot cache, type hierarchy, `completionItem/resolve`, snippets, a ranking score, range or on-type formatting, organize imports, a missing-import quick fix, `relatedInformation`, or a new semantic-token key.
1. Published editor diagnostic `source` is the bound frontend `languageId`. For PBS that is `pbs`. `code` stays the stable compiler code. The code is not copied into `source`.
2. A document link covers only the span of a `ModuleRef` whose module assembly already resolved to a regular file. Virtual stdlib and unresolved modules produce no link.
3. Folding comes from recovered spans of the listed declarations, their bodies, parameter lists, and `Doc` text blocks. A brace, parenthesis, or text block the tree does not cover still folds, including an unmatched opener through the end of the text. Selection grows outward from identifier, to argument or parameter list, to block or `Doc` text block, to declaration.
4. Formatting is full-document only. It reprints the token stream, indents by four spaces where brace or parenthesis nesting changes, leaves `Doc` text-block interiors untouched, and does not reorder declarations or move comments off their line.
5. Semantic tokens keep `PBSSemanticTokenProvider` as the baseline. Hover resolution may replace an identifier token only when an existing `PbsSemanticKind` matches the symbol. Failure, a broken file, or a symbol with no existing key keeps the baseline token.
6. Completion inside a `ModuleRef` lists project and stdlib modules the current resolver can already name. Every other offset keeps the existing member or general completion. Items stay eager and `resolveProvider` stays false.
7. Call hierarchy walks `CallExpr` nodes for functions and methods. It does not use the references index. A cursor that is not on a callable is empty. Type hierarchy is not advertised.
## Final Implementation
`CompilerLanguageServiceBridge` reads `LspProjectContext.languageId` into `BaselineDocumentIssue.source`. Document links read the physical module map that module assembly already built; stdlib files under `/virtual/stdlib` are not regular files, so they are not links. Folding, selection, and formatting reparse the open buffer with the existing lexer and parser. They do not require a successful project analysis. The semantic-token overlay runs only when editorial context is available and otherwise returns the provider tokens, including when project discovery fails. Module completion labels are the project modules from that same physical map plus the modules `ResourceStdlibEnvironmentResolver` can already resolve. Call hierarchy collects function, method, and constructor declarations from the recovered AST and matches `CallExpr` callees by name and call shape.
Spec 23 §8.3 states each of these rules. The nine earlier agendas, `AGD-0048` through `AGD-0056`, were abandoned and are not normative.
## Examples
A project import `@app:a` that resolves to `src/a/source.pbs` is one document link on that module-reference span. `@sdk:log` is not, because the stdlib file is virtual.
```pbs
fn helper() -> int { return 1; }
fn main() -> int { return helper; }
```
The use of `helper` without a call is an identifier in the lexical pass. Hover resolves it as a function, so the overlay key is `pbs-function`. A local such as `value` has no existing key, so it stays `pbs-identifier`.
```pbs
fn helper() -> int { return 1; }
fn main() -> int { return helper(); }
```
Prepare on `main` returns that function. Outgoing calls include `helper`. Incoming calls of `helper` include `main`. A cursor on a struct name returns no item.
## Common Pitfalls
- Do not copy the diagnostic code into `source`. The editor then shows the code twice and loses the language id.
- Do not build document links to `/virtual/stdlib`. Definition already refuses that destination.
- Do not treat document symbols as folding ranges. Outline and folding are different payloads.
- Do not pretty-print from the AST. Comments and `Doc` text-block interiors are not fully modeled as trivia on the tree.
- Do not add `pbs-local`, `pbs-parameter`, or `pbs-field` in the overlay. Those symbols stay on the baseline token until a later decision adds the key.
- Do not implement call hierarchy by inverting the references index. A reference is a use of a name, not a call.
- Do not start a snapshot cache because a later feature feels slow. This wave deliberately kept request-local analysis.
## References
- `DEC-0058` and `PLN-0137`, removed by housekeeping after this lesson
- `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md` §8.3
- Earlier surface: `LSN-0068`, `LSN-0069`, `LSN-0070`, `LSN-0071`, `LSN-0072`, `LSN-0073`
- Abandoned agendas: `AGD-0048` through `AGD-0056`

View File

@ -1,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) <slug>`;
- 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.

View File

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

View File

@ -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 <path>`, 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) <slug>`.
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/`

View File

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

View File

@ -16,12 +16,15 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocum
import p.studio.compiler.models.SourceKind; import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.semantics.PbsEditorialRename; import p.studio.compiler.pbs.semantics.PbsEditorialRename;
import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol; import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol;
import p.studio.compiler.pbs.PbsCallHierarchy;
import p.studio.compiler.pbs.PbsDocumentFormatter; import p.studio.compiler.pbs.PbsDocumentFormatter;
import p.studio.compiler.pbs.PbsSemanticTokenOverlay; import p.studio.compiler.pbs.PbsSemanticTokenOverlay;
import p.studio.compiler.pbs.PbsStructureRanges; import p.studio.compiler.pbs.PbsStructureRanges;
import p.studio.compiler.pbs.semantics.PbsQuickFix; import p.studio.compiler.pbs.semantics.PbsQuickFix;
import p.studio.compiler.pbs.semantics.PbsQuickFixCollector; import p.studio.compiler.pbs.semantics.PbsQuickFixCollector;
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface; import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
import p.studio.compiler.services.FrontendCallHierarchyItem;
import p.studio.compiler.services.FrontendCallSite;
import p.studio.compiler.services.FrontendCodeAction; import p.studio.compiler.services.FrontendCodeAction;
import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendCompletionCandidate;
import p.studio.compiler.services.FrontendSymbolKind; import p.studio.compiler.services.FrontendSymbolKind;
@ -482,6 +485,77 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
offsets); offsets);
} }
@Override
public boolean callHierarchySupported() {
return true;
}
@Override
public List<FrontendCallHierarchyItem> prepareCallHierarchy(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset) {
final PbsAst.File ast = callHierarchyAst(editorialContext);
if (ast == null) {
return List.of();
}
return PbsCallHierarchy.prepare(ast, offset).stream().map(this::toCallItem).toList();
}
@Override
public List<FrontendCallSite> incomingCalls(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final FrontendCallHierarchyItem target) {
return callSites(callHierarchyAst(editorialContext), target, true);
}
@Override
public List<FrontendCallSite> outgoingCalls(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final FrontendCallHierarchyItem target) {
return callSites(callHierarchyAst(editorialContext), target, false);
}
private List<FrontendCallSite> callSites(
final PbsAst.File ast,
final FrontendCallHierarchyItem target,
final boolean incoming) {
if (ast == null || target == null) {
return List.of();
}
final PbsCallHierarchy.Callable callable = new PbsCallHierarchy.Callable(
target.name(),
target.method(),
target.startOffset(),
target.endOffset());
final var calls = incoming
? PbsCallHierarchy.incomingCalls(ast, callable)
: PbsCallHierarchy.outgoingCalls(ast, callable);
return calls.stream()
.map(call -> new FrontendCallSite(
toCallItem(incoming ? call.caller() : call.callee()),
call.startOffset(),
call.endOffset()))
.toList();
}
private static PbsAst.File callHierarchyAst(final FrontendEditorialContext editorialContext) {
if (editorialContext == null || !(editorialContext.syntaxTree() instanceof PbsAst.File ast)) {
return null;
}
return ast;
}
private FrontendCallHierarchyItem toCallItem(final PbsCallHierarchy.Callable callable) {
return new FrontendCallHierarchyItem(
callable.name(),
callable.method(),
callable.startOffset(),
callable.endOffset());
}
@Override @Override
public boolean formattingSupported() { public boolean formattingSupported() {
return true; return true;

View File

@ -0,0 +1,335 @@
package p.studio.compiler.pbs;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.Span;
import java.util.ArrayList;
import java.util.List;
public final class PbsCallHierarchy {
private PbsCallHierarchy() {
}
public record Callable(
String name,
boolean method,
int startOffset,
int endOffset) {
}
public record CallSite(
Callable caller,
Callable callee,
int startOffset,
int endOffset) {
}
public static List<Callable> prepare(final PbsAst.File ast, final int offset) {
Callable best = null;
for (final Callable callable : callables(ast)) {
if (callable.startOffset() > offset || offset >= callable.endOffset()) {
continue;
}
if (best == null || callable.endOffset() - callable.startOffset() < best.endOffset() - best.startOffset()) {
best = callable;
}
}
return best == null ? List.of() : List.of(best);
}
public static List<CallSite> incomingCalls(final PbsAst.File ast, final Callable target) {
if (target == null) {
return List.of();
}
final ArrayList<CallSite> calls = new ArrayList<>();
for (final CallSite call : calls(ast)) {
if (sameCallable(call.callee(), target)) {
calls.add(call);
}
}
return List.copyOf(calls);
}
public static List<CallSite> outgoingCalls(final PbsAst.File ast, final Callable target) {
if (target == null) {
return List.of();
}
final ArrayList<CallSite> calls = new ArrayList<>();
for (final CallSite call : calls(ast)) {
if (sameCallable(call.caller(), target)) {
calls.add(call);
}
}
return List.copyOf(calls);
}
private static boolean sameCallable(final Callable left, final Callable right) {
return left != null
&& right != null
&& left.method() == right.method()
&& left.name().equals(right.name())
&& left.startOffset() == right.startOffset()
&& left.endOffset() == right.endOffset();
}
private static List<Callable> callables(final PbsAst.File ast) {
final ArrayList<Callable> callables = new ArrayList<>();
if (ast == null || ast.topDecls() == null) {
return callables;
}
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
collectCallables(topDecl, callables);
}
return callables;
}
private static void collectCallables(final PbsAst.TopDecl topDecl, final List<Callable> callables) {
switch (topDecl) {
case null -> {
}
case PbsAst.FunctionDecl functionDecl -> callables.add(callable(functionDecl.name(), false, functionDecl.span()));
case PbsAst.StructDecl structDecl -> {
if (structDecl.methods() != null) {
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
if (method != null) {
callables.add(callable(method.name(), true, method.span()));
}
}
}
if (structDecl.ctors() != null) {
for (final PbsAst.CtorDecl ctor : structDecl.ctors()) {
if (ctor != null) {
callables.add(callable(ctor.name(), true, ctor.span()));
}
}
}
}
case PbsAst.ServiceDecl serviceDecl -> {
if (serviceDecl.methods() != null) {
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
if (method != null) {
callables.add(callable(method.name(), true, method.span()));
}
}
}
}
default -> {
}
}
}
private static List<CallSite> calls(final PbsAst.File ast) {
final List<Callable> callables = callables(ast);
final ArrayList<CallSite> calls = new ArrayList<>();
for (final Callable caller : callables) {
final PbsAst.Block body = body(ast, caller);
if (body == null) {
continue;
}
final ArrayList<PbsAst.CallExpr> found = new ArrayList<>();
collectCalls(body, found);
for (final PbsAst.CallExpr call : found) {
final Callable callee = resolveCallee(call, callables);
final int[] span = offsets(call.span());
if (callee == null || span == null) {
continue;
}
calls.add(new CallSite(caller, callee, span[0], span[1]));
}
}
return calls;
}
private static Callable resolveCallee(final PbsAst.CallExpr call, final List<Callable> callables) {
if (call == null || call.callee() == null) {
return null;
}
final boolean method = call.callee() instanceof PbsAst.MemberExpr;
final String name = switch (call.callee()) {
case PbsAst.IdentifierExpr identifier -> identifier.name();
case PbsAst.MemberExpr member -> member.memberName();
default -> null;
};
if (name == null) {
return null;
}
for (final Callable callable : callables) {
if (callable.method() == method && callable.name().equals(name)) {
return callable;
}
}
return null;
}
private static PbsAst.Block body(final PbsAst.File ast, final Callable caller) {
for (final Callable callable : callables(ast)) {
if (!sameCallable(callable, caller)) {
continue;
}
}
return findBody(ast, caller);
}
private static PbsAst.Block findBody(final PbsAst.File ast, final Callable caller) {
if (ast.topDecls() == null) {
return null;
}
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
final PbsAst.Block body = findBody(topDecl, caller);
if (body != null) {
return body;
}
}
return null;
}
private static PbsAst.Block findBody(final PbsAst.TopDecl topDecl, final Callable caller) {
switch (topDecl) {
case null -> {
return null;
}
case PbsAst.FunctionDecl functionDecl -> {
if (matches(functionDecl.name(), false, functionDecl.span(), caller)) {
return functionDecl.body();
}
return null;
}
case PbsAst.StructDecl structDecl -> {
if (structDecl.methods() != null) {
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
if (method != null && matches(method.name(), true, method.span(), caller)) {
return method.body();
}
}
}
if (structDecl.ctors() != null) {
for (final PbsAst.CtorDecl ctor : structDecl.ctors()) {
if (ctor != null && matches(ctor.name(), true, ctor.span(), caller)) {
return ctor.body();
}
}
}
return null;
}
case PbsAst.ServiceDecl serviceDecl -> {
if (serviceDecl.methods() == null) {
return null;
}
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
if (method != null && matches(method.name(), true, method.span(), caller)) {
return method.body();
}
}
return null;
}
default -> {
return null;
}
}
}
private static void collectCalls(final PbsAst.Block block, final List<PbsAst.CallExpr> calls) {
if (block == null || block.statements() == null) {
return;
}
for (final PbsAst.Statement statement : block.statements()) {
collectCalls(statement, calls);
}
collectCalls(block.tailExpression(), calls);
}
private static void collectCalls(final PbsAst.Statement statement, final List<PbsAst.CallExpr> calls) {
switch (statement) {
case null -> {
}
case PbsAst.LetStatement letStatement -> collectCalls(letStatement.initializer(), calls);
case PbsAst.ReturnStatement returnStatement -> collectCalls(returnStatement.value(), calls);
case PbsAst.AssignStatement assignStatement -> collectCalls(assignStatement.value(), calls);
case PbsAst.ExpressionStatement expressionStatement -> collectCalls(expressionStatement.expression(), calls);
case PbsAst.IfStatement ifStatement -> {
collectCalls(ifStatement.condition(), calls);
collectCalls(ifStatement.thenBlock(), calls);
collectCalls(ifStatement.elseBlock(), calls);
collectCalls(ifStatement.elseIf(), calls);
}
case PbsAst.ForStatement forStatement -> {
collectCalls(forStatement.fromExpression(), calls);
collectCalls(forStatement.untilExpression(), calls);
collectCalls(forStatement.stepExpression(), calls);
collectCalls(forStatement.body(), calls);
}
case PbsAst.WhileStatement whileStatement -> {
collectCalls(whileStatement.condition(), calls);
collectCalls(whileStatement.body(), calls);
}
default -> {
}
}
}
private static void collectCalls(final PbsAst.Expression expression, final List<PbsAst.CallExpr> calls) {
switch (expression) {
case null -> {
}
case PbsAst.CallExpr callExpr -> {
calls.add(callExpr);
collectCalls(callExpr.callee(), calls);
if (callExpr.arguments() != null) {
for (final PbsAst.Expression argument : callExpr.arguments()) {
collectCalls(argument, calls);
}
}
}
case PbsAst.MemberExpr memberExpr -> collectCalls(memberExpr.receiver(), calls);
case PbsAst.BinaryExpr binaryExpr -> {
collectCalls(binaryExpr.left(), calls);
collectCalls(binaryExpr.right(), calls);
}
case PbsAst.UnaryExpr unaryExpr -> collectCalls(unaryExpr.expression(), calls);
case PbsAst.GroupExpr groupExpr -> collectCalls(groupExpr.expression(), calls);
case PbsAst.IfExpr ifExpr -> {
collectCalls(ifExpr.condition(), calls);
collectCalls(ifExpr.thenBlock(), calls);
collectCalls(ifExpr.elseExpression(), calls);
}
case PbsAst.BlockExpr blockExpr -> collectCalls(blockExpr.block(), calls);
case PbsAst.ApplyExpr applyExpr -> {
collectCalls(applyExpr.callee(), calls);
collectCalls(applyExpr.argument(), calls);
}
default -> {
}
}
}
private static boolean matches(
final String name,
final boolean method,
final Span span,
final Callable caller) {
final Callable callable = callable(name, method, span);
return callable != null && sameCallable(callable, caller);
}
private static Callable callable(final String name, final boolean method, final Span span) {
final int[] offsets = offsets(span);
if (name == null || name.isBlank() || offsets == null) {
return null;
}
return new Callable(name, method, offsets[0], offsets[1]);
}
private static int[] offsets(final Span span) {
if (span == null || span.getEnd() <= span.getStart()) {
return null;
}
return new int[] {toOffset(span.getStart()), toOffset(span.getEnd())};
}
private static int toOffset(final long value) {
if (value <= 0L) {
return 0;
}
return (int) Math.min(Integer.MAX_VALUE, value);
}
}

View File

@ -0,0 +1,73 @@
package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsCallHierarchyTest {
private static final String SOURCE = """
fn helper() -> int {
return 1;
}
declare struct Box() {
fn blend(dx: int) -> int {
return dx;
}
}
fn main(box: Box) -> int {
box.blend(1);
return helper();
}
""";
@Test
void functionAndMethodCallsComeFromTheAst() {
final PbsAst.File ast = parse(SOURCE);
final int mainOffset = byteOffset(SOURCE, "fn main");
final var prepared = PbsCallHierarchy.prepare(ast, mainOffset);
assertEquals(1, prepared.size());
assertEquals("main", prepared.getFirst().name());
assertEquals(false, prepared.getFirst().method());
final var outgoing = PbsCallHierarchy.outgoingCalls(ast, prepared.getFirst());
assertTrue(outgoing.stream().anyMatch(call -> call.callee().name().equals("helper") && !call.callee().method()));
assertTrue(outgoing.stream().anyMatch(call -> call.callee().name().equals("blend") && call.callee().method()));
final int helperOffset = byteOffset(SOURCE, "fn helper");
final var helper = PbsCallHierarchy.prepare(ast, helperOffset).getFirst();
final var incoming = PbsCallHierarchy.incomingCalls(ast, helper);
assertEquals(1, incoming.size());
assertEquals("main", incoming.getFirst().caller().name());
}
@Test
void aStructNameIsNotACallable() {
final PbsAst.File ast = parse(SOURCE);
assertTrue(PbsCallHierarchy.prepare(ast, byteOffset(SOURCE, "struct Box")).isEmpty());
}
private static PbsAst.File parse(final String source) {
return PbsParser.parse(
PbsLexer.lex(source, FileId.none(), DiagnosticSink.empty()),
FileId.none(),
DiagnosticSink.empty());
}
private static int byteOffset(final String source, final String marker) {
final int index = source.indexOf(marker);
if (index < 0) {
throw new IllegalArgumentException(marker);
}
return source.substring(0, index + 1).getBytes(StandardCharsets.UTF_8).length;
}
}

View File

@ -0,0 +1,20 @@
package p.studio.compiler.services;
import java.util.Objects;
public record FrontendCallHierarchyItem(
String name,
boolean method,
int startOffset,
int endOffset) {
public FrontendCallHierarchyItem {
name = Objects.requireNonNull(name, "name").trim();
if (name.isEmpty()) {
throw new IllegalArgumentException("name must not be blank");
}
if (startOffset < 0 || endOffset < startOffset) {
throw new IllegalArgumentException("call hierarchy range is invalid");
}
}
}

View File

@ -0,0 +1,16 @@
package p.studio.compiler.services;
import java.util.Objects;
public record FrontendCallSite(
FrontendCallHierarchyItem item,
int startOffset,
int endOffset) {
public FrontendCallSite {
item = Objects.requireNonNull(item, "item");
if (startOffset < 0 || endOffset < startOffset) {
throw new IllegalArgumentException("call site range is invalid");
}
}
}

View File

@ -190,6 +190,31 @@ public interface FrontendLanguageService {
return selectionRanges(request, offsets); return selectionRanges(request, offsets);
} }
default boolean callHierarchySupported() {
return false;
}
default java.util.List<FrontendCallHierarchyItem> prepareCallHierarchy(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final int offset) {
return java.util.List.of();
}
default java.util.List<FrontendCallSite> incomingCalls(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final FrontendCallHierarchyItem target) {
return java.util.List.of();
}
default java.util.List<FrontendCallSite> outgoingCalls(
final FrontendDocumentRequest request,
final FrontendEditorialContext editorialContext,
final FrontendCallHierarchyItem target) {
return java.util.List.of();
}
default boolean formattingSupported() { default boolean formattingSupported() {
return false; return false;
} }

View File

@ -36,6 +36,8 @@ class FrontendLanguageServiceTest {
assertFalse(service.foldingRangesSupported()); assertFalse(service.foldingRangesSupported());
assertTrue(service.foldingRanges(request).isEmpty()); assertTrue(service.foldingRanges(request).isEmpty());
assertTrue(service.foldingRanges(request, null).isEmpty()); assertTrue(service.foldingRanges(request, null).isEmpty());
assertFalse(service.callHierarchySupported());
assertTrue(service.prepareCallHierarchy(request, null, 0).isEmpty());
assertFalse(service.formattingSupported()); assertFalse(service.formattingSupported());
assertTrue(service.formatDocument(request).isEmpty()); assertTrue(service.formatDocument(request).isEmpty());
assertFalse(service.selectionRangesSupported()); assertFalse(service.selectionRangesSupported());

View File

@ -0,0 +1,24 @@
package p.studio.lsp.messages;
import java.util.Objects;
public record BaselineCallHierarchyItem(
String name,
boolean method,
String uri,
int startLine,
int startCharacter,
int endLine,
int endCharacter) {
public BaselineCallHierarchyItem {
name = Objects.requireNonNull(name, "name").trim();
uri = Objects.requireNonNull(uri, "uri").trim();
if (name.isEmpty() || uri.isEmpty()) {
throw new IllegalArgumentException("call hierarchy item identity must not be blank");
}
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
throw new IllegalArgumentException("call hierarchy range coordinates must not be negative");
}
}
}

View File

@ -0,0 +1,18 @@
package p.studio.lsp.messages;
import java.util.Objects;
public record BaselineCallSite(
BaselineCallHierarchyItem item,
int startLine,
int startCharacter,
int endLine,
int endCharacter) {
public BaselineCallSite {
item = Objects.requireNonNull(item, "item");
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
throw new IllegalArgumentException("call site coordinates must not be negative");
}
}
}

View File

@ -20,6 +20,7 @@ public record BaselineServerDescription(
boolean foldingRangesSupported, boolean foldingRangesSupported,
boolean selectionRangesSupported, boolean selectionRangesSupported,
boolean formattingSupported, boolean formattingSupported,
boolean callHierarchySupported,
List<String> semanticTokenTypes, List<String> semanticTokenTypes,
List<BaselineSemanticHostProjection> semanticHostProjections, List<BaselineSemanticHostProjection> semanticHostProjections,
List<BaselineVisualTheme> visualThemes, List<BaselineVisualTheme> visualThemes,

View File

@ -11,6 +11,8 @@ import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
import p.studio.lsp.messages.BaselineCodeActions; import p.studio.lsp.messages.BaselineCodeActions;
import p.studio.lsp.messages.BaselineDocumentLinks; import p.studio.lsp.messages.BaselineDocumentLinks;
import p.studio.lsp.messages.BaselineFoldingRanges; 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.BaselineFormatting;
import p.studio.lsp.messages.BaselineSelectionRanges; import p.studio.lsp.messages.BaselineSelectionRanges;
import p.studio.lsp.messages.DocumentPosition; import p.studio.lsp.messages.DocumentPosition;
@ -77,6 +79,31 @@ public interface LanguageServiceBridge {
return BaselineFormatting.empty(); return BaselineFormatting.empty();
} }
default java.util.List<BaselineCallHierarchyItem> prepareCallHierarchy(
LspProjectContext context,
String documentUri,
String text,
int line,
int character) {
return java.util.List.of();
}
default java.util.List<BaselineCallSite> incomingCalls(
LspProjectContext context,
String documentUri,
String text,
BaselineCallHierarchyItem target) {
return java.util.List.of();
}
default java.util.List<BaselineCallSite> outgoingCalls(
LspProjectContext context,
String documentUri,
String text,
BaselineCallHierarchyItem target) {
return java.util.List.of();
}
default BaselineSelectionRanges selectionRanges( default BaselineSelectionRanges selectionRanges(
LspProjectContext context, LspProjectContext context,
String documentUri, String documentUri,

View File

@ -5,6 +5,8 @@ import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.exceptions.BuildException; import p.studio.compiler.exceptions.BuildException;
import p.studio.compiler.messages.*; import p.studio.compiler.messages.*;
import p.studio.compiler.models.*; import p.studio.compiler.models.*;
import p.studio.compiler.services.FrontendCallHierarchyItem;
import p.studio.compiler.services.FrontendCallSite;
import p.studio.compiler.services.FrontendCodeAction; import p.studio.compiler.services.FrontendCodeAction;
import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendCompletionCandidate;
import p.studio.compiler.services.FrontendDocumentLink; import p.studio.compiler.services.FrontendDocumentLink;
@ -73,6 +75,9 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
final boolean formattingSupported = languageService final boolean formattingSupported = languageService
.map(FrontendLanguageService::formattingSupported) .map(FrontendLanguageService::formattingSupported)
.orElse(false); .orElse(false);
final boolean callHierarchySupported = languageService
.map(FrontendLanguageService::callHierarchySupported)
.orElse(false);
return new BaselineServerDescription( return new BaselineServerDescription(
"Prometeu Studio LSP", "Prometeu Studio LSP",
"0.1.0", "0.1.0",
@ -90,6 +95,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
foldingRangesSupported, foldingRangesSupported,
selectionRangesSupported, selectionRangesSupported,
formattingSupported, formattingSupported,
callHierarchySupported,
presentation.semanticKeys(), presentation.semanticKeys(),
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
presentation.themes().stream().map(this::mapVisualTheme).toList(), presentation.themes().stream().map(this::mapVisualTheme).toList(),
@ -408,6 +414,130 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
.orElseGet(BaselineDocumentLinks::empty); .orElseGet(BaselineDocumentLinks::empty);
} }
@Override
public List<BaselineCallHierarchyItem> prepareCallHierarchy(
final LspProjectContext context,
final String documentUri,
final String text,
final int line,
final int character) {
final var languageService = callHierarchyService(context);
if (languageService == null) {
return List.of();
}
try {
return editorialDocument(context, documentUri, text)
.map(document -> {
final DocumentPositionMapper mapper = new DocumentPositionMapper(document.text());
final int offset = mapper.byteOffsetOf(line, character);
return languageService.prepareCallHierarchy(
callRequest(context, documentUri, document.text()),
document.editorialContext(),
offset)
.stream()
.map(item -> toBaselineItem(item, documentUri, mapper))
.toList();
})
.orElseGet(List::of);
} catch (final BuildException ignored) {
return List.of();
}
}
@Override
public List<BaselineCallSite> incomingCalls(
final LspProjectContext context,
final String documentUri,
final String text,
final BaselineCallHierarchyItem target) {
return directedCalls(context, documentUri, text, target, true);
}
@Override
public List<BaselineCallSite> outgoingCalls(
final LspProjectContext context,
final String documentUri,
final String text,
final BaselineCallHierarchyItem target) {
return directedCalls(context, documentUri, text, target, false);
}
private List<BaselineCallSite> directedCalls(
final LspProjectContext context,
final String documentUri,
final String text,
final BaselineCallHierarchyItem target,
final boolean incoming) {
final var languageService = callHierarchyService(context);
if (languageService == null || target == null) {
return List.of();
}
try {
return editorialDocument(context, documentUri, text)
.map(document -> {
final DocumentPositionMapper mapper = new DocumentPositionMapper(document.text());
final FrontendCallHierarchyItem frontendTarget = new FrontendCallHierarchyItem(
target.name(),
target.method(),
mapper.byteOffsetOf(target.startLine(), target.startCharacter()),
mapper.byteOffsetOf(target.endLine(), target.endCharacter()));
final List<FrontendCallSite> calls = incoming
? languageService.incomingCalls(
callRequest(context, documentUri, document.text()),
document.editorialContext(),
frontendTarget)
: languageService.outgoingCalls(
callRequest(context, documentUri, document.text()),
document.editorialContext(),
frontendTarget);
return calls.stream().map(call -> new BaselineCallSite(
toBaselineItem(call.item(), documentUri, mapper),
mapper.positionOf(call.startOffset()).line(),
mapper.positionOf(call.startOffset()).character(),
mapper.positionOf(call.endOffset()).line(),
mapper.positionOf(call.endOffset()).character())).toList();
})
.orElseGet(List::of);
} catch (final BuildException ignored) {
return List.of();
}
}
private FrontendLanguageService callHierarchyService(final LspProjectContext context) {
Objects.requireNonNull(context, "context");
final var languageService = frontendProvider(context).languageService();
if (languageService.isEmpty() || !languageService.orElseThrow().callHierarchySupported()) {
return null;
}
return languageService.orElseThrow();
}
private FrontendDocumentRequest callRequest(
final LspProjectContext context,
final String documentUri,
final String text) {
return new FrontendDocumentRequest(
context.projectRoot(),
normalizeDocumentPath(documentUri),
text == null ? "" : text);
}
private BaselineCallHierarchyItem toBaselineItem(
final FrontendCallHierarchyItem item,
final String documentUri,
final DocumentPositionMapper mapper) {
final DocumentPosition start = mapper.positionOf(item.startOffset());
final DocumentPosition end = mapper.positionOf(item.endOffset());
return new BaselineCallHierarchyItem(
item.name(),
item.method(),
documentUri,
start.line(),
start.character(),
end.line(),
end.character());
}
@Override @Override
public BaselineFormatting formatDocument( public BaselineFormatting formatDocument(
final LspProjectContext context, final LspProjectContext context,

View File

@ -8,6 +8,8 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.TextDocumentService;
import p.studio.lsp.messages.BaselineCallHierarchyItem;
import p.studio.lsp.messages.BaselineCallSite;
import p.studio.lsp.messages.BaselineCodeActionDiagnostic; import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
import p.studio.lsp.messages.DocumentPosition; import p.studio.lsp.messages.DocumentPosition;
import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.messages.LspProjectContext;
@ -169,6 +171,51 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result)); return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
} }
@Override
public CompletableFuture<List<CallHierarchyItem>> prepareCallHierarchy(final CallHierarchyPrepareParams params) {
final String uri = params.getTextDocument().getUri();
final String text = documentTextByUri.get(uri);
return CompletableFuture.completedFuture(protocolMessageMapper.mapCallHierarchyItems(
languageServiceBridge.prepareCallHierarchy(
project,
uri,
text,
params.getPosition().getLine(),
params.getPosition().getCharacter())));
}
@Override
public CompletableFuture<List<CallHierarchyIncomingCall>> callHierarchyIncomingCalls(
final CallHierarchyIncomingCallsParams params) {
return CompletableFuture.completedFuture(protocolMessageMapper.mapIncomingCalls(
directedCallSites(params.getItem(), true)));
}
@Override
public CompletableFuture<List<CallHierarchyOutgoingCall>> callHierarchyOutgoingCalls(
final CallHierarchyOutgoingCallsParams params) {
return CompletableFuture.completedFuture(protocolMessageMapper.mapOutgoingCalls(
directedCallSites(params.getItem(), false)));
}
private List<BaselineCallSite> directedCallSites(final CallHierarchyItem item, final boolean incoming) {
if (item == null || item.getUri() == null || item.getSelectionRange() == null) {
return List.of();
}
final String text = documentTextByUri.get(item.getUri());
final BaselineCallHierarchyItem target = new BaselineCallHierarchyItem(
item.getName(),
item.getKind() == SymbolKind.Method,
item.getUri(),
item.getSelectionRange().getStart().getLine(),
item.getSelectionRange().getStart().getCharacter(),
item.getSelectionRange().getEnd().getLine(),
item.getSelectionRange().getEnd().getCharacter());
return incoming
? languageServiceBridge.incomingCalls(project, item.getUri(), text, target)
: languageServiceBridge.outgoingCalls(project, item.getUri(), text, target);
}
@Override @Override
public CompletableFuture<List<? extends TextEdit>> formatting(final DocumentFormattingParams params) { public CompletableFuture<List<? extends TextEdit>> formatting(final DocumentFormattingParams params) {
final String uri = params.getTextDocument().getUri(); final String uri = params.getTextDocument().getUri();

View File

@ -67,6 +67,9 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
if (description.formattingSupported()) { if (description.formattingSupported()) {
capabilities.setDocumentFormattingProvider(true); capabilities.setDocumentFormattingProvider(true);
} }
if (description.callHierarchySupported()) {
capabilities.setCallHierarchyProvider(true);
}
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
semanticTokens.setFull(true); semanticTokens.setFull(true);
@ -190,6 +193,53 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return List.copyOf(mapped); return List.copyOf(mapped);
} }
@Override
public List<CallHierarchyItem> mapCallHierarchyItems(final List<BaselineCallHierarchyItem> items) {
if (items == null || items.isEmpty()) {
return List.of();
}
return items.stream().map(this::mapCallHierarchyItem).toList();
}
@Override
public List<CallHierarchyIncomingCall> mapIncomingCalls(final List<BaselineCallSite> calls) {
if (calls == null || calls.isEmpty()) {
return List.of();
}
return calls.stream().map(call -> new CallHierarchyIncomingCall(
mapCallHierarchyItem(call.item()),
List.of(callRange(call)))).toList();
}
@Override
public List<CallHierarchyOutgoingCall> mapOutgoingCalls(final List<BaselineCallSite> calls) {
if (calls == null || calls.isEmpty()) {
return List.of();
}
return calls.stream().map(call -> new CallHierarchyOutgoingCall(
mapCallHierarchyItem(call.item()),
List.of(callRange(call)))).toList();
}
private CallHierarchyItem mapCallHierarchyItem(final BaselineCallHierarchyItem item) {
final CallHierarchyItem mapped = new CallHierarchyItem();
mapped.setName(item.name());
mapped.setKind(item.method() ? SymbolKind.Method : SymbolKind.Function);
mapped.setUri(item.uri());
final Range range = new Range(
new Position(item.startLine(), item.startCharacter()),
new Position(item.endLine(), item.endCharacter()));
mapped.setRange(range);
mapped.setSelectionRange(range);
return mapped;
}
private Range callRange(final BaselineCallSite call) {
return new Range(
new Position(call.startLine(), call.startCharacter()),
new Position(call.endLine(), call.endCharacter()));
}
@Override @Override
public List<TextEdit> mapFormatting(final BaselineFormatting formatting) { public List<TextEdit> mapFormatting(final BaselineFormatting formatting) {
if (formatting == null || formatting.edits().isEmpty()) { if (formatting == null || formatting.edits().isEmpty()) {

View File

@ -11,6 +11,9 @@ import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DocumentLink; import org.eclipse.lsp4j.DocumentLink;
import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.CallHierarchyIncomingCall;
import org.eclipse.lsp4j.CallHierarchyItem;
import org.eclipse.lsp4j.CallHierarchyOutgoingCall;
import org.eclipse.lsp4j.FoldingRange; import org.eclipse.lsp4j.FoldingRange;
import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.SelectionRange; 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.BaselineDocumentAnalysis;
import p.studio.lsp.messages.BaselineDocumentLinks; import p.studio.lsp.messages.BaselineDocumentLinks;
import p.studio.lsp.messages.BaselineFoldingRanges; 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.BaselineFormatting;
import p.studio.lsp.messages.BaselineSelectionRanges; import p.studio.lsp.messages.BaselineSelectionRanges;
import p.studio.lsp.messages.BaselineDocumentSymbols; import p.studio.lsp.messages.BaselineDocumentSymbols;
@ -79,6 +84,18 @@ public interface ProtocolMessageMapper {
return List.of(); return List.of();
} }
default List<CallHierarchyItem> mapCallHierarchyItems(List<BaselineCallHierarchyItem> items) {
return List.of();
}
default List<CallHierarchyIncomingCall> mapIncomingCalls(List<BaselineCallSite> calls) {
return List.of();
}
default List<CallHierarchyOutgoingCall> mapOutgoingCalls(List<BaselineCallSite> calls) {
return List.of();
}
default List<SelectionRange> mapSelectionRanges(BaselineSelectionRanges ranges) { default List<SelectionRange> mapSelectionRanges(BaselineSelectionRanges ranges) {
return List.of(); return List.of();
} }

View File

@ -222,6 +222,62 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(brokenTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-punctuation"))); assertTrue(brokenTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-punctuation")));
} }
@Test
void callHierarchyUsesAstCallsAndRefusesAStruct(@TempDir final Path tempDir) throws IOException {
final Path projectRoot = tempDir.resolve("app");
final Path source = projectRoot.resolve("src").resolve("main.pbs");
Files.createDirectories(source.getParent());
Files.writeString(projectRoot.resolve("prometeu.json"), """
{
"name": "app",
"version": "1.0.0",
"language": "pbs",
"stdlib": "1",
"target": "Game",
"dependencies": []
}
""");
final String text = """
fn helper() -> int {
return 1;
}
fn main() -> int {
return helper();
}
""";
Files.writeString(source, text);
Files.writeString(source.getParent().resolve("mod.barrel"), """
pub fn helper() -> int;
pub fn main() -> int;
""");
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final LspProjectContext context = new LspProjectContext("app", "pbs", projectRoot.toRealPath());
final String uri = source.toRealPath().toUri().toString();
final DocumentPositionMapper mapper = new DocumentPositionMapper(text);
final var mainPosition = mapper.positionOf(text.indexOf("fn main") + 1);
final var prepared = bridge.prepareCallHierarchy(
context,
uri,
text,
mainPosition.line(),
mainPosition.character());
assertEquals(1, prepared.size());
assertEquals("main", prepared.getFirst().name());
final var outgoing = bridge.outgoingCalls(context, uri, text, prepared.getFirst());
assertTrue(outgoing.stream().anyMatch(call -> call.item().name().equals("helper")));
final var structPosition = mapper.positionOf(text.indexOf("fn helper") + 1);
final var helper = bridge.prepareCallHierarchy(
context,
uri,
text,
structPosition.line(),
structPosition.character());
assertEquals("helper", helper.getFirst().name());
assertTrue(bridge.incomingCalls(context, uri, text, helper.getFirst()).stream()
.anyMatch(call -> call.item().name().equals("main")));
}
@Test @Test
void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() { void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()) final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
@ -830,6 +886,7 @@ class CompilerLanguageServiceBridgeTest {
assertTrue(description.foldingRangesSupported()); assertTrue(description.foldingRangesSupported());
assertTrue(description.selectionRangesSupported()); assertTrue(description.selectionRangesSupported());
assertTrue(description.formattingSupported()); assertTrue(description.formattingSupported());
assertTrue(description.callHierarchySupported());
assertEquals(1, description.semanticHostProjections().size()); assertEquals(1, description.semanticHostProjections().size());
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()

View File

@ -109,6 +109,7 @@ class PrometeuLanguageServerTest {
false, false,
false, false,
false, false,
false,
List.of(), List.of(),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",

View File

@ -61,6 +61,7 @@ final class Lsp4jProtocolMessageMapperTest {
true, true,
true, true,
true, true,
true,
List.of("demo-keyword"), List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",
@ -94,6 +95,8 @@ final class Lsp4jProtocolMessageMapperTest {
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentFormattingProvider().getLeft()); assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentFormattingProvider().getLeft());
assertNull(result.getCapabilities().getDocumentRangeFormattingProvider()); assertNull(result.getCapabilities().getDocumentRangeFormattingProvider());
assertNull(result.getCapabilities().getDocumentOnTypeFormattingProvider()); assertNull(result.getCapabilities().getDocumentOnTypeFormattingProvider());
assertEquals(Boolean.TRUE, result.getCapabilities().getCallHierarchyProvider().getLeft());
assertNull(result.getCapabilities().getTypeHierarchyProvider());
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
@ -348,6 +351,7 @@ final class Lsp4jProtocolMessageMapperTest {
documentLinksSupported, documentLinksSupported,
documentLinksSupported, documentLinksSupported,
documentLinksSupported, documentLinksSupported,
documentLinksSupported,
List.of("demo-keyword"), List.of("demo-keyword"),
List.of(new BaselineSemanticHostProjection( List.of(new BaselineSemanticHostProjection(
"vscode", "vscode",