implements PLN-0062
This commit is contained in:
parent
85d7211b3c
commit
cebef90118
@ -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-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]}
|
||||||
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||||
{"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"}]}
|
{"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"}]}
|
||||||
|
|||||||
@ -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.
|
||||||
@ -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.
|
||||||
@ -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.
|
||||||
@ -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.
|
||||||
|
|
||||||
@ -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.
|
||||||
|
|
||||||
@ -2,14 +2,10 @@ package p.studio;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import p.packer.Packer;
|
import p.packer.Packer;
|
||||||
import p.studio.lsp.LspServiceFactory;
|
|
||||||
import p.studio.lsp.LspServiceFactoryImpl;
|
|
||||||
import p.studio.lsp.events.StudioEventBus;
|
import p.studio.lsp.events.StudioEventBus;
|
||||||
import p.studio.lsp.events.StudioPackerEventAdapter;
|
import p.studio.lsp.events.StudioPackerEventAdapter;
|
||||||
import p.studio.utilities.ThemeService;
|
import p.studio.utilities.ThemeService;
|
||||||
import p.studio.utilities.i18n.I18nService;
|
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.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@ -21,8 +17,6 @@ public final class AppContainer implements Container {
|
|||||||
private final ThemeService themeService;
|
private final ThemeService themeService;
|
||||||
private final StudioEventBus studioEventBus;
|
private final StudioEventBus studioEventBus;
|
||||||
private final ObjectMapper mapper;
|
private final ObjectMapper mapper;
|
||||||
private final LspServiceFactory lspServiceFactory;
|
|
||||||
private final ProjectDocumentVfsFactory projectDocumentVfsFactory;
|
|
||||||
private final EmbeddedPacker embeddedPacker;
|
private final EmbeddedPacker embeddedPacker;
|
||||||
private final StudioBackgroundTasks backgroundTasks;
|
private final StudioBackgroundTasks backgroundTasks;
|
||||||
|
|
||||||
@ -31,8 +25,6 @@ public final class AppContainer implements Container {
|
|||||||
this.themeService = new ThemeService();
|
this.themeService = new ThemeService();
|
||||||
this.studioEventBus = new StudioEventBus();
|
this.studioEventBus = new StudioEventBus();
|
||||||
this.mapper = new ObjectMapper();
|
this.mapper = new ObjectMapper();
|
||||||
this.lspServiceFactory = new LspServiceFactoryImpl();
|
|
||||||
this.projectDocumentVfsFactory = new FilesystemProjectDocumentVfsFactory();
|
|
||||||
final ExecutorService backgroundExecutor = Executors.newFixedThreadPool(2, new StudioWorkerThreadFactory());
|
final ExecutorService backgroundExecutor = Executors.newFixedThreadPool(2, new StudioWorkerThreadFactory());
|
||||||
this.backgroundTasks = new StudioBackgroundTasks(backgroundExecutor);
|
this.backgroundTasks = new StudioBackgroundTasks(backgroundExecutor);
|
||||||
final Packer packer = Packer.bootstrap(this.mapper, new StudioPackerEventAdapter(studioEventBus));
|
final Packer packer = Packer.bootstrap(this.mapper, new StudioPackerEventAdapter(studioEventBus));
|
||||||
@ -59,16 +51,6 @@ public final class AppContainer implements Container {
|
|||||||
return mapper;
|
return mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public LspServiceFactory getPrometeuLspServiceFactory() {
|
|
||||||
return lspServiceFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ProjectDocumentVfsFactory getProjectDocumentVfsFactory() {
|
|
||||||
return projectDocumentVfsFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EmbeddedPacker getPacker() {
|
public EmbeddedPacker getPacker() {
|
||||||
return embeddedPacker;
|
return embeddedPacker;
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
package p.studio;
|
package p.studio;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import p.studio.lsp.LspServiceFactory;
|
|
||||||
import p.studio.lsp.events.StudioEventBus;
|
import p.studio.lsp.events.StudioEventBus;
|
||||||
import p.studio.utilities.ThemeService;
|
import p.studio.utilities.ThemeService;
|
||||||
import p.studio.utilities.i18n.I18nService;
|
import p.studio.utilities.i18n.I18nService;
|
||||||
import p.studio.vfs.ProjectDocumentVfsFactory;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@ -19,10 +17,6 @@ public interface Container {
|
|||||||
|
|
||||||
ObjectMapper getMapper();
|
ObjectMapper getMapper();
|
||||||
|
|
||||||
LspServiceFactory getPrometeuLspServiceFactory();
|
|
||||||
|
|
||||||
ProjectDocumentVfsFactory getProjectDocumentVfsFactory();
|
|
||||||
|
|
||||||
EmbeddedPacker getPacker();
|
EmbeddedPacker getPacker();
|
||||||
|
|
||||||
StudioBackgroundTasks getBackgroundTasks();
|
StudioBackgroundTasks getBackgroundTasks();
|
||||||
@ -66,14 +60,6 @@ public interface Container {
|
|||||||
return current().getMapper();
|
return current().getMapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
static LspServiceFactory prometeuLspServiceFactory() {
|
|
||||||
return current().getPrometeuLspServiceFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
static ProjectDocumentVfsFactory projectDocumentVfsFactory() {
|
|
||||||
return current().getProjectDocumentVfsFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
static EmbeddedPacker packer() {
|
static EmbeddedPacker packer() {
|
||||||
return current().getPacker();
|
return current().getPacker();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,15 @@
|
|||||||
package p.studio.projectsessions;
|
package p.studio.projectsessions;
|
||||||
|
|
||||||
import p.studio.execution.StudioExecutionSessionService;
|
import p.studio.execution.StudioExecutionSessionService;
|
||||||
import p.studio.lsp.LspService;
|
|
||||||
import p.studio.projectstate.ProjectLocalStudioSetup;
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projectstate.ProjectLocalStudioStateService;
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.vfs.VfsProjectDocument;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class StudioProjectSession implements AutoCloseable {
|
public final class StudioProjectSession implements AutoCloseable {
|
||||||
private final ProjectReference projectReference;
|
private final ProjectReference projectReference;
|
||||||
private final LspService prometeuLspService;
|
|
||||||
private final VfsProjectDocument vfsProjectDocument;
|
|
||||||
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
||||||
private final ProjectLocalStudioSetup projectLocalStudioSetup;
|
private final ProjectLocalStudioSetup projectLocalStudioSetup;
|
||||||
private final StudioExecutionSessionService executionSessionService;
|
private final StudioExecutionSessionService executionSessionService;
|
||||||
@ -21,13 +17,9 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
|
||||||
public StudioProjectSession(
|
public StudioProjectSession(
|
||||||
final ProjectReference projectReference,
|
final ProjectReference projectReference) {
|
||||||
final LspService prometeuLspService,
|
|
||||||
final VfsProjectDocument vfsProjectDocument) {
|
|
||||||
this(
|
this(
|
||||||
projectReference,
|
projectReference,
|
||||||
prometeuLspService,
|
|
||||||
vfsProjectDocument,
|
|
||||||
new ProjectLocalStudioStateService(),
|
new ProjectLocalStudioStateService(),
|
||||||
ProjectLocalStudioSetup.defaults(),
|
ProjectLocalStudioSetup.defaults(),
|
||||||
new StudioExecutionSessionService(),
|
new StudioExecutionSessionService(),
|
||||||
@ -36,15 +28,11 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
|
|
||||||
StudioProjectSession(
|
StudioProjectSession(
|
||||||
final ProjectReference projectReference,
|
final ProjectReference projectReference,
|
||||||
final LspService prometeuLspService,
|
|
||||||
final VfsProjectDocument vfsProjectDocument,
|
|
||||||
final ProjectLocalStudioStateService projectLocalStudioStateService,
|
final ProjectLocalStudioStateService projectLocalStudioStateService,
|
||||||
final ProjectLocalStudioSetup projectLocalStudioSetup,
|
final ProjectLocalStudioSetup projectLocalStudioSetup,
|
||||||
final StudioExecutionSessionService executionSessionService,
|
final StudioExecutionSessionService executionSessionService,
|
||||||
final ProjectLocalStudioState projectLocalStudioState) {
|
final ProjectLocalStudioState projectLocalStudioState) {
|
||||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
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.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
||||||
this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup");
|
this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup");
|
||||||
this.executionSessionService = Objects.requireNonNull(executionSessionService, "executionSessionService");
|
this.executionSessionService = Objects.requireNonNull(executionSessionService, "executionSessionService");
|
||||||
@ -55,14 +43,6 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
return projectReference;
|
return projectReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VfsProjectDocument projectDocumentVfs() {
|
|
||||||
return vfsProjectDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LspService prometeuLspService() {
|
|
||||||
return prometeuLspService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProjectLocalStudioState projectLocalStudioState() {
|
public ProjectLocalStudioState projectLocalStudioState() {
|
||||||
return projectLocalStudioState;
|
return projectLocalStudioState;
|
||||||
}
|
}
|
||||||
@ -96,25 +76,13 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
failure = runtimeException;
|
failure = runtimeException;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
prometeuLspService.close();
|
if (failure != null) {
|
||||||
} catch (RuntimeException runtimeException) {
|
throw failure;
|
||||||
if (failure == null) {
|
|
||||||
failure = runtimeException;
|
|
||||||
} else {
|
|
||||||
failure.addSuppressed(runtimeException);
|
|
||||||
}
|
}
|
||||||
}
|
} finally {
|
||||||
try {
|
if (failure != null) {
|
||||||
vfsProjectDocument.close();
|
throw failure;
|
||||||
} catch (RuntimeException runtimeException) {
|
|
||||||
if (failure == null) {
|
|
||||||
failure = runtimeException;
|
|
||||||
} else {
|
|
||||||
failure.addSuppressed(runtimeException);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failure != null) {
|
|
||||||
throw failure;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +1,36 @@
|
|||||||
package p.studio.projectsessions;
|
package p.studio.projectsessions;
|
||||||
|
|
||||||
import p.studio.execution.StudioExecutionSessionService;
|
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.ProjectLocalStudioSetupService;
|
||||||
import p.studio.projectstate.ProjectLocalStudioStateService;
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.vfs.VfsProjectDocument;
|
|
||||||
import p.studio.vfs.ProjectDocumentVfsFactory;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class StudioProjectSessionFactory {
|
public final class StudioProjectSessionFactory {
|
||||||
private final LspServiceFactory lspServiceFactory;
|
|
||||||
private final ProjectDocumentVfsFactory projectDocumentVfsFactory;
|
|
||||||
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
||||||
private final ProjectLocalStudioSetupService projectLocalStudioSetupService;
|
private final ProjectLocalStudioSetupService projectLocalStudioSetupService;
|
||||||
|
|
||||||
public StudioProjectSessionFactory(
|
public StudioProjectSessionFactory() {
|
||||||
final LspServiceFactory lspServiceFactory,
|
|
||||||
final ProjectDocumentVfsFactory projectDocumentVfsFactory) {
|
|
||||||
this(
|
this(
|
||||||
lspServiceFactory,
|
|
||||||
projectDocumentVfsFactory,
|
|
||||||
new ProjectLocalStudioStateService(),
|
new ProjectLocalStudioStateService(),
|
||||||
new ProjectLocalStudioSetupService());
|
new ProjectLocalStudioSetupService());
|
||||||
}
|
}
|
||||||
|
|
||||||
StudioProjectSessionFactory(
|
StudioProjectSessionFactory(
|
||||||
final LspServiceFactory lspServiceFactory,
|
|
||||||
final ProjectDocumentVfsFactory projectDocumentVfsFactory,
|
|
||||||
final ProjectLocalStudioStateService projectLocalStudioStateService,
|
final ProjectLocalStudioStateService projectLocalStudioStateService,
|
||||||
final ProjectLocalStudioSetupService projectLocalStudioSetupService) {
|
final ProjectLocalStudioSetupService projectLocalStudioSetupService) {
|
||||||
this.lspServiceFactory = Objects.requireNonNull(lspServiceFactory, "prometeuLspServiceFactory");
|
|
||||||
this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory");
|
|
||||||
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
||||||
this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService");
|
this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService");
|
||||||
}
|
}
|
||||||
|
|
||||||
public StudioProjectSession open(final ProjectReference projectReference) {
|
public StudioProjectSession open(final ProjectReference projectReference) {
|
||||||
final ProjectReference target = Objects.requireNonNull(projectReference, "projectReference");
|
final ProjectReference target = Objects.requireNonNull(projectReference, "projectReference");
|
||||||
final VfsProjectDocument vfsProjectDocument = projectDocumentVfsFactory.open(target.toVfsProjectContext());
|
|
||||||
return new StudioProjectSession(
|
return new StudioProjectSession(
|
||||||
target,
|
target,
|
||||||
lspServiceFactory.open(lspProjectContext(target), vfsProjectDocument),
|
|
||||||
vfsProjectDocument,
|
|
||||||
projectLocalStudioStateService,
|
projectLocalStudioStateService,
|
||||||
projectLocalStudioSetupService.load(target),
|
projectLocalStudioSetupService.load(target),
|
||||||
new StudioExecutionSessionService(),
|
new StudioExecutionSessionService(),
|
||||||
projectLocalStudioStateService.load(target));
|
projectLocalStudioStateService.load(target));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LspProjectContext lspProjectContext(final ProjectReference projectReference) {
|
|
||||||
return new LspProjectContext(
|
|
||||||
projectReference.name(),
|
|
||||||
projectReference.languageId(),
|
|
||||||
projectReference.rootPath());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,57 +3,48 @@ package p.studio.projectstate;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record ProjectLocalStudioState(
|
public record ProjectLocalStudioState(
|
||||||
int version,
|
int version,
|
||||||
ShellLayout shellLayout,
|
ShellLayout shellLayout,
|
||||||
OpenShellState openShellState,
|
OpenShellState openShellState) {
|
||||||
EditorRestorationState editorRestoration) {
|
|
||||||
public static final int CURRENT_VERSION = 1;
|
public static final int CURRENT_VERSION = 1;
|
||||||
|
|
||||||
public ProjectLocalStudioState {
|
public ProjectLocalStudioState {
|
||||||
version = version <= 0 ? CURRENT_VERSION : version;
|
version = version <= 0 ? CURRENT_VERSION : version;
|
||||||
shellLayout = shellLayout == null ? ShellLayout.defaults() : shellLayout.normalize();
|
shellLayout = shellLayout == null ? ShellLayout.defaults() : shellLayout.normalize();
|
||||||
openShellState = openShellState == null ? OpenShellState.defaults() : openShellState.normalize();
|
openShellState = openShellState == null ? OpenShellState.defaults() : openShellState.normalize();
|
||||||
editorRestoration = editorRestoration == null ? EditorRestorationState.defaults() : editorRestoration.normalize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProjectLocalStudioState defaults() {
|
public static ProjectLocalStudioState defaults() {
|
||||||
return new ProjectLocalStudioState(
|
return new ProjectLocalStudioState(
|
||||||
CURRENT_VERSION,
|
CURRENT_VERSION,
|
||||||
ShellLayout.defaults(),
|
ShellLayout.defaults(),
|
||||||
OpenShellState.defaults(),
|
OpenShellState.defaults());
|
||||||
EditorRestorationState.defaults());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProjectLocalStudioState withShellLayout(final ShellLayout nextShellLayout) {
|
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) {
|
public ProjectLocalStudioState withOpenShellState(final OpenShellState nextOpenShellState) {
|
||||||
return new ProjectLocalStudioState(version, shellLayout, nextOpenShellState, editorRestoration);
|
return new ProjectLocalStudioState(version, shellLayout, nextOpenShellState);
|
||||||
}
|
|
||||||
|
|
||||||
public ProjectLocalStudioState withEditorRestoration(final EditorRestorationState nextEditorRestoration) {
|
|
||||||
return new ProjectLocalStudioState(version, shellLayout, openShellState, nextEditorRestoration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record ShellLayout(EditorLayoutState editorLayout, AssetsLayoutState assetsLayout) {
|
public record ShellLayout(AssetsLayoutState assetsLayout) {
|
||||||
public ShellLayout {
|
public ShellLayout {
|
||||||
editorLayout = editorLayout == null ? EditorLayoutState.defaults() : editorLayout.normalize();
|
|
||||||
assetsLayout = assetsLayout == null ? AssetsLayoutState.defaults() : assetsLayout.normalize();
|
assetsLayout = assetsLayout == null ? AssetsLayoutState.defaults() : assetsLayout.normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ShellLayout defaults() {
|
public static ShellLayout defaults() {
|
||||||
return new ShellLayout(EditorLayoutState.defaults(), AssetsLayoutState.defaults());
|
return new ShellLayout(AssetsLayoutState.defaults());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ShellLayout normalize() {
|
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 record OpenShellState(String selectedWorkspaceId) {
|
||||||
public OpenShellState {
|
public OpenShellState {
|
||||||
selectedWorkspaceId = normalizeText(selectedWorkspaceId);
|
selectedWorkspaceId = normalizeText(selectedWorkspaceId);
|
||||||
|
if ("EDITOR".equals(selectedWorkspaceId)) {
|
||||||
|
selectedWorkspaceId = "ASSETS";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OpenShellState defaults() {
|
public static OpenShellState defaults() {
|
||||||
@ -87,80 +81,6 @@ public record ProjectLocalStudioState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record EditorLayoutState(
|
|
||||||
List<Double> contentSplitDividers,
|
|
||||||
List<Double> leftColumnDividers,
|
|
||||||
List<Double> rightColumnDividers,
|
|
||||||
DockPanelState outlinePanel,
|
|
||||||
DockPanelState helperPanel,
|
|
||||||
List<String> navigatorExpandedPaths) {
|
|
||||||
public EditorLayoutState {
|
|
||||||
contentSplitDividers = normalizeDividerPositions(contentSplitDividers);
|
|
||||||
leftColumnDividers = normalizeDividerPositions(leftColumnDividers);
|
|
||||||
rightColumnDividers = normalizeDividerPositions(rightColumnDividers);
|
|
||||||
outlinePanel = outlinePanel == null ? DockPanelState.defaults() : outlinePanel.normalize();
|
|
||||||
helperPanel = helperPanel == null ? DockPanelState.defaults() : helperPanel.normalize();
|
|
||||||
navigatorExpandedPaths = normalizePaths(navigatorExpandedPaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static EditorLayoutState defaults() {
|
|
||||||
return new EditorLayoutState(
|
|
||||||
List.of(),
|
|
||||||
List.of(),
|
|
||||||
List.of(),
|
|
||||||
DockPanelState.defaults(),
|
|
||||||
DockPanelState.defaults(),
|
|
||||||
List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
private EditorLayoutState normalize() {
|
|
||||||
return new EditorLayoutState(
|
|
||||||
contentSplitDividers,
|
|
||||||
leftColumnDividers,
|
|
||||||
rightColumnDividers,
|
|
||||||
outlinePanel,
|
|
||||||
helperPanel,
|
|
||||||
navigatorExpandedPaths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record DockPanelState(boolean expanded, Double dividerPosition) {
|
|
||||||
public DockPanelState {
|
|
||||||
dividerPosition = normalizeDividerPosition(dividerPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DockPanelState defaults() {
|
|
||||||
return new DockPanelState(true, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DockPanelState normalize() {
|
|
||||||
return new DockPanelState(expanded, dividerPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record EditorRestorationState(List<String> openTabPaths, String activeTabPath) {
|
|
||||||
public EditorRestorationState {
|
|
||||||
openTabPaths = normalizePaths(openTabPaths);
|
|
||||||
activeTabPath = normalizeText(activeTabPath);
|
|
||||||
if (activeTabPath != null && !openTabPaths.contains(activeTabPath)) {
|
|
||||||
final List<String> expanded = new ArrayList<>(openTabPaths);
|
|
||||||
expanded.add(activeTabPath);
|
|
||||||
openTabPaths = List.copyOf(expanded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static EditorRestorationState defaults() {
|
|
||||||
return new EditorRestorationState(List.of(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EditorRestorationState normalize() {
|
|
||||||
return new EditorRestorationState(openTabPaths, activeTabPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeText(final String value) {
|
private static String normalizeText(final String value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return null;
|
return null;
|
||||||
@ -169,20 +89,6 @@ public record ProjectLocalStudioState(
|
|||||||
return trimmed.isEmpty() ? null : trimmed;
|
return trimmed.isEmpty() ? null : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> normalizePaths(final List<String> values) {
|
|
||||||
if (values == null || values.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
final LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
|
||||||
for (final String value : values) {
|
|
||||||
final String text = normalizeText(value);
|
|
||||||
if (text != null) {
|
|
||||||
normalized.add(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.copyOf(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Double> normalizeDividerPositions(final List<Double> values) {
|
private static List<Double> normalizeDividerPositions(final List<Double> values) {
|
||||||
if (values == null || values.isEmpty()) {
|
if (values == null || values.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
@ -196,11 +102,4 @@ public record ProjectLocalStudioState(
|
|||||||
}
|
}
|
||||||
return List.copyOf(normalized);
|
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import p.studio.workspaces.WorkspaceId;
|
|||||||
import p.studio.workspaces.assets.AssetWorkspace;
|
import p.studio.workspaces.assets.AssetWorkspace;
|
||||||
import p.studio.workspaces.builder.ShipperWorkspace;
|
import p.studio.workspaces.builder.ShipperWorkspace;
|
||||||
import p.studio.workspaces.debug.DebugWorkspace;
|
import p.studio.workspaces.debug.DebugWorkspace;
|
||||||
import p.studio.workspaces.editor.EditorWorkspace;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -24,7 +23,6 @@ public final class MainView extends BorderPane {
|
|||||||
private final ProjectReference projectReference;
|
private final ProjectReference projectReference;
|
||||||
private final StudioProjectSession projectSession;
|
private final StudioProjectSession projectSession;
|
||||||
private final AssetWorkspace assetWorkspace;
|
private final AssetWorkspace assetWorkspace;
|
||||||
private final EditorWorkspace editorWorkspace;
|
|
||||||
private final StudioWorkspaceRailControl<WorkspaceId> workspaceRail;
|
private final StudioWorkspaceRailControl<WorkspaceId> workspaceRail;
|
||||||
private final StudioPlayStopCoordinator playStopCoordinator;
|
private final StudioPlayStopCoordinator playStopCoordinator;
|
||||||
private boolean initializing;
|
private boolean initializing;
|
||||||
@ -50,23 +48,14 @@ public final class MainView extends BorderPane {
|
|||||||
|
|
||||||
assetWorkspace = new AssetWorkspace(projectReference);
|
assetWorkspace = new AssetWorkspace(projectReference);
|
||||||
host.register(assetWorkspace);
|
host.register(assetWorkspace);
|
||||||
editorWorkspace = new EditorWorkspace(
|
|
||||||
projectReference,
|
|
||||||
projectSession.projectDocumentVfs(),
|
|
||||||
projectSession.prometeuLspService(),
|
|
||||||
projectSetup.editorIndentation());
|
|
||||||
host.register(editorWorkspace);
|
|
||||||
assetWorkspace.restoreProjectLocalState(persistedState);
|
assetWorkspace.restoreProjectLocalState(persistedState);
|
||||||
editorWorkspace.restoreProjectLocalState(persistedState);
|
|
||||||
assetWorkspace.setStateChangedAction(this::persistProjectLocalState);
|
assetWorkspace.setStateChangedAction(this::persistProjectLocalState);
|
||||||
editorWorkspace.setStateChangedAction(this::persistProjectLocalState);
|
|
||||||
host.register(new DebugWorkspace(projectReference, projectSession.executionSession()));
|
host.register(new DebugWorkspace(projectReference, projectSession.executionSession()));
|
||||||
host.register(new ShipperWorkspace(projectReference));
|
host.register(new ShipperWorkspace(projectReference));
|
||||||
|
|
||||||
workspaceRail = new StudioWorkspaceRailControl<>(
|
workspaceRail = new StudioWorkspaceRailControl<>(
|
||||||
List.of(
|
List.of(
|
||||||
new StudioWorkspaceRailItem<>(WorkspaceId.ASSETS, "📦", Container.i18n().bind(I18n.WORKSPACE_ASSETS)),
|
new StudioWorkspaceRailItem<>(WorkspaceId.ASSETS, "📦", Container.i18n().bind(I18n.WORKSPACE_ASSETS)),
|
||||||
new StudioWorkspaceRailItem<>(WorkspaceId.EDITOR, "📝", Container.i18n().bind(I18n.WORKSPACE_CODE)),
|
|
||||||
new StudioWorkspaceRailItem<>(WorkspaceId.DEBUG, "🎮", Container.i18n().bind(I18n.WORKSPACE_DEBUG)),
|
new StudioWorkspaceRailItem<>(WorkspaceId.DEBUG, "🎮", Container.i18n().bind(I18n.WORKSPACE_DEBUG)),
|
||||||
new StudioWorkspaceRailItem<>(WorkspaceId.SHIPPER, "⚙️", Container.i18n().bind(I18n.WORKSPACE_SHIPPER))
|
new StudioWorkspaceRailItem<>(WorkspaceId.SHIPPER, "⚙️", Container.i18n().bind(I18n.WORKSPACE_SHIPPER))
|
||||||
),
|
),
|
||||||
@ -102,14 +91,11 @@ public final class MainView extends BorderPane {
|
|||||||
projectSession.replaceProjectLocalStudioState(
|
projectSession.replaceProjectLocalStudioState(
|
||||||
current.withOpenShellState(new ProjectLocalStudioState.OpenShellState(selectedWorkspace.name()))
|
current.withOpenShellState(new ProjectLocalStudioState.OpenShellState(selectedWorkspace.name()))
|
||||||
.withShellLayout(new ProjectLocalStudioState.ShellLayout(
|
.withShellLayout(new ProjectLocalStudioState.ShellLayout(
|
||||||
editorWorkspace.captureLayoutState(),
|
assetWorkspace.captureLayoutState())));
|
||||||
assetWorkspace.captureLayoutState()))
|
|
||||||
.withEditorRestoration(editorWorkspace.captureRestorationState()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reapplyProjectLocalLayout() {
|
public void reapplyProjectLocalLayout() {
|
||||||
assetWorkspace.reapplyPendingLayoutState();
|
assetWorkspace.reapplyPendingLayoutState();
|
||||||
editorWorkspace.reapplyPendingLayoutState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
|
|||||||
@ -37,9 +37,7 @@ public final class StudioWindowCoordinator {
|
|||||||
this.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot());
|
this.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot());
|
||||||
this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService);
|
this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService);
|
||||||
this.windowStateService = new WindowStateService(resolveWindowStateStorage());
|
this.windowStateService = new WindowStateService(resolveWindowStateStorage());
|
||||||
this.projectSessionFactory = new StudioProjectSessionFactory(
|
this.projectSessionFactory = new StudioProjectSessionFactory();
|
||||||
Container.prometeuLspServiceFactory(),
|
|
||||||
Container.projectDocumentVfsFactory());
|
|
||||||
this.launcherView = new ProjectLauncherView(
|
this.launcherView = new ProjectLauncherView(
|
||||||
knownProjectsService,
|
knownProjectsService,
|
||||||
projectCatalogService,
|
projectCatalogService,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package p.studio.workspaces;
|
package p.studio.workspaces;
|
||||||
|
|
||||||
public enum WorkspaceId {
|
public enum WorkspaceId {
|
||||||
EDITOR,
|
|
||||||
ASSETS,
|
ASSETS,
|
||||||
SHIPPER,
|
SHIPPER,
|
||||||
DEBUG
|
DEBUG
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
package p.studio.workspaces.editor;
|
|
||||||
|
|
||||||
enum EditorDocumentHighlightOwner {
|
|
||||||
LOCAL,
|
|
||||||
LSP
|
|
||||||
}
|
|
||||||
@ -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<Collection<String>> styleSpans,
|
|
||||||
List<LspInlineHintDTO> inlineHints) {
|
|
||||||
|
|
||||||
EditorDocumentHighlightingResult {
|
|
||||||
owner = Objects.requireNonNull(owner, "owner");
|
|
||||||
styleSpans = Objects.requireNonNull(styleSpans, "styleSpans");
|
|
||||||
inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<LspInlineHintDTO> route(
|
|
||||||
final EditorOpenFileBuffer fileBuffer,
|
|
||||||
final LspAnalyzeDocumentResult analysis) {
|
|
||||||
if (!fileBuffer.frontendDocument() || analysis == null || analysis.inlineHints().isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return analysis.inlineHints();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<String> stylesheetUrls,
|
|
||||||
List<String> 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<Collection<String>> highlight(final String content) {
|
|
||||||
return syntaxHighlighting.highlight(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean supportsSemanticHighlighting() {
|
|
||||||
return !semanticKeys.isEmpty() && !stylesheetUrls.isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<String> stylesheetUrls = resolveFrontendStylesheets(
|
|
||||||
semanticPresentation == null ? java.util.List.of() : semanticPresentation.resources(),
|
|
||||||
frontendPresentationSpec.resources());
|
|
||||||
final java.util.List<String> 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<String> 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<String> resolveFrontendStylesheets(
|
|
||||||
final java.util.List<String> primaryResources,
|
|
||||||
final java.util.List<String> fallbackResources) {
|
|
||||||
final java.util.List<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<EditorDocumentScopeGuideModel.ActiveGuideSegment> segments;
|
|
||||||
|
|
||||||
private ScopeGuideCanvas(
|
|
||||||
final java.util.List<EditorDocumentScopeGuideModel.ActiveGuideSegment> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<GuideRange> ranges;
|
|
||||||
private final List<Integer> lineStarts;
|
|
||||||
private final int contentLength;
|
|
||||||
|
|
||||||
private EditorDocumentScopeGuideModel(
|
|
||||||
final List<GuideRange> ranges,
|
|
||||||
final List<Integer> 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<LspSymbolDTO> 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<LspStructuralAnchorDTO> 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 <T> EditorDocumentScopeGuideModel fromRanges(
|
|
||||||
final String content,
|
|
||||||
final List<T> roots,
|
|
||||||
final int initialDepth,
|
|
||||||
final Integer initialParentId,
|
|
||||||
final RangeFactory<T> rangeFactory,
|
|
||||||
final ChildAccessor<T> childAccessor) {
|
|
||||||
final List<Integer> lineStarts = lineStarts(content);
|
|
||||||
final List<GuideRange> 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<ActiveGuideSegment> segmentsForLine(
|
|
||||||
final int lineIndex,
|
|
||||||
final ActiveGuides activeGuides) {
|
|
||||||
if (lineIndex < 0 || lineIndex >= totalLines() || activeGuides.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
final List<ActiveGuideSegment> 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 <T> void appendRangeNode(
|
|
||||||
final List<GuideRange> ranges,
|
|
||||||
final List<Integer> lineStarts,
|
|
||||||
final int contentLength,
|
|
||||||
final T node,
|
|
||||||
final int depth,
|
|
||||||
final Integer parentId,
|
|
||||||
final int[] nextId,
|
|
||||||
final RangeFactory<T> rangeFactory,
|
|
||||||
final ChildAccessor<T> 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<Integer> 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<ActiveGuideSegment> 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<Integer> lineStarts(final String content) {
|
|
||||||
if (content.isEmpty()) {
|
|
||||||
return List.of(0);
|
|
||||||
}
|
|
||||||
final List<Integer> 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<Integer> 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<T> {
|
|
||||||
List<T> children(T node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
private interface RangeFactory<T> {
|
|
||||||
GuideRange create(
|
|
||||||
T node,
|
|
||||||
int depth,
|
|
||||||
Integer parentId,
|
|
||||||
int[] nextId,
|
|
||||||
List<Integer> lineStarts,
|
|
||||||
int contentLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<LspDiagnosticDTO> diagnostics) {
|
|
||||||
summary.textProperty().unbind();
|
|
||||||
summary.setText(documentPath.getFileName() + " • semantic read-only");
|
|
||||||
rebuildDiagnostics(diagnostics);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rebuildDiagnostics(final List<LspDiagnosticDTO> 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<Placement> layout(
|
|
||||||
final List<LspInlineHintDTO> inlineHints,
|
|
||||||
final AnchorBoundsResolver anchorBoundsResolver) {
|
|
||||||
final var placements = new ArrayList<Placement>();
|
|
||||||
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<AnchorBounds> 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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<LspInlineHintDTO, Label> labelsByHint = new LinkedHashMap<>();
|
|
||||||
private final Subscription viewportSubscription;
|
|
||||||
private List<LspInlineHintDTO> 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<LspInlineHintDTO> 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<LspInlineHintDTO>();
|
|
||||||
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<EditorInlineHintLayout.AnchorBounds> 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<Bounds> characterBounds = codeArea.getCharacterBoundsOnScreen(anchorOffset, anchorOffset);
|
|
||||||
return characterBounds.map(bounds -> new EditorInlineHintLayout.AnchorBounds(
|
|
||||||
bounds.getMinX(),
|
|
||||||
bounds.getMinY(),
|
|
||||||
bounds.getMaxX(),
|
|
||||||
bounds.getHeight()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<Collection<String>> displayStyles;
|
|
||||||
private final List<ProtectedRange> protectedRanges;
|
|
||||||
|
|
||||||
private EditorInlineHintProjection(
|
|
||||||
final String displayText,
|
|
||||||
final StyleSpans<Collection<String>> displayStyles,
|
|
||||||
final List<ProtectedRange> 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<Collection<String>> sourceStyles,
|
|
||||||
final List<LspInlineHintDTO> 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<Collection<String>>();
|
|
||||||
final var protectedRanges = new ArrayList<ProtectedRange>();
|
|
||||||
|
|
||||||
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<Collection<String>> 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<Collection<String>> builder,
|
|
||||||
final StyleSpans<Collection<String>> styles) {
|
|
||||||
for (final StyleSpan<Collection<String>> span : styles) {
|
|
||||||
builder.add(span.getStyle(), span.getLength());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Collection<String> hintClasses(final LspInlineHintDTO inlineHint) {
|
|
||||||
final var classes = new LinkedHashSet<String>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<EditorOpenFileBuffer> 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<EditorOpenFileBuffer> openFiles() {
|
|
||||||
return List.copyOf(openFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<EditorOpenFileBuffer> activeFile() {
|
|
||||||
if (activePath == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return find(activePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<EditorOpenFileBuffer> 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<EditorOpenFileBuffer> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<LspSymbolDTO> symbols) {
|
|
||||||
summary.textProperty().unbind();
|
|
||||||
summary.setText(documentPath.getFileName() + " • semantic read-only");
|
|
||||||
rebuildSymbols(symbols);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rebuildSymbols(final List<LspSymbolDTO> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<VfsProjectNode> treeView = new TreeView<>();
|
|
||||||
private final Label emptyState = emptyLabel();
|
|
||||||
private final StackPane content = new StackPane();
|
|
||||||
private Runnable revealActiveFileAction = () -> { };
|
|
||||||
private Runnable refreshAction = () -> { };
|
|
||||||
private Consumer<VfsProjectNode> fileSelectionAction = node -> { };
|
|
||||||
private Set<Path> 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<VfsProjectNode> 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<VfsProjectNode> 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<String> expandedPathStrings() {
|
|
||||||
return captureExpandedPaths(treeView.getRoot()).stream()
|
|
||||||
.map(Path::toString)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restoreExpandedPaths(final List<String> expandedPaths) {
|
|
||||||
final LinkedHashSet<Path> restored = new LinkedHashSet<>();
|
|
||||||
if (expandedPaths != null) {
|
|
||||||
for (final String expandedPath : expandedPaths) {
|
|
||||||
if (expandedPath == null || expandedPath.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
restored.add(Path.of(expandedPath).toAbsolutePath().normalize());
|
|
||||||
} catch (RuntimeException ignored) {
|
|
||||||
// Ignore malformed persisted paths and continue with the remaining ones.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
preferredExpandedPaths = Set.copyOf(restored);
|
|
||||||
final TreeItem<VfsProjectNode> rootItem = treeView.getRoot();
|
|
||||||
if (rootItem != null) {
|
|
||||||
final var currentSelection = selectedPath();
|
|
||||||
treeView.setRoot(buildTreeItem(rootItem.getValue(), preferredExpandedPaths, true));
|
|
||||||
restoreSelection(treeView.getRoot(), currentSelection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void revealPath(final Path path) {
|
|
||||||
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<VfsProjectNode> buildTreeItem(
|
|
||||||
final VfsProjectNode node,
|
|
||||||
final Set<Path> expandedPaths,
|
|
||||||
final boolean isRoot) {
|
|
||||||
final TreeItem<VfsProjectNode> 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<Path> captureExpandedPaths(final TreeItem<VfsProjectNode> item) {
|
|
||||||
final var expandedPaths = new HashSet<Path>();
|
|
||||||
if (item == null) {
|
|
||||||
return expandedPaths;
|
|
||||||
}
|
|
||||||
collectExpandedPaths(item, expandedPaths);
|
|
||||||
return expandedPaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void collectExpandedPaths(final TreeItem<VfsProjectNode> item, final Set<Path> expandedPaths) {
|
|
||||||
final var value = item.getValue();
|
|
||||||
if (value != null && item.isExpanded()) {
|
|
||||||
expandedPaths.add(value.path());
|
|
||||||
}
|
|
||||||
item.getChildren().forEach(child -> collectExpandedPaths(child, expandedPaths));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<Path> mergeExpandedPaths() {
|
|
||||||
final LinkedHashSet<Path> merged = new LinkedHashSet<>(preferredExpandedPaths);
|
|
||||||
merged.addAll(captureExpandedPaths(treeView.getRoot()));
|
|
||||||
return Set.copyOf(merged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Path> selectedPath() {
|
|
||||||
return Optional.ofNullable(treeView.getSelectionModel().getSelectedItem())
|
|
||||||
.map(TreeItem::getValue)
|
|
||||||
.map(VfsProjectNode::path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void restoreSelection(final TreeItem<VfsProjectNode> rootItem, final Optional<Path> selectedPath) {
|
|
||||||
treeView.getSelectionModel().clearSelection();
|
|
||||||
if (selectedPath.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
findTreeItem(rootItem, selectedPath.orElseThrow())
|
|
||||||
.ifPresent(item -> treeView.getSelectionModel().select(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<TreeItem<VfsProjectNode>> findTreeItem(final TreeItem<VfsProjectNode> 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<VfsProjectNode> item) {
|
|
||||||
TreeItem<VfsProjectNode> current = item;
|
|
||||||
while (current != null) {
|
|
||||||
current.setExpanded(true);
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node iconFor(final VfsProjectNode node, final TreeItem<VfsProjectNode> 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<VfsProjectNode> 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<VfsProjectNode> 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<VfsProjectNode> 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<VfsProjectNode> {
|
|
||||||
private DirectoryTreeItem(final VfsProjectNode node) {
|
|
||||||
super(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLeaf() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<EditorOpenFileBuffer> openFiles = new ArrayList<>();
|
|
||||||
private Consumer<Path> tabSelectionAction = path -> { };
|
|
||||||
private Consumer<Path> 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<Path> tabSelectionAction) {
|
|
||||||
this.tabSelectionAction = Objects.requireNonNull(tabSelectionAction, "tabSelectionAction");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTabCloseAction(final Consumer<Path> tabCloseAction) {
|
|
||||||
this.tabCloseAction = Objects.requireNonNull(tabCloseAction, "tabCloseAction");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showOpenFiles(final List<EditorOpenFileBuffer> 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<MenuItem> buildOverflowItems(final int startIndex, final int endIndex) {
|
|
||||||
final var items = new ArrayList<MenuItem>();
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<WarningItem> 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<WarningItem> 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<CodeArea> 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<String> activePresentationStylesheets = new ArrayList<>();
|
|
||||||
private final IntFunction<Node> 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<Double> dividerPositions(final SplitPane splitPane) {
|
|
||||||
if (splitPane == null || splitPane.getDividers().isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return splitPane.getDividers().stream()
|
|
||||||
.map(SplitPane.Divider::getPosition)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyDividerPositions(final SplitPane splitPane, final List<Double> positions) {
|
|
||||||
if (splitPane == null || positions == null || positions.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final double[] values = positions.stream().mapToDouble(Double::doubleValue).toArray();
|
|
||||||
splitPane.setDividerPositions(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProjectLocalStudioState.DockPanelState dockPanelState(final p.studio.controls.WorkspaceDockPane dockPane) {
|
|
||||||
final p.studio.controls.WorkspaceDockPane.DockState state = dockPane.captureDockState();
|
|
||||||
return new ProjectLocalStudioState.DockPanelState(state.expanded(), state.dividerPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void restoreDockPanelState(
|
|
||||||
final p.studio.controls.WorkspaceDockPane dockPane,
|
|
||||||
final ProjectLocalStudioState.DockPanelState dockPanelState) {
|
|
||||||
if (dockPanelState == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final double dividerPosition = dockPanelState.dividerPosition() == null
|
|
||||||
? dockPane.captureDockState().dividerPosition()
|
|
||||||
: dockPanelState.dividerPosition();
|
|
||||||
dockPane.restoreDockState(new p.studio.controls.WorkspaceDockPane.DockState(
|
|
||||||
dockPanelState.expanded(),
|
|
||||||
dividerPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void observeStatefulSplitPane(final SplitPane splitPane) {
|
|
||||||
splitPane.getDividers().forEach(divider ->
|
|
||||||
divider.positionProperty().addListener((ignored, previous, current) -> {
|
|
||||||
if (applyingPendingLayoutState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notifyStateChanged();
|
|
||||||
}));
|
|
||||||
splitPane.getDividers().addListener((javafx.collections.ListChangeListener<? super SplitPane.Divider>) change -> {
|
|
||||||
while (change.next()) {
|
|
||||||
if (change.wasAdded()) {
|
|
||||||
change.getAddedSubList().forEach(divider ->
|
|
||||||
divider.positionProperty().addListener((ignored, previous, current) -> {
|
|
||||||
if (applyingPendingLayoutState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notifyStateChanged();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void observeDockPane(final p.studio.controls.WorkspaceDockPane dockPane) {
|
|
||||||
dockPane.expandedProperty().addListener((ignored, previous, current) -> {
|
|
||||||
if (applyingPendingLayoutState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notifyStateChanged();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyStateChanged() {
|
|
||||||
pendingEditorLayoutState = captureLayoutState();
|
|
||||||
stateChangedAction.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void schedulePendingLayoutReapply() {
|
|
||||||
if (contentSplit == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (contentSplit.getWidth() > 0) {
|
|
||||||
Platform.runLater(this::reapplyPendingLayoutState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final javafx.beans.value.ChangeListener<Number>[] listenerRef = new javafx.beans.value.ChangeListener[1];
|
|
||||||
listenerRef[0] = (ignored, previous, current) -> {
|
|
||||||
if (current == null || current.doubleValue() <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
contentSplit.widthProperty().removeListener(listenerRef[0]);
|
|
||||||
Platform.runLater(this::reapplyPendingLayoutState);
|
|
||||||
};
|
|
||||||
contentSplit.widthProperty().addListener(listenerRef[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyEditorLayoutState(final ProjectLocalStudioState.EditorLayoutState editorLayoutState) {
|
|
||||||
applyingPendingLayoutState = true;
|
|
||||||
applyDividerPositions(contentSplit, editorLayoutState.contentSplitDividers());
|
|
||||||
applyDividerPositions(leftColumnSplit, editorLayoutState.leftColumnDividers());
|
|
||||||
applyDividerPositions(rightColumnSplit, editorLayoutState.rightColumnDividers());
|
|
||||||
restoreDockPanelState(outlinePanel, editorLayoutState.outlinePanel());
|
|
||||||
restoreDockPanelState(helperPanel, editorLayoutState.helperPanel());
|
|
||||||
navigatorPanel.restoreExpandedPaths(editorLayoutState.navigatorExpandedPaths());
|
|
||||||
Platform.runLater(() -> applyingPendingLayoutState = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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("<path[^>]*\\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 <path d=\"...\"> in icon resource: " + resourcePath);
|
|
||||||
}
|
|
||||||
return matcher.group(1);
|
|
||||||
} catch (IOException exception) {
|
|
||||||
throw new IllegalStateException("Unable to read icon resource: " + resourcePath, exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<String> 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<Collection<String>> highlight(
|
|
||||||
final String content,
|
|
||||||
final List<LspHighlightSpanDTO> semanticHighlights) {
|
|
||||||
final StyleSpansBuilder<Collection<String>> builder = new StyleSpansBuilder<>();
|
|
||||||
final List<LspHighlightSpanDTO> 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<Collection<String>> overlay(
|
|
||||||
final StyleSpans<Collection<String>> baseHighlighting,
|
|
||||||
final StyleSpans<Collection<String>> semanticHighlighting) {
|
|
||||||
return baseHighlighting.overlay(semanticHighlighting, EditorDocumentSemanticHighlighting::mergeStyles);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Collection<String> mergeStyles(
|
|
||||||
final Collection<String> baseStyles,
|
|
||||||
final Collection<String> semanticStyles) {
|
|
||||||
if (baseStyles.isEmpty()) {
|
|
||||||
return semanticStyles;
|
|
||||||
}
|
|
||||||
if (semanticStyles.isEmpty()) {
|
|
||||||
return baseStyles;
|
|
||||||
}
|
|
||||||
final LinkedHashSet<String> merged = new LinkedHashSet<>(baseStyles);
|
|
||||||
merged.addAll(semanticStyles);
|
|
||||||
return List.copyOf(merged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<EditorDocumentHighlightToken> 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<Collection<String>> highlight(final String content) {
|
|
||||||
final StyleSpansBuilder<Collection<String>> 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<String> 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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(
|
|
||||||
"(?<SHEBANG>^#![^\\n]*)"
|
|
||||||
+ "|(?<COMMENT>(?m)(?<!\\S)#[^\\n]*)"
|
|
||||||
+ "|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')"
|
|
||||||
+ "|(?<VARIABLE>\\$\\{?[A-Za-z_][A-Za-z0-9_]*}?|\\$[0-9@*#?!$-])"
|
|
||||||
+ "|(?<TESTBUILTIN>(?m)^[ \\t]*test\\b)"
|
|
||||||
+ "|(?<COMMAND>(?m)^[ \\t]*[A-Za-z_./-][A-Za-z0-9_./-]*)"
|
|
||||||
+ "|(?<KEYWORD>\\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time)\\b)"
|
|
||||||
+ "|(?<BUILTIN>\\b(?:echo|printf|read|cd|pwd|export|local|readonly|unset|return|shift|source|trap|exit)\\b)"
|
|
||||||
+ "|(?<OPERATOR>\\|\\||&&|;;|<<-?|>>|[|&;<>~=(){}\\[\\]])"),
|
|
||||||
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")));
|
|
||||||
}
|
|
||||||
@ -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(
|
|
||||||
"(?<KEY>\"(?:\\\\.|[^\"\\\\])*\"(?=\\s*:))"
|
|
||||||
+ "|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\")"
|
|
||||||
+ "|(?<NUMBER>-?\\b\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)"
|
|
||||||
+ "|(?<BOOLEAN>\\b(?:true|false)\\b)"
|
|
||||||
+ "|(?<NULL>\\bnull\\b)"
|
|
||||||
+ "|(?<PUNCTUATION>[\\{\\}\\[\\],:])"),
|
|
||||||
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")));
|
|
||||||
}
|
|
||||||
@ -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());
|
|
||||||
}
|
|
||||||
@ -1,25 +1,11 @@
|
|||||||
package p.studio.projectsessions;
|
package p.studio.projectsessions;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import p.studio.lsp.messages.LspProjectContext;
|
|
||||||
import p.studio.lsp.LspService;
|
|
||||||
import p.studio.lsp.LspServiceFactory;
|
|
||||||
import p.studio.projectstate.ProjectLocalStudioSetup;
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
import p.studio.projectstate.ProjectLocalStudioSetupService;
|
import p.studio.projectstate.ProjectLocalStudioSetupService;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projectstate.ProjectLocalStudioStateService;
|
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.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;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
@ -28,14 +14,10 @@ import static org.junit.jupiter.api.Assertions.assertSame;
|
|||||||
|
|
||||||
final class StudioProjectSessionFactoryTest {
|
final class StudioProjectSessionFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
void openCreatesProjectSessionBackedByProjectScopedVfs() {
|
void openCreatesProjectSessionWithoutLegacyEditorServices() {
|
||||||
final RecordingLspFactory lspFactory = new RecordingLspFactory();
|
|
||||||
final RecordingVfsFactory vfsFactory = new RecordingVfsFactory();
|
|
||||||
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
||||||
final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService();
|
final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService();
|
||||||
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(
|
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(
|
||||||
lspFactory,
|
|
||||||
vfsFactory,
|
|
||||||
stateService,
|
stateService,
|
||||||
setupService);
|
setupService);
|
||||||
final ProjectReference projectReference = new ProjectReference(
|
final ProjectReference projectReference = new ProjectReference(
|
||||||
@ -48,14 +30,6 @@ final class StudioProjectSessionFactoryTest {
|
|||||||
final StudioProjectSession session = sessionFactory.open(projectReference);
|
final StudioProjectSession session = sessionFactory.open(projectReference);
|
||||||
|
|
||||||
assertSame(projectReference, session.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(stateService.loadedState, session.projectLocalStudioState());
|
||||||
assertSame(setupService.loadedSetup, session.projectLocalStudioSetup());
|
assertSame(setupService.loadedSetup, session.projectLocalStudioSetup());
|
||||||
assertEquals("IDLE", session.executionSession().snapshot().state().name());
|
assertEquals("IDLE", session.executionSession().snapshot().state().name());
|
||||||
@ -68,8 +42,7 @@ final class StudioProjectSessionFactoryTest {
|
|||||||
private final ProjectLocalStudioState loadedState = new ProjectLocalStudioState(
|
private final ProjectLocalStudioState loadedState = new ProjectLocalStudioState(
|
||||||
ProjectLocalStudioState.CURRENT_VERSION,
|
ProjectLocalStudioState.CURRENT_VERSION,
|
||||||
ProjectLocalStudioState.ShellLayout.defaults(),
|
ProjectLocalStudioState.ShellLayout.defaults(),
|
||||||
new ProjectLocalStudioState.OpenShellState("EDITOR"),
|
new ProjectLocalStudioState.OpenShellState("ASSETS"));
|
||||||
ProjectLocalStudioState.EditorRestorationState.defaults());
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ProjectLocalStudioState load(final ProjectReference projectReference) {
|
public ProjectLocalStudioState load(final ProjectReference projectReference) {
|
||||||
@ -90,84 +63,4 @@ final class StudioProjectSessionFactoryTest {
|
|||||||
return loadedSetup;
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,22 +2,10 @@ package p.studio.projectsessions;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import p.studio.execution.StudioExecutionSessionService;
|
import p.studio.execution.StudioExecutionSessionService;
|
||||||
import p.studio.lsp.messages.LspProjectContext;
|
|
||||||
import p.studio.lsp.LspService;
|
|
||||||
import p.studio.projectstate.ProjectLocalStudioSetup;
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projectstate.ProjectLocalStudioStateService;
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
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.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;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
@ -26,14 +14,10 @@ import static org.junit.jupiter.api.Assertions.assertSame;
|
|||||||
|
|
||||||
final class StudioProjectSessionTest {
|
final class StudioProjectSessionTest {
|
||||||
@Test
|
@Test
|
||||||
void closeDelegatesToUnderlyingServicesOnlyOnce() {
|
void closeSavesStateOnlyOnce() {
|
||||||
final CountingVfsProjectDocument vfs = new CountingVfsProjectDocument();
|
|
||||||
final CountingPrometeuLspService lsp = new CountingPrometeuLspService(vfs);
|
|
||||||
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
||||||
final StudioProjectSession session = new StudioProjectSession(
|
final StudioProjectSession session = new StudioProjectSession(
|
||||||
projectReference(),
|
projectReference(),
|
||||||
lsp,
|
|
||||||
vfs,
|
|
||||||
stateService,
|
stateService,
|
||||||
ProjectLocalStudioSetup.defaults(),
|
ProjectLocalStudioSetup.defaults(),
|
||||||
new StudioExecutionSessionService(),
|
new StudioExecutionSessionService(),
|
||||||
@ -42,27 +26,21 @@ final class StudioProjectSessionTest {
|
|||||||
session.close();
|
session.close();
|
||||||
session.close();
|
session.close();
|
||||||
|
|
||||||
assertEquals(1, lsp.closeCalls);
|
|
||||||
assertEquals(1, vfs.closeCalls);
|
|
||||||
assertEquals(1, stateService.saveCalls);
|
assertEquals(1, stateService.saveCalls);
|
||||||
assertEquals(projectReference(), stateService.savedProjectReference);
|
assertEquals(projectReference(), stateService.savedProjectReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void replaceProjectLocalStudioStateUpdatesTheSavedSnapshot() {
|
void replaceProjectLocalStudioStateUpdatesTheSavedSnapshot() {
|
||||||
final CountingVfsProjectDocument vfs = new CountingVfsProjectDocument();
|
|
||||||
final CountingPrometeuLspService lsp = new CountingPrometeuLspService(vfs);
|
|
||||||
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
||||||
final StudioProjectSession session = new StudioProjectSession(
|
final StudioProjectSession session = new StudioProjectSession(
|
||||||
projectReference(),
|
projectReference(),
|
||||||
lsp,
|
|
||||||
vfs,
|
|
||||||
stateService,
|
stateService,
|
||||||
ProjectLocalStudioSetup.defaults(),
|
ProjectLocalStudioSetup.defaults(),
|
||||||
new StudioExecutionSessionService(),
|
new StudioExecutionSessionService(),
|
||||||
ProjectLocalStudioState.defaults());
|
ProjectLocalStudioState.defaults());
|
||||||
final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults()
|
final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults()
|
||||||
.withOpenShellState(new ProjectLocalStudioState.OpenShellState("EDITOR"));
|
.withOpenShellState(new ProjectLocalStudioState.OpenShellState("ASSETS"));
|
||||||
|
|
||||||
session.replaceProjectLocalStudioState(nextState);
|
session.replaceProjectLocalStudioState(nextState);
|
||||||
session.close();
|
session.close();
|
||||||
@ -86,77 +64,4 @@ final class StudioProjectSessionTest {
|
|||||||
savedState = state;
|
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,22 +23,8 @@ final class ProjectLocalStudioStateServiceTest {
|
|||||||
final ProjectLocalStudioState state = new ProjectLocalStudioState(
|
final ProjectLocalStudioState state = new ProjectLocalStudioState(
|
||||||
ProjectLocalStudioState.CURRENT_VERSION,
|
ProjectLocalStudioState.CURRENT_VERSION,
|
||||||
new ProjectLocalStudioState.ShellLayout(
|
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.AssetsLayoutState(List.of(0.42d))),
|
||||||
new ProjectLocalStudioState.OpenShellState("EDITOR"),
|
new ProjectLocalStudioState.OpenShellState("ASSETS"));
|
||||||
new ProjectLocalStudioState.EditorRestorationState(
|
|
||||||
List.of(
|
|
||||||
project.rootPath().resolve("src/main.pbs").toString(),
|
|
||||||
project.rootPath().resolve("README.md").toString()),
|
|
||||||
project.rootPath().resolve("src/main.pbs").toString()));
|
|
||||||
|
|
||||||
service.save(project, state);
|
service.save(project, state);
|
||||||
|
|
||||||
@ -72,6 +58,43 @@ final class ProjectLocalStudioStateServiceTest {
|
|||||||
assertEquals(ProjectLocalStudioState.defaults(), service.load(project));
|
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) {
|
private ProjectReference project(final String name) {
|
||||||
final Path projectRoot = tempDir.resolve(name);
|
final Path projectRoot = tempDir.resolve(name);
|
||||||
return new ProjectReference("Main", "1.0.0", "pbs", 1, projectRoot);
|
return new ProjectReference("Main", "1.0.0", "pbs", 1, projectRoot);
|
||||||
|
|||||||
@ -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<Collection<String>> styleSpans,
|
|
||||||
final String styleClass) {
|
|
||||||
for (int index = 0; index < styleSpans.getSpanCount(); index++) {
|
|
||||||
if (styleSpans.getStyleSpan(index).getStyle().contains(styleClass)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<LspSymbolDTO> 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user