editor with write capability
This commit is contained in:
parent
fc96e45435
commit
a0e19fd143
@ -1,4 +1,4 @@
|
|||||||
{"type":"meta","next_id":{"DSC":21,"AGD":22,"DEC":19,"PLN":40,"LSN":34,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":22,"AGD":23,"DEC":20,"PLN":41,"LSN":34,"CLSN":1}}
|
||||||
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
||||||
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
||||||
@ -19,3 +19,4 @@
|
|||||||
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"studio-project-local-studio-state-under-dot-studio","title":"Persist project-local Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","shell","layout","setup"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"discussion/lessons/DSC-0018-studio-project-local-studio-state-under-dot-studio/LSN-0031-project-local-studio-state-and-lifecycle-safe-layout-restoration.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"studio-project-local-studio-state-under-dot-studio","title":"Persist project-local Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","shell","layout","setup"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"discussion/lessons/DSC-0018-studio-project-local-studio-state-under-dot-studio/LSN-0031-project-local-studio-state-and-lifecycle-safe-layout-restoration.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
||||||
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"studio-project-local-setup-separate-from-main-studio-state","title":"Separate project-local setup from the main Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","setup","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"discussion/lessons/DSC-0019-studio-project-local-setup-separate-from-main-studio-state/LSN-0032-separate-project-config-from-session-restoration-state.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"studio-project-local-setup-separate-from-main-studio-state","title":"Separate project-local setup from the main Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","setup","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"discussion/lessons/DSC-0019-studio-project-local-setup-separate-from-main-studio-state/LSN-0032-separate-project-config-from-session-restoration-state.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
||||||
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"studio-editor-indentation-policy-and-project-setup","title":"Indentation Policy, Status-Bar Semantics, and Project-Local Editor Setup","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","editor","indentation","tabs","setup","dot-studio","configuration","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"studio-editor-indentation-policy-and-project-setup","title":"Indentation Policy, Status-Bar Semantics, and Project-Local Editor Setup","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","editor","indentation","tabs","setup","dot-studio","configuration","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0021","status":"open","ticket":"studio-frontend-editor-write-and-save-wave","title":"Enable Frontend Editing and Save in the Studio Code Editor","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","editor","frontend","write","save","vfs","lsp","pbs","access-policy"],"agendas":[{"id":"AGD-0022","file":"AGD-0022-studio-frontend-editor-write-and-save-wave.md","status":"accepted","created_at":"2026-04-04","updated_at":"2026-04-04"}],"decisions":[{"id":"DEC-0019","file":"DEC-0019-studio-frontend-editor-write-and-save-wave.md","status":"accepted","created_at":"2026-04-04","updated_at":"2026-04-04","ref_agenda":"AGD-0022"}],"plans":[{"id":"PLN-0040","file":"PLN-0040-studio-frontend-editor-write-and-save-wave.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04","ref_decisions":["DEC-0019"]}],"lessons":[]}
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
---
|
||||||
|
id: AGD-0022
|
||||||
|
discussion: DSC-0021
|
||||||
|
ticket: studio-frontend-editor-write-and-save-wave
|
||||||
|
title: Enable Frontend Editing and Save in the Studio Code Editor
|
||||||
|
status: accepted
|
||||||
|
created: 2026-04-04
|
||||||
|
updated: 2026-04-04
|
||||||
|
owner: studio
|
||||||
|
tags: [studio, editor, frontend, write, save, vfs, lsp, pbs, access-policy]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The Studio Code Editor already delivers frontend semantic-read value through `prometeu-lsp`, but frontend documents are still hard `read-only`.
|
||||||
|
|
||||||
|
That leaves the FE editing wave unfinished:
|
||||||
|
|
||||||
|
- frontend files can be opened, analyzed, highlighted, and navigated;
|
||||||
|
- but they cannot be edited or saved;
|
||||||
|
- and the current contract explicitly says that FE editing requires a separate decision.
|
||||||
|
|
||||||
|
The next step is to define how FE mutation and save rights should be released without breaking the VFS/LSP/editor ownership split that the repository has already stabilized.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Domain owner: `studio`.
|
||||||
|
|
||||||
|
The current durable constraints are already clear:
|
||||||
|
|
||||||
|
1. `prometeu-vfs` owns document classification, access mode, snapshots, and save behavior;
|
||||||
|
2. `prometeu-lsp` is a semantic consumer of VFS-backed snapshots and must not absorb save or access-policy ownership;
|
||||||
|
3. Studio UI is a consumer of both boundaries and must not re-derive document rights locally;
|
||||||
|
4. frontend semantic-read was intentionally released before frontend edit rights;
|
||||||
|
5. FE edit rights now require a new explicit discussion and decision.
|
||||||
|
|
||||||
|
This means the new wave cannot be framed as “let the editor mutate text and figure out persistence later”.
|
||||||
|
If FE editing is released, it must be released through the same document-runtime owner that already controls access and save semantics.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. Should FE editing be released only together with FE save in the same wave?
|
||||||
|
2. Must FE editable snapshots remain owned by `prometeu-vfs`, exactly like non-frontend editable documents?
|
||||||
|
3. How should `prometeu-lsp` consume dirty FE snapshots after mutation: on-demand analyze only, background refresh, or both?
|
||||||
|
4. Are FE edit rights universal for all frontend-scoped supported files, or do they need an additional narrowing rule in this first FE write wave?
|
||||||
|
5. What explicit safeguards are needed so FE editing does not accidentally turn `prometeu-lsp` into the document owner?
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### Option 1: Allow FE editing in the editor before FE save exists
|
||||||
|
|
||||||
|
The editor would accept FE text mutation locally, and LSP could consume that dirty text, but FE save would remain unavailable or deferred.
|
||||||
|
|
||||||
|
Tradeoffs:
|
||||||
|
|
||||||
|
- appears to unlock FE editing quickly;
|
||||||
|
- creates a half-owned document state with unclear persistence semantics;
|
||||||
|
- weakens the current VFS ownership model;
|
||||||
|
- encourages exactly the “editor mutates first, persistence later” drift the repository has avoided so far.
|
||||||
|
|
||||||
|
This option is weak and should be rejected.
|
||||||
|
|
||||||
|
### Option 2: Release FE editing and FE save together through `prometeu-vfs`
|
||||||
|
|
||||||
|
FE documents become editable only when `prometeu-vfs` can own their writable snapshots, dirty tracking, and save behavior.
|
||||||
|
The editor consumes that access mode, and `prometeu-lsp` continues to analyze VFS-backed FE snapshots as a consumer.
|
||||||
|
|
||||||
|
Tradeoffs:
|
||||||
|
|
||||||
|
- preserves the current architecture cleanly;
|
||||||
|
- avoids split-brain ownership between editor, VFS, and LSP;
|
||||||
|
- makes FE write behavior coherent on day one;
|
||||||
|
- requires a broader implementation wave than a UI-only unlock.
|
||||||
|
|
||||||
|
This is the strongest option.
|
||||||
|
|
||||||
|
### Option 3: Let `prometeu-lsp` become the temporary owner of editable FE session text
|
||||||
|
|
||||||
|
LSP would hold the live FE text state because it already consumes FE semantics, and save integration would come later.
|
||||||
|
|
||||||
|
Tradeoffs:
|
||||||
|
|
||||||
|
- seems convenient because FE semantics already live there;
|
||||||
|
- collapses semantic ownership into document ownership;
|
||||||
|
- directly conflicts with the established VFS boundary;
|
||||||
|
- would make the previous editor/VFS/LSP split editorially and architecturally inconsistent.
|
||||||
|
|
||||||
|
This option should be rejected.
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Adopt Option 2.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
1. FE editing and FE save should be released in the same wave.
|
||||||
|
2. `prometeu-vfs` should remain the sole owner of FE writable snapshots, access mode, dirty tracking, and persistence.
|
||||||
|
3. `prometeu-lsp` should continue as a semantic consumer of VFS-backed FE snapshots, including dirty in-memory FE state.
|
||||||
|
4. Studio UI should remain a policy consumer and must not introduce FE-local save or access heuristics outside the VFS contract.
|
||||||
|
5. The first FE write wave should explicitly define scope, save semantics, user-visible access changes, and any resulting LSP refresh model.
|
||||||
|
|
||||||
|
## Next Step
|
||||||
|
|
||||||
|
If this agenda is accepted, convert it into a decision that normatively locks:
|
||||||
|
|
||||||
|
- whether FE edit and FE save release together;
|
||||||
|
- VFS ownership of FE writable snapshots and save behavior;
|
||||||
|
- LSP consumption rules for dirty FE snapshots;
|
||||||
|
- and the exact scope of the first FE write wave.
|
||||||
|
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
Accepted on 2026-04-04.
|
||||||
|
|
||||||
|
The discussion is closed with the following resolution:
|
||||||
|
|
||||||
|
1. FE editing and FE save release together in the same wave.
|
||||||
|
2. `prometeu-vfs` remains the sole owner of FE writable snapshots, access mode, dirty tracking, and persistence.
|
||||||
|
3. `prometeu-lsp` continues as a semantic consumer of VFS-backed FE snapshots, including dirty in-memory FE state, but no new LSP implementation work is part of the immediate execution wave.
|
||||||
|
4. The first FE write wave applies to all frontend-scoped supported files rather than introducing an additional narrowing rule.
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
id: DEC-0019
|
||||||
|
discussion: DSC-0021
|
||||||
|
agenda: AGD-0022
|
||||||
|
ticket: studio-frontend-editor-write-and-save-wave
|
||||||
|
title: Enable Frontend Editing and Save in the Studio Code Editor
|
||||||
|
status: accepted
|
||||||
|
created: 2026-04-04
|
||||||
|
updated: 2026-04-04
|
||||||
|
owner: studio
|
||||||
|
tags: [studio, editor, frontend, write, save, vfs, lsp, pbs, access-policy]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Studio already provides frontend semantic-read value through `prometeu-lsp`, while frontend documents remain hard `read-only`.
|
||||||
|
The repository also already locked the architectural split between document ownership and semantic ownership:
|
||||||
|
|
||||||
|
1. `prometeu-vfs` owns document classification, access mode, snapshots, and save behavior;
|
||||||
|
2. `prometeu-lsp` consumes VFS-backed snapshots for semantic analysis;
|
||||||
|
3. Studio UI consumes both boundaries and must not re-derive document rights locally.
|
||||||
|
|
||||||
|
The previous semantic-read wave explicitly stated that frontend editing requires a separate decision.
|
||||||
|
This decision closes that gap.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Studio SHALL release frontend editing and frontend save together in one explicit write wave.
|
||||||
|
|
||||||
|
The wave is normatively defined as follows:
|
||||||
|
|
||||||
|
1. Frontend editing MUST NOT be released without frontend save in the same wave.
|
||||||
|
2. `prometeu-vfs` MUST remain the sole owner of frontend writable snapshots.
|
||||||
|
3. `prometeu-vfs` MUST remain the sole owner of frontend access mode.
|
||||||
|
4. `prometeu-vfs` MUST remain the sole owner of frontend dirty tracking.
|
||||||
|
5. `prometeu-vfs` MUST remain the sole owner of frontend save and save-all behavior.
|
||||||
|
6. Studio UI MUST consume frontend editability and save rights from `prometeu-vfs` rather than introducing editor-local policy.
|
||||||
|
7. `prometeu-lsp` MUST continue as a semantic consumer of VFS-backed frontend snapshots, including dirty in-memory frontend state.
|
||||||
|
8. This decision does NOT authorize a new LSP implementation wave as part of the immediate execution scope.
|
||||||
|
9. The first frontend write wave MUST apply to all frontend-scoped supported files and MUST NOT introduce an additional narrowing rule in this wave.
|
||||||
|
10. Releasing frontend editing MUST NOT collapse semantic ownership, document ownership, and persistence ownership into one module.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
Frontend mutation without frontend save would create a half-owned editorial state and weaken the document boundary that the repository has already stabilized.
|
||||||
|
The correct release shape is therefore coherent FE mutation plus coherent FE persistence under the same owner.
|
||||||
|
|
||||||
|
Keeping `prometeu-vfs` as the sole owner preserves the existing architecture:
|
||||||
|
|
||||||
|
- VFS owns documents and persistence;
|
||||||
|
- LSP consumes document state for semantics;
|
||||||
|
- UI renders and interacts with policy.
|
||||||
|
|
||||||
|
This decision also avoids using a fake transitional step where the editor mutates frontend text locally while persistence remains undefined.
|
||||||
|
|
||||||
|
## Technical Specification
|
||||||
|
|
||||||
|
### Frontend Document Ownership
|
||||||
|
|
||||||
|
Frontend-scoped supported files MUST move from hard `read-only` to editable only through `prometeu-vfs`.
|
||||||
|
|
||||||
|
That means:
|
||||||
|
|
||||||
|
1. writable FE in-memory snapshots MUST be created and owned by VFS;
|
||||||
|
2. FE dirty tracking MUST be owned by VFS;
|
||||||
|
3. FE save and save-all MUST route through VFS;
|
||||||
|
4. editor-visible FE access mode MUST be surfaced from VFS.
|
||||||
|
|
||||||
|
### Editor Behavior
|
||||||
|
|
||||||
|
The Studio editor MUST consume the new FE access policy from VFS exactly as it already does for non-frontend editable documents.
|
||||||
|
|
||||||
|
The editor MUST NOT:
|
||||||
|
|
||||||
|
1. create a separate FE-only mutation model;
|
||||||
|
2. implement FE-local persistence outside VFS;
|
||||||
|
3. infer FE save rights through UI heuristics.
|
||||||
|
|
||||||
|
### LSP Consumption Boundary
|
||||||
|
|
||||||
|
`prometeu-lsp` remains authorized to consume dirty FE snapshots from VFS.
|
||||||
|
However, this decision does not authorize a new LSP feature wave as part of immediate execution.
|
||||||
|
|
||||||
|
Immediate implementation may rely on the existing semantic-read seam, provided that:
|
||||||
|
|
||||||
|
1. no new persistence ownership moves into LSP;
|
||||||
|
2. no new access-policy ownership moves into LSP;
|
||||||
|
3. FE write enablement does not depend on introducing a new LSP-owned document model.
|
||||||
|
|
||||||
|
### Scope of First FE Write Wave
|
||||||
|
|
||||||
|
The first FE write wave applies to all frontend-scoped supported files currently recognized by the VFS/frontend boundary.
|
||||||
|
|
||||||
|
This decision does not authorize:
|
||||||
|
|
||||||
|
1. a temporary subset of FE file classes;
|
||||||
|
2. an editor-only FE draft mode;
|
||||||
|
3. FE edit rights without FE save rights.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
1. This decision does not authorize a new LSP implementation scope by itself.
|
||||||
|
2. This decision does not authorize moving document state ownership into Studio UI.
|
||||||
|
3. This decision does not authorize moving document state ownership into LSP.
|
||||||
|
4. This decision does not authorize releasing FE editing through a separate temporary persistence path.
|
||||||
|
|
||||||
|
## Propagation Targets
|
||||||
|
|
||||||
|
- Specs:
|
||||||
|
- `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`
|
||||||
|
- Plans:
|
||||||
|
- FE VFS write/access/save propagation
|
||||||
|
- Studio FE editor access-mode and save UX propagation
|
||||||
|
- Code:
|
||||||
|
- `prometeu-vfs` FE writable snapshot and save policy
|
||||||
|
- `prometeu-studio` FE editor write UX and access-state propagation
|
||||||
|
- Tests:
|
||||||
|
- FE VFS access-mode and save coverage
|
||||||
|
- FE editor editable/saveable behavior
|
||||||
|
|
||||||
|
## Revision Log
|
||||||
|
|
||||||
|
- 2026-04-04: Accepted from `AGD-0022` to release FE editing and save together under VFS ownership, with no new immediate LSP implementation scope.
|
||||||
@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0040
|
||||||
|
discussion: DSC-0021
|
||||||
|
decision: DEC-0019
|
||||||
|
ticket: studio-frontend-editor-write-and-save-wave
|
||||||
|
title: Implement the frontend editor write and save wave through VFS ownership
|
||||||
|
status: done
|
||||||
|
created: 2026-04-04
|
||||||
|
updated: 2026-04-04
|
||||||
|
owner: studio
|
||||||
|
tags: [studio, editor, frontend, write, save, vfs, lsp, pbs, access-policy]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Implement the accepted frontend write wave for the Studio Code Editor.
|
||||||
|
Frontend editing and frontend save must be released together, must remain owned by `prometeu-vfs`, and must be consumed by Studio UI without introducing a new immediate LSP implementation wave.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Move frontend-scoped supported files from hard `read-only` to VFS-owned editable/saveable documents while preserving the current boundary:
|
||||||
|
|
||||||
|
- `prometeu-vfs` owns document state and persistence;
|
||||||
|
- `prometeu-lsp` remains a semantic consumer of VFS-backed snapshots;
|
||||||
|
- Studio UI remains a policy consumer.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `DEC-0019` Enable Frontend Editing and Save in the Studio Code Editor
|
||||||
|
- existing `prometeu-vfs` editable snapshot and save model for non-frontend textual documents
|
||||||
|
- existing Studio editor consumption of VFS access mode and save behavior
|
||||||
|
- existing semantic-read seam where `prometeu-lsp` reads VFS-backed frontend snapshots
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- propagate the new FE write/save contract into Studio and VFS specs
|
||||||
|
- extend `prometeu-vfs` so frontend-scoped supported files become editable/saveable
|
||||||
|
- keep FE writable snapshots, dirty tracking, and save ownership inside VFS
|
||||||
|
- update Studio editor behavior to consume FE editability from VFS
|
||||||
|
- update FE write/save tests in VFS and Studio
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- any new LSP feature implementation wave
|
||||||
|
- per-file FE access policy
|
||||||
|
- partial FE draft mode without save
|
||||||
|
- moving persistence logic into Studio UI
|
||||||
|
- moving persistence logic into `prometeu-lsp`
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
### 1. Propagate the accepted contract into specs
|
||||||
|
|
||||||
|
Update the Studio specs that still describe FE files as hard `read-only` so they now describe the new FE write/save wave and preserve VFS ownership plus semantic-consumer-only LSP ownership.
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
|
||||||
|
- `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`
|
||||||
|
|
||||||
|
### 2. Change FE access policy in `prometeu-vfs`
|
||||||
|
|
||||||
|
Update the filesystem-backed VFS classification so frontend-scoped supported files become `EDITABLE` rather than `READ_ONLY`, while preserving frontend classification itself.
|
||||||
|
|
||||||
|
What changes:
|
||||||
|
|
||||||
|
- document kind classification for FE files
|
||||||
|
- VFS acceptance of FE `updateDocument`
|
||||||
|
- VFS save/save-all behavior for FE snapshots
|
||||||
|
- FE dirty tracking semantics under the same VFS owner model already used for editable non-frontend documents
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
|
||||||
|
- `prometeu-vfs/prometeu-vfs-v1/src/main/java/p/studio/vfs/FilesystemVfsProjectDocument.java`
|
||||||
|
- any supporting VFS API contracts if required by the implementation
|
||||||
|
|
||||||
|
Dependency ordering:
|
||||||
|
|
||||||
|
- this step must land before Studio editor UX changes so the UI can consume the new policy rather than invent it.
|
||||||
|
|
||||||
|
### 3. Update VFS tests for FE edit/save behavior
|
||||||
|
|
||||||
|
Replace tests that assert FE hard `read-only` behavior with tests that assert:
|
||||||
|
|
||||||
|
1. FE documents open as frontend documents with `EDITABLE` access mode;
|
||||||
|
2. FE documents accept `updateDocument`;
|
||||||
|
3. FE documents save correctly through `saveDocument`;
|
||||||
|
4. FE documents participate correctly in `saveAllDocuments`.
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
|
||||||
|
- `prometeu-vfs/prometeu-vfs-v1/src/test/java/p/studio/vfs/FilesystemVfsProjectDocumentTest.java`
|
||||||
|
|
||||||
|
### 4. Propagate FE editability into the Studio editor
|
||||||
|
|
||||||
|
Update the Studio editor to consume the new FE access mode from VFS exactly as it already does for editable non-frontend documents.
|
||||||
|
|
||||||
|
What changes:
|
||||||
|
|
||||||
|
- FE tabs become editable when VFS reports `EDITABLE`
|
||||||
|
- FE warning surfaces that are specific to hard `read-only` FE behavior must be revised or removed
|
||||||
|
- editor save/save-all enablement must work for dirty FE buffers through the existing VFS-backed flow
|
||||||
|
- no FE-local persistence path may be introduced
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java`
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java`
|
||||||
|
- any FE-warning or editor-session support classes affected by access-mode transition
|
||||||
|
|
||||||
|
Dependency ordering:
|
||||||
|
|
||||||
|
- this step depends on VFS access-mode propagation from Step 2.
|
||||||
|
|
||||||
|
### 5. Keep LSP integration stable without opening new implementation scope
|
||||||
|
|
||||||
|
Verify that the existing editor and semantic-read flow still calls `prometeu-lsp` as a consumer of FE snapshots, but do not introduce a new LSP implementation wave.
|
||||||
|
|
||||||
|
What changes:
|
||||||
|
|
||||||
|
- only compatibility adjustments that are strictly necessary because FE snapshots are now editable may be made;
|
||||||
|
- no new semantic features, refresh model, or ownership expansion is authorized here.
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
|
||||||
|
- only the minimum compatibility surface if compilation or existing tests require it
|
||||||
|
|
||||||
|
Dependency ordering:
|
||||||
|
|
||||||
|
- this is verification-oriented and should follow the VFS/editor write changes.
|
||||||
|
|
||||||
|
### 6. Update Studio tests for FE editable/saveable behavior
|
||||||
|
|
||||||
|
Add or revise editor tests so they assert FE files are no longer treated as permanently hard `read-only` once VFS grants FE `EDITABLE` access mode.
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
|
||||||
|
- Studio editor tests under `prometeu-studio/src/test/java/p/studio/workspaces/editor/...`
|
||||||
|
- any session/editor tests affected by FE access-mode transition
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. Frontend-scoped supported files open through VFS with `EDITABLE` access mode.
|
||||||
|
2. Frontend-scoped supported files can be updated and saved through VFS-owned snapshots.
|
||||||
|
3. Studio editor FE tabs become editable only because VFS reports them as editable.
|
||||||
|
4. FE save and save-all route through the same VFS owner path used by other editable documents.
|
||||||
|
5. No new persistence or access-policy ownership moves into Studio UI.
|
||||||
|
6. No new persistence or access-policy ownership moves into `prometeu-lsp`.
|
||||||
|
7. Specs no longer describe FE files as hard `read-only` for the current wave.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- VFS tests for FE access mode, update, save, and save-all
|
||||||
|
- Studio editor tests for FE editable/saveable behavior
|
||||||
|
- regression tests that confirm non-frontend editable documents still behave correctly
|
||||||
|
- compile/test verification that existing LSP consumer seams still pass without opening new LSP feature scope
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `discussion/workflow/decisions/DEC-0019-studio-frontend-editor-write-and-save-wave.md`
|
||||||
|
- `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`
|
||||||
|
- `prometeu-vfs/prometeu-vfs-v1/src/main/java/p/studio/vfs/FilesystemVfsProjectDocument.java`
|
||||||
|
- `prometeu-vfs/prometeu-vfs-v1/src/test/java/p/studio/vfs/FilesystemVfsProjectDocumentTest.java`
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/workspaces/editor/...`
|
||||||
|
- Studio editor tests
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- existing specs and tests currently encode the old hard `read-only` FE contract and will fail until all propagation lands coherently
|
||||||
|
- FE warning/UI surfaces may assume old read-only semantics in more than one place
|
||||||
|
- LSP snapshot consumption could reveal hidden assumptions about FE immutability even without opening a new LSP scope
|
||||||
|
- if implementation order is reversed and Studio is changed before VFS policy, UI code may drift into local heuristics
|
||||||
@ -8,7 +8,7 @@ Active
|
|||||||
|
|
||||||
- `prometeu-studio`
|
- `prometeu-studio`
|
||||||
- the Studio `Code Editor` workspace
|
- the Studio `Code Editor` workspace
|
||||||
- the first controlled editor write wave for supported non-frontend documents
|
- the first controlled editor write wave for supported documents including frontend sources
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ This specification stabilizes:
|
|||||||
|
|
||||||
- the baseline visual composition of the workspace,
|
- the baseline visual composition of the workspace,
|
||||||
- the `Project Navigator` role and scope,
|
- the `Project Navigator` role and scope,
|
||||||
- the mixed editable and hard `read-only` file-opening model,
|
- the controlled editable file-opening model,
|
||||||
- the responsive tab baseline,
|
- the responsive tab baseline,
|
||||||
- the editor-owned composition surfaces that host save and semantic-read UX,
|
- the editor-owned composition surfaces that host save and semantic-read UX,
|
||||||
- the gutter-based active-structure indicator model for semantic editor scopes,
|
- the gutter-based active-structure indicator model for semantic editor scopes,
|
||||||
@ -42,8 +42,7 @@ If this document conflicts with the global Studio shell specifications, the shel
|
|||||||
The `Code Editor` workspace must assume:
|
The `Code Editor` workspace must assume:
|
||||||
|
|
||||||
- the Studio shell already mounts `Code Editor` as a baseline workspace,
|
- the Studio shell already mounts `Code Editor` as a baseline workspace,
|
||||||
- the current wave allows editing only for the supported non-frontend document classes classified by `prometeu-vfs` as editable,
|
- the current wave allows editing for the supported document classes classified by `prometeu-vfs` as editable, including frontend-scoped supported documents,
|
||||||
- frontend-scoped supported documents remain hard `read-only`,
|
|
||||||
- all project files remain visible in the editor workspace even when only some are frontend-relevant,
|
- all project files remain visible in the editor workspace even when only some are frontend-relevant,
|
||||||
- `prometeu.json` plus the selected frontend may identify source roots worth tagging,
|
- `prometeu.json` plus the selected frontend may identify source roots worth tagging,
|
||||||
- the integrated frontend semantic-read phase may add diagnostics, symbols, outline-facing structure, definition, and highlight for frontend documents,
|
- the integrated frontend semantic-read phase may add diagnostics, symbols, outline-facing structure, definition, and highlight for frontend documents,
|
||||||
@ -61,7 +60,7 @@ The `Code Editor` workspace is:
|
|||||||
|
|
||||||
- project-aware,
|
- project-aware,
|
||||||
- file-oriented,
|
- file-oriented,
|
||||||
- mixed-mode in this wave, with editable supported non-frontend documents and hard `read-only` frontend documents,
|
- controlled-editable in this wave, with supported documents following the canonical access mode provided by `prometeu-vfs`,
|
||||||
- and not a full semantic IDE surface yet.
|
- and not a full semantic IDE surface yet.
|
||||||
|
|
||||||
The workspace must help the user:
|
The workspace must help the user:
|
||||||
@ -69,15 +68,14 @@ The workspace must help the user:
|
|||||||
- see the full project tree,
|
- see the full project tree,
|
||||||
- identify frontend-relevant source roots visually,
|
- identify frontend-relevant source roots visually,
|
||||||
- open supported files into editor tabs,
|
- open supported files into editor tabs,
|
||||||
- save editable non-frontend documents through editor-local commands,
|
- save editable supported documents through editor-local commands,
|
||||||
- consume frontend semantic-read UX provided through the integrated LSP phase when that phase is active,
|
- consume frontend semantic-read UX provided through the integrated LSP phase when that phase is active,
|
||||||
- understand the active file context,
|
- understand the active file context,
|
||||||
- and understand that frontend editing and broader IDE automation remain outside this wave.
|
- and understand that broader IDE automation remains outside this wave.
|
||||||
|
|
||||||
The workspace must not pretend to offer:
|
The workspace must not pretend to offer:
|
||||||
|
|
||||||
- merge behavior,
|
- merge behavior,
|
||||||
- frontend editing,
|
|
||||||
- completion,
|
- completion,
|
||||||
- rename, code actions, or formatting,
|
- rename, code actions, or formatting,
|
||||||
- or editor-owned semantic inference that bypasses the integrated LSP phase.
|
- or editor-owned semantic inference that bypasses the integrated LSP phase.
|
||||||
@ -163,7 +161,7 @@ Rules:
|
|||||||
- Selecting a supported file that is not already open must open it in a new tab.
|
- Selecting a supported file that is not already open must open it in a new tab.
|
||||||
- File opening must resolve document content through `prometeu-vfs`.
|
- File opening must resolve document content through `prometeu-vfs`.
|
||||||
- The editor must maintain opened-file content in memory for the active Studio session only.
|
- The editor must maintain opened-file content in memory for the active Studio session only.
|
||||||
- Frontend-scoped supported documents may coexist in tabs with editable non-frontend documents.
|
- Frontend-scoped supported documents may coexist in tabs with other editable supported documents.
|
||||||
- The tab strip must be responsive rather than fixed to one hardcoded tab count.
|
- The tab strip must be responsive rather than fixed to one hardcoded tab count.
|
||||||
- Overflow tabs must remain accessible through an IntelliJ-style overflow control.
|
- Overflow tabs must remain accessible through an IntelliJ-style overflow control.
|
||||||
- The active tab must remain visible.
|
- The active tab must remain visible.
|
||||||
@ -193,13 +191,11 @@ Rules:
|
|||||||
|
|
||||||
- the workspace must treat `prometeu-vfs` as the canonical source of document access mode for supported files;
|
- the workspace must treat `prometeu-vfs` as the canonical source of document access mode for supported files;
|
||||||
- the workspace must not infer frontend scope from path heuristics, local UI state, or ad hoc extension checks;
|
- the workspace must not infer frontend scope from path heuristics, local UI state, or ad hoc extension checks;
|
||||||
- supported frontend-scoped documents must remain hard `read-only`;
|
- supported frontend-scoped documents may be editable when `prometeu-vfs` classifies them as editable;
|
||||||
- editable scope is limited to the supported non-frontend textual classes exposed by `prometeu-vfs` for this wave;
|
- editable scope is limited to the supported textual classes exposed by `prometeu-vfs` for this wave, including supported frontend sources;
|
||||||
- the workspace must not allow local editorial mutation for hard `read-only` frontend documents;
|
|
||||||
- the workspace must expose save behavior only through an editor-local command bar containing at least `Save` and `Save All`;
|
- the workspace must expose save behavior only through an editor-local command bar containing at least `Save` and `Save All`;
|
||||||
- the global shell `Save` menu item must not be the save surface for this wave;
|
- the global shell `Save` menu item must not be the save surface for this wave;
|
||||||
- save intent must route through `prometeu-vfs`, which remains the owner of persistence policy;
|
- save intent must route through `prometeu-vfs`, which remains the owner of persistence policy;
|
||||||
- a frontend hard `read-only` tab must show a top warning that the file cannot be edited or saved in this wave;
|
|
||||||
- the active indentation policy for editable documents MUST come from project-local setup rather than from file-content heuristics;
|
- the active indentation policy for editable documents MUST come from project-local setup rather than from file-content heuristics;
|
||||||
- the status-bar indentation chip MUST display the active configured policy, not inferred file contents;
|
- the status-bar indentation chip MUST display the active configured policy, not inferred file contents;
|
||||||
- pressing `Tab` in an editable document MUST insert spaces according to the active configured indentation width;
|
- pressing `Tab` in an editable document MUST insert spaces according to the active configured indentation width;
|
||||||
@ -224,7 +220,7 @@ Rules:
|
|||||||
|
|
||||||
## Integrated Semantic-Read Boundary
|
## Integrated Semantic-Read Boundary
|
||||||
|
|
||||||
- Frontend documents remain hard `read-only` even when semantic-read capabilities are active.
|
- Frontend documents may be editable when `prometeu-vfs` grants editable access mode.
|
||||||
- Diagnostics, document symbols, workspace symbols, outline-facing structure, definition, and frontend highlight must come through the integrated LSP semantic-read phase rather than from editor-local inference.
|
- Diagnostics, document symbols, workspace symbols, outline-facing structure, definition, and frontend highlight must come through the integrated LSP semantic-read phase rather than from editor-local inference.
|
||||||
- Exact anchor positions for semantic scope indicators must come from frontend-owned structural metadata rather than Studio-local text scanning when that metadata is available.
|
- Exact anchor positions for semantic scope indicators must come from frontend-owned structural metadata rather than Studio-local text scanning when that metadata is available.
|
||||||
- Opened frontend documents must be analyzed from the VFS-owned in-memory snapshot exposed through `prometeu-vfs`.
|
- Opened frontend documents must be analyzed from the VFS-owned in-memory snapshot exposed through `prometeu-vfs`.
|
||||||
@ -234,7 +230,7 @@ Rules:
|
|||||||
- Frontend semantic presentation resources must remain frontend-owned and must not be replaced by a Studio-owned generic fallback theme.
|
- Frontend semantic presentation resources must remain frontend-owned and must not be replaced by a Studio-owned generic fallback theme.
|
||||||
- When semantic presentation descriptor data or usable frontend resources are absent, the workspace must continue without semantic highlight for that frontend document.
|
- When semantic presentation descriptor data or usable frontend resources are absent, the workspace must continue without semantic highlight for that frontend document.
|
||||||
- The workspace must not surface this condition as a product-facing editor error.
|
- The workspace must not surface this condition as a product-facing editor error.
|
||||||
- The workspace must not treat semantic-read over editorial snapshots as authorization for frontend save, mutation, or build participation.
|
- The workspace must not treat semantic-read over editorial snapshots as authorization for build participation or for bypassing `prometeu-vfs` access policy.
|
||||||
|
|
||||||
## Inline Hint Rules
|
## Inline Hint Rules
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ This specification stabilizes:
|
|||||||
- the filesystem-backed first-wave contract,
|
- the filesystem-backed first-wave contract,
|
||||||
- structural tree and document access responsibilities,
|
- structural tree and document access responsibilities,
|
||||||
- canonical frontend scope and access policy ownership,
|
- canonical frontend scope and access policy ownership,
|
||||||
- editorial snapshot and save ownership for editable non-frontend documents,
|
- editorial snapshot and save ownership for editable supported documents including frontend sources,
|
||||||
- the semantic-read consumer boundary used by the integrated LSP phase,
|
- the semantic-read consumer boundary used by the integrated LSP phase,
|
||||||
- the RPC-first public API baseline,
|
- the RPC-first public API baseline,
|
||||||
- and explicit first-wave exclusions such as public event publication and watchers.
|
- and explicit first-wave exclusions such as public event publication and watchers.
|
||||||
@ -140,8 +140,8 @@ Rules:
|
|||||||
- `FrontendSpec.allowedExtensions` is the source of truth for frontend scope in this wave;
|
- `FrontendSpec.allowedExtensions` is the source of truth for frontend scope in this wave;
|
||||||
- the VFS document contract must expose a canonical frontend-compatible `typeId` or equivalent scope marker derived from that source of truth;
|
- the VFS document contract must expose a canonical frontend-compatible `typeId` or equivalent scope marker derived from that source of truth;
|
||||||
- consumers must not infer frontend scope from raw dynamic language identifiers, path heuristics, or local UI extension checks;
|
- consumers must not infer frontend scope from raw dynamic language identifiers, path heuristics, or local UI extension checks;
|
||||||
- frontend-scoped supported documents must remain hard `read-only` in this wave;
|
- frontend-scoped supported documents must be editable in this wave when they are supported by the current frontend;
|
||||||
- the initial editable non-frontend set is limited to the currently supported textual classes represented as `text`, `json`, `ndjson`, and `bash`;
|
- the initial editable set is limited to the currently supported textual classes represented as `text`, `json`, `ndjson`, `bash`, and supported frontend source documents resolved from `FrontendSpec.allowedExtensions`;
|
||||||
- and no additional editable class may be inferred by implementation convenience during this wave.
|
- and no additional editable class may be inferred by implementation convenience during this wave.
|
||||||
|
|
||||||
## Document Access Context
|
## Document Access Context
|
||||||
@ -161,7 +161,7 @@ Rules:
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- opened frontend documents must be exposed to `prometeu-lsp` from the in-memory editorial snapshot held by `prometeu-vfs`;
|
- opened frontend documents must be exposed to `prometeu-lsp` from the in-memory editorial snapshot held by `prometeu-vfs`, including dirty snapshots not yet saved to disk;
|
||||||
- unopened frontend documents may be exposed to `prometeu-lsp` from filesystem-backed state through the same boundary;
|
- unopened frontend documents may be exposed to `prometeu-lsp` from filesystem-backed state through the same boundary;
|
||||||
- `prometeu-lsp` must not bypass `prometeu-vfs` with ad hoc filesystem reads inside Studio UI code;
|
- `prometeu-lsp` must not bypass `prometeu-vfs` with ad hoc filesystem reads inside Studio UI code;
|
||||||
- `prometeu-lsp` must not become the owner of save, persistence, or access policy;
|
- `prometeu-lsp` must not become the owner of save, persistence, or access policy;
|
||||||
@ -219,7 +219,6 @@ Rules:
|
|||||||
- merge or conflict handling
|
- merge or conflict handling
|
||||||
- non-project content snapshots
|
- non-project content snapshots
|
||||||
- a generic product-wide filesystem abstraction
|
- a generic product-wide filesystem abstraction
|
||||||
- frontend editing
|
|
||||||
- treating editorial snapshots as canonical build input
|
- treating editorial snapshots as canonical build input
|
||||||
|
|
||||||
## Exit Criteria
|
## Exit Criteria
|
||||||
@ -230,7 +229,7 @@ This specification is complete enough when:
|
|||||||
- the project-session lifecycle rule is unambiguous,
|
- the project-session lifecycle rule is unambiguous,
|
||||||
- the structural tree contract is explicitly non-visual,
|
- the structural tree contract is explicitly non-visual,
|
||||||
- frontend scope and access policy ownership are explicit,
|
- frontend scope and access policy ownership are explicit,
|
||||||
- editable non-frontend snapshot and save ownership are explicit,
|
- editable supported-document snapshot and save ownership are explicit,
|
||||||
- the semantic-read consumer boundary with `prometeu-lsp` is explicit,
|
- the semantic-read consumer boundary with `prometeu-lsp` is explicit,
|
||||||
- the RPC-first public API rule is explicit,
|
- the RPC-first public API rule is explicit,
|
||||||
- and deferred public events and watchers are clearly out of scope.
|
- and deferred public events and watchers are clearly out of scope.
|
||||||
|
|||||||
@ -9,7 +9,7 @@ Active
|
|||||||
- `prometeu-studio`
|
- `prometeu-studio`
|
||||||
- `prometeu-vfs`
|
- `prometeu-vfs`
|
||||||
- `prometeu-lsp`
|
- `prometeu-lsp`
|
||||||
- the frontend read-only semantic phase in the Studio `Code Editor` workspace
|
- the integrated frontend semantic phase in the Studio `Code Editor` workspace
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ Define the normative Studio contract for the integrated frontend semantic-read p
|
|||||||
|
|
||||||
This specification stabilizes:
|
This specification stabilizes:
|
||||||
|
|
||||||
- the phase boundary between semantic read and frontend editing,
|
- the phase boundary between semantic ownership and frontend editing ownership,
|
||||||
- the ownership relationship between `prometeu-vfs`, `prometeu-lsp`, and the Studio editor,
|
- the ownership relationship between `prometeu-vfs`, `prometeu-lsp`, and the Studio editor,
|
||||||
- the minimum semantic capability set for frontend documents,
|
- the minimum semantic capability set for frontend documents,
|
||||||
- the dedicated semantic surface used for structural anchors and guide-aware editor structure,
|
- the dedicated semantic surface used for structural anchors and guide-aware editor structure,
|
||||||
@ -37,8 +37,8 @@ If this document conflicts with shell-wide Studio rules, shell rules control she
|
|||||||
|
|
||||||
The integrated semantic-read phase must assume:
|
The integrated semantic-read phase must assume:
|
||||||
|
|
||||||
- frontend-scoped documents remain hard `read-only`,
|
- frontend-scoped documents may be editable under the controlled write wave exposed by `prometeu-vfs`,
|
||||||
- editable non-frontend documents remain governed by the controlled write wave,
|
- editable supported documents remain governed by the controlled write wave,
|
||||||
- `FrontendSpec.allowedExtensions` remains the source of truth for frontend scope,
|
- `FrontendSpec.allowedExtensions` remains the source of truth for frontend scope,
|
||||||
- `FrontendSpec` is the canonical source of frontend semantic presentation contract data,
|
- `FrontendSpec` is the canonical source of frontend semantic presentation contract data,
|
||||||
- `prometeu-vfs` owns document state, snapshots, persistence, and access policy,
|
- `prometeu-vfs` owns document state, snapshots, persistence, and access policy,
|
||||||
@ -46,13 +46,11 @@ The integrated semantic-read phase must assume:
|
|||||||
|
|
||||||
## Phase Boundary
|
## Phase Boundary
|
||||||
|
|
||||||
This phase is a semantic-read phase for frontend documents, not a frontend editing phase.
|
This phase is a semantic phase for frontend documents, not an ownership phase for frontend editing.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- frontend documents must remain hard `read-only` throughout this phase;
|
- no capability in this phase may override `prometeu-vfs` access policy, save policy, or snapshot ownership;
|
||||||
- no capability in this phase may imply frontend save, mutation, or edit-right release;
|
|
||||||
- the future release of frontend editing requires a separate explicit decision;
|
|
||||||
- completion, rename, code actions, and formatting remain outside this phase.
|
- completion, rename, code actions, and formatting remain outside this phase.
|
||||||
|
|
||||||
## Ownership Rules
|
## Ownership Rules
|
||||||
@ -165,7 +163,6 @@ Rules:
|
|||||||
|
|
||||||
## Non-Goals
|
## Non-Goals
|
||||||
|
|
||||||
- frontend editing
|
|
||||||
- frontend save policy
|
- frontend save policy
|
||||||
- completion
|
- completion
|
||||||
- rename
|
- rename
|
||||||
|
|||||||
@ -101,6 +101,11 @@ public enum I18n {
|
|||||||
CODE_EDITOR_STATUS_LANGUAGE("codeEditor.status.language"),
|
CODE_EDITOR_STATUS_LANGUAGE("codeEditor.status.language"),
|
||||||
CODE_EDITOR_COMMAND_SAVE("codeEditor.command.save"),
|
CODE_EDITOR_COMMAND_SAVE("codeEditor.command.save"),
|
||||||
CODE_EDITOR_COMMAND_SAVE_ALL("codeEditor.command.saveAll"),
|
CODE_EDITOR_COMMAND_SAVE_ALL("codeEditor.command.saveAll"),
|
||||||
|
CODE_EDITOR_CLOSE_DIRTY_TITLE("codeEditor.closeDirty.title"),
|
||||||
|
CODE_EDITOR_CLOSE_DIRTY_MESSAGE("codeEditor.closeDirty.message"),
|
||||||
|
CODE_EDITOR_CLOSE_DIRTY_SAVE("codeEditor.closeDirty.save"),
|
||||||
|
CODE_EDITOR_CLOSE_DIRTY_DISCARD("codeEditor.closeDirty.discard"),
|
||||||
|
CODE_EDITOR_CLOSE_DIRTY_CANCEL("codeEditor.closeDirty.cancel"),
|
||||||
CODE_EDITOR_WARNING_FRONTEND_READ_ONLY("codeEditor.warning.frontendReadOnly"),
|
CODE_EDITOR_WARNING_FRONTEND_READ_ONLY("codeEditor.warning.frontendReadOnly"),
|
||||||
CODE_EDITOR_UNSUPPORTED_FILE_TITLE("codeEditor.unsupportedFile.title"),
|
CODE_EDITOR_UNSUPPORTED_FILE_TITLE("codeEditor.unsupportedFile.title"),
|
||||||
CODE_EDITOR_UNSUPPORTED_FILE_MESSAGE("codeEditor.unsupportedFile.message"),
|
CODE_EDITOR_UNSUPPORTED_FILE_MESSAGE("codeEditor.unsupportedFile.message"),
|
||||||
|
|||||||
@ -30,6 +30,24 @@ public final class EditorOpenFileSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
public List<EditorOpenFileBuffer> openFiles() {
|
||||||
return List.copyOf(openFiles);
|
return List.copyOf(openFiles);
|
||||||
}
|
}
|
||||||
@ -41,6 +59,10 @@ public final class EditorOpenFileSession {
|
|||||||
return find(activePath);
|
return find(activePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<EditorOpenFileBuffer> file(final Path path) {
|
||||||
|
return find(normalize(path));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasDirtyEditableFiles() {
|
public boolean hasDirtyEditableFiles() {
|
||||||
return openFiles.stream().anyMatch(EditorOpenFileBuffer::saveEnabled);
|
return openFiles.stream().anyMatch(EditorOpenFileBuffer::saveEnabled);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,7 +106,11 @@ public final class EditorProjectNavigatorPanel extends BorderPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
treeView.getSelectionModel().selectedItemProperty().addListener((ignored, previous, current) -> {
|
treeView.setOnMouseClicked(event -> {
|
||||||
|
if (event.getClickCount() != 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final TreeItem<VfsProjectNode> current = treeView.getSelectionModel().getSelectedItem();
|
||||||
if (current == null || current.getValue() == null || current.getValue().directory()) {
|
if (current == null || current.getValue() == null || current.getValue().directory()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,11 @@ import javafx.application.Platform;
|
|||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ButtonBar;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyCodeCombination;
|
||||||
|
import javafx.scene.input.KeyCombination;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
import javafx.scene.control.SplitPane;
|
import javafx.scene.control.SplitPane;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
@ -32,6 +36,12 @@ import java.util.Objects;
|
|||||||
import java.util.function.IntFunction;
|
import java.util.function.IntFunction;
|
||||||
|
|
||||||
public final class EditorWorkspace extends Workspace {
|
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 BorderPane root = new BorderPane();
|
||||||
private final CodeArea codeArea = new CodeArea();
|
private final CodeArea codeArea = new CodeArea();
|
||||||
private final VirtualizedScrollPane<CodeArea> codeScroller = new VirtualizedScrollPane<>(codeArea);
|
private final VirtualizedScrollPane<CodeArea> codeScroller = new VirtualizedScrollPane<>(codeArea);
|
||||||
@ -59,6 +69,8 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
List.of());
|
List.of());
|
||||||
private boolean syncingEditor;
|
private boolean syncingEditor;
|
||||||
private boolean applyingPendingLayoutState;
|
private boolean applyingPendingLayoutState;
|
||||||
|
private int activeGuideParagraph = -1;
|
||||||
|
private boolean pendingCaretContextRestore;
|
||||||
private Runnable stateChangedAction = () -> { };
|
private Runnable stateChangedAction = () -> { };
|
||||||
private ProjectLocalStudioState.EditorLayoutState pendingEditorLayoutState = ProjectLocalStudioState.EditorLayoutState.defaults();
|
private ProjectLocalStudioState.EditorLayoutState pendingEditorLayoutState = ProjectLocalStudioState.EditorLayoutState.defaults();
|
||||||
private SplitPane contentSplit;
|
private SplitPane contentSplit;
|
||||||
@ -81,12 +93,15 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current));
|
codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current));
|
||||||
codeArea.caretPositionProperty().addListener((ignored, previous, current) -> {
|
codeArea.caretPositionProperty().addListener((ignored, previous, current) -> {
|
||||||
final int caretOffset = current == null ? 0 : current.intValue();
|
final int caretOffset = current == null ? 0 : current.intValue();
|
||||||
updateActiveGuides(caretOffset);
|
updateActiveGuides(caretOffset, codeArea.getCurrentParagraph());
|
||||||
refreshStatusBarCaret();
|
refreshStatusBarCaret();
|
||||||
});
|
});
|
||||||
|
codeArea.estimatedScrollYProperty().addListener((ignored, previous, current) ->
|
||||||
|
restoreCaretScrollContextIfNeeded(previous, current));
|
||||||
codeArea.getStyleClass().add("editor-workspace-code-area");
|
codeArea.getStyleClass().add("editor-workspace-code-area");
|
||||||
codeArea.addEventFilter(KeyEvent.KEY_PRESSED, this::guardInlineHintMutation);
|
codeArea.addEventFilter(KeyEvent.KEY_PRESSED, this::guardInlineHintMutation);
|
||||||
codeArea.addEventFilter(KeyEvent.KEY_TYPED, this::guardInlineHintMutation);
|
codeArea.addEventFilter(KeyEvent.KEY_TYPED, this::guardInlineHintMutation);
|
||||||
|
root.addEventFilter(KeyEvent.KEY_PRESSED, this::handleWorkspaceShortcuts);
|
||||||
inlineHintChangeSubscription = codeArea.plainTextChanges().subscribe(change -> {
|
inlineHintChangeSubscription = codeArea.plainTextChanges().subscribe(change -> {
|
||||||
if (syncingEditor) {
|
if (syncingEditor) {
|
||||||
return;
|
return;
|
||||||
@ -108,6 +123,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
notifyStateChanged();
|
notifyStateChanged();
|
||||||
renderSession();
|
renderSession();
|
||||||
});
|
});
|
||||||
|
tabStrip.setTabCloseAction(this::requestCloseFile);
|
||||||
|
|
||||||
root.setCenter(buildLayout());
|
root.setCenter(buildLayout());
|
||||||
statusBar.showPlaceholder(presentationRegistry.resolve("text"));
|
statusBar.showPlaceholder(presentationRegistry.resolve("text"));
|
||||||
@ -218,16 +234,13 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
codeArea.replaceText(inlineHintProjection.displayText());
|
codeArea.replaceText(inlineHintProjection.displayText());
|
||||||
codeArea.setStyleSpans(0, inlineHintProjection.displayStyles());
|
codeArea.setStyleSpans(0, inlineHintProjection.displayStyles());
|
||||||
codeArea.moveTo(0);
|
codeArea.moveTo(0);
|
||||||
codeArea.requestFollowCaret();
|
|
||||||
} finally {
|
} finally {
|
||||||
syncingEditor = false;
|
syncingEditor = false;
|
||||||
}
|
}
|
||||||
activeGuides = scopeGuideModel.resolveActiveGuides(codeArea.getCaretPosition());
|
activeGuides = scopeGuideModel.resolveActiveGuides(codeArea.getCaretPosition());
|
||||||
|
activeGuideParagraph = codeArea.getCurrentParagraph();
|
||||||
refreshParagraphGraphics();
|
refreshParagraphGraphics();
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
codeArea.moveTo(0);
|
|
||||||
codeArea.showParagraphAtTop(0);
|
|
||||||
codeArea.requestFollowCaret();
|
|
||||||
if (fileBuffer.editable()) {
|
if (fileBuffer.editable()) {
|
||||||
codeArea.requestFocus();
|
codeArea.requestFocus();
|
||||||
}
|
}
|
||||||
@ -261,6 +274,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
|
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
|
||||||
scopeGuideModel = EditorDocumentScopeGuideModel.empty();
|
scopeGuideModel = EditorDocumentScopeGuideModel.empty();
|
||||||
activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty();
|
activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty();
|
||||||
|
activeGuideParagraph = -1;
|
||||||
refreshParagraphGraphics();
|
refreshParagraphGraphics();
|
||||||
applyPresentationStylesheets(presentation);
|
applyPresentationStylesheets(presentation);
|
||||||
syncingEditor = true;
|
syncingEditor = true;
|
||||||
@ -269,15 +283,9 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
codeArea.replaceText("");
|
codeArea.replaceText("");
|
||||||
codeArea.setStyleSpans(0, inlineHintProjection.displayStyles());
|
codeArea.setStyleSpans(0, inlineHintProjection.displayStyles());
|
||||||
codeArea.moveTo(0);
|
codeArea.moveTo(0);
|
||||||
codeArea.requestFollowCaret();
|
|
||||||
} finally {
|
} finally {
|
||||||
syncingEditor = false;
|
syncingEditor = false;
|
||||||
}
|
}
|
||||||
Platform.runLater(() -> {
|
|
||||||
codeArea.moveTo(0);
|
|
||||||
codeArea.showParagraphAtTop(0);
|
|
||||||
codeArea.requestFollowCaret();
|
|
||||||
});
|
|
||||||
codeArea.setEditable(false);
|
codeArea.setEditable(false);
|
||||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
||||||
saveButton.setDisable(true);
|
saveButton.setDisable(true);
|
||||||
@ -295,11 +303,11 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refreshParagraphGraphics() {
|
private void refreshParagraphGraphics() {
|
||||||
codeArea.setParagraphGraphicFactory(paragraphIndex -> EditorDocumentScopeGuideGraphicFactory.create(
|
preserveViewport(() -> codeArea.setParagraphGraphicFactory(paragraphIndex -> EditorDocumentScopeGuideGraphicFactory.create(
|
||||||
lineNumberFactory.apply(paragraphIndex),
|
lineNumberFactory.apply(paragraphIndex),
|
||||||
paragraphIndex,
|
paragraphIndex,
|
||||||
scopeGuideModel,
|
scopeGuideModel,
|
||||||
activeGuides));
|
activeGuides)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private VBox buildLayout() {
|
private VBox buildLayout() {
|
||||||
@ -346,7 +354,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
private void configureCommandBar() {
|
private void configureCommandBar() {
|
||||||
saveButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE));
|
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));
|
saveAllButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE_ALL));
|
||||||
saveButton.getStyleClass().addAll("studio-button", "editor-workspace-command-button");
|
saveButton.getStyleClass().addAll("studio-button", "studio-button-primary", "editor-workspace-command-button");
|
||||||
saveAllButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "editor-workspace-command-button");
|
saveAllButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "editor-workspace-command-button");
|
||||||
saveButton.setFocusTraversable(false);
|
saveButton.setFocusTraversable(false);
|
||||||
saveAllButton.setFocusTraversable(false);
|
saveAllButton.setFocusTraversable(false);
|
||||||
@ -378,6 +386,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
final String sourceContent = inlineHintProjection.stripDecorations(content);
|
final String sourceContent = inlineHintProjection.stripDecorations(content);
|
||||||
final VfsDocumentOpenResult.VfsTextDocument updatedDocument = vfsProjectDocument.updateDocument(activeFile.path(), sourceContent);
|
final VfsDocumentOpenResult.VfsTextDocument updatedDocument = vfsProjectDocument.updateDocument(activeFile.path(), sourceContent);
|
||||||
openFileSession.open(bufferFrom(updatedDocument));
|
openFileSession.open(bufferFrom(updatedDocument));
|
||||||
|
pendingCaretContextRestore = true;
|
||||||
refreshEditableHighlighting(updatedDocument);
|
refreshEditableHighlighting(updatedDocument);
|
||||||
statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), indentationSetup.statusLabel());
|
statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), indentationSetup.statusLabel());
|
||||||
tabStrip.showOpenFiles(
|
tabStrip.showOpenFiles(
|
||||||
@ -393,7 +402,33 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
updatedDocument.content(),
|
updatedDocument.content(),
|
||||||
presentation.highlight(updatedDocument.content()),
|
presentation.highlight(updatedDocument.content()),
|
||||||
List.of());
|
List.of());
|
||||||
codeArea.setStyleSpans(0, inlineHintProjection.displayStyles());
|
preserveViewport(() -> codeArea.setStyleSpans(0, inlineHintProjection.displayStyles()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
private void saveActiveFile() {
|
||||||
@ -415,6 +450,67 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
renderSession();
|
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) {
|
private void reloadOpenFilesFromVfs(final Path activePath) {
|
||||||
final var openPaths = openFileSession.openFiles().stream()
|
final var openPaths = openFileSession.openFiles().stream()
|
||||||
.map(EditorOpenFileBuffer::path)
|
.map(EditorOpenFileBuffer::path)
|
||||||
@ -433,16 +529,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
private void refreshCommandSurfaces(final EditorOpenFileBuffer fileBuffer) {
|
private void refreshCommandSurfaces(final EditorOpenFileBuffer fileBuffer) {
|
||||||
saveButton.setDisable(!fileBuffer.saveEnabled());
|
saveButton.setDisable(!fileBuffer.saveEnabled());
|
||||||
saveAllButton.setDisable(!openFileSession.hasDirtyEditableFiles());
|
saveAllButton.setDisable(!openFileSession.hasDirtyEditableFiles());
|
||||||
final List<EditorWarningStrip.WarningItem> warnings = new ArrayList<>();
|
|
||||||
if (fileBuffer.frontendDocument() && fileBuffer.readOnly()) {
|
|
||||||
warnings.add(new EditorWarningStrip.WarningItem(
|
|
||||||
p.studio.Container.i18n().text(I18n.CODE_EDITOR_WARNING_FRONTEND_READ_ONLY)));
|
|
||||||
}
|
|
||||||
if (warnings.isEmpty()) {
|
|
||||||
warningStrip.clearWarnings();
|
warningStrip.clearWarnings();
|
||||||
return;
|
|
||||||
}
|
|
||||||
warningStrip.showWarnings(warnings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private EditorOpenFileBuffer bufferFrom(final VfsDocumentOpenResult.VfsTextDocument textDocument) {
|
private EditorOpenFileBuffer bufferFrom(final VfsDocumentOpenResult.VfsTextDocument textDocument) {
|
||||||
@ -483,12 +570,17 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
return EditorDocumentScopeGuideModel.from(fileBuffer.content(), analysis.documentSymbols());
|
return EditorDocumentScopeGuideModel.from(fileBuffer.content(), analysis.documentSymbols());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateActiveGuides(final int caretOffset) {
|
private void updateActiveGuides(final int caretOffset, final int currentParagraph) {
|
||||||
final EditorDocumentScopeGuideModel.ActiveGuides next = scopeGuideModel.resolveActiveGuides(caretOffset);
|
final EditorDocumentScopeGuideModel.ActiveGuides next = scopeGuideModel.resolveActiveGuides(caretOffset);
|
||||||
if (Objects.equals(activeGuides, next)) {
|
if (Objects.equals(activeGuides, next) && activeGuideParagraph == currentParagraph) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeGuides = next;
|
activeGuides = next;
|
||||||
|
final boolean paragraphChanged = activeGuideParagraph != currentParagraph;
|
||||||
|
activeGuideParagraph = currentParagraph;
|
||||||
|
if (!paragraphChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
refreshParagraphGraphics();
|
refreshParagraphGraphics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -92,6 +92,11 @@ codeEditor.status.indentation=Spaces: 4
|
|||||||
codeEditor.status.language=Text
|
codeEditor.status.language=Text
|
||||||
codeEditor.command.save=Save
|
codeEditor.command.save=Save
|
||||||
codeEditor.command.saveAll=Save All
|
codeEditor.command.saveAll=Save All
|
||||||
|
codeEditor.closeDirty.title=Unsaved changes
|
||||||
|
codeEditor.closeDirty.message=Save changes to {0} before closing?
|
||||||
|
codeEditor.closeDirty.save=Save
|
||||||
|
codeEditor.closeDirty.discard=Discard
|
||||||
|
codeEditor.closeDirty.cancel=Cancel
|
||||||
codeEditor.warning.frontendReadOnly=This frontend file is read-only in this wave. It cannot be edited or saved yet.
|
codeEditor.warning.frontendReadOnly=This frontend file is read-only in this wave. It cannot be edited or saved yet.
|
||||||
codeEditor.unsupportedFile.title=Unsupported file
|
codeEditor.unsupportedFile.title=Unsupported file
|
||||||
codeEditor.unsupportedFile.message=This file is not supported in this wave: {0}
|
codeEditor.unsupportedFile.message=This file is not supported in this wave: {0}
|
||||||
|
|||||||
@ -598,6 +598,29 @@
|
|||||||
-fx-text-fill: #d6dde6;
|
-fx-text-fill: #d6dde6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-container {
|
||||||
|
-fx-spacing: 8;
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
-fx-padding: 0 8 0 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-label {
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-close-chip {
|
||||||
|
-fx-alignment: center;
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-close-icon {
|
||||||
|
-fx-fill: transparent;
|
||||||
|
-fx-stroke: #d6dde6;
|
||||||
|
-fx-stroke-width: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-workspace-tab-button-active {
|
.editor-workspace-tab-button-active {
|
||||||
-fx-background-color: #16283d;
|
-fx-background-color: #16283d;
|
||||||
-fx-border-color: #8fc4f2 #516579 #516579 #516579;
|
-fx-border-color: #8fc4f2 #516579 #516579 #516579;
|
||||||
@ -612,12 +635,24 @@
|
|||||||
-fx-text-fill: #d9dee5;
|
-fx-text-fill: #d9dee5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-label.editor-workspace-tab-button-read-only {
|
||||||
|
-fx-text-fill: #d9dee5;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-workspace-tab-button-read-only:hover {
|
.editor-workspace-tab-button-read-only:hover {
|
||||||
-fx-background-color: #2b323d;
|
-fx-background-color: #2b323d;
|
||||||
-fx-border-color: #5b6878;
|
-fx-border-color: #5b6878;
|
||||||
-fx-text-fill: #eff4fa;
|
-fx-text-fill: #eff4fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-label.editor-workspace-tab-button-read-only:hover {
|
||||||
|
-fx-text-fill: #eff4fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-close-chip.editor-workspace-tab-button-read-only .editor-workspace-tab-close-icon {
|
||||||
|
-fx-stroke: #d9dee5;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-workspace-tab-button-read-only.editor-workspace-tab-button-active {
|
.editor-workspace-tab-button-read-only.editor-workspace-tab-button-active {
|
||||||
-fx-background-color: #16283d;
|
-fx-background-color: #16283d;
|
||||||
-fx-border-color: #8fc4f2 #516579 #516579 #516579;
|
-fx-border-color: #8fc4f2 #516579 #516579 #516579;
|
||||||
@ -626,6 +661,11 @@
|
|||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-label.editor-workspace-tab-button-read-only.editor-workspace-tab-button-active {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-workspace-tab-button-read-only.editor-workspace-tab-button-active:hover {
|
.editor-workspace-tab-button-read-only.editor-workspace-tab-button-active:hover {
|
||||||
-fx-background-color: #1c3148;
|
-fx-background-color: #1c3148;
|
||||||
-fx-border-color: #a7d7ff #5c738b #5c738b #5c738b;
|
-fx-border-color: #a7d7ff #5c738b #5c738b #5c738b;
|
||||||
@ -637,12 +677,24 @@
|
|||||||
-fx-text-fill: #e8f6eb;
|
-fx-text-fill: #e8f6eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-label.editor-workspace-tab-button-editable {
|
||||||
|
-fx-text-fill: #e8f6eb;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-workspace-tab-button-editable:hover {
|
.editor-workspace-tab-button-editable:hover {
|
||||||
-fx-background-color: #29412f;
|
-fx-background-color: #29412f;
|
||||||
-fx-border-color: #6f957a;
|
-fx-border-color: #6f957a;
|
||||||
-fx-text-fill: #f4fff5;
|
-fx-text-fill: #f4fff5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-label.editor-workspace-tab-button-editable:hover {
|
||||||
|
-fx-text-fill: #f4fff5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-workspace-tab-close-chip.editor-workspace-tab-button-editable .editor-workspace-tab-close-icon {
|
||||||
|
-fx-stroke: #e8f6eb;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-workspace-tab-button-editable.editor-workspace-tab-button-active {
|
.editor-workspace-tab-button-editable.editor-workspace-tab-button-active {
|
||||||
-fx-background-color: #1d3a2a;
|
-fx-background-color: #1d3a2a;
|
||||||
-fx-border-color: #8ad3a2 #587464 #587464 #587464;
|
-fx-border-color: #8ad3a2 #587464 #587464 #587464;
|
||||||
@ -656,6 +708,7 @@
|
|||||||
-fx-border-color: #a5efbd #688676 #688676 #688676;
|
-fx-border-color: #a5efbd #688676 #688676 #688676;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.editor-workspace-tab-overflow {
|
.editor-workspace-tab-overflow {
|
||||||
-fx-background-radius: 0;
|
-fx-background-radius: 0;
|
||||||
-fx-border-radius: 0;
|
-fx-border-radius: 0;
|
||||||
@ -1849,3 +1902,7 @@
|
|||||||
-fx-text-fill: #b9cae0;
|
-fx-text-fill: #b9cae0;
|
||||||
-fx-font-size: 12px;
|
-fx-font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.editor-workspace-tab-label.editor-workspace-tab-button-editable.editor-workspace-tab-button-active {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ final class EditorOpenFileSessionTest {
|
|||||||
@Test
|
@Test
|
||||||
void openAddsNewFileAndMarksItActive() {
|
void openAddsNewFileAndMarksItActive() {
|
||||||
final var session = new EditorOpenFileSession();
|
final var session = new EditorOpenFileSession();
|
||||||
final var file = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "fn main(): void\n");
|
final var file = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "fn main(): void\n");
|
||||||
|
|
||||||
session.open(file);
|
session.open(file);
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ final class EditorOpenFileSessionTest {
|
|||||||
@Test
|
@Test
|
||||||
void openDoesNotDuplicateExistingTab() {
|
void openDoesNotDuplicateExistingTab() {
|
||||||
final var session = new EditorOpenFileSession();
|
final var session = new EditorOpenFileSession();
|
||||||
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
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.READ_ONLY, false, true, "b");
|
final var second = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "b");
|
||||||
|
|
||||||
session.open(first);
|
session.open(first);
|
||||||
session.open(second);
|
session.open(second);
|
||||||
@ -38,8 +38,8 @@ final class EditorOpenFileSessionTest {
|
|||||||
@Test
|
@Test
|
||||||
void activateSwitchesTheActiveTabWithinTheCurrentSession() {
|
void activateSwitchesTheActiveTabWithinTheCurrentSession() {
|
||||||
final var session = new EditorOpenFileSession();
|
final var session = new EditorOpenFileSession();
|
||||||
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
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.READ_ONLY, false, true, "b");
|
final var second = fileBuffer(Path.of("src/other.pbs"), "other.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "b");
|
||||||
|
|
||||||
session.open(first);
|
session.open(first);
|
||||||
session.open(second);
|
session.open(second);
|
||||||
@ -58,20 +58,54 @@ final class EditorOpenFileSessionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void frontendReadOnlyFilesDoNotEnableSaveActions() {
|
void frontendEditableFilesParticipateInSaveStateWhenDirty() {
|
||||||
final var session = new EditorOpenFileSession();
|
final var session = new EditorOpenFileSession();
|
||||||
session.open(fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "fn main(): void"));
|
session.open(fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, true, true, "fn main(): void"));
|
||||||
|
|
||||||
assertFalse(session.hasSaveableActiveFile());
|
assertTrue(session.hasSaveableActiveFile());
|
||||||
assertFalse(session.hasDirtyEditableFiles());
|
assertTrue(session.hasDirtyEditableFiles());
|
||||||
assertTrue(session.activeFile().orElseThrow().frontendDocument());
|
assertTrue(session.activeFile().orElseThrow().frontendDocument());
|
||||||
assertTrue(session.activeFile().orElseThrow().readOnly());
|
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
|
@Test
|
||||||
void exportsRestorationStateFromOpenTabsAndActiveTab() {
|
void exportsRestorationStateFromOpenTabsAndActiveTab() {
|
||||||
final var session = new EditorOpenFileSession();
|
final var session = new EditorOpenFileSession();
|
||||||
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
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 second = fileBuffer(Path.of("README.md"), "README.md", VfsDocumentAccessMode.EDITABLE, false, false, "b");
|
||||||
|
|
||||||
session.open(first);
|
session.open(first);
|
||||||
|
|||||||
@ -33,6 +33,10 @@ public interface VfsProjectDocument extends AutoCloseable {
|
|||||||
throw new UnsupportedOperationException("Document save is not supported by this VFS implementation.");
|
throw new UnsupportedOperationException("Document save is not supported by this VFS implementation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default VfsDocumentOpenResult.VfsTextDocument discardDocument(final Path path) {
|
||||||
|
throw new UnsupportedOperationException("Document discard is not supported by this VFS implementation.");
|
||||||
|
}
|
||||||
|
|
||||||
default List<VfsDocumentSaveResult> saveAllDocuments() {
|
default List<VfsDocumentSaveResult> saveAllDocuments() {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,6 +125,13 @@ final class FilesystemVfsProjectDocument implements VfsProjectDocument {
|
|||||||
return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.SAVED);
|
return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.SAVED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VfsDocumentOpenResult.VfsTextDocument discardDocument(final Path path) {
|
||||||
|
final var supportedDocument = requireSupportedDocument(path);
|
||||||
|
editableSnapshots.remove(supportedDocument.path());
|
||||||
|
return toVfsTextDocument(supportedDocument, false, accessContextFor(supportedDocument));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<VfsDocumentSaveResult> saveAllDocuments() {
|
public List<VfsDocumentSaveResult> saveAllDocuments() {
|
||||||
return editableSnapshots.keySet().stream()
|
return editableSnapshots.keySet().stream()
|
||||||
@ -230,7 +237,7 @@ final class FilesystemVfsProjectDocument implements VfsProjectDocument {
|
|||||||
return new DocumentKind(VfsDocumentTypeIds.BASH, false, VfsDocumentAccessMode.EDITABLE);
|
return new DocumentKind(VfsDocumentTypeIds.BASH, false, VfsDocumentAccessMode.EDITABLE);
|
||||||
}
|
}
|
||||||
if (isFrontendSourceDocument(extension)) {
|
if (isFrontendSourceDocument(extension)) {
|
||||||
return new DocumentKind(projectContext.languageId(), true, VfsDocumentAccessMode.READ_ONLY);
|
return new DocumentKind(projectContext.languageId(), true, VfsDocumentAccessMode.EDITABLE);
|
||||||
}
|
}
|
||||||
return new DocumentKind(VfsDocumentTypeIds.TEXT, false, VfsDocumentAccessMode.EDITABLE);
|
return new DocumentKind(VfsDocumentTypeIds.TEXT, false, VfsDocumentAccessMode.EDITABLE);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ final class FilesystemVfsProjectDocumentTest {
|
|||||||
assertEquals("main.pbs", document.documentName());
|
assertEquals("main.pbs", document.documentName());
|
||||||
assertEquals("pbs", document.typeId());
|
assertEquals("pbs", document.typeId());
|
||||||
assertEquals("LF", document.lineSeparator());
|
assertEquals("LF", document.lineSeparator());
|
||||||
assertEquals(VfsDocumentAccessMode.READ_ONLY, document.accessContext().accessMode());
|
assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode());
|
||||||
assertTrue(document.accessContext().frontendDocument());
|
assertTrue(document.accessContext().frontendDocument());
|
||||||
assertTrue(document.content().contains("fn main()"));
|
assertTrue(document.content().contains("fn main()"));
|
||||||
}
|
}
|
||||||
@ -92,13 +92,23 @@ final class FilesystemVfsProjectDocumentTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void updateDocumentRejectsFrontendDocumentsAsHardReadOnly() throws Exception {
|
void frontendDocumentsUseEditableInMemorySnapshotsUntilSavePersistsThem() throws Exception {
|
||||||
final Path file = tempDir.resolve("main.pbs");
|
final Path file = tempDir.resolve("main.pbs");
|
||||||
Files.writeString(file, "fn main(): void\n");
|
Files.writeString(file, "fn main(): void\n");
|
||||||
|
|
||||||
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||||
|
final var updated = vfs.updateDocument(file, "fn main(): int\n");
|
||||||
|
|
||||||
assertThrows(IllegalStateException.class, () -> vfs.updateDocument(file, "fn main(): int\n"));
|
assertTrue(updated.dirty());
|
||||||
|
assertEquals("fn main(): int\n", updated.content());
|
||||||
|
assertEquals("fn main(): void\n", Files.readString(file));
|
||||||
|
assertEquals("fn main(): int\n", assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).content());
|
||||||
|
|
||||||
|
final VfsDocumentSaveResult saveResult = vfs.saveDocument(file);
|
||||||
|
|
||||||
|
assertEquals(VfsDocumentSaveStatus.SAVED, saveResult.status());
|
||||||
|
assertEquals("fn main(): int\n", Files.readString(file));
|
||||||
|
assertFalse(assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).dirty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -123,7 +133,23 @@ final class FilesystemVfsProjectDocumentTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void saveAllDocumentsPersistsOnlyEditableDirtySnapshots() throws Exception {
|
void discardDocumentDropsDirtySnapshotAndRestoresFilesystemState() throws Exception {
|
||||||
|
final Path file = tempDir.resolve("notes.txt");
|
||||||
|
Files.writeString(file, "alpha\n");
|
||||||
|
|
||||||
|
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||||
|
vfs.updateDocument(file, "beta\n");
|
||||||
|
|
||||||
|
final var discarded = vfs.discardDocument(file);
|
||||||
|
|
||||||
|
assertFalse(discarded.dirty());
|
||||||
|
assertEquals("alpha\n", discarded.content());
|
||||||
|
assertEquals("alpha\n", Files.readString(file));
|
||||||
|
assertEquals("alpha\n", assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).content());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveAllDocumentsPersistsEditableDirtySnapshotsIncludingFrontendDocuments() throws Exception {
|
||||||
final Path editable = tempDir.resolve("notes.txt");
|
final Path editable = tempDir.resolve("notes.txt");
|
||||||
final Path frontend = tempDir.resolve("main.pbs");
|
final Path frontend = tempDir.resolve("main.pbs");
|
||||||
Files.writeString(editable, "alpha\n");
|
Files.writeString(editable, "alpha\n");
|
||||||
@ -131,14 +157,17 @@ final class FilesystemVfsProjectDocumentTest {
|
|||||||
|
|
||||||
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||||
vfs.updateDocument(editable, "beta\n");
|
vfs.updateDocument(editable, "beta\n");
|
||||||
|
vfs.updateDocument(frontend, "fn main(): int\n");
|
||||||
|
|
||||||
final var results = vfs.saveAllDocuments();
|
final var results = vfs.saveAllDocuments();
|
||||||
|
|
||||||
assertEquals(1, results.size());
|
assertEquals(2, results.size());
|
||||||
assertEquals(VfsDocumentSaveStatus.SAVED, results.get(0).status());
|
|
||||||
assertEquals(editable.toAbsolutePath().normalize(), results.get(0).path());
|
assertEquals(editable.toAbsolutePath().normalize(), results.get(0).path());
|
||||||
|
assertEquals(VfsDocumentSaveStatus.SAVED, results.get(0).status());
|
||||||
|
assertEquals(frontend.toAbsolutePath().normalize(), results.get(1).path());
|
||||||
|
assertEquals(VfsDocumentSaveStatus.SAVED, results.get(1).status());
|
||||||
assertEquals("beta\n", Files.readString(editable));
|
assertEquals("beta\n", Files.readString(editable));
|
||||||
assertEquals("fn main(): void\n", Files.readString(frontend));
|
assertEquals("fn main(): int\n", Files.readString(frontend));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -26,7 +26,7 @@ fn frame() -> void
|
|||||||
Gfx.clear(new Color(6577));
|
Gfx.clear(new Color(6577));
|
||||||
|
|
||||||
if (loading_handle == -1) {
|
if (loading_handle == -1) {
|
||||||
let t = Assets.load(assets.ui.atlas2, 3);
|
let t : (status: int, loading_handle: int) = Assets.load(assets.ui.atlas2, 3);
|
||||||
if (t.status != 0) {
|
if (t.status != 0) {
|
||||||
Log.failure("load failed");
|
Log.failure("load failed");
|
||||||
} else {
|
} else {
|
||||||
@ -34,14 +34,14 @@ fn frame() -> void
|
|||||||
Log.info("state: loading");
|
Log.info("state: loading");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let s = Assets.status(loading_handle);
|
let s : int = Assets.status(loading_handle);
|
||||||
if (s == 2) {
|
if (s == 2) {
|
||||||
let commit_status = Assets.commit(loading_handle);
|
let commit_status : int = Assets.commit(loading_handle);
|
||||||
if (commit_status != 0) {
|
if (commit_status != 0) {
|
||||||
Log.failure("commit failed");
|
Log.failure("commit failed");
|
||||||
}
|
}
|
||||||
} else if (s == 3) {
|
} else if (s == 3) {
|
||||||
let sprite_status = Gfx.set_sprite(3, 10, 150, 150, 0, 0, true, false, false, 1);
|
let sprite_status : int = Gfx.set_sprite(3, 10, 150, 150, 0, 0, true, false, false, 1);
|
||||||
if (sprite_status != 0) {
|
if (sprite_status != 0) {
|
||||||
Log.failure("set_sprite failed");
|
Log.failure("set_sprite failed");
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ fn frame() -> void
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
let touch = Input.touch();
|
let touch : InputTouch = Input.touch();
|
||||||
|
|
||||||
if (touch.button().released())
|
if (touch.button().released())
|
||||||
{
|
{
|
||||||
@ -77,9 +77,9 @@ fn frame() -> void
|
|||||||
Gfx.set_sprite(0, 0, touch.x() - 16, touch.y() + 8, tile_id, 0, true, true, false, 0);
|
Gfx.set_sprite(0, 0, touch.x() - 16, touch.y() + 8, tile_id, 0, true, true, false, 0);
|
||||||
Gfx.set_sprite(0, 1, touch.x() + 16, touch.y() + 8, tile_id, 0, true, false, false, 0);
|
Gfx.set_sprite(0, 1, touch.x() + 16, touch.y() + 8, tile_id, 0, true, false, false, 0);
|
||||||
|
|
||||||
let a = 10;
|
let a : int = 10;
|
||||||
let b = 15;
|
let b : int = 15;
|
||||||
let total = a + b;
|
let total : int = a + b;
|
||||||
|
|
||||||
if (Input.pad().a().pressed())
|
if (Input.pad().a().pressed())
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user