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-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"}]}
|
||||
@ -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-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-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`
|
||||
- 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
|
||||
|
||||
@ -18,7 +18,7 @@ This specification stabilizes:
|
||||
|
||||
- the baseline visual composition of the workspace,
|
||||
- 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 editor-owned composition surfaces that host save and semantic-read UX,
|
||||
- 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 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,
|
||||
- frontend-scoped supported documents remain hard `read-only`,
|
||||
- the current wave allows editing for the supported document classes classified by `prometeu-vfs` as editable, including frontend-scoped supported documents,
|
||||
- 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,
|
||||
- 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,
|
||||
- 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.
|
||||
|
||||
The workspace must help the user:
|
||||
@ -69,15 +68,14 @@ The workspace must help the user:
|
||||
- see the full project tree,
|
||||
- identify frontend-relevant source roots visually,
|
||||
- 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,
|
||||
- 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:
|
||||
|
||||
- merge behavior,
|
||||
- frontend editing,
|
||||
- completion,
|
||||
- rename, code actions, or formatting,
|
||||
- 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.
|
||||
- File opening must resolve document content through `prometeu-vfs`.
|
||||
- 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.
|
||||
- Overflow tabs must remain accessible through an IntelliJ-style overflow control.
|
||||
- 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 not infer frontend scope from path heuristics, local UI state, or ad hoc extension checks;
|
||||
- supported frontend-scoped documents must remain hard `read-only`;
|
||||
- editable scope is limited to the supported non-frontend textual classes exposed by `prometeu-vfs` for this wave;
|
||||
- the workspace must not allow local editorial mutation for hard `read-only` frontend documents;
|
||||
- supported frontend-scoped documents may be editable when `prometeu-vfs` classifies them as editable;
|
||||
- editable scope is limited to the supported textual classes exposed by `prometeu-vfs` for this wave, including supported frontend sources;
|
||||
- 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;
|
||||
- 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 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;
|
||||
@ -224,7 +220,7 @@ Rules:
|
||||
|
||||
## 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.
|
||||
- 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`.
|
||||
@ -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.
|
||||
- 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 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
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ This specification stabilizes:
|
||||
- the filesystem-backed first-wave contract,
|
||||
- structural tree and document access responsibilities,
|
||||
- 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 RPC-first public API baseline,
|
||||
- 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;
|
||||
- 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;
|
||||
- frontend-scoped supported documents must remain hard `read-only` in this wave;
|
||||
- the initial editable non-frontend set is limited to the currently supported textual classes represented as `text`, `json`, `ndjson`, and `bash`;
|
||||
- frontend-scoped supported documents must be editable in this wave when they are supported by the current frontend;
|
||||
- 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.
|
||||
|
||||
## Document Access Context
|
||||
@ -161,7 +161,7 @@ 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;
|
||||
- `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;
|
||||
@ -219,7 +219,6 @@ Rules:
|
||||
- merge or conflict handling
|
||||
- non-project content snapshots
|
||||
- a generic product-wide filesystem abstraction
|
||||
- frontend editing
|
||||
- treating editorial snapshots as canonical build input
|
||||
|
||||
## Exit Criteria
|
||||
@ -230,7 +229,7 @@ This specification is complete enough when:
|
||||
- the project-session lifecycle rule is unambiguous,
|
||||
- the structural tree contract is explicitly non-visual,
|
||||
- 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 RPC-first public API rule is explicit,
|
||||
- and deferred public events and watchers are clearly out of scope.
|
||||
|
||||
@ -9,7 +9,7 @@ Active
|
||||
- `prometeu-studio`
|
||||
- `prometeu-vfs`
|
||||
- `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
|
||||
|
||||
@ -17,7 +17,7 @@ Define the normative Studio contract for the integrated frontend semantic-read p
|
||||
|
||||
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 minimum semantic capability set for frontend documents,
|
||||
- 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:
|
||||
|
||||
- frontend-scoped documents remain hard `read-only`,
|
||||
- editable non-frontend documents remain governed by the controlled write wave,
|
||||
- frontend-scoped documents may be editable under the controlled write wave exposed by `prometeu-vfs`,
|
||||
- editable supported documents remain governed by the controlled write wave,
|
||||
- `FrontendSpec.allowedExtensions` remains the source of truth for frontend scope,
|
||||
- `FrontendSpec` is the canonical source of frontend semantic presentation contract data,
|
||||
- `prometeu-vfs` owns document state, snapshots, persistence, and access policy,
|
||||
@ -46,13 +46,11 @@ The integrated semantic-read phase must assume:
|
||||
|
||||
## 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:
|
||||
|
||||
- frontend documents must remain hard `read-only` throughout this phase;
|
||||
- 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;
|
||||
- no capability in this phase may override `prometeu-vfs` access policy, save policy, or snapshot ownership;
|
||||
- completion, rename, code actions, and formatting remain outside this phase.
|
||||
|
||||
## Ownership Rules
|
||||
@ -165,7 +163,6 @@ Rules:
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- frontend editing
|
||||
- frontend save policy
|
||||
- completion
|
||||
- rename
|
||||
|
||||
@ -101,6 +101,11 @@ public enum I18n {
|
||||
CODE_EDITOR_STATUS_LANGUAGE("codeEditor.status.language"),
|
||||
CODE_EDITOR_COMMAND_SAVE("codeEditor.command.save"),
|
||||
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_UNSUPPORTED_FILE_TITLE("codeEditor.unsupportedFile.title"),
|
||||
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() {
|
||||
return List.copyOf(openFiles);
|
||||
}
|
||||
@ -41,6 +59,10 @@ public final class EditorOpenFileSession {
|
||||
return find(activePath);
|
||||
}
|
||||
|
||||
public Optional<EditorOpenFileBuffer> file(final Path path) {
|
||||
return find(normalize(path));
|
||||
}
|
||||
|
||||
public boolean hasDirtyEditableFiles() {
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -4,7 +4,11 @@ import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.control.SplitPane;
|
||||
import javafx.scene.layout.*;
|
||||
@ -32,6 +36,12 @@ import java.util.Objects;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
public final class EditorWorkspace extends Workspace {
|
||||
private static final int CARET_SCROLL_CONTEXT_LINES = 2;
|
||||
private static final KeyCombination SAVE_SHORTCUT = new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN);
|
||||
private static final KeyCombination SAVE_ALL_SHORTCUT = new KeyCodeCombination(
|
||||
KeyCode.S,
|
||||
KeyCombination.SHORTCUT_DOWN,
|
||||
KeyCombination.SHIFT_DOWN);
|
||||
private final BorderPane root = new BorderPane();
|
||||
private final CodeArea codeArea = new CodeArea();
|
||||
private final VirtualizedScrollPane<CodeArea> codeScroller = new VirtualizedScrollPane<>(codeArea);
|
||||
@ -59,6 +69,8 @@ public final class EditorWorkspace extends Workspace {
|
||||
List.of());
|
||||
private boolean syncingEditor;
|
||||
private boolean applyingPendingLayoutState;
|
||||
private int activeGuideParagraph = -1;
|
||||
private boolean pendingCaretContextRestore;
|
||||
private Runnable stateChangedAction = () -> { };
|
||||
private ProjectLocalStudioState.EditorLayoutState pendingEditorLayoutState = ProjectLocalStudioState.EditorLayoutState.defaults();
|
||||
private SplitPane contentSplit;
|
||||
@ -81,12 +93,15 @@ public final class EditorWorkspace extends Workspace {
|
||||
codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current));
|
||||
codeArea.caretPositionProperty().addListener((ignored, previous, current) -> {
|
||||
final int caretOffset = current == null ? 0 : current.intValue();
|
||||
updateActiveGuides(caretOffset);
|
||||
updateActiveGuides(caretOffset, codeArea.getCurrentParagraph());
|
||||
refreshStatusBarCaret();
|
||||
});
|
||||
codeArea.estimatedScrollYProperty().addListener((ignored, previous, current) ->
|
||||
restoreCaretScrollContextIfNeeded(previous, current));
|
||||
codeArea.getStyleClass().add("editor-workspace-code-area");
|
||||
codeArea.addEventFilter(KeyEvent.KEY_PRESSED, this::guardInlineHintMutation);
|
||||
codeArea.addEventFilter(KeyEvent.KEY_TYPED, this::guardInlineHintMutation);
|
||||
root.addEventFilter(KeyEvent.KEY_PRESSED, this::handleWorkspaceShortcuts);
|
||||
inlineHintChangeSubscription = codeArea.plainTextChanges().subscribe(change -> {
|
||||
if (syncingEditor) {
|
||||
return;
|
||||
@ -108,6 +123,7 @@ public final class EditorWorkspace extends Workspace {
|
||||
notifyStateChanged();
|
||||
renderSession();
|
||||
});
|
||||
tabStrip.setTabCloseAction(this::requestCloseFile);
|
||||
|
||||
root.setCenter(buildLayout());
|
||||
statusBar.showPlaceholder(presentationRegistry.resolve("text"));
|
||||
@ -218,16 +234,13 @@ public final class EditorWorkspace extends Workspace {
|
||||
codeArea.replaceText(inlineHintProjection.displayText());
|
||||
codeArea.setStyleSpans(0, inlineHintProjection.displayStyles());
|
||||
codeArea.moveTo(0);
|
||||
codeArea.requestFollowCaret();
|
||||
} finally {
|
||||
syncingEditor = false;
|
||||
}
|
||||
activeGuides = scopeGuideModel.resolveActiveGuides(codeArea.getCaretPosition());
|
||||
activeGuideParagraph = codeArea.getCurrentParagraph();
|
||||
refreshParagraphGraphics();
|
||||
Platform.runLater(() -> {
|
||||
codeArea.moveTo(0);
|
||||
codeArea.showParagraphAtTop(0);
|
||||
codeArea.requestFollowCaret();
|
||||
if (fileBuffer.editable()) {
|
||||
codeArea.requestFocus();
|
||||
}
|
||||
@ -261,6 +274,7 @@ public final class EditorWorkspace extends Workspace {
|
||||
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
|
||||
scopeGuideModel = EditorDocumentScopeGuideModel.empty();
|
||||
activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty();
|
||||
activeGuideParagraph = -1;
|
||||
refreshParagraphGraphics();
|
||||
applyPresentationStylesheets(presentation);
|
||||
syncingEditor = true;
|
||||
@ -269,15 +283,9 @@ public final class EditorWorkspace extends Workspace {
|
||||
codeArea.replaceText("");
|
||||
codeArea.setStyleSpans(0, inlineHintProjection.displayStyles());
|
||||
codeArea.moveTo(0);
|
||||
codeArea.requestFollowCaret();
|
||||
} finally {
|
||||
syncingEditor = false;
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
codeArea.moveTo(0);
|
||||
codeArea.showParagraphAtTop(0);
|
||||
codeArea.requestFollowCaret();
|
||||
});
|
||||
codeArea.setEditable(false);
|
||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
||||
saveButton.setDisable(true);
|
||||
@ -295,11 +303,11 @@ public final class EditorWorkspace extends Workspace {
|
||||
}
|
||||
|
||||
private void refreshParagraphGraphics() {
|
||||
codeArea.setParagraphGraphicFactory(paragraphIndex -> EditorDocumentScopeGuideGraphicFactory.create(
|
||||
preserveViewport(() -> codeArea.setParagraphGraphicFactory(paragraphIndex -> EditorDocumentScopeGuideGraphicFactory.create(
|
||||
lineNumberFactory.apply(paragraphIndex),
|
||||
paragraphIndex,
|
||||
scopeGuideModel,
|
||||
activeGuides));
|
||||
activeGuides)));
|
||||
}
|
||||
|
||||
private VBox buildLayout() {
|
||||
@ -346,7 +354,7 @@ public final class EditorWorkspace extends Workspace {
|
||||
private void configureCommandBar() {
|
||||
saveButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE));
|
||||
saveAllButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE_ALL));
|
||||
saveButton.getStyleClass().addAll("studio-button", "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");
|
||||
saveButton.setFocusTraversable(false);
|
||||
saveAllButton.setFocusTraversable(false);
|
||||
@ -378,6 +386,7 @@ public final class EditorWorkspace extends Workspace {
|
||||
final String sourceContent = inlineHintProjection.stripDecorations(content);
|
||||
final VfsDocumentOpenResult.VfsTextDocument updatedDocument = vfsProjectDocument.updateDocument(activeFile.path(), sourceContent);
|
||||
openFileSession.open(bufferFrom(updatedDocument));
|
||||
pendingCaretContextRestore = true;
|
||||
refreshEditableHighlighting(updatedDocument);
|
||||
statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), indentationSetup.statusLabel());
|
||||
tabStrip.showOpenFiles(
|
||||
@ -393,7 +402,33 @@ public final class EditorWorkspace extends Workspace {
|
||||
updatedDocument.content(),
|
||||
presentation.highlight(updatedDocument.content()),
|
||||
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() {
|
||||
@ -415,6 +450,67 @@ public final class EditorWorkspace extends Workspace {
|
||||
renderSession();
|
||||
}
|
||||
|
||||
private void requestCloseFile(final Path path) {
|
||||
final var fileBuffer = openFileSession.file(path).orElse(null);
|
||||
if (fileBuffer == null) {
|
||||
return;
|
||||
}
|
||||
if (fileBuffer.dirty() && !confirmDirtyFileClose(fileBuffer)) {
|
||||
return;
|
||||
}
|
||||
openFileSession.close(path);
|
||||
notifyStateChanged();
|
||||
renderSession();
|
||||
}
|
||||
|
||||
private boolean confirmDirtyFileClose(final EditorOpenFileBuffer fileBuffer) {
|
||||
final var alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
if (root.getScene() != null) {
|
||||
alert.initOwner(root.getScene().getWindow());
|
||||
}
|
||||
final var saveButtonType = new ButtonType(
|
||||
p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_SAVE),
|
||||
ButtonBar.ButtonData.YES);
|
||||
final var discardButtonType = new ButtonType(
|
||||
p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_DISCARD),
|
||||
ButtonBar.ButtonData.NO);
|
||||
final var cancelButtonType = new ButtonType(
|
||||
p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_CANCEL),
|
||||
ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
alert.setTitle(p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_TITLE));
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(p.studio.Container.i18n().format(
|
||||
I18n.CODE_EDITOR_CLOSE_DIRTY_MESSAGE,
|
||||
fileBuffer.tabLabel()));
|
||||
alert.getButtonTypes().setAll(saveButtonType, discardButtonType, cancelButtonType);
|
||||
final var result = alert.showAndWait().orElse(cancelButtonType);
|
||||
if (result == saveButtonType) {
|
||||
vfsProjectDocument.saveDocument(fileBuffer.path());
|
||||
return true;
|
||||
}
|
||||
if (result == discardButtonType) {
|
||||
vfsProjectDocument.discardDocument(fileBuffer.path());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleWorkspaceShortcuts(final KeyEvent event) {
|
||||
if (SAVE_ALL_SHORTCUT.match(event)) {
|
||||
if (!saveAllButton.isDisabled()) {
|
||||
saveAllFiles();
|
||||
}
|
||||
event.consume();
|
||||
return;
|
||||
}
|
||||
if (SAVE_SHORTCUT.match(event)) {
|
||||
if (!saveButton.isDisabled()) {
|
||||
saveActiveFile();
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadOpenFilesFromVfs(final Path activePath) {
|
||||
final var openPaths = openFileSession.openFiles().stream()
|
||||
.map(EditorOpenFileBuffer::path)
|
||||
@ -433,16 +529,7 @@ public final class EditorWorkspace extends Workspace {
|
||||
private void refreshCommandSurfaces(final EditorOpenFileBuffer fileBuffer) {
|
||||
saveButton.setDisable(!fileBuffer.saveEnabled());
|
||||
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();
|
||||
return;
|
||||
}
|
||||
warningStrip.showWarnings(warnings);
|
||||
}
|
||||
|
||||
private EditorOpenFileBuffer bufferFrom(final VfsDocumentOpenResult.VfsTextDocument textDocument) {
|
||||
@ -483,12 +570,17 @@ public final class EditorWorkspace extends Workspace {
|
||||
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);
|
||||
if (Objects.equals(activeGuides, next)) {
|
||||
if (Objects.equals(activeGuides, next) && activeGuideParagraph == currentParagraph) {
|
||||
return;
|
||||
}
|
||||
activeGuides = next;
|
||||
final boolean paragraphChanged = activeGuideParagraph != currentParagraph;
|
||||
activeGuideParagraph = currentParagraph;
|
||||
if (!paragraphChanged) {
|
||||
return;
|
||||
}
|
||||
refreshParagraphGraphics();
|
||||
}
|
||||
|
||||
|
||||
@ -92,6 +92,11 @@ codeEditor.status.indentation=Spaces: 4
|
||||
codeEditor.status.language=Text
|
||||
codeEditor.command.save=Save
|
||||
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.unsupportedFile.title=Unsupported file
|
||||
codeEditor.unsupportedFile.message=This file is not supported in this wave: {0}
|
||||
|
||||
@ -598,6 +598,29 @@
|
||||
-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 {
|
||||
-fx-background-color: #16283d;
|
||||
-fx-border-color: #8fc4f2 #516579 #516579 #516579;
|
||||
@ -612,12 +635,24 @@
|
||||
-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 {
|
||||
-fx-background-color: #2b323d;
|
||||
-fx-border-color: #5b6878;
|
||||
-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 {
|
||||
-fx-background-color: #16283d;
|
||||
-fx-border-color: #8fc4f2 #516579 #516579 #516579;
|
||||
@ -626,6 +661,11 @@
|
||||
-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 {
|
||||
-fx-background-color: #1c3148;
|
||||
-fx-border-color: #a7d7ff #5c738b #5c738b #5c738b;
|
||||
@ -637,12 +677,24 @@
|
||||
-fx-text-fill: #e8f6eb;
|
||||
}
|
||||
|
||||
.editor-workspace-tab-label.editor-workspace-tab-button-editable {
|
||||
-fx-text-fill: #e8f6eb;
|
||||
}
|
||||
|
||||
.editor-workspace-tab-button-editable:hover {
|
||||
-fx-background-color: #29412f;
|
||||
-fx-border-color: #6f957a;
|
||||
-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 {
|
||||
-fx-background-color: #1d3a2a;
|
||||
-fx-border-color: #8ad3a2 #587464 #587464 #587464;
|
||||
@ -656,6 +708,7 @@
|
||||
-fx-border-color: #a5efbd #688676 #688676 #688676;
|
||||
}
|
||||
|
||||
|
||||
.editor-workspace-tab-overflow {
|
||||
-fx-background-radius: 0;
|
||||
-fx-border-radius: 0;
|
||||
@ -1849,3 +1902,7 @@
|
||||
-fx-text-fill: #b9cae0;
|
||||
-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
|
||||
void openAddsNewFileAndMarksItActive() {
|
||||
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);
|
||||
|
||||
@ -24,8 +24,8 @@ final class EditorOpenFileSessionTest {
|
||||
@Test
|
||||
void openDoesNotDuplicateExistingTab() {
|
||||
final var session = new EditorOpenFileSession();
|
||||
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
||||
final var second = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "b");
|
||||
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a");
|
||||
final var second = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "b");
|
||||
|
||||
session.open(first);
|
||||
session.open(second);
|
||||
@ -38,8 +38,8 @@ final class EditorOpenFileSessionTest {
|
||||
@Test
|
||||
void activateSwitchesTheActiveTabWithinTheCurrentSession() {
|
||||
final var session = new EditorOpenFileSession();
|
||||
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
||||
final var second = fileBuffer(Path.of("src/other.pbs"), "other.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "b");
|
||||
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a");
|
||||
final var second = fileBuffer(Path.of("src/other.pbs"), "other.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "b");
|
||||
|
||||
session.open(first);
|
||||
session.open(second);
|
||||
@ -58,20 +58,54 @@ final class EditorOpenFileSessionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void frontendReadOnlyFilesDoNotEnableSaveActions() {
|
||||
void frontendEditableFilesParticipateInSaveStateWhenDirty() {
|
||||
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());
|
||||
assertFalse(session.hasDirtyEditableFiles());
|
||||
assertTrue(session.hasSaveableActiveFile());
|
||||
assertTrue(session.hasDirtyEditableFiles());
|
||||
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
|
||||
void exportsRestorationStateFromOpenTabsAndActiveTab() {
|
||||
final var session = new EditorOpenFileSession();
|
||||
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
||||
final var 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);
|
||||
|
||||
@ -33,6 +33,10 @@ public interface VfsProjectDocument extends AutoCloseable {
|
||||
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() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@ -125,6 +125,13 @@ final class FilesystemVfsProjectDocument implements VfsProjectDocument {
|
||||
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
|
||||
public List<VfsDocumentSaveResult> saveAllDocuments() {
|
||||
return editableSnapshots.keySet().stream()
|
||||
@ -230,7 +237,7 @@ final class FilesystemVfsProjectDocument implements VfsProjectDocument {
|
||||
return new DocumentKind(VfsDocumentTypeIds.BASH, false, VfsDocumentAccessMode.EDITABLE);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ final class FilesystemVfsProjectDocumentTest {
|
||||
assertEquals("main.pbs", document.documentName());
|
||||
assertEquals("pbs", document.typeId());
|
||||
assertEquals("LF", document.lineSeparator());
|
||||
assertEquals(VfsDocumentAccessMode.READ_ONLY, document.accessContext().accessMode());
|
||||
assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode());
|
||||
assertTrue(document.accessContext().frontendDocument());
|
||||
assertTrue(document.content().contains("fn main()"));
|
||||
}
|
||||
@ -92,13 +92,23 @@ final class FilesystemVfsProjectDocumentTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateDocumentRejectsFrontendDocumentsAsHardReadOnly() throws Exception {
|
||||
void frontendDocumentsUseEditableInMemorySnapshotsUntilSavePersistsThem() throws Exception {
|
||||
final Path file = tempDir.resolve("main.pbs");
|
||||
Files.writeString(file, "fn main(): void\n");
|
||||
|
||||
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
|
||||
@ -123,7 +133,23 @@ final class FilesystemVfsProjectDocumentTest {
|
||||
}
|
||||
|
||||
@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 frontend = tempDir.resolve("main.pbs");
|
||||
Files.writeString(editable, "alpha\n");
|
||||
@ -131,14 +157,17 @@ final class FilesystemVfsProjectDocumentTest {
|
||||
|
||||
final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||
vfs.updateDocument(editable, "beta\n");
|
||||
vfs.updateDocument(frontend, "fn main(): int\n");
|
||||
|
||||
final var results = vfs.saveAllDocuments();
|
||||
|
||||
assertEquals(1, results.size());
|
||||
assertEquals(VfsDocumentSaveStatus.SAVED, results.get(0).status());
|
||||
assertEquals(2, results.size());
|
||||
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("fn main(): void\n", Files.readString(frontend));
|
||||
assertEquals("fn main(): int\n", Files.readString(frontend));
|
||||
}
|
||||
|
||||
@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));
|
||||
|
||||
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) {
|
||||
Log.failure("load failed");
|
||||
} else {
|
||||
@ -34,14 +34,14 @@ fn frame() -> void
|
||||
Log.info("state: loading");
|
||||
}
|
||||
} else {
|
||||
let s = Assets.status(loading_handle);
|
||||
let s : int = Assets.status(loading_handle);
|
||||
if (s == 2) {
|
||||
let commit_status = Assets.commit(loading_handle);
|
||||
let commit_status : int = Assets.commit(loading_handle);
|
||||
if (commit_status != 0) {
|
||||
Log.failure("commit failed");
|
||||
}
|
||||
} 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) {
|
||||
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())
|
||||
{
|
||||
@ -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, 1, touch.x() + 16, touch.y() + 8, tile_id, 0, true, false, false, 0);
|
||||
|
||||
let a = 10;
|
||||
let b = 15;
|
||||
let total = a + b;
|
||||
let a : int = 10;
|
||||
let b : int = 15;
|
||||
let total : int = a + b;
|
||||
|
||||
if (Input.pad().a().pressed())
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user