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

View File

@ -5,6 +5,8 @@ plugins {
dependencies { dependencies {
implementation(project(":prometeu-infra")) 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-api"))
implementation(project(":prometeu-packer:prometeu-packer-v1")) implementation(project(":prometeu-packer:prometeu-packer-v1"))
implementation(project(":prometeu-studio")) implementation(project(":prometeu-studio"))

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
package p.studio.projectsessions; package p.studio.projectsessions;
import p.studio.execution.StudioExecutionSessionService; 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.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projectstate.ProjectLocalStudioStateService; import p.studio.projectstate.ProjectLocalStudioStateService;
@ -13,6 +16,8 @@ public final class StudioProjectSession implements AutoCloseable {
private final ProjectLocalStudioStateService projectLocalStudioStateService; private final ProjectLocalStudioStateService projectLocalStudioStateService;
private final ProjectLocalStudioSetup projectLocalStudioSetup; private final ProjectLocalStudioSetup projectLocalStudioSetup;
private final StudioExecutionSessionService executionSessionService; private final StudioExecutionSessionService executionSessionService;
private final LspServerLifecycle lspServerLifecycle;
private final LspServerHandle lspServerHandle;
private ProjectLocalStudioState projectLocalStudioState; private ProjectLocalStudioState projectLocalStudioState;
private boolean closed; private boolean closed;
@ -23,6 +28,8 @@ public final class StudioProjectSession implements AutoCloseable {
new ProjectLocalStudioStateService(), new ProjectLocalStudioStateService(),
ProjectLocalStudioSetup.defaults(), ProjectLocalStudioSetup.defaults(),
new StudioExecutionSessionService(), new StudioExecutionSessionService(),
null,
null,
ProjectLocalStudioState.defaults()); ProjectLocalStudioState.defaults());
} }
@ -31,11 +38,15 @@ public final class StudioProjectSession implements AutoCloseable {
final ProjectLocalStudioStateService projectLocalStudioStateService, final ProjectLocalStudioStateService projectLocalStudioStateService,
final ProjectLocalStudioSetup projectLocalStudioSetup, final ProjectLocalStudioSetup projectLocalStudioSetup,
final StudioExecutionSessionService executionSessionService, final StudioExecutionSessionService executionSessionService,
final LspServerLifecycle lspServerLifecycle,
final LspServerHandle lspServerHandle,
final ProjectLocalStudioState projectLocalStudioState) { final ProjectLocalStudioState projectLocalStudioState) {
this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService"); this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup"); this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup");
this.executionSessionService = Objects.requireNonNull(executionSessionService, "executionSessionService"); this.executionSessionService = Objects.requireNonNull(executionSessionService, "executionSessionService");
this.lspServerLifecycle = lspServerLifecycle;
this.lspServerHandle = lspServerHandle;
this.projectLocalStudioState = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState"); this.projectLocalStudioState = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState");
} }
@ -55,6 +66,10 @@ public final class StudioProjectSession implements AutoCloseable {
return executionSessionService; return executionSessionService;
} }
public LspServerHandle lspServerHandle() {
return lspServerHandle;
}
public void replaceProjectLocalStudioState(final ProjectLocalStudioState nextProjectLocalStudioState) { public void replaceProjectLocalStudioState(final ProjectLocalStudioState nextProjectLocalStudioState) {
this.projectLocalStudioState = Objects.requireNonNull(nextProjectLocalStudioState, "nextProjectLocalStudioState"); this.projectLocalStudioState = Objects.requireNonNull(nextProjectLocalStudioState, "nextProjectLocalStudioState");
} }
@ -76,8 +91,12 @@ public final class StudioProjectSession implements AutoCloseable {
failure = runtimeException; failure = runtimeException;
} }
try { try {
if (failure != null) { shutdownLspServer();
throw failure; } catch (RuntimeException runtimeException) {
if (failure == null) {
failure = runtimeException;
} else {
failure.addSuppressed(runtimeException);
} }
} finally { } finally {
if (failure != null) { 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; 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.execution.StudioExecutionSessionService;
import p.studio.projectstate.ProjectLocalStudioSetupService; import p.studio.projectstate.ProjectLocalStudioSetupService;
import p.studio.projectstate.ProjectLocalStudioStateService; import p.studio.projectstate.ProjectLocalStudioStateService;
@ -10,27 +14,44 @@ import java.util.Objects;
public final class StudioProjectSessionFactory { public final class StudioProjectSessionFactory {
private final ProjectLocalStudioStateService projectLocalStudioStateService; private final ProjectLocalStudioStateService projectLocalStudioStateService;
private final ProjectLocalStudioSetupService projectLocalStudioSetupService; private final ProjectLocalStudioSetupService projectLocalStudioSetupService;
private final LspServerLifecycle lspServerLifecycle;
public StudioProjectSessionFactory() { public StudioProjectSessionFactory(final LspServerLifecycle lspServerLifecycle) {
this( this(
new ProjectLocalStudioStateService(), new ProjectLocalStudioStateService(),
new ProjectLocalStudioSetupService()); new ProjectLocalStudioSetupService(),
Objects.requireNonNull(lspServerLifecycle, "lspServerLifecycle"));
} }
StudioProjectSessionFactory( StudioProjectSessionFactory(
final ProjectLocalStudioStateService projectLocalStudioStateService, final ProjectLocalStudioStateService projectLocalStudioStateService,
final ProjectLocalStudioSetupService projectLocalStudioSetupService) { final ProjectLocalStudioSetupService projectLocalStudioSetupService,
final LspServerLifecycle lspServerLifecycle) {
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService"); this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService"); this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService");
this.lspServerLifecycle = Objects.requireNonNull(lspServerLifecycle, "lspServerLifecycle");
} }
public StudioProjectSession open(final ProjectReference projectReference) { public StudioProjectSession open(final ProjectReference projectReference) {
final ProjectReference target = Objects.requireNonNull(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( return new StudioProjectSession(
target, target,
projectLocalStudioStateService, projectLocalStudioStateService,
projectLocalStudioSetupService.load(target), setup,
new StudioExecutionSessionService(), 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.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot());
this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService); this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService);
this.windowStateService = new WindowStateService(resolveWindowStateStorage()); this.windowStateService = new WindowStateService(resolveWindowStateStorage());
this.projectSessionFactory = new StudioProjectSessionFactory(); this.projectSessionFactory = new StudioProjectSessionFactory(Container.lspServerLifecycle());
this.launcherView = new ProjectLauncherView( this.launcherView = new ProjectLauncherView(
knownProjectsService, knownProjectsService,
projectCatalogService, projectCatalogService,

View File

@ -1,6 +1,10 @@
package p.studio.projectsessions; package p.studio.projectsessions;
import org.junit.jupiter.api.Test; 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.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioSetupService; import p.studio.projectstate.ProjectLocalStudioSetupService;
import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioState;
@ -10,16 +14,20 @@ import p.studio.projects.ProjectReference;
import java.nio.file.Path; import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
final class StudioProjectSessionFactoryTest { final class StudioProjectSessionFactoryTest {
@Test @Test
void openCreatesProjectSessionWithoutLegacyEditorServices() { void openCreatesProjectSessionAndBootsLspServer() {
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService(); final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService(); final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService();
final RecordingLspServerLifecycle lspServerLifecycle = new RecordingLspServerLifecycle();
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory( final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(
stateService, stateService,
setupService); setupService,
lspServerLifecycle);
final ProjectReference projectReference = new ProjectReference( final ProjectReference projectReference = new ProjectReference(
"Example", "Example",
"1.0.0", "1.0.0",
@ -35,6 +43,29 @@ final class StudioProjectSessionFactoryTest {
assertEquals("IDLE", session.executionSession().snapshot().state().name()); assertEquals("IDLE", session.executionSession().snapshot().state().name());
assertSame(projectReference, stateService.loadedProjectReference); assertSame(projectReference, stateService.loadedProjectReference);
assertSame(projectReference, setupService.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 { private static final class RecordingProjectLocalStudioStateService extends ProjectLocalStudioStateService {
@ -63,4 +94,33 @@ final class StudioProjectSessionFactoryTest {
return loadedSetup; 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 org.junit.jupiter.api.Test;
import p.studio.execution.StudioExecutionSessionService; 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.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projectstate.ProjectLocalStudioStateService; import p.studio.projectstate.ProjectLocalStudioStateService;
@ -21,6 +24,8 @@ final class StudioProjectSessionTest {
stateService, stateService,
ProjectLocalStudioSetup.defaults(), ProjectLocalStudioSetup.defaults(),
new StudioExecutionSessionService(), new StudioExecutionSessionService(),
null,
null,
ProjectLocalStudioState.defaults()); ProjectLocalStudioState.defaults());
session.close(); session.close();
@ -38,6 +43,8 @@ final class StudioProjectSessionTest {
stateService, stateService,
ProjectLocalStudioSetup.defaults(), ProjectLocalStudioSetup.defaults(),
new StudioExecutionSessionService(), new StudioExecutionSessionService(),
null,
null,
ProjectLocalStudioState.defaults()); ProjectLocalStudioState.defaults());
final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults() final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults()
.withOpenShellState(new ProjectLocalStudioState.OpenShellState("ASSETS")); .withOpenShellState(new ProjectLocalStudioState.OpenShellState("ASSETS"));
@ -48,6 +55,29 @@ final class StudioProjectSessionTest {
assertSame(nextState, stateService.savedState); 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() { private ProjectReference projectReference() {
return new ProjectReference("Example", "1.0.0", "pbs", 1, Path.of("/tmp/example")); return new ProjectReference("Example", "1.0.0", "pbs", 1, Path.of("/tmp/example"));
} }
@ -64,4 +94,20 @@ final class StudioProjectSessionTest {
savedState = state; 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;
}
}
} }