diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 28aa88dd..ee59b7d3 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -10,4 +10,4 @@ {"type":"discussion","id":"DSC-0009","status":"open","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["studio","debugger","workspace","integration","shell"],"agendas":[{"id":"AGD-0009","file":"AGD-0009-studio-debugger-workspace-integration.md","status":"open","created_at":"2026-03-30","updated_at":"2026-03-30"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]} {"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} -{"type":"discussion","id":"DSC-0012","status":"open","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[{"id":"AGD-0012","file":"AGD-0012-studio-editor-document-vfs-boundary.md","status":"in_progress","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0009","file":"DEC-0009-studio-prometeu-vfs-project-document-boundary.md","status":"in_progress","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0012"}],"plans":[{"id":"PLN-0015","file":"PLN-0015-propagate-dec-0009-into-studio-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0016","file":"PLN-0016-build-prometeu-vfs-filesystem-backed-core.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0017","file":"PLN-0017-add-studio-project-session-ownership-for-prometeu-vfs.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0018","file":"PLN-0018-migrate-code-editor-to-prometeu-vfs.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0012","status":"open","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[{"id":"AGD-0012","file":"AGD-0012-studio-editor-document-vfs-boundary.md","status":"in_progress","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0009","file":"DEC-0009-studio-prometeu-vfs-project-document-boundary.md","status":"in_progress","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0012"}],"plans":[{"id":"PLN-0015","file":"PLN-0015-propagate-dec-0009-into-studio-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0016","file":"PLN-0016-build-prometeu-vfs-filesystem-backed-core.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0017","file":"PLN-0017-add-studio-project-session-ownership-for-prometeu-vfs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0018","file":"PLN-0018-migrate-code-editor-to-prometeu-vfs.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]}],"lessons":[]} diff --git a/discussion/workflow/plans/PLN-0017-add-studio-project-session-ownership-for-prometeu-vfs.md b/discussion/workflow/plans/PLN-0017-add-studio-project-session-ownership-for-prometeu-vfs.md index 3b7183ea..abb7d75f 100644 --- a/discussion/workflow/plans/PLN-0017-add-studio-project-session-ownership-for-prometeu-vfs.md +++ b/discussion/workflow/plans/PLN-0017-add-studio-project-session-ownership-for-prometeu-vfs.md @@ -2,9 +2,9 @@ id: PLN-0017 ticket: studio-editor-document-vfs-boundary title: Add Studio project-session ownership for `prometeu-vfs` -status: review +status: done created: 2026-03-31 -completed: +completed: 2026-03-31 tags: - studio - vfs diff --git a/prometeu-app/src/main/java/p/studio/AppContainer.java b/prometeu-app/src/main/java/p/studio/AppContainer.java index df5449bd..9afa8619 100644 --- a/prometeu-app/src/main/java/p/studio/AppContainer.java +++ b/prometeu-app/src/main/java/p/studio/AppContainer.java @@ -6,6 +6,8 @@ import p.studio.events.StudioEventBus; import p.studio.events.StudioPackerEventAdapter; import p.studio.utilities.ThemeService; import p.studio.utilities.i18n.I18nService; +import p.studio.vfs.FilesystemProjectDocumentVfsFactory; +import p.studio.vfs.ProjectDocumentVfsFactory; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -17,6 +19,7 @@ public final class AppContainer implements Container { private final ThemeService themeService; private final StudioEventBus studioEventBus; private final ObjectMapper mapper; + private final ProjectDocumentVfsFactory projectDocumentVfsFactory; private final EmbeddedPacker embeddedPacker; private final StudioBackgroundTasks backgroundTasks; @@ -25,6 +28,7 @@ public final class AppContainer implements Container { this.themeService = new ThemeService(); this.studioEventBus = new StudioEventBus(); this.mapper = new ObjectMapper(); + this.projectDocumentVfsFactory = new FilesystemProjectDocumentVfsFactory(); final ExecutorService backgroundExecutor = Executors.newFixedThreadPool(2, new StudioWorkerThreadFactory()); this.backgroundTasks = new StudioBackgroundTasks(backgroundExecutor); final Packer packer = Packer.bootstrap(this.mapper, new StudioPackerEventAdapter(studioEventBus)); @@ -51,6 +55,11 @@ public final class AppContainer implements Container { return mapper; } + @Override + public ProjectDocumentVfsFactory getProjectDocumentVfsFactory() { + return projectDocumentVfsFactory; + } + @Override public EmbeddedPacker getPacker() { return embeddedPacker; diff --git a/prometeu-studio/src/main/java/p/studio/Container.java b/prometeu-studio/src/main/java/p/studio/Container.java index 1535264d..0155c001 100644 --- a/prometeu-studio/src/main/java/p/studio/Container.java +++ b/prometeu-studio/src/main/java/p/studio/Container.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import p.studio.events.StudioEventBus; import p.studio.utilities.ThemeService; import p.studio.utilities.i18n.I18nService; +import p.studio.vfs.ProjectDocumentVfsFactory; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; @@ -17,6 +18,8 @@ public interface Container { ObjectMapper getMapper(); + ProjectDocumentVfsFactory getProjectDocumentVfsFactory(); + EmbeddedPacker getPacker(); StudioBackgroundTasks getBackgroundTasks(); @@ -60,6 +63,10 @@ public interface Container { return current().getMapper(); } + static ProjectDocumentVfsFactory projectDocumentVfsFactory() { + return current().getProjectDocumentVfsFactory(); + } + static EmbeddedPacker packer() { return current().getPacker(); } diff --git a/prometeu-studio/src/main/java/p/studio/projects/ProjectReference.java b/prometeu-studio/src/main/java/p/studio/projects/ProjectReference.java index 20e7fb84..c56e9b48 100644 --- a/prometeu-studio/src/main/java/p/studio/projects/ProjectReference.java +++ b/prometeu-studio/src/main/java/p/studio/projects/ProjectReference.java @@ -2,6 +2,7 @@ package p.studio.projects; import org.apache.commons.lang3.StringUtils; import p.packer.messages.PackerProjectContext; +import p.studio.vfs.VfsProjectContext; import java.nio.file.Path; @@ -35,4 +36,8 @@ public record ProjectReference( public PackerProjectContext toPackerProjectContext() { return new PackerProjectContext(name, absoluteRootPath()); } + + public VfsProjectContext toVfsProjectContext() { + return new VfsProjectContext(name, languageId, absoluteRootPath()); + } } diff --git a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java new file mode 100644 index 00000000..0bfbea1f --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java @@ -0,0 +1,36 @@ +package p.studio.projectsessions; + +import p.studio.projects.ProjectReference; +import p.studio.vfs.ProjectDocumentVfs; + +import java.util.Objects; + +public final class StudioProjectSession implements AutoCloseable { + private final ProjectReference projectReference; + private final ProjectDocumentVfs projectDocumentVfs; + private boolean closed; + + public StudioProjectSession( + final ProjectReference projectReference, + final ProjectDocumentVfs projectDocumentVfs) { + this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); + this.projectDocumentVfs = Objects.requireNonNull(projectDocumentVfs, "projectDocumentVfs"); + } + + public ProjectReference projectReference() { + return projectReference; + } + + public ProjectDocumentVfs projectDocumentVfs() { + return projectDocumentVfs; + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + projectDocumentVfs.close(); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java new file mode 100644 index 00000000..9b4820c1 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java @@ -0,0 +1,21 @@ +package p.studio.projectsessions; + +import p.studio.projects.ProjectReference; +import p.studio.vfs.ProjectDocumentVfsFactory; + +import java.util.Objects; + +public final class StudioProjectSessionFactory { + private final ProjectDocumentVfsFactory projectDocumentVfsFactory; + + public StudioProjectSessionFactory(final ProjectDocumentVfsFactory projectDocumentVfsFactory) { + this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory"); + } + + public StudioProjectSession open(final ProjectReference projectReference) { + final ProjectReference target = Objects.requireNonNull(projectReference, "projectReference"); + return new StudioProjectSession( + target, + projectDocumentVfsFactory.open(target.toVfsProjectContext())); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/window/MainView.java b/prometeu-studio/src/main/java/p/studio/window/MainView.java index 3fbe4aad..342053a4 100644 --- a/prometeu-studio/src/main/java/p/studio/window/MainView.java +++ b/prometeu-studio/src/main/java/p/studio/window/MainView.java @@ -5,6 +5,7 @@ import p.studio.Container; import p.studio.controls.shell.*; import p.studio.events.StudioWorkspaceSelectedEvent; import p.studio.projects.ProjectReference; +import p.studio.projectsessions.StudioProjectSession; import p.studio.utilities.i18n.I18n; import p.studio.workspaces.WorkspaceHost; import p.studio.workspaces.WorkspaceId; @@ -17,9 +18,11 @@ import java.util.List; public final class MainView extends BorderPane { private final WorkspaceHost host = new WorkspaceHost(); private final ProjectReference projectReference; + private final StudioProjectSession projectSession; - public MainView(final ProjectReference projectReference) { - this.projectReference = projectReference; + public MainView(final StudioProjectSession projectSession) { + this.projectSession = projectSession; + this.projectReference = projectSession.projectReference(); final var menuBar = new StudioShellMenuBarControl(); final var runSurface = new StudioRunSurfaceControl(); setTop(new StudioShellTopBarControl(menuBar)); @@ -53,6 +56,10 @@ public final class MainView extends BorderPane { return projectReference; } + public StudioProjectSession projectSession() { + return projectSession; + } + private void loadWorkspace(WorkspaceId workspaceId) { host.change(workspaceId); Container.eventBus().publish(new StudioWorkspaceSelectedEvent(workspaceId)); 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 937db163..027236dc 100644 --- a/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java +++ b/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java @@ -10,6 +10,8 @@ import p.studio.events.*; import p.studio.projects.KnownProjectsService; import p.studio.projects.ProjectCatalogService; import p.studio.projects.ProjectReference; +import p.studio.projectsessions.StudioProjectSession; +import p.studio.projectsessions.StudioProjectSessionFactory; import p.studio.utilities.i18n.I18n; import java.nio.file.Path; @@ -28,12 +30,14 @@ public final class StudioWindowCoordinator { private final KnownProjectsService knownProjectsService; private final WindowStateService windowStateService; private final ProjectLauncherView launcherView; + private final StudioProjectSessionFactory projectSessionFactory; public StudioWindowCoordinator(Stage launcherStage) { this.launcherStage = Objects.requireNonNull(launcherStage, "launcherStage"); this.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot()); this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService); this.windowStateService = new WindowStateService(resolveWindowStateStorage()); + this.projectSessionFactory = new StudioProjectSessionFactory(Container.projectDocumentVfsFactory()); this.launcherView = new ProjectLauncherView( knownProjectsService, projectCatalogService, @@ -111,6 +115,7 @@ public final class StudioWindowCoordinator { } private void initializeProjectAndOpenWindow(ProjectReference projectReference, Stage loadingStage) { + StudioProjectSession projectSession = null; try { Container.eventBus().publish(new StudioProjectLoadingProgressEvent( StudioProjectLoadingPhase.INITIALIZING_SERVICES, @@ -125,21 +130,28 @@ public final class StudioWindowCoordinator { Container.i18n().text(I18n.SHIELD_STATUS_RESTORING), Progress.RESTORING_WORKSPACES, false)); - Platform.runLater(() -> finishProjectOpen(projectReference, loadingStage)); + projectSession = projectSessionFactory.open(projectReference); + final StudioProjectSession readySession = projectSession; + Platform.runLater(() -> finishProjectOpen(readySession, loadingStage)); } catch (RuntimeException exception) { + if (projectSession != null) { + projectSession.close(); + } Platform.runLater(() -> failProjectOpen(projectReference, loadingStage, exception)); } } - private void finishProjectOpen(ProjectReference projectReference, Stage loadingStage) { + private void finishProjectOpen(StudioProjectSession projectSession, Stage loadingStage) { + final ProjectReference projectReference = projectSession.projectReference(); final Stage projectStage = new Stage(); - final Scene scene = new Scene(new MainView(projectReference), PROJECT_WIDTH, PROJECT_HEIGHT); + final Scene scene = new Scene(new MainView(projectSession), PROJECT_WIDTH, PROJECT_HEIGHT); scene.getStylesheets().add(Container.theme().getDefaultTheme()); projectStage.setTitle(Container.i18n().format(I18n.APP_PROJECT_TITLE, projectReference.name())); projectStage.setScene(scene); windowStateService.installProjectShellState(projectStage, projectReference, PROJECT_WIDTH, PROJECT_HEIGHT); projectStage.setOnHidden(ignored -> { + projectSession.close(); launcherView.reloadProjects(); launcherStage.show(); launcherStage.centerOnScreen(); diff --git a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java new file mode 100644 index 00000000..7d0bb181 --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java @@ -0,0 +1,75 @@ +package p.studio.projectsessions; + +import org.junit.jupiter.api.Test; +import p.studio.projects.ProjectReference; +import p.studio.vfs.ProjectDocumentVfs; +import p.studio.vfs.ProjectDocumentVfsFactory; +import p.studio.vfs.VfsDocumentOpenResult; +import p.studio.vfs.VfsProjectContext; +import p.studio.vfs.VfsProjectSnapshot; +import p.studio.vfs.VfsRefreshRequest; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +final class StudioProjectSessionFactoryTest { + @Test + void openCreatesProjectSessionBackedByProjectScopedVfs() { + final RecordingVfsFactory vfsFactory = new RecordingVfsFactory(); + final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(vfsFactory); + final ProjectReference projectReference = new ProjectReference( + "Example", + "1.0.0", + "pbs", + 1, + Path.of("/tmp/example")); + + final StudioProjectSession session = sessionFactory.open(projectReference); + + assertSame(projectReference, session.projectReference()); + assertSame(vfsFactory.vfs, session.projectDocumentVfs()); + assertEquals("Example", vfsFactory.capturedContext.projectName()); + assertEquals("pbs", vfsFactory.capturedContext.languageId()); + assertEquals(projectReference.rootPath().toAbsolutePath().normalize(), vfsFactory.capturedContext.rootPath()); + } + + private static final class RecordingVfsFactory implements ProjectDocumentVfsFactory { + private VfsProjectContext capturedContext; + private final ProjectDocumentVfs vfs = new NoOpProjectDocumentVfs(); + + @Override + public ProjectDocumentVfs open(VfsProjectContext projectContext) { + this.capturedContext = projectContext; + return vfs; + } + } + + private static final class NoOpProjectDocumentVfs implements ProjectDocumentVfs { + @Override + public VfsProjectContext projectContext() { + throw new UnsupportedOperationException(); + } + + @Override + public VfsProjectSnapshot snapshot() { + throw new UnsupportedOperationException(); + } + + @Override + public VfsProjectSnapshot refresh() { + throw new UnsupportedOperationException(); + } + + @Override + public VfsProjectSnapshot refresh(VfsRefreshRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public VfsDocumentOpenResult openDocument(Path path) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java new file mode 100644 index 00000000..b8d7165f --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java @@ -0,0 +1,64 @@ +package p.studio.projectsessions; + +import org.junit.jupiter.api.Test; +import p.studio.projects.ProjectReference; +import p.studio.vfs.ProjectDocumentVfs; +import p.studio.vfs.VfsDocumentOpenResult; +import p.studio.vfs.VfsProjectContext; +import p.studio.vfs.VfsProjectSnapshot; +import p.studio.vfs.VfsRefreshRequest; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class StudioProjectSessionTest { + @Test + void closeDelegatesToUnderlyingVfsOnlyOnce() { + final CountingProjectDocumentVfs vfs = new CountingProjectDocumentVfs(); + final StudioProjectSession session = new StudioProjectSession(projectReference(), vfs); + + session.close(); + session.close(); + + assertEquals(1, vfs.closeCalls); + } + + private ProjectReference projectReference() { + return new ProjectReference("Example", "1.0.0", "pbs", 1, Path.of("/tmp/example")); + } + + private static final class CountingProjectDocumentVfs implements ProjectDocumentVfs { + private int closeCalls; + + @Override + public VfsProjectContext projectContext() { + throw new UnsupportedOperationException(); + } + + @Override + public VfsProjectSnapshot snapshot() { + throw new UnsupportedOperationException(); + } + + @Override + public VfsProjectSnapshot refresh() { + throw new UnsupportedOperationException(); + } + + @Override + public VfsProjectSnapshot refresh(VfsRefreshRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public VfsDocumentOpenResult openDocument(Path path) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + closeCalls++; + } + } +}