implements PLN-0066

This commit is contained in:
bQUARKz 2026-05-05 15:54:11 +01:00
parent b0073cd7bd
commit 18dc64e36c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 195 additions and 18 deletions

View File

@ -1,5 +1,5 @@
{"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-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":"done","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

@ -2,9 +2,9 @@
id: PLN-0066
ticket: studio-new-lsp-api-and-v1-boundary
title: Project-Scoped LSP Server Lifecycle in Studio
status: open
status: done
created: 2026-05-05
completed:
completed: 2026-05-05
tags: [studio, lsp, lifecycle, projects, boot, shutdown]
---
@ -85,11 +85,11 @@ These surfaces already own project startup and teardown flow and therefore are t
## 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.
- [x] Opening a Studio project boots an LSP server for that project.
- [x] Closing the project shuts down that server.
- [x] The lifecycle is owned by project session boundaries, not global app startup.
- [x] Existing non-LSP project flows remain operational.
- [x] The current dumb connectivity workflow remains usable as a temporary implementation phase.
## Dependencies

View File

@ -5,6 +5,8 @@ plugins {
dependencies {
implementation(project(":prometeu-infra"))
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
implementation(project(":prometeu-lsp:prometeu-lsp-v1"))
implementation(project(":prometeu-packer:prometeu-packer-api"))
implementation(project(":prometeu-packer:prometeu-packer-v1"))
implementation(project(":prometeu-studio"))

View File

@ -2,8 +2,12 @@ package p.studio;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.Packer;
import p.studio.lsp.api.LspServerLifecycle;
import p.studio.lsp.events.StudioEventBus;
import p.studio.lsp.events.StudioPackerEventAdapter;
import p.studio.lsp.v1.bootstrap.LspV1ServerLifecycle;
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
import p.studio.lsp.v1.host.TcpLspServerHostFactory;
import p.studio.utilities.ThemeService;
import p.studio.utilities.i18n.I18nService;
@ -19,6 +23,7 @@ public final class AppContainer implements Container {
private final ObjectMapper mapper;
private final EmbeddedPacker embeddedPacker;
private final StudioBackgroundTasks backgroundTasks;
private final LspServerLifecycle lspServerLifecycle;
public AppContainer() {
this.i18nService = new I18nService();
@ -29,6 +34,9 @@ public final class AppContainer implements Container {
this.backgroundTasks = new StudioBackgroundTasks(backgroundExecutor);
final Packer packer = Packer.bootstrap(this.mapper, new StudioPackerEventAdapter(studioEventBus));
this.embeddedPacker = new EmbeddedPacker(packer.workspaceService(), packer::close);
this.lspServerLifecycle = new LspV1ServerLifecycle(
new TcpLspServerHostFactory(),
new CompilerLanguageServiceBridge());
}
@Override
@ -61,6 +69,11 @@ public final class AppContainer implements Container {
return backgroundTasks;
}
@Override
public LspServerLifecycle getLspServerLifecycle() {
return lspServerLifecycle;
}
@Override
public void shutdownContainer() {
embeddedPacker.shutdown();

View File

@ -5,6 +5,7 @@ plugins {
dependencies {
implementation(project(":prometeu-infra"))
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
implementation(project(":prometeu-packer:prometeu-packer-api"))
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))

View File

@ -1,6 +1,7 @@
package p.studio;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.studio.lsp.api.LspServerLifecycle;
import p.studio.lsp.events.StudioEventBus;
import p.studio.utilities.ThemeService;
import p.studio.utilities.i18n.I18nService;
@ -21,6 +22,8 @@ public interface Container {
StudioBackgroundTasks getBackgroundTasks();
LspServerLifecycle getLspServerLifecycle();
void shutdownContainer();
static void install(Container container) {
@ -68,6 +71,10 @@ public interface Container {
return current().getBackgroundTasks();
}
static LspServerLifecycle lspServerLifecycle() {
return current().getLspServerLifecycle();
}
static void shutdown() {
final Container container = Holder.CURRENT.get();
if (container != null) {

View File

@ -1,6 +1,9 @@
package p.studio.projectsessions;
import p.studio.execution.StudioExecutionSessionService;
import p.studio.lsp.api.LspProjectContext;
import p.studio.lsp.api.LspServerHandle;
import p.studio.lsp.api.LspServerLifecycle;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projectstate.ProjectLocalStudioStateService;
@ -13,6 +16,8 @@ public final class StudioProjectSession implements AutoCloseable {
private final ProjectLocalStudioStateService projectLocalStudioStateService;
private final ProjectLocalStudioSetup projectLocalStudioSetup;
private final StudioExecutionSessionService executionSessionService;
private final LspServerLifecycle lspServerLifecycle;
private final LspServerHandle lspServerHandle;
private ProjectLocalStudioState projectLocalStudioState;
private boolean closed;
@ -23,6 +28,8 @@ public final class StudioProjectSession implements AutoCloseable {
new ProjectLocalStudioStateService(),
ProjectLocalStudioSetup.defaults(),
new StudioExecutionSessionService(),
null,
null,
ProjectLocalStudioState.defaults());
}
@ -31,11 +38,15 @@ public final class StudioProjectSession implements AutoCloseable {
final ProjectLocalStudioStateService projectLocalStudioStateService,
final ProjectLocalStudioSetup projectLocalStudioSetup,
final StudioExecutionSessionService executionSessionService,
final LspServerLifecycle lspServerLifecycle,
final LspServerHandle lspServerHandle,
final ProjectLocalStudioState projectLocalStudioState) {
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup");
this.executionSessionService = Objects.requireNonNull(executionSessionService, "executionSessionService");
this.lspServerLifecycle = lspServerLifecycle;
this.lspServerHandle = lspServerHandle;
this.projectLocalStudioState = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState");
}
@ -55,6 +66,10 @@ public final class StudioProjectSession implements AutoCloseable {
return executionSessionService;
}
public LspServerHandle lspServerHandle() {
return lspServerHandle;
}
public void replaceProjectLocalStudioState(final ProjectLocalStudioState nextProjectLocalStudioState) {
this.projectLocalStudioState = Objects.requireNonNull(nextProjectLocalStudioState, "nextProjectLocalStudioState");
}
@ -76,8 +91,12 @@ public final class StudioProjectSession implements AutoCloseable {
failure = runtimeException;
}
try {
if (failure != null) {
throw failure;
shutdownLspServer();
} catch (RuntimeException runtimeException) {
if (failure == null) {
failure = runtimeException;
} else {
failure.addSuppressed(runtimeException);
}
} finally {
if (failure != null) {
@ -85,4 +104,12 @@ public final class StudioProjectSession implements AutoCloseable {
}
}
}
private void shutdownLspServer() {
if (lspServerLifecycle == null || lspServerHandle == null) {
return;
}
final LspProjectContext projectContext = lspServerHandle.project();
lspServerLifecycle.shutdownServer(projectContext);
}
}

View File

@ -1,5 +1,9 @@
package p.studio.projectsessions;
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.execution.StudioExecutionSessionService;
import p.studio.projectstate.ProjectLocalStudioSetupService;
import p.studio.projectstate.ProjectLocalStudioStateService;
@ -10,27 +14,44 @@ import java.util.Objects;
public final class StudioProjectSessionFactory {
private final ProjectLocalStudioStateService projectLocalStudioStateService;
private final ProjectLocalStudioSetupService projectLocalStudioSetupService;
private final LspServerLifecycle lspServerLifecycle;
public StudioProjectSessionFactory() {
public StudioProjectSessionFactory(final LspServerLifecycle lspServerLifecycle) {
this(
new ProjectLocalStudioStateService(),
new ProjectLocalStudioSetupService());
new ProjectLocalStudioSetupService(),
Objects.requireNonNull(lspServerLifecycle, "lspServerLifecycle"));
}
StudioProjectSessionFactory(
final ProjectLocalStudioStateService projectLocalStudioStateService,
final ProjectLocalStudioSetupService projectLocalStudioSetupService) {
final ProjectLocalStudioSetupService projectLocalStudioSetupService,
final LspServerLifecycle lspServerLifecycle) {
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService");
this.lspServerLifecycle = Objects.requireNonNull(lspServerLifecycle, "lspServerLifecycle");
}
public StudioProjectSession open(final ProjectReference projectReference) {
final ProjectReference target = Objects.requireNonNull(projectReference, "projectReference");
final var setup = projectLocalStudioSetupService.load(target);
final var state = projectLocalStudioStateService.load(target);
final LspProjectContext lspProjectContext = toLspProjectContext(target);
final LspServerHandle lspServerHandle = lspServerLifecycle.bootServer(LspServerBootRequest.defaults(lspProjectContext));
return new StudioProjectSession(
target,
projectLocalStudioStateService,
projectLocalStudioSetupService.load(target),
setup,
new StudioExecutionSessionService(),
projectLocalStudioStateService.load(target));
lspServerLifecycle,
lspServerHandle,
state);
}
private static LspProjectContext toLspProjectContext(final ProjectReference projectReference) {
return new LspProjectContext(
projectReference.rootPath().toAbsolutePath().normalize().toString(),
projectReference.languageId(),
projectReference.rootPath());
}
}

View File

@ -37,7 +37,7 @@ public final class StudioWindowCoordinator {
this.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot());
this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService);
this.windowStateService = new WindowStateService(resolveWindowStateStorage());
this.projectSessionFactory = new StudioProjectSessionFactory();
this.projectSessionFactory = new StudioProjectSessionFactory(Container.lspServerLifecycle());
this.launcherView = new ProjectLauncherView(
knownProjectsService,
projectCatalogService,

View File

@ -1,6 +1,10 @@
package p.studio.projectsessions;
import org.junit.jupiter.api.Test;
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.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioSetupService;
import p.studio.projectstate.ProjectLocalStudioState;
@ -10,16 +14,20 @@ import p.studio.projects.ProjectReference;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
final class StudioProjectSessionFactoryTest {
@Test
void openCreatesProjectSessionWithoutLegacyEditorServices() {
void openCreatesProjectSessionAndBootsLspServer() {
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService();
final RecordingLspServerLifecycle lspServerLifecycle = new RecordingLspServerLifecycle();
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(
stateService,
setupService);
setupService,
lspServerLifecycle);
final ProjectReference projectReference = new ProjectReference(
"Example",
"1.0.0",
@ -35,6 +43,29 @@ final class StudioProjectSessionFactoryTest {
assertEquals("IDLE", session.executionSession().snapshot().state().name());
assertSame(projectReference, stateService.loadedProjectReference);
assertSame(projectReference, setupService.loadedProjectReference);
assertNotNull(session.lspServerHandle());
assertEquals(1, lspServerLifecycle.bootCalls);
assertEquals(projectReference.languageId(), lspServerLifecycle.lastRequest.project().languageId());
assertEquals(projectReference.rootPath().toAbsolutePath().normalize(), lspServerLifecycle.lastRequest.project().projectRoot());
}
@Test
void openSurfacesLspBootFailure() {
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService();
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(
stateService,
setupService,
new FailingLspServerLifecycle());
final IllegalStateException failure = assertThrows(IllegalStateException.class, () -> sessionFactory.open(new ProjectReference(
"Example",
"1.0.0",
"pbs",
1,
Path.of("/tmp/example"))));
assertEquals("boom", failure.getMessage());
}
private static final class RecordingProjectLocalStudioStateService extends ProjectLocalStudioStateService {
@ -63,4 +94,33 @@ final class StudioProjectSessionFactoryTest {
return loadedSetup;
}
}
private static final class RecordingLspServerLifecycle implements LspServerLifecycle {
private int bootCalls;
private LspServerBootRequest lastRequest;
@Override
public LspServerHandle bootServer(final LspServerBootRequest request) {
bootCalls++;
lastRequest = request;
return new LspServerHandle(
request.project(),
new p.studio.lsp.api.LspServerEndpoint("127.0.0.1", request.configuration().port()));
}
@Override
public void shutdownServer(final LspProjectContext project) {
}
}
private static final class FailingLspServerLifecycle implements LspServerLifecycle {
@Override
public LspServerHandle bootServer(final LspServerBootRequest request) {
throw new IllegalStateException("boom");
}
@Override
public void shutdownServer(final LspProjectContext project) {
}
}
}

View File

@ -2,6 +2,9 @@ package p.studio.projectsessions;
import org.junit.jupiter.api.Test;
import p.studio.execution.StudioExecutionSessionService;
import p.studio.lsp.api.LspProjectContext;
import p.studio.lsp.api.LspServerHandle;
import p.studio.lsp.api.LspServerLifecycle;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projectstate.ProjectLocalStudioStateService;
@ -21,6 +24,8 @@ final class StudioProjectSessionTest {
stateService,
ProjectLocalStudioSetup.defaults(),
new StudioExecutionSessionService(),
null,
null,
ProjectLocalStudioState.defaults());
session.close();
@ -38,6 +43,8 @@ final class StudioProjectSessionTest {
stateService,
ProjectLocalStudioSetup.defaults(),
new StudioExecutionSessionService(),
null,
null,
ProjectLocalStudioState.defaults());
final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults()
.withOpenShellState(new ProjectLocalStudioState.OpenShellState("ASSETS"));
@ -48,6 +55,29 @@ final class StudioProjectSessionTest {
assertSame(nextState, stateService.savedState);
}
@Test
void closeShutsDownLspServerOnlyOnce() {
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
final RecordingLspServerLifecycle lspServerLifecycle = new RecordingLspServerLifecycle();
final LspServerHandle lspServerHandle = new LspServerHandle(
new LspProjectContext("demo", "pbs", Path.of("/tmp/example")),
new p.studio.lsp.api.LspServerEndpoint("127.0.0.1", 7777));
final StudioProjectSession session = new StudioProjectSession(
projectReference(),
stateService,
ProjectLocalStudioSetup.defaults(),
new StudioExecutionSessionService(),
lspServerLifecycle,
lspServerHandle,
ProjectLocalStudioState.defaults());
session.close();
session.close();
assertEquals(1, lspServerLifecycle.shutdownCalls);
assertEquals(lspServerHandle.project(), lspServerLifecycle.lastProject);
}
private ProjectReference projectReference() {
return new ProjectReference("Example", "1.0.0", "pbs", 1, Path.of("/tmp/example"));
}
@ -64,4 +94,20 @@ final class StudioProjectSessionTest {
savedState = state;
}
}
private static final class RecordingLspServerLifecycle implements LspServerLifecycle {
private int shutdownCalls;
private LspProjectContext lastProject;
@Override
public LspServerHandle bootServer(final p.studio.lsp.api.LspServerBootRequest request) {
throw new UnsupportedOperationException();
}
@Override
public void shutdownServer(final LspProjectContext project) {
shutdownCalls++;
lastProject = project;
}
}
}