diff --git a/discussion/index.ndjson b/discussion/index.ndjson index a54cea18..105c34f9 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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"}]} diff --git a/discussion/workflow/plans/PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md b/discussion/workflow/plans/PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md index c6144269..14fa4f60 100644 --- a/discussion/workflow/plans/PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md +++ b/discussion/workflow/plans/PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md @@ -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 diff --git a/prometeu-app/build.gradle.kts b/prometeu-app/build.gradle.kts index 59d3d29b..92e953ee 100644 --- a/prometeu-app/build.gradle.kts +++ b/prometeu-app/build.gradle.kts @@ -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")) diff --git a/prometeu-app/src/main/java/p/studio/AppContainer.java b/prometeu-app/src/main/java/p/studio/AppContainer.java index ec86a4b9..8bcaeb8f 100644 --- a/prometeu-app/src/main/java/p/studio/AppContainer.java +++ b/prometeu-app/src/main/java/p/studio/AppContainer.java @@ -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(); diff --git a/prometeu-studio/build.gradle.kts b/prometeu-studio/build.gradle.kts index 57bf98cc..93d2ea90 100644 --- a/prometeu-studio/build.gradle.kts +++ b/prometeu-studio/build.gradle.kts @@ -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")) diff --git a/prometeu-studio/src/main/java/p/studio/Container.java b/prometeu-studio/src/main/java/p/studio/Container.java index 2e9cae4e..8af847e8 100644 --- a/prometeu-studio/src/main/java/p/studio/Container.java +++ b/prometeu-studio/src/main/java/p/studio/Container.java @@ -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) { diff --git a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java index af36c5c7..3e2b08b5 100644 --- a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java +++ b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java @@ -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); + } } diff --git a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java index 2dde9464..5258a7d3 100644 --- a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java +++ b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java @@ -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()); } } diff --git a/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java b/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java index b0be78b2..be2b6321 100644 --- a/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java +++ b/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java @@ -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, diff --git a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java index 62cf61f5..6b2aa5fc 100644 --- a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java +++ b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java @@ -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) { + } + } } diff --git a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java index 87eca3bb..f9a8eb8b 100644 --- a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java +++ b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java @@ -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; + } + } }