diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 1c6ed076..a0100ba9 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,5 @@ -{"type":"meta","next_id":{"DSC":31,"AGD":34,"DEC":31,"PLN":62,"LSN":47,"CLSN":1}} +{"type":"meta","next_id":{"DSC":32,"AGD":35,"DEC":32,"PLN":65,"LSN":47,"CLSN":1}} +{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]} {"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} {"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]} diff --git a/discussion/workflow/agendas/AGD-0034-vscode-editor-migration-feasibility.md b/discussion/workflow/agendas/AGD-0034-vscode-editor-migration-feasibility.md new file mode 100644 index 00000000..79011aab --- /dev/null +++ b/discussion/workflow/agendas/AGD-0034-vscode-editor-migration-feasibility.md @@ -0,0 +1,106 @@ +--- +id: AGD-0034 +ticket: studio-editor-workspace-cleanup +title: Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados +status: accepted +created: 2026-05-05 +resolved: 2026-05-05 +decision: DEC-0031 +tags: [studio, editor, cleanup, vfs, lsp, migration] +--- + +## Pain + +O repositório ainda carrega um `Workspace Editor` no `prometeu-studio` junto com camadas de `VFS` e `LSP` desenhadas especificamente para esse editor embutido. + +Esse conjunto virou um legado arquitetural: + +- o editor JavaFX não é mais a direção do produto; +- o `prometeu-vfs` atual está acoplado ao modelo editorial do editor embutido; +- o `prometeu-lsp` atual está acoplado ao `VFS` e ao host local do Studio; +- e manter essa base viva só aumenta o custo da futura reconstrução correta do backend de código. + +## Context + +- O monorepo já separa `prometeu-compiler`, `prometeu-packer` e `prometeu-studio`, mas o `prometeu-lsp` e o `prometeu-vfs` atuais nasceram como suporte ao editor embutido. +- A capacidade de compilação dos projetos deve continuar existindo no Studio. +- O alvo imediato não é discutir o novo LSP nem o novo contrato de automação. +- O alvo imediato é remover: + - o `Workspace Editor`; + - o `prometeu-lsp` legado usado por ele; + - e o `prometeu-vfs` acoplado a esse modelo editorial. +- O Studio continua existindo como produto fora do editor: + - shell e launcher de projeto; + - assets workspace; + - shipper/build surfaces; + - play/stop e integração com runtime/debug. +- Uma agenda futura poderá discutir a reconstrução do backend de código e do novo servidor LSP, mas isso fica fora deste artefato. + +## Open Questions + +- Quais partes do `prometeu-vfs` atual pertencem exclusivamente ao `Workspace Editor` e podem ser removidas integralmente? +- Existe algum consumer ainda válido do `prometeu-lsp` legado fora do editor embutido? +- O fluxo `play` ou qualquer outro fluxo do Studio depende hoje, mesmo indiretamente, de APIs do editor legado, do `VFS` legado ou do `LSP` legado? +- A limpeza deve remover apenas código de produção ou também specs, testes, lessons e referências que tratam o editor embutido como superfície ativa? +- Quais contratos de compilação precisam ser preservados explicitamente enquanto a camada de edição é removida? + +## Options + +### Option A - Manter o editor legado até a reconstrução completa do backend de código +- **Approach:** adiar a limpeza e preservar temporariamente `Workspace Editor`, `prometeu-vfs` legado e `prometeu-lsp` legado até o novo backend existir. +- **Pro:** menor ruptura imediata. +- **Con:** prolonga a vida do legado, aumenta custo de manutenção e deixa a futura reconstrução mais confusa. +- **Maintainability:** fraca. + +### Option B - Limpeza completa agora, preservando apenas a compilação do Studio +- **Approach:** remover `Workspace Editor`, `prometeu-lsp` legado e o `prometeu-vfs` acoplado a esse modelo, mantendo apenas as superfícies de compilação e execução que não dependem desse stack. +- **Pro:** reduz o legado antes da reconstrução, clarifica boundaries e impede que o futuro LSP nasça comprometido por compatibilidade acidental. +- **Con:** cria um período sem editor embutido e sem backend de código novo dentro do Studio. +- **Maintainability:** forte. + +Vamos de Option B. + +## Discussion + +O trabalho desta agenda não é desenhar o novo backend de código. + +O trabalho desta agenda é remover o legado que impediria esse redesenho. + +Isso implica alguns cortes diretos: + +- o `Workspace Editor` deixa de ser superfície ativa do Studio; +- o `prometeu-lsp` atual deixa de ser tratado como base evolutiva; +- o `prometeu-vfs` atual, por ser acoplado ao modelo editorial do editor embutido, sai junto com esse stack; +- as capacidades de compilação, build e execução que não dependem desse stack devem ser preservadas. + +O princípio aqui é de limpeza arquitetural antes de reconstrução: + +- não carregar compatibilidade acidental com o editor antigo; +- não manter um `VFS` ou `LSP` cujo contrato nasce de um modelo que já foi abandonado; +- e não misturar, nesta discussão, a limpeza do legado com o desenho do futuro servidor LSP. + +Isso também reduz o risco de falsa migração: + +- em vez de adaptar o editor atual, o repositório assume explicitamente que ele vai morrer; +- em vez de reaproveitar um `LSP` embrionário acoplado ao host local, o repositório aceita que um novo backend de código será desenhado depois; +- e em vez de preservar o `VFS` por inércia, o repositório valida se ele ainda possui valor fora do modelo que o originou. + +## Resolution + +Recomendação preliminar: + +- seguir com a Option B; +- tratar esta agenda exclusivamente como limpeza completa do `Workspace Editor` do Studio; +- remover junto o `prometeu-lsp` legado e o `prometeu-vfs` acoplado a esse modelo; +- preservar explicitamente a capacidade de compilação e os fluxos do Studio que não dependem desse stack; +- e abrir uma discussão futura separada para o backend de código novo, o novo LSP e o contrato de automação. + +## Next Step + +Se a direção fizer sentido, o próximo passo disciplinado é fechar uma `decision` com: + +1. o escopo exato da remoção do `Workspace Editor`; +2. o escopo exato da remoção do `prometeu-lsp` legado; +3. o escopo exato da remoção do `prometeu-vfs` acoplado a esse modelo; +4. as capacidades de compilação e execução que devem sobreviver intactas; +5. e os artefatos normativos, testes e código que precisam ser limpos ou preservados. diff --git a/discussion/workflow/decisions/DEC-0031-studio-editor-workspace-cleanup.md b/discussion/workflow/decisions/DEC-0031-studio-editor-workspace-cleanup.md new file mode 100644 index 00000000..44777193 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0031-studio-editor-workspace-cleanup.md @@ -0,0 +1,148 @@ +--- +id: DEC-0031 +ticket: studio-editor-workspace-cleanup +title: Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados +status: in_progress +created: 2026-05-05 +accepted: 2026-05-05 +agenda: AGD-0034 +plans: [PLN-0062, PLN-0063, PLN-0064] +tags: [studio, editor, cleanup, vfs, lsp, migration] +--- + +## Decision + +O repositório SHALL remover integralmente o `Workspace Editor` do Studio como superfície ativa do produto. + +Essa remoção SHALL incluir: + +1. o workspace de código embutido em `prometeu-studio`; +2. o `prometeu-lsp` legado usado por esse workspace; +3. o `prometeu-vfs` legado acoplado ao modelo editorial desse workspace; +4. os pontos de integração do shell, da sessão de projeto e do bootstrap que assumem a existência desse stack; +5. e os artefatos normativos, testes e referências documentais que tratam esse stack como superfície vigente. + +Ao mesmo tempo, esta decisão ALSO locks que: + +1. a capacidade de compilação dos projetos MUST permanecer disponível no Studio; +2. os fluxos atuais de `build`, `play`, `run` e `debug` MUST permanecer funcionais desde que não dependam do editor legado, do `prometeu-lsp` legado ou do `prometeu-vfs` legado; +3. esta decisão MUST NOT introduzir o novo backend de código, o novo servidor LSP ou o futuro contrato de automação; +4. qualquer reconstrução dessas superfícies MUST acontecer em discussão posterior e separada. + +## Rationale + +O `Workspace Editor`, o `prometeu-lsp` atual e o `prometeu-vfs` atual formam um stack único, criado especificamente para o editor embutido do Studio. + +Esse stack não é mais alinhado à direção do produto: + +1. o editor JavaFX deixou de ser a direção principal; +2. o `prometeu-lsp` atual não deve ser tratado como base séria para um futuro servidor LSP; +3. o `prometeu-vfs` atual carrega um contrato editorial pensado para o editor embutido; +4. manter esse legado vivo aumenta o custo de reconstrução e preserva boundaries errados por inércia. + +Portanto, a limpeza precisa acontecer antes da reconstrução. + +## Technical Specification + +### 1. Editor Workspace Removal + +O Studio MUST remover a superfície `Code Editor` do shell. + +Isso inclui: + +1. remover `WorkspaceId.EDITOR`; +2. remover o `EditorWorkspace` e todos os controles, painéis, modelos e helpers sob `prometeu-studio/.../workspaces/editor`; +3. remover a presença do editor na workspace rail, no layout principal e no fluxo de restauração de shell; +4. remover strings, ícones e estado visual exclusivos dessa superfície. + +### 2. Legacy LSP Removal + +O repositório MUST remover o `prometeu-lsp` legado como módulo ativo. + +Isso inclui: + +1. remover os módulos `prometeu-lsp-api` e `prometeu-lsp-v1`; +2. remover `LspService`, `LspServiceFactory` e todos os DTOs/messages específicos desse stack legado; +3. remover a criação e injeção dessas dependências no `AppContainer`, no `Container`, na sessão de projeto e nos testes associados; +4. remover a hipótese de que esse `LSP` será evoluído para o futuro backend de código. + +Os conceitos úteis descobertos nesse stack MAY ser reaproveitados mais tarde, mas esta decisão não preserva compatibilidade de código, pacote ou contrato com ele. + +### 3. Legacy VFS Removal + +O repositório MUST remover o `prometeu-vfs` legado como módulo ativo. + +Isso inclui: + +1. remover os módulos `prometeu-vfs-api` e `prometeu-vfs-v1`; +2. remover `VfsProjectDocument`, `ProjectDocumentVfsFactory` e implementações filesystem-backed associadas; +3. remover a criação e posse desse `VFS` na sessão de projeto; +4. remover qualquer regra que trate esse `VFS` como boundary documental vigente do Studio. + +Esta decisão assume que o `prometeu-vfs` atual pertence ao modelo do editor legado e, portanto, não deve sobreviver por compatibilidade acidental. + +### 4. Session and Shell Cleanup + +O Studio MUST reconfigurar a sessão de projeto e o shell principal para não depender do stack removido. + +Isso inclui: + +1. remover `LSP` e `VFS` legados da estrutura de `StudioProjectSession`; +2. remover sua criação em `StudioProjectSessionFactory`; +3. remover dependências globais correspondentes do bootstrap e do container; +4. remover persistência de layout e restauração editorial específicas do editor. + +### 5. State Cleanup + +O estado local do projeto MUST deixar de persistir dados exclusivos do editor legado. + +Isso inclui: + +1. remover `editorRestoration`; +2. remover `shellLayout.editorLayout`; +3. garantir fallback seguro para estados antigos já persistidos em `.studio/state.json`; +4. preservar apenas os campos ainda pertencentes a workspaces vivos e ao shell remanescente. + +### 6. Documentation and Test Cleanup + +O repositório MUST limpar artefatos normativos e testes que descrevem o stack legado como vigente. + +Isso inclui: + +1. revisar ou remover as specs do `Code Editor`, do `Project Document VFS` e do `Integrated LSP Semantic Read Phase`; +2. revisar `docs/specs/studio/README.md`; +3. remover ou arquivar testes exclusivos do editor, do `prometeu-lsp` legado e do `prometeu-vfs` legado; +4. revisar lessons e referências históricas para deixar explícito que se tratam de legado, quando forem mantidas. + +### 7. Capabilities That MUST Survive + +As seguintes capacidades MUST sobreviver a esta limpeza: + +1. compilação de projetos pelo pipeline canônico existente; +2. build de artefatos via `BuilderPipelineService`; +3. preparação do fluxo de shipper; +4. `play`, `run` e `debug` do Studio, desde que apoiados nas superfícies remanescentes e não no stack removido; +5. workspaces `Assets`, `Shipper` e `Debug`, além do launcher e do shell geral. + +### 8. Explicit Non-Goals + +Esta decisão MUST NOT: + +1. definir o novo servidor LSP; +2. definir o novo contrato de automação; +3. definir o novo modelo de sessão de código; +4. definir integração com VS Code; +5. reter compatibilidade com o `LSP` ou `VFS` legados por conveniência. + +## Constraints + +1. A limpeza MUST preservar o pipeline canônico `analyze`, `compile` e `build` já existente no compiler. +2. A limpeza MUST evitar remover ou degradar fluxos de `build/run/debug` que hoje dependem apenas de `compiler`, `shipper`, runtime e shell. +3. A limpeza MUST deixar o shell do Studio em estado coerente, sem workspace fantasma, sem layout órfão e sem dependências mortas. +4. A limpeza MUST tratar specs e estado persistido como parte do trabalho, não como detalhe pós-código. +5. A futura reconstrução do backend de código SHALL nascer em discussão separada, sem herdar obrigação de compatibilidade com o stack removido. + +## Revision Log + +- 2026-05-05: Initial draft from AGD-0034. +- 2026-05-05: Accepted and decomposed into PLN-0062, PLN-0063, and PLN-0064. diff --git a/discussion/workflow/plans/PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md b/discussion/workflow/plans/PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md new file mode 100644 index 00000000..31268083 --- /dev/null +++ b/discussion/workflow/plans/PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md @@ -0,0 +1,91 @@ +--- +id: PLN-0062 +ticket: studio-editor-workspace-cleanup +title: Shell Session and State Cleanup After Editor Removal +status: done +created: 2026-05-05 +completed: 2026-05-05 +tags: [studio, editor, cleanup, shell, session, state] +--- + +## Objective + +Remove the `Code Editor` workspace from the Studio shell and reconfigure project session and persisted state so the application remains coherent without any editor-owned concepts. + +## Background + +`DEC-0031` locks the complete removal of the embedded editor stack. The shell, project session, workspace registry, and `.studio/state.json` currently still assume the existence of `WorkspaceId.EDITOR`, `EditorWorkspace`, editor restoration state, and editor-specific layout state. + +## Scope + +### Included +- Remove `WorkspaceId.EDITOR` from the Studio shell. +- Remove `EditorWorkspace` registration and shell integration. +- Remove editor-specific state persistence and restoration paths. +- Remove `LSP`/`VFS` legacy ownership from `StudioProjectSession` and `StudioProjectSessionFactory`. +- Keep `Assets`, `Shipper`, `Debug`, launcher, and shell flows functional. + +### Excluded +- Removal of the `prometeu-lsp` and `prometeu-vfs` module source trees themselves. +- Documentation and lesson cleanup. +- New code backend or replacement LSP architecture. + +## Execution Steps + +### Step 1 - Remove editor workspace registration from the shell + +**What:** Remove all shell-level references to the `Code Editor` workspace. +**How:** Update the main window composition to stop creating `EditorWorkspace`, remove the editor rail item, stop selecting or restoring `WorkspaceId.EDITOR`, and ensure shell fallback remains stable when old persisted state still references the editor. +**File(s):** `prometeu-studio/src/main/java/p/studio/window/MainView.java`, `prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java`, any directly related shell controls or selectors. + +### Step 2 - Remove editor-owned state and restoration from project-local state + +**What:** Remove editor layout and restoration data from persisted project-local Studio state. +**How:** Simplify `ProjectLocalStudioState` to remove `editorRestoration` and `shellLayout.editorLayout`, update normalization/defaults, and make loading old `.studio/state.json` degrade safely without crashes or stale editor assumptions. +**File(s):** `prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java`, `prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioStateService.java`, any callers that write or restore editor state. + +### Step 3 - Reconfigure project session to stop owning editor legacy dependencies + +**What:** Remove legacy editor-driven `LSP` and `VFS` ownership from project session composition. +**How:** Refactor `StudioProjectSession` and `StudioProjectSessionFactory` so opening a project no longer constructs `VfsProjectDocument` or `LspService`, and so close semantics remain valid without those resources. +**File(s):** `prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java`, `prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java`, dependent tests. + +### Step 4 - Remove bootstrap/container dependencies that only existed for the editor stack + +**What:** Remove application bootstrap and container seams that exist only to inject legacy editor stack services. +**How:** Delete `LspServiceFactory` and `ProjectDocumentVfsFactory` accessors from the container surfaces once shell/session no longer depend on them, and update `AppContainer` accordingly. +**File(s):** `prometeu-studio/src/main/java/p/studio/Container.java`, `prometeu-app/src/main/java/p/studio/AppContainer.java`, directly affected tests. + +## Test Requirements + +### Unit Tests +- Update session and state tests to reflect the absence of editor state. +- Add coverage for loading old persisted project-local state without editor restoration fields being required. + +### Integration Tests +- Verify the main Studio shell opens a project and activates a surviving workspace without editor registration. +- Verify session close still saves valid project-local state and shuts down surviving services cleanly. + +### Manual Verification +- Open the Studio shell and confirm only surviving workspaces appear. +- Reopen a project with stale `.studio/state.json` containing editor data and confirm safe fallback. +- Confirm project switching and shell startup remain stable. + +## Acceptance Criteria + +- [x] `WorkspaceId.EDITOR` no longer exists in production code. +- [x] `MainView` no longer instantiates or restores `EditorWorkspace`. +- [x] Project-local state no longer persists editor restoration or editor layout fields. +- [x] `StudioProjectSession` no longer owns legacy editor `LSP`/`VFS` resources. +- [x] The Studio shell remains functional with surviving workspaces. + +## Dependencies + +- `DEC-0031` accepted and normatively locked. +- Knowledge of all shell/session/state call sites that currently assume the editor exists. + +## Risks + +- Shell restoration may break if old persisted state is not normalized defensively. +- Session teardown may regress if hidden resource ownership remains after field removal. +- Removing editor assumptions too early may temporarily orphan callers still using editor-specific state APIs. diff --git a/discussion/workflow/plans/PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md b/discussion/workflow/plans/PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md new file mode 100644 index 00000000..1429643b --- /dev/null +++ b/discussion/workflow/plans/PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md @@ -0,0 +1,92 @@ +--- +id: PLN-0063 +ticket: studio-editor-workspace-cleanup +title: Legacy LSP and VFS Module Removal with Runtime Preservation +status: open +created: 2026-05-05 +completed: +tags: [studio, editor, cleanup, lsp, vfs, runtime] +--- + +## Objective + +Remove the legacy `prometeu-lsp` and `prometeu-vfs` modules and all remaining code references to them while preserving compiler, shipper, play, run, and debug flows that do not depend on the editor stack. + +## Background + +`DEC-0031` treats the current `prometeu-lsp` and `prometeu-vfs` modules as part of the embedded editor legacy stack. The Studio execution path must continue to compile, build, package, and run projects through the existing compiler and runtime orchestration layers. + +## Scope + +### Included +- Remove `prometeu-lsp` modules from Gradle settings and codebase references. +- Remove `prometeu-vfs` modules from Gradle settings and codebase references. +- Remove tests dedicated to these legacy modules. +- Verify `StudioShipperService`, `StudioPlayStopCoordinator`, and runtime/debug flows remain intact. + +### Excluded +- Shell/editor state cleanup handled by a separate plan. +- Documentation/spec/lesson cleanup handled by a separate plan. +- New replacement backend services. + +## Execution Steps + +### Step 1 - Remove Gradle module registration for legacy editor stack modules + +**What:** Stop building the legacy `prometeu-lsp` and `prometeu-vfs` modules. +**How:** Remove the module includes and any direct module dependencies that point to `prometeu-lsp:*` and `prometeu-vfs:*`, then fix resulting build graph fallout in Studio modules. +**File(s):** `settings.gradle.kts`, affected `build.gradle.kts` files in `prometeu-app`, `prometeu-studio`, and module directories. + +### Step 2 - Remove legacy module source trees and direct test coverage + +**What:** Delete the legacy `prometeu-lsp` and `prometeu-vfs` code once all consumers are detached. +**How:** Remove module source files and their dedicated tests only after production call sites no longer compile against them. +**File(s):** `prometeu-lsp/**`, `prometeu-vfs/**`, related test sources in `prometeu-studio`. + +### Step 3 - Preserve compiler and shipper build flow + +**What:** Ensure build preparation still uses the canonical compiler pipeline without editor legacy dependencies. +**How:** Validate and adjust `StudioShipperService` and any shipper/build callers so they continue using `BuilderPipelineService`, `BuilderPipelineConfig`, and `BuilderPipelineContext` directly without reintroducing removed editor-layer seams. +**File(s):** `prometeu-studio/src/main/java/p/studio/shipper/StudioShipperService.java`, affected compiler/shipper integration tests. + +### Step 4 - Preserve play, run, and debug execution flow + +**What:** Ensure runtime orchestration continues to function after legacy module removal. +**How:** Re-test and patch `StudioPlayStopCoordinator`, runtime handshake services, execution session handling, and debug workspace projections if any hidden import or event dependency pointed at removed editor-stack modules. +**File(s):** `prometeu-studio/src/main/java/p/studio/execution/StudioPlayStopCoordinator.java`, `prometeu-studio/src/main/java/p/studio/debug/runtime/**`, `prometeu-studio/src/main/java/p/studio/workspaces/debug/**`, related tests. + +## Test Requirements + +### Unit Tests +- Update or remove module-specific tests for `prometeu-lsp` and `prometeu-vfs`. +- Preserve unit coverage for execution session, play/stop coordination, and runtime handshake behavior. + +### Integration Tests +- Run compiler pipeline integration tests that cover `analyze`, `compile`, and `build`. +- Run shipper/build integration tests that exercise `StudioShipperService`. +- Run runtime/debug integration or coordinator tests that validate play/stop behavior. + +### Manual Verification +- Trigger Studio build flow and confirm artifact generation still works. +- Trigger Studio play flow and confirm runtime launch/stop still works. +- Open Debug and Shipper workspaces and confirm they remain operational. + +## Acceptance Criteria + +- [ ] `prometeu-lsp` is no longer part of the build graph. +- [ ] `prometeu-vfs` is no longer part of the build graph. +- [ ] No production Studio class imports legacy `LSP` or `VFS` module types. +- [ ] Compiler and shipper build flows still work. +- [ ] Play/run/debug flows still work without the removed modules. + +## Dependencies + +- `DEC-0031` accepted and normatively locked. +- Shell/session cleanup completed enough that no surviving production code still requires legacy `LSP`/`VFS` types. + +## Risks + +- Hidden transitive dependencies may keep the build graph coupled to removed modules longer than expected. +- Runtime or debug flows may have indirect event or session assumptions not obvious from imports alone. +- Deleting modules too early may obscure compile errors that should instead be fixed in surviving Studio code first. + diff --git a/discussion/workflow/plans/PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md b/discussion/workflow/plans/PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md new file mode 100644 index 00000000..1572f13a --- /dev/null +++ b/discussion/workflow/plans/PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md @@ -0,0 +1,91 @@ +--- +id: PLN-0064 +ticket: studio-editor-workspace-cleanup +title: Normative Test and Lesson Cleanup for Editor Stack Removal +status: open +created: 2026-05-05 +completed: +tags: [studio, editor, cleanup, docs, tests, lessons] +--- + +## Objective + +Remove or revise the normative documentation, tests, lessons, and user-facing resources that describe the embedded editor stack as a current Studio surface. + +## Background + +`DEC-0031` treats the editor stack removal as both code cleanup and normative cleanup. The repository currently contains active specs, README entries, i18n resources, and lessons that define `Code Editor`, `prometeu-vfs`, and the integrated semantic-read phase as active architecture. + +## Scope + +### Included +- Remove or rewrite Studio specs dedicated to the embedded editor stack. +- Update the Studio specs index and README. +- Remove or archive editor/LSP/VFS-specific tests that are no longer valid. +- Reclassify legacy lessons and references so they are not presented as active guidance. +- Remove user-facing i18n strings that only existed for the editor stack. + +### Excluded +- Implementation of shell/session/module cleanup itself. +- Future specs for the replacement backend or replacement external-editor strategy. + +## Execution Steps + +### Step 1 - Clean normative Studio specs + +**What:** Remove or replace specs that define the embedded editor stack as active architecture. +**How:** Delete or explicitly retire the `Code Editor`, `Project Document VFS`, and `Integrated LSP Semantic Read Phase` specs, then rewrite any remaining Studio README/index material so it no longer points to dead architecture. +**File(s):** `docs/specs/studio/5. Code Editor Workspace Specification.md`, `docs/specs/studio/6. Project Document VFS Specification.md`, `docs/specs/studio/7. Integrated LSP Semantic Read Phase Specification.md`, `docs/specs/studio/README.md`. + +### Step 2 - Clean user-facing resources and shell references + +**What:** Remove editor-specific strings, labels, and messages that no longer correspond to product surfaces. +**How:** Delete `workspace.code` and `codeEditor.*` message keys and any equivalent theme or resource references still tied to removed editor UI. +**File(s):** `prometeu-studio/src/main/resources/i18n/messages.properties`, any directly related theme or resource files. + +### Step 3 - Clean tests and update surviving expectations + +**What:** Remove tests whose sole purpose was validating the editor stack and update remaining tests to stop expecting that stack. +**How:** Delete or rewrite tests for editor workspace behavior, legacy `LSP`, legacy `VFS`, session expectations, and any shell assertions that still require the editor workspace. +**File(s):** `prometeu-studio/src/test/java/p/studio/projectsessions/**`, editor-related tests, `prometeu-lsp/**/test/**`, `prometeu-vfs/**/test/**`, any shell-level tests affected by workspace removal. + +### Step 4 - Reclassify lessons and historical references + +**What:** Ensure discussion lessons remain historically useful without being mistaken for current architecture. +**How:** Review the lessons and references tied to editor/VFS/LSP waves and either archive them, mark them as legacy, or update index/readme material so they are not treated as active operational guidance. +**File(s):** affected files under `discussion/lessons/DSC-0010*`, `DSC-0012*`, `DSC-0013*`, `DSC-0014*`, `DSC-0016*`, `DSC-0017*`, `DSC-0020*`, `DSC-0021*`, plus any summary/index surfaces that present them. + +## Test Requirements + +### Unit Tests +- Ensure no surviving test suite still references removed editor stack types or message keys. +- Add assertions where needed that remaining shell/resource state is internally consistent. + +### Integration Tests +- Validate the Studio docs/spec tree remains internally linked after removal or retirement of the editor specs. +- Validate the build/test graph no longer includes editor-stack-only suites. + +### Manual Verification +- Read the Studio spec README and confirm it no longer advertises the removed stack as active. +- Inspect project lessons/readme surfaces and confirm editor-stack lessons are clearly historical or retired. +- Launch the application and confirm no missing i18n keys or dead UI labels remain. + +## Acceptance Criteria + +- [ ] Studio specs no longer define the embedded editor stack as an active surface. +- [ ] Studio README/index material no longer points to removed editor/VFS/LSP architecture as current. +- [ ] Editor-specific i18n keys are removed or no longer consumed. +- [ ] Obsolete tests are removed or rewritten. +- [ ] Historical lessons are clearly demoted from active operational guidance. + +## Dependencies + +- `DEC-0031` accepted and normatively locked. +- Sufficient implementation progress that the codebase no longer depends on the removed specs/resources/tests. + +## Risks + +- Removing normative docs too early may temporarily leave the repo without clear current guidance. +- Historical lessons may still be valuable; deleting them outright could lose context that should instead be reclassified. +- Partial cleanup can leave broken links or missing i18n keys that only appear at runtime. + diff --git a/prometeu-app/src/main/java/p/studio/AppContainer.java b/prometeu-app/src/main/java/p/studio/AppContainer.java index 44e825d3..ec86a4b9 100644 --- a/prometeu-app/src/main/java/p/studio/AppContainer.java +++ b/prometeu-app/src/main/java/p/studio/AppContainer.java @@ -2,14 +2,10 @@ package p.studio; import com.fasterxml.jackson.databind.ObjectMapper; import p.packer.Packer; -import p.studio.lsp.LspServiceFactory; -import p.studio.lsp.LspServiceFactoryImpl; import p.studio.lsp.events.StudioEventBus; import p.studio.lsp.events.StudioPackerEventAdapter; import p.studio.utilities.ThemeService; import p.studio.utilities.i18n.I18nService; -import p.studio.vfs.FilesystemProjectDocumentVfsFactory; -import p.studio.vfs.ProjectDocumentVfsFactory; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -21,8 +17,6 @@ public final class AppContainer implements Container { private final ThemeService themeService; private final StudioEventBus studioEventBus; private final ObjectMapper mapper; - private final LspServiceFactory lspServiceFactory; - private final ProjectDocumentVfsFactory projectDocumentVfsFactory; private final EmbeddedPacker embeddedPacker; private final StudioBackgroundTasks backgroundTasks; @@ -31,8 +25,6 @@ public final class AppContainer implements Container { this.themeService = new ThemeService(); this.studioEventBus = new StudioEventBus(); this.mapper = new ObjectMapper(); - this.lspServiceFactory = new LspServiceFactoryImpl(); - this.projectDocumentVfsFactory = new FilesystemProjectDocumentVfsFactory(); final ExecutorService backgroundExecutor = Executors.newFixedThreadPool(2, new StudioWorkerThreadFactory()); this.backgroundTasks = new StudioBackgroundTasks(backgroundExecutor); final Packer packer = Packer.bootstrap(this.mapper, new StudioPackerEventAdapter(studioEventBus)); @@ -59,16 +51,6 @@ public final class AppContainer implements Container { return mapper; } - @Override - public LspServiceFactory getPrometeuLspServiceFactory() { - return lspServiceFactory; - } - - @Override - public ProjectDocumentVfsFactory getProjectDocumentVfsFactory() { - return projectDocumentVfsFactory; - } - @Override public EmbeddedPacker getPacker() { return embeddedPacker; diff --git a/prometeu-studio/src/main/java/p/studio/Container.java b/prometeu-studio/src/main/java/p/studio/Container.java index a5a226da..2e9cae4e 100644 --- a/prometeu-studio/src/main/java/p/studio/Container.java +++ b/prometeu-studio/src/main/java/p/studio/Container.java @@ -1,11 +1,9 @@ package p.studio; import com.fasterxml.jackson.databind.ObjectMapper; -import p.studio.lsp.LspServiceFactory; import p.studio.lsp.events.StudioEventBus; import p.studio.utilities.ThemeService; import p.studio.utilities.i18n.I18nService; -import p.studio.vfs.ProjectDocumentVfsFactory; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; @@ -19,10 +17,6 @@ public interface Container { ObjectMapper getMapper(); - LspServiceFactory getPrometeuLspServiceFactory(); - - ProjectDocumentVfsFactory getProjectDocumentVfsFactory(); - EmbeddedPacker getPacker(); StudioBackgroundTasks getBackgroundTasks(); @@ -66,14 +60,6 @@ public interface Container { return current().getMapper(); } - static LspServiceFactory prometeuLspServiceFactory() { - return current().getPrometeuLspServiceFactory(); - } - - static ProjectDocumentVfsFactory projectDocumentVfsFactory() { - return current().getProjectDocumentVfsFactory(); - } - static EmbeddedPacker packer() { return current().getPacker(); } diff --git a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java index 68401342..af36c5c7 100644 --- a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java +++ b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java @@ -1,19 +1,15 @@ package p.studio.projectsessions; import p.studio.execution.StudioExecutionSessionService; -import p.studio.lsp.LspService; import p.studio.projectstate.ProjectLocalStudioSetup; import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioStateService; import p.studio.projects.ProjectReference; -import p.studio.vfs.VfsProjectDocument; import java.util.Objects; public final class StudioProjectSession implements AutoCloseable { private final ProjectReference projectReference; - private final LspService prometeuLspService; - private final VfsProjectDocument vfsProjectDocument; private final ProjectLocalStudioStateService projectLocalStudioStateService; private final ProjectLocalStudioSetup projectLocalStudioSetup; private final StudioExecutionSessionService executionSessionService; @@ -21,13 +17,9 @@ public final class StudioProjectSession implements AutoCloseable { private boolean closed; public StudioProjectSession( - final ProjectReference projectReference, - final LspService prometeuLspService, - final VfsProjectDocument vfsProjectDocument) { + final ProjectReference projectReference) { this( projectReference, - prometeuLspService, - vfsProjectDocument, new ProjectLocalStudioStateService(), ProjectLocalStudioSetup.defaults(), new StudioExecutionSessionService(), @@ -36,15 +28,11 @@ public final class StudioProjectSession implements AutoCloseable { StudioProjectSession( final ProjectReference projectReference, - final LspService prometeuLspService, - final VfsProjectDocument vfsProjectDocument, final ProjectLocalStudioStateService projectLocalStudioStateService, final ProjectLocalStudioSetup projectLocalStudioSetup, final StudioExecutionSessionService executionSessionService, final ProjectLocalStudioState projectLocalStudioState) { this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); - this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService"); - this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"); this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService"); this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup"); this.executionSessionService = Objects.requireNonNull(executionSessionService, "executionSessionService"); @@ -55,14 +43,6 @@ public final class StudioProjectSession implements AutoCloseable { return projectReference; } - public VfsProjectDocument projectDocumentVfs() { - return vfsProjectDocument; - } - - public LspService prometeuLspService() { - return prometeuLspService; - } - public ProjectLocalStudioState projectLocalStudioState() { return projectLocalStudioState; } @@ -96,25 +76,13 @@ public final class StudioProjectSession implements AutoCloseable { failure = runtimeException; } try { - prometeuLspService.close(); - } catch (RuntimeException runtimeException) { - if (failure == null) { - failure = runtimeException; - } else { - failure.addSuppressed(runtimeException); + if (failure != null) { + throw failure; } - } - try { - vfsProjectDocument.close(); - } catch (RuntimeException runtimeException) { - if (failure == null) { - failure = runtimeException; - } else { - failure.addSuppressed(runtimeException); + } finally { + if (failure != null) { + throw failure; } } - if (failure != null) { - throw failure; - } } } diff --git a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java index cedb35c5..2dde9464 100644 --- a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java +++ b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java @@ -1,60 +1,36 @@ package p.studio.projectsessions; import p.studio.execution.StudioExecutionSessionService; -import p.studio.lsp.messages.LspProjectContext; -import p.studio.lsp.LspServiceFactory; import p.studio.projectstate.ProjectLocalStudioSetupService; import p.studio.projectstate.ProjectLocalStudioStateService; import p.studio.projects.ProjectReference; -import p.studio.vfs.VfsProjectDocument; -import p.studio.vfs.ProjectDocumentVfsFactory; import java.util.Objects; public final class StudioProjectSessionFactory { - private final LspServiceFactory lspServiceFactory; - private final ProjectDocumentVfsFactory projectDocumentVfsFactory; private final ProjectLocalStudioStateService projectLocalStudioStateService; private final ProjectLocalStudioSetupService projectLocalStudioSetupService; - public StudioProjectSessionFactory( - final LspServiceFactory lspServiceFactory, - final ProjectDocumentVfsFactory projectDocumentVfsFactory) { + public StudioProjectSessionFactory() { this( - lspServiceFactory, - projectDocumentVfsFactory, new ProjectLocalStudioStateService(), new ProjectLocalStudioSetupService()); } StudioProjectSessionFactory( - final LspServiceFactory lspServiceFactory, - final ProjectDocumentVfsFactory projectDocumentVfsFactory, final ProjectLocalStudioStateService projectLocalStudioStateService, final ProjectLocalStudioSetupService projectLocalStudioSetupService) { - this.lspServiceFactory = Objects.requireNonNull(lspServiceFactory, "prometeuLspServiceFactory"); - this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory"); this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService"); this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService"); } public StudioProjectSession open(final ProjectReference projectReference) { final ProjectReference target = Objects.requireNonNull(projectReference, "projectReference"); - final VfsProjectDocument vfsProjectDocument = projectDocumentVfsFactory.open(target.toVfsProjectContext()); return new StudioProjectSession( target, - lspServiceFactory.open(lspProjectContext(target), vfsProjectDocument), - vfsProjectDocument, projectLocalStudioStateService, projectLocalStudioSetupService.load(target), new StudioExecutionSessionService(), projectLocalStudioStateService.load(target)); } - - private LspProjectContext lspProjectContext(final ProjectReference projectReference) { - return new LspProjectContext( - projectReference.name(), - projectReference.languageId(), - projectReference.rootPath()); - } } diff --git a/prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java b/prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java index 7bc6278d..120b6c36 100644 --- a/prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java +++ b/prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java @@ -3,57 +3,48 @@ 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, ShellLayout shellLayout, - OpenShellState openShellState, - EditorRestorationState editorRestoration) { + OpenShellState openShellState) { public static final int CURRENT_VERSION = 1; public ProjectLocalStudioState { version = version <= 0 ? CURRENT_VERSION : version; 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, ShellLayout.defaults(), - OpenShellState.defaults(), - EditorRestorationState.defaults()); + OpenShellState.defaults()); } public ProjectLocalStudioState withShellLayout(final ShellLayout nextShellLayout) { - return new ProjectLocalStudioState(version, nextShellLayout, openShellState, editorRestoration); + return new ProjectLocalStudioState(version, nextShellLayout, openShellState); } public ProjectLocalStudioState withOpenShellState(final OpenShellState nextOpenShellState) { - return new ProjectLocalStudioState(version, shellLayout, nextOpenShellState, editorRestoration); - } - - public ProjectLocalStudioState withEditorRestoration(final EditorRestorationState nextEditorRestoration) { - return new ProjectLocalStudioState(version, shellLayout, openShellState, nextEditorRestoration); + return new ProjectLocalStudioState(version, shellLayout, nextOpenShellState); } @JsonIgnoreProperties(ignoreUnknown = true) - public record ShellLayout(EditorLayoutState editorLayout, AssetsLayoutState assetsLayout) { + public record ShellLayout(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()); + return new ShellLayout(AssetsLayoutState.defaults()); } private ShellLayout normalize() { - return new ShellLayout(editorLayout, assetsLayout); + return new ShellLayout(assetsLayout); } } @@ -76,6 +67,9 @@ public record ProjectLocalStudioState( public record OpenShellState(String selectedWorkspaceId) { public OpenShellState { selectedWorkspaceId = normalizeText(selectedWorkspaceId); + if ("EDITOR".equals(selectedWorkspaceId)) { + selectedWorkspaceId = "ASSETS"; + } } public static OpenShellState defaults() { @@ -87,80 +81,6 @@ public record ProjectLocalStudioState( } } - @JsonIgnoreProperties(ignoreUnknown = true) - public record EditorLayoutState( - List contentSplitDividers, - List leftColumnDividers, - List rightColumnDividers, - DockPanelState outlinePanel, - DockPanelState helperPanel, - List 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 openTabPaths, String activeTabPath) { - public EditorRestorationState { - openTabPaths = normalizePaths(openTabPaths); - activeTabPath = normalizeText(activeTabPath); - if (activeTabPath != null && !openTabPaths.contains(activeTabPath)) { - final List 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; @@ -169,20 +89,6 @@ public record ProjectLocalStudioState( return trimmed.isEmpty() ? null : trimmed; } - private static List normalizePaths(final List values) { - if (values == null || values.isEmpty()) { - return List.of(); - } - final LinkedHashSet 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 normalizeDividerPositions(final List values) { if (values == null || values.isEmpty()) { return List.of(); @@ -196,11 +102,4 @@ public record ProjectLocalStudioState( } 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)); - } } diff --git a/prometeu-studio/src/main/java/p/studio/window/MainView.java b/prometeu-studio/src/main/java/p/studio/window/MainView.java index 5a1a89d8..3fa406e6 100644 --- a/prometeu-studio/src/main/java/p/studio/window/MainView.java +++ b/prometeu-studio/src/main/java/p/studio/window/MainView.java @@ -15,7 +15,6 @@ import p.studio.workspaces.WorkspaceId; import p.studio.workspaces.assets.AssetWorkspace; import p.studio.workspaces.builder.ShipperWorkspace; import p.studio.workspaces.debug.DebugWorkspace; -import p.studio.workspaces.editor.EditorWorkspace; import java.util.List; @@ -24,7 +23,6 @@ public final class MainView extends BorderPane { private final ProjectReference projectReference; private final StudioProjectSession projectSession; private final AssetWorkspace assetWorkspace; - private final EditorWorkspace editorWorkspace; private final StudioWorkspaceRailControl workspaceRail; private final StudioPlayStopCoordinator playStopCoordinator; private boolean initializing; @@ -50,23 +48,14 @@ public final class MainView extends BorderPane { assetWorkspace = new AssetWorkspace(projectReference); host.register(assetWorkspace); - editorWorkspace = new EditorWorkspace( - projectReference, - projectSession.projectDocumentVfs(), - projectSession.prometeuLspService(), - projectSetup.editorIndentation()); - host.register(editorWorkspace); assetWorkspace.restoreProjectLocalState(persistedState); - editorWorkspace.restoreProjectLocalState(persistedState); assetWorkspace.setStateChangedAction(this::persistProjectLocalState); - editorWorkspace.setStateChangedAction(this::persistProjectLocalState); host.register(new DebugWorkspace(projectReference, projectSession.executionSession())); host.register(new ShipperWorkspace(projectReference)); workspaceRail = new StudioWorkspaceRailControl<>( List.of( new StudioWorkspaceRailItem<>(WorkspaceId.ASSETS, "📦", Container.i18n().bind(I18n.WORKSPACE_ASSETS)), - new StudioWorkspaceRailItem<>(WorkspaceId.EDITOR, "📝", Container.i18n().bind(I18n.WORKSPACE_CODE)), new StudioWorkspaceRailItem<>(WorkspaceId.DEBUG, "🎮", Container.i18n().bind(I18n.WORKSPACE_DEBUG)), new StudioWorkspaceRailItem<>(WorkspaceId.SHIPPER, "⚙️", Container.i18n().bind(I18n.WORKSPACE_SHIPPER)) ), @@ -102,14 +91,11 @@ public final class MainView extends BorderPane { projectSession.replaceProjectLocalStudioState( current.withOpenShellState(new ProjectLocalStudioState.OpenShellState(selectedWorkspace.name())) .withShellLayout(new ProjectLocalStudioState.ShellLayout( - editorWorkspace.captureLayoutState(), - assetWorkspace.captureLayoutState())) - .withEditorRestoration(editorWorkspace.captureRestorationState())); + assetWorkspace.captureLayoutState()))); } public void reapplyProjectLocalLayout() { assetWorkspace.reapplyPendingLayoutState(); - editorWorkspace.reapplyPendingLayoutState(); } public void close() { diff --git a/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java b/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java index e198201b..b0be78b2 100644 --- a/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java +++ b/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java @@ -37,9 +37,7 @@ public final class StudioWindowCoordinator { this.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot()); this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService); this.windowStateService = new WindowStateService(resolveWindowStateStorage()); - this.projectSessionFactory = new StudioProjectSessionFactory( - Container.prometeuLspServiceFactory(), - Container.projectDocumentVfsFactory()); + this.projectSessionFactory = new StudioProjectSessionFactory(); this.launcherView = new ProjectLauncherView( knownProjectsService, projectCatalogService, diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java b/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java index 8f0b043e..c3c88b44 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java @@ -1,7 +1,6 @@ package p.studio.workspaces; public enum WorkspaceId { - EDITOR, ASSETS, SHIPPER, DEBUG diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java deleted file mode 100644 index 9e24dded..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java +++ /dev/null @@ -1,6 +0,0 @@ -package p.studio.workspaces.editor; - -enum EditorDocumentHighlightOwner { - LOCAL, - LSP -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java deleted file mode 100644 index 6323aacf..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java +++ /dev/null @@ -1,20 +0,0 @@ -package p.studio.workspaces.editor; - -import org.fxmisc.richtext.model.StyleSpans; -import p.studio.lsp.dtos.LspInlineHintDTO; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -record EditorDocumentHighlightingResult( - EditorDocumentHighlightOwner owner, - StyleSpans> styleSpans, - List inlineHints) { - - EditorDocumentHighlightingResult { - owner = Objects.requireNonNull(owner, "owner"); - styleSpans = Objects.requireNonNull(styleSpans, "styleSpans"); - inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints")); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java deleted file mode 100644 index c5cf550a..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java +++ /dev/null @@ -1,32 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSemanticHighlighting; - -final class EditorDocumentHighlightingRouter { - private EditorDocumentHighlightingRouter() { - } - - static EditorDocumentHighlightingResult route( - final EditorOpenFileBuffer fileBuffer, - final EditorDocumentPresentation presentation, - final LspAnalyzeDocumentResult analysis) { - final var localHighlighting = presentation.highlight(fileBuffer.content()); - final var inlineHints = EditorDocumentInlineHintRouter.route(fileBuffer, analysis); - if (fileBuffer.frontendDocument() - && analysis != null - && presentation.supportsSemanticHighlighting() - && !analysis.semanticHighlights().isEmpty()) { - return new EditorDocumentHighlightingResult( - EditorDocumentHighlightOwner.LSP, - EditorDocumentSemanticHighlighting.overlay( - localHighlighting, - EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights())), - inlineHints); - } - return new EditorDocumentHighlightingResult( - EditorDocumentHighlightOwner.LOCAL, - localHighlighting, - inlineHints); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentInlineHintRouter.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentInlineHintRouter.java deleted file mode 100644 index 84065142..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentInlineHintRouter.java +++ /dev/null @@ -1,20 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.lsp.dtos.LspInlineHintDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; - -import java.util.List; - -final class EditorDocumentInlineHintRouter { - private EditorDocumentInlineHintRouter() { - } - - static List route( - final EditorOpenFileBuffer fileBuffer, - final LspAnalyzeDocumentResult analysis) { - if (!fileBuffer.frontendDocument() || analysis == null || analysis.inlineHints().isEmpty()) { - return List.of(); - } - return analysis.inlineHints(); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java deleted file mode 100644 index 2b7dd55f..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java +++ /dev/null @@ -1,30 +0,0 @@ -package p.studio.workspaces.editor; - -import org.fxmisc.richtext.model.StyleSpans; -import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -record EditorDocumentPresentation( - String styleKey, - List stylesheetUrls, - List semanticKeys, - EditorDocumentSyntaxHighlighting syntaxHighlighting) { - - EditorDocumentPresentation { - styleKey = Objects.requireNonNull(styleKey, "styleKey"); - stylesheetUrls = List.copyOf(Objects.requireNonNull(stylesheetUrls, "stylesheetUrls")); - semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys")); - syntaxHighlighting = Objects.requireNonNull(syntaxHighlighting, "syntaxHighlighting"); - } - - StyleSpans> highlight(final String content) { - return syntaxHighlighting.highlight(content); - } - - boolean supportsSemanticHighlighting() { - return !semanticKeys.isEmpty() && !stylesheetUrls.isEmpty(); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java deleted file mode 100644 index 846b280c..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java +++ /dev/null @@ -1,122 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.compiler.FrontendRegistryService; -import p.studio.compiler.models.FrontendSemanticPresentationSpec; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; -import p.studio.vfs.messages.VfsDocumentTypeIds; -import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting; - -import java.net.URL; -import java.util.Locale; -import java.util.Objects; -import java.util.Optional; -import java.util.logging.Logger; - -final class EditorDocumentPresentationRegistry { - private static final Logger LOGGER = Logger.getLogger(EditorDocumentPresentationRegistry.class.getName()); - private static final EditorDocumentPresentation TEXT_PRESENTATION = new EditorDocumentPresentation( - "text", - java.util.List.of(), - java.util.List.of(), - EditorDocumentSyntaxHighlighting.plainText()); - private static final EditorDocumentPresentation JSON_PRESENTATION = new EditorDocumentPresentation( - "json", - java.util.List.of(stylesheet("presentations/json.css")), - java.util.List.of(), - EditorDocumentSyntaxHighlighting.json()); - private static final EditorDocumentPresentation BASH_PRESENTATION = new EditorDocumentPresentation( - "bash", - java.util.List.of(stylesheet("presentations/bash.css")), - java.util.List.of(), - EditorDocumentSyntaxHighlighting.bash()); - - EditorDocumentPresentation resolve(final String typeId) { - return resolve(typeId, null); - } - - EditorDocumentPresentation resolve( - final String typeId, - final LspSemanticPresentationDTO semanticPresentation) { - final String normalizedTypeId = normalize(typeId); - if (normalizedTypeId.isBlank()) { - return TEXT_PRESENTATION; - } - if (FrontendRegistryService.getFrontendSpec(normalizedTypeId).isPresent()) { - return frontendPresentation(normalizedTypeId, semanticPresentation); - } - if (VfsDocumentTypeIds.JSON.equals(normalizedTypeId)) { - return JSON_PRESENTATION; - } - if (VfsDocumentTypeIds.BASH.equals(normalizedTypeId)) { - return BASH_PRESENTATION; - } - return TEXT_PRESENTATION; - } - - private EditorDocumentPresentation frontendPresentation( - final String normalizedTypeId, - final LspSemanticPresentationDTO semanticPresentation) { - final var frontendSpec = FrontendRegistryService.getFrontendSpec(normalizedTypeId).orElseThrow(); - final FrontendSemanticPresentationSpec frontendPresentationSpec = frontendSpec.getSemanticPresentation(); - final java.util.List stylesheetUrls = resolveFrontendStylesheets( - semanticPresentation == null ? java.util.List.of() : semanticPresentation.resources(), - frontendPresentationSpec.resources()); - final java.util.List semanticKeys = semanticPresentation == null - ? frontendPresentationSpec.semanticKeys() - : semanticPresentation.semanticKeys(); - return new EditorDocumentPresentation( - normalizedTypeId, - stylesheetUrls, - semanticKeys, - frontendSyntaxHighlighting(normalizedTypeId)); - } - - private String normalize(final String typeId) { - return typeId == null ? "" : typeId.trim().toLowerCase(Locale.ROOT); - } - - private static String stylesheet(final String relativePath) { - return java.util.Objects.requireNonNull( - EditorDocumentPresentationRegistry.class.getResource("/themes/editor/" + relativePath), - "missing editor presentation stylesheet: " + relativePath) - .toExternalForm(); - } - - private static Optional resourceStylesheet(final String resourcePath) { - final String normalizedPath = normalizeResourcePath(resourcePath); - final URL resource = EditorDocumentPresentationRegistry.class.getResource(normalizedPath); - if (resource == null) { - LOGGER.fine("missing frontend semantic presentation resource: " + resourcePath); - return Optional.empty(); - } - return Optional.of(resource.toExternalForm()); - } - - private static java.util.List resolveFrontendStylesheets( - final java.util.List primaryResources, - final java.util.List fallbackResources) { - final java.util.List primary = primaryResources.stream() - .map(EditorDocumentPresentationRegistry::resourceStylesheet) - .flatMap(Optional::stream) - .toList(); - if (!primary.isEmpty()) { - return primary; - } - return fallbackResources.stream() - .map(EditorDocumentPresentationRegistry::resourceStylesheet) - .flatMap(Optional::stream) - .toList(); - } - - private static EditorDocumentSyntaxHighlighting frontendSyntaxHighlighting(final String normalizedTypeId) { - return EditorDocumentSyntaxHighlighting.plainText(); - } - - private static String normalizeResourcePath(final String resourcePath) { - final String normalized = Objects.requireNonNull(resourcePath, "resourcePath").trim(); - if (normalized.isEmpty()) { - throw new IllegalArgumentException("resourcePath cannot be blank"); - } - return normalized.startsWith("/") ? normalized : "/" + normalized; - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationStyles.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationStyles.java deleted file mode 100644 index cf0bedcb..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationStyles.java +++ /dev/null @@ -1,28 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.scene.Node; -import javafx.scene.control.Label; - -final class EditorDocumentPresentationStyles { - private static final String CODE_AREA_TYPE_CLASS_PREFIX = "editor-workspace-code-area-type-"; - private static final String STATUS_CHIP_TYPE_CLASS_PREFIX = "editor-workspace-status-chip-type-"; - - private EditorDocumentPresentationStyles() { - } - - static void applyToCodeArea(final Node node, final EditorDocumentPresentation presentation) { - replaceTypeClass(node, CODE_AREA_TYPE_CLASS_PREFIX, presentation.styleKey()); - } - - static void applyToStatusChip(final Label label, final EditorDocumentPresentation presentation) { - replaceTypeClass(label, STATUS_CHIP_TYPE_CLASS_PREFIX, presentation.styleKey()); - } - - private static void replaceTypeClass( - final Node node, - final String prefix, - final String styleKey) { - node.getStyleClass().removeIf(styleClass -> styleClass.startsWith(prefix)); - node.getStyleClass().add(prefix + styleKey); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java deleted file mode 100644 index d079e7fc..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java +++ /dev/null @@ -1,91 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.scene.Node; -import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import javafx.scene.paint.Color; - -final class EditorDocumentScopeGuideGraphicFactory { - private static final double COLUMN_WIDTH = 10.0; - private static final double CANVAS_PADDING = 6.0; - private static final double CONTAINER_X = CANVAS_PADDING + (COLUMN_WIDTH / 2.0); - private static final double SCOPE_X = CANVAS_PADDING + COLUMN_WIDTH + (COLUMN_WIDTH / 2.0); - private static final double GUIDE_CAP = 4.0; - private static final Color CONTAINER_COLOR = Color.web("#5f7896"); - private static final Color SCOPE_COLOR = Color.web("#8f72ff"); - - private EditorDocumentScopeGuideGraphicFactory() { - } - - static Node create( - final Node lineNumberNode, - final int paragraphIndex, - final EditorDocumentScopeGuideModel model, - final EditorDocumentScopeGuideModel.ActiveGuides activeGuides) { - final var container = new HBox(lineNumberNode); - final var segments = model.segmentsForLine(paragraphIndex, activeGuides); - container.getChildren().add(new ScopeGuideCanvas(segments)); - return container; - } - - private static final class ScopeGuideCanvas extends Region { - private final Canvas canvas = new Canvas(); - private final java.util.List segments; - - private ScopeGuideCanvas( - final java.util.List segments) { - this.segments = segments; - getStyleClass().add("editor-workspace-scope-guide-gutter"); - getChildren().add(canvas); - setMinWidth(computeGuideWidth()); - setPrefWidth(computeGuideWidth()); - setMaxWidth(computeGuideWidth()); - } - - @Override - protected double computePrefWidth(final double height) { - return computeGuideWidth(); - } - - @Override - protected void layoutChildren() { - final double width = snapSizeX(getWidth()); - final double height = Math.max(1.0, snapSizeY(getHeight())); - canvas.setWidth(width); - canvas.setHeight(height); - draw(width, height); - } - - private void draw(final double width, final double height) { - final GraphicsContext graphics = canvas.getGraphicsContext2D(); - graphics.clearRect(0, 0, width, height); - if (segments.isEmpty()) { - return; - } - final double centerY = Math.floor(height / 2.0); - for (final var segment : segments) { - final boolean container = segment.role() == EditorDocumentScopeGuideModel.GuideRole.ACTIVE_CONTAINER; - final double x = container ? CONTAINER_X : SCOPE_X; - graphics.setLineWidth(container ? 1.0 : 1.4); - graphics.setStroke(container ? CONTAINER_COLOR : SCOPE_COLOR); - switch (segment.kind()) { - case START -> { - graphics.strokeLine(x, centerY, x, height); - graphics.strokeLine(x, centerY, x + GUIDE_CAP, centerY); - } - case CONTINUE -> graphics.strokeLine(x, 0, x, height); - case END -> { - graphics.strokeLine(x, 0, x, centerY); - graphics.strokeLine(x, centerY, x + GUIDE_CAP, centerY); - } - } - } - } - - private static double computeGuideWidth() { - return CANVAS_PADDING * 2.0 + (COLUMN_WIDTH * 2.0); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java deleted file mode 100644 index b38a6706..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java +++ /dev/null @@ -1,340 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.lsp.dtos.LspRangeDTO; -import p.studio.lsp.dtos.LspStructuralAnchorDTO; -import p.studio.lsp.dtos.LspSymbolDTO; -import p.studio.lsp.messages.LspSymbolKind; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; - -final class EditorDocumentScopeGuideModel { - private static final EditorDocumentScopeGuideModel EMPTY = - new EditorDocumentScopeGuideModel(List.of(), List.of(0), 0); - - private final List ranges; - private final List lineStarts; - private final int contentLength; - - private EditorDocumentScopeGuideModel( - final List ranges, - final List lineStarts, - final int contentLength) { - this.ranges = ranges; - this.lineStarts = lineStarts; - this.contentLength = contentLength; - } - - static EditorDocumentScopeGuideModel empty() { - return EMPTY; - } - - static EditorDocumentScopeGuideModel from( - final String content, - final List symbols) { - return fromRanges( - content, - symbols, - 0, - null, - (symbol, depth, parentId, nextId, lineStarts, contentLength) -> { - if (symbol.kind() == LspSymbolKind.UNKNOWN) { - return null; - } - final LspRangeDTO range = symbol.range(); - return toGuideRange( - lineStarts, - contentLength, - range, - new LspRangeDTO(range.startOffset(), Math.min(range.endOffset(), range.startOffset() + 1)), - new LspRangeDTO(Math.max(range.startOffset(), range.endOffset() - 1), range.endOffset()), - depth, - parentId, - nextId[0]); - }, - LspSymbolDTO::children); - } - - static EditorDocumentScopeGuideModel fromStructuralAnchors( - final String content, - final List structuralAnchors) { - return fromRanges( - content, - structuralAnchors, - 0, - null, - (anchor, depth, parentId, nextId, lineStarts, contentLength) -> toGuideRange( - lineStarts, - contentLength, - anchor.range(), - anchor.startAnchor(), - anchor.endAnchor(), - depth, - parentId, - nextId[0]), - LspStructuralAnchorDTO::children); - } - - private static EditorDocumentScopeGuideModel fromRanges( - final String content, - final List roots, - final int initialDepth, - final Integer initialParentId, - final RangeFactory rangeFactory, - final ChildAccessor childAccessor) { - final List lineStarts = lineStarts(content); - final List ranges = new ArrayList<>(); - final int[] nextId = new int[] {1}; - for (final T root : roots) { - appendRangeNode( - ranges, - lineStarts, - content.length(), - root, - initialDepth, - initialParentId, - nextId, - rangeFactory, - childAccessor); - } - ranges.sort(Comparator - .comparingInt(GuideRange::startOffset) - .thenComparingInt(GuideRange::depth) - .thenComparingInt(GuideRange::endOffset)); - return new EditorDocumentScopeGuideModel(List.copyOf(ranges), lineStarts, content.length()); - } - - ActiveGuides resolveActiveGuides(final int caretOffset) { - if (ranges.isEmpty()) { - return ActiveGuides.empty(); - } - final int probeOffset = clampCaretOffset(caretOffset); - GuideRange activeScope = null; - for (final GuideRange range : ranges) { - if (!contains(range, probeOffset)) { - continue; - } - if (activeScope == null - || span(range) < span(activeScope) - || (span(range) == span(activeScope) && range.depth() > activeScope.depth())) { - activeScope = range; - } - } - if (activeScope == null) { - return ActiveGuides.empty(); - } - final GuideRange activeContainer = activeScope.parentId() == null - ? null - : findById(activeScope.parentId()); - return new ActiveGuides(activeContainer, activeScope); - } - - List segmentsForLine( - final int lineIndex, - final ActiveGuides activeGuides) { - if (lineIndex < 0 || lineIndex >= totalLines() || activeGuides.isEmpty()) { - return List.of(); - } - final List segments = new ArrayList<>(2); - appendSegments(segments, lineIndex, activeGuides.activeContainer(), GuideRole.ACTIVE_CONTAINER); - appendSegments(segments, lineIndex, activeGuides.activeScope(), GuideRole.ACTIVE_SCOPE); - return List.copyOf(segments); - } - - int totalLines() { - return lineStarts.size(); - } - - private static void appendRangeNode( - final List ranges, - final List lineStarts, - final int contentLength, - final T node, - final int depth, - final Integer parentId, - final int[] nextId, - final RangeFactory rangeFactory, - final ChildAccessor childAccessor) { - Integer currentParentId = parentId; - final GuideRange range = rangeFactory.create(node, depth, parentId, nextId, lineStarts, contentLength); - if (range != null) { - ranges.add(range); - currentParentId = range.id(); - nextId[0]++; - } - for (final T child : childAccessor.children(node)) { - appendRangeNode(ranges, lineStarts, contentLength, child, depth + 1, currentParentId, nextId, rangeFactory, childAccessor); - } - } - - private static GuideRange toGuideRange( - final List lineStarts, - final int contentLength, - final LspRangeDTO range, - final LspRangeDTO startAnchor, - final LspRangeDTO endAnchor, - final int depth, - final Integer parentId, - final int id) { - final int startOffset = clamp(range.startOffset(), 0, contentLength); - final int endOffset = Math.max(startOffset, clamp(range.endOffset(), 0, contentLength)); - final int startAnchorOffset = clamp(startAnchor.startOffset(), startOffset, endOffset); - final int endAnchorOffset = clamp(Math.max(startAnchorOffset, endAnchor.startOffset()), startOffset, endOffset); - final int startLine = lineForOffset(lineStarts, startAnchorOffset); - final int inclusiveEndOffset = Math.max(startOffset, Math.max(startOffset, endOffset) - 1); - final int inclusiveEndAnchorOffset = Math.max( - startAnchorOffset, - Math.min(Math.max(startOffset, endOffset) - 1, Math.max(endAnchorOffset, startAnchorOffset))); - final int endLine = lineForOffset(lineStarts, clamp(inclusiveEndAnchorOffset, 0, contentLength)); - if (endLine <= startLine) { - return null; - } - return new GuideRange(id, depth, startOffset, endOffset, startAnchorOffset, endAnchorOffset, startLine, endLine, parentId); - } - - private GuideRange findById(final int id) { - for (final GuideRange range : ranges) { - if (range.id() == id) { - return range; - } - } - return null; - } - - private int clampCaretOffset(final int offset) { - if (contentLength <= 0) { - return 0; - } - return clamp(offset, 0, contentLength - 1); - } - - private static boolean contains(final GuideRange range, final int offset) { - return offset >= range.startOffset() && offset < range.endOffset(); - } - - private static int span(final GuideRange range) { - return range.endOffset() - range.startOffset(); - } - - private static void appendSegments( - final List segments, - final int lineIndex, - final GuideRange range, - final GuideRole role) { - if (range == null || lineIndex < range.startLine() || lineIndex > range.endLine()) { - return; - } - final GuideSegmentKind kind; - if (lineIndex == range.startLine()) { - kind = GuideSegmentKind.START; - } else if (lineIndex == range.endLine()) { - kind = GuideSegmentKind.END; - } else { - kind = GuideSegmentKind.CONTINUE; - } - segments.add(new ActiveGuideSegment(role, kind)); - } - - private static List lineStarts(final String content) { - if (content.isEmpty()) { - return List.of(0); - } - final List starts = new ArrayList<>(); - starts.add(0); - for (int index = 0; index < content.length(); index++) { - if (content.charAt(index) == '\n') { - starts.add(index + 1); - } - } - return Collections.unmodifiableList(starts); - } - - private static int lineForOffset(final List lineStarts, final int offset) { - int low = 0; - int high = lineStarts.size() - 1; - while (low <= high) { - final int mid = (low + high) >>> 1; - final int start = lineStarts.get(mid); - final int nextStart = mid + 1 < lineStarts.size() ? lineStarts.get(mid + 1) : Integer.MAX_VALUE; - if (offset < start) { - high = mid - 1; - continue; - } - if (offset >= nextStart) { - low = mid + 1; - continue; - } - return mid; - } - return Math.max(0, Math.min(lineStarts.size() - 1, low)); - } - - private static int clamp(final int value, final int minimum, final int maximum) { - return Math.max(minimum, Math.min(maximum, value)); - } - - enum GuideRole { - ACTIVE_CONTAINER, - ACTIVE_SCOPE - } - - enum GuideSegmentKind { - START, - CONTINUE, - END - } - - record GuideRange( - int id, - int depth, - int startOffset, - int endOffset, - int startAnchorOffset, - int endAnchorOffset, - int startLine, - int endLine, - Integer parentId) { - } - - record ActiveGuides( - GuideRange activeContainer, - GuideRange activeScope) { - private static final ActiveGuides EMPTY = new ActiveGuides(null, null); - - static ActiveGuides empty() { - return EMPTY; - } - - boolean isEmpty() { - return activeScope == null; - } - } - - record ActiveGuideSegment( - GuideRole role, - GuideSegmentKind kind) { - ActiveGuideSegment { - Objects.requireNonNull(role, "role"); - Objects.requireNonNull(kind, "kind"); - } - } - - @FunctionalInterface - private interface ChildAccessor { - List children(T node); - } - - @FunctionalInterface - private interface RangeFactory { - GuideRange create( - T node, - int depth, - Integer parentId, - int[] nextId, - List lineStarts, - int contentLength); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java deleted file mode 100644 index ba8ef5ef..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java +++ /dev/null @@ -1,96 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.layout.VBox; -import p.studio.Container; -import p.studio.controls.WorkspaceDockPane; -import p.studio.lsp.dtos.LspDiagnosticDTO; -import p.studio.utilities.i18n.I18n; - -import java.nio.file.Path; -import java.util.List; - -public final class EditorHelperPanel extends WorkspaceDockPane { - public static final double COLLAPSED_HEIGHT = 34.0; - public static final double MINIMUM_EXPANDED_HEIGHT = 120.0; - public static final double DEFAULT_EXPANDED_HEIGHT = 180.0; - private final Label summary = new Label(); - private final Label diagnosticsTitle = new Label(); - private final VBox diagnosticsBox = new VBox(6); - - public EditorHelperPanel() { - super( - Container.i18n().bind(I18n.CODE_EDITOR_HELPER_TITLE), - COLLAPSED_HEIGHT, - MINIMUM_EXPANDED_HEIGHT, - DEFAULT_EXPANDED_HEIGHT, - false, - "editor-workspace-helper-pane", - "editor-workspace-helper-panel"); - - summary.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-helper-summary"); - summary.setWrapText(true); - diagnosticsTitle.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_HELPER_DIAGNOSTICS)); - diagnosticsTitle.getStyleClass().add("editor-workspace-outline-section-title"); - diagnosticsBox.getStyleClass().add("editor-workspace-outline-list"); - - final var content = new VBox(10, summary, diagnosticsTitle, diagnosticsBox); - content.getStyleClass().add("editor-workspace-panel-content"); - content.setPadding(new Insets(10, 16, 14, 16)); - final var scrollPane = new ScrollPane(content); - scrollPane.setFitToWidth(true); - scrollPane.getStyleClass().add("editor-workspace-outline-scroll"); - setDockContent(scrollPane); - showPlaceholder(); - } - - public void showPlaceholder() { - summary.textProperty().unbind(); - summary.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_HELPER_PLACEHOLDER)); - diagnosticsBox.getChildren().setAll(placeholderLabel(I18n.CODE_EDITOR_HELPER_EMPTY_DIAGNOSTICS)); - } - - public void showSemanticReadResult( - final Path documentPath, - final List diagnostics) { - summary.textProperty().unbind(); - summary.setText(documentPath.getFileName() + " • semantic read-only"); - rebuildDiagnostics(diagnostics); - } - - private void rebuildDiagnostics(final List diagnostics) { - diagnosticsBox.getChildren().clear(); - if (diagnostics.isEmpty()) { - diagnosticsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_HELPER_EMPTY_DIAGNOSTICS)); - return; - } - for (final LspDiagnosticDTO diagnostic : diagnostics) { - final var label = new Label(formatDiagnostic(diagnostic)); - label.setWrapText(true); - label.getStyleClass().addAll( - "editor-workspace-outline-item", - diagnostic.severity().name().equals("ERROR") - ? "editor-workspace-outline-diagnostic-error" - : "editor-workspace-outline-diagnostic-warning"); - diagnosticsBox.getChildren().add(label); - } - } - - private Label placeholderLabel(final I18n key) { - final var label = new Label(); - label.textProperty().bind(Container.i18n().bind(key)); - label.setWrapText(true); - label.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-item"); - return label; - } - - private String formatDiagnostic(final LspDiagnosticDTO diagnostic) { - return "%s [%d,%d) %s".formatted( - diagnostic.severity().name(), - diagnostic.range().startOffset(), - diagnostic.range().endOffset(), - diagnostic.message()); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintLayout.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintLayout.java deleted file mode 100644 index bab8e530..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintLayout.java +++ /dev/null @@ -1,72 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.lsp.dtos.LspInlineHintDTO; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -final class EditorInlineHintLayout { - static final double HORIZONTAL_GAP = 6.0; - - private EditorInlineHintLayout() { - } - - static List layout( - final List inlineHints, - final AnchorBoundsResolver anchorBoundsResolver) { - final var placements = new ArrayList(); - for (final var inlineHint : inlineHints) { - final var anchorBounds = anchorBoundsResolver.resolve(inlineHint); - if (anchorBounds.isEmpty()) { - continue; - } - final var bounds = anchorBounds.orElseThrow(); - placements.add(new Placement( - inlineHint, - displayText(inlineHint), - categoryStyleClass(inlineHint), - bounds.maxX() + HORIZONTAL_GAP, - bounds.minY(), - bounds.height())); - } - return List.copyOf(placements); - } - - static String displayText(final LspInlineHintDTO inlineHint) { - return ": %s".formatted(inlineHint.label()); - } - - static String categoryStyleClass(final LspInlineHintDTO inlineHint) { - final var normalizedCategory = inlineHint.category() - .toLowerCase(Locale.ROOT) - .replaceAll("[^a-z0-9]+", "-") - .replaceAll("^-+", "") - .replaceAll("-+$", ""); - return normalizedCategory.isBlank() - ? "editor-inline-hint-generic" - : "editor-inline-hint-" + normalizedCategory; - } - - @FunctionalInterface - interface AnchorBoundsResolver { - Optional resolve(LspInlineHintDTO inlineHint); - } - - record AnchorBounds( - double minX, - double minY, - double maxX, - double height) { - } - - record Placement( - LspInlineHintDTO inlineHint, - String displayText, - String categoryStyleClass, - double screenX, - double screenY, - double anchorHeight) { - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintOverlay.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintOverlay.java deleted file mode 100644 index 039e716b..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintOverlay.java +++ /dev/null @@ -1,124 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.application.Platform; -import javafx.geometry.Bounds; -import javafx.geometry.Point2D; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.Pane; -import org.fxmisc.richtext.CodeArea; -import org.reactfx.Subscription; -import p.studio.lsp.dtos.LspInlineHintDTO; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -final class EditorInlineHintOverlay { - private final CodeArea codeArea; - private final Pane overlayPane = new Pane(); - private final Map labelsByHint = new LinkedHashMap<>(); - private final Subscription viewportSubscription; - private List inlineHints = List.of(); - - EditorInlineHintOverlay(final CodeArea codeArea) { - this.codeArea = Objects.requireNonNull(codeArea, "codeArea"); - overlayPane.setMouseTransparent(true); - overlayPane.setPickOnBounds(false); - overlayPane.getStyleClass().add("editor-inline-hint-layer"); - codeArea.estimatedScrollXProperty().addListener((ignored, previous, current) -> refresh()); - codeArea.estimatedScrollYProperty().addListener((ignored, previous, current) -> refresh()); - codeArea.layoutBoundsProperty().addListener((ignored, previous, current) -> refresh()); - codeArea.textProperty().addListener((ignored, previous, current) -> refresh()); - overlayPane.layoutBoundsProperty().addListener((ignored, previous, current) -> refresh()); - overlayPane.sceneProperty().addListener((ignored, previous, current) -> scheduleRefresh()); - viewportSubscription = codeArea.viewportDirtyEvents().subscribe(ignored -> refresh()); - } - - Node node() { - return overlayPane; - } - - void setInlineHints(final List inlineHints) { - this.inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints")); - rebuildLabels(); - scheduleRefresh(); - } - - void clear() { - setInlineHints(List.of()); - } - - void refreshLater() { - scheduleRefresh(); - } - - void dispose() { - viewportSubscription.unsubscribe(); - } - - private void rebuildLabels() { - labelsByHint.clear(); - overlayPane.getChildren().clear(); - for (final var inlineHint : inlineHints) { - final var label = new Label(EditorInlineHintLayout.displayText(inlineHint)); - label.setManaged(false); - label.setMouseTransparent(true); - label.setPickOnBounds(false); - label.getStyleClass().add("editor-inline-hint"); - label.getStyleClass().add(EditorInlineHintLayout.categoryStyleClass(inlineHint)); - labelsByHint.put(inlineHint, label); - overlayPane.getChildren().add(label); - } - } - - private void scheduleRefresh() { - Platform.runLater(this::refresh); - } - - private void refresh() { - if (overlayPane.getScene() == null || inlineHints.isEmpty()) { - overlayPane.getChildren().forEach(node -> node.setVisible(false)); - return; - } - final var placements = EditorInlineHintLayout.layout(inlineHints, this::resolveAnchorBounds); - final var placedHints = new java.util.HashSet(); - for (final var placement : placements) { - final var label = labelsByHint.get(placement.inlineHint()); - if (label == null) { - continue; - } - label.applyCss(); - label.autosize(); - final Point2D localPoint = overlayPane.screenToLocal(placement.screenX(), placement.screenY()); - if (!Double.isFinite(localPoint.getX()) || !Double.isFinite(localPoint.getY())) { - label.setVisible(false); - continue; - } - final double layoutY = localPoint.getY() + Math.max(0.0d, (placement.anchorHeight() - label.getHeight()) / 2.0d); - label.relocate(localPoint.getX(), layoutY); - label.setVisible(true); - placedHints.add(placement.inlineHint()); - } - labelsByHint.forEach((inlineHint, label) -> { - if (!placedHints.contains(inlineHint)) { - label.setVisible(false); - } - }); - } - - private Optional resolveAnchorBounds(final LspInlineHintDTO inlineHint) { - if (codeArea.getLength() == 0) { - return Optional.empty(); - } - final int anchorOffset = Math.max(0, Math.min(codeArea.getLength(), inlineHint.anchor().endOffset())); - final Optional characterBounds = codeArea.getCharacterBoundsOnScreen(anchorOffset, anchorOffset); - return characterBounds.map(bounds -> new EditorInlineHintLayout.AnchorBounds( - bounds.getMinX(), - bounds.getMinY(), - bounds.getMaxX(), - bounds.getHeight())); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintProjection.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintProjection.java deleted file mode 100644 index 63f010ac..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintProjection.java +++ /dev/null @@ -1,159 +0,0 @@ -package p.studio.workspaces.editor; - -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyleSpansBuilder; -import org.fxmisc.richtext.model.StyleSpan; -import p.studio.lsp.dtos.LspInlineHintDTO; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; - -final class EditorInlineHintProjection { - private final String displayText; - private final StyleSpans> displayStyles; - private final List protectedRanges; - - private EditorInlineHintProjection( - final String displayText, - final StyleSpans> displayStyles, - final List protectedRanges) { - this.displayText = Objects.requireNonNull(displayText, "displayText"); - this.displayStyles = Objects.requireNonNull(displayStyles, "displayStyles"); - this.protectedRanges = new ArrayList<>(Objects.requireNonNull(protectedRanges, "protectedRanges")); - } - - static EditorInlineHintProjection create( - final String sourceText, - final StyleSpans> sourceStyles, - final List inlineHints) { - Objects.requireNonNull(sourceText, "sourceText"); - Objects.requireNonNull(sourceStyles, "sourceStyles"); - Objects.requireNonNull(inlineHints, "inlineHints"); - if (inlineHints.isEmpty()) { - return new EditorInlineHintProjection(sourceText, sourceStyles, List.of()); - } - - final var sortedHints = inlineHints.stream() - .sorted(Comparator.comparingInt(hint -> hint.anchor().endOffset())) - .toList(); - final var displayText = new StringBuilder(sourceText.length() + (sortedHints.size() * 8)); - final var displayStyles = new StyleSpansBuilder>(); - final var protectedRanges = new ArrayList(); - - int sourceOffset = 0; - int displayOffset = 0; - for (final var inlineHint : sortedHints) { - final int anchorOffset = Math.max(0, Math.min(sourceText.length(), inlineHint.anchor().endOffset())); - if (anchorOffset > sourceOffset) { - displayText.append(sourceText, sourceOffset, anchorOffset); - appendSourceStyles(displayStyles, sourceStyles.subView(sourceOffset, anchorOffset)); - displayOffset += anchorOffset - sourceOffset; - sourceOffset = anchorOffset; - } - - final String hintText = " " + EditorInlineHintLayout.displayText(inlineHint); - displayText.append(hintText); - displayStyles.add(hintClasses(inlineHint), hintText.length()); - protectedRanges.add(new ProtectedRange(displayOffset, displayOffset + hintText.length())); - displayOffset += hintText.length(); - } - - if (sourceOffset < sourceText.length()) { - displayText.append(sourceText, sourceOffset, sourceText.length()); - appendSourceStyles(displayStyles, sourceStyles.subView(sourceOffset, sourceText.length())); - } - return new EditorInlineHintProjection(displayText.toString(), displayStyles.create(), protectedRanges); - } - - String displayText() { - return displayText; - } - - StyleSpans> displayStyles() { - return displayStyles; - } - - String stripDecorations(final String currentDisplayText) { - Objects.requireNonNull(currentDisplayText, "currentDisplayText"); - if (protectedRanges.isEmpty()) { - return currentDisplayText; - } - final var sourceText = new StringBuilder(currentDisplayText); - for (int index = protectedRanges.size() - 1; index >= 0; index--) { - final var range = protectedRanges.get(index); - final int start = Math.max(0, Math.min(sourceText.length(), range.start())); - final int end = Math.max(start, Math.min(sourceText.length(), range.end())); - sourceText.delete(start, end); - } - return sourceText.toString(); - } - - void applyDisplayChange(final int position, final int removedLength, final int insertedLength) { - final int delta = insertedLength - removedLength; - for (int index = 0; index < protectedRanges.size(); index++) { - final var range = protectedRanges.get(index); - if (range.end() <= position) { - continue; - } - if (range.start() >= position + removedLength) { - protectedRanges.set(index, range.shift(delta)); - continue; - } - protectedRanges.set(index, range); - } - } - - boolean containsOffset(final int offset) { - return protectedRanges.stream().anyMatch(range -> range.contains(offset)); - } - - boolean touchesRange(final int start, final int end) { - if (start == end) { - return containsOffset(start); - } - return protectedRanges.stream().anyMatch(range -> range.intersects(start, end)); - } - - int clampCaret(final int offset, final boolean preferRightBoundary) { - for (final var range : protectedRanges) { - if (!range.contains(offset)) { - continue; - } - return preferRightBoundary ? range.end() : range.start(); - } - return offset; - } - - private static void appendSourceStyles( - final StyleSpansBuilder> builder, - final StyleSpans> styles) { - for (final StyleSpan> span : styles) { - builder.add(span.getStyle(), span.getLength()); - } - } - - private static Collection hintClasses(final LspInlineHintDTO inlineHint) { - final var classes = new LinkedHashSet(); - classes.add("editor-inline-hint"); - classes.add(EditorInlineHintLayout.categoryStyleClass(inlineHint)); - return List.copyOf(classes); - } - - record ProtectedRange(int start, int end) { - boolean contains(final int offset) { - return offset >= start && offset < end; - } - - boolean intersects(final int rangeStart, final int rangeEnd) { - return rangeStart < end && rangeEnd > start; - } - - ProtectedRange shift(final int delta) { - return new ProtectedRange(start + delta, end + delta); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileBuffer.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileBuffer.java deleted file mode 100644 index 62e491f7..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileBuffer.java +++ /dev/null @@ -1,37 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.vfs.messages.VfsDocumentAccessMode; - -import java.nio.file.Path; -import java.util.Objects; - -public record EditorOpenFileBuffer( - Path path, - String tabLabel, - String typeId, - String content, - String lineSeparator, - boolean frontendDocument, - VfsDocumentAccessMode accessMode, - boolean dirty) { - public EditorOpenFileBuffer { - path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - tabLabel = Objects.requireNonNull(tabLabel, "tabLabel"); - typeId = Objects.requireNonNull(typeId, "typeId"); - content = Objects.requireNonNull(content, "content"); - lineSeparator = Objects.requireNonNull(lineSeparator, "lineSeparator"); - Objects.requireNonNull(accessMode, "accessMode"); - } - - public boolean editable() { - return accessMode == VfsDocumentAccessMode.EDITABLE; - } - - public boolean readOnly() { - return !editable(); - } - - public boolean saveEnabled() { - return editable() && dirty; - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileSession.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileSession.java deleted file mode 100644 index b1e4bdb0..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileSession.java +++ /dev/null @@ -1,112 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.projectstate.ProjectLocalStudioState; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public final class EditorOpenFileSession { - private final List openFiles = new ArrayList<>(); - private Path activePath; - - public void open(final EditorOpenFileBuffer fileBuffer) { - final var buffer = Objects.requireNonNull(fileBuffer, "fileBuffer"); - final var existingIndex = indexOf(buffer.path()); - if (existingIndex < 0) { - openFiles.add(buffer); - } else { - openFiles.set(existingIndex, buffer); - } - activePath = buffer.path(); - } - - public void activate(final Path path) { - final var normalizedPath = normalize(path); - if (find(normalizedPath).isPresent()) { - activePath = normalizedPath; - } - } - - public void close(final Path path) { - final int index = indexOf(path); - if (index < 0) { - return; - } - final Path normalizedPath = normalize(path); - openFiles.remove(index); - if (openFiles.isEmpty()) { - activePath = null; - return; - } - if (!normalizedPath.equals(activePath)) { - return; - } - final int nextIndex = Math.min(index, openFiles.size() - 1); - activePath = openFiles.get(nextIndex).path(); - } - - public List openFiles() { - return List.copyOf(openFiles); - } - - public Optional activeFile() { - if (activePath == null) { - return Optional.empty(); - } - return find(activePath); - } - - public Optional file(final Path path) { - return find(normalize(path)); - } - - public boolean hasDirtyEditableFiles() { - return openFiles.stream().anyMatch(EditorOpenFileBuffer::saveEnabled); - } - - public boolean hasSaveableActiveFile() { - return activeFile().map(EditorOpenFileBuffer::saveEnabled).orElse(false); - } - - public boolean 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 find(final Path path) { - final int index = indexOf(path); - if (index < 0) { - return Optional.empty(); - } - return Optional.of(openFiles.get(index)); - } - - private int indexOf(final Path path) { - final var normalizedPath = normalize(path); - for (int index = 0; index < openFiles.size(); index++) { - if (openFiles.get(index).path().equals(normalizedPath)) { - return index; - } - } - return -1; - } - - private Path normalize(final Path path) { - return Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java deleted file mode 100644 index 2432a6d6..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java +++ /dev/null @@ -1,116 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.layout.VBox; -import p.studio.lsp.dtos.LspSymbolDTO; -import p.studio.lsp.messages.LspSymbolKind; -import p.studio.Container; -import p.studio.controls.WorkspaceDockPane; -import p.studio.utilities.i18n.I18n; - -import java.nio.file.Path; -import java.util.List; - -public final class EditorOutlinePanel extends WorkspaceDockPane { - private static final double COLLAPSED_HEIGHT = 34.0; - private static final double MINIMUM_EXPANDED_HEIGHT = 120.0; - private static final double DEFAULT_HEIGHT = 180.0; - private final Label summary = new Label(); - private final VBox symbolsBox = new VBox(6); - - public EditorOutlinePanel() { - super( - Container.i18n().bind(I18n.CODE_EDITOR_OUTLINE_TITLE), - COLLAPSED_HEIGHT, - MINIMUM_EXPANDED_HEIGHT, - DEFAULT_HEIGHT, - true, - "editor-workspace-outline-panel"); - - summary.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-summary"); - summary.setWrapText(true); - - final var symbolsTitle = sectionTitle(I18n.CODE_EDITOR_OUTLINE_SYMBOLS); - - symbolsBox.getStyleClass().add("editor-workspace-outline-list"); - - final var content = new VBox(10, - summary, - symbolsTitle, - symbolsBox); - content.getStyleClass().add("editor-workspace-panel-content"); - content.setPadding(new Insets(10, 16, 14, 16)); - final var scrollPane = new ScrollPane(content); - scrollPane.setFitToWidth(true); - scrollPane.getStyleClass().add("editor-workspace-outline-scroll"); - setDockContent(scrollPane); - showPlaceholder(); - } - - public void showPlaceholder() { - summary.textProperty().unbind(); - summary.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_OUTLINE_PLACEHOLDER)); - symbolsBox.getChildren().setAll(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS)); - } - - public void showSemanticReadResult( - final Path documentPath, - final List symbols) { - summary.textProperty().unbind(); - summary.setText(documentPath.getFileName() + " • semantic read-only"); - rebuildSymbols(symbols); - } - - private void rebuildSymbols(final List symbols) { - symbolsBox.getChildren().clear(); - int visible = 0; - for (final LspSymbolDTO symbol : symbols) { - visible += appendSymbol(symbol, 0); - } - if (visible == 0) { - symbolsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS)); - } - } - - private int appendSymbol(final LspSymbolDTO symbol, final int depth) { - int visible = 0; - int childDepth = depth; - if (showInOutline(symbol.kind())) { - final var label = new Label(symbol.name() + " • " + symbol.kind().name().toLowerCase()); - label.setWrapText(true); - label.setPadding(new Insets(0, 0, 0, depth * 12)); - label.getStyleClass().add("editor-workspace-outline-item"); - symbolsBox.getChildren().add(label); - visible++; - childDepth++; - } - for (final LspSymbolDTO child : symbol.children()) { - visible += appendSymbol(child, childDepth); - } - return visible; - } - - private boolean showInOutline(final LspSymbolKind kind) { - return switch (kind) { - case IF, SWITCH, FOR, WHILE, HANDLE -> false; - default -> true; - }; - } - - private Label sectionTitle(final I18n key) { - final var label = new Label(); - label.textProperty().bind(Container.i18n().bind(key)); - label.getStyleClass().add("editor-workspace-outline-section-title"); - return label; - } - - private Label placeholderLabel(final I18n key) { - final var label = new Label(); - label.textProperty().bind(Container.i18n().bind(key)); - label.setWrapText(true); - label.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-item"); - return label; - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorProjectNavigatorPanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorProjectNavigatorPanel.java deleted file mode 100644 index bbcb13a8..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorProjectNavigatorPanel.java +++ /dev/null @@ -1,337 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; -import javafx.scene.control.TreeCell; -import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeView; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import p.studio.Container; -import p.studio.utilities.i18n.I18n; -import p.studio.vfs.messages.VfsProjectNode; -import p.studio.vfs.messages.VfsProjectSnapshot; - -import java.nio.file.Path; -import java.util.LinkedHashSet; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; - -public final class EditorProjectNavigatorPanel extends BorderPane { - private final Button revealActiveButton = new Button(); - private final Button refreshButton = new Button(); - private final TreeView treeView = new TreeView<>(); - private final Label emptyState = emptyLabel(); - private final StackPane content = new StackPane(); - private Runnable revealActiveFileAction = () -> { }; - private Runnable refreshAction = () -> { }; - private Consumer fileSelectionAction = node -> { }; - private Set preferredExpandedPaths = Set.of(); - - public EditorProjectNavigatorPanel() { - getStyleClass().addAll("editor-workspace-panel", "editor-workspace-navigator-panel"); - setPadding(new Insets(14, 16, 14, 16)); - setTop(buildHeader()); - configureTreeView(); - content.getStyleClass().add("editor-workspace-panel-content"); - content.getChildren().addAll(treeView, emptyState); - setCenter(content); - showEmptyState(true); - } - - private HBox buildHeader() { - final var title = new Label(); - title.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_NAVIGATOR_TITLE)); - title.getStyleClass().add("editor-workspace-panel-title"); - - revealActiveButton.setFocusTraversable(false); - revealActiveButton.setDisable(true); - revealActiveButton.getStyleClass().addAll( - "studio-button", - "studio-button-icon", - "editor-workspace-header-icon-button"); - revealActiveButton.setGraphic(EditorWorkspaceIcons.target()); - revealActiveButton.setTooltip(new Tooltip(Container.i18n().text(I18n.CODE_EDITOR_NAVIGATOR_REVEAL_ACTIVE))); - revealActiveButton.setOnAction(event -> revealActiveFileAction.run()); - - refreshButton.setFocusTraversable(false); - refreshButton.getStyleClass().addAll( - "studio-button", - "studio-button-icon", - "editor-workspace-header-icon-button"); - refreshButton.setGraphic(EditorWorkspaceIcons.refresh()); - refreshButton.setTooltip(new Tooltip(Container.i18n().text(I18n.CODE_EDITOR_NAVIGATOR_REFRESH))); - refreshButton.setOnAction(event -> refreshAction.run()); - - final var spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - - final var header = new HBox(8, title, spacer, revealActiveButton, refreshButton); - header.getStyleClass().add("editor-workspace-panel-header"); - return header; - } - - private void configureTreeView() { - treeView.getStyleClass().add("editor-workspace-tree"); - treeView.setShowRoot(true); - treeView.setFocusTraversable(false); - treeView.setCellFactory(ignored -> new TreeCell<>() { - @Override - protected void updateItem(final VfsProjectNode item, final boolean empty) { - super.updateItem(item, empty); - getStyleClass().remove("editor-workspace-tree-cell-tagged"); - getStyleClass().remove("editor-workspace-tree-cell-root"); - if (empty || item == null) { - setText(null); - setGraphic(null); - return; - } - setText(labelFor(item, getTreeItem())); - setGraphic(iconFor(item, getTreeItem())); - if (isRoot(getTreeItem())) { - getStyleClass().add("editor-workspace-tree-cell-root"); - } - if (item.taggedSourceRoot() || isInsideTaggedSourceRoot(getTreeItem())) { - getStyleClass().add("editor-workspace-tree-cell-tagged"); - } - } - }); - treeView.setOnMouseClicked(event -> { - if (event.getClickCount() != 2) { - return; - } - final TreeItem current = treeView.getSelectionModel().getSelectedItem(); - if (current == null || current.getValue() == null || current.getValue().directory()) { - return; - } - fileSelectionAction.accept(current.getValue()); - }); - } - - public void setRefreshAction(final Runnable refreshAction) { - this.refreshAction = Objects.requireNonNull(refreshAction, "refreshAction"); - } - - public void setRevealActiveFileAction(final Runnable revealActiveFileAction) { - this.revealActiveFileAction = Objects.requireNonNull(revealActiveFileAction, "revealActiveFileAction"); - } - - public void setRevealActiveFileAvailable(final boolean available) { - revealActiveButton.setDisable(!available); - } - - public void setFileSelectionAction(final Consumer fileSelectionAction) { - this.fileSelectionAction = Objects.requireNonNull(fileSelectionAction, "fileSelectionAction"); - } - - public void setSnapshot(final VfsProjectSnapshot snapshot) { - final var currentSelection = selectedPath(); - final var expandedPaths = mergeExpandedPaths(); - final var rootItem = buildTreeItem(Objects.requireNonNull(snapshot, "snapshot").rootNode(), expandedPaths, true); - treeView.setRoot(rootItem); - restoreSelection(rootItem, currentSelection); - showEmptyState(false); - } - - public List expandedPathStrings() { - return captureExpandedPaths(treeView.getRoot()).stream() - .map(Path::toString) - .toList(); - } - - public void restoreExpandedPaths(final List expandedPaths) { - final LinkedHashSet 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 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) { - final var normalizedPath = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - final var rootItem = treeView.getRoot(); - if (rootItem == null) { - return; - } - findTreeItem(rootItem, normalizedPath).ifPresent(item -> { - expandAncestors(item); - treeView.getSelectionModel().select(item); - treeView.scrollTo(Math.max(treeView.getRow(item) - 2, 0)); - }); - } - - private TreeItem buildTreeItem( - final VfsProjectNode node, - final Set expandedPaths, - final boolean isRoot) { - final TreeItem item = node.directory() - ? new DirectoryTreeItem(node) - : new TreeItem<>(node); - item.setExpanded(isRoot || expandedPaths.contains(node.path())); - node.children().stream() - .map(child -> buildTreeItem(child, expandedPaths, false)) - .forEach(item.getChildren()::add); - return item; - } - - private Set captureExpandedPaths(final TreeItem item) { - final var expandedPaths = new HashSet(); - if (item == null) { - return expandedPaths; - } - collectExpandedPaths(item, expandedPaths); - return expandedPaths; - } - - private void collectExpandedPaths(final TreeItem item, final Set expandedPaths) { - final var value = item.getValue(); - if (value != null && item.isExpanded()) { - expandedPaths.add(value.path()); - } - item.getChildren().forEach(child -> collectExpandedPaths(child, expandedPaths)); - } - - private Set mergeExpandedPaths() { - final LinkedHashSet merged = new LinkedHashSet<>(preferredExpandedPaths); - merged.addAll(captureExpandedPaths(treeView.getRoot())); - return Set.copyOf(merged); - } - - private Optional selectedPath() { - return Optional.ofNullable(treeView.getSelectionModel().getSelectedItem()) - .map(TreeItem::getValue) - .map(VfsProjectNode::path); - } - - private void restoreSelection(final TreeItem rootItem, final Optional selectedPath) { - treeView.getSelectionModel().clearSelection(); - if (selectedPath.isEmpty()) { - return; - } - findTreeItem(rootItem, selectedPath.orElseThrow()) - .ifPresent(item -> treeView.getSelectionModel().select(item)); - } - - private Optional> findTreeItem(final TreeItem item, final Path path) { - if (item == null || item.getValue() == null) { - return Optional.empty(); - } - if (item.getValue().path().equals(path)) { - return Optional.of(item); - } - for (final var child : item.getChildren()) { - final var match = findTreeItem(child, path); - if (match.isPresent()) { - return match; - } - } - return Optional.empty(); - } - - private void showEmptyState(final boolean empty) { - treeView.setVisible(!empty); - treeView.setManaged(!empty); - emptyState.setVisible(empty); - emptyState.setManaged(empty); - } - - private void expandAncestors(final TreeItem item) { - TreeItem current = item; - while (current != null) { - current.setExpanded(true); - current = current.getParent(); - } - } - - private Node iconFor(final VfsProjectNode node, final TreeItem treeItem) { - if (node.directory()) { - if (isBuildTone(node)) { - return EditorWorkspaceIcons.folderBuild(); - } - if (node.taggedSourceRoot() || isInsideTaggedSourceRoot(treeItem)) { - return EditorWorkspaceIcons.folderSource(); - } - return EditorWorkspaceIcons.folderDefault(); - } - if ("prometeu.json".equals(node.displayName())) { - return EditorWorkspaceIcons.cog(); - } - - return EditorWorkspaceIcons.file(); - } - - private String labelFor(final VfsProjectNode node, final TreeItem treeItem) { - if (!isRoot(treeItem)) { - return node.displayName(); - } - final var directoryName = Optional.ofNullable(node.path().getFileName()) - .map(Path::toString) - .filter(name -> !name.isBlank()) - .orElse(node.displayName()); - return directoryName + " (" + node.displayName() + ")"; - } - - private boolean isInsideTaggedSourceRoot(final TreeItem item) { - var current = item == null ? null : item.getParent(); - while (current != null && current.getValue() != null) { - if (current.getValue().taggedSourceRoot()) { - return true; - } - current = current.getParent(); - } - return false; - } - - private boolean isRoot(final TreeItem item) { - return item != null && item.getParent() == null; - } - - private boolean isBuildTone(final VfsProjectNode node) { - return node.directory() - && ("build".equals(node.displayName()) || "cartridge".equals(node.displayName())); - } - - private Label emptyLabel() { - final var placeholder = new Label(); - placeholder.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_NAVIGATOR_PLACEHOLDER)); - placeholder.getStyleClass().add("editor-workspace-placeholder"); - placeholder.setWrapText(true); - return placeholder; - } - - private static final class DirectoryTreeItem extends TreeItem { - private DirectoryTreeItem(final VfsProjectNode node) { - super(node); - } - - @Override - public boolean isLeaf() { - return false; - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java deleted file mode 100644 index dde4b197..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java +++ /dev/null @@ -1,218 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import p.studio.Container; -import p.studio.projects.ProjectReference; -import p.studio.utilities.i18n.I18n; - -import java.nio.file.Path; -import java.util.Optional; - -public final class EditorStatusBar extends HBox { - private final HBox breadcrumb = new HBox(6); - private final Label position = new Label(); - private final Label lineSeparator = new Label(); - private final Label indentation = new Label(); - private final Label language = new Label(); - private final StackPane accessMode = new StackPane(); - - public EditorStatusBar() { - setAlignment(Pos.CENTER_LEFT); - setSpacing(8); - getStyleClass().add("editor-workspace-status-bar"); - - breadcrumb.getStyleClass().add("editor-workspace-status-breadcrumb"); - breadcrumb.setAlignment(Pos.CENTER_LEFT); - - final var spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - - styleChip(position); - position.getStyleClass().add("editor-workspace-status-chip-position"); - styleChip(lineSeparator); - lineSeparator.getStyleClass().add("editor-workspace-status-chip-line-separator"); - styleChip(indentation); - indentation.getStyleClass().add("editor-workspace-status-chip-indentation"); - styleChip(language); - language.getStyleClass().add("editor-workspace-status-chip-language"); - accessMode.getStyleClass().addAll("editor-workspace-status-chip", "editor-workspace-status-chip-access-mode"); - - getChildren().addAll( - breadcrumb, - spacer, - position, - lineSeparator, - indentation, - language, - accessMode - ); - - } - - public void showFile( - final ProjectReference projectReference, - final EditorOpenFileBuffer fileBuffer, - final EditorDocumentPresentation presentation, - final String indentationPolicyLabel) { - showBreadcrumb(projectReference, fileBuffer.path()); - showMetadata(true); - showPosition(fileBuffer.editable(), 1, 1); - showDocumentFormatting(fileBuffer.lineSeparator(), indentationPolicyLabel); - setText(language, fileBuffer.typeId()); - EditorDocumentPresentationStyles.applyToStatusChip(language, presentation); - showAccessMode(fileBuffer.readOnly()); - } - - public void showPlaceholder(final EditorDocumentPresentation presentation) { - breadcrumb.getChildren().clear(); - showMetadata(false); - showPosition(false, 1, 1); - bindDefault(lineSeparator, I18n.CODE_EDITOR_STATUS_LINE_SEPARATOR); - bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION); - bindDefault(language, I18n.CODE_EDITOR_STATUS_LANGUAGE); - EditorDocumentPresentationStyles.applyToStatusChip(language, presentation); - accessMode.getChildren().clear(); - accessMode.getStyleClass().removeAll("editor-workspace-status-chip-editable", "editor-workspace-status-chip-read-only"); - } - - public void showCaretPosition(final int line, final int column) { - if (!position.isManaged()) { - return; - } - setText(position, line + ":" + column); - } - - public void showDocumentFormatting(final String separator, final String indentationPolicyLabel) { - setText(lineSeparator, separator); - setText(indentation, indentationPolicyLabel); - } - - private void bindDefault(final Label label, final I18n key) { - bindText(label, key); - } - - private void bindText(final Label label, final I18n key) { - label.textProperty().unbind(); - label.textProperty().bind(Container.i18n().bind(key)); - } - - private void showBreadcrumb(final ProjectReference projectReference, final Path filePath) { - breadcrumb.getChildren().clear(); - breadcrumb.getChildren().add(segment( - rootLabel(projectReference), - EditorWorkspaceIcons.folderDefault(), - true)); - - final var relativePath = projectReference.rootPath() - .toAbsolutePath() - .normalize() - .relativize(filePath.toAbsolutePath().normalize()); - - boolean insideSourceTree = false; - Path accumulated = projectReference.rootPath().toAbsolutePath().normalize(); - for (int index = 0; index < relativePath.getNameCount(); index++) { - final var part = relativePath.getName(index).toString(); - accumulated = accumulated.resolve(part); - final boolean isLast = index == relativePath.getNameCount() - 1; - if ("src".equals(part)) { - insideSourceTree = true; - } - breadcrumb.getChildren().add(separator()); - breadcrumb.getChildren().add(segment( - part, - iconForSegment(part, isLast, insideSourceTree), - false)); - } - } - - private String rootLabel(final ProjectReference projectReference) { - return Optional.ofNullable(projectReference.rootPath().getFileName()) - .map(Path::toString) - .filter(name -> !name.isBlank()) - .orElse(projectReference.name()); - } - - private HBox segment(final String text, final Node icon, final boolean root) { - final var label = new Label(text); - label.getStyleClass().add("editor-workspace-status-breadcrumb-text"); - - final var box = new HBox(6, icon, label); - box.setAlignment(Pos.CENTER_LEFT); - box.getStyleClass().add("editor-workspace-status-breadcrumb-segment"); - if (root) { - box.getStyleClass().add("editor-workspace-status-breadcrumb-segment-root"); - } - return box; - } - - private Label separator() { - final var label = new Label(">"); - label.getStyleClass().add("editor-workspace-status-breadcrumb-separator"); - return label; - } - - private Node iconForSegment(final String name, final boolean isFile, final boolean insideSourceTree) { - if (isFile) { - if ("prometeu.json".equals(name)) { - return EditorWorkspaceIcons.cog(); - } - return EditorWorkspaceIcons.file(); - } - - if ("build".equals(name) || "cartridge".equals(name)) { - return EditorWorkspaceIcons.folderBuild(); - } - if ("src".equals(name) || insideSourceTree) { - return EditorWorkspaceIcons.folderSource(); - } - return EditorWorkspaceIcons.folderDefault(); - } - - private void setText(final Label label, final String text) { - label.textProperty().unbind(); - label.setText(text); - } - - private void showMetadata(final boolean visible) { - setVisibleManaged(lineSeparator, visible); - setVisibleManaged(indentation, visible); - setVisibleManaged(language, visible); - setVisibleManaged(accessMode, visible); - } - - private void showPosition(final boolean visible, final int line, final int column) { - setVisibleManaged(position, visible); - if (!visible) { - bindDefault(position, I18n.CODE_EDITOR_STATUS_POSITION); - return; - } - setText(position, line + ":" + column); - } - - private void showAccessMode(final boolean readOnlyMode) { - accessMode.getChildren().setAll(readOnlyMode - ? EditorWorkspaceIcons.lockClosed() - : EditorWorkspaceIcons.lockOpen()); - accessMode.getStyleClass().removeAll("editor-workspace-status-chip-editable", "editor-workspace-status-chip-read-only"); - accessMode.getStyleClass().add(readOnlyMode - ? "editor-workspace-status-chip-read-only" - : "editor-workspace-status-chip-editable"); - } - - private void setVisibleManaged(final Node node, final boolean visible) { - node.setVisible(visible); - node.setManaged(visible); - } - - private void styleChip(final Label label) { - if (!label.getStyleClass().contains("editor-workspace-status-chip")) { - label.getStyleClass().add("editor-workspace-status-chip"); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java deleted file mode 100644 index 84dc590e..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java +++ /dev/null @@ -1,252 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.css.PseudoClass; -import javafx.geometry.Pos; -import javafx.scene.control.Button; -import javafx.scene.control.CustomMenuItem; -import javafx.scene.control.Label; -import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; -import javafx.scene.control.OverrunStyle; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.shape.SVGPath; -import p.studio.Container; -import p.studio.utilities.i18n.I18n; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -public final class EditorTabStrip extends HBox { - private static final PseudoClass PRESSED_PSEUDO_CLASS = PseudoClass.getPseudoClass("pressed"); - private static final double TAB_WIDTH_HINT = 176.0; - private static final double TAB_HEIGHT_HINT = 34.0; - private static final double STRIP_HEIGHT_HINT = 50.0; - private static final double OVERFLOW_WIDTH_HINT = 128.0; - private static final double OVERFLOW_MENU_WIDTH_HINT = 180.0; - - private final MenuButton overflowButton = new MenuButton(); - private final Region spacer = new Region(); - private final List openFiles = new ArrayList<>(); - private Consumer tabSelectionAction = path -> { }; - private Consumer tabCloseAction = path -> { }; - private Path activePath; - - public EditorTabStrip() { - setAlignment(Pos.CENTER_LEFT); - setSpacing(10); - getStyleClass().add("editor-workspace-tab-strip"); - setMinHeight(STRIP_HEIGHT_HINT); - setPrefHeight(STRIP_HEIGHT_HINT); - setMaxHeight(STRIP_HEIGHT_HINT); - HBox.setHgrow(spacer, Priority.ALWAYS); - - overflowButton.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_TABS_OVERFLOW)); - overflowButton.setFocusTraversable(false); - overflowButton.getStyleClass().addAll( - "studio-button", - "studio-button-secondary", - "editor-workspace-tab-overflow"); - applyOverflowMetrics(); - widthProperty().addListener((ignored, previous, current) -> rebuild()); - rebuild(); - } - - public void setTabSelectionAction(final Consumer tabSelectionAction) { - this.tabSelectionAction = Objects.requireNonNull(tabSelectionAction, "tabSelectionAction"); - } - - public void setTabCloseAction(final Consumer tabCloseAction) { - this.tabCloseAction = Objects.requireNonNull(tabCloseAction, "tabCloseAction"); - } - - public void showOpenFiles(final List files, final Path activePath) { - this.openFiles.clear(); - this.openFiles.addAll(Objects.requireNonNull(files, "files")); - this.activePath = activePath == null ? null : activePath.toAbsolutePath().normalize(); - rebuild(); - } - - private void rebuild() { - getChildren().clear(); - if (openFiles.isEmpty()) { - overflowButton.getItems().clear(); - getChildren().add(spacer); - overflowButton.setVisible(false); - overflowButton.setManaged(false); - return; - } - - final int visibleCount = visibleCount(); - final int activeIndex = activeIndex(); - final int startIndex = Math.max(0, Math.min(activeIndex - visibleCount + 1, openFiles.size() - visibleCount)); - final int endIndex = Math.min(openFiles.size(), startIndex + visibleCount); - - for (int index = startIndex; index < endIndex; index++) { - final var fileBuffer = openFiles.get(index); - final var tabButton = new Button(fileBuffer.tabLabel()); - final var tabLabel = new Label(fileBuffer.tabLabel()); - final var dirtyIndicator = new StackPane(); - final var closeChip = new StackPane(); - final var closeIcon = new SVGPath(); - final var tabContent = new HBox(); - tabButton.setFocusTraversable(false); - tabButton.getStyleClass().addAll( - "studio-button", - "studio-button-secondary", - "editor-workspace-tab-button"); - tabButton.getStyleClass().add( - fileBuffer.readOnly() - ? "editor-workspace-tab-button-read-only" - : "editor-workspace-tab-button-editable"); - tabLabel.getStyleClass().add("editor-workspace-tab-label"); - tabLabel.setTextOverrun(OverrunStyle.ELLIPSIS); - dirtyIndicator.getStyleClass().add("editor-workspace-tab-dirty-indicator"); - closeChip.getStyleClass().add("editor-workspace-tab-close-chip"); - closeIcon.setContent("M 3 3 L 9 9 M 9 3 L 3 9"); - closeIcon.getStyleClass().add("editor-workspace-tab-close-icon"); - closeIcon.setMouseTransparent(true); - closeChip.getChildren().add(closeIcon); - tabContent.getStyleClass().add("editor-workspace-tab-content"); - tabContent.setAlignment(Pos.CENTER_LEFT); - HBox.setHgrow(tabLabel, Priority.ALWAYS); - dirtyIndicator.setManaged(fileBuffer.dirty()); - dirtyIndicator.setVisible(fileBuffer.dirty()); - tabContent.getChildren().addAll(tabLabel, dirtyIndicator, closeChip); - tabButton.setText(null); - tabButton.setGraphic(tabContent); - applyTabMetrics(tabButton, tabLabel, dirtyIndicator, closeChip); - if (fileBuffer.path().equals(activePath)) { - tabButton.getStyleClass().add("editor-workspace-tab-button-active"); - } - tabButton.setOnAction(event -> tabSelectionAction.accept(fileBuffer.path())); - configureCloseChip(closeChip, fileBuffer.path()); - getChildren().add(tabButton); - } - - getChildren().add(spacer); - final boolean hasOverflow = openFiles.size() > visibleCount; - overflowButton.getItems().setAll(buildOverflowItems(startIndex, endIndex)); - overflowButton.setVisible(hasOverflow); - overflowButton.setManaged(hasOverflow); - overflowButton.setDisable(!hasOverflow); - getChildren().add(overflowButton); - } - - private List buildOverflowItems(final int startIndex, final int endIndex) { - final var items = new ArrayList(); - for (int index = 0; index < openFiles.size(); index++) { - if (index >= startIndex && index < endIndex) { - continue; - } - final var fileBuffer = openFiles.get(index); - final var label = new Label(fileBuffer.tabLabel()); - label.getStyleClass().add("editor-workspace-tab-overflow-item"); - label.setMinWidth(OVERFLOW_MENU_WIDTH_HINT); - label.setPrefWidth(OVERFLOW_MENU_WIDTH_HINT); - label.setMaxWidth(OVERFLOW_MENU_WIDTH_HINT); - label.setMinHeight(TAB_HEIGHT_HINT); - label.setPrefHeight(TAB_HEIGHT_HINT); - label.setMaxHeight(TAB_HEIGHT_HINT); - label.setAlignment(Pos.CENTER_LEFT); - label.setTextOverrun(OverrunStyle.ELLIPSIS); - - final var item = new CustomMenuItem(label, true); - item.getStyleClass().add("editor-workspace-tab-overflow-menu-item"); - item.setOnAction(event -> tabSelectionAction.accept(fileBuffer.path())); - items.add(item); - } - return items; - } - - private int activeIndex() { - if (activePath == null) { - return Math.max(0, openFiles.size() - 1); - } - for (int index = 0; index < openFiles.size(); index++) { - if (openFiles.get(index).path().equals(activePath)) { - return index; - } - } - return Math.max(0, openFiles.size() - 1); - } - - private int visibleCount() { - final double availableWidth = getWidth() > 0 ? getWidth() : 760; - final double usableWidth = Math.max(availableWidth - OVERFLOW_WIDTH_HINT, TAB_WIDTH_HINT); - return Math.max(1, (int) Math.floor(usableWidth / TAB_WIDTH_HINT)); - } - - private void applyTabMetrics( - final Button button, - final Label tabLabel, - final StackPane dirtyIndicator, - final StackPane closeChip) { - button.setMinWidth(TAB_WIDTH_HINT); - button.setPrefWidth(TAB_WIDTH_HINT); - button.setMaxWidth(TAB_WIDTH_HINT); - button.setMinHeight(TAB_HEIGHT_HINT); - button.setPrefHeight(TAB_HEIGHT_HINT); - button.setMaxHeight(TAB_HEIGHT_HINT); - button.setMnemonicParsing(false); - tabLabel.setMinWidth(0); - tabLabel.setPrefWidth(TAB_WIDTH_HINT - 30); - tabLabel.setMaxWidth(Double.MAX_VALUE); - dirtyIndicator.setMinWidth(8); - dirtyIndicator.setPrefWidth(8); - dirtyIndicator.setMaxWidth(8); - dirtyIndicator.setMinHeight(8); - dirtyIndicator.setPrefHeight(8); - dirtyIndicator.setMaxHeight(8); - closeChip.setMinWidth(14); - closeChip.setPrefWidth(14); - closeChip.setMaxWidth(14); - closeChip.setMinHeight(14); - closeChip.setPrefHeight(14); - closeChip.setMaxHeight(14); - } - - private void applyOverflowMetrics() { - overflowButton.setMinWidth(OVERFLOW_WIDTH_HINT); - overflowButton.setPrefWidth(OVERFLOW_WIDTH_HINT); - overflowButton.setMaxWidth(OVERFLOW_WIDTH_HINT); - overflowButton.setMinHeight(TAB_HEIGHT_HINT); - overflowButton.setPrefHeight(TAB_HEIGHT_HINT); - overflowButton.setMaxHeight(TAB_HEIGHT_HINT); - overflowButton.setMnemonicParsing(false); - } - - private void configureCloseChip(final StackPane closeChip, final Path filePath) { - closeChip.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { - event.consume(); - if (event.getButton() == MouseButton.PRIMARY && closeChip.contains(event.getX(), event.getY())) { - closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, true); - } - }); - closeChip.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> { - event.consume(); - closeChip.pseudoClassStateChanged( - PRESSED_PSEUDO_CLASS, - event.isPrimaryButtonDown() && closeChip.contains(event.getX(), event.getY())); - }); - closeChip.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> { - event.consume(); - final boolean shouldClose = event.getButton() == MouseButton.PRIMARY - && closeChip.getPseudoClassStates().contains(PRESSED_PSEUDO_CLASS) - && closeChip.contains(event.getX(), event.getY()); - closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, false); - if (shouldClose) { - tabCloseAction.accept(filePath); - } - }); - closeChip.addEventFilter(MouseEvent.MOUSE_EXITED, event -> - closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, false)); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java deleted file mode 100644 index 615c205c..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java +++ /dev/null @@ -1,103 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Pos; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; - -import java.util.List; -import java.util.Objects; - -public final class EditorWarningStrip extends HBox { - private final Label message = new Label(); - private final Label counter = new Label(); - private final Button previousButton = new Button("‹"); - private final Button nextButton = new Button("›"); - private final Button closeButton = new Button("×"); - - private List warnings = List.of(); - private int activeIndex; - private boolean dismissed; - - public EditorWarningStrip() { - setAlignment(Pos.CENTER_LEFT); - getStyleClass().add("editor-workspace-warning"); - - message.setWrapText(true); - message.setMaxWidth(Double.MAX_VALUE); - HBox.setHgrow(message, Priority.ALWAYS); - - counter.getStyleClass().add("editor-workspace-warning-counter"); - - previousButton.getStyleClass().add("editor-workspace-warning-button"); - nextButton.getStyleClass().add("editor-workspace-warning-button"); - closeButton.getStyleClass().add("editor-workspace-warning-button"); - - previousButton.setFocusTraversable(false); - nextButton.setFocusTraversable(false); - closeButton.setFocusTraversable(false); - - previousButton.setOnAction(event -> showIndex(activeIndex - 1)); - nextButton.setOnAction(event -> showIndex(activeIndex + 1)); - closeButton.setOnAction(event -> dismiss()); - - final var spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - getChildren().addAll(message, spacer, counter, previousButton, nextButton, closeButton); - setVisible(false); - setManaged(false); - } - - public void showWarnings(final List warningItems) { - warnings = List.copyOf(Objects.requireNonNull(warningItems, "warningItems")); - activeIndex = 0; - dismissed = false; - refresh(); - } - - public void clearWarnings() { - warnings = List.of(); - activeIndex = 0; - dismissed = false; - refresh(); - } - - private void dismiss() { - dismissed = true; - refresh(); - } - - private void showIndex(final int candidateIndex) { - if (warnings.isEmpty()) { - return; - } - activeIndex = Math.floorMod(candidateIndex, warnings.size()); - refresh(); - } - - private void refresh() { - final boolean visible = !dismissed && !warnings.isEmpty(); - setVisible(visible); - setManaged(visible); - if (!visible) { - message.setText(""); - counter.setText(""); - return; - } - - final WarningItem warning = warnings.get(activeIndex); - message.setText(warning.message()); - counter.setText(warnings.size() == 1 ? "1/1" : (activeIndex + 1) + "/" + warnings.size()); - final boolean canNavigate = warnings.size() > 1; - previousButton.setDisable(!canNavigate); - nextButton.setDisable(!canNavigate); - } - - public record WarningItem(String message) { - public WarningItem { - message = Objects.requireNonNull(message, "message"); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java deleted file mode 100644 index cf9d3cc7..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java +++ /dev/null @@ -1,784 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.application.Platform; -import javafx.scene.Node; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonBar; -import javafx.scene.control.ButtonType; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; -import javafx.scene.input.KeyEvent; -import javafx.scene.control.SplitPane; -import javafx.scene.layout.*; -import org.fxmisc.flowless.VirtualizedScrollPane; -import org.fxmisc.richtext.CodeArea; -import org.fxmisc.richtext.LineNumberFactory; -import org.reactfx.Subscription; -import p.studio.lsp.LspService; -import p.studio.lsp.messages.LspAnalyzeDocumentRequest; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.projectstate.ProjectLocalStudioSetup; -import p.studio.projectstate.ProjectLocalStudioState; -import p.studio.projects.ProjectReference; -import p.studio.utilities.i18n.I18n; -import p.studio.vfs.VfsProjectDocument; -import p.studio.vfs.VfsDocumentOpenResult; -import p.studio.vfs.messages.VfsProjectNode; -import p.studio.workspaces.Workspace; -import p.studio.workspaces.WorkspaceId; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.IntFunction; - -public final class EditorWorkspace extends Workspace { - private static final int CARET_SCROLL_CONTEXT_LINES = 2; - private static final KeyCombination SAVE_SHORTCUT = new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN); - private static final KeyCombination SAVE_ALL_SHORTCUT = new KeyCodeCombination( - KeyCode.S, - KeyCombination.SHORTCUT_DOWN, - KeyCombination.SHIFT_DOWN); - private final BorderPane root = new BorderPane(); - private final CodeArea codeArea = new CodeArea(); - private final VirtualizedScrollPane codeScroller = new VirtualizedScrollPane<>(codeArea); - private final Button saveButton = new Button(); - private final Button saveAllButton = new Button(); - private final EditorWarningStrip warningStrip = new EditorWarningStrip(); - private final EditorProjectNavigatorPanel navigatorPanel = new EditorProjectNavigatorPanel(); - private final EditorOutlinePanel outlinePanel = new EditorOutlinePanel(); - private final EditorHelperPanel helperPanel = new EditorHelperPanel(); - private final EditorStatusBar statusBar = new EditorStatusBar(); - private final EditorTabStrip tabStrip = new EditorTabStrip(); - private final EditorDocumentPresentationRegistry presentationRegistry = new EditorDocumentPresentationRegistry(); - private final LspService prometeuLspService; - private final VfsProjectDocument vfsProjectDocument; - private final ProjectLocalStudioSetup.EditorIndentationSetup indentationSetup; - private final EditorOpenFileSession openFileSession = new EditorOpenFileSession(); - private final Subscription inlineHintChangeSubscription; - private final List activePresentationStylesheets = new ArrayList<>(); - private final IntFunction lineNumberFactory = LineNumberFactory.get(codeArea); - private EditorDocumentScopeGuideModel scopeGuideModel = EditorDocumentScopeGuideModel.empty(); - private EditorDocumentScopeGuideModel.ActiveGuides activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty(); - private EditorInlineHintProjection inlineHintProjection = EditorInlineHintProjection.create( - "", - presentationRegistry.resolve("text").highlight(""), - List.of()); - private boolean syncingEditor; - private boolean applyingPendingLayoutState; - private int activeGuideParagraph = -1; - private boolean pendingCaretContextRestore; - private Runnable stateChangedAction = () -> { }; - private ProjectLocalStudioState.EditorLayoutState pendingEditorLayoutState = ProjectLocalStudioState.EditorLayoutState.defaults(); - private SplitPane contentSplit; - private SplitPane leftColumnSplit; - private SplitPane rightColumnSplit; - - public EditorWorkspace( - final ProjectReference projectReference, - final VfsProjectDocument vfsProjectDocument, - final LspService prometeuLspService, - final ProjectLocalStudioSetup.EditorIndentationSetup indentationSetup) { - super(projectReference); - this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"); - this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService"); - this.indentationSetup = Objects.requireNonNull(indentationSetup, "indentationSetup"); - root.getStyleClass().add("editor-workspace"); - refreshParagraphGraphics(); - codeArea.setEditable(false); - codeArea.setWrapText(false); - codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current)); - codeArea.caretPositionProperty().addListener((ignored, previous, current) -> { - final int caretOffset = current == null ? 0 : current.intValue(); - updateActiveGuides(caretOffset, codeArea.getCurrentParagraph()); - refreshStatusBarCaret(); - }); - codeArea.estimatedScrollYProperty().addListener((ignored, previous, current) -> - restoreCaretScrollContextIfNeeded(previous, current)); - codeArea.getStyleClass().add("editor-workspace-code-area"); - codeArea.addEventFilter(KeyEvent.KEY_PRESSED, this::guardInlineHintMutation); - codeArea.addEventFilter(KeyEvent.KEY_TYPED, this::guardInlineHintMutation); - root.addEventFilter(KeyEvent.KEY_PRESSED, this::handleWorkspaceShortcuts); - inlineHintChangeSubscription = codeArea.plainTextChanges().subscribe(change -> { - if (syncingEditor) { - return; - } - inlineHintProjection.applyDisplayChange( - change.getPosition(), - change.getRemoved().length(), - change.getInserted().length()); - }); - EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentationRegistry.resolve("text")); - configureCommandBar(); - configureWarning(); - showEditorPlaceholder(); - navigatorPanel.setRefreshAction(this::refreshNavigator); - navigatorPanel.setRevealActiveFileAction(this::revealActiveFileInNavigator); - navigatorPanel.setFileSelectionAction(this::openNode); - tabStrip.setTabSelectionAction(path -> { - openFileSession.activate(path); - notifyStateChanged(); - renderSession(); - }); - tabStrip.setTabCloseAction(this::requestCloseFile); - - root.setCenter(buildLayout()); - statusBar.showPlaceholder(presentationRegistry.resolve("text")); - } - - @Override public WorkspaceId workspaceId() { return WorkspaceId.EDITOR; } - @Override public I18n title() { return I18n.WORKSPACE_CODE; } - @Override public Node rootNode() { return root; } - - @Override - public void load() { - applyEditorLayoutState(pendingEditorLayoutState); - schedulePendingLayoutReapply(); - refreshNavigator(); - } - - @Override - public void unLoad() { - inlineHintChangeSubscription.unsubscribe(); - } - - 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() { - navigatorPanel.setSnapshot(vfsProjectDocument.refresh()); - } - - private void openNode(final VfsProjectNode node) { - final VfsDocumentOpenResult result = vfsProjectDocument.openDocument(node.path()); - if (result instanceof VfsDocumentOpenResult.VfsTextDocument textDocument) { - openFile(textDocument); - return; - } - showUnsupportedFileModal(node.path()); - } - - private void openFile(final VfsDocumentOpenResult.VfsTextDocument textDocument) { - openFileSession.open(bufferFrom(textDocument)); - notifyStateChanged(); - renderSession(); - } - - private void renderSession() { - final var activeFile = openFileSession.activeFile(); - navigatorPanel.setRevealActiveFileAvailable(activeFile.isPresent()); - tabStrip.showOpenFiles( - openFileSession.openFiles(), - activeFile.map(EditorOpenFileBuffer::path).orElse(null)); - if (activeFile.isEmpty()) { - showEditorPlaceholder(); - final EditorDocumentPresentation presentation = presentationRegistry.resolve("text"); - applyPresentationStylesheets(presentation); - EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); - statusBar.showPlaceholder(presentation); - outlinePanel.showPlaceholder(); - return; - } - - final var fileBuffer = activeFile.orElseThrow(); - final LspAnalyzeDocumentResult analysis = fileBuffer.frontendDocument() - ? prometeuLspService.analyzeDocument(new LspAnalyzeDocumentRequest(fileBuffer.path())) - : null; - final EditorDocumentPresentation presentation = presentationRegistry.resolve( - fileBuffer.typeId(), - analysis == null ? null : analysis.semanticPresentation()); - final EditorDocumentHighlightingResult highlighting = EditorDocumentHighlightingRouter.route( - fileBuffer, - presentation, - analysis); - scopeGuideModel = guidesFor(fileBuffer, analysis); - applyPresentationStylesheets(presentation); - syncingEditor = true; - try { - inlineHintProjection = EditorInlineHintProjection.create( - fileBuffer.content(), - highlighting.styleSpans(), - highlighting.inlineHints()); - codeArea.replaceText(inlineHintProjection.displayText()); - codeArea.setStyleSpans(0, inlineHintProjection.displayStyles()); - codeArea.moveTo(0); - } finally { - syncingEditor = false; - } - activeGuides = scopeGuideModel.resolveActiveGuides(codeArea.getCaretPosition()); - activeGuideParagraph = codeArea.getCurrentParagraph(); - refreshParagraphGraphics(); - Platform.runLater(() -> { - if (fileBuffer.editable()) { - codeArea.requestFocus(); - } - }); - codeArea.setEditable(fileBuffer.editable()); - EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); - refreshCommandSurfaces(fileBuffer); - statusBar.showFile(projectReference, fileBuffer, presentation, indentationSetup.statusLabel()); - refreshStatusBarCaret(); - refreshSemanticOutline(fileBuffer, analysis); - } - - private void revealActiveFileInNavigator() { - openFileSession.activeFile() - .map(EditorOpenFileBuffer::path) - .ifPresent(navigatorPanel::revealPath); - } - - private void showUnsupportedFileModal(final Path path) { - final var alert = new Alert(Alert.AlertType.INFORMATION); - if (root.getScene() != null) { - alert.initOwner(root.getScene().getWindow()); - } - alert.setHeaderText(null); - alert.setTitle(p.studio.Container.i18n().text(I18n.CODE_EDITOR_UNSUPPORTED_FILE_TITLE)); - alert.setContentText(p.studio.Container.i18n().format(I18n.CODE_EDITOR_UNSUPPORTED_FILE_MESSAGE, path.getFileName())); - alert.showAndWait(); - } - - private void showEditorPlaceholder() { - final EditorDocumentPresentation presentation = presentationRegistry.resolve("text"); - scopeGuideModel = EditorDocumentScopeGuideModel.empty(); - activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty(); - activeGuideParagraph = -1; - refreshParagraphGraphics(); - applyPresentationStylesheets(presentation); - syncingEditor = true; - try { - inlineHintProjection = EditorInlineHintProjection.create("", presentation.highlight(""), List.of()); - codeArea.replaceText(""); - codeArea.setStyleSpans(0, inlineHintProjection.displayStyles()); - codeArea.moveTo(0); - } finally { - syncingEditor = false; - } - codeArea.setEditable(false); - EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); - saveButton.setDisable(true); - saveAllButton.setDisable(true); - warningStrip.clearWarnings(); - outlinePanel.showPlaceholder(); - helperPanel.showPlaceholder(); - } - - private void applyPresentationStylesheets(final EditorDocumentPresentation presentation) { - root.getStylesheets().removeAll(activePresentationStylesheets); - activePresentationStylesheets.clear(); - activePresentationStylesheets.addAll(presentation.stylesheetUrls()); - root.getStylesheets().addAll(activePresentationStylesheets); - } - - private void refreshParagraphGraphics() { - preserveViewport(() -> codeArea.setParagraphGraphicFactory(paragraphIndex -> EditorDocumentScopeGuideGraphicFactory.create( - lineNumberFactory.apply(paragraphIndex), - paragraphIndex, - scopeGuideModel, - activeGuides))); - } - - private VBox buildLayout() { - contentSplit = new SplitPane(buildLeftColumn(), buildRightColumn()); - contentSplit.setDividerPositions(0.30); - contentSplit.getStyleClass().add("editor-workspace-split"); - observeStatefulSplitPane(contentSplit); - - final var layout = new VBox(12, buildCommandBar(), contentSplit, statusBar); - layout.getStyleClass().add("editor-workspace-layout"); - VBox.setVgrow(contentSplit, Priority.ALWAYS); - return layout; - } - - private SplitPane buildLeftColumn() { - leftColumnSplit = outlinePanel.createBottomDockLayout(navigatorPanel, "editor-workspace-left-split"); - final var leftColumn = leftColumnSplit; - leftColumn.getStyleClass().add("editor-workspace-left-column"); - observeStatefulSplitPane(leftColumnSplit); - observeDockPane(outlinePanel); - return leftColumn; - } - - private VBox buildCenterColumn() { - final var editorViewport = new StackPane(codeScroller); - editorViewport.getStyleClass().add("editor-workspace-editor-viewport"); - final var editorSurface = new VBox(0, warningStrip, editorViewport); - editorSurface.getStyleClass().add("editor-workspace-editor-surface"); - codeScroller.getStyleClass().add("editor-workspace-code-scroller"); - VBox.setVgrow(editorViewport, Priority.ALWAYS); - final var centerColumn = new VBox(12, tabStrip, editorSurface); - centerColumn.getStyleClass().add("editor-workspace-center-column"); - VBox.setVgrow(editorSurface, Priority.ALWAYS); - return centerColumn; - } - - private SplitPane buildRightColumn() { - rightColumnSplit = helperPanel.createBottomDockLayout(buildCenterColumn(), "editor-workspace-right-split"); - observeStatefulSplitPane(rightColumnSplit); - observeDockPane(helperPanel); - return rightColumnSplit; - } - - private void configureCommandBar() { - saveButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE)); - saveAllButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE_ALL)); - saveButton.getStyleClass().addAll("studio-button", "studio-button-primary", "editor-workspace-command-button"); - saveAllButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "editor-workspace-command-button"); - saveButton.setFocusTraversable(false); - saveAllButton.setFocusTraversable(false); - saveButton.setDisable(true); - saveAllButton.setDisable(true); - saveButton.setOnAction(event -> saveActiveFile()); - saveAllButton.setOnAction(event -> saveAllFiles()); - } - - private void configureWarning() { - warningStrip.clearWarnings(); - } - - private HBox buildCommandBar() { - final var spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - final var commandBar = new HBox(8, saveButton, saveAllButton, spacer); - commandBar.getStyleClass().add("editor-workspace-command-bar"); - return commandBar; - } - - private void syncActiveDocumentToVfs(final String content) { - if (syncingEditor) { - return; - } - openFileSession.activeFile() - .filter(EditorOpenFileBuffer::editable) - .ifPresent(activeFile -> { - final String sourceContent = inlineHintProjection.stripDecorations(content); - final VfsDocumentOpenResult.VfsTextDocument updatedDocument = vfsProjectDocument.updateDocument(activeFile.path(), sourceContent); - openFileSession.open(bufferFrom(updatedDocument)); - pendingCaretContextRestore = true; - refreshEditableHighlighting(updatedDocument); - refreshEditableSemanticState(updatedDocument); - statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), indentationSetup.statusLabel()); - tabStrip.showOpenFiles( - openFileSession.openFiles(), - openFileSession.activeFile().map(EditorOpenFileBuffer::path).orElse(null)); - refreshCommandSurfaces(openFileSession.activeFile().orElseThrow()); - }); - } - - private void refreshEditableHighlighting(final VfsDocumentOpenResult.VfsTextDocument updatedDocument) { - final EditorDocumentPresentation presentation = presentationRegistry.resolve(updatedDocument.typeId()); - inlineHintProjection = EditorInlineHintProjection.create( - updatedDocument.content(), - presentation.highlight(updatedDocument.content()), - List.of()); - preserveViewport(() -> codeArea.setStyleSpans(0, inlineHintProjection.displayStyles())); - } - - private void refreshEditableSemanticState(final VfsDocumentOpenResult.VfsTextDocument updatedDocument) { - final EditorOpenFileBuffer fileBuffer = openFileSession.activeFile().orElse(null); - if (fileBuffer == null || !fileBuffer.path().equals(updatedDocument.path())) { - return; - } - if (!updatedDocument.accessContext().frontendDocument()) { - scopeGuideModel = EditorDocumentScopeGuideModel.empty(); - activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty(); - activeGuideParagraph = -1; - refreshParagraphGraphics(); - outlinePanel.showPlaceholder(); - helperPanel.showPlaceholder(); - return; - } - final LspAnalyzeDocumentResult analysis = prometeuLspService.analyzeDocument( - new LspAnalyzeDocumentRequest(updatedDocument.path())); - scopeGuideModel = guidesFor(fileBuffer, analysis); - activeGuides = scopeGuideModel.resolveActiveGuides(codeArea.getCaretPosition()); - activeGuideParagraph = codeArea.getCurrentParagraph(); - refreshParagraphGraphics(); - refreshSemanticOutline(fileBuffer, analysis); - } - - private void preserveViewport(final Runnable action) { - final int caretPosition = codeArea.getCaretPosition(); - final double scrollX = codeArea.estimatedScrollXProperty().getValue(); - final double scrollY = codeArea.estimatedScrollYProperty().getValue(); - action.run(); - codeArea.moveTo(Math.max(0, Math.min(caretPosition, codeArea.getLength()))); - Platform.runLater(() -> { - codeArea.scrollXToPixel(scrollX); - codeArea.scrollYToPixel(scrollY); - }); - } - - private void restoreCaretScrollContextIfNeeded(final Number previous, final Number current) { - if (!pendingCaretContextRestore) { - return; - } - final double previousValue = previous == null ? Double.NaN : previous.doubleValue(); - final double currentValue = current == null ? Double.NaN : current.doubleValue(); - if (!Double.isFinite(previousValue) || !Double.isFinite(currentValue) || Double.compare(previousValue, currentValue) == 0) { - return; - } - pendingCaretContextRestore = false; - Platform.runLater(() -> codeArea.showParagraphAtTop( - Math.max(0, codeArea.getCurrentParagraph() - CARET_SCROLL_CONTEXT_LINES))); - } - - private void saveActiveFile() { - openFileSession.activeFile() - .filter(EditorOpenFileBuffer::saveEnabled) - .ifPresent(activeFile -> { - vfsProjectDocument.saveDocument(activeFile.path()); - reloadOpenFilesFromVfs(activeFile.path()); - renderSession(); - }); - } - - private void saveAllFiles() { - if (!openFileSession.hasDirtyEditableFiles()) { - return; - } - vfsProjectDocument.saveAllDocuments(); - reloadOpenFilesFromVfs(openFileSession.activeFile().map(EditorOpenFileBuffer::path).orElse(null)); - renderSession(); - } - - private void requestCloseFile(final Path path) { - final var fileBuffer = openFileSession.file(path).orElse(null); - if (fileBuffer == null) { - return; - } - if (fileBuffer.dirty() && !confirmDirtyFileClose(fileBuffer)) { - return; - } - openFileSession.close(path); - notifyStateChanged(); - renderSession(); - } - - private boolean confirmDirtyFileClose(final EditorOpenFileBuffer fileBuffer) { - final var alert = new Alert(Alert.AlertType.CONFIRMATION); - if (root.getScene() != null) { - alert.initOwner(root.getScene().getWindow()); - } - final var saveButtonType = new ButtonType( - p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_SAVE), - ButtonBar.ButtonData.YES); - final var discardButtonType = new ButtonType( - p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_DISCARD), - ButtonBar.ButtonData.NO); - final var cancelButtonType = new ButtonType( - p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_CANCEL), - ButtonBar.ButtonData.CANCEL_CLOSE); - alert.setTitle(p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_TITLE)); - alert.setHeaderText(null); - alert.setContentText(p.studio.Container.i18n().format( - I18n.CODE_EDITOR_CLOSE_DIRTY_MESSAGE, - fileBuffer.tabLabel())); - alert.getButtonTypes().setAll(saveButtonType, discardButtonType, cancelButtonType); - final var result = alert.showAndWait().orElse(cancelButtonType); - if (result == saveButtonType) { - vfsProjectDocument.saveDocument(fileBuffer.path()); - return true; - } - if (result == discardButtonType) { - vfsProjectDocument.discardDocument(fileBuffer.path()); - return true; - } - return false; - } - - private void handleWorkspaceShortcuts(final KeyEvent event) { - if (SAVE_ALL_SHORTCUT.match(event)) { - if (!saveAllButton.isDisabled()) { - saveAllFiles(); - } - event.consume(); - return; - } - if (SAVE_SHORTCUT.match(event)) { - if (!saveButton.isDisabled()) { - saveActiveFile(); - } - event.consume(); - } - } - - private void reloadOpenFilesFromVfs(final Path activePath) { - final var openPaths = openFileSession.openFiles().stream() - .map(EditorOpenFileBuffer::path) - .toList(); - for (final var path : openPaths) { - final var result = vfsProjectDocument.openDocument(path); - if (result instanceof VfsDocumentOpenResult.VfsTextDocument textDocument) { - openFileSession.open(bufferFrom(textDocument)); - } - } - if (activePath != null) { - openFileSession.activate(activePath); - } - } - - private void refreshCommandSurfaces(final EditorOpenFileBuffer fileBuffer) { - saveButton.setDisable(!fileBuffer.saveEnabled()); - saveAllButton.setDisable(!openFileSession.hasDirtyEditableFiles()); - warningStrip.clearWarnings(); - } - - private EditorOpenFileBuffer bufferFrom(final VfsDocumentOpenResult.VfsTextDocument textDocument) { - return new EditorOpenFileBuffer( - textDocument.path(), - textDocument.documentName(), - textDocument.typeId(), - textDocument.content(), - textDocument.lineSeparator(), - textDocument.accessContext().frontendDocument(), - textDocument.accessContext().accessMode(), - textDocument.dirty()); - } - - private void refreshSemanticOutline( - final EditorOpenFileBuffer fileBuffer, - final LspAnalyzeDocumentResult analysis) { - if (!fileBuffer.frontendDocument() || analysis == null) { - outlinePanel.showPlaceholder(); - helperPanel.showPlaceholder(); - return; - } - outlinePanel.showSemanticReadResult(fileBuffer.path(), analysis.documentSymbols()); - helperPanel.showSemanticReadResult(fileBuffer.path(), analysis.diagnostics()); - } - - private EditorDocumentScopeGuideModel guidesFor( - final EditorOpenFileBuffer fileBuffer, - final LspAnalyzeDocumentResult analysis) { - if (!fileBuffer.frontendDocument() || analysis == null) { - return EditorDocumentScopeGuideModel.empty(); - } - if (!analysis.structuralAnchors().isEmpty()) { - return EditorDocumentScopeGuideModel.fromStructuralAnchors( - fileBuffer.content(), - analysis.structuralAnchors()); - } - return EditorDocumentScopeGuideModel.from(fileBuffer.content(), analysis.documentSymbols()); - } - - private void updateActiveGuides(final int caretOffset, final int currentParagraph) { - final EditorDocumentScopeGuideModel.ActiveGuides next = scopeGuideModel.resolveActiveGuides(caretOffset); - if (Objects.equals(activeGuides, next) && activeGuideParagraph == currentParagraph) { - return; - } - final boolean guidesChanged = !Objects.equals(activeGuides, next); - activeGuides = next; - final boolean paragraphChanged = activeGuideParagraph != currentParagraph; - activeGuideParagraph = currentParagraph; - if (!paragraphChanged && !guidesChanged) { - return; - } - refreshParagraphGraphics(); - } - - private void refreshStatusBarCaret() { - openFileSession.activeFile() - .filter(EditorOpenFileBuffer::editable) - .ifPresent(ignored -> statusBar.showCaretPosition( - codeArea.getCurrentParagraph() + 1, - codeArea.getCaretColumn() + 1)); - } - - private void guardInlineHintMutation(final KeyEvent event) { - if (syncingEditor || !codeArea.isEditable()) { - return; - } - final int selectionStart = codeArea.getSelection().getStart(); - final int selectionEnd = codeArea.getSelection().getEnd(); - if (selectionStart != selectionEnd && inlineHintProjection.touchesRange(selectionStart, selectionEnd)) { - event.consume(); - codeArea.moveTo(inlineHintProjection.clampCaret(selectionStart, false)); - return; - } - final int caret = codeArea.getCaretPosition(); - if (event.getEventType() == KeyEvent.KEY_TYPED) { - if (inlineHintProjection.containsOffset(caret)) { - event.consume(); - codeArea.moveTo(inlineHintProjection.clampCaret(caret, true)); - } - return; - } - if (event.getCode() == KeyCode.TAB) { - event.consume(); - codeArea.replaceSelection(indentationSetup.tabInsertion()); - return; - } - if (event.getCode() == KeyCode.BACK_SPACE && inlineHintProjection.containsOffset(Math.max(0, caret - 1))) { - event.consume(); - codeArea.moveTo(inlineHintProjection.clampCaret(Math.max(0, caret - 1), false)); - return; - } - if (event.getCode() == KeyCode.DELETE && inlineHintProjection.containsOffset(caret)) { - event.consume(); - 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 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 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) 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[] 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); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java deleted file mode 100644 index 15d28072..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java +++ /dev/null @@ -1,85 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.scene.Node; -import javafx.scene.layout.StackPane; -import javafx.scene.shape.SVGPath; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class EditorWorkspaceIcons { - private static final Pattern SVG_PATH_PATTERN = Pattern.compile("]*\\sd=\"([^\"]+)\""); - private static final String FOLDER_PATH = "M1.5 4.5A1.5 1.5 0 0 1 3 3h3.2l1.1 1.4H13A1.5 1.5 0 0 1 14.5 5.9V12A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12z"; - private static final String FILE_PATH = "M4 1.5h5l3 3V13A1.5 1.5 0 0 1 10.5 14.5h-6A1.5 1.5 0 0 1 3 13V3A1.5 1.5 0 0 1 4.5 1.5zm4.7 1.2V5h2.3z"; - private static final String COG_PATH = "M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.24-.68-2.64.72-1.96 1.96l.17.31c.446.816.023 1.84-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1c.895.265 1.318 1.289.872 2.105l-.17.31c-.68 1.24.72 2.64 1.96 1.96l.31-.17a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.24.68 2.64-.72 1.96-1.96l-.17-.31a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.68-1.24-.72-2.64-1.96-1.96l-.31.17a1.464 1.464 0 0 1-2.105-.872z"; - private static final String REFRESH_PATH = "M8 2.2a5.8 5.8 0 0 1 4.3 1.9V2.8h1.2v3.8H9.7V5.4h1.8A4.6 4.6 0 1 0 12 11h1.3A5.9 5.9 0 1 1 8 2.2z"; - private static final String TARGET_PATH = "M7.25 1.5h1.5v1.9a4.85 4.85 0 0 1 3.85 3.85h1.9v1.5h-1.9a4.85 4.85 0 0 1-3.85 3.85v1.9h-1.5v-1.9A4.85 4.85 0 0 1 3.4 8.75H1.5v-1.5h1.9A4.85 4.85 0 0 1 7.25 3.4zm.75 3.25A3.25 3.25 0 1 0 8 11.25 3.25 3.25 0 0 0 8 4.75zm0 1.65a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"; - - private EditorWorkspaceIcons() { - } - - public static Node folderDefault() { - return icon(FOLDER_PATH, "editor-workspace-icon-folder-default"); - } - - public static Node folderSource() { - return icon(FOLDER_PATH, "editor-workspace-icon-folder-source"); - } - - public static Node folderBuild() { - return icon(FOLDER_PATH, "editor-workspace-icon-folder-build"); - } - - public static Node file() { - return icon(FILE_PATH, "editor-workspace-icon-file"); - } - - public static Node cog() { - return icon(COG_PATH, "editor-workspace-icon-cog"); - } - - public static Node refresh() { - return icon(REFRESH_PATH, "editor-workspace-icon-refresh"); - } - - public static Node target() { - return icon(TARGET_PATH, "editor-workspace-icon-target"); - } - - public static Node lockClosed() { - return icon(iconPathFromResource("/icons/editor/lock-closed.svg"), "editor-workspace-icon-lock-closed"); - } - - public static Node lockOpen() { - return icon(iconPathFromResource("/icons/editor/lock-open.svg"), "editor-workspace-icon-lock-open"); - } - - private static Node icon(final String path, final String toneClass) { - final var shape = new SVGPath(); - shape.setContent(path); - shape.getStyleClass().addAll("editor-workspace-icon-shape", toneClass); - - final var icon = new StackPane(shape); - icon.getStyleClass().add("editor-workspace-icon"); - return icon; - } - - private static String iconPathFromResource(final String resourcePath) { - try (InputStream inputStream = EditorWorkspaceIcons.class.getResourceAsStream(resourcePath)) { - if (inputStream == null) { - throw new IllegalStateException("Missing icon resource: " + resourcePath); - } - final String svg = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - final Matcher matcher = SVG_PATH_PATTERN.matcher(svg); - if (!matcher.find()) { - throw new IllegalStateException("Missing in icon resource: " + resourcePath); - } - return matcher.group(1); - } catch (IOException exception) { - throw new IllegalStateException("Unable to read icon resource: " + resourcePath, exception); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentHighlightToken.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentHighlightToken.java deleted file mode 100644 index fc1c8c9d..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentHighlightToken.java +++ /dev/null @@ -1,25 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -public record EditorDocumentHighlightToken( - String groupName, - Collection styleClasses) { - - public EditorDocumentHighlightToken(final String groupName, final String styleClass) { - this(groupName, List.of(styleClass)); - } - - public EditorDocumentHighlightToken { - groupName = Objects.requireNonNull(groupName, "groupName"); - styleClasses = List.copyOf(Objects.requireNonNull(styleClasses, "styleClasses")); - if (groupName.isBlank()) { - throw new IllegalArgumentException("groupName cannot be blank"); - } - if (styleClasses.isEmpty()) { - throw new IllegalArgumentException("styleClasses cannot be empty"); - } - } -} \ No newline at end of file diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java deleted file mode 100644 index 89dd1487..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java +++ /dev/null @@ -1,64 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyleSpansBuilder; -import p.studio.lsp.dtos.LspHighlightSpanDTO; - -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashSet; -import java.util.List; - -public final class EditorDocumentSemanticHighlighting { - private EditorDocumentSemanticHighlighting() { - } - - public static StyleSpans> highlight( - final String content, - final List semanticHighlights) { - final StyleSpansBuilder> builder = new StyleSpansBuilder<>(); - final List orderedHighlights = semanticHighlights.stream() - .sorted(Comparator.comparingInt(highlight -> highlight.range().startOffset())) - .toList(); - int cursor = 0; - for (final LspHighlightSpanDTO highlight : orderedHighlights) { - final int start = Math.max(cursor, highlight.range().startOffset()); - final int end = Math.min(content.length(), highlight.range().endOffset()); - if (start > cursor) { - builder.add(Collections.emptyList(), start - cursor); - } - if (end > start) { - builder.add(List.of("editor-semantic-" + highlight.semanticKey()), end - start); - cursor = end; - } - } - if (cursor < content.length()) { - builder.add(Collections.emptyList(), content.length() - cursor); - } - if (content.isEmpty()) { - builder.add(Collections.emptyList(), 0); - } - return builder.create(); - } - - public static StyleSpans> overlay( - final StyleSpans> baseHighlighting, - final StyleSpans> semanticHighlighting) { - return baseHighlighting.overlay(semanticHighlighting, EditorDocumentSemanticHighlighting::mergeStyles); - } - - private static Collection mergeStyles( - final Collection baseStyles, - final Collection semanticStyles) { - if (baseStyles.isEmpty()) { - return semanticStyles; - } - if (semanticStyles.isEmpty()) { - return baseStyles; - } - final LinkedHashSet merged = new LinkedHashSet<>(baseStyles); - merged.addAll(semanticStyles); - return List.copyOf(merged); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java deleted file mode 100644 index 92271b9b..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java +++ /dev/null @@ -1,59 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyleSpansBuilder; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public record EditorDocumentSyntaxHighlighting( - Pattern tokenPattern, - List tokens) { - - public EditorDocumentSyntaxHighlighting { - tokens = List.copyOf(Objects.requireNonNull(tokens, "tokens")); - if (tokenPattern == null && !tokens.isEmpty()) { - throw new IllegalArgumentException("plain text highlighting cannot define tokens"); - } - if (tokenPattern != null && tokens.isEmpty()) { - throw new IllegalArgumentException("token-based highlighting requires tokens"); - } - } - - public static EditorDocumentSyntaxHighlighting plainText() { return EditorDocumentSyntaxHighlightingPlainText.PLAIN_TEXT; } - public static EditorDocumentSyntaxHighlighting json() { return EditorDocumentSyntaxHighlightingJson.JSON; } - public static EditorDocumentSyntaxHighlighting bash() { return EditorDocumentSyntaxHighlightingBash.BASH; } - - public StyleSpans> highlight(final String content) { - final StyleSpansBuilder> builder = new StyleSpansBuilder<>(); - if (tokenPattern == null) { - builder.add(Collections.emptyList(), content.length()); - return builder.create(); - } - - final Matcher matcher = tokenPattern.matcher(content); - int lastMatchEnd = 0; - - while (matcher.find()) { - builder.add(Collections.emptyList(), matcher.start() - lastMatchEnd); - builder.add(styleClassFor(matcher), matcher.end() - matcher.start()); - lastMatchEnd = matcher.end(); - } - - builder.add(Collections.emptyList(), content.length() - lastMatchEnd); - return builder.create(); - } - - private Collection styleClassFor(final Matcher matcher) { - for (final EditorDocumentHighlightToken token : tokens) { - if (matcher.group(token.groupName()) != null) { - return token.styleClasses(); - } - } - throw new IllegalStateException("matched token without registered style"); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingBash.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingBash.java deleted file mode 100644 index 3623b37e..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingBash.java +++ /dev/null @@ -1,28 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import java.util.List; -import java.util.regex.Pattern; - -public class EditorDocumentSyntaxHighlightingBash { - public static final EditorDocumentSyntaxHighlighting BASH = new EditorDocumentSyntaxHighlighting( - Pattern.compile( - "(?^#![^\\n]*)" - + "|(?(?m)(?\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')" - + "|(?\\$\\{?[A-Za-z_][A-Za-z0-9_]*}?|\\$[0-9@*#?!$-])" - + "|(?(?m)^[ \\t]*test\\b)" - + "|(?(?m)^[ \\t]*[A-Za-z_./-][A-Za-z0-9_./-]*)" - + "|(?\\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time)\\b)" - + "|(?\\b(?:echo|printf|read|cd|pwd|export|local|readonly|unset|return|shift|source|trap|exit)\\b)" - + "|(?\\|\\||&&|;;|<<-?|>>|[|&;<>~=(){}\\[\\]])"), - List.of( - new EditorDocumentHighlightToken("SHEBANG", "editor-syntax-bash-shebang"), - new EditorDocumentHighlightToken("COMMENT", "editor-syntax-bash-comment"), - new EditorDocumentHighlightToken("STRING", "editor-syntax-bash-string"), - new EditorDocumentHighlightToken("VARIABLE", "editor-syntax-bash-variable"), - new EditorDocumentHighlightToken("TESTBUILTIN", "editor-syntax-bash-builtin"), - new EditorDocumentHighlightToken("KEYWORD", "editor-syntax-bash-keyword"), - new EditorDocumentHighlightToken("BUILTIN", "editor-syntax-bash-builtin"), - new EditorDocumentHighlightToken("COMMAND", "editor-syntax-bash-command"), - new EditorDocumentHighlightToken("OPERATOR", "editor-syntax-bash-operator"))); -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingJson.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingJson.java deleted file mode 100644 index 58f803e8..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingJson.java +++ /dev/null @@ -1,22 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import java.util.List; -import java.util.regex.Pattern; - -public class EditorDocumentSyntaxHighlightingJson { - public static final EditorDocumentSyntaxHighlighting JSON = new EditorDocumentSyntaxHighlighting( - Pattern.compile( - "(?\"(?:\\\\.|[^\"\\\\])*\"(?=\\s*:))" - + "|(?\"(?:\\\\.|[^\"\\\\])*\")" - + "|(?-?\\b\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)" - + "|(?\\b(?:true|false)\\b)" - + "|(?\\bnull\\b)" - + "|(?[\\{\\}\\[\\],:])"), - List.of( - new EditorDocumentHighlightToken("KEY", "editor-syntax-json-key"), - new EditorDocumentHighlightToken("STRING", "editor-syntax-json-string"), - new EditorDocumentHighlightToken("NUMBER", "editor-syntax-json-number"), - new EditorDocumentHighlightToken("BOOLEAN", "editor-syntax-json-boolean"), - new EditorDocumentHighlightToken("NULL", "editor-syntax-json-null"), - new EditorDocumentHighlightToken("PUNCTUATION", "editor-syntax-json-punctuation"))); -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingPlainText.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingPlainText.java deleted file mode 100644 index 1f13091b..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingPlainText.java +++ /dev/null @@ -1,9 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import java.util.List; - -public class EditorDocumentSyntaxHighlightingPlainText { - public static final EditorDocumentSyntaxHighlighting PLAIN_TEXT = new EditorDocumentSyntaxHighlighting( - null, - List.of()); -} diff --git a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java index 42419295..62cf61f5 100644 --- a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java +++ b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java @@ -1,25 +1,11 @@ package p.studio.projectsessions; import org.junit.jupiter.api.Test; -import p.studio.lsp.messages.LspProjectContext; -import p.studio.lsp.LspService; -import p.studio.lsp.LspServiceFactory; import p.studio.projectstate.ProjectLocalStudioSetup; import p.studio.projectstate.ProjectLocalStudioSetupService; import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioStateService; -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentRequest; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.lsp.messages.LspDefinitionRequest; -import p.studio.lsp.messages.LspDefinitionResult; import p.studio.projects.ProjectReference; -import p.studio.vfs.VfsProjectDocument; -import p.studio.vfs.ProjectDocumentVfsFactory; -import p.studio.vfs.VfsDocumentOpenResult; -import p.studio.vfs.messages.VfsProjectContext; -import p.studio.vfs.messages.VfsProjectSnapshot; -import p.studio.vfs.messages.VfsRefreshRequest; import java.nio.file.Path; @@ -28,14 +14,10 @@ import static org.junit.jupiter.api.Assertions.assertSame; final class StudioProjectSessionFactoryTest { @Test - void openCreatesProjectSessionBackedByProjectScopedVfs() { - final RecordingLspFactory lspFactory = new RecordingLspFactory(); - final RecordingVfsFactory vfsFactory = new RecordingVfsFactory(); + void openCreatesProjectSessionWithoutLegacyEditorServices() { final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService(); final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService(); final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory( - lspFactory, - vfsFactory, stateService, setupService); final ProjectReference projectReference = new ProjectReference( @@ -48,14 +30,6 @@ final class StudioProjectSessionFactoryTest { final StudioProjectSession session = sessionFactory.open(projectReference); assertSame(projectReference, session.projectReference()); - assertSame(vfsFactory.vfs, session.projectDocumentVfs()); - assertSame(lspFactory.service, session.prometeuLspService()); - assertEquals("Example", vfsFactory.capturedContext.projectName()); - assertEquals("pbs", vfsFactory.capturedContext.languageId()); - assertEquals(projectReference.rootPath().toAbsolutePath().normalize(), vfsFactory.capturedContext.rootPath()); - assertEquals("Example", lspFactory.capturedContext.projectName()); - assertEquals("pbs", lspFactory.capturedContext.languageId()); - assertSame(vfsFactory.vfs, lspFactory.capturedVfs); assertSame(stateService.loadedState, session.projectLocalStudioState()); assertSame(setupService.loadedSetup, session.projectLocalStudioSetup()); assertEquals("IDLE", session.executionSession().snapshot().state().name()); @@ -68,8 +42,7 @@ final class StudioProjectSessionFactoryTest { private final ProjectLocalStudioState loadedState = new ProjectLocalStudioState( ProjectLocalStudioState.CURRENT_VERSION, ProjectLocalStudioState.ShellLayout.defaults(), - new ProjectLocalStudioState.OpenShellState("EDITOR"), - ProjectLocalStudioState.EditorRestorationState.defaults()); + new ProjectLocalStudioState.OpenShellState("ASSETS")); @Override public ProjectLocalStudioState load(final ProjectReference projectReference) { @@ -90,84 +63,4 @@ final class StudioProjectSessionFactoryTest { return loadedSetup; } } - - private static final class RecordingVfsFactory implements ProjectDocumentVfsFactory { - private VfsProjectContext capturedContext; - private final VfsProjectDocument vfs = new NoOpVfsProjectDocument(); - - @Override - public VfsProjectDocument open(VfsProjectContext projectContext) { - this.capturedContext = projectContext; - return vfs; - } - } - - private static final class RecordingLspFactory implements LspServiceFactory { - private LspProjectContext capturedContext; - private VfsProjectDocument capturedVfs; - private final LspService service = new NoOpPrometeuLspService(); - - @Override - public LspService open( - final LspProjectContext projectContext, - final VfsProjectDocument vfsProjectDocument) { - this.capturedContext = projectContext; - this.capturedVfs = vfsProjectDocument; - return service; - } - } - - private static final class NoOpVfsProjectDocument implements VfsProjectDocument { - @Override - public VfsProjectContext projectContext() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot snapshot() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot refresh() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot refresh(VfsRefreshRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public VfsDocumentOpenResult openDocument(Path path) { - throw new UnsupportedOperationException(); - } - } - - private static final class NoOpPrometeuLspService implements LspService { - @Override - public LspProjectContext projectContext() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectDocument projectDocumentVfs() { - throw new UnsupportedOperationException(); - } - - @Override - public LspSessionStateDTO snapshot() { - throw new UnsupportedOperationException(); - } - - @Override - public LspAnalyzeDocumentResult analyzeDocument(final LspAnalyzeDocumentRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public LspDefinitionResult definition(final LspDefinitionRequest request) { - throw new UnsupportedOperationException(); - } - } } diff --git a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java index 5b731bc8..87eca3bb 100644 --- a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java +++ b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java @@ -2,22 +2,10 @@ package p.studio.projectsessions; import org.junit.jupiter.api.Test; import p.studio.execution.StudioExecutionSessionService; -import p.studio.lsp.messages.LspProjectContext; -import p.studio.lsp.LspService; import p.studio.projectstate.ProjectLocalStudioSetup; import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioStateService; -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentRequest; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.lsp.messages.LspDefinitionRequest; -import p.studio.lsp.messages.LspDefinitionResult; import p.studio.projects.ProjectReference; -import p.studio.vfs.VfsProjectDocument; -import p.studio.vfs.VfsDocumentOpenResult; -import p.studio.vfs.messages.VfsProjectContext; -import p.studio.vfs.messages.VfsProjectSnapshot; -import p.studio.vfs.messages.VfsRefreshRequest; import java.nio.file.Path; @@ -26,14 +14,10 @@ import static org.junit.jupiter.api.Assertions.assertSame; final class StudioProjectSessionTest { @Test - void closeDelegatesToUnderlyingServicesOnlyOnce() { - final CountingVfsProjectDocument vfs = new CountingVfsProjectDocument(); - final CountingPrometeuLspService lsp = new CountingPrometeuLspService(vfs); + void closeSavesStateOnlyOnce() { final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService(); final StudioProjectSession session = new StudioProjectSession( projectReference(), - lsp, - vfs, stateService, ProjectLocalStudioSetup.defaults(), new StudioExecutionSessionService(), @@ -42,27 +26,21 @@ final class StudioProjectSessionTest { session.close(); session.close(); - assertEquals(1, lsp.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, ProjectLocalStudioSetup.defaults(), new StudioExecutionSessionService(), ProjectLocalStudioState.defaults()); final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults() - .withOpenShellState(new ProjectLocalStudioState.OpenShellState("EDITOR")); + .withOpenShellState(new ProjectLocalStudioState.OpenShellState("ASSETS")); session.replaceProjectLocalStudioState(nextState); session.close(); @@ -86,77 +64,4 @@ final class StudioProjectSessionTest { savedState = state; } } - - private static final class CountingVfsProjectDocument implements VfsProjectDocument { - private int closeCalls; - - @Override - public VfsProjectContext projectContext() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot snapshot() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot refresh() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot refresh(VfsRefreshRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public VfsDocumentOpenResult openDocument(Path path) { - throw new UnsupportedOperationException(); - } - - @Override - public void close() { - closeCalls++; - } - } - - private static final class CountingPrometeuLspService implements LspService { - private final VfsProjectDocument vfsProjectDocument; - private int closeCalls; - - private CountingPrometeuLspService(final VfsProjectDocument vfsProjectDocument) { - this.vfsProjectDocument = vfsProjectDocument; - } - - @Override - public LspProjectContext projectContext() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectDocument projectDocumentVfs() { - return vfsProjectDocument; - } - - @Override - public LspSessionStateDTO snapshot() { - throw new UnsupportedOperationException(); - } - - @Override - public LspAnalyzeDocumentResult analyzeDocument(final LspAnalyzeDocumentRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public LspDefinitionResult definition(final LspDefinitionRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public void close() { - closeCalls++; - } - } } diff --git a/prometeu-studio/src/test/java/p/studio/projectstate/ProjectLocalStudioStateServiceTest.java b/prometeu-studio/src/test/java/p/studio/projectstate/ProjectLocalStudioStateServiceTest.java index 259b2f85..023b5d90 100644 --- a/prometeu-studio/src/test/java/p/studio/projectstate/ProjectLocalStudioStateServiceTest.java +++ b/prometeu-studio/src/test/java/p/studio/projectstate/ProjectLocalStudioStateServiceTest.java @@ -23,22 +23,8 @@ final class ProjectLocalStudioStateServiceTest { final ProjectLocalStudioState state = new ProjectLocalStudioState( ProjectLocalStudioState.CURRENT_VERSION, 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())); + new ProjectLocalStudioState.OpenShellState("ASSETS")); service.save(project, state); @@ -72,6 +58,43 @@ final class ProjectLocalStudioStateServiceTest { assertEquals(ProjectLocalStudioState.defaults(), service.load(project)); } + @Test + void legacyEditorStateLoadsWithAssetsFallbackAndIgnoresEditorFields() throws Exception { + final ProjectLocalStudioStateService service = new ProjectLocalStudioStateService(); + final ProjectReference project = project("main"); + Files.createDirectories(ProjectStudioPaths.studioRoot(project)); + Files.writeString( + ProjectStudioPaths.statePath(project), + """ + { + "version": 1, + "shellLayout": { + "editorLayout": { + "contentSplitDividers": [0.2, 0.8] + }, + "assetsLayout": { + "contentSplitDividers": [0.42] + } + }, + "openShellState": { + "selectedWorkspaceId": "EDITOR" + }, + "editorRestoration": { + "openTabPaths": ["src/main.pbs"], + "activeTabPath": "src/main.pbs" + } + } + """); + + assertEquals( + new ProjectLocalStudioState( + ProjectLocalStudioState.CURRENT_VERSION, + new ProjectLocalStudioState.ShellLayout( + new ProjectLocalStudioState.AssetsLayoutState(List.of(0.42d))), + new ProjectLocalStudioState.OpenShellState("ASSETS")), + 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); diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java deleted file mode 100644 index 26b9d405..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java +++ /dev/null @@ -1,315 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.lsp.dtos.LspHighlightSpanDTO; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; -import p.studio.lsp.dtos.LspRangeDTO; -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.vfs.messages.VfsDocumentAccessMode; - -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class EditorDocumentHighlightingRouterTest { - @Test - void frontendDocumentsUseLspOwnedHighlightsWhenSemanticSpansExist() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "fn main() -> void {}", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-keyword", "pbs-function"), - List.of("/themes/pbs/semantic-highlighting.css")), - List.of(), - List.of( - new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword"), - new LspHighlightSpanDTO(new LspRangeDTO(3, 7), "pbs-function")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); - } - - @Test - void nonFrontendDocumentsStayOnLocalHighlighting() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/prometeu.json"), - "prometeu.json", - "json", - "{\n \"name\": \"Example\"\n}\n", - "LF", - false, - VfsDocumentAccessMode.EDITABLE, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO(List.of(), List.of()), - List.of(), - List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 1), "fe-punctuation")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("json"), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - } - - @Test - void frontendDocumentsFallbackToLocalPbsSyntaxWhenSemanticPresentationResourcesAreUnavailable() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "fn main() -> void {}", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-keyword", "pbs-function"), - List.of("/themes/pbs/missing.css")), - List.of(), - List.of( - new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword"), - new LspHighlightSpanDTO(new LspRangeDTO(3, 7), "pbs-function")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); - } - - @Test - void frontendServiceHighlightsProjectToDedicatedServiceCssClass() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "declare service Game {}", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-service"), - List.of("/themes/pbs/semantic-highlighting.css")), - List.of(), - List.of(new LspHighlightSpanDTO(new LspRangeDTO(16, 20), "pbs-service")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); - } - - @Test - void frontendDocumentsKeepLocalSyntaxForRangesWithoutSemanticCoverage() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "fn main() -> void { Game.tick(1); }", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-keyword", "pbs-service", "pbs-function"), - List.of("/themes/pbs/semantic-highlighting.css")), - List.of(), - List.of( - new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword"), - new LspHighlightSpanDTO(new LspRangeDTO(3, 7), "pbs-function"), - new LspHighlightSpanDTO(new LspRangeDTO(19, 23), "pbs-service")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); - } - - @Test - void frontendDocumentsPreserveTransportedInlineHintsEvenWithoutSemanticHighlightCoverage() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "fn main() -> void { let value = 1; }", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-function"), - List.of("/themes/pbs/semantic-highlighting.css")), - List.of(), - List.of(), - List.of(new p.studio.lsp.dtos.LspInlineHintDTO(new LspRangeDTO(24, 29), "int", "type")), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); - assertEquals(1, result.inlineHints().size()); - assertEquals("int", result.inlineHints().getFirst().label()); - } - - @Test - void nonFrontendDocumentsDiscardTransportedInlineHints() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/prometeu.json"), - "prometeu.json", - "json", - "{\n \"name\": \"Example\"\n}\n", - "LF", - false, - VfsDocumentAccessMode.EDITABLE, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO(List.of(), List.of()), - List.of(), - List.of(), - List.of(new p.studio.lsp.dtos.LspInlineHintDTO(new LspRangeDTO(0, 1), "ignored", "type")), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("json"), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - } - - @Test - void bashBuiltinTestOnlyHighlightsInCommandPosition() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer commandBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/run.sh"), - "run.sh", - "bash", - "test -f ./file\n", - "LF", - false, - VfsDocumentAccessMode.EDITABLE, - false); - final EditorOpenFileBuffer plainTextBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/run.sh"), - "run.sh", - "bash", - "echo \"$test\"\n", - "LF", - false, - VfsDocumentAccessMode.EDITABLE, - false); - - final EditorDocumentHighlightingResult commandResult = EditorDocumentHighlightingRouter.route( - commandBuffer, - registry.resolve("bash"), - null); - final EditorDocumentHighlightingResult plainTextResult = EditorDocumentHighlightingRouter.route( - plainTextBuffer, - registry.resolve("bash"), - null); - - assertTrue(containsStyle(commandResult.styleSpans(), "editor-syntax-bash-builtin")); - assertFalse(containsStyle(plainTextResult.styleSpans(), "editor-syntax-bash-builtin")); - } - - private boolean containsStyle( - final org.fxmisc.richtext.model.StyleSpans> styleSpans, - final String styleClass) { - for (int index = 0; index < styleSpans.getSpanCount(); index++) { - if (styleSpans.getStyleSpan(index).getStyle().contains(styleClass)) { - return true; - } - } - return false; - } -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java deleted file mode 100644 index 13450c3b..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class EditorDocumentPresentationRegistryTest { - private final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - - @Test - void resolvesFrontendTypeIdsToFrontendPresentation() { - final EditorDocumentPresentation presentation = registry.resolve( - "pbs", - new LspSemanticPresentationDTO( - java.util.List.of("pbs-keyword", "pbs-service"), - java.util.List.of("/themes/pbs/semantic-highlighting.css"))); - - assertEquals("pbs", presentation.styleKey()); - assertEquals(java.util.List.of("pbs-keyword", "pbs-service"), presentation.semanticKeys()); - assertEquals(1, presentation.stylesheetUrls().size()); - assertTrue(presentation.stylesheetUrls().getFirst().endsWith("/themes/pbs/semantic-highlighting.css")); - } - - @Test - void missingFrontendResourcesFallbackToFrontendOwnedPresentationResources() { - final EditorDocumentPresentation presentation = registry.resolve( - "pbs", - new LspSemanticPresentationDTO( - java.util.List.of("pbs-keyword"), - java.util.List.of("/themes/pbs/missing.css"))); - - assertEquals("pbs", presentation.styleKey()); - assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys()); - assertEquals(1, presentation.stylesheetUrls().size()); - assertTrue(presentation.highlight("fn main() -> void {}").getStyleSpan(0).getStyle().isEmpty()); - } - - @Test - void resolvesJsonTypeIdsToJsonPresentation() { - final EditorDocumentPresentation presentation = registry.resolve("json"); - - assertEquals("json", presentation.styleKey()); - assertEquals(1, presentation.stylesheetUrls().size()); - } - - @Test - void resolvesBashTypeIdsToBashPresentation() { - final EditorDocumentPresentation presentation = registry.resolve("bash"); - - assertEquals("bash", presentation.styleKey()); - assertEquals(1, presentation.stylesheetUrls().size()); - } - - @Test - void fallsBackToTextPresentationForUnknownTypeIds() { - assertEquals("text", registry.resolve("markdown").styleKey()); - } -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java deleted file mode 100644 index b5efe311..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java +++ /dev/null @@ -1,166 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.lsp.dtos.LspRangeDTO; -import p.studio.lsp.dtos.LspStructuralAnchorDTO; -import p.studio.lsp.dtos.LspSymbolDTO; -import p.studio.lsp.messages.LspSymbolKind; - -import java.nio.file.Path; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -final class EditorDocumentScopeGuideModelTest { - @Test - void selectsActiveScopeAsSmallestContainingRange() { - final String content = """ - struct Hero { - fn attack() { - let value = 1 - } - } - - enum Kind { - Unit - } - """; - final LspSymbolDTO attack = symbol(content, "fn attack()", "}", LspSymbolKind.METHOD, List.of()); - final LspSymbolDTO hero = new LspSymbolDTO( - "struct Hero", - LspSymbolKind.STRUCT, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO( - content.indexOf("struct Hero"), - content.indexOf("\n\nenum Kind")), - List.of(attack)); - final LspSymbolDTO kind = symbol(content, "enum Kind", "}\n", LspSymbolKind.ENUM, List.of()); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(hero, kind)); - final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("let value")); - - assertEquals(content.indexOf("fn attack()"), guides.activeScope().startOffset()); - assertEquals(content.indexOf("struct Hero"), guides.activeContainer().startOffset()); - } - - @Test - void omitsActiveContainerWhenScopeHasNoParent() { - final String content = """ - struct Hero { - hp: Int - } - """; - final LspSymbolDTO symbol = new LspSymbolDTO( - "struct Hero", - LspSymbolKind.STRUCT, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO(0, content.length()), - List.of()); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(symbol)); - final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("hp")); - - assertEquals(content.indexOf("struct Hero"), guides.activeScope().startOffset()); - assertNull(guides.activeContainer()); - } - - @Test - void exposesAtMostTwoActiveSegmentsPerLine() { - final String content = """ - struct Hero { - fn attack() { - let value = 1 - } - } - """; - final LspSymbolDTO attack = symbol(content, "fn attack()", "}", LspSymbolKind.METHOD, List.of()); - final LspSymbolDTO hero = new LspSymbolDTO( - "struct Hero", - LspSymbolKind.STRUCT, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO(0, content.length()), - List.of(attack)); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(hero)); - final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("let value")); - - assertSegments(model, 1, guides, "ACTIVE_CONTAINER:CONTINUE", "ACTIVE_SCOPE:START"); - assertSegments(model, 2, guides, "ACTIVE_CONTAINER:CONTINUE", "ACTIVE_SCOPE:CONTINUE"); - assertSegments(model, 3, guides, "ACTIVE_CONTAINER:CONTINUE", "ACTIVE_SCOPE:END"); - } - - @Test - void ignoresSingleLineAndUnknownSymbols() { - final String content = """ - if ready { - value() - } - """; - final LspSymbolDTO symbol = new LspSymbolDTO( - "if ready", - LspSymbolKind.UNKNOWN, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO(0, content.length()), - List.of()); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(symbol)); - - assertEquals(EditorDocumentScopeGuideModel.ActiveGuides.empty(), model.resolveActiveGuides(content.indexOf("value"))); - } - - @Test - void structuralAnchorsDriveGuideStartAndEndLines() { - final String content = """ - fn main( - value: int - ) -> void - { - helper(); - } - """; - final LspStructuralAnchorDTO anchor = new LspStructuralAnchorDTO( - new LspRangeDTO(content.indexOf("fn main"), content.length()), - new LspRangeDTO(content.indexOf("{"), content.indexOf("{") + 1), - new LspRangeDTO(content.indexOf("}"), content.indexOf("}") + 1), - List.of()); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.fromStructuralAnchors(content, List.of(anchor)); - final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("helper")); - - assertSegments(model, 0, guides); - assertSegments(model, 1, guides); - assertSegments(model, 2, guides); - assertSegments(model, 3, guides, "ACTIVE_SCOPE:START"); - assertSegments(model, 4, guides, "ACTIVE_SCOPE:CONTINUE"); - assertSegments(model, 5, guides, "ACTIVE_SCOPE:END"); - } - - private static LspSymbolDTO symbol( - final String content, - final String startMarker, - final String endMarker, - final LspSymbolKind kind, - final List children) { - final int start = content.indexOf(startMarker); - final int end = content.indexOf(endMarker, start) + endMarker.length(); - return new LspSymbolDTO( - startMarker, - kind, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO(start, end), - children); - } - - private static void assertSegments( - final EditorDocumentScopeGuideModel model, - final int line, - final EditorDocumentScopeGuideModel.ActiveGuides guides, - final String... expectedKinds) { - assertEquals( - List.of(expectedKinds), - model.segmentsForLine(line, guides).stream() - .map(segment -> segment.role().name() + ":" + segment.kind().name()) - .toList()); - } -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorInlineHintLayoutTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorInlineHintLayoutTest.java deleted file mode 100644 index 7bcc9c23..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorInlineHintLayoutTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.lsp.dtos.LspInlineHintDTO; -import p.studio.lsp.dtos.LspRangeDTO; - -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -final class EditorInlineHintLayoutTest { - @Test - void layoutCreatesDecorativePlacementsForHintsWithResolvedAnchors() { - final var hint = new LspInlineHintDTO(new LspRangeDTO(10, 15), "int", "type"); - - final var placements = EditorInlineHintLayout.layout( - List.of(hint), - ignored -> Optional.of(new EditorInlineHintLayout.AnchorBounds(20.0d, 8.0d, 40.0d, 16.0d))); - - assertEquals(1, placements.size()); - assertEquals(": int", placements.getFirst().displayText()); - assertEquals("editor-inline-hint-type", placements.getFirst().categoryStyleClass()); - assertEquals(46.0d, placements.getFirst().screenX()); - assertEquals(8.0d, placements.getFirst().screenY()); - assertEquals(16.0d, placements.getFirst().anchorHeight()); - } - - @Test - void layoutKeepsValidHintsWhenOnlySomeAnchorsResolve() { - final var stable = new LspInlineHintDTO(new LspRangeDTO(10, 15), "int", "type"); - final var missing = new LspInlineHintDTO(new LspRangeDTO(30, 35), "Player", "type"); - - final var placements = EditorInlineHintLayout.layout( - List.of(stable, missing), - hint -> hint == stable - ? Optional.of(new EditorInlineHintLayout.AnchorBounds(12.0d, 4.0d, 28.0d, 14.0d)) - : Optional.empty()); - - assertEquals(1, placements.size()); - assertEquals(stable, placements.getFirst().inlineHint()); - assertEquals(": int", placements.getFirst().displayText()); - } -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorOpenFileSessionTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorOpenFileSessionTest.java deleted file mode 100644 index c793092e..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorOpenFileSessionTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.vfs.messages.VfsDocumentAccessMode; - -import java.nio.file.Path; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -final class EditorOpenFileSessionTest { - - @Test - void openAddsNewFileAndMarksItActive() { - final var session = new EditorOpenFileSession(); - final var file = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "fn main(): void\n"); - - session.open(file); - - assertEquals(1, session.openFiles().size()); - assertEquals(file.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - } - - @Test - void openDoesNotDuplicateExistingTab() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a"); - final var second = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "b"); - - session.open(first); - session.open(second); - - assertEquals(1, session.openFiles().size()); - assertEquals(first.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - assertEquals("b", session.activeFile().orElseThrow().content()); - } - - @Test - void activateSwitchesTheActiveTabWithinTheCurrentSession() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a"); - final var second = fileBuffer(Path.of("src/other.pbs"), "other.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "b"); - - session.open(first); - session.open(second); - session.activate(first.path()); - - assertEquals(first.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - } - - @Test - void reportsSaveStateForDirtyEditableFilesOnly() { - final var session = new EditorOpenFileSession(); - session.open(fileBuffer(Path.of("notes.txt"), "notes.txt", VfsDocumentAccessMode.EDITABLE, true, false, "alpha")); - - assertTrue(session.hasSaveableActiveFile()); - assertTrue(session.hasDirtyEditableFiles()); - } - - @Test - void frontendEditableFilesParticipateInSaveStateWhenDirty() { - final var session = new EditorOpenFileSession(); - session.open(fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, true, true, "fn main(): void")); - - assertTrue(session.hasSaveableActiveFile()); - assertTrue(session.hasDirtyEditableFiles()); - assertTrue(session.activeFile().orElseThrow().frontendDocument()); - assertTrue(session.activeFile().orElseThrow().editable()); - } - - @Test - void closeRemovesInactiveTabWithoutChangingTheActiveTab() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, 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.close(first.path()); - - assertEquals(1, session.openFiles().size()); - assertEquals(second.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - } - - @Test - void closePromotesTheNextTabWhenTheActiveTabIsClosed() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a"); - final var second = fileBuffer(Path.of("README.md"), "README.md", VfsDocumentAccessMode.EDITABLE, false, false, "b"); - final var third = fileBuffer(Path.of("notes.txt"), "notes.txt", VfsDocumentAccessMode.EDITABLE, false, false, "c"); - - session.open(first); - session.open(second); - session.open(third); - session.activate(second.path()); - session.close(second.path()); - - assertEquals(List.of( - first.path().toAbsolutePath().normalize(), - third.path().toAbsolutePath().normalize()), - session.openFiles().stream().map(EditorOpenFileBuffer::path).toList()); - assertEquals(third.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - } - - @Test - void exportsRestorationStateFromOpenTabsAndActiveTab() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, 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( - final Path path, - final String tabLabel, - final VfsDocumentAccessMode accessMode, - final boolean dirty, - final boolean frontendDocument, - final String content) { - return new EditorOpenFileBuffer( - path, - tabLabel, - frontendDocument ? "pbs" : "text", - content, - "LF", - frontendDocument, - accessMode, - dirty); - } -}