studio session persistent
This commit is contained in:
parent
d1722b23a3
commit
a824b2185f
@ -1,4 +1,4 @@
|
|||||||
{"type":"meta","next_id":{"DSC":18,"AGD":19,"DEC":16,"PLN":37,"LSN":31,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":19,"AGD":20,"DEC":17,"PLN":38,"LSN":31,"CLSN":1}}
|
||||||
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
||||||
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
||||||
@ -16,3 +16,4 @@
|
|||||||
{"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]}
|
{"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]}
|
||||||
{"type":"discussion","id":"DSC-0016","status":"open","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-studio-editor-scope-guides-and-brace-anchoring.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0014","file":"DEC-0014-studio-editor-active-scope-and-structural-anchors.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"plans":[{"id":"PLN-0030","file":"PLN-0030-studio-active-container-and-active-scope-gutter-wave-1.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0031","file":"PLN-0031-studio-structural-anchor-semantic-surface-specification.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0032","file":"PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}],"lessons":[]}
|
{"type":"discussion","id":"DSC-0016","status":"open","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-studio-editor-scope-guides-and-brace-anchoring.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0014","file":"DEC-0014-studio-editor-active-scope-and-structural-anchors.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"plans":[{"id":"PLN-0030","file":"PLN-0030-studio-active-container-and-active-scope-gutter-wave-1.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0031","file":"PLN-0031-studio-structural-anchor-semantic-surface-specification.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0032","file":"PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0017","status":"open","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-studio-editor-inline-type-hints-for-let-bindings.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0015","file":"DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03","ref_agenda":"AGD-0018"}],"plans":[{"id":"PLN-0033","file":"PLN-0033-inline-hint-spec-and-contract-propagation.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0034","file":"PLN-0034-lsp-inline-hint-transport-contract.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0035","file":"PLN-0035-pbs-inline-type-hint-payload-production.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0036","file":"PLN-0036-studio-inline-hint-rendering-and-rollout.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]}],"lessons":[]}
|
{"type":"discussion","id":"DSC-0017","status":"open","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-studio-editor-inline-type-hints-for-let-bindings.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0015","file":"DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03","ref_agenda":"AGD-0018"}],"plans":[{"id":"PLN-0033","file":"PLN-0033-inline-hint-spec-and-contract-propagation.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0034","file":"PLN-0034-lsp-inline-hint-transport-contract.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0035","file":"PLN-0035-pbs-inline-type-hint-payload-production.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0036","file":"PLN-0036-studio-inline-hint-rendering-and-rollout.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]}],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0018","status":"open","ticket":"studio-project-local-studio-state-under-dot-studio","title":"Persist project-local Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","shell","layout","setup"],"agendas":[{"id":"AGD-0019","file":"AGD-0019-studio-project-open-state-under-dot-studio.md","status":"accepted","created_at":"2026-04-04","updated_at":"2026-04-04"}],"decisions":[{"id":"DEC-0016","file":"DEC-0016-project-local-studio-state-under-dot-studio.md","status":"accepted","created_at":"2026-04-04","updated_at":"2026-04-04","ref_agenda":"AGD-0019"}],"plans":[{"id":"PLN-0037","file":"PLN-0037-project-local-studio-state-store-and-restoration.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04","ref_decisions":["DEC-0016"]}],"lessons":[]}
|
||||||
|
|||||||
@ -0,0 +1,213 @@
|
|||||||
|
---
|
||||||
|
id: AGD-0019
|
||||||
|
ticket: studio-project-local-studio-state-under-dot-studio
|
||||||
|
title: Persist project-local Studio state under .studio
|
||||||
|
status: accepted
|
||||||
|
created: 2026-04-04
|
||||||
|
resolved: 2026-04-04
|
||||||
|
decision: DEC-0016
|
||||||
|
tags: [studio, project-session, project-state, persistence, dot-studio, shell, layout, setup]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pain
|
||||||
|
|
||||||
|
O Studio ja tem um modelo de `project session` para runtime do projeto aberto, mas ainda nao tem um contrato explicito para estado persistente do proprio Studio por projeto.
|
||||||
|
|
||||||
|
Sem esse contrato, a organizacao e o setup do Studio por projeto correm tres riscos:
|
||||||
|
|
||||||
|
- virar estado apenas em memoria e se perder toda vez que o Studio fecha;
|
||||||
|
- ser espalhado em arquivos ad hoc, sem uma raiz reservada e sem schema claro;
|
||||||
|
- misturar configuracao local do projeto com estado global do launcher, quebrando o boundary entre "este projeto no Studio" e "preferencias gerais da aplicacao".
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Domain owner: `studio`
|
||||||
|
Owner surfaces: `docs/specs/studio/1. Studio Shell and Workspace Layout Specification.md`
|
||||||
|
Related context: `discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md`, `discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md`
|
||||||
|
Relevant code: `prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java`
|
||||||
|
|
||||||
|
Ja existe precedente documental para duas direcoes importantes:
|
||||||
|
|
||||||
|
- `.studio/` e a raiz reservada para estado local ao projeto;
|
||||||
|
- estado que precisa sobreviver a troca de workspace, mas continuar pertencendo ao projeto aberto, deve ser owned por `project session`.
|
||||||
|
|
||||||
|
O que ainda nao esta fechado e o contrato desta nova camada de estado persistente do Studio por projeto:
|
||||||
|
|
||||||
|
- quais dados entram nela;
|
||||||
|
- quais dados devem continuar apenas em memoria;
|
||||||
|
- o que e projeto-local versus launcher-global;
|
||||||
|
- como versionar, carregar, gravar e tolerar corrupcao parcial;
|
||||||
|
- se o modelo deve ser um store unico do projeto ou varios arquivos especializados sob `.studio/`.
|
||||||
|
|
||||||
|
Exemplos concretos do escopo pretendido agora:
|
||||||
|
|
||||||
|
- posicoes de divisorias e proporcoes de layout;
|
||||||
|
- quais janelas, paineis ou workspaces estavam abertos;
|
||||||
|
- quais tabs do editor estavam abertas e qual estava ativa;
|
||||||
|
- caminhos de setup local do Studio para o projeto, como runtime do Prometeu;
|
||||||
|
- futuras configuracoes locais do Studio que pertencam a este projeto e nao ao launcher global.
|
||||||
|
|
||||||
|
Categoria relacionada, mas separada deste contrato principal:
|
||||||
|
|
||||||
|
- `project-local activity` pode continuar vivendo sob `.studio/`, mas nao precisa pertencer ao mesmo store canonico de `project session` usado para layout, tabs, janelas e setup.
|
||||||
|
|
||||||
|
Observacao importante:
|
||||||
|
|
||||||
|
- `project-local setup` ja e requisito relevante agora, mesmo antes de existir uma UI dedicada para edita-lo, porque o Studio pode precisar consumir configuracoes locais do projeto antes da superficie visual de configuracao ficar pronta.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
### What to store
|
||||||
|
|
||||||
|
- [ ] Qual deve ser o escopo minimo obrigatorio do estado persistido do Studio por projeto na primeira wave?
|
||||||
|
- [ ] O owner normativo do runtime deve ser `StudioProjectSession` com persistencia sob `.studio/`, ou cada workspace pode persistir diretamente seu proprio estado?
|
||||||
|
- [x] O contrato deve usar um arquivo consolidado de estado do projeto ou uma arvore de arquivos especializados dentro de `.studio/`?
|
||||||
|
Fechado: `single file`. O store principal da `project session` deve usar um arquivo consolidado, por ser mais simples de manter e governar.
|
||||||
|
- [ ] Quais categorias entram desde a wave 1: layout do shell, divisorias, paineis e janelas abertas, tabs abertas, documento ativo, caminhos de setup local, configuracoes por workspace?
|
||||||
|
- [ ] `Project-local setup` deve ser tratado como categoria de primeira classe desde o inicio, mesmo antes de haver UI propria?
|
||||||
|
- [x] A `project-local activity` deve pertencer ao mesmo store de `project session` desta agenda?
|
||||||
|
Fechado: nao. `Project-local activity` pode viver dentro de `.studio/`, mas deve permanecer dissociada do store principal de `project session` tratado nesta agenda.
|
||||||
|
- [ ] Quais categorias devem ser explicitamente proibidas por agora: cache derivado, estado efemero de hover, resultados recomputaveis, output operacional pesado, artefatos operacionais transient?
|
||||||
|
|
||||||
|
### When to save and restore
|
||||||
|
|
||||||
|
- [x] Quando o Studio grava esse estado: sob eventos incrementais, no fechamento da sessao, em pontos de checkpoint, ou combinacao controlada?
|
||||||
|
Fechado: a politica base deve ser gravacao no fechamento; checkpoints sao aceitaveis apenas para categorias que realmente precisarem disso; gravacao incremental como regra geral nao e a direcao.
|
||||||
|
- [x] Quais categorias devem ser restauradas imediatamente ao abrir o projeto e quais devem ser carregadas sob demanda?
|
||||||
|
Fechado: o estado restauravel de shell deve ser reaplicado quando o projeto sobe; no mesmo boundary, quando o shell do projeto desce, o estado correspondente deve ser salvo.
|
||||||
|
- [x] O setup local do projeto deve ser lido logo na abertura da `project session`, antes mesmo de o usuario abrir workspaces especificos?
|
||||||
|
Fechado: sim. O setup local do projeto deve ser lido na abertura da `project session`.
|
||||||
|
- [x] Como o Studio deve reagir a `.studio/` ausente, estado em schema antigo, arquivo invalido ou dados parcialmente corrompidos?
|
||||||
|
Fechado: fallback para valores default. A ausencia de `.studio/`, schema nao reconhecido ou conteudo invalido/corrompido nao deve impedir a abertura do projeto; o Studio deve cair para defaults seguros.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### Option A - Persistencia ad hoc por area do Studio sob `.studio/`
|
||||||
|
- **Approach:** cada area do Studio grava diretamente os arquivos de que precisa sob `.studio/`, por exemplo shell/layout, editor tabs, setup local e outros modulos futuros, sem um owner central de sessao nem um schema compartilhado forte.
|
||||||
|
- **Pro:** destrava persistencia rapido para features isoladas e exige pouco desenho inicial.
|
||||||
|
- **Con:** tende a fragmentar naming, lifecycle, migration e recovery; piora consistencia entre workspaces e dificulta distinguir estado normativo de lixo historico.
|
||||||
|
- **Maintainability:** baixa a media. Facil de iniciar, dificil de governar quando mais workspaces passarem a persistir.
|
||||||
|
- **Trade-offs detalhados:**
|
||||||
|
- reduz custo de entrada para a primeira feature consumidora, mas troca esse ganho por uma futura consolidacao mais cara;
|
||||||
|
- favorece autonomia local de shell e workspaces, mas quebra o boundary de `project session` que o repositorio vem fortalecendo;
|
||||||
|
- simplifica a escrita inicial, mas complica leitura, migracao de schema e tolerancia a falhas, porque cada workspace pode passar a degradar de um jeito diferente;
|
||||||
|
- permite fatiar rollout por workspace, mas aumenta o risco de formatos incompatíveis coexistirem dentro de `.studio/`.
|
||||||
|
|
||||||
|
### Option B - Project-session state store canonico sob `.studio/`
|
||||||
|
- **Approach:** definir um contrato explicito de estado persistente do Studio por projeto, owned pela `StudioProjectSession`, com raiz canonica em `.studio/` e schema/versionamento governados pelo Studio.
|
||||||
|
- **Pro:** preserva o boundary correto entre runtime de sessao, persistencia local do projeto e concerns globais do launcher; facilita recovery, migration e extensao futura.
|
||||||
|
- **Con:** exige desenhar ownership, schema e politica de gravacao antes de implementar a primeira feature consumidora.
|
||||||
|
- **Maintainability:** forte. O Studio ganha uma surface clara para persistencia de layout, tabs, setup local e futuras configuracoes por projeto sem espalhar responsabilidade por cada modulo.
|
||||||
|
- **Trade-offs detalhados:**
|
||||||
|
- aumenta custo de desenho agora, mas reduz custo estrutural das proximas features que precisarem persistir estado por projeto;
|
||||||
|
- centraliza ownership e recovery, o que melhora consistencia, mas exige uma API suficientemente clara para nao virar gargalo ou objeto “god state”;
|
||||||
|
- facilita schema versioning, migration e corrupcao parcial, mas pede disciplina para separar configuracao local canonica de cache derivado;
|
||||||
|
- protege a diferenca entre projeto-local e launcher-global, mas exige que o recorte inicial seja pequeno o bastante para nao bloquear implementacao por excesso de ambicao;
|
||||||
|
- acomoda bem layout, setup e estado restauravel de UI, mas precisa de limites claros para nao começar a absorver qualquer detalhe efemero de sessao.
|
||||||
|
|
||||||
|
### Option C - Manter apenas estado global do launcher e evitar persistencia por projeto nesta fase
|
||||||
|
- **Approach:** limitar a memoria persistente a recent projects e preferencias globais; cada projeto reconstroi layout, setup local e tabs do zero a cada abertura.
|
||||||
|
- **Pro:** menor custo imediato e nenhum contrato novo dentro do projeto.
|
||||||
|
- **Con:** conflita com o objetivo de tornar o projeto aberto uma unidade com estado proprio; degrada continuidade de uso e ignora o precedente ja registrado para `.studio/`.
|
||||||
|
- **Maintainability:** simples no curtissimo prazo, mas funcionalmente insuficiente se o Studio quer evoluir como ambiente de trabalho persistente por projeto.
|
||||||
|
- **Trade-offs detalhados:**
|
||||||
|
- preserva simplicidade operacional agora, mas empurra o problema para depois sem melhorar a arquitetura do projeto aberto;
|
||||||
|
- evita erro de modelagem prematuro, mas tambem impede validar cedo a disciplina de `.studio/` e `project session`;
|
||||||
|
- mantem o launcher como unico ponto persistente, mas mistura setup local do projeto com concerns globais que deveriam permanecer fora do projeto;
|
||||||
|
- reduz risco de schema local mal desenhado, mas aumenta risco de cada nova feature tentar contornar a ausencia de persistencia por atalhos locais.
|
||||||
|
|
||||||
|
## Discussion
|
||||||
|
|
||||||
|
O ponto principal aqui nao e apenas "onde salvar um JSON". O que precisa ser fechado e o boundary arquitetural:
|
||||||
|
|
||||||
|
1. ownership de runtime:
|
||||||
|
quem monta o estado em memoria enquanto o projeto esta aberto;
|
||||||
|
|
||||||
|
2. ownership de persistencia:
|
||||||
|
quem serializa e restaura esse estado sem deixar cada workspace inventar sua propria disciplina;
|
||||||
|
|
||||||
|
3. shape do dado:
|
||||||
|
o que conta como organizacao e setup do Studio por projeto, o que e apenas view state efemero e o que pertence ao launcher global;
|
||||||
|
|
||||||
|
4. resiliencia:
|
||||||
|
como o Studio lida com schema evolution, arquivos ausentes e degradacao parcial sem falhar ao abrir um projeto.
|
||||||
|
|
||||||
|
O precedente atual aponta para uma direcao relativamente forte:
|
||||||
|
|
||||||
|
- `.studio/` deve continuar sendo a raiz reservada do projeto para concerns do Studio;
|
||||||
|
- `StudioProjectSession` ja e o owner natural do estado que vive acima do foco de um workspace;
|
||||||
|
- workspaces devem consumir servicos de sessao, nao redefinir ownership de persistencia por conta propria.
|
||||||
|
- nem todo dado sob `.studio/` precisa cair dentro do mesmo store de `project session`; `activity` e o exemplo claro de uma categoria projeto-local, mas com semantica e politica de persistencia diferentes.
|
||||||
|
- para o store principal desta agenda, `single file` e a direcao preferida, porque simplifica manutencao, leitura, escrita e governanca inicial.
|
||||||
|
|
||||||
|
O risco de ir por um caminho ad hoc e que shell, editor e setup local comecem a salvar layout, tabs e caminhos em formatos independentes e acidentalmente normativos.
|
||||||
|
Depois disso, consolidar ownership, migration e cleanup fica mais caro do que desenhar a superficie certa agora.
|
||||||
|
|
||||||
|
Ao mesmo tempo, uma primeira wave disciplinada nao precisa resolver toda a persistencia futura do Studio.
|
||||||
|
Ela pode fechar:
|
||||||
|
|
||||||
|
- a raiz canonicamente reservada;
|
||||||
|
- o owner do runtime e da serializacao;
|
||||||
|
- as categorias de estado permitidas e proibidas na wave inicial;
|
||||||
|
- o formato geral de schema e versionamento;
|
||||||
|
- a politica de recovery minima para nao travar abertura de projeto.
|
||||||
|
|
||||||
|
Trade-off central da agenda:
|
||||||
|
|
||||||
|
- se priorizarmos velocidade local da primeira feature, Option A parece atraente, mas ela compra essa velocidade quebrando o owner correto;
|
||||||
|
- se priorizarmos architecture hygiene, Option B e a direcao mais consistente, mas ela precisa de um recorte rigoroso para nao tentar resolver persistencia total do Studio numa tacada so;
|
||||||
|
- Option C so faz sentido se o objetivo real nao for ainda ter projeto aberto como unidade persistente, o que conflita com a premissa desta agenda.
|
||||||
|
|
||||||
|
Os trade-offs tambem se separam naturalmente em dois eixos:
|
||||||
|
|
||||||
|
1. `what to store`
|
||||||
|
quanto mais categorias entram na wave 1, maior o valor imediato de restauracao do Studio por projeto;
|
||||||
|
por outro lado, maior o risco de misturar setup canonico com estado efemero ou cache derivado.
|
||||||
|
|
||||||
|
A convergencia atual deste eixo ja sugere uma separacao interna:
|
||||||
|
|
||||||
|
- um store principal de `project session` para layout, setup, janelas e restauracao de tabs;
|
||||||
|
- stores ou artefatos adjacentes sob `.studio/` para categorias com semantica diferente, como `project-local activity`.
|
||||||
|
|
||||||
|
2. `when to save/restore`
|
||||||
|
quanto mais automatico e frequente for o save, menor a perda de estado;
|
||||||
|
por outro lado, maior o risco de churn de escrita, snapshots inconsistentes e acoplamento excessivo a eventos de UI.
|
||||||
|
|
||||||
|
Leituras ja convergidas neste eixo:
|
||||||
|
|
||||||
|
- `project-local setup` deve ser restaurado logo na abertura da `project session`;
|
||||||
|
- o estado restauravel do shell deve subir com o projeto e ser salvo quando esse shell descer;
|
||||||
|
- `save on close` deve ser a politica padrao;
|
||||||
|
- checkpoints podem existir, mas apenas quando alguma categoria realmente exigir persistencia intermediaria.
|
||||||
|
|
||||||
|
Leitura atual dos trade-offs:
|
||||||
|
|
||||||
|
1. o maior risco de produto e nao ter continuidade de organizacao e setup do Studio entre aberturas do projeto;
|
||||||
|
2. `project-local setup` tem prioridade alta porque pode ser consumido mesmo antes da UI de configuracao existir;
|
||||||
|
3. o maior risco arquitetural e permitir que shell e workspaces inventem persistencia independente para layout, tabs e configuracao local;
|
||||||
|
4. o maior risco de execucao em Option B e superdimensionar a primeira wave ou salvar estado demais cedo demais;
|
||||||
|
5. portanto, a melhor convergencia parece ser um store canonico de sessao/projeto com escopo inicial deliberadamente estreito, `single file` no inicio, mas explicitamente capaz de crescer para layout, setup e configuracoes futuras.
|
||||||
|
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
Recommended direction: seguir com **Option B**.
|
||||||
|
|
||||||
|
A agenda deve convergir para uma decision com os seguintes fechamentos:
|
||||||
|
|
||||||
|
1. `.studio/` permanece a raiz canonica de estado local ao projeto para o Studio;
|
||||||
|
2. `StudioProjectSession` ou um servico explicitamente owned por ela deve ser o owner do runtime e da persistencia do estado local do Studio por projeto;
|
||||||
|
3. shell e workspaces devem consumir esse estado por contrato, em vez de cada um persistir arbitrariamente por conta propria;
|
||||||
|
4. a primeira wave deve definir claramente quais categorias entram, com forte prioridade para `project-local setup`, layout restauravel e tabs/janelas abertas;
|
||||||
|
5. o contrato deve prever schema/versionamento e recovery de dados ausentes ou invalidos;
|
||||||
|
6. o boundary entre estado projeto-local e estado launcher-global deve ficar explicito;
|
||||||
|
7. o desenho inicial deve suportar extensao futura para novas categorias de configuracao local sem exigir reorganizacao ad hoc de `.studio/`;
|
||||||
|
8. a decision deve separar explicitamente `what to store` de `when to save/restore`, em vez de misturar conteudo e politica operacional;
|
||||||
|
9. a politica base de persistencia deve ser `read on project-session open`, `restore on shell mount`, `save on shell/project close`, com checkpoints apenas quando justificadamente necessarios;
|
||||||
|
10. `project-local activity` deve ser explicitamente tratada como concern separado: continua sob `.studio/`, mas fora do store principal de `project session`;
|
||||||
|
11. o store principal deve comecar como `single file`;
|
||||||
|
12. ausencia, schema antigo ou conteudo invalido/corrompido devem degradar para valores default seguros, sem bloquear abertura do projeto.
|
||||||
|
|
||||||
|
Next step suggestion: fechar nesta agenda, em separado, `what to store` e `when to save/restore`:
|
||||||
|
|
||||||
|
- `what to store`: pelo menos `project-local setup`, `shell layout`, `open panels/workspaces`, `editor open tabs + active tab`;
|
||||||
|
- `when to save/restore`: leitura do setup ja na abertura da `project session`, restauracao do layout/tabs na entrada do shell, save no fechamento, e checkpoints apenas para categorias que realmente precisarem.
|
||||||
@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
id: DEC-0016
|
||||||
|
ticket: studio-project-local-studio-state-under-dot-studio
|
||||||
|
title: Project-Local Studio State under .studio
|
||||||
|
status: accepted
|
||||||
|
created: 2026-04-04
|
||||||
|
accepted: 2026-04-04
|
||||||
|
agenda: AGD-0019
|
||||||
|
plans: []
|
||||||
|
tags: [studio, project-session, project-state, persistence, dot-studio, shell, layout, setup]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The Studio MUST persist project-local Studio state under `.studio/` in the project root.
|
||||||
|
|
||||||
|
This decision normatively separates three concerns:
|
||||||
|
|
||||||
|
1. project-local Studio state owned by the project and restored with that project;
|
||||||
|
2. launcher-global state that is not owned by any specific project;
|
||||||
|
3. project-local activity artifacts that may live under `.studio/` but are not part of the main project-session state store defined here.
|
||||||
|
|
||||||
|
The main project-local Studio state store MUST be owned by `StudioProjectSession`, or by a persistence service explicitly owned by `StudioProjectSession`.
|
||||||
|
|
||||||
|
Shells and workspaces MUST consume that state through the project-session boundary.
|
||||||
|
They MUST NOT define ad hoc independent persistence contracts for layout, tabs, windows, or project-local setup.
|
||||||
|
|
||||||
|
The main project-session store MUST start as a `single file` under `.studio/`.
|
||||||
|
|
||||||
|
The first accepted scope of that store MUST include:
|
||||||
|
|
||||||
|
- project-local setup required by Studio to operate on the project, including paths such as the Prometeu runtime path;
|
||||||
|
- shell layout state such as splitter positions and layout proportions;
|
||||||
|
- open shell/workspace state such as which panels, windows, or workspaces were open;
|
||||||
|
- editor restoration state including open tabs and the active tab.
|
||||||
|
|
||||||
|
`Project-local setup` is a first-class category from the start, even before a dedicated UI exists to edit it.
|
||||||
|
The Studio MAY consume such setup programmatically before a UI surface exists.
|
||||||
|
|
||||||
|
`Project-local activity` MUST remain outside the main project-session store defined by this decision.
|
||||||
|
It MAY continue to live under `.studio/`, but it SHALL be treated as a separate concern with its own storage contract and persistence policy.
|
||||||
|
|
||||||
|
The main project-session store MUST NOT be used for:
|
||||||
|
|
||||||
|
- derived cache;
|
||||||
|
- ephemeral hover or transient interaction state;
|
||||||
|
- recomputable operational results;
|
||||||
|
- heavy operational output;
|
||||||
|
- activity history.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
The repository already converged on two architectural directions:
|
||||||
|
|
||||||
|
- `.studio/` is the reserved root for project-local Studio concerns;
|
||||||
|
- `StudioProjectSession` is the correct owner for state that must survive workspace focus changes while remaining scoped to one opened project.
|
||||||
|
|
||||||
|
This decision extends those directions into a normative persistence contract.
|
||||||
|
|
||||||
|
The chosen model prevents three failure modes:
|
||||||
|
|
||||||
|
1. launcher-global persistence swallowing project-local setup and organization;
|
||||||
|
2. each shell or workspace inventing its own persistence format under `.studio/`;
|
||||||
|
3. the main session state store absorbing unrelated concerns such as activity history or cache data.
|
||||||
|
|
||||||
|
Starting with a `single file` is intentionally conservative.
|
||||||
|
It reduces governance and maintenance cost for the first wave while preserving the option to evolve the schema later if needed.
|
||||||
|
|
||||||
|
Treating project-local setup as first-class from the start is necessary because Studio runtime behavior may depend on local project configuration before any settings UI is available.
|
||||||
|
|
||||||
|
## Technical Specification
|
||||||
|
|
||||||
|
### 1. Storage Root
|
||||||
|
|
||||||
|
The canonical root for project-local Studio persistence MUST be `.studio/` in the project root.
|
||||||
|
|
||||||
|
The main project-session state store MUST be created under that root.
|
||||||
|
|
||||||
|
### 2. Ownership Boundary
|
||||||
|
|
||||||
|
`StudioProjectSession` MUST own the lifecycle of the main project-session state store.
|
||||||
|
|
||||||
|
That ownership includes:
|
||||||
|
|
||||||
|
- loading project-local setup when the project session opens;
|
||||||
|
- exposing the loaded state to shell and workspace consumers through project-session-owned contracts;
|
||||||
|
- coordinating restoration when the project shell mounts;
|
||||||
|
- coordinating persistence when the project shell or project session closes.
|
||||||
|
|
||||||
|
No workspace MAY bypass this boundary by defining an unrelated primary store for project-local Studio organization or setup.
|
||||||
|
|
||||||
|
### 3. Store Shape
|
||||||
|
|
||||||
|
The main store MUST begin as a `single file`.
|
||||||
|
|
||||||
|
This file MUST act as the canonical snapshot for the categories accepted by this decision.
|
||||||
|
|
||||||
|
The schema MUST be explicitly versioned so future revisions can detect compatibility and migration boundaries.
|
||||||
|
|
||||||
|
### 4. Accepted Wave-1 Categories
|
||||||
|
|
||||||
|
Wave 1 of the main project-session store MUST cover:
|
||||||
|
|
||||||
|
- `projectLocalSetup`
|
||||||
|
Project-local configuration required by Studio to operate on the project, including Prometeu runtime path and equivalent future setup fields.
|
||||||
|
|
||||||
|
- `shellLayout`
|
||||||
|
Restorable shell organization such as divider positions, layout proportions, and equivalent shell-level geometry state.
|
||||||
|
|
||||||
|
- `openShellState`
|
||||||
|
Which panels, windows, or workspaces were open when the project shell last persisted.
|
||||||
|
|
||||||
|
- `editorRestoration`
|
||||||
|
Open editor tabs and the active tab.
|
||||||
|
|
||||||
|
These names are conceptual categories.
|
||||||
|
Plans MAY refine the concrete field names, but they MUST preserve the exact normative scope above.
|
||||||
|
|
||||||
|
### 5. Explicit Exclusions
|
||||||
|
|
||||||
|
The main project-session store MUST NOT include:
|
||||||
|
|
||||||
|
- activity history;
|
||||||
|
- derived cache data;
|
||||||
|
- ephemeral UI interaction state such as hover-only data;
|
||||||
|
- heavy operational logs or output buffers;
|
||||||
|
- recomputable results that do not represent project-local Studio setup or restoration state.
|
||||||
|
|
||||||
|
If a future category is proposed, it MUST be classified explicitly as either:
|
||||||
|
|
||||||
|
- main project-session state;
|
||||||
|
- separate `.studio/` concern;
|
||||||
|
- launcher-global concern;
|
||||||
|
- or non-persisted transient state.
|
||||||
|
|
||||||
|
### 6. Read / Restore / Save Policy
|
||||||
|
|
||||||
|
The main policy is normatively locked as follows:
|
||||||
|
|
||||||
|
- project-local setup MUST be read when `StudioProjectSession` opens;
|
||||||
|
- restorable shell and editor state MUST be restored when the project shell mounts;
|
||||||
|
- the main project-session store MUST be saved when the project shell or project session closes.
|
||||||
|
|
||||||
|
The repository default for this store is `save on close`.
|
||||||
|
|
||||||
|
General incremental persistence MUST NOT be the default policy for the main store.
|
||||||
|
|
||||||
|
Checkpoints MAY be introduced only for categories that explicitly require intermediate persistence.
|
||||||
|
Any such checkpoint behavior MUST be justified in a later plan or revision and MUST remain category-scoped rather than becoming an uncontrolled global autosave rule.
|
||||||
|
|
||||||
|
### 7. Failure Handling
|
||||||
|
|
||||||
|
If `.studio/` is absent, the main store is missing, the schema is unknown, or the content is invalid or partially corrupted, the Studio MUST fall back to safe default values.
|
||||||
|
|
||||||
|
Such conditions MUST NOT block opening the project.
|
||||||
|
|
||||||
|
The fallback behavior MUST preserve the ability to open the project and operate the Studio with default shell, workspace, and setup assumptions as far as possible.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- No plan may reinterpret launcher-global state as project-local state without an explicit revision to this decision.
|
||||||
|
- No plan may move `project-local activity` into the main project-session store without an explicit revision to this decision.
|
||||||
|
- No plan may replace the `single file` starting point with a multi-file tree by inference.
|
||||||
|
- No plan may treat ephemeral UI noise or derived cache as accepted wave-1 project-session state.
|
||||||
|
- If a later implementation needs checkpoint persistence for a specific category, that need MUST be justified explicitly instead of silently broadening the save policy.
|
||||||
|
|
||||||
|
## Revision Log
|
||||||
|
|
||||||
|
- 2026-04-04: Initial accepted decision from AGD-0019.
|
||||||
@ -0,0 +1,217 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0037
|
||||||
|
ticket: studio-project-local-studio-state-under-dot-studio
|
||||||
|
title: Project-local Studio state store and restoration
|
||||||
|
status: done
|
||||||
|
created: 2026-04-04
|
||||||
|
completed: 2026-04-04
|
||||||
|
tags: [studio, project-session, project-state, persistence, dot-studio, shell, layout, setup]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement the first accepted wave of project-local Studio state persistence under `.studio/`, using a single-file project-session-owned store that restores setup, shell organization, and editor tabs for a project.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
DEC-0016 normatively locks the following:
|
||||||
|
|
||||||
|
- project-local Studio state must live under `.studio/` in the project root;
|
||||||
|
- the main store must be owned by `StudioProjectSession`;
|
||||||
|
- the main store must start as a single file;
|
||||||
|
- wave 1 must include project-local setup, shell layout, open shell/workspace state, and editor restoration;
|
||||||
|
- project-local activity must remain outside the main store;
|
||||||
|
- the policy is read on project-session open, restore on shell mount, and save on shell/project close;
|
||||||
|
- missing or invalid persisted state must fall back to safe defaults without blocking project open.
|
||||||
|
|
||||||
|
The current codebase already has a `StudioProjectSession` boundary plus a `MainView` shell and an `EditorWorkspace` with in-memory tab/session state, but it has no project-local persistence contract for those concerns.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Included
|
||||||
|
- Propagate DEC-0016 into Studio specifications.
|
||||||
|
- Introduce a project-session-owned single-file state store under `.studio/`.
|
||||||
|
- Load project-local setup when a project session opens.
|
||||||
|
- Restore shell layout and open shell/workspace state when the project shell mounts.
|
||||||
|
- Restore editor open tabs and active tab from project-local persisted state.
|
||||||
|
- Save the main project-session store when the shell or project session closes.
|
||||||
|
- Fall back to default values when persisted state is absent, unknown, or invalid.
|
||||||
|
- Add tests for store parsing, fallback behavior, session ownership, shell restoration, and editor tab restoration.
|
||||||
|
|
||||||
|
### Excluded
|
||||||
|
- Project-local activity persistence changes.
|
||||||
|
- General autosave or global incremental persistence for the main store.
|
||||||
|
- Multi-file store layout under `.studio/`.
|
||||||
|
- New settings UI for project-local setup editing.
|
||||||
|
- Persistence of ephemeral hover state, derived cache, heavy logs, or recomputable operational output.
|
||||||
|
|
||||||
|
## Execution Steps
|
||||||
|
|
||||||
|
### Step 1 - Propagate DEC-0016 into Studio specs
|
||||||
|
|
||||||
|
**What:** Add normative spec coverage for the project-local Studio state store and its wave-1 boundaries.
|
||||||
|
|
||||||
|
**How:** Update the existing shell and editor specs, and add a dedicated Studio persistence spec if needed, so the repository explicitly states:
|
||||||
|
- `.studio/` is the project-local root for the main Studio state store;
|
||||||
|
- `StudioProjectSession` owns load/save lifecycle for the store;
|
||||||
|
- the store begins as a single file;
|
||||||
|
- wave-1 categories are `projectLocalSetup`, `shellLayout`, `openShellState`, and `editorRestoration`;
|
||||||
|
- `project-local activity` is excluded from the main store;
|
||||||
|
- read/restore/save policy and default fallback behavior are normative.
|
||||||
|
|
||||||
|
The preferred propagation shape is:
|
||||||
|
- update [`docs/specs/studio/1. Studio Shell and Workspace Layout Specification.md`](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/docs/specs/studio/1.%20Studio%20Shell%20and%20Workspace%20Layout%20Specification.md) for shell/session ownership and restore/save timing;
|
||||||
|
- update [`docs/specs/studio/5. Code Editor Workspace Specification.md`](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/docs/specs/studio/5.%20Code%20Editor%20Workspace%20Specification.md) to replace the current “no cross-session tab restoration” rule with the accepted wave-1 restoration contract;
|
||||||
|
- add a new dedicated spec such as [`docs/specs/studio/8. Project-Local Studio State Specification.md`](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/docs/specs/studio/8.%20Project-Local%20Studio%20State%20Specification.md) to hold the store schema boundary and exclusions.
|
||||||
|
|
||||||
|
**File(s):**
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/docs/specs/studio/1. Studio Shell and Workspace Layout Specification.md`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/docs/specs/studio/5. Code Editor Workspace Specification.md`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/docs/specs/studio/8. Project-Local Studio State Specification.md`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/docs/specs/studio/README.md` if the new spec must be indexed
|
||||||
|
|
||||||
|
### Step 2 - Define the project-session state store contract and single-file codec
|
||||||
|
|
||||||
|
**What:** Create the main persisted-state model and the single-file read/write boundary used by `StudioProjectSession`.
|
||||||
|
|
||||||
|
**How:** Introduce a small persistence module or package in `prometeu-studio` that defines:
|
||||||
|
- a versioned root DTO for the main state file;
|
||||||
|
- wave-1 category DTOs for `projectLocalSetup`, `shellLayout`, `openShellState`, and `editorRestoration`;
|
||||||
|
- a file path convention under `.studio/`;
|
||||||
|
- loader/writer logic that returns safe defaults when the file is absent, unreadable, schema-incompatible, or invalid.
|
||||||
|
|
||||||
|
The implementation must keep `project-local activity` out of this file and must not broaden the schema to cache-like or transient UI data.
|
||||||
|
|
||||||
|
**File(s):**
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/projectsessions/`
|
||||||
|
- a new package for state persistence such as `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/projectstate/`
|
||||||
|
- corresponding test files under `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/test/java/p/studio/`
|
||||||
|
|
||||||
|
### Step 3 - Extend StudioProjectSession to load setup and expose restorable state
|
||||||
|
|
||||||
|
**What:** Move the main store lifecycle into the project-session boundary.
|
||||||
|
|
||||||
|
**How:** Update `StudioProjectSession` and `StudioProjectSessionFactory` so opening a project session:
|
||||||
|
- loads the main store from `.studio/`;
|
||||||
|
- exposes the loaded state to shell/workspace consumers through explicit accessors or a dedicated session-owned state service;
|
||||||
|
- makes project-local setup available before workspace-specific UI mounts.
|
||||||
|
|
||||||
|
The session boundary must remain the owner of the persistence lifecycle; `MainView` and workspaces may consume the state but must not redefine load semantics.
|
||||||
|
|
||||||
|
**File(s):**
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java`
|
||||||
|
- any new project-session-owned state service under `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/projectsessions/` or `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/projectstate/`
|
||||||
|
|
||||||
|
### Step 4 - Restore shell layout and open shell/workspace state from the session store
|
||||||
|
|
||||||
|
**What:** Apply persisted shell organization when the project shell mounts and capture it again when the shell closes.
|
||||||
|
|
||||||
|
**How:** Update `MainView` and any shell controls it owns so they can:
|
||||||
|
- restore the selected/open workspace state from the session store instead of hardcoding the default assets workspace when persisted state exists;
|
||||||
|
- restore accepted shell layout properties such as splitter positions or equivalent shell-level geometry once the relevant shell surfaces are mounted;
|
||||||
|
- collect the latest shell state before the shell or project session closes so `save on close` persists the canonical snapshot.
|
||||||
|
|
||||||
|
If a specific shell surface does not yet expose restorable layout state, introduce the minimal shell-owned API needed to read/write that state without making the shell controls own persistence themselves.
|
||||||
|
|
||||||
|
**File(s):**
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/window/MainView.java`
|
||||||
|
- shell controls under `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/controls/shell/`
|
||||||
|
- application/window close wiring where the current Studio shell lifecycle is finalized
|
||||||
|
|
||||||
|
### Step 5 - Restore editor open tabs and active tab through an editor-owned snapshot contract
|
||||||
|
|
||||||
|
**What:** Persist and restore the editor workspace’s accepted wave-1 restoration state.
|
||||||
|
|
||||||
|
**How:** Refactor the editor workspace so it can:
|
||||||
|
- export a pure restoration snapshot containing open tab paths and active tab path;
|
||||||
|
- accept a restoration snapshot when the workspace mounts under a project session;
|
||||||
|
- reopen supported files through `prometeu-vfs` using those persisted paths;
|
||||||
|
- fall back safely when a previously open file no longer exists or is no longer supported;
|
||||||
|
- preserve the active-tab choice when restoration succeeds.
|
||||||
|
|
||||||
|
This step must not broaden into persistence of editor-local transient data beyond what DEC-0016 accepts.
|
||||||
|
|
||||||
|
**File(s):**
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileSession.java`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java` if small UI adjustments are needed for restore-first behavior
|
||||||
|
|
||||||
|
### Step 6 - Wire save-on-close and category-scoped checkpoints if any are required
|
||||||
|
|
||||||
|
**What:** Make the accepted save policy real in the application lifecycle.
|
||||||
|
|
||||||
|
**How:** Ensure the main store is serialized:
|
||||||
|
- when the project shell closes;
|
||||||
|
- and when the project session closes through its normal lifecycle.
|
||||||
|
|
||||||
|
Do not introduce general autosave for the main store.
|
||||||
|
Only add checkpoint saves if a specific accepted category proves it cannot safely rely on close-time persistence alone, and keep that logic isolated to that category.
|
||||||
|
|
||||||
|
**File(s):**
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/window/MainView.java`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java`
|
||||||
|
- any shell/window lifecycle integration surface that currently owns project-window close behavior
|
||||||
|
|
||||||
|
### Step 7 - Add tests for defaults, restoration, and persistence boundaries
|
||||||
|
|
||||||
|
**What:** Lock the decision through automated tests.
|
||||||
|
|
||||||
|
**How:** Add tests that verify:
|
||||||
|
- missing `.studio/` falls back to defaults;
|
||||||
|
- invalid or schema-incompatible persisted content falls back to defaults;
|
||||||
|
- project-local setup is loaded at project-session open;
|
||||||
|
- shell/workspace state is restored from persisted state when present;
|
||||||
|
- editor open tabs and active tab restore correctly;
|
||||||
|
- `project-local activity` is not part of the main store contract;
|
||||||
|
- saving the main store happens on the accepted close boundary.
|
||||||
|
|
||||||
|
**File(s):**
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/test/java/p/studio/projectsessions/`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/test/java/p/studio/window/`
|
||||||
|
- `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/test/java/p/studio/workspaces/editor/`
|
||||||
|
- tests for the new persistence package under `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/test/java/p/studio/projectstate/`
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Persisted-state codec tests for versioned single-file read/write behavior.
|
||||||
|
- Fallback tests for absent, invalid, and schema-incompatible state files.
|
||||||
|
- Session-service tests proving default values are exposed when persisted state is unusable.
|
||||||
|
- Editor snapshot tests proving only open-tab paths and active-tab identity are persisted.
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- `StudioProjectSessionFactory` tests that verify setup is loaded during session open.
|
||||||
|
- Shell or window tests that verify persisted workspace selection and shell state are applied on mount.
|
||||||
|
- Editor workspace tests that reopen tabs from persisted paths and preserve the active tab when possible.
|
||||||
|
- Close-lifecycle tests that verify the main store is written when the shell or session closes.
|
||||||
|
|
||||||
|
### Manual Verification
|
||||||
|
- Open a project with no `.studio/` state and confirm the Studio boots with defaults.
|
||||||
|
- Configure project-local setup such as a Prometeu runtime path, close the shell, reopen the project, and confirm the setup is restored.
|
||||||
|
- Open multiple editor tabs, switch the active tab, close the project, reopen it, and confirm the same tabs and active tab are restored.
|
||||||
|
- Change shell/workspace organization that is in scope for wave 1, close the project, reopen it, and confirm that layout/workspace state is restored.
|
||||||
|
- Corrupt or remove the persisted state file and confirm the project still opens with defaults instead of failing.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Studio specs normatively describe the `.studio/` single-file project-session store and its exclusions.
|
||||||
|
- [ ] `StudioProjectSession` owns loading of project-local setup and access to the main state store.
|
||||||
|
- [ ] The main store is persisted as a single file under `.studio/`.
|
||||||
|
- [ ] Wave-1 categories are implemented without admitting excluded categories such as activity history or derived cache.
|
||||||
|
- [ ] Shell/workspace state restores on shell mount and saves on shell/project close.
|
||||||
|
- [ ] Editor open tabs and active tab restore from persisted project-local state.
|
||||||
|
- [ ] Missing or invalid persisted state falls back to safe defaults without blocking project open.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Accepted decision [`DEC-0016-project-local-studio-state-under-dot-studio.md`](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/discussion/workflow/decisions/DEC-0016-project-local-studio-state-under-dot-studio.md)
|
||||||
|
- Existing project-session boundary in `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/projectsessions/`
|
||||||
|
- Existing editor tab/session behavior in `/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/prometeu-studio/src/main/java/p/studio/workspaces/editor/`
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- The current shell controls may not yet expose enough structured state to restore all accepted wave-1 layout fields cleanly.
|
||||||
|
- Editor restoration may need a small refactor to separate pure restore snapshots from current UI-driven open-file behavior.
|
||||||
|
- A new persistence spec may overlap with current shell/editor specs unless propagation boundaries are written carefully.
|
||||||
|
- Save-on-close behavior may depend on application/window lifecycle code not yet centralized in one obvious surface.
|
||||||
@ -79,6 +79,9 @@ Rules:
|
|||||||
- the shell may construct and own project-session services that survive workspace focus changes;
|
- the shell may construct and own project-session services that survive workspace focus changes;
|
||||||
- workspaces may consume those project-session services without inheriting their ownership;
|
- workspaces may consume those project-session services without inheriting their ownership;
|
||||||
- introducing a project-session service must not collapse shell and workspace responsibilities into one monolithic root.
|
- introducing a project-session service must not collapse shell and workspace responsibilities into one monolithic root.
|
||||||
|
- project-local Studio state under `.studio/` must be loaded through the project-session boundary rather than through shell-local ad hoc persistence;
|
||||||
|
- project-local setup required before workspace use must be available when the project session opens;
|
||||||
|
- shell-restorable state accepted by Studio must be restored when the project shell mounts and persisted when the project shell closes.
|
||||||
|
|
||||||
## Right Utility Panel
|
## Right Utility Panel
|
||||||
|
|
||||||
|
|||||||
@ -174,8 +174,16 @@ The first wave must not define:
|
|||||||
- tab pinning,
|
- tab pinning,
|
||||||
- drag-and-drop tab reordering,
|
- drag-and-drop tab reordering,
|
||||||
- grouped tabs,
|
- grouped tabs,
|
||||||
- split editors,
|
- split editors.
|
||||||
- or cross-session tab restoration.
|
|
||||||
|
Project-session-owned cross-session tab restoration is accepted for the project-local Studio state wave.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- persisted editor restoration state must come from the project-session-owned `.studio/` state store rather than from an editor-owned ad hoc file;
|
||||||
|
- accepted restoration scope in this wave is limited to open tabs and active tab;
|
||||||
|
- editor layout restoration accepted in this wave may include divider-driven editor sizing, `Project Navigator` expansion state, and dock-panel configuration such as `Outline` and `Helper` open/closed state plus their restored size;
|
||||||
|
- restoration must degrade safely when a previously open file is missing or unsupported.
|
||||||
|
|
||||||
## Access-Mode and Save Model
|
## Access-Mode and Save Model
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,85 @@
|
|||||||
|
# Project-Local Studio State Specification
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Active
|
||||||
|
|
||||||
|
## Applies To
|
||||||
|
|
||||||
|
- `prometeu-studio`
|
||||||
|
- Studio project-session persistence under `.studio/`
|
||||||
|
- project-local Studio setup and restoration state
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define the normative contract for the main project-local Studio state store.
|
||||||
|
|
||||||
|
## Authority and Precedence
|
||||||
|
|
||||||
|
This specification implements the accepted decision for project-local Studio state under `.studio/`.
|
||||||
|
|
||||||
|
It extends:
|
||||||
|
|
||||||
|
- [`1. Studio Shell and Workspace Layout Specification.md`](1.%20Studio%20Shell%20and%20Workspace%20Layout%20Specification.md)
|
||||||
|
- [`5. Code Editor Workspace Specification.md`](5.%20Code%20Editor%20Workspace%20Specification.md)
|
||||||
|
|
||||||
|
## Core Rules
|
||||||
|
|
||||||
|
1. The canonical root for project-local Studio persistence MUST be `.studio/` in the project root.
|
||||||
|
2. The main project-local Studio state store MUST be owned by `StudioProjectSession`, or by a service explicitly owned by `StudioProjectSession`.
|
||||||
|
3. The main store MUST start as a single file.
|
||||||
|
4. The main store MUST be versioned.
|
||||||
|
5. The main store MUST load when the project session opens.
|
||||||
|
6. Restorable shell and editor state accepted by this wave MUST restore when the project shell mounts.
|
||||||
|
7. The main store MUST save when the project shell or project session closes.
|
||||||
|
8. Missing, invalid, or schema-incompatible state MUST fall back to safe default values and MUST NOT block project open.
|
||||||
|
|
||||||
|
## Accepted Wave-1 Categories
|
||||||
|
|
||||||
|
The main store MUST include:
|
||||||
|
|
||||||
|
- `projectLocalSetup`
|
||||||
|
- `shellLayout`
|
||||||
|
- `openShellState`
|
||||||
|
- `editorRestoration`
|
||||||
|
|
||||||
|
Wave-1 accepted meaning is:
|
||||||
|
|
||||||
|
- `projectLocalSetup`: project-local Studio setup required to operate on the project, including runtime paths such as the Prometeu runtime path;
|
||||||
|
- `shellLayout`: restorable layout state already accepted by implementation, including divider positions and equivalent layout proportions for accepted workspaces such as `Assets` and `Code Editor`, `Project Navigator` expansion state, and editor dock-panel open/closed plus restored-size configuration;
|
||||||
|
- `openShellState`: which workspace or equivalent shell-owned project view was open;
|
||||||
|
- `editorRestoration`: open editor tabs and active tab.
|
||||||
|
|
||||||
|
## Explicit Exclusions
|
||||||
|
|
||||||
|
The main store MUST NOT include:
|
||||||
|
|
||||||
|
- project-local activity history;
|
||||||
|
- derived cache;
|
||||||
|
- transient hover-only or equivalent ephemeral UI state;
|
||||||
|
- heavy operational output;
|
||||||
|
- recomputable results that do not represent project-local setup or restoration state.
|
||||||
|
|
||||||
|
`Project-local activity` may still live under `.studio/`, but it is a separate persistence concern and not part of the main store defined here.
|
||||||
|
|
||||||
|
## Ownership Rules
|
||||||
|
|
||||||
|
- shell and workspace UI may consume the main store through project-session-owned contracts;
|
||||||
|
- shell and workspace UI must not define competing primary persistence files for accepted wave-1 categories;
|
||||||
|
- project-local setup may be consumed before a dedicated settings UI exists.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- defining persistence for project-local activity
|
||||||
|
- introducing a multi-file main store in this wave
|
||||||
|
- defining a settings editor UI for project-local setup
|
||||||
|
- persisting arbitrary transient workspace detail by convenience
|
||||||
|
|
||||||
|
## Exit Criteria
|
||||||
|
|
||||||
|
This specification is complete enough when:
|
||||||
|
|
||||||
|
- the `.studio/` boundary is unambiguous;
|
||||||
|
- accepted and excluded categories are explicit;
|
||||||
|
- load, restore, save, and fallback timing are explicit;
|
||||||
|
- and the ownership boundary with `StudioProjectSession` is normatively locked.
|
||||||
@ -57,6 +57,7 @@ The current Studio core corpus is:
|
|||||||
5. [`5. Code Editor Workspace Specification.md`](5.%20Code%20Editor%20Workspace%20Specification.md)
|
5. [`5. Code Editor Workspace Specification.md`](5.%20Code%20Editor%20Workspace%20Specification.md)
|
||||||
6. [`6. Project Document VFS Specification.md`](6.%20Project%20Document%20VFS%20Specification.md)
|
6. [`6. Project Document VFS Specification.md`](6.%20Project%20Document%20VFS%20Specification.md)
|
||||||
7. [`7. Integrated LSP Semantic Read Phase Specification.md`](7.%20Integrated%20LSP%20Semantic%20Read%20Phase%20Specification.md)
|
7. [`7. Integrated LSP Semantic Read Phase Specification.md`](7.%20Integrated%20LSP%20Semantic%20Read%20Phase%20Specification.md)
|
||||||
|
8. [`8. Project-Local Studio State Specification.md`](8.%20Project-Local%20Studio%20State%20Specification.md)
|
||||||
|
|
||||||
## Reading Order
|
## Reading Order
|
||||||
|
|
||||||
@ -68,7 +69,8 @@ Recommended order:
|
|||||||
4. assets workspace behavior;
|
4. assets workspace behavior;
|
||||||
5. project document VFS boundary;
|
5. project document VFS boundary;
|
||||||
6. code editor workspace behavior;
|
6. code editor workspace behavior;
|
||||||
7. integrated LSP semantic-read behavior.
|
7. integrated LSP semantic-read behavior;
|
||||||
|
8. project-local Studio state behavior.
|
||||||
|
|
||||||
## Current Wave Notes
|
## Current Wave Notes
|
||||||
|
|
||||||
|
|||||||
@ -112,6 +112,26 @@ public abstract class WorkspaceDockPane extends TitledPane {
|
|||||||
return split;
|
return split;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final DockState captureDockState() {
|
||||||
|
final double preferredExpandedDivider = lastExpandedDividerPosition != null
|
||||||
|
? lastExpandedDividerPosition
|
||||||
|
: currentDivider();
|
||||||
|
return new DockState(isExpanded(), preferredExpandedDivider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void restoreDockState(final DockState dockState) {
|
||||||
|
if (dockState == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastExpandedDividerPosition = dockState.dividerPosition();
|
||||||
|
setExpanded(dockState.expanded());
|
||||||
|
if (dockState.expanded()) {
|
||||||
|
setDivider(clampExpandedDivider(dockState.dividerPosition()));
|
||||||
|
} else if (dockSplit != null) {
|
||||||
|
applyCollapsedDivider();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected final void setDockContent(final Node content) {
|
protected final void setDockContent(final Node content) {
|
||||||
setContent(Objects.requireNonNull(content, "content"));
|
setContent(Objects.requireNonNull(content, "content"));
|
||||||
}
|
}
|
||||||
@ -227,4 +247,7 @@ public abstract class WorkspaceDockPane extends TitledPane {
|
|||||||
node.setManaged(!disabled);
|
node.setManaged(!disabled);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record DockState(boolean expanded, double dividerPosition) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import java.util.Objects;
|
|||||||
public final class ProjectStudioPaths {
|
public final class ProjectStudioPaths {
|
||||||
private static final String STUDIO_DIR = ".studio";
|
private static final String STUDIO_DIR = ".studio";
|
||||||
private static final String ACTIVITIES_FILE = "activities.json";
|
private static final String ACTIVITIES_FILE = "activities.json";
|
||||||
|
private static final String STATE_FILE = "state.json";
|
||||||
|
|
||||||
private ProjectStudioPaths() {
|
private ProjectStudioPaths() {
|
||||||
}
|
}
|
||||||
@ -24,4 +25,11 @@ public final class ProjectStudioPaths {
|
|||||||
.toAbsolutePath()
|
.toAbsolutePath()
|
||||||
.normalize();
|
.normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Path statePath(ProjectReference projectReference) {
|
||||||
|
return studioRoot(projectReference)
|
||||||
|
.resolve(STATE_FILE)
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package p.studio.projectsessions;
|
package p.studio.projectsessions;
|
||||||
|
|
||||||
import p.studio.lsp.LspService;
|
import p.studio.lsp.LspService;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.vfs.VfsProjectDocument;
|
import p.studio.vfs.VfsProjectDocument;
|
||||||
|
|
||||||
@ -10,15 +12,33 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
private final ProjectReference projectReference;
|
private final ProjectReference projectReference;
|
||||||
private final LspService prometeuLspService;
|
private final LspService prometeuLspService;
|
||||||
private final VfsProjectDocument vfsProjectDocument;
|
private final VfsProjectDocument vfsProjectDocument;
|
||||||
|
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
||||||
|
private ProjectLocalStudioState projectLocalStudioState;
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
|
||||||
public StudioProjectSession(
|
public StudioProjectSession(
|
||||||
final ProjectReference projectReference,
|
final ProjectReference projectReference,
|
||||||
final LspService prometeuLspService,
|
final LspService prometeuLspService,
|
||||||
final VfsProjectDocument vfsProjectDocument) {
|
final VfsProjectDocument vfsProjectDocument) {
|
||||||
|
this(
|
||||||
|
projectReference,
|
||||||
|
prometeuLspService,
|
||||||
|
vfsProjectDocument,
|
||||||
|
new ProjectLocalStudioStateService(),
|
||||||
|
ProjectLocalStudioState.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
StudioProjectSession(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final LspService prometeuLspService,
|
||||||
|
final VfsProjectDocument vfsProjectDocument,
|
||||||
|
final ProjectLocalStudioStateService projectLocalStudioStateService,
|
||||||
|
final ProjectLocalStudioState projectLocalStudioState) {
|
||||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||||
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
|
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
|
||||||
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
|
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
|
||||||
|
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
||||||
|
this.projectLocalStudioState = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState");
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProjectReference projectReference() {
|
public ProjectReference projectReference() {
|
||||||
@ -33,13 +53,50 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
return prometeuLspService;
|
return prometeuLspService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState projectLocalStudioState() {
|
||||||
|
return projectLocalStudioState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceProjectLocalStudioState(final ProjectLocalStudioState nextProjectLocalStudioState) {
|
||||||
|
this.projectLocalStudioState = Objects.requireNonNull(nextProjectLocalStudioState, "nextProjectLocalStudioState");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveProjectLocalStudioState() {
|
||||||
|
projectLocalStudioStateService.save(projectReference, projectLocalStudioState);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (closed) {
|
if (closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
closed = true;
|
closed = true;
|
||||||
prometeuLspService.close();
|
RuntimeException failure = null;
|
||||||
vfsProjectDocument.close();
|
try {
|
||||||
|
saveProjectLocalStudioState();
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
failure = runtimeException;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
prometeuLspService.close();
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
if (failure == null) {
|
||||||
|
failure = runtimeException;
|
||||||
|
} else {
|
||||||
|
failure.addSuppressed(runtimeException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
vfsProjectDocument.close();
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
if (failure == null) {
|
||||||
|
failure = runtimeException;
|
||||||
|
} else {
|
||||||
|
failure.addSuppressed(runtimeException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package p.studio.projectsessions;
|
|||||||
|
|
||||||
import p.studio.lsp.messages.LspProjectContext;
|
import p.studio.lsp.messages.LspProjectContext;
|
||||||
import p.studio.lsp.LspServiceFactory;
|
import p.studio.lsp.LspServiceFactory;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.vfs.VfsProjectDocument;
|
import p.studio.vfs.VfsProjectDocument;
|
||||||
import p.studio.vfs.ProjectDocumentVfsFactory;
|
import p.studio.vfs.ProjectDocumentVfsFactory;
|
||||||
@ -11,12 +12,21 @@ import java.util.Objects;
|
|||||||
public final class StudioProjectSessionFactory {
|
public final class StudioProjectSessionFactory {
|
||||||
private final LspServiceFactory lspServiceFactory;
|
private final LspServiceFactory lspServiceFactory;
|
||||||
private final ProjectDocumentVfsFactory projectDocumentVfsFactory;
|
private final ProjectDocumentVfsFactory projectDocumentVfsFactory;
|
||||||
|
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
||||||
|
|
||||||
public StudioProjectSessionFactory(
|
public StudioProjectSessionFactory(
|
||||||
final LspServiceFactory lspServiceFactory,
|
final LspServiceFactory lspServiceFactory,
|
||||||
final ProjectDocumentVfsFactory projectDocumentVfsFactory) {
|
final ProjectDocumentVfsFactory projectDocumentVfsFactory) {
|
||||||
|
this(lspServiceFactory, projectDocumentVfsFactory, new ProjectLocalStudioStateService());
|
||||||
|
}
|
||||||
|
|
||||||
|
StudioProjectSessionFactory(
|
||||||
|
final LspServiceFactory lspServiceFactory,
|
||||||
|
final ProjectDocumentVfsFactory projectDocumentVfsFactory,
|
||||||
|
final ProjectLocalStudioStateService projectLocalStudioStateService) {
|
||||||
this.lspServiceFactory = Objects.requireNonNull(lspServiceFactory, "prometeuLspServiceFactory");
|
this.lspServiceFactory = Objects.requireNonNull(lspServiceFactory, "prometeuLspServiceFactory");
|
||||||
this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory");
|
this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory");
|
||||||
|
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
||||||
}
|
}
|
||||||
|
|
||||||
public StudioProjectSession open(final ProjectReference projectReference) {
|
public StudioProjectSession open(final ProjectReference projectReference) {
|
||||||
@ -25,7 +35,9 @@ public final class StudioProjectSessionFactory {
|
|||||||
return new StudioProjectSession(
|
return new StudioProjectSession(
|
||||||
target,
|
target,
|
||||||
lspServiceFactory.open(lspProjectContext(target), vfsProjectDocument),
|
lspServiceFactory.open(lspProjectContext(target), vfsProjectDocument),
|
||||||
vfsProjectDocument);
|
vfsProjectDocument,
|
||||||
|
projectLocalStudioStateService,
|
||||||
|
projectLocalStudioStateService.load(target));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LspProjectContext lspProjectContext(final ProjectReference projectReference) {
|
private LspProjectContext lspProjectContext(final ProjectReference projectReference) {
|
||||||
|
|||||||
@ -0,0 +1,228 @@
|
|||||||
|
package p.studio.projectstate;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record ProjectLocalStudioState(
|
||||||
|
int version,
|
||||||
|
ProjectLocalSetup projectLocalSetup,
|
||||||
|
ShellLayout shellLayout,
|
||||||
|
OpenShellState openShellState,
|
||||||
|
EditorRestorationState editorRestoration) {
|
||||||
|
public static final int CURRENT_VERSION = 1;
|
||||||
|
|
||||||
|
public ProjectLocalStudioState {
|
||||||
|
version = version <= 0 ? CURRENT_VERSION : version;
|
||||||
|
projectLocalSetup = projectLocalSetup == null ? ProjectLocalSetup.defaults() : projectLocalSetup.normalize();
|
||||||
|
shellLayout = shellLayout == null ? ShellLayout.defaults() : shellLayout.normalize();
|
||||||
|
openShellState = openShellState == null ? OpenShellState.defaults() : openShellState.normalize();
|
||||||
|
editorRestoration = editorRestoration == null ? EditorRestorationState.defaults() : editorRestoration.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProjectLocalStudioState defaults() {
|
||||||
|
return new ProjectLocalStudioState(
|
||||||
|
CURRENT_VERSION,
|
||||||
|
ProjectLocalSetup.defaults(),
|
||||||
|
ShellLayout.defaults(),
|
||||||
|
OpenShellState.defaults(),
|
||||||
|
EditorRestorationState.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState withShellLayout(final ShellLayout nextShellLayout) {
|
||||||
|
return new ProjectLocalStudioState(version, projectLocalSetup, nextShellLayout, openShellState, editorRestoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState withOpenShellState(final OpenShellState nextOpenShellState) {
|
||||||
|
return new ProjectLocalStudioState(version, projectLocalSetup, shellLayout, nextOpenShellState, editorRestoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState withEditorRestoration(final EditorRestorationState nextEditorRestoration) {
|
||||||
|
return new ProjectLocalStudioState(version, projectLocalSetup, shellLayout, openShellState, nextEditorRestoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState withProjectLocalSetup(final ProjectLocalSetup nextProjectLocalSetup) {
|
||||||
|
return new ProjectLocalStudioState(version, nextProjectLocalSetup, shellLayout, openShellState, editorRestoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record ProjectLocalSetup(String prometeuRuntimePath) {
|
||||||
|
public ProjectLocalSetup {
|
||||||
|
prometeuRuntimePath = normalizeText(prometeuRuntimePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProjectLocalSetup defaults() {
|
||||||
|
return new ProjectLocalSetup(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectLocalSetup normalize() {
|
||||||
|
return new ProjectLocalSetup(prometeuRuntimePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record ShellLayout(EditorLayoutState editorLayout, AssetsLayoutState assetsLayout) {
|
||||||
|
public ShellLayout {
|
||||||
|
editorLayout = editorLayout == null ? EditorLayoutState.defaults() : editorLayout.normalize();
|
||||||
|
assetsLayout = assetsLayout == null ? AssetsLayoutState.defaults() : assetsLayout.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShellLayout defaults() {
|
||||||
|
return new ShellLayout(EditorLayoutState.defaults(), AssetsLayoutState.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShellLayout normalize() {
|
||||||
|
return new ShellLayout(editorLayout, assetsLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record AssetsLayoutState(List<Double> contentSplitDividers) {
|
||||||
|
public AssetsLayoutState {
|
||||||
|
contentSplitDividers = normalizeDividerPositions(contentSplitDividers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AssetsLayoutState defaults() {
|
||||||
|
return new AssetsLayoutState(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetsLayoutState normalize() {
|
||||||
|
return new AssetsLayoutState(contentSplitDividers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record OpenShellState(String selectedWorkspaceId) {
|
||||||
|
public OpenShellState {
|
||||||
|
selectedWorkspaceId = normalizeText(selectedWorkspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OpenShellState defaults() {
|
||||||
|
return new OpenShellState(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OpenShellState normalize() {
|
||||||
|
return new OpenShellState(selectedWorkspaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record EditorLayoutState(
|
||||||
|
List<Double> contentSplitDividers,
|
||||||
|
List<Double> leftColumnDividers,
|
||||||
|
List<Double> rightColumnDividers,
|
||||||
|
DockPanelState outlinePanel,
|
||||||
|
DockPanelState helperPanel,
|
||||||
|
List<String> navigatorExpandedPaths) {
|
||||||
|
public EditorLayoutState {
|
||||||
|
contentSplitDividers = normalizeDividerPositions(contentSplitDividers);
|
||||||
|
leftColumnDividers = normalizeDividerPositions(leftColumnDividers);
|
||||||
|
rightColumnDividers = normalizeDividerPositions(rightColumnDividers);
|
||||||
|
outlinePanel = outlinePanel == null ? DockPanelState.defaults() : outlinePanel.normalize();
|
||||||
|
helperPanel = helperPanel == null ? DockPanelState.defaults() : helperPanel.normalize();
|
||||||
|
navigatorExpandedPaths = normalizePaths(navigatorExpandedPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EditorLayoutState defaults() {
|
||||||
|
return new EditorLayoutState(
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
DockPanelState.defaults(),
|
||||||
|
DockPanelState.defaults(),
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private EditorLayoutState normalize() {
|
||||||
|
return new EditorLayoutState(
|
||||||
|
contentSplitDividers,
|
||||||
|
leftColumnDividers,
|
||||||
|
rightColumnDividers,
|
||||||
|
outlinePanel,
|
||||||
|
helperPanel,
|
||||||
|
navigatorExpandedPaths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record DockPanelState(boolean expanded, Double dividerPosition) {
|
||||||
|
public DockPanelState {
|
||||||
|
dividerPosition = normalizeDividerPosition(dividerPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DockPanelState defaults() {
|
||||||
|
return new DockPanelState(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DockPanelState normalize() {
|
||||||
|
return new DockPanelState(expanded, dividerPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record EditorRestorationState(List<String> openTabPaths, String activeTabPath) {
|
||||||
|
public EditorRestorationState {
|
||||||
|
openTabPaths = normalizePaths(openTabPaths);
|
||||||
|
activeTabPath = normalizeText(activeTabPath);
|
||||||
|
if (activeTabPath != null && !openTabPaths.contains(activeTabPath)) {
|
||||||
|
final List<String> expanded = new ArrayList<>(openTabPaths);
|
||||||
|
expanded.add(activeTabPath);
|
||||||
|
openTabPaths = List.copyOf(expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EditorRestorationState defaults() {
|
||||||
|
return new EditorRestorationState(List.of(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EditorRestorationState normalize() {
|
||||||
|
return new EditorRestorationState(openTabPaths, activeTabPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeText(final String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String trimmed = value.trim();
|
||||||
|
return trimmed.isEmpty() ? null : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> normalizePaths(final List<String> values) {
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||||
|
for (final String value : values) {
|
||||||
|
final String text = normalizeText(value);
|
||||||
|
if (text != null) {
|
||||||
|
normalized.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Double> normalizeDividerPositions(final List<Double> values) {
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final List<Double> normalized = new ArrayList<>();
|
||||||
|
for (final Double value : values) {
|
||||||
|
if (value == null || value.isNaN() || value.isInfinite()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalized.add(Math.max(0.0d, Math.min(1.0d, value)));
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double normalizeDividerPosition(final Double value) {
|
||||||
|
if (value == null || value.isNaN() || value.isInfinite()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.max(0.0d, Math.min(1.0d, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package p.studio.projectstate;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.projects.ProjectStudioPaths;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ProjectLocalStudioStateService {
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper()
|
||||||
|
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
|
|
||||||
|
public ProjectLocalStudioState load(final ProjectReference projectReference) {
|
||||||
|
final Path statePath = ProjectStudioPaths.statePath(projectReference);
|
||||||
|
if (!Files.isRegularFile(statePath)) {
|
||||||
|
return ProjectLocalStudioState.defaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ProjectLocalStudioState loaded = MAPPER.readValue(statePath.toFile(), ProjectLocalStudioState.class);
|
||||||
|
if (loaded == null || loaded.version() != ProjectLocalStudioState.CURRENT_VERSION) {
|
||||||
|
return ProjectLocalStudioState.defaults();
|
||||||
|
}
|
||||||
|
return loaded;
|
||||||
|
} catch (IOException | RuntimeException ignored) {
|
||||||
|
return ProjectLocalStudioState.defaults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(final ProjectReference projectReference, final ProjectLocalStudioState state) {
|
||||||
|
final Path statePath = ProjectStudioPaths.statePath(projectReference);
|
||||||
|
final ProjectLocalStudioState normalized = Objects.requireNonNull(state, "state");
|
||||||
|
try {
|
||||||
|
Files.createDirectories(statePath.getParent());
|
||||||
|
MAPPER.writeValue(statePath.toFile(), normalized);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new UncheckedIOException(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import javafx.scene.layout.BorderPane;
|
|||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.controls.shell.*;
|
import p.studio.controls.shell.*;
|
||||||
import p.studio.lsp.events.StudioWorkspaceSelectedEvent;
|
import p.studio.lsp.events.StudioWorkspaceSelectedEvent;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.projectsessions.StudioProjectSession;
|
import p.studio.projectsessions.StudioProjectSession;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
@ -19,23 +20,36 @@ public final class MainView extends BorderPane {
|
|||||||
private final WorkspaceHost host = new WorkspaceHost();
|
private final WorkspaceHost host = new WorkspaceHost();
|
||||||
private final ProjectReference projectReference;
|
private final ProjectReference projectReference;
|
||||||
private final StudioProjectSession projectSession;
|
private final StudioProjectSession projectSession;
|
||||||
|
private final AssetWorkspace assetWorkspace;
|
||||||
|
private final EditorWorkspace editorWorkspace;
|
||||||
|
private final StudioWorkspaceRailControl<WorkspaceId> workspaceRail;
|
||||||
|
private boolean initializing;
|
||||||
|
private boolean workspaceLifecycleInitialized;
|
||||||
|
|
||||||
public MainView(final StudioProjectSession projectSession) {
|
public MainView(final StudioProjectSession projectSession) {
|
||||||
|
initializing = true;
|
||||||
this.projectSession = projectSession;
|
this.projectSession = projectSession;
|
||||||
this.projectReference = projectSession.projectReference();
|
this.projectReference = projectSession.projectReference();
|
||||||
|
final ProjectLocalStudioState persistedState = projectSession.projectLocalStudioState();
|
||||||
final var menuBar = new StudioShellMenuBarControl();
|
final var menuBar = new StudioShellMenuBarControl();
|
||||||
final var runSurface = new StudioRunSurfaceControl();
|
final var runSurface = new StudioRunSurfaceControl();
|
||||||
setTop(new StudioShellTopBarControl(menuBar));
|
setTop(new StudioShellTopBarControl(menuBar));
|
||||||
|
|
||||||
host.register(new AssetWorkspace(projectReference));
|
assetWorkspace = new AssetWorkspace(projectReference);
|
||||||
host.register(new EditorWorkspace(
|
host.register(assetWorkspace);
|
||||||
|
editorWorkspace = new EditorWorkspace(
|
||||||
projectReference,
|
projectReference,
|
||||||
projectSession.projectDocumentVfs(),
|
projectSession.projectDocumentVfs(),
|
||||||
projectSession.prometeuLspService()));
|
projectSession.prometeuLspService());
|
||||||
|
host.register(editorWorkspace);
|
||||||
|
assetWorkspace.restoreProjectLocalState(persistedState);
|
||||||
|
editorWorkspace.restoreProjectLocalState(persistedState);
|
||||||
|
assetWorkspace.setStateChangedAction(this::persistProjectLocalState);
|
||||||
|
editorWorkspace.setStateChangedAction(this::persistProjectLocalState);
|
||||||
// host.register(new PlaceholderWorkspace(WorkspaceId.DEBUG, I18n.WORKSPACE_DEBUG, "Debug"));
|
// host.register(new PlaceholderWorkspace(WorkspaceId.DEBUG, I18n.WORKSPACE_DEBUG, "Debug"));
|
||||||
host.register(new ShipperWorkspace(projectReference));
|
host.register(new ShipperWorkspace(projectReference));
|
||||||
|
|
||||||
final var workspaceRail = new StudioWorkspaceRailControl<>(
|
workspaceRail = new StudioWorkspaceRailControl<>(
|
||||||
List.of(
|
List.of(
|
||||||
new StudioWorkspaceRailItem<>(WorkspaceId.ASSETS, "📦", Container.i18n().bind(I18n.WORKSPACE_ASSETS)),
|
new StudioWorkspaceRailItem<>(WorkspaceId.ASSETS, "📦", Container.i18n().bind(I18n.WORKSPACE_ASSETS)),
|
||||||
new StudioWorkspaceRailItem<>(WorkspaceId.EDITOR, "📝", Container.i18n().bind(I18n.WORKSPACE_CODE)),
|
new StudioWorkspaceRailItem<>(WorkspaceId.EDITOR, "📝", Container.i18n().bind(I18n.WORKSPACE_CODE)),
|
||||||
@ -50,9 +64,11 @@ public final class MainView extends BorderPane {
|
|||||||
Container.i18n().bind(I18n.SHELL_ACTIVITY),
|
Container.i18n().bind(I18n.SHELL_ACTIVITY),
|
||||||
new StudioActivityFeedControl(projectReference)));
|
new StudioActivityFeedControl(projectReference)));
|
||||||
|
|
||||||
// default
|
final WorkspaceId initialWorkspace = restoreInitialWorkspace(persistedState.openShellState());
|
||||||
workspaceRail.select(WorkspaceId.ASSETS);
|
workspaceRail.select(initialWorkspace);
|
||||||
loadWorkspace(WorkspaceId.ASSETS);
|
loadWorkspace(initialWorkspace);
|
||||||
|
workspaceLifecycleInitialized = true;
|
||||||
|
initializing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProjectReference projectReference() {
|
public ProjectReference projectReference() {
|
||||||
@ -63,8 +79,43 @@ public final class MainView extends BorderPane {
|
|||||||
return projectSession;
|
return projectSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void persistProjectLocalState() {
|
||||||
|
if (initializing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final WorkspaceId selectedWorkspace = host.getCurrentWorkspaceId() == null ? WorkspaceId.ASSETS : host.getCurrentWorkspaceId();
|
||||||
|
final ProjectLocalStudioState current = projectSession.projectLocalStudioState();
|
||||||
|
projectSession.replaceProjectLocalStudioState(
|
||||||
|
current.withOpenShellState(new ProjectLocalStudioState.OpenShellState(selectedWorkspace.name()))
|
||||||
|
.withShellLayout(new ProjectLocalStudioState.ShellLayout(
|
||||||
|
editorWorkspace.captureLayoutState(),
|
||||||
|
assetWorkspace.captureLayoutState()))
|
||||||
|
.withEditorRestoration(editorWorkspace.captureRestorationState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reapplyProjectLocalLayout() {
|
||||||
|
assetWorkspace.reapplyPendingLayoutState();
|
||||||
|
editorWorkspace.reapplyPendingLayoutState();
|
||||||
|
}
|
||||||
|
|
||||||
private void loadWorkspace(WorkspaceId workspaceId) {
|
private void loadWorkspace(WorkspaceId workspaceId) {
|
||||||
host.change(workspaceId);
|
host.change(workspaceId);
|
||||||
|
if (workspaceLifecycleInitialized) {
|
||||||
|
persistProjectLocalState();
|
||||||
|
projectSession.saveProjectLocalStudioState();
|
||||||
|
}
|
||||||
Container.eventBus().publish(new StudioWorkspaceSelectedEvent(workspaceId));
|
Container.eventBus().publish(new StudioWorkspaceSelectedEvent(workspaceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WorkspaceId restoreInitialWorkspace(final ProjectLocalStudioState.OpenShellState openShellState) {
|
||||||
|
if (openShellState.selectedWorkspaceId() == null) {
|
||||||
|
return WorkspaceId.ASSETS;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return WorkspaceId.valueOf(openShellState.selectedWorkspaceId());
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return WorkspaceId.ASSETS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,7 +146,8 @@ public final class StudioWindowCoordinator {
|
|||||||
private void finishProjectOpen(StudioProjectSession projectSession, Stage loadingStage) {
|
private void finishProjectOpen(StudioProjectSession projectSession, Stage loadingStage) {
|
||||||
final ProjectReference projectReference = projectSession.projectReference();
|
final ProjectReference projectReference = projectSession.projectReference();
|
||||||
final Stage projectStage = new Stage();
|
final Stage projectStage = new Stage();
|
||||||
final Scene scene = new Scene(new MainView(projectSession), PROJECT_WIDTH, PROJECT_HEIGHT);
|
final MainView mainView = new MainView(projectSession);
|
||||||
|
final Scene scene = new Scene(mainView, PROJECT_WIDTH, PROJECT_HEIGHT);
|
||||||
scene.getStylesheets().add(Container.theme().getDefaultTheme());
|
scene.getStylesheets().add(Container.theme().getDefaultTheme());
|
||||||
|
|
||||||
projectStage.setTitle(Container.i18n().format(I18n.APP_PROJECT_TITLE, projectReference.name()));
|
projectStage.setTitle(Container.i18n().format(I18n.APP_PROJECT_TITLE, projectReference.name()));
|
||||||
@ -169,6 +170,7 @@ public final class StudioWindowCoordinator {
|
|||||||
loadingStage.close();
|
loadingStage.close();
|
||||||
projectStage.show();
|
projectStage.show();
|
||||||
projectStage.toFront();
|
projectStage.toFront();
|
||||||
|
Platform.runLater(mainView::reapplyProjectLocalLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void failProjectOpen(
|
private void failProjectOpen(
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import javafx.scene.layout.HBox;
|
|||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
import p.studio.workspaces.Workspace;
|
import p.studio.workspaces.Workspace;
|
||||||
@ -27,6 +28,10 @@ public final class AssetWorkspace extends Workspace {
|
|||||||
private final AssetListControl assetListControl;
|
private final AssetListControl assetListControl;
|
||||||
private final AssetDetailsControl detailsControl;
|
private final AssetDetailsControl detailsControl;
|
||||||
private final AssetLogsPane logsPane;
|
private final AssetLogsPane logsPane;
|
||||||
|
private SplitPane splitPane;
|
||||||
|
private Runnable stateChangedAction = () -> { };
|
||||||
|
private ProjectLocalStudioState.AssetsLayoutState pendingAssetsLayoutState = ProjectLocalStudioState.AssetsLayoutState.defaults();
|
||||||
|
private boolean applyingPendingLayoutState;
|
||||||
|
|
||||||
public AssetWorkspace(final ProjectReference projectReference) {
|
public AssetWorkspace(final ProjectReference projectReference) {
|
||||||
super(projectReference);
|
super(projectReference);
|
||||||
@ -55,6 +60,8 @@ public final class AssetWorkspace extends Workspace {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() {
|
public void load() {
|
||||||
|
reapplyPendingLayoutState();
|
||||||
|
schedulePendingLayoutReapply();
|
||||||
workspaceEventBus.publish(new StudioAssetsRefreshRequestedEvent());
|
workspaceEventBus.publish(new StudioAssetsRefreshRequestedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +74,37 @@ public final class AssetWorkspace extends Workspace {
|
|||||||
return List.of(assetListControl, detailsControl, logsPane);
|
return List.of(assetListControl, detailsControl, logsPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setStateChangedAction(final Runnable stateChangedAction) {
|
||||||
|
this.stateChangedAction = java.util.Objects.requireNonNull(stateChangedAction, "stateChangedAction");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restoreProjectLocalState(final ProjectLocalStudioState projectLocalStudioState) {
|
||||||
|
pendingAssetsLayoutState = projectLocalStudioState.shellLayout().assetsLayout();
|
||||||
|
reapplyPendingLayoutState();
|
||||||
|
schedulePendingLayoutReapply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reapplyPendingLayoutState() {
|
||||||
|
if (splitPane == null || pendingAssetsLayoutState.contentSplitDividers().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyingPendingLayoutState = true;
|
||||||
|
splitPane.setDividerPositions(pendingAssetsLayoutState.contentSplitDividers().stream()
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.toArray());
|
||||||
|
javafx.application.Platform.runLater(() -> applyingPendingLayoutState = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState.AssetsLayoutState captureLayoutState() {
|
||||||
|
if (splitPane == null || splitPane.getDividers().isEmpty()) {
|
||||||
|
return ProjectLocalStudioState.AssetsLayoutState.defaults();
|
||||||
|
}
|
||||||
|
return new ProjectLocalStudioState.AssetsLayoutState(
|
||||||
|
splitPane.getDividers().stream()
|
||||||
|
.map(SplitPane.Divider::getPosition)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private VBox buildLayout() {
|
private VBox buildLayout() {
|
||||||
final var topProgress = createTopProgressBox();
|
final var topProgress = createTopProgressBox();
|
||||||
final var actionBar = createActionBar();
|
final var actionBar = createActionBar();
|
||||||
@ -144,10 +182,37 @@ public final class AssetWorkspace extends Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SplitPane createAssetSplitPane() {
|
private SplitPane createAssetSplitPane() {
|
||||||
final var splitPane = new SplitPane(assetListControl, detailsControl);
|
splitPane = new SplitPane(assetListControl, detailsControl);
|
||||||
splitPane.setDividerPositions(0.34);
|
splitPane.setDividerPositions(0.34);
|
||||||
splitPane.getStyleClass().add("assets-workspace-split");
|
splitPane.getStyleClass().add("assets-workspace-split");
|
||||||
|
splitPane.getDividers().forEach(divider ->
|
||||||
|
divider.positionProperty().addListener((ignored, previous, current) -> {
|
||||||
|
if (applyingPendingLayoutState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingAssetsLayoutState = captureLayoutState();
|
||||||
|
stateChangedAction.run();
|
||||||
|
}));
|
||||||
VBox.setVgrow(splitPane, Priority.ALWAYS);
|
VBox.setVgrow(splitPane, Priority.ALWAYS);
|
||||||
return splitPane;
|
return splitPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void schedulePendingLayoutReapply() {
|
||||||
|
if (splitPane == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (splitPane.getWidth() > 0) {
|
||||||
|
javafx.application.Platform.runLater(this::reapplyPendingLayoutState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final javafx.beans.value.ChangeListener<Number>[] listenerRef = new javafx.beans.value.ChangeListener[1];
|
||||||
|
listenerRef[0] = (ignored, previous, current) -> {
|
||||||
|
if (current == null || current.doubleValue() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
splitPane.widthProperty().removeListener(listenerRef[0]);
|
||||||
|
javafx.application.Platform.runLater(this::reapplyPendingLayoutState);
|
||||||
|
};
|
||||||
|
splitPane.widthProperty().addListener(listenerRef[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package p.studio.workspaces.editor;
|
package p.studio.workspaces.editor;
|
||||||
|
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -51,6 +53,19 @@ public final class EditorOpenFileSession {
|
|||||||
return openFiles.isEmpty();
|
return openFiles.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
openFiles.clear();
|
||||||
|
activePath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState.EditorRestorationState restorationState() {
|
||||||
|
return new ProjectLocalStudioState.EditorRestorationState(
|
||||||
|
openFiles.stream()
|
||||||
|
.map(buffer -> buffer.path().toString())
|
||||||
|
.toList(),
|
||||||
|
activePath == null ? null : activePath.toString());
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<EditorOpenFileBuffer> find(final Path path) {
|
private Optional<EditorOpenFileBuffer> find(final Path path) {
|
||||||
final int index = indexOf(path);
|
final int index = indexOf(path);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
|||||||
@ -19,7 +19,9 @@ import p.studio.vfs.messages.VfsProjectNode;
|
|||||||
import p.studio.vfs.messages.VfsProjectSnapshot;
|
import p.studio.vfs.messages.VfsProjectSnapshot;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -34,6 +36,7 @@ public final class EditorProjectNavigatorPanel extends BorderPane {
|
|||||||
private Runnable revealActiveFileAction = () -> { };
|
private Runnable revealActiveFileAction = () -> { };
|
||||||
private Runnable refreshAction = () -> { };
|
private Runnable refreshAction = () -> { };
|
||||||
private Consumer<VfsProjectNode> fileSelectionAction = node -> { };
|
private Consumer<VfsProjectNode> fileSelectionAction = node -> { };
|
||||||
|
private Set<Path> preferredExpandedPaths = Set.of();
|
||||||
|
|
||||||
public EditorProjectNavigatorPanel() {
|
public EditorProjectNavigatorPanel() {
|
||||||
getStyleClass().addAll("editor-workspace-panel", "editor-workspace-navigator-panel");
|
getStyleClass().addAll("editor-workspace-panel", "editor-workspace-navigator-panel");
|
||||||
@ -129,13 +132,42 @@ public final class EditorProjectNavigatorPanel extends BorderPane {
|
|||||||
|
|
||||||
public void setSnapshot(final VfsProjectSnapshot snapshot) {
|
public void setSnapshot(final VfsProjectSnapshot snapshot) {
|
||||||
final var currentSelection = selectedPath();
|
final var currentSelection = selectedPath();
|
||||||
final var expandedPaths = captureExpandedPaths(treeView.getRoot());
|
final var expandedPaths = mergeExpandedPaths();
|
||||||
final var rootItem = buildTreeItem(Objects.requireNonNull(snapshot, "snapshot").rootNode(), expandedPaths, true);
|
final var rootItem = buildTreeItem(Objects.requireNonNull(snapshot, "snapshot").rootNode(), expandedPaths, true);
|
||||||
treeView.setRoot(rootItem);
|
treeView.setRoot(rootItem);
|
||||||
restoreSelection(rootItem, currentSelection);
|
restoreSelection(rootItem, currentSelection);
|
||||||
showEmptyState(false);
|
showEmptyState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> expandedPathStrings() {
|
||||||
|
return captureExpandedPaths(treeView.getRoot()).stream()
|
||||||
|
.map(Path::toString)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restoreExpandedPaths(final List<String> expandedPaths) {
|
||||||
|
final LinkedHashSet<Path> restored = new LinkedHashSet<>();
|
||||||
|
if (expandedPaths != null) {
|
||||||
|
for (final String expandedPath : expandedPaths) {
|
||||||
|
if (expandedPath == null || expandedPath.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
restored.add(Path.of(expandedPath).toAbsolutePath().normalize());
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
// Ignore malformed persisted paths and continue with the remaining ones.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preferredExpandedPaths = Set.copyOf(restored);
|
||||||
|
final TreeItem<VfsProjectNode> rootItem = treeView.getRoot();
|
||||||
|
if (rootItem != null) {
|
||||||
|
final var currentSelection = selectedPath();
|
||||||
|
treeView.setRoot(buildTreeItem(rootItem.getValue(), preferredExpandedPaths, true));
|
||||||
|
restoreSelection(treeView.getRoot(), currentSelection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void revealPath(final Path path) {
|
public void revealPath(final Path path) {
|
||||||
final var normalizedPath = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
final var normalizedPath = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||||
final var rootItem = treeView.getRoot();
|
final var rootItem = treeView.getRoot();
|
||||||
@ -180,6 +212,12 @@ public final class EditorProjectNavigatorPanel extends BorderPane {
|
|||||||
item.getChildren().forEach(child -> collectExpandedPaths(child, expandedPaths));
|
item.getChildren().forEach(child -> collectExpandedPaths(child, expandedPaths));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<Path> mergeExpandedPaths() {
|
||||||
|
final LinkedHashSet<Path> merged = new LinkedHashSet<>(preferredExpandedPaths);
|
||||||
|
merged.addAll(captureExpandedPaths(treeView.getRoot()));
|
||||||
|
return Set.copyOf(merged);
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<Path> selectedPath() {
|
private Optional<Path> selectedPath() {
|
||||||
return Optional.ofNullable(treeView.getSelectionModel().getSelectedItem())
|
return Optional.ofNullable(treeView.getSelectionModel().getSelectedItem())
|
||||||
.map(TreeItem::getValue)
|
.map(TreeItem::getValue)
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import org.reactfx.Subscription;
|
|||||||
import p.studio.lsp.LspService;
|
import p.studio.lsp.LspService;
|
||||||
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
||||||
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
|
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
import p.studio.vfs.VfsProjectDocument;
|
import p.studio.vfs.VfsProjectDocument;
|
||||||
@ -55,6 +56,12 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
presentationRegistry.resolve("text").highlight(""),
|
presentationRegistry.resolve("text").highlight(""),
|
||||||
List.of());
|
List.of());
|
||||||
private boolean syncingEditor;
|
private boolean syncingEditor;
|
||||||
|
private boolean applyingPendingLayoutState;
|
||||||
|
private Runnable stateChangedAction = () -> { };
|
||||||
|
private ProjectLocalStudioState.EditorLayoutState pendingEditorLayoutState = ProjectLocalStudioState.EditorLayoutState.defaults();
|
||||||
|
private SplitPane contentSplit;
|
||||||
|
private SplitPane leftColumnSplit;
|
||||||
|
private SplitPane rightColumnSplit;
|
||||||
|
|
||||||
public EditorWorkspace(
|
public EditorWorkspace(
|
||||||
final ProjectReference projectReference,
|
final ProjectReference projectReference,
|
||||||
@ -91,6 +98,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
navigatorPanel.setFileSelectionAction(this::openNode);
|
navigatorPanel.setFileSelectionAction(this::openNode);
|
||||||
tabStrip.setTabSelectionAction(path -> {
|
tabStrip.setTabSelectionAction(path -> {
|
||||||
openFileSession.activate(path);
|
openFileSession.activate(path);
|
||||||
|
notifyStateChanged();
|
||||||
renderSession();
|
renderSession();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -104,6 +112,8 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() {
|
public void load() {
|
||||||
|
applyEditorLayoutState(pendingEditorLayoutState);
|
||||||
|
schedulePendingLayoutReapply();
|
||||||
refreshNavigator();
|
refreshNavigator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +124,36 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
|
|
||||||
public CodeArea codeArea() { return codeArea; }
|
public CodeArea codeArea() { return codeArea; }
|
||||||
|
|
||||||
|
public void setStateChangedAction(final Runnable stateChangedAction) {
|
||||||
|
this.stateChangedAction = Objects.requireNonNull(stateChangedAction, "stateChangedAction");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restoreProjectLocalState(final ProjectLocalStudioState projectLocalStudioState) {
|
||||||
|
final ProjectLocalStudioState state = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState");
|
||||||
|
pendingEditorLayoutState = state.shellLayout().editorLayout();
|
||||||
|
reapplyPendingLayoutState();
|
||||||
|
schedulePendingLayoutReapply();
|
||||||
|
restoreEditorState(state.editorRestoration());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reapplyPendingLayoutState() {
|
||||||
|
applyEditorLayoutState(pendingEditorLayoutState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState.EditorLayoutState captureLayoutState() {
|
||||||
|
return new ProjectLocalStudioState.EditorLayoutState(
|
||||||
|
dividerPositions(contentSplit),
|
||||||
|
dividerPositions(leftColumnSplit),
|
||||||
|
dividerPositions(rightColumnSplit),
|
||||||
|
dockPanelState(outlinePanel),
|
||||||
|
dockPanelState(helperPanel),
|
||||||
|
navigatorPanel.expandedPathStrings());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioState.EditorRestorationState captureRestorationState() {
|
||||||
|
return openFileSession.restorationState();
|
||||||
|
}
|
||||||
|
|
||||||
private void refreshNavigator() {
|
private void refreshNavigator() {
|
||||||
navigatorPanel.setSnapshot(vfsProjectDocument.refresh());
|
navigatorPanel.setSnapshot(vfsProjectDocument.refresh());
|
||||||
}
|
}
|
||||||
@ -129,6 +169,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
|
|
||||||
private void openFile(final VfsDocumentOpenResult.VfsTextDocument textDocument) {
|
private void openFile(final VfsDocumentOpenResult.VfsTextDocument textDocument) {
|
||||||
openFileSession.open(bufferFrom(textDocument));
|
openFileSession.open(bufferFrom(textDocument));
|
||||||
|
notifyStateChanged();
|
||||||
renderSession();
|
renderSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,19 +295,23 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private VBox buildLayout() {
|
private VBox buildLayout() {
|
||||||
final var content = new SplitPane(buildLeftColumn(), buildRightColumn());
|
contentSplit = new SplitPane(buildLeftColumn(), buildRightColumn());
|
||||||
content.setDividerPositions(0.30);
|
contentSplit.setDividerPositions(0.30);
|
||||||
content.getStyleClass().add("editor-workspace-split");
|
contentSplit.getStyleClass().add("editor-workspace-split");
|
||||||
|
observeStatefulSplitPane(contentSplit);
|
||||||
|
|
||||||
final var layout = new VBox(12, buildCommandBar(), content, statusBar);
|
final var layout = new VBox(12, buildCommandBar(), contentSplit, statusBar);
|
||||||
layout.getStyleClass().add("editor-workspace-layout");
|
layout.getStyleClass().add("editor-workspace-layout");
|
||||||
VBox.setVgrow(content, Priority.ALWAYS);
|
VBox.setVgrow(contentSplit, Priority.ALWAYS);
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SplitPane buildLeftColumn() {
|
private SplitPane buildLeftColumn() {
|
||||||
final var leftColumn = outlinePanel.createBottomDockLayout(navigatorPanel, "editor-workspace-left-split");
|
leftColumnSplit = outlinePanel.createBottomDockLayout(navigatorPanel, "editor-workspace-left-split");
|
||||||
|
final var leftColumn = leftColumnSplit;
|
||||||
leftColumn.getStyleClass().add("editor-workspace-left-column");
|
leftColumn.getStyleClass().add("editor-workspace-left-column");
|
||||||
|
observeStatefulSplitPane(leftColumnSplit);
|
||||||
|
observeDockPane(outlinePanel);
|
||||||
return leftColumn;
|
return leftColumn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,7 +329,10 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SplitPane buildRightColumn() {
|
private SplitPane buildRightColumn() {
|
||||||
return helperPanel.createBottomDockLayout(buildCenterColumn(), "editor-workspace-right-split");
|
rightColumnSplit = helperPanel.createBottomDockLayout(buildCenterColumn(), "editor-workspace-right-split");
|
||||||
|
observeStatefulSplitPane(rightColumnSplit);
|
||||||
|
observeDockPane(helperPanel);
|
||||||
|
return rightColumnSplit;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureCommandBar() {
|
private void configureCommandBar() {
|
||||||
@ -454,4 +502,134 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
codeArea.moveTo(inlineHintProjection.clampCaret(caret, true));
|
codeArea.moveTo(inlineHintProjection.clampCaret(caret, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void restoreEditorState(final ProjectLocalStudioState.EditorRestorationState editorRestorationState) {
|
||||||
|
openFileSession.clear();
|
||||||
|
for (final String pathText : editorRestorationState.openTabPaths()) {
|
||||||
|
tryRestoreOpenFile(pathText);
|
||||||
|
}
|
||||||
|
if (editorRestorationState.activeTabPath() != null) {
|
||||||
|
try {
|
||||||
|
openFileSession.activate(Path.of(editorRestorationState.activeTabPath()));
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
// Fall back to the currently active restored tab.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryRestoreOpenFile(final String pathText) {
|
||||||
|
try {
|
||||||
|
final Path path = Path.of(pathText).toAbsolutePath().normalize();
|
||||||
|
final VfsDocumentOpenResult result = vfsProjectDocument.openDocument(path);
|
||||||
|
if (result instanceof VfsDocumentOpenResult.VfsTextDocument textDocument) {
|
||||||
|
openFileSession.open(bufferFrom(textDocument));
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
// Invalid or unavailable persisted tabs are skipped and the editor falls back to the remaining state.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Double> dividerPositions(final SplitPane splitPane) {
|
||||||
|
if (splitPane == null || splitPane.getDividers().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return splitPane.getDividers().stream()
|
||||||
|
.map(SplitPane.Divider::getPosition)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyDividerPositions(final SplitPane splitPane, final List<Double> positions) {
|
||||||
|
if (splitPane == null || positions == null || positions.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final double[] values = positions.stream().mapToDouble(Double::doubleValue).toArray();
|
||||||
|
splitPane.setDividerPositions(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectLocalStudioState.DockPanelState dockPanelState(final p.studio.controls.WorkspaceDockPane dockPane) {
|
||||||
|
final p.studio.controls.WorkspaceDockPane.DockState state = dockPane.captureDockState();
|
||||||
|
return new ProjectLocalStudioState.DockPanelState(state.expanded(), state.dividerPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreDockPanelState(
|
||||||
|
final p.studio.controls.WorkspaceDockPane dockPane,
|
||||||
|
final ProjectLocalStudioState.DockPanelState dockPanelState) {
|
||||||
|
if (dockPanelState == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final double dividerPosition = dockPanelState.dividerPosition() == null
|
||||||
|
? dockPane.captureDockState().dividerPosition()
|
||||||
|
: dockPanelState.dividerPosition();
|
||||||
|
dockPane.restoreDockState(new p.studio.controls.WorkspaceDockPane.DockState(
|
||||||
|
dockPanelState.expanded(),
|
||||||
|
dividerPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void observeStatefulSplitPane(final SplitPane splitPane) {
|
||||||
|
splitPane.getDividers().forEach(divider ->
|
||||||
|
divider.positionProperty().addListener((ignored, previous, current) -> {
|
||||||
|
if (applyingPendingLayoutState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifyStateChanged();
|
||||||
|
}));
|
||||||
|
splitPane.getDividers().addListener((javafx.collections.ListChangeListener<? super SplitPane.Divider>) change -> {
|
||||||
|
while (change.next()) {
|
||||||
|
if (change.wasAdded()) {
|
||||||
|
change.getAddedSubList().forEach(divider ->
|
||||||
|
divider.positionProperty().addListener((ignored, previous, current) -> {
|
||||||
|
if (applyingPendingLayoutState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifyStateChanged();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void observeDockPane(final p.studio.controls.WorkspaceDockPane dockPane) {
|
||||||
|
dockPane.expandedProperty().addListener((ignored, previous, current) -> {
|
||||||
|
if (applyingPendingLayoutState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifyStateChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyStateChanged() {
|
||||||
|
pendingEditorLayoutState = captureLayoutState();
|
||||||
|
stateChangedAction.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void schedulePendingLayoutReapply() {
|
||||||
|
if (contentSplit == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (contentSplit.getWidth() > 0) {
|
||||||
|
Platform.runLater(this::reapplyPendingLayoutState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final javafx.beans.value.ChangeListener<Number>[] listenerRef = new javafx.beans.value.ChangeListener[1];
|
||||||
|
listenerRef[0] = (ignored, previous, current) -> {
|
||||||
|
if (current == null || current.doubleValue() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contentSplit.widthProperty().removeListener(listenerRef[0]);
|
||||||
|
Platform.runLater(this::reapplyPendingLayoutState);
|
||||||
|
};
|
||||||
|
contentSplit.widthProperty().addListener(listenerRef[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyEditorLayoutState(final ProjectLocalStudioState.EditorLayoutState editorLayoutState) {
|
||||||
|
applyingPendingLayoutState = true;
|
||||||
|
applyDividerPositions(contentSplit, editorLayoutState.contentSplitDividers());
|
||||||
|
applyDividerPositions(leftColumnSplit, editorLayoutState.leftColumnDividers());
|
||||||
|
applyDividerPositions(rightColumnSplit, editorLayoutState.rightColumnDividers());
|
||||||
|
restoreDockPanelState(outlinePanel, editorLayoutState.outlinePanel());
|
||||||
|
restoreDockPanelState(helperPanel, editorLayoutState.helperPanel());
|
||||||
|
navigatorPanel.restoreExpandedPaths(editorLayoutState.navigatorExpandedPaths());
|
||||||
|
Platform.runLater(() -> applyingPendingLayoutState = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
|
|||||||
import p.studio.lsp.messages.LspProjectContext;
|
import p.studio.lsp.messages.LspProjectContext;
|
||||||
import p.studio.lsp.LspService;
|
import p.studio.lsp.LspService;
|
||||||
import p.studio.lsp.LspServiceFactory;
|
import p.studio.lsp.LspServiceFactory;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.lsp.dtos.LspSessionStateDTO;
|
import p.studio.lsp.dtos.LspSessionStateDTO;
|
||||||
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
||||||
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
|
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
|
||||||
@ -27,7 +29,8 @@ final class StudioProjectSessionFactoryTest {
|
|||||||
void openCreatesProjectSessionBackedByProjectScopedVfs() {
|
void openCreatesProjectSessionBackedByProjectScopedVfs() {
|
||||||
final RecordingLspFactory lspFactory = new RecordingLspFactory();
|
final RecordingLspFactory lspFactory = new RecordingLspFactory();
|
||||||
final RecordingVfsFactory vfsFactory = new RecordingVfsFactory();
|
final RecordingVfsFactory vfsFactory = new RecordingVfsFactory();
|
||||||
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(lspFactory, vfsFactory);
|
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
||||||
|
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(lspFactory, vfsFactory, stateService);
|
||||||
final ProjectReference projectReference = new ProjectReference(
|
final ProjectReference projectReference = new ProjectReference(
|
||||||
"Example",
|
"Example",
|
||||||
"1.0.0",
|
"1.0.0",
|
||||||
@ -46,6 +49,24 @@ final class StudioProjectSessionFactoryTest {
|
|||||||
assertEquals("Example", lspFactory.capturedContext.projectName());
|
assertEquals("Example", lspFactory.capturedContext.projectName());
|
||||||
assertEquals("pbs", lspFactory.capturedContext.languageId());
|
assertEquals("pbs", lspFactory.capturedContext.languageId());
|
||||||
assertSame(vfsFactory.vfs, lspFactory.capturedVfs);
|
assertSame(vfsFactory.vfs, lspFactory.capturedVfs);
|
||||||
|
assertSame(stateService.loadedState, session.projectLocalStudioState());
|
||||||
|
assertSame(projectReference, stateService.loadedProjectReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingProjectLocalStudioStateService extends ProjectLocalStudioStateService {
|
||||||
|
private ProjectReference loadedProjectReference;
|
||||||
|
private final ProjectLocalStudioState loadedState = new ProjectLocalStudioState(
|
||||||
|
ProjectLocalStudioState.CURRENT_VERSION,
|
||||||
|
new ProjectLocalStudioState.ProjectLocalSetup("/opt/prometeu"),
|
||||||
|
ProjectLocalStudioState.ShellLayout.defaults(),
|
||||||
|
new ProjectLocalStudioState.OpenShellState("EDITOR"),
|
||||||
|
ProjectLocalStudioState.EditorRestorationState.defaults());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProjectLocalStudioState load(final ProjectReference projectReference) {
|
||||||
|
this.loadedProjectReference = projectReference;
|
||||||
|
return loadedState;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class RecordingVfsFactory implements ProjectDocumentVfsFactory {
|
private static final class RecordingVfsFactory implements ProjectDocumentVfsFactory {
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package p.studio.projectsessions;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import p.studio.lsp.messages.LspProjectContext;
|
import p.studio.lsp.messages.LspProjectContext;
|
||||||
import p.studio.lsp.LspService;
|
import p.studio.lsp.LspService;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.lsp.dtos.LspSessionStateDTO;
|
import p.studio.lsp.dtos.LspSessionStateDTO;
|
||||||
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
||||||
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
|
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
|
||||||
@ -24,19 +26,60 @@ final class StudioProjectSessionTest {
|
|||||||
void closeDelegatesToUnderlyingServicesOnlyOnce() {
|
void closeDelegatesToUnderlyingServicesOnlyOnce() {
|
||||||
final CountingVfsProjectDocument vfs = new CountingVfsProjectDocument();
|
final CountingVfsProjectDocument vfs = new CountingVfsProjectDocument();
|
||||||
final CountingPrometeuLspService lsp = new CountingPrometeuLspService(vfs);
|
final CountingPrometeuLspService lsp = new CountingPrometeuLspService(vfs);
|
||||||
final StudioProjectSession session = new StudioProjectSession(projectReference(), lsp, vfs);
|
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
||||||
|
final StudioProjectSession session = new StudioProjectSession(
|
||||||
|
projectReference(),
|
||||||
|
lsp,
|
||||||
|
vfs,
|
||||||
|
stateService,
|
||||||
|
ProjectLocalStudioState.defaults());
|
||||||
|
|
||||||
session.close();
|
session.close();
|
||||||
session.close();
|
session.close();
|
||||||
|
|
||||||
assertEquals(1, lsp.closeCalls);
|
assertEquals(1, lsp.closeCalls);
|
||||||
assertEquals(1, vfs.closeCalls);
|
assertEquals(1, vfs.closeCalls);
|
||||||
|
assertEquals(1, stateService.saveCalls);
|
||||||
|
assertEquals(projectReference(), stateService.savedProjectReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void replaceProjectLocalStudioStateUpdatesTheSavedSnapshot() {
|
||||||
|
final CountingVfsProjectDocument vfs = new CountingVfsProjectDocument();
|
||||||
|
final CountingPrometeuLspService lsp = new CountingPrometeuLspService(vfs);
|
||||||
|
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
||||||
|
final StudioProjectSession session = new StudioProjectSession(
|
||||||
|
projectReference(),
|
||||||
|
lsp,
|
||||||
|
vfs,
|
||||||
|
stateService,
|
||||||
|
ProjectLocalStudioState.defaults());
|
||||||
|
final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults()
|
||||||
|
.withProjectLocalSetup(new ProjectLocalStudioState.ProjectLocalSetup("/opt/prometeu/runtime"));
|
||||||
|
|
||||||
|
session.replaceProjectLocalStudioState(nextState);
|
||||||
|
session.close();
|
||||||
|
|
||||||
|
assertEquals("/opt/prometeu/runtime", stateService.savedState.projectLocalSetup().prometeuRuntimePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class RecordingProjectLocalStudioStateService extends ProjectLocalStudioStateService {
|
||||||
|
private int saveCalls;
|
||||||
|
private ProjectReference savedProjectReference;
|
||||||
|
private ProjectLocalStudioState savedState;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(final ProjectReference projectReference, final ProjectLocalStudioState state) {
|
||||||
|
saveCalls++;
|
||||||
|
savedProjectReference = projectReference;
|
||||||
|
savedState = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class CountingVfsProjectDocument implements VfsProjectDocument {
|
private static final class CountingVfsProjectDocument implements VfsProjectDocument {
|
||||||
private int closeCalls;
|
private int closeCalls;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,83 @@
|
|||||||
|
package p.studio.projectstate;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.projects.ProjectStudioPaths;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
final class ProjectLocalStudioStateServiceTest {
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void savesAndLoadsSingleFileProjectLocalStudioState() {
|
||||||
|
final ProjectLocalStudioStateService service = new ProjectLocalStudioStateService();
|
||||||
|
final ProjectReference project = project("main");
|
||||||
|
final ProjectLocalStudioState state = new ProjectLocalStudioState(
|
||||||
|
ProjectLocalStudioState.CURRENT_VERSION,
|
||||||
|
new ProjectLocalStudioState.ProjectLocalSetup("/opt/prometeu/runtime"),
|
||||||
|
new ProjectLocalStudioState.ShellLayout(
|
||||||
|
new ProjectLocalStudioState.EditorLayoutState(
|
||||||
|
List.of(0.25d),
|
||||||
|
List.of(0.80d),
|
||||||
|
List.of(0.70d),
|
||||||
|
new ProjectLocalStudioState.DockPanelState(true, 0.80d),
|
||||||
|
new ProjectLocalStudioState.DockPanelState(false, 0.70d),
|
||||||
|
List.of(
|
||||||
|
project.rootPath().resolve("src").toString(),
|
||||||
|
project.rootPath().resolve("src/game").toString())),
|
||||||
|
new ProjectLocalStudioState.AssetsLayoutState(List.of(0.42d))),
|
||||||
|
new ProjectLocalStudioState.OpenShellState("EDITOR"),
|
||||||
|
new ProjectLocalStudioState.EditorRestorationState(
|
||||||
|
List.of(
|
||||||
|
project.rootPath().resolve("src/main.pbs").toString(),
|
||||||
|
project.rootPath().resolve("README.md").toString()),
|
||||||
|
project.rootPath().resolve("src/main.pbs").toString()));
|
||||||
|
|
||||||
|
service.save(project, state);
|
||||||
|
|
||||||
|
assertTrue(Files.isRegularFile(ProjectStudioPaths.statePath(project)));
|
||||||
|
assertEquals(state, service.load(project));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void malformedStateFallsBackToDefaults() throws Exception {
|
||||||
|
final ProjectLocalStudioStateService service = new ProjectLocalStudioStateService();
|
||||||
|
final ProjectReference project = project("main");
|
||||||
|
Files.createDirectories(ProjectStudioPaths.studioRoot(project));
|
||||||
|
Files.writeString(ProjectStudioPaths.statePath(project), "{ not valid json");
|
||||||
|
|
||||||
|
assertEquals(ProjectLocalStudioState.defaults(), service.load(project));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unsupportedVersionFallsBackToDefaults() throws Exception {
|
||||||
|
final ProjectLocalStudioStateService service = new ProjectLocalStudioStateService();
|
||||||
|
final ProjectReference project = project("main");
|
||||||
|
Files.createDirectories(ProjectStudioPaths.studioRoot(project));
|
||||||
|
Files.writeString(
|
||||||
|
ProjectStudioPaths.statePath(project),
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"version": 99,
|
||||||
|
"projectLocalSetup": {
|
||||||
|
"prometeuRuntimePath": "/opt/legacy/runtime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertEquals(ProjectLocalStudioState.defaults(), service.load(project));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectReference project(final String name) {
|
||||||
|
final Path projectRoot = tempDir.resolve(name);
|
||||||
|
return new ProjectReference("Main", "1.0.0", "pbs", 1, projectRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import p.studio.vfs.messages.VfsDocumentAccessMode;
|
import p.studio.vfs.messages.VfsDocumentAccessMode;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@ -67,6 +68,24 @@ final class EditorOpenFileSessionTest {
|
|||||||
assertTrue(session.activeFile().orElseThrow().readOnly());
|
assertTrue(session.activeFile().orElseThrow().readOnly());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exportsRestorationStateFromOpenTabsAndActiveTab() {
|
||||||
|
final var session = new EditorOpenFileSession();
|
||||||
|
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
||||||
|
final var second = fileBuffer(Path.of("README.md"), "README.md", VfsDocumentAccessMode.EDITABLE, false, false, "b");
|
||||||
|
|
||||||
|
session.open(first);
|
||||||
|
session.open(second);
|
||||||
|
session.activate(first.path());
|
||||||
|
|
||||||
|
final var restorationState = session.restorationState();
|
||||||
|
|
||||||
|
assertEquals(List.of(
|
||||||
|
first.path().toAbsolutePath().normalize().toString(),
|
||||||
|
second.path().toAbsolutePath().normalize().toString()), restorationState.openTabPaths());
|
||||||
|
assertEquals(first.path().toAbsolutePath().normalize().toString(), restorationState.activeTabPath());
|
||||||
|
}
|
||||||
|
|
||||||
private EditorOpenFileBuffer fileBuffer(
|
private EditorOpenFileBuffer fileBuffer(
|
||||||
final Path path,
|
final Path path,
|
||||||
final String tabLabel,
|
final String tabLabel,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user