implements PLN-0065

This commit is contained in:
bQUARKz 2026-05-05 15:50:55 +01:00
parent e5342e34f7
commit b0073cd7bd
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
32 changed files with 1445 additions and 277 deletions

View File

@ -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"}]}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,6 @@
plugins {
id("gradle.java-library-conventions")
}
dependencies {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package p.studio.lsp.api;
public interface LspServerLifecycle {
LspServerHandle bootServer(LspServerBootRequest request);
void shutdownServer(LspProjectContext project);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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