implements PLN-0065
This commit is contained in:
parent
e5342e34f7
commit
b0073cd7bd
@ -1,4 +1,5 @@
|
||||
{"type":"meta","next_id":{"DSC":32,"AGD":35,"DEC":32,"PLN":65,"LSN":47,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":33,"AGD":36,"DEC":33,"PLN":68,"LSN":47,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0032","status":"in_progress","ticket":"studio-new-lsp-api-and-v1-boundary","title":"Novo boundary entre lsp-api, lsp-v1 e a extensao VS Code","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","lsp","vscode","protocol","api","boundary"],"agendas":[{"id":"AGD-0035","file":"AGD-0035-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0035"}],"plans":[{"id":"PLN-0065","file":"PLN-0065-lsp-boundary-and-module-scaffolding.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0066","file":"PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0067","file":"PLN-0067-compiler-backed-dumb-lsp-server-baseline.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]}
|
||||
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||
|
||||
@ -0,0 +1,128 @@
|
||||
---
|
||||
id: AGD-0035
|
||||
ticket: studio-new-lsp-api-and-v1-boundary
|
||||
title: Novo boundary entre lsp-api, lsp-v1 e a extensao VS Code
|
||||
status: accepted
|
||||
created: 2026-05-05
|
||||
resolved: 2026-05-05
|
||||
decision: DEC-0032
|
||||
tags: [studio, lsp, vscode, protocol, api, boundary]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
O stack legado de editor, `prometeu-lsp` e `prometeu-vfs` acabou de ser removido. O próximo passo natural é reconstruir o backend de linguagem, mas sem repetir o erro anterior de misturar engine semântica, transporte LSP, sessão editorial e host UI em um único desenho.
|
||||
|
||||
Ao mesmo tempo, já existe uma extensão em `tools/vscode-extension` que hoje sabe conectar via TCP em `127.0.0.1:7777` e falar LSP como cliente real. Isso reduz incerteza no lado do FE, mas aumenta a exigência arquitetural do backend: o servidor novo precisa nascer com boundary correto.
|
||||
|
||||
O risco central é reintroduzir acoplamento acidental entre:
|
||||
|
||||
- contratos internos de linguagem;
|
||||
- transporte externo LSP;
|
||||
- implementação concreta baseada em `LSP4J`;
|
||||
- e necessidades específicas do VS Code.
|
||||
|
||||
## Context
|
||||
|
||||
- O cleanup do stack legado foi fechado por `DEC-0031`.
|
||||
- A extensão VS Code já existe em `tools/vscode-extension`.
|
||||
- A extensão usa `vscode-languageclient` e conecta por socket TCP configurável.
|
||||
- O usuário quer explicitamente que `LSP4J` **não vaze** de `lsp-v1`.
|
||||
- A intenção geral do produto é que o Studio fale a própria língua internamente e exponha adapters externos para FEs, no mesmo espírito já discutido para integrações como Tiled.
|
||||
|
||||
Hoje, a pergunta não é "como implementar o LSP inteiro", mas sim:
|
||||
|
||||
- que módulos recriar;
|
||||
- qual boundary o `lsp-api` realmente deve carregar;
|
||||
- e qual responsabilidade cabe a `lsp-v1` como adapter concreto.
|
||||
|
||||
Também já existe um direcionamento adicional do produto:
|
||||
|
||||
- `lsp-v1` deve consumir serviços existentes de `compiler`;
|
||||
- `lsp-api` deve expor funcionalidades reutilizáveis por outras partes do sistema;
|
||||
- o bootstrap do servidor deve acontecer quando um projeto é aberto, e o shutdown quando o projeto é fechado;
|
||||
- a API pode começar mínima, por exemplo com `boot server` e `shutdown server`, e crescer só quando houver necessidade real.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [x] O `lsp-api` deve expor contratos próprios de domínio/serviço ou DTOs alinhados diretamente ao protocolo LSP?
|
||||
R: o lsp-api deve expor somente servicos que serao usados por outras partes do sistema, e nao ser um espelho do protocolo.
|
||||
- [x] O `lsp-v1` deve ser somente um adapter `LSP4J` sobre serviços internos de `compiler`, ou também pode carregar parte da orquestração de sessão?
|
||||
R: O lsp-v1 deve conter o servidor e todo o necessario para falar LSP, incluindo DTOs e dependencias de LSP4J (e fazer uso de compiler quando necessario). o start e o stop do servidor podem ser chamados via lsp-api, mas a logica de orquestracao de sessao deve ficar dentro do lsp-v1.
|
||||
- [x] O lifecycle de bootstrap/shutdown do servidor por projeto pertence ao `lsp-api`, ao `lsp-v1`, ou a outro serviço do Studio?
|
||||
R: o contrato fica no lsp-api, mas a logica de orquestracao fica no lsp-v1. o lsp-api pode expor um contrato como `bootServer(project)` e `shutdownServer(project)`, mas a implementacao concreta e a logica de associar o servidor ao projeto fica no lsp-v1.
|
||||
- [x] O canal de automação (`compile/build/run/debug`) deve viver fora do novo `lsp-api` desde o início?
|
||||
R: o canal de automacao deve viver fora do lsp-api. nesse primeiro momento o foco principal deve ser o LSP e o comportamento editorial/semantico. o canal de automacao pode ser discutido e implementado separadamente, sem misturar responsabilidades.
|
||||
- [x] O `tools/vscode-extension` deve continuar falando LSP puro em socket TCP, ou o produto precisa reservar outra estratégia de transporte já nesta fase?
|
||||
R: o vscode-extension deve continuar falando LSP puro em socket TCP. o lsp-api deve ser agnóstico ao transporte, mas ainda aceito recomendacoes q sejam mais faceis ou performaticas.
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - `lsp-api` como API diretamente moldada pelo protocolo LSP
|
||||
- **Approach:** recriar `lsp-api` com tipos e serviços muito próximos da superfície do protocolo, deixando `lsp-v1` apenas como bootstrap `LSP4J` e roteamento para `compiler`.
|
||||
- **Pro:** acelera integração com o cliente existente e reduz tradução entre camadas.
|
||||
- **Con:** tende a tornar o protocolo externo dono do desenho interno; muda mal quando surgir outro FE ou outro adapter.
|
||||
- **Maintainability:** fraca. O risco de vazamento conceitual do protocolo para dentro do domínio é alto, mesmo sem importar classes `LSP4J` fora de `lsp-v1`.
|
||||
|
||||
### Option B - `lsp-api` mínima e operacional; `lsp-v1` como adapter LSP4J que consome `compiler`
|
||||
- **Approach:** `lsp-api` expõe uma surface mínima e reutilizável para o sistema, começando com operações como `boot server(project)` e `shutdown server(project)` e, no máximo, contratos estáveis que outras partes do Studio precisem enxergar. `lsp-v1` concentra a implementação concreta do servidor, depende exclusivamente de `LSP4J`, e consome os serviços do `compiler` para responder às capacidades LSP.
|
||||
- **Pro:** respeita a regra de não vazamento de `LSP4J`, evita inflar a API cedo demais e alinha o lifecycle do servidor ao projeto aberto, não ao processo global do Studio.
|
||||
- **Con:** exige disciplina para não transformar `lsp-api` em um espelho parcial do protocolo nem `lsp-v1` em um segundo backend semântico solto do `compiler`.
|
||||
- **Maintainability:** forte. O backend nasce hexagonal e o custo de evolução fica mais previsível.
|
||||
|
||||
### Option C - Sem `lsp-api`; expor apenas um servidor LSP concreto em `lsp-v1`
|
||||
- **Approach:** pular a separação e concentrar tudo em um único módulo novo de servidor, com contratos locais apenas package-private ou internos.
|
||||
- **Pro:** menor custo inicial de scaffolding.
|
||||
- **Con:** repete exatamente o tipo de colapso arquitetural que gerou o legado descartado; dificulta teste, substituição de transporte e integração com outros FEs.
|
||||
- **Maintainability:** ruim. A velocidade inicial é comprada com dívida estrutural imediata.
|
||||
|
||||
## Discussion
|
||||
|
||||
O dado novo mais importante é que o cliente VS Code já está funcional como cliente LSP puro. Isso elimina a necessidade de desenhar o protocolo "pensando no editor". O editor já sabe falar LSP; quem precisa de disciplina agora é o Studio.
|
||||
|
||||
Isso empurra a arquitetura para uma separação bem objetiva:
|
||||
|
||||
- `lsp-api` não deve ser "API pública do protocolo";
|
||||
- `lsp-api` deve ser uma API interna e mínima do backend de linguagem do Studio;
|
||||
- `lsp-v1` deve ser o adapter LSP concreto e consumidor dos serviços de `compiler`;
|
||||
- `LSP4J` deve existir exclusivamente em `lsp-v1`.
|
||||
|
||||
O ponto sensível é não confundir "API interna" com "engine concreta". Se `lsp-api` virar um lugar para pôr qualquer detalhe de sessão, thread, socket, `CompletableFuture` de transporte, ou estruturas do `LSP4J`, ele já nasce errado. Pelo direcionamento atual, a API deve começar deliberadamente estreita:
|
||||
|
||||
- boot do servidor por projeto;
|
||||
- shutdown do servidor por projeto;
|
||||
- e só depois crescer quando outro consumidor real do sistema exigir isso.
|
||||
|
||||
Também vale separar desde já o que não pertence ao LSP:
|
||||
|
||||
- `compile/build/run/debug` não devem entrar no `lsp-api` por conveniência;
|
||||
- esses fluxos pertencem a um contrato de automação paralelo;
|
||||
- o LSP deve se limitar ao que é comportamento editorial/semântico.
|
||||
|
||||
Outro ponto: o usuário já fixou que o Studio é o backend autoritativo e o cliente externo é só cliente. Isso reforça que o lifecycle do servidor precisa acompanhar o projeto aberto. Em outras palavras:
|
||||
|
||||
- abrir Studio não implica subir servidor LSP global;
|
||||
- abrir projeto pode implicar subir o servidor LSP daquele projeto;
|
||||
- fechar projeto deve encerrar o servidor correspondente.
|
||||
|
||||
As respostas atuais também fecharam um boundary operacional importante:
|
||||
|
||||
- quem "fala LSP" de verdade é o `lsp-v1`;
|
||||
- `lsp-api` não é adapter nem espelho do protocolo;
|
||||
- `lsp-api` existe para expor um boundary interno consumível pelo Studio;
|
||||
- o contrato mínimo inicial pode ser pequeno, desde que suficiente para boot/shutdown por projeto;
|
||||
- DTOs, tipos e dependências do protocolo podem existir em `lsp-v1` sem contaminar a API interna.
|
||||
|
||||
## Resolution
|
||||
|
||||
Direção recomendada para convergir:
|
||||
|
||||
1. recriar `lsp-api` como surface interna mínima e reutilizável do sistema, começando com bootstrap e shutdown por projeto;
|
||||
2. recriar `lsp-v1` como adapter LSP/JSON-RPC concreto, com dependência exclusiva de `LSP4J`;
|
||||
3. fazer `lsp-v1` consumir serviços existentes de `compiler`, em vez de reconstruir pipeline semântico próprio;
|
||||
4. deixar a lógica concreta de orquestração de sessão e associação `projeto -> servidor` dentro de `lsp-v1`, mesmo quando o bootstrap/shutdown for disparado através de `lsp-api`;
|
||||
5. tratar qualquer tipo `LSP4J` fora de `lsp-v1` como violação arquitetural;
|
||||
6. manter `tools/vscode-extension` como cliente LSP puro em socket TCP;
|
||||
7. discutir separadamente o contrato de automação para `compile/build/run/debug`, em vez de empurrá-lo para dentro do LSP.
|
||||
|
||||
Neste ponto, a agenda já tem direção clara o bastante para virar `decision`. O que ainda resta fechar depois disso é implementação, não escolha arquitetural principal.
|
||||
@ -0,0 +1,196 @@
|
||||
---
|
||||
id: DEC-0032
|
||||
ticket: studio-new-lsp-api-and-v1-boundary
|
||||
title: Boundary normativo entre lsp-api, lsp-v1 e a extensao VS Code
|
||||
status: accepted
|
||||
created: 2026-05-05
|
||||
accepted: 2026-05-05
|
||||
agenda: AGD-0035
|
||||
plans: [PLN-0065, PLN-0066, PLN-0067]
|
||||
tags: [studio, lsp, vscode, protocol, api, boundary, compiler]
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
O repositório SHALL reconstruir o stack de linguagem do Studio com separação explícita entre:
|
||||
|
||||
1. `lsp-api` como boundary interno do Studio;
|
||||
2. `lsp-v1` como adapter LSP/JSON-RPC concreto;
|
||||
3. `compiler` como owner dos serviços semânticos e de análise consumidos por esse adapter;
|
||||
4. `tools/vscode-extension` como cliente externo que continua falando LSP puro via socket TCP.
|
||||
|
||||
Esta decisão ALSO locks os seguintes pontos:
|
||||
|
||||
1. `lsp-api` MUST NOT ser espelho do protocolo LSP;
|
||||
2. `lsp-api` MUST expor apenas funcionalidades internas realmente consumíveis por outras partes do Studio;
|
||||
3. `lsp-api` MAY começar mínima, por exemplo com contratos como `bootServer(project)` e `shutdownServer(project)`;
|
||||
4. `lsp-v1` MUST concentrar o servidor LSP concreto, os DTOs de protocolo, o transporte e qualquer dependência de `LSP4J`;
|
||||
5. `LSP4J` MUST NOT vazar para fora de `lsp-v1`;
|
||||
6. `lsp-v1` MUST consumir serviços existentes de `compiler` em vez de reconstruir pipeline semântico próprio;
|
||||
7. o lifecycle do servidor LSP MUST ser associado à abertura e ao fechamento de projeto, não à abertura global do processo Studio;
|
||||
8. o atual `lsp-v1` presente hoje no repositório serve apenas como mock de conectividade VS Code ↔ Studio e MUST NOT ser tratado como padrão arquitetural, base estrutural ou referência de implementação do backend definitivo.
|
||||
9. a primeira onda de implementação desta decisão SHALL priorizar arquitetura, lifecycle e separação de responsabilidades, mesmo que o comportamento LSP permaneça deliberadamente simples ou "dumb" por um período controlado.
|
||||
|
||||
## Rationale
|
||||
|
||||
O stack legado anterior foi removido justamente porque misturava responsabilidades erradas e criou boundaries frágeis entre UI, sessão, semântica e transporte.
|
||||
|
||||
Ao mesmo tempo, o projeto já tem dois fatos importantes:
|
||||
|
||||
1. a extensão VS Code já existe e já sabe atuar como cliente LSP puro;
|
||||
2. o Studio já possui serviços de `compiler` que devem continuar sendo a fonte real de comportamento semântico.
|
||||
|
||||
Logo, o problema não é "inventar um cliente" nem "reescrever a semântica". O problema é definir corretamente:
|
||||
|
||||
1. o que é boundary interno reutilizável do Studio;
|
||||
2. o que é adapter concreto de protocolo;
|
||||
3. e onde termina a influência do protocolo externo.
|
||||
|
||||
Essa separação reduz acoplamento, melhora teste e evita que detalhes de `LSP4J` ou do transporte contaminem o restante da base.
|
||||
|
||||
Também existe uma prioridade explícita de rollout:
|
||||
|
||||
1. primeiro consolidar a arquitetura;
|
||||
2. depois adicionar capacidade semântica por camadas;
|
||||
3. sem obrigar a primeira implementação a já ser semanticamente rica.
|
||||
|
||||
Isso evita repetir o padrão anterior de crescer funcionalidade em cima de uma base estrutural errada.
|
||||
|
||||
## Technical Specification
|
||||
|
||||
### 1. Module Roles
|
||||
|
||||
`lsp-api` MUST ser tratado como módulo de boundary interno do Studio.
|
||||
|
||||
Ele SHALL:
|
||||
|
||||
1. expor somente contratos que façam sentido para outros consumidores internos do sistema;
|
||||
2. permanecer agnóstico ao protocolo LSP concreto;
|
||||
3. permanecer agnóstico a `LSP4J`;
|
||||
4. permanecer agnóstico ao transporte TCP ou a qualquer detalhe de socket;
|
||||
5. começar com surface mínima suficiente para o lifecycle do servidor por projeto.
|
||||
|
||||
`lsp-v1` MUST ser tratado como o primeiro adapter concreto do boundary acima.
|
||||
|
||||
Ele SHALL:
|
||||
|
||||
1. implementar o servidor LSP;
|
||||
2. carregar os DTOs e tipos de protocolo que forem necessários para falar LSP;
|
||||
3. carregar a dependência de `LSP4J`;
|
||||
4. realizar a tradução entre o mundo do protocolo e os serviços internos consumidos;
|
||||
5. concentrar a lógica concreta de sessão e associação `projeto -> servidor`.
|
||||
|
||||
### 2. LSP4J Containment
|
||||
|
||||
O repositório MUST tratar `LSP4J` como dependência exclusiva de `lsp-v1`.
|
||||
|
||||
Portanto:
|
||||
|
||||
1. nenhum tipo de `LSP4J` MAY aparecer em `lsp-api`;
|
||||
2. nenhum tipo de `LSP4J` MAY aparecer em outros módulos do Studio;
|
||||
3. nenhum contrato de `lsp-api` MAY depender semanticamente de tipos, nomes ou formas obrigadas pelo `LSP4J`;
|
||||
4. qualquer vazamento de `LSP4J` para fora de `lsp-v1` SHALL ser tratado como violação arquitetural.
|
||||
|
||||
### 3. Compiler Ownership
|
||||
|
||||
`lsp-v1` MUST consumir serviços existentes de `compiler` quando precisar responder capacidades editoriais e semânticas.
|
||||
|
||||
Isso implica:
|
||||
|
||||
1. `lsp-v1` MUST NOT reconstruir uma engine semântica paralela por conveniência;
|
||||
2. `lsp-v1` MUST preferir compor sobre entrypoints e serviços canônicos do `compiler`;
|
||||
3. qualquer lacuna percebida no `compiler` para servir o novo backend SHALL ser tratada como evolução explícita do `compiler`, e não como autorização para duplicação semântica dentro de `lsp-v1`.
|
||||
|
||||
### 4. Server Lifecycle
|
||||
|
||||
O lifecycle do servidor MUST ser project-scoped.
|
||||
|
||||
Regras:
|
||||
|
||||
1. abrir o processo Studio MUST NOT implicitamente significar "subir um servidor LSP global";
|
||||
2. abrir um projeto MAY disparar `bootServer(project)` através do boundary exposto por `lsp-api`;
|
||||
3. fechar um projeto MUST disparar `shutdownServer(project)` para o servidor correspondente;
|
||||
4. a implementação concreta desse bootstrap/shutdown e o vínculo entre projeto e instância do servidor MUST residir em `lsp-v1`.
|
||||
|
||||
### 5. Internal API Minimalism
|
||||
|
||||
`lsp-api` MUST nascer pequeno.
|
||||
|
||||
Ele SHALL:
|
||||
|
||||
1. expor apenas o que outro consumidor interno realmente precise enxergar;
|
||||
2. evitar DTOs de protocolo sem necessidade;
|
||||
3. evitar antecipar capacidades futuras sem consumidor real;
|
||||
4. crescer somente quando houver pressão concreta de uso interno.
|
||||
|
||||
O contrato inicial mínimo aceito por esta decisão é:
|
||||
|
||||
1. `bootServer(project)`
|
||||
2. `shutdownServer(project)`
|
||||
|
||||
Contratos adicionais MAY ser adicionados depois, mas não fazem parte do lock inicial desta decisão.
|
||||
|
||||
### 6. VS Code Transport
|
||||
|
||||
`tools/vscode-extension` SHALL permanecer cliente LSP puro via socket TCP nesta fase.
|
||||
|
||||
Isso implica:
|
||||
|
||||
1. a extensão atual pode continuar evoluindo nesse modelo;
|
||||
2. `lsp-api` MUST permanecer agnóstico a esse transporte;
|
||||
3. mudanças futuras de transporte MAY ser discutidas depois, mas não fazem parte do boundary normativo inicial.
|
||||
|
||||
### 7. Current Mock Status
|
||||
|
||||
O `lsp-v1` que existe hoje para testes de conectividade entre VS Code e Studio MUST ser tratado como mock transitório.
|
||||
|
||||
Ele MUST NOT:
|
||||
|
||||
1. servir de blueprint estrutural do novo servidor;
|
||||
2. servir de contrato implícito do sistema;
|
||||
3. servir de justificativa para manter responsabilidades colapsadas;
|
||||
4. contaminar o padrão de código alvo do backend definitivo.
|
||||
|
||||
O backend definitivo SHALL priorizar:
|
||||
|
||||
1. separação clara de responsabilidades;
|
||||
2. boundaries pequenos e explícitos;
|
||||
3. código fácil de manter;
|
||||
4. composição sobre serviços canônicos do `compiler`;
|
||||
5. isolamento rígido do adapter de protocolo.
|
||||
|
||||
### 8. Implementation Wave 1
|
||||
|
||||
A primeira onda de implementação derivada desta decisão MUST focar em transformar o mock atual em uma estrutura sólida de trabalho.
|
||||
|
||||
Essa onda SHALL incluir:
|
||||
|
||||
1. `lsp-api` mínimo com bootstrap e shutdown por projeto;
|
||||
2. `lsp-v1` organizado como adapter concreto limpo, mesmo que ainda responda um conjunto reduzido ou simplificado de capacidades;
|
||||
3. boot do servidor quando o projeto abrir;
|
||||
4. shutdown do servidor quando o projeto fechar;
|
||||
5. isolamento explícito entre boundary interno, adapter de protocolo e consumo de `compiler`.
|
||||
|
||||
Essa onda MUST NOT ser bloqueada pela ausência de "carne semântica" completa.
|
||||
|
||||
Em outras palavras:
|
||||
|
||||
1. é aceitável manter um comportamento LSP inicial deliberadamente simples;
|
||||
2. não é aceitável manter um padrão arquitetural improvisado só porque o comportamento ainda é simples;
|
||||
3. a evolução semântica futura SHALL acontecer por camadas sobre a estrutura consolidada.
|
||||
|
||||
## Constraints
|
||||
|
||||
1. Esta decisão MUST preservar a separação entre boundary interno e adapter externo.
|
||||
2. Esta decisão MUST preservar `compiler` como owner semântico do sistema.
|
||||
3. Esta decisão MUST impedir vazamento de `LSP4J` para fora de `lsp-v1`.
|
||||
4. Esta decisão MUST impedir que o mock atual de conectividade seja usado como padrão do produto.
|
||||
5. Esta decisão MUST manter `compile/build/run/debug` fora do escopo do `lsp-api` nesta fase.
|
||||
6. Qualquer plano derivado desta decisão MUST refletir explicitamente o lifecycle por projeto e a contenção de protocolo em `lsp-v1`.
|
||||
7. O primeiro plano derivado desta decisão MUST priorizar a consolidação estrutural do servidor antes do enriquecimento semântico.
|
||||
|
||||
## Revision Log
|
||||
|
||||
- 2026-05-05: Initial draft from AGD-0035.
|
||||
- 2026-05-05: Added explicit phase-1 rollout guidance to solidify architecture before semantic depth.
|
||||
- 2026-05-05: Accepted and decomposed into PLN-0065, PLN-0066, and PLN-0067.
|
||||
@ -0,0 +1,118 @@
|
||||
---
|
||||
id: PLN-0065
|
||||
ticket: studio-new-lsp-api-and-v1-boundary
|
||||
title: LSP Boundary and Module Scaffolding
|
||||
status: done
|
||||
created: 2026-05-05
|
||||
completed: 2026-05-05
|
||||
tags: [studio, lsp, api, boundary, modules, architecture]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Recreate `prometeu-lsp:prometeu-lsp-api` and `prometeu-lsp:prometeu-lsp-v1` with a structurally correct boundary that is ready for long-term maintenance, while intentionally keeping the initial server behavior minimal.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0032` locks that:
|
||||
|
||||
- `lsp-api` is an internal Studio boundary, not a mirror of the LSP protocol;
|
||||
- `lsp-v1` is the concrete LSP adapter and the only module allowed to depend on `LSP4J`;
|
||||
- the current mock server shape MUST NOT be treated as the product pattern;
|
||||
- wave 1 prioritizes architecture and lifecycle over semantic depth.
|
||||
|
||||
The repository already reserves `prometeu-lsp:*` in `settings.gradle.kts`, but the modules are currently empty shells after the cleanup.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Recreate Gradle/module structure for `prometeu-lsp-api` and `prometeu-lsp-v1`.
|
||||
- Define the minimal internal API contract for server lifecycle by project.
|
||||
- Establish package boundaries that keep protocol DTOs and `LSP4J` types out of `lsp-api`.
|
||||
- Add boundary-oriented tests or build checks where feasible.
|
||||
|
||||
### Excluded
|
||||
- Wiring the server into Studio project open/close.
|
||||
- Rich semantic behavior, diagnostics, definition, symbols, or incremental analysis.
|
||||
- Automation channel work for `compile/build/run/debug`.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Full language feature implementation.
|
||||
- Any attempt to preserve compatibility with the legacy removed `prometeu-lsp`.
|
||||
- Transport changes in the VS Code extension.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Recreate the Gradle modules with explicit dependency ownership
|
||||
|
||||
**What:** Reintroduce the `prometeu-lsp-api` and `prometeu-lsp-v1` modules as clean boundaries.
|
||||
**How:** Restore the module directories and `build.gradle.kts` files, wire them in `settings.gradle.kts`, and keep dependencies explicit:
|
||||
|
||||
- `prometeu-lsp-api` depends only on stable internal modules it truly needs;
|
||||
- `prometeu-lsp-v1` depends on `prometeu-lsp-api`, `compiler` services it consumes, and `LSP4J`;
|
||||
- no other Studio module depends on `LSP4J`.
|
||||
|
||||
**File(s):** `settings.gradle.kts`, `prometeu-lsp/prometeu-lsp-api/build.gradle.kts`, `prometeu-lsp/prometeu-lsp-v1/build.gradle.kts`, affected root/module build files.
|
||||
|
||||
### Step 2 - Define the minimal internal lifecycle contract in `lsp-api`
|
||||
|
||||
**What:** Create the smallest stable API needed by the Studio host.
|
||||
**How:** Add internal contracts such as a project-scoped `bootServer(project)` and `shutdownServer(project)` surface plus the minimal supporting DTO/entity types required by that lifecycle. Keep the API transport-agnostic and free of protocol DTOs.
|
||||
**File(s):** `prometeu-lsp/prometeu-lsp-api/src/main/java/**`.
|
||||
|
||||
### Step 3 - Define the adapter shape in `lsp-v1`
|
||||
|
||||
**What:** Establish the internal code organization of the concrete adapter.
|
||||
**How:** Create clear package slices inside `lsp-v1`, such as:
|
||||
|
||||
- lifecycle/bootstrap,
|
||||
- server host/transport,
|
||||
- protocol DTO mapping,
|
||||
- compiler-facing service bridge.
|
||||
|
||||
The implementation may stay behaviorally dumb, but the structure MUST already reflect the final responsibility split.
|
||||
**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/main/java/**`.
|
||||
|
||||
### Step 4 - Add boundary conformance tests
|
||||
|
||||
**What:** Prevent architectural drift from day 1.
|
||||
**How:** Add tests or static checks that fail if:
|
||||
|
||||
- `lsp-api` imports `LSP4J`;
|
||||
- protocol DTOs leak into `lsp-api`;
|
||||
- non-`lsp-v1` Studio modules start importing `LSP4J`.
|
||||
|
||||
**File(s):** `prometeu-lsp/prometeu-lsp-api/src/test/java/**`, `prometeu-lsp/prometeu-lsp-v1/src/test/java/**`, or build-level checks if that is cleaner.
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- Verify the lifecycle contracts in `lsp-api` have deterministic defaults and null-safety.
|
||||
- Verify `lsp-v1` module-local bootstrap pieces can be instantiated without a running Studio shell.
|
||||
- Verify boundary checks catch `LSP4J` leakage.
|
||||
|
||||
### Integration Tests
|
||||
- Run a root or targeted Gradle build proving the new modules compile and coexist with `prometeu-studio` and `prometeu-app`.
|
||||
|
||||
### Manual Verification
|
||||
- Inspect the recreated module tree and confirm `lsp-api` contains no protocol or `LSP4J` types.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `prometeu-lsp-api` and `prometeu-lsp-v1` exist again as active modules.
|
||||
- [x] `lsp-api` exposes only internal lifecycle-oriented contracts.
|
||||
- [x] `lsp-v1` is the only module allowed to depend on `LSP4J`.
|
||||
- [x] The new module structure expresses clear responsibility boundaries even with dumb behavior.
|
||||
- [x] Boundary conformance is covered by tests or equivalent build checks.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `DEC-0032` accepted and normatively locked.
|
||||
- Current Gradle graph and module includes available for reactivation.
|
||||
|
||||
## Risks
|
||||
|
||||
- Reintroducing the modules too loosely can recreate the same ambiguity that killed the legacy stack.
|
||||
- An underspecified API can force churn later; an oversized API can lock the wrong shape too early.
|
||||
- Build-only enforcement may miss architectural drift unless the checks are explicit.
|
||||
@ -0,0 +1,103 @@
|
||||
---
|
||||
id: PLN-0066
|
||||
ticket: studio-new-lsp-api-and-v1-boundary
|
||||
title: Project-Scoped LSP Server Lifecycle in Studio
|
||||
status: open
|
||||
created: 2026-05-05
|
||||
completed:
|
||||
tags: [studio, lsp, lifecycle, projects, boot, shutdown]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Wire the new LSP server lifecycle into the Studio project lifecycle so a server starts when a project opens and stops when that project closes, without introducing a process-global LSP service.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0032` requires a project-scoped server lifecycle and explicitly rejects a global Studio-wide server. The current natural integration points are:
|
||||
|
||||
- `StudioProjectSession`
|
||||
- `StudioProjectSessionFactory`
|
||||
- `StudioWindowCoordinator`
|
||||
|
||||
These surfaces already own project startup and teardown flow and therefore are the correct place to consume `lsp-api`.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Add project-scoped LSP server ownership to the Studio project session lifecycle.
|
||||
- Boot the server during project open.
|
||||
- Shutdown the server during project close.
|
||||
- Preserve existing shell, packer, play/stop, and debug flows.
|
||||
|
||||
### Excluded
|
||||
- Rich semantic responses.
|
||||
- Automation channel work.
|
||||
- VS Code extension transport changes.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Cross-project shared server process management.
|
||||
- Multi-project pooling or server reuse.
|
||||
- Changing the overall launcher/window model.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Extend project session ownership to include the LSP lifecycle boundary
|
||||
|
||||
**What:** Make `StudioProjectSession` own the new project-scoped LSP service reference.
|
||||
**How:** Add the minimal `lsp-api` contract to `StudioProjectSession`, ensure it is initialized by `StudioProjectSessionFactory`, and guarantee shutdown on `close()`, including failure-safe teardown ordering.
|
||||
**File(s):** `prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java`, `prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java`, related tests.
|
||||
|
||||
### Step 2 - Trigger boot during project open flow
|
||||
|
||||
**What:** Start the LSP server as part of project initialization.
|
||||
**How:** Update the project-open sequence so the server boots after the project context is ready and before the project window is considered fully opened. Ensure failures surface as project-open failures instead of becoming silent background errors.
|
||||
**File(s):** `prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java`, possibly `prometeu-app/src/main/java/p/studio/AppContainer.java` and `prometeu-studio/src/main/java/p/studio/Container.java` if dependency injection changes are needed.
|
||||
|
||||
### Step 3 - Trigger shutdown during project close flow
|
||||
|
||||
**What:** Cleanly stop the project-bound server when the project closes.
|
||||
**How:** Ensure the existing project close path tears down the LSP service through the session close path, with no leaked socket listener or hanging background resources.
|
||||
**File(s):** `prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java`, `prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java`.
|
||||
|
||||
### Step 4 - Preserve current dumb connectivity semantics
|
||||
|
||||
**What:** Keep the initial server behavior intentionally simple while validating lifecycle.
|
||||
**How:** Wire the server so the VS Code extension can still connect over TCP, even if the server behavior remains a minimal handshake or stubbed capability set for now.
|
||||
**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/main/java/**`, `tools/vscode-extension/src/extension.ts` only if configuration or expectations need adjustment.
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- Verify project session close calls LSP shutdown exactly once.
|
||||
- Verify boot failures are surfaced deterministically during project open.
|
||||
- Verify repeated close remains idempotent.
|
||||
|
||||
### Integration Tests
|
||||
- Run `prometeu-studio` tests covering project open/close lifecycle.
|
||||
- Add a focused integration test proving a project-scoped server is booted and then shut down.
|
||||
|
||||
### Manual Verification
|
||||
- Open a Studio project, confirm the server starts.
|
||||
- Close the project, confirm the server port is released and the process/resources terminate.
|
||||
- Connect via the existing VS Code extension and confirm the dumb server remains reachable.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Opening a Studio project boots an LSP server for that project.
|
||||
- [ ] Closing the project shuts down that server.
|
||||
- [ ] The lifecycle is owned by project session boundaries, not global app startup.
|
||||
- [ ] Existing non-LSP project flows remain operational.
|
||||
- [ ] The current dumb connectivity workflow remains usable as a temporary implementation phase.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `DEC-0032` accepted and normatively locked.
|
||||
- `PLN-0065` providing the new module and API boundary.
|
||||
|
||||
## Risks
|
||||
|
||||
- Startup ordering mistakes can make project open flaky or hide boot errors.
|
||||
- Shutdown ordering mistakes can leak ports or threads.
|
||||
- Lifecycle wiring can accidentally reintroduce global process ownership if the session boundary is not kept strict.
|
||||
@ -0,0 +1,100 @@
|
||||
---
|
||||
id: PLN-0067
|
||||
ticket: studio-new-lsp-api-and-v1-boundary
|
||||
title: Compiler-Backed Dumb LSP Server Baseline
|
||||
status: open
|
||||
created: 2026-05-05
|
||||
completed:
|
||||
tags: [studio, lsp, compiler, baseline, protocol, vscode]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Establish a maintainable, compiler-backed baseline inside `lsp-v1` that keeps the server behavior deliberately simple while creating the seams required for later semantic layering.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0032` explicitly allows wave 1 to remain semantically thin, but it forbids keeping an ad hoc mock structure as the long-term coding pattern. The backend therefore needs a baseline that is:
|
||||
|
||||
- dumb in behavior when necessary,
|
||||
- but already correctly composed over `compiler` services,
|
||||
- and already structured for layered enrichment later.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Create compiler-facing service seams used by `lsp-v1`.
|
||||
- Provide a small, explicit set of LSP capabilities or responses that prove the adapter shape.
|
||||
- Keep the VS Code extension compatible with the current TCP model.
|
||||
- Document the temporary behavioral limits in code and tests.
|
||||
|
||||
### Excluded
|
||||
- Full diagnostics, definition, symbols, inlay hints, or multi-file semantic completeness.
|
||||
- Automation channel work.
|
||||
- Non-TCP transport experiments.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Matching the full final language feature set.
|
||||
- Recreating the deleted legacy `prometeu-lsp` behavior.
|
||||
- Optimizing for performance before the baseline structure is validated.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Define compiler-facing bridge services inside `lsp-v1`
|
||||
|
||||
**What:** Introduce explicit bridge classes or adapters between protocol handlers and `compiler`.
|
||||
**How:** Create narrow services inside `lsp-v1` that own calls into `compiler` entrypoints. Even if the first concrete responses are stubbed or partial, the protocol handlers must already call through these seams instead of embedding compiler knowledge everywhere.
|
||||
**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/main/java/**`, relevant `prometeu-compiler` integration points when discoverable.
|
||||
|
||||
### Step 2 - Implement a minimal but structured capability slice
|
||||
|
||||
**What:** Keep the server intentionally dumb, but no longer structurally improvised.
|
||||
**How:** Choose a small capability surface such as `initialize`, `shutdown`, and one lightweight language feature or health response. The exact feature set may stay narrow, but the code must already follow the final layering:
|
||||
|
||||
- protocol request handling,
|
||||
- bridge service invocation,
|
||||
- result mapping.
|
||||
|
||||
**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/main/java/**`, potentially `tools/vscode-extension/src/extension.ts` if initialization expectations need tightening.
|
||||
|
||||
### Step 3 - Codify temporary behavior limits
|
||||
|
||||
**What:** Make the current limitations explicit instead of implicit.
|
||||
**How:** Add tests and lightweight documentation/comments that define the intentionally supported baseline behavior so future work can add capability in layers without guessing what the "mock" currently means.
|
||||
**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/test/java/**`, targeted source comments where they reduce ambiguity.
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- Verify protocol handlers call the correct bridge services.
|
||||
- Verify compiler bridge services can be instantiated and return stable baseline results.
|
||||
- Verify no handler bypasses the bridge layer to embed semantic logic ad hoc.
|
||||
|
||||
### Integration Tests
|
||||
- Run a targeted Studio/LSP integration proving the VS Code client can initialize against the dumb server.
|
||||
- Run compiler-related tests needed to validate the consumed service seams.
|
||||
|
||||
### Manual Verification
|
||||
- Connect from VS Code and confirm the server initializes and remains stable.
|
||||
- Confirm the limited capability set behaves predictably and does not masquerade as richer support than actually implemented.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `lsp-v1` talks to `compiler` through explicit bridge services.
|
||||
- [ ] The baseline server behavior remains intentionally simple but structurally clean.
|
||||
- [ ] At least one minimal end-to-end capability slice proves the final layering.
|
||||
- [ ] The extension can still connect over TCP without protocol regressions.
|
||||
- [ ] Temporary semantic limits are explicit in tests and code structure.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `DEC-0032` accepted and normatively locked.
|
||||
- `PLN-0065` for module/boundary setup.
|
||||
- `PLN-0066` for project-scoped lifecycle integration.
|
||||
|
||||
## Risks
|
||||
|
||||
- A "temporary" dumb server can calcify unless the seams are truly explicit and tested.
|
||||
- Over-stubbing can hide compiler integration issues that should be surfaced early.
|
||||
- Picking too many baseline features in this wave can dilute the architectural focus.
|
||||
@ -6,6 +6,7 @@ jacoco = "0.8.12"
|
||||
lombok = "1.18.32"
|
||||
junit-jupiter = "5.12.1"
|
||||
slf4j = "2.0.7"
|
||||
lsp4j = "1.0.0"
|
||||
|
||||
[libraries]
|
||||
javafx-controls = { group = "org.openjfx", name = "javafx-controls", version.ref = "javafx" }
|
||||
@ -20,6 +21,7 @@ lombok = { group = "org.projectlombok", name = "lombok", version.ref = "lombok"
|
||||
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" }
|
||||
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
|
||||
lsp4j = { group = "org.eclipse.lsp4j", name = "org.eclipse.lsp4j", version.ref = "lsp4j" }
|
||||
|
||||
[plugins]
|
||||
javafx = { id = "org.openjfx.javafxplugin", version = "0.1.0" }
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
plugins {
|
||||
id("gradle.java-library-conventions")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package p.studio.lsp.api;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record LspProjectContext(
|
||||
String projectKey,
|
||||
String languageId,
|
||||
Path projectRoot) {
|
||||
|
||||
public LspProjectContext {
|
||||
projectKey = requireText(projectKey, "projectKey");
|
||||
languageId = requireText(languageId, "languageId");
|
||||
projectRoot = Objects.requireNonNull(projectRoot, "projectRoot").toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
final String value,
|
||||
final String field) {
|
||||
final String candidate = Objects.requireNonNull(value, field).trim();
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.lsp.api;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record LspServerBootRequest(
|
||||
LspProjectContext project,
|
||||
LspServerConfiguration configuration) {
|
||||
|
||||
public LspServerBootRequest {
|
||||
project = Objects.requireNonNull(project, "project");
|
||||
configuration = Objects.requireNonNull(configuration, "configuration");
|
||||
}
|
||||
|
||||
public static LspServerBootRequest defaults(final LspProjectContext project) {
|
||||
return new LspServerBootRequest(project, LspServerConfiguration.defaults());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package p.studio.lsp.api;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record LspServerConfiguration(
|
||||
String host,
|
||||
int port) {
|
||||
public static final String DEFAULT_HOST = "127.0.0.1";
|
||||
public static final int DEFAULT_PORT = 7777;
|
||||
|
||||
public LspServerConfiguration {
|
||||
host = requireHost(host);
|
||||
if (port < 0 || port > 65535) {
|
||||
throw new IllegalArgumentException("port must be between 0 and 65535");
|
||||
}
|
||||
}
|
||||
|
||||
public static LspServerConfiguration defaults() {
|
||||
return new LspServerConfiguration(DEFAULT_HOST, DEFAULT_PORT);
|
||||
}
|
||||
|
||||
private static String requireHost(final String value) {
|
||||
final String candidate = Objects.requireNonNull(value, "host").trim();
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException("host must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package p.studio.lsp.api;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record LspServerEndpoint(
|
||||
String host,
|
||||
int port) {
|
||||
|
||||
public LspServerEndpoint {
|
||||
final String candidate = Objects.requireNonNull(host, "host").trim();
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException("host must not be blank");
|
||||
}
|
||||
if (port < 0 || port > 65535) {
|
||||
throw new IllegalArgumentException("port must be between 0 and 65535");
|
||||
}
|
||||
host = candidate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.api;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record LspServerHandle(
|
||||
LspProjectContext project,
|
||||
LspServerEndpoint endpoint) {
|
||||
|
||||
public LspServerHandle {
|
||||
project = Objects.requireNonNull(project, "project");
|
||||
endpoint = Objects.requireNonNull(endpoint, "endpoint");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package p.studio.lsp.api;
|
||||
|
||||
public interface LspServerLifecycle {
|
||||
LspServerHandle bootServer(LspServerBootRequest request);
|
||||
|
||||
void shutdownServer(LspProjectContext project);
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package p.studio.lsp.api;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LspApiBoundaryTest {
|
||||
|
||||
@Test
|
||||
void apiSourceMustRemainFreeOfLsp4jAndProtocolPackages() throws IOException {
|
||||
final Path sourceRoot = Path.of("src/main/java");
|
||||
final List<Path> javaFiles;
|
||||
try (var stream = Files.walk(sourceRoot)) {
|
||||
javaFiles = stream
|
||||
.filter(path -> path.toString().endsWith(".java"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
for (final Path javaFile : javaFiles) {
|
||||
final String source = Files.readString(javaFile);
|
||||
assertTrue(!source.contains("org.eclipse.lsp4j"),
|
||||
() -> "LSP4J leaked into lsp-api: " + javaFile);
|
||||
assertTrue(!source.contains(".protocol."),
|
||||
() -> "Protocol package leaked into lsp-api: " + javaFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package p.studio.lsp.api;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class LspServerBootRequestTest {
|
||||
|
||||
@Test
|
||||
void defaultsUseDeterministicLoopbackConfiguration() {
|
||||
final LspProjectContext project = new LspProjectContext("demo", "pbs", Path.of("."));
|
||||
|
||||
final LspServerBootRequest request = LspServerBootRequest.defaults(project);
|
||||
|
||||
assertEquals(project, request.project());
|
||||
assertEquals(LspServerConfiguration.DEFAULT_HOST, request.configuration().host());
|
||||
assertEquals(LspServerConfiguration.DEFAULT_PORT, request.configuration().port());
|
||||
}
|
||||
|
||||
@Test
|
||||
void configurationRejectsInvalidValues() {
|
||||
assertThrows(NullPointerException.class, () -> new LspServerConfiguration(null, 7777));
|
||||
assertThrows(IllegalArgumentException.class, () -> new LspServerConfiguration(" ", 7777));
|
||||
assertThrows(IllegalArgumentException.class, () -> new LspServerConfiguration("127.0.0.1", -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void projectContextNormalizesAndRejectsBlankFields() {
|
||||
final LspProjectContext context = new LspProjectContext(" demo ", " pbs ", Path.of("."));
|
||||
|
||||
assertEquals("demo", context.projectKey());
|
||||
assertEquals("pbs", context.languageId());
|
||||
assertEquals(Path.of(".").toAbsolutePath().normalize(), context.projectRoot());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> new LspProjectContext(" ", "pbs", Path.of(".")));
|
||||
assertThrows(IllegalArgumentException.class, () -> new LspProjectContext("demo", " ", Path.of(".")));
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":prometeu-infra"))
|
||||
|
||||
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:1.0.0")
|
||||
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
|
||||
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||
implementation(libs.lsp4j)
|
||||
}
|
||||
@ -1,271 +0,0 @@
|
||||
package p.studio.lsp;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.jsonrpc.Launcher;
|
||||
import org.eclipse.lsp4j.launch.LSPLauncher;
|
||||
import org.eclipse.lsp4j.services.*;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public final class PrometeuStudioLspMain {
|
||||
|
||||
private static final String HOST = "127.0.0.1";
|
||||
private static final int PORT = 7777;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
InetAddress bindAddress = InetAddress.getByName(HOST);
|
||||
|
||||
try (ServerSocket serverSocket = new ServerSocket(PORT, 50, bindAddress)) {
|
||||
System.out.println("Prometeu Studio LSP listening on " + HOST + ":" + PORT);
|
||||
|
||||
while (true) {
|
||||
Socket socket = serverSocket.accept();
|
||||
|
||||
Thread clientThread = new Thread(() -> handleClient(socket));
|
||||
clientThread.setName("prometeu-lsp-client");
|
||||
clientThread.setDaemon(true);
|
||||
clientThread.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleClient(Socket socket) {
|
||||
try {
|
||||
System.out.println("VS Code connected: " + socket.getRemoteSocketAddress());
|
||||
|
||||
InputStream input = socket.getInputStream();
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
PrometeuLanguageServer server = new PrometeuLanguageServer();
|
||||
|
||||
Launcher<LanguageClient> launcher =
|
||||
LSPLauncher.createServerLauncher(server, input, output);
|
||||
|
||||
LanguageClient client = launcher.getRemoteProxy();
|
||||
server.connect(client);
|
||||
|
||||
launcher.startListening();
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("LSP client failed: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PrometeuLanguageServer implements LanguageServer, LanguageClientAware {
|
||||
|
||||
private LanguageClient client;
|
||||
private final TextDocumentService textDocumentService = new PrometeuTextDocumentService(this);
|
||||
private final WorkspaceService workspaceService = new PrometeuWorkspaceService();
|
||||
|
||||
private boolean shutdownRequested = false;
|
||||
|
||||
@Override
|
||||
public void connect(LanguageClient client) {
|
||||
this.client = client;
|
||||
|
||||
if (textDocumentService instanceof PrometeuTextDocumentService service) {
|
||||
service.connect(client);
|
||||
}
|
||||
}
|
||||
|
||||
public LanguageClient client() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
|
||||
ServerCapabilities capabilities = new ServerCapabilities();
|
||||
|
||||
TextDocumentSyncOptions syncOptions = new TextDocumentSyncOptions();
|
||||
syncOptions.setOpenClose(true);
|
||||
syncOptions.setChange(TextDocumentSyncKind.Full);
|
||||
|
||||
SaveOptions saveOptions = new SaveOptions();
|
||||
saveOptions.setIncludeText(true);
|
||||
syncOptions.setSave(saveOptions);
|
||||
|
||||
capabilities.setTextDocumentSync(syncOptions);
|
||||
capabilities.setHoverProvider(true);
|
||||
|
||||
InitializeResult result = new InitializeResult(capabilities);
|
||||
|
||||
ServerInfo serverInfo = new ServerInfo();
|
||||
serverInfo.setName("Prometeu Studio LSP");
|
||||
serverInfo.setVersion("0.1.0");
|
||||
result.setServerInfo(serverInfo);
|
||||
|
||||
return CompletableFuture.completedFuture(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialized(InitializedParams params) {
|
||||
if (client != null) {
|
||||
client.logMessage(new MessageParams(
|
||||
MessageType.Info,
|
||||
"Hello from Prometeu Studio LSP"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Object> shutdown() {
|
||||
shutdownRequested = true;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
System.out.println("LSP client requested exit. shutdownRequested=" + shutdownRequested);
|
||||
|
||||
// Importantíssimo:
|
||||
// Como isso está dentro do Studio, NÃO use System.exit().
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDocumentService getTextDocumentService() {
|
||||
return textDocumentService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkspaceService getWorkspaceService() {
|
||||
return workspaceService;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
|
||||
private final PrometeuLanguageServer server;
|
||||
private LanguageClient client;
|
||||
|
||||
private PrometeuTextDocumentService(PrometeuLanguageServer server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
private void connect(LanguageClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didOpen(DidOpenTextDocumentParams params) {
|
||||
String uri = params.getTextDocument().getUri();
|
||||
String text = params.getTextDocument().getText();
|
||||
|
||||
System.out.println("didOpen: " + uri);
|
||||
|
||||
publishHelloDiagnostic(uri, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didChange(DidChangeTextDocumentParams params) {
|
||||
String uri = params.getTextDocument().getUri();
|
||||
|
||||
String text = "";
|
||||
if (!params.getContentChanges().isEmpty()) {
|
||||
text = params.getContentChanges().get(0).getText();
|
||||
}
|
||||
|
||||
System.out.println("didChange: " + uri);
|
||||
|
||||
publishHelloDiagnostic(uri, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didClose(DidCloseTextDocumentParams params) {
|
||||
String uri = params.getTextDocument().getUri();
|
||||
|
||||
System.out.println("didClose: " + uri);
|
||||
|
||||
if (client != null) {
|
||||
client.publishDiagnostics(new PublishDiagnosticsParams(uri, List.of()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didSave(DidSaveTextDocumentParams params) {
|
||||
String uri = params.getTextDocument().getUri();
|
||||
|
||||
System.out.println("didSave: " + uri);
|
||||
|
||||
if (client != null) {
|
||||
client.logMessage(new MessageParams(
|
||||
MessageType.Info,
|
||||
"Prometeu Studio saw save: " + uri
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Hover> hover(HoverParams params) {
|
||||
MarkupContent content = new MarkupContent();
|
||||
content.setKind(MarkupKind.MARKDOWN);
|
||||
content.setValue("""
|
||||
**Prometeu Studio LSP**
|
||||
|
||||
Hello from Java + LSP4J.
|
||||
|
||||
This hover came from the Studio process.
|
||||
""");
|
||||
|
||||
Hover hover = new Hover();
|
||||
hover.setContents(content);
|
||||
|
||||
return CompletableFuture.completedFuture(hover);
|
||||
}
|
||||
|
||||
private void publishHelloDiagnostic(String uri, String text) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Diagnostic diagnostic = new Diagnostic();
|
||||
|
||||
diagnostic.setRange(new Range(
|
||||
new Position(0, 0),
|
||||
new Position(0, Math.max(1, firstLineLength(text)))
|
||||
));
|
||||
|
||||
diagnostic.setSeverity(DiagnosticSeverity.Information);
|
||||
diagnostic.setSource("Prometeu Studio");
|
||||
diagnostic.setMessage("Hello from Prometeu Studio LSP");
|
||||
|
||||
client.publishDiagnostics(new PublishDiagnosticsParams(
|
||||
uri,
|
||||
List.of(diagnostic)
|
||||
));
|
||||
}
|
||||
|
||||
private int firstLineLength(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int newline = text.indexOf('\n');
|
||||
|
||||
if (newline < 0) {
|
||||
return text.length();
|
||||
}
|
||||
|
||||
return newline;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PrometeuWorkspaceService implements WorkspaceService {
|
||||
|
||||
@Override
|
||||
public void didChangeConfiguration(DidChangeConfigurationParams params) {
|
||||
System.out.println("didChangeConfiguration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
|
||||
System.out.println("didChangeWatchedFiles: " + params.getChanges().size() + " changes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package p.studio.lsp.v1;
|
||||
|
||||
import p.studio.lsp.api.LspProjectContext;
|
||||
import p.studio.lsp.api.LspServerBootRequest;
|
||||
import p.studio.lsp.v1.bootstrap.LspV1ServerLifecycle;
|
||||
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
|
||||
import p.studio.lsp.v1.host.TcpLspServerHostFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class PrometeuStudioLspMain {
|
||||
private PrometeuStudioLspMain() {
|
||||
}
|
||||
|
||||
public static void main(final String[] args) throws Exception {
|
||||
final var lifecycle = new LspV1ServerLifecycle(
|
||||
new TcpLspServerHostFactory(),
|
||||
new CompilerLanguageServiceBridge());
|
||||
|
||||
final var request = LspServerBootRequest.defaults(new LspProjectContext(
|
||||
"standalone-mock",
|
||||
"pbs",
|
||||
Path.of(".")));
|
||||
|
||||
final var handle = lifecycle.bootServer(request);
|
||||
System.out.println("Prometeu Studio LSP listening on " + handle.endpoint().host() + ":" + handle.endpoint().port());
|
||||
Thread.currentThread().join();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package p.studio.lsp.v1.bootstrap;
|
||||
|
||||
import p.studio.lsp.api.LspProjectContext;
|
||||
import p.studio.lsp.api.LspServerBootRequest;
|
||||
import p.studio.lsp.api.LspServerHandle;
|
||||
import p.studio.lsp.api.LspServerLifecycle;
|
||||
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
|
||||
import p.studio.lsp.v1.host.ProjectScopedLspServerHost;
|
||||
import p.studio.lsp.v1.host.ProjectScopedLspServerHostFactory;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
public final class LspV1ServerLifecycle implements LspServerLifecycle {
|
||||
private final ProjectScopedLspServerHostFactory serverHostFactory;
|
||||
private final CompilerLanguageServiceBridge compilerBridge;
|
||||
private final ConcurrentMap<String, ProjectScopedLspServerHost> activeServers;
|
||||
|
||||
public LspV1ServerLifecycle(
|
||||
final ProjectScopedLspServerHostFactory serverHostFactory,
|
||||
final CompilerLanguageServiceBridge compilerBridge) {
|
||||
this.serverHostFactory = Objects.requireNonNull(serverHostFactory, "serverHostFactory");
|
||||
this.compilerBridge = Objects.requireNonNull(compilerBridge, "compilerBridge");
|
||||
this.activeServers = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LspServerHandle bootServer(final LspServerBootRequest request) {
|
||||
final LspServerBootRequest safeRequest = Objects.requireNonNull(request, "request");
|
||||
final ProjectScopedLspServerHost host = serverHostFactory.create(safeRequest, compilerBridge);
|
||||
final ProjectScopedLspServerHost previous = activeServers.putIfAbsent(safeRequest.project().projectKey(), host);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("LSP server already booted for project " + safeRequest.project().projectKey());
|
||||
}
|
||||
|
||||
try {
|
||||
final var endpoint = host.start();
|
||||
return new LspServerHandle(safeRequest.project(), endpoint);
|
||||
} catch (RuntimeException runtimeException) {
|
||||
activeServers.remove(safeRequest.project().projectKey(), host);
|
||||
closeQuietly(host);
|
||||
throw runtimeException;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownServer(final LspProjectContext project) {
|
||||
final LspProjectContext safeProject = Objects.requireNonNull(project, "project");
|
||||
final ProjectScopedLspServerHost host = activeServers.remove(safeProject.projectKey());
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
closeQuietly(host);
|
||||
}
|
||||
|
||||
private static void closeQuietly(final ProjectScopedLspServerHost host) {
|
||||
try {
|
||||
host.close();
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("failed to close LSP host", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package p.studio.lsp.v1.compiler;
|
||||
|
||||
import p.studio.compiler.workspaces.BuilderPipelineService;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class CompilerLanguageServiceBridge {
|
||||
private final BuilderPipelineService builderPipelineService;
|
||||
|
||||
public CompilerLanguageServiceBridge() {
|
||||
this(BuilderPipelineService.INSTANCE);
|
||||
}
|
||||
|
||||
public CompilerLanguageServiceBridge(final BuilderPipelineService builderPipelineService) {
|
||||
this.builderPipelineService = Objects.requireNonNull(builderPipelineService, "builderPipelineService");
|
||||
}
|
||||
|
||||
public BuilderPipelineService builderPipelineService() {
|
||||
return builderPipelineService;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package p.studio.lsp.v1.host;
|
||||
|
||||
import p.studio.lsp.api.LspServerEndpoint;
|
||||
|
||||
public interface ProjectScopedLspServerHost extends AutoCloseable {
|
||||
LspServerEndpoint start();
|
||||
|
||||
@Override
|
||||
void close() throws Exception;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package p.studio.lsp.v1.host;
|
||||
|
||||
import p.studio.lsp.api.LspServerBootRequest;
|
||||
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
|
||||
|
||||
public interface ProjectScopedLspServerHostFactory {
|
||||
ProjectScopedLspServerHost create(
|
||||
LspServerBootRequest request,
|
||||
CompilerLanguageServiceBridge compilerBridge);
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package p.studio.lsp.v1.host;
|
||||
|
||||
import org.eclipse.lsp4j.jsonrpc.Launcher;
|
||||
import org.eclipse.lsp4j.launch.LSPLauncher;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import p.studio.lsp.api.LspServerBootRequest;
|
||||
import p.studio.lsp.api.LspServerEndpoint;
|
||||
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
|
||||
import p.studio.lsp.v1.protocol.PrometeuLanguageServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class TcpLspServerHost implements ProjectScopedLspServerHost {
|
||||
private final LspServerBootRequest request;
|
||||
private final CompilerLanguageServiceBridge compilerBridge;
|
||||
private final ExecutorService clientExecutor;
|
||||
private volatile boolean closed;
|
||||
private volatile ServerSocket serverSocket;
|
||||
private volatile Thread acceptThread;
|
||||
private volatile LspServerEndpoint endpoint;
|
||||
|
||||
public TcpLspServerHost(
|
||||
final LspServerBootRequest request,
|
||||
final CompilerLanguageServiceBridge compilerBridge) {
|
||||
this.request = Objects.requireNonNull(request, "request");
|
||||
this.compilerBridge = Objects.requireNonNull(compilerBridge, "compilerBridge");
|
||||
this.clientExecutor = Executors.newCachedThreadPool(runnable -> {
|
||||
final Thread thread = new Thread(runnable);
|
||||
thread.setName("prometeu-lsp-v1-client");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized LspServerEndpoint start() {
|
||||
if (endpoint != null) {
|
||||
return endpoint;
|
||||
}
|
||||
try {
|
||||
final InetAddress bindAddress = InetAddress.getByName(request.configuration().host());
|
||||
final ServerSocket socket = new ServerSocket(request.configuration().port(), 50, bindAddress);
|
||||
serverSocket = socket;
|
||||
endpoint = new LspServerEndpoint(bindAddress.getHostAddress(), socket.getLocalPort());
|
||||
acceptThread = new Thread(this::acceptLoop, "prometeu-lsp-v1-accept");
|
||||
acceptThread.setDaemon(true);
|
||||
acceptThread.start();
|
||||
return endpoint;
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("failed to start TCP LSP host", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptLoop() {
|
||||
while (!closed) {
|
||||
try {
|
||||
final Socket socket = serverSocket.accept();
|
||||
clientExecutor.submit(() -> handleClient(socket));
|
||||
} catch (IOException exception) {
|
||||
if (!closed) {
|
||||
throw new IllegalStateException("failed to accept LSP client", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClient(final Socket socket) {
|
||||
try (socket) {
|
||||
final var server = new PrometeuLanguageServer(request.project(), compilerBridge);
|
||||
final Launcher<LanguageClient> launcher = LSPLauncher.createServerLauncher(
|
||||
server,
|
||||
socket.getInputStream(),
|
||||
socket.getOutputStream());
|
||||
server.connect(launcher.getRemoteProxy());
|
||||
launcher.startListening().get();
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("LSP client failed", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws Exception {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
if (serverSocket != null) {
|
||||
serverSocket.close();
|
||||
}
|
||||
if (acceptThread != null) {
|
||||
acceptThread.interrupt();
|
||||
}
|
||||
clientExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.v1.host;
|
||||
|
||||
import p.studio.lsp.api.LspServerBootRequest;
|
||||
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
|
||||
|
||||
public final class TcpLspServerHostFactory implements ProjectScopedLspServerHostFactory {
|
||||
@Override
|
||||
public ProjectScopedLspServerHost create(
|
||||
final LspServerBootRequest request,
|
||||
final CompilerLanguageServiceBridge compilerBridge) {
|
||||
return new TcpLspServerHost(request, compilerBridge);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package p.studio.lsp.v1.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.LanguageClientAware;
|
||||
import org.eclipse.lsp4j.services.LanguageServer;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
import org.eclipse.lsp4j.services.WorkspaceService;
|
||||
import p.studio.lsp.api.LspProjectContext;
|
||||
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
|
||||
import p.studio.lsp.v1.protocol.mapping.ProtocolServerProfile;
|
||||
import p.studio.lsp.v1.protocol.mapping.ServerCapabilitiesMapper;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public final class PrometeuLanguageServer implements LanguageServer, LanguageClientAware {
|
||||
private final LspProjectContext project;
|
||||
private final CompilerLanguageServiceBridge compilerBridge;
|
||||
private final ServerCapabilitiesMapper capabilitiesMapper;
|
||||
private final TextDocumentService textDocumentService;
|
||||
private final WorkspaceService workspaceService;
|
||||
private volatile LanguageClient client;
|
||||
private volatile boolean shutdownRequested;
|
||||
|
||||
public PrometeuLanguageServer(
|
||||
final LspProjectContext project,
|
||||
final CompilerLanguageServiceBridge compilerBridge) {
|
||||
this(project,
|
||||
compilerBridge,
|
||||
new ServerCapabilitiesMapper(),
|
||||
new PrometeuTextDocumentService(),
|
||||
new PrometeuWorkspaceService());
|
||||
}
|
||||
|
||||
PrometeuLanguageServer(
|
||||
final LspProjectContext project,
|
||||
final CompilerLanguageServiceBridge compilerBridge,
|
||||
final ServerCapabilitiesMapper capabilitiesMapper,
|
||||
final TextDocumentService textDocumentService,
|
||||
final WorkspaceService workspaceService) {
|
||||
this.project = Objects.requireNonNull(project, "project");
|
||||
this.compilerBridge = Objects.requireNonNull(compilerBridge, "compilerBridge");
|
||||
this.capabilitiesMapper = Objects.requireNonNull(capabilitiesMapper, "capabilitiesMapper");
|
||||
this.textDocumentService = Objects.requireNonNull(textDocumentService, "textDocumentService");
|
||||
this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(final LanguageClient client) {
|
||||
this.client = client;
|
||||
if (textDocumentService instanceof PrometeuTextDocumentService service) {
|
||||
service.connect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<InitializeResult> initialize(final InitializeParams params) {
|
||||
final ProtocolServerProfile profile = ProtocolServerProfile.defaults();
|
||||
return CompletableFuture.completedFuture(capabilitiesMapper.map(profile));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialized(final InitializedParams params) {
|
||||
final LanguageClient currentClient = client;
|
||||
if (currentClient == null) {
|
||||
return;
|
||||
}
|
||||
currentClient.logMessage(new MessageParams(
|
||||
MessageType.Info,
|
||||
"Prometeu Studio LSP connected for project " + project.projectKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Object> shutdown() {
|
||||
shutdownRequested = true;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
if (!shutdownRequested) {
|
||||
final LanguageClient currentClient = client;
|
||||
if (currentClient != null) {
|
||||
currentClient.logMessage(new MessageParams(
|
||||
MessageType.Warning,
|
||||
"Prometeu Studio LSP received exit before shutdown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDocumentService getTextDocumentService() {
|
||||
return textDocumentService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkspaceService getWorkspaceService() {
|
||||
return workspaceService;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package p.studio.lsp.v1.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
private volatile LanguageClient client;
|
||||
|
||||
void connect(final LanguageClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didOpen(final DidOpenTextDocumentParams params) {
|
||||
publishHelloDiagnostic(params.getTextDocument().getUri(), params.getTextDocument().getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didChange(final DidChangeTextDocumentParams params) {
|
||||
final String text = params.getContentChanges().isEmpty()
|
||||
? ""
|
||||
: params.getContentChanges().getFirst().getText();
|
||||
publishHelloDiagnostic(params.getTextDocument().getUri(), text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didClose(final DidCloseTextDocumentParams params) {
|
||||
final LanguageClient currentClient = client;
|
||||
if (currentClient == null) {
|
||||
return;
|
||||
}
|
||||
currentClient.publishDiagnostics(new PublishDiagnosticsParams(params.getTextDocument().getUri(), List.of()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didSave(final DidSaveTextDocumentParams params) {
|
||||
final LanguageClient currentClient = client;
|
||||
if (currentClient == null) {
|
||||
return;
|
||||
}
|
||||
currentClient.logMessage(new MessageParams(
|
||||
MessageType.Info,
|
||||
"Prometeu Studio saw save: " + params.getTextDocument().getUri()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Hover> hover(final HoverParams params) {
|
||||
final MarkupContent content = new MarkupContent();
|
||||
content.setKind(MarkupKind.MARKDOWN);
|
||||
content.setValue("""
|
||||
**Prometeu Studio LSP**
|
||||
|
||||
Structured baseline server.
|
||||
|
||||
Semantic enrichment will be layered later.
|
||||
""");
|
||||
|
||||
final Hover hover = new Hover();
|
||||
hover.setContents(content);
|
||||
return CompletableFuture.completedFuture(hover);
|
||||
}
|
||||
|
||||
private void publishHelloDiagnostic(
|
||||
final String uri,
|
||||
final String text) {
|
||||
final LanguageClient currentClient = client;
|
||||
if (currentClient == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Diagnostic diagnostic = new Diagnostic();
|
||||
diagnostic.setRange(new Range(
|
||||
new Position(0, 0),
|
||||
new Position(0, Math.max(1, firstLineLength(text)))));
|
||||
diagnostic.setSeverity(DiagnosticSeverity.Information);
|
||||
diagnostic.setSource("Prometeu Studio");
|
||||
diagnostic.setMessage("Structured baseline LSP response");
|
||||
|
||||
currentClient.publishDiagnostics(new PublishDiagnosticsParams(uri, List.of(diagnostic)));
|
||||
}
|
||||
|
||||
private int firstLineLength(final String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
final int newline = text.indexOf('\n');
|
||||
return newline < 0 ? text.length() : newline;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package p.studio.lsp.v1.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.DidChangeConfigurationParams;
|
||||
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
|
||||
import org.eclipse.lsp4j.services.WorkspaceService;
|
||||
|
||||
public final class PrometeuWorkspaceService implements WorkspaceService {
|
||||
@Override
|
||||
public void didChangeConfiguration(final DidChangeConfigurationParams params) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didChangeWatchedFiles(final DidChangeWatchedFilesParams params) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package p.studio.lsp.v1.protocol.mapping;
|
||||
|
||||
public record ProtocolServerProfile(
|
||||
String name,
|
||||
String version,
|
||||
boolean hoverSupported) {
|
||||
|
||||
public static ProtocolServerProfile defaults() {
|
||||
return new ProtocolServerProfile("Prometeu Studio LSP", "0.1.0", true);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package p.studio.lsp.v1.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
|
||||
public final class ServerCapabilitiesMapper {
|
||||
public InitializeResult map(final ProtocolServerProfile profile) {
|
||||
final ServerCapabilities capabilities = new ServerCapabilities();
|
||||
|
||||
final TextDocumentSyncOptions syncOptions = new TextDocumentSyncOptions();
|
||||
syncOptions.setOpenClose(true);
|
||||
syncOptions.setChange(TextDocumentSyncKind.Full);
|
||||
|
||||
final SaveOptions saveOptions = new SaveOptions();
|
||||
saveOptions.setIncludeText(true);
|
||||
syncOptions.setSave(saveOptions);
|
||||
|
||||
capabilities.setTextDocumentSync(syncOptions);
|
||||
capabilities.setHoverProvider(profile.hoverSupported());
|
||||
|
||||
final InitializeResult result = new InitializeResult(capabilities);
|
||||
final ServerInfo serverInfo = new ServerInfo();
|
||||
serverInfo.setName(profile.name());
|
||||
serverInfo.setVersion(profile.version());
|
||||
result.setServerInfo(serverInfo);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package p.studio.lsp.v1.bootstrap;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.lsp.api.LspProjectContext;
|
||||
import p.studio.lsp.api.LspServerBootRequest;
|
||||
import p.studio.lsp.api.LspServerEndpoint;
|
||||
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
|
||||
import p.studio.lsp.v1.host.ProjectScopedLspServerHost;
|
||||
import p.studio.lsp.v1.host.ProjectScopedLspServerHostFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LspV1ServerLifecycleTest {
|
||||
|
||||
@Test
|
||||
void lifecycleBootsAndShutsDownProjectScopedHost() {
|
||||
final FakeHost host = new FakeHost();
|
||||
final ProjectScopedLspServerHostFactory factory = (request, compilerBridge) -> host;
|
||||
final var lifecycle = new LspV1ServerLifecycle(factory, new CompilerLanguageServiceBridge());
|
||||
final var project = new LspProjectContext("demo", "pbs", Path.of("."));
|
||||
|
||||
final var handle = lifecycle.bootServer(LspServerBootRequest.defaults(project));
|
||||
|
||||
assertEquals(project, handle.project());
|
||||
assertEquals("127.0.0.1", handle.endpoint().host());
|
||||
assertEquals(7777, handle.endpoint().port());
|
||||
assertTrue(host.started);
|
||||
|
||||
lifecycle.shutdownServer(project);
|
||||
assertTrue(host.closed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lifecycleRejectsDoubleBootForSameProject() {
|
||||
final ProjectScopedLspServerHostFactory factory = (request, compilerBridge) -> new FakeHost();
|
||||
final var lifecycle = new LspV1ServerLifecycle(factory, new CompilerLanguageServiceBridge());
|
||||
final var request = LspServerBootRequest.defaults(new LspProjectContext("demo", "pbs", Path.of(".")));
|
||||
|
||||
lifecycle.bootServer(request);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> lifecycle.bootServer(request));
|
||||
}
|
||||
|
||||
private static final class FakeHost implements ProjectScopedLspServerHost {
|
||||
private boolean started;
|
||||
private boolean closed;
|
||||
|
||||
@Override
|
||||
public LspServerEndpoint start() {
|
||||
started = true;
|
||||
return new LspServerEndpoint("127.0.0.1", 7777);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package p.studio.lsp.v1.boundary;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LspV1BoundaryTest {
|
||||
|
||||
@Test
|
||||
void lsp4jMustRemainContainedInsideLspV1Sources() throws IOException {
|
||||
final List<Path> forbiddenRoots = List.of(
|
||||
Path.of("../prometeu-lsp-api/src/main/java"),
|
||||
Path.of("../../../prometeu-studio/src/main/java"),
|
||||
Path.of("../../../prometeu-app/src/main/java"),
|
||||
Path.of("../../../prometeu-compiler"),
|
||||
Path.of("../../../prometeu-packer"),
|
||||
Path.of("../../../prometeu-infra"));
|
||||
|
||||
for (final Path root : forbiddenRoots) {
|
||||
assertNoLsp4jImports(root.normalize());
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertNoLsp4jImports(final Path root) throws IOException {
|
||||
if (!Files.exists(root)) {
|
||||
return;
|
||||
}
|
||||
final List<Path> javaFiles;
|
||||
try (var stream = Files.walk(root)) {
|
||||
javaFiles = stream
|
||||
.filter(path -> path.toString().endsWith(".java"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
for (final Path javaFile : javaFiles) {
|
||||
final String source = Files.readString(javaFile);
|
||||
assertTrue(!source.contains("org.eclipse.lsp4j"),
|
||||
() -> "LSP4J leaked outside lsp-v1: " + javaFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user