setup process correct behavior
This commit is contained in:
parent
e82d3e90b4
commit
fc96e45435
@ -1,4 +1,4 @@
|
|||||||
{"type":"meta","next_id":{"DSC":20,"AGD":21,"DEC":18,"PLN":39,"LSN":33,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":21,"AGD":22,"DEC":19,"PLN":40,"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"}]}
|
||||||
@ -18,3 +18,4 @@
|
|||||||
{"type":"discussion","id":"DSC-0017","status":"open","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-studio-editor-inline-type-hints-for-let-bindings.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0015","file":"DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03","ref_agenda":"AGD-0018"}],"plans":[{"id":"PLN-0033","file":"PLN-0033-inline-hint-spec-and-contract-propagation.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0034","file":"PLN-0034-lsp-inline-hint-transport-contract.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0035","file":"PLN-0035-pbs-inline-type-hint-payload-production.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0036","file":"PLN-0036-studio-inline-hint-rendering-and-rollout.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]}],"lessons":[]}
|
{"type":"discussion","id":"DSC-0017","status":"open","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-studio-editor-inline-type-hints-for-let-bindings.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0015","file":"DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03","ref_agenda":"AGD-0018"}],"plans":[{"id":"PLN-0033","file":"PLN-0033-inline-hint-spec-and-contract-propagation.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0034","file":"PLN-0034-lsp-inline-hint-transport-contract.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0035","file":"PLN-0035-pbs-inline-type-hint-payload-production.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0036","file":"PLN-0036-studio-inline-hint-rendering-and-rollout.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]}],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0018","status":"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"}]}
|
||||||
|
|||||||
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
id: LSN-0033
|
||||||
|
ticket: studio-editor-indentation-policy-and-project-setup
|
||||||
|
title: Setup-Owned Indentation Policy and Project Bootstrap Defaults
|
||||||
|
created: 2026-04-04
|
||||||
|
tags: [studio, editor, indentation, tabs, setup, dot-studio, project-creation, gitignore]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The Studio editor exposed an indentation chip in the status bar, but that chip was driven by file-content heuristics rather than by a stable editor policy.
|
||||||
|
At the same time, `Tab` insertion did not obey the displayed value, which made the editor self-contradictory during normal editing.
|
||||||
|
|
||||||
|
The work also surfaced a second bootstrap problem: project creation already owned `.studio/` and `.workspace/`, but it was not yet treating project-local setup and baseline ignore rules as first-class bootstrapped assets.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Project-Local Setup Owns Editor Indentation Policy
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
The active indentation policy is now owned by `.studio/setup.json`, loaded into memory once per project session, shown in the status bar, and used directly by editable-document `Tab` handling.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
The status bar must show the active policy, not a moving guess.
|
||||||
|
If `Tab` behavior and the visible chip disagree, the editor loses trust immediately.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
This first wave intentionally stays project-wide.
|
||||||
|
It does not attempt per-file or per-language precedence, and it does not reformat existing files that already diverge from the configured policy.
|
||||||
|
|
||||||
|
### Project Creation Must Bootstrap Real Project-Local Setup
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
The project-creation wizard now includes a dedicated details step before location selection.
|
||||||
|
That step captures indentation width and Prometeu runtime path and persists them into `.studio/setup.json`.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
Indentation policy and runtime path are project-local configuration, so they should exist from project birth rather than appear later as ad hoc repair.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
Project creation becomes a little longer, but the result is a better-initialized project and a cleaner setup boundary.
|
||||||
|
|
||||||
|
### New Projects Must Ship With a Baseline `.gitignore`
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
Project bootstrap now writes a root `.gitignore` with Studio-local state and common OS junk excluded by default.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
`.studio/` and `.workspace/` are local machine artifacts and should not depend on users remembering to ignore them later.
|
||||||
|
The same applies to common macOS, Windows, and Linux metadata noise.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
The generated file is intentionally conservative and generic rather than ecosystem-specific.
|
||||||
|
|
||||||
|
## Final Implementation
|
||||||
|
|
||||||
|
The final state established these rules in code and specs:
|
||||||
|
|
||||||
|
- `ProjectLocalStudioSetup` now carries editor indentation configuration with default resolution to `Spaces: 4`.
|
||||||
|
- `ProjectLocalStudioSetupService` now supports both loading and saving setup data.
|
||||||
|
- `StudioProjectSession` loads setup once and keeps it available in memory for runtime consumers.
|
||||||
|
- `EditorStatusBar` now renders configured indentation policy instead of inspecting file contents.
|
||||||
|
- `EditorWorkspace` converts `Tab` into the configured number of spaces for editable files.
|
||||||
|
- `NewProjectWizard` now captures indentation width and runtime path on a dedicated details step before location.
|
||||||
|
- `ProjectCatalogService` persists `.studio/setup.json` and writes a baseline `.gitignore` during project creation.
|
||||||
|
|
||||||
|
## Patterns and Algorithms
|
||||||
|
|
||||||
|
### Pattern: Policy Chips Must Reflect Runtime Policy, Not Content Heuristics
|
||||||
|
|
||||||
|
If a status-bar chip describes how the editor will behave, it must be backed by the same runtime value that input handling uses.
|
||||||
|
Do not derive policy chips from observed content when the editor behavior is actually driven elsewhere.
|
||||||
|
|
||||||
|
### Pattern: Load Setup Once, Use It Repeatedly
|
||||||
|
|
||||||
|
Project-local setup is durable configuration, not per-keystroke state.
|
||||||
|
Load it at project-session time, keep the resolved value in memory, and let editor interactions consume that in-memory representation directly.
|
||||||
|
|
||||||
|
### Pattern: Bootstrap Project Hygiene at Creation Time
|
||||||
|
|
||||||
|
If a project requires local-only directories such as `.studio/` or `.workspace/`, generate the corresponding `.gitignore` entries when the project is created.
|
||||||
|
Do not push that responsibility onto users later.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Do not let the status-bar indentation chip switch based on whatever indentation currently exists in the open file.
|
||||||
|
- Do not make `Tab` obey one rule while the chip displays another.
|
||||||
|
- Do not reread `.studio/setup.json` on every editing interaction just because the file is durable configuration.
|
||||||
|
- Do not auto-reformat preexisting file content just because a project now has a configured indentation policy.
|
||||||
|
- Do not keep `.studio/` and `.workspace/` out of `.gitignore` and expect users to fix repository hygiene manually.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `docs/specs/studio/1. Studio Shell and Workspace Layout Specification.md`
|
||||||
|
- `docs/specs/studio/5. Code Editor Workspace Specification.md`
|
||||||
|
- `docs/specs/studio/8. Project-Local Studio State Specification.md`
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioSetup.java`
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioSetupService.java`
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/window/NewProjectWizard.java`
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java`
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/projects/ProjectCatalogService.java`
|
||||||
|
|
||||||
|
## Takeaways
|
||||||
|
|
||||||
|
- Editor policy must be explicit, stable, and shared between UI and input handling.
|
||||||
|
- `.studio/setup.json` is the right owner for project-local editor configuration such as indentation and runtime path.
|
||||||
|
- Project creation should bootstrap both configuration and repository hygiene, not leave them as follow-up chores.
|
||||||
@ -35,6 +35,7 @@ Baseline project entry behavior is:
|
|||||||
- show a lightweight launcher or home surface first;
|
- show a lightweight launcher or home surface first;
|
||||||
- show existing or recent projects;
|
- show existing or recent projects;
|
||||||
- expose `Open Project` and `Create Project` as first-class actions;
|
- expose `Open Project` and `Create Project` as first-class actions;
|
||||||
|
- the project-creation flow may include an optional extra-details step for project-local setup such as editor indentation policy;
|
||||||
- enter the main workspace shell only after a concrete project is selected or created.
|
- enter the main workspace shell only after a concrete project is selected or created.
|
||||||
|
|
||||||
## Workspace Model
|
## Workspace Model
|
||||||
|
|||||||
@ -200,6 +200,11 @@ Rules:
|
|||||||
- 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;
|
- 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;
|
||||||
|
- the active indentation policy MUST remain stable while the user edits document contents;
|
||||||
|
- opening a file whose existing indentation diverges from the configured policy MUST NOT trigger automatic reformatting;
|
||||||
- and the workspace must not define local merge/conflict behavior against disk changes.
|
- and the workspace must not define local merge/conflict behavior against disk changes.
|
||||||
|
|
||||||
## Outline Rules
|
## Outline Rules
|
||||||
|
|||||||
@ -61,6 +61,9 @@ Rules:
|
|||||||
- project-local setup MUST live in `.studio/setup.json`;
|
- project-local setup MUST live in `.studio/setup.json`;
|
||||||
- project-local setup MUST be treated as project configuration rather than session state;
|
- project-local setup MUST be treated as project configuration rather than session state;
|
||||||
- project-local setup MAY contain runtime-oriented values such as the Prometeu runtime path;
|
- project-local setup MAY contain runtime-oriented values such as the Prometeu runtime path;
|
||||||
|
- project-local setup MUST own the project-wide editor indentation policy;
|
||||||
|
- the editor indentation policy MUST define at least indentation mode and indentation width;
|
||||||
|
- when setup does not provide an explicit indentation policy, consumers MUST resolve it to `spaces` with width `4`;
|
||||||
- project-local setup MAY grow with additional manual or automatic configuration keys over time;
|
- project-local setup MAY grow with additional manual or automatic configuration keys over time;
|
||||||
- consumers such as `Play` MUST read project-local setup from the dedicated setup file rather than from `.studio/state.json`;
|
- consumers such as `Play` MUST read project-local setup from the dedicated setup file rather than from `.studio/state.json`;
|
||||||
- this revision uses a dry format change and MUST NOT preserve compatibility with the obsolete shape where `projectLocalSetup` was embedded in `.studio/state.json`.
|
- this revision uses a dry format change and MUST NOT preserve compatibility with the obsolete shape where `projectLocalSetup` was embedded in `.studio/state.json`.
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package p.studio.projects;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetupService;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
@ -17,6 +19,7 @@ public final class ProjectCatalogService {
|
|||||||
private static final String MANIFEST_FILE_NAME = "prometeu.json";
|
private static final String MANIFEST_FILE_NAME = "prometeu.json";
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private final Path projectsRoot;
|
private final Path projectsRoot;
|
||||||
|
private final ProjectLocalStudioSetupService projectLocalStudioSetupService = new ProjectLocalStudioSetupService();
|
||||||
|
|
||||||
public ProjectCatalogService(Path projectsRoot) {
|
public ProjectCatalogService(Path projectsRoot) {
|
||||||
this.projectsRoot = Objects.requireNonNull(projectsRoot, "projectsRoot").toAbsolutePath().normalize();
|
this.projectsRoot = Objects.requireNonNull(projectsRoot, "projectsRoot").toAbsolutePath().normalize();
|
||||||
@ -67,11 +70,11 @@ public final class ProjectCatalogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ProjectReference createProject(String projectName) {
|
public ProjectReference createProject(String projectName) {
|
||||||
return createProject(new ProjectCreationRequest(projectName, projectsRoot, "pbs", 1, "src"));
|
return createProject(new ProjectCreationRequest(projectName, projectsRoot, "pbs", 1, "src", 4, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProjectReference createProject(String projectName, Path parentLocation) {
|
public ProjectReference createProject(String projectName, Path parentLocation) {
|
||||||
return createProject(new ProjectCreationRequest(projectName, parentLocation, "pbs", 1, "src"));
|
return createProject(new ProjectCreationRequest(projectName, parentLocation, "pbs", 1, "src", 4, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProjectReference createProject(ProjectCreationRequest request) {
|
public ProjectReference createProject(ProjectCreationRequest request) {
|
||||||
@ -87,6 +90,9 @@ public final class ProjectCatalogService {
|
|||||||
if (request.stdlib() <= 0) {
|
if (request.stdlib() <= 0) {
|
||||||
throw new IllegalArgumentException("project stdlib major must be positive");
|
throw new IllegalArgumentException("project stdlib major must be positive");
|
||||||
}
|
}
|
||||||
|
if (request.indentationWidth() <= 0) {
|
||||||
|
throw new IllegalArgumentException("project indentation width must be positive");
|
||||||
|
}
|
||||||
|
|
||||||
final Path sourceRoot = normalizeSourceRoot(request.sourceRoot());
|
final Path sourceRoot = normalizeSourceRoot(request.sourceRoot());
|
||||||
final Path normalizedParent = Objects.requireNonNull(request.parentLocation(), "request.parentLocation")
|
final Path normalizedParent = Objects.requireNonNull(request.parentLocation(), "request.parentLocation")
|
||||||
@ -104,17 +110,24 @@ public final class ProjectCatalogService {
|
|||||||
try {
|
try {
|
||||||
Files.createDirectories(normalizedParent);
|
Files.createDirectories(normalizedParent);
|
||||||
Files.createDirectories(projectRoot.resolve(".workspace"));
|
Files.createDirectories(projectRoot.resolve(".workspace"));
|
||||||
Files.createDirectories(ProjectStudioPaths.studioRoot(new ProjectReference(
|
final ProjectReference projectReference = new ProjectReference(
|
||||||
displayName,
|
displayName,
|
||||||
"1.0.0",
|
"1.0.0",
|
||||||
languageId,
|
languageId,
|
||||||
request.stdlib(),
|
request.stdlib(),
|
||||||
projectRoot)));
|
projectRoot);
|
||||||
|
Files.createDirectories(ProjectStudioPaths.studioRoot(projectReference));
|
||||||
Files.createDirectories(projectRoot.resolve(sourceRoot));
|
Files.createDirectories(projectRoot.resolve(sourceRoot));
|
||||||
Files.createDirectories(projectRoot.resolve("assets"));
|
Files.createDirectories(projectRoot.resolve("assets"));
|
||||||
Files.createDirectories(projectRoot.resolve("build"));
|
Files.createDirectories(projectRoot.resolve("build"));
|
||||||
Files.createDirectories(projectRoot.resolve("cartridge"));
|
Files.createDirectories(projectRoot.resolve("cartridge"));
|
||||||
Files.writeString(manifestPath(projectRoot), defaultManifest(displayName, languageId, request.stdlib()));
|
Files.writeString(manifestPath(projectRoot), defaultManifest(displayName, languageId, request.stdlib()));
|
||||||
|
Files.writeString(projectRoot.resolve(".gitignore"), defaultGitIgnore());
|
||||||
|
projectLocalStudioSetupService.save(
|
||||||
|
projectReference,
|
||||||
|
new ProjectLocalStudioSetup(
|
||||||
|
request.runtimePath(),
|
||||||
|
new ProjectLocalStudioSetup.EditorIndentationSetup("spaces", request.indentationWidth())));
|
||||||
} catch (IOException ioException) {
|
} catch (IOException ioException) {
|
||||||
throw new UncheckedIOException(ioException);
|
throw new UncheckedIOException(ioException);
|
||||||
}
|
}
|
||||||
@ -188,6 +201,27 @@ public final class ProjectCatalogService {
|
|||||||
""".formatted(projectName, languageId, stdlibMajor);
|
""".formatted(projectName, languageId, stdlibMajor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String defaultGitIgnore() {
|
||||||
|
return """
|
||||||
|
.studio/
|
||||||
|
.workspace/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Linux / Unix desktop metadata
|
||||||
|
.directory
|
||||||
|
*~
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
private record ProjectManifestSummary(String name, String version, String language, String stdlib) {
|
private record ProjectManifestSummary(String name, String version, String language, String stdlib) {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,5 +7,7 @@ public record ProjectCreationRequest(
|
|||||||
Path parentLocation,
|
Path parentLocation,
|
||||||
String languageId,
|
String languageId,
|
||||||
int stdlib,
|
int stdlib,
|
||||||
String sourceRoot) {
|
String sourceRoot,
|
||||||
|
int indentationWidth,
|
||||||
|
String runtimePath) {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package p.studio.projectsessions;
|
package p.studio.projectsessions;
|
||||||
|
|
||||||
import p.studio.lsp.LspService;
|
import p.studio.lsp.LspService;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projectstate.ProjectLocalStudioStateService;
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
@ -13,6 +14,7 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
private final LspService prometeuLspService;
|
private final LspService prometeuLspService;
|
||||||
private final VfsProjectDocument vfsProjectDocument;
|
private final VfsProjectDocument vfsProjectDocument;
|
||||||
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
||||||
|
private final ProjectLocalStudioSetup projectLocalStudioSetup;
|
||||||
private ProjectLocalStudioState projectLocalStudioState;
|
private ProjectLocalStudioState projectLocalStudioState;
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
|
||||||
@ -25,6 +27,7 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
prometeuLspService,
|
prometeuLspService,
|
||||||
vfsProjectDocument,
|
vfsProjectDocument,
|
||||||
new ProjectLocalStudioStateService(),
|
new ProjectLocalStudioStateService(),
|
||||||
|
ProjectLocalStudioSetup.defaults(),
|
||||||
ProjectLocalStudioState.defaults());
|
ProjectLocalStudioState.defaults());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,11 +36,13 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
final LspService prometeuLspService,
|
final LspService prometeuLspService,
|
||||||
final VfsProjectDocument vfsProjectDocument,
|
final VfsProjectDocument vfsProjectDocument,
|
||||||
final ProjectLocalStudioStateService projectLocalStudioStateService,
|
final ProjectLocalStudioStateService projectLocalStudioStateService,
|
||||||
|
final ProjectLocalStudioSetup projectLocalStudioSetup,
|
||||||
final ProjectLocalStudioState projectLocalStudioState) {
|
final ProjectLocalStudioState projectLocalStudioState) {
|
||||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||||
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
|
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
|
||||||
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
|
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
|
||||||
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
||||||
|
this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup");
|
||||||
this.projectLocalStudioState = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState");
|
this.projectLocalStudioState = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +62,10 @@ public final class StudioProjectSession implements AutoCloseable {
|
|||||||
return projectLocalStudioState;
|
return projectLocalStudioState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProjectLocalStudioSetup projectLocalStudioSetup() {
|
||||||
|
return projectLocalStudioSetup;
|
||||||
|
}
|
||||||
|
|
||||||
public void replaceProjectLocalStudioState(final ProjectLocalStudioState nextProjectLocalStudioState) {
|
public void replaceProjectLocalStudioState(final ProjectLocalStudioState nextProjectLocalStudioState) {
|
||||||
this.projectLocalStudioState = Objects.requireNonNull(nextProjectLocalStudioState, "nextProjectLocalStudioState");
|
this.projectLocalStudioState = Objects.requireNonNull(nextProjectLocalStudioState, "nextProjectLocalStudioState");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package p.studio.projectsessions;
|
|||||||
|
|
||||||
import p.studio.lsp.messages.LspProjectContext;
|
import p.studio.lsp.messages.LspProjectContext;
|
||||||
import p.studio.lsp.LspServiceFactory;
|
import p.studio.lsp.LspServiceFactory;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetupService;
|
||||||
import p.studio.projectstate.ProjectLocalStudioStateService;
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.vfs.VfsProjectDocument;
|
import p.studio.vfs.VfsProjectDocument;
|
||||||
@ -13,20 +14,27 @@ public final class StudioProjectSessionFactory {
|
|||||||
private final LspServiceFactory lspServiceFactory;
|
private final LspServiceFactory lspServiceFactory;
|
||||||
private final ProjectDocumentVfsFactory projectDocumentVfsFactory;
|
private final ProjectDocumentVfsFactory projectDocumentVfsFactory;
|
||||||
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
private final ProjectLocalStudioStateService projectLocalStudioStateService;
|
||||||
|
private final ProjectLocalStudioSetupService projectLocalStudioSetupService;
|
||||||
|
|
||||||
public StudioProjectSessionFactory(
|
public StudioProjectSessionFactory(
|
||||||
final LspServiceFactory lspServiceFactory,
|
final LspServiceFactory lspServiceFactory,
|
||||||
final ProjectDocumentVfsFactory projectDocumentVfsFactory) {
|
final ProjectDocumentVfsFactory projectDocumentVfsFactory) {
|
||||||
this(lspServiceFactory, projectDocumentVfsFactory, new ProjectLocalStudioStateService());
|
this(
|
||||||
|
lspServiceFactory,
|
||||||
|
projectDocumentVfsFactory,
|
||||||
|
new ProjectLocalStudioStateService(),
|
||||||
|
new ProjectLocalStudioSetupService());
|
||||||
}
|
}
|
||||||
|
|
||||||
StudioProjectSessionFactory(
|
StudioProjectSessionFactory(
|
||||||
final LspServiceFactory lspServiceFactory,
|
final LspServiceFactory lspServiceFactory,
|
||||||
final ProjectDocumentVfsFactory projectDocumentVfsFactory,
|
final ProjectDocumentVfsFactory projectDocumentVfsFactory,
|
||||||
final ProjectLocalStudioStateService projectLocalStudioStateService) {
|
final ProjectLocalStudioStateService projectLocalStudioStateService,
|
||||||
|
final ProjectLocalStudioSetupService projectLocalStudioSetupService) {
|
||||||
this.lspServiceFactory = Objects.requireNonNull(lspServiceFactory, "prometeuLspServiceFactory");
|
this.lspServiceFactory = Objects.requireNonNull(lspServiceFactory, "prometeuLspServiceFactory");
|
||||||
this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory");
|
this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory");
|
||||||
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
|
||||||
|
this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService");
|
||||||
}
|
}
|
||||||
|
|
||||||
public StudioProjectSession open(final ProjectReference projectReference) {
|
public StudioProjectSession open(final ProjectReference projectReference) {
|
||||||
@ -37,6 +45,7 @@ public final class StudioProjectSessionFactory {
|
|||||||
lspServiceFactory.open(lspProjectContext(target), vfsProjectDocument),
|
lspServiceFactory.open(lspProjectContext(target), vfsProjectDocument),
|
||||||
vfsProjectDocument,
|
vfsProjectDocument,
|
||||||
projectLocalStudioStateService,
|
projectLocalStudioStateService,
|
||||||
|
projectLocalStudioSetupService.load(target),
|
||||||
projectLocalStudioStateService.load(target));
|
projectLocalStudioStateService.load(target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,18 @@ package p.studio.projectstate;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record ProjectLocalStudioSetup(String prometeuRuntimePath) {
|
public record ProjectLocalStudioSetup(
|
||||||
|
String prometeuRuntimePath,
|
||||||
|
EditorIndentationSetup editorIndentation) {
|
||||||
public ProjectLocalStudioSetup {
|
public ProjectLocalStudioSetup {
|
||||||
prometeuRuntimePath = normalizeText(prometeuRuntimePath);
|
prometeuRuntimePath = normalizeText(prometeuRuntimePath);
|
||||||
|
editorIndentation = editorIndentation == null
|
||||||
|
? EditorIndentationSetup.defaults()
|
||||||
|
: editorIndentation.normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProjectLocalStudioSetup defaults() {
|
public static ProjectLocalStudioSetup defaults() {
|
||||||
return new ProjectLocalStudioSetup(null);
|
return new ProjectLocalStudioSetup(null, EditorIndentationSetup.defaults());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String normalizeText(final String value) {
|
private static String normalizeText(final String value) {
|
||||||
@ -19,4 +24,57 @@ public record ProjectLocalStudioSetup(String prometeuRuntimePath) {
|
|||||||
final String trimmed = value.trim();
|
final String trimmed = value.trim();
|
||||||
return trimmed.isEmpty() ? null : trimmed;
|
return trimmed.isEmpty() ? null : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record EditorIndentationSetup(
|
||||||
|
String mode,
|
||||||
|
Integer width) {
|
||||||
|
private static final String DEFAULT_MODE = "spaces";
|
||||||
|
private static final int DEFAULT_WIDTH = 4;
|
||||||
|
|
||||||
|
public EditorIndentationSetup {
|
||||||
|
mode = normalizeMode(mode);
|
||||||
|
width = normalizeWidth(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EditorIndentationSetup defaults() {
|
||||||
|
return new EditorIndentationSetup(DEFAULT_MODE, DEFAULT_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EditorIndentationSetup normalize() {
|
||||||
|
return new EditorIndentationSetup(mode, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String statusLabel() {
|
||||||
|
return switch (mode) {
|
||||||
|
case "tabs" -> "Tabs: " + width;
|
||||||
|
default -> "Spaces: " + width;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public String tabInsertion() {
|
||||||
|
return " ".repeat(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeMode(final String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return DEFAULT_MODE;
|
||||||
|
}
|
||||||
|
final String normalized = value.trim().toLowerCase();
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
return DEFAULT_MODE;
|
||||||
|
}
|
||||||
|
return switch (normalized) {
|
||||||
|
case "tabs", "spaces" -> normalized;
|
||||||
|
default -> DEFAULT_MODE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer normalizeWidth(final Integer value) {
|
||||||
|
if (value == null || value <= 0) {
|
||||||
|
return DEFAULT_WIDTH;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,9 @@ import p.studio.projects.ProjectStudioPaths;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class ProjectLocalStudioSetupService {
|
public class ProjectLocalStudioSetupService {
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
public ProjectLocalStudioSetup load(final ProjectReference projectReference) {
|
public ProjectLocalStudioSetup load(final ProjectReference projectReference) {
|
||||||
@ -24,4 +25,15 @@ public final class ProjectLocalStudioSetupService {
|
|||||||
return ProjectLocalStudioSetup.defaults();
|
return ProjectLocalStudioSetup.defaults();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void save(final ProjectReference projectReference, final ProjectLocalStudioSetup setup) {
|
||||||
|
final Path setupPath = ProjectStudioPaths.setupPath(projectReference);
|
||||||
|
final ProjectLocalStudioSetup normalized = Objects.requireNonNull(setup, "setup");
|
||||||
|
try {
|
||||||
|
Files.createDirectories(setupPath.getParent());
|
||||||
|
MAPPER.writerWithDefaultPrettyPrinter().writeValue(setupPath.toFile(), normalized);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new RuntimeException("unable to save project setup: " + setupPath, ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,22 +49,30 @@ public enum I18n {
|
|||||||
WIZARD_STEP_LANGUAGE_DESCRIPTION("wizard.step.language.description"),
|
WIZARD_STEP_LANGUAGE_DESCRIPTION("wizard.step.language.description"),
|
||||||
WIZARD_STEP_LOCATION_TITLE("wizard.step.location.title"),
|
WIZARD_STEP_LOCATION_TITLE("wizard.step.location.title"),
|
||||||
WIZARD_STEP_LOCATION_DESCRIPTION("wizard.step.location.description"),
|
WIZARD_STEP_LOCATION_DESCRIPTION("wizard.step.location.description"),
|
||||||
|
WIZARD_STEP_DETAILS_TITLE("wizard.step.details.title"),
|
||||||
|
WIZARD_STEP_DETAILS_DESCRIPTION("wizard.step.details.description"),
|
||||||
WIZARD_STEP_CONFIRM_TITLE("wizard.step.confirm.title"),
|
WIZARD_STEP_CONFIRM_TITLE("wizard.step.confirm.title"),
|
||||||
WIZARD_STEP_CONFIRM_DESCRIPTION("wizard.step.confirm.description"),
|
WIZARD_STEP_CONFIRM_DESCRIPTION("wizard.step.confirm.description"),
|
||||||
WIZARD_LANGUAGE_LABEL("wizard.language.label"),
|
WIZARD_LANGUAGE_LABEL("wizard.language.label"),
|
||||||
WIZARD_STDLIB_LABEL("wizard.stdlib.label"),
|
WIZARD_STDLIB_LABEL("wizard.stdlib.label"),
|
||||||
WIZARD_SOURCE_ROOT_LABEL("wizard.sourceRoot.label"),
|
WIZARD_SOURCE_ROOT_LABEL("wizard.sourceRoot.label"),
|
||||||
|
WIZARD_INDENTATION_LABEL("wizard.indentation.label"),
|
||||||
|
WIZARD_RUNTIME_LABEL("wizard.runtime.label"),
|
||||||
|
WIZARD_RUNTIME_PROMPT("wizard.runtime.prompt"),
|
||||||
WIZARD_CONFIRM_NAME("wizard.confirm.name"),
|
WIZARD_CONFIRM_NAME("wizard.confirm.name"),
|
||||||
WIZARD_CONFIRM_LANGUAGE("wizard.confirm.language"),
|
WIZARD_CONFIRM_LANGUAGE("wizard.confirm.language"),
|
||||||
WIZARD_CONFIRM_STDLIB("wizard.confirm.stdlib"),
|
WIZARD_CONFIRM_STDLIB("wizard.confirm.stdlib"),
|
||||||
WIZARD_CONFIRM_SOURCE_ROOT("wizard.confirm.sourceRoot"),
|
WIZARD_CONFIRM_SOURCE_ROOT("wizard.confirm.sourceRoot"),
|
||||||
WIZARD_CONFIRM_LOCATION("wizard.confirm.location"),
|
WIZARD_CONFIRM_LOCATION("wizard.confirm.location"),
|
||||||
|
WIZARD_CONFIRM_INDENTATION("wizard.confirm.indentation"),
|
||||||
|
WIZARD_CONFIRM_RUNTIME("wizard.confirm.runtime"),
|
||||||
WIZARD_CONFIRM_ROOT("wizard.confirm.root"),
|
WIZARD_CONFIRM_ROOT("wizard.confirm.root"),
|
||||||
WIZARD_ERROR_NAME_REQUIRED("wizard.error.nameRequired"),
|
WIZARD_ERROR_NAME_REQUIRED("wizard.error.nameRequired"),
|
||||||
WIZARD_ERROR_LANGUAGE_REQUIRED("wizard.error.languageRequired"),
|
WIZARD_ERROR_LANGUAGE_REQUIRED("wizard.error.languageRequired"),
|
||||||
WIZARD_ERROR_STDLIB_REQUIRED("wizard.error.stdlibRequired"),
|
WIZARD_ERROR_STDLIB_REQUIRED("wizard.error.stdlibRequired"),
|
||||||
WIZARD_ERROR_SOURCE_ROOT_REQUIRED("wizard.error.sourceRootRequired"),
|
WIZARD_ERROR_SOURCE_ROOT_REQUIRED("wizard.error.sourceRootRequired"),
|
||||||
WIZARD_ERROR_LOCATION_REQUIRED("wizard.error.locationRequired"),
|
WIZARD_ERROR_LOCATION_REQUIRED("wizard.error.locationRequired"),
|
||||||
|
WIZARD_ERROR_INDENTATION_REQUIRED("wizard.error.indentationRequired"),
|
||||||
|
|
||||||
TOOLBAR_PLAY("toolbar.play"),
|
TOOLBAR_PLAY("toolbar.play"),
|
||||||
TOOLBAR_STOP("toolbar.stop"),
|
TOOLBAR_STOP("toolbar.stop"),
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package p.studio.window;
|
|||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.controls.shell.*;
|
import p.studio.controls.shell.*;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
import p.studio.lsp.events.StudioWorkspaceSelectedEvent;
|
import p.studio.lsp.events.StudioWorkspaceSelectedEvent;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
@ -31,6 +32,7 @@ public final class MainView extends BorderPane {
|
|||||||
this.projectSession = projectSession;
|
this.projectSession = projectSession;
|
||||||
this.projectReference = projectSession.projectReference();
|
this.projectReference = projectSession.projectReference();
|
||||||
final ProjectLocalStudioState persistedState = projectSession.projectLocalStudioState();
|
final ProjectLocalStudioState persistedState = projectSession.projectLocalStudioState();
|
||||||
|
final ProjectLocalStudioSetup projectSetup = projectSession.projectLocalStudioSetup();
|
||||||
final var menuBar = new StudioShellMenuBarControl();
|
final var menuBar = new StudioShellMenuBarControl();
|
||||||
final var runSurface = new StudioRunSurfaceControl();
|
final var runSurface = new StudioRunSurfaceControl();
|
||||||
setTop(new StudioShellTopBarControl(menuBar));
|
setTop(new StudioShellTopBarControl(menuBar));
|
||||||
@ -40,7 +42,8 @@ public final class MainView extends BorderPane {
|
|||||||
editorWorkspace = new EditorWorkspace(
|
editorWorkspace = new EditorWorkspace(
|
||||||
projectReference,
|
projectReference,
|
||||||
projectSession.projectDocumentVfs(),
|
projectSession.projectDocumentVfs(),
|
||||||
projectSession.prometeuLspService());
|
projectSession.prometeuLspService(),
|
||||||
|
projectSetup.editorIndentation());
|
||||||
host.register(editorWorkspace);
|
host.register(editorWorkspace);
|
||||||
assetWorkspace.restoreProjectLocalState(persistedState);
|
assetWorkspace.restoreProjectLocalState(persistedState);
|
||||||
editorWorkspace.restoreProjectLocalState(persistedState);
|
editorWorkspace.restoreProjectLocalState(persistedState);
|
||||||
|
|||||||
@ -37,9 +37,11 @@ public final class NewProjectWizard {
|
|||||||
private final Button createButton = new Button();
|
private final Button createButton = new Button();
|
||||||
private final TextField projectNameField = new TextField();
|
private final TextField projectNameField = new TextField();
|
||||||
private final TextField locationField = new TextField();
|
private final TextField locationField = new TextField();
|
||||||
|
private final TextField runtimePathField = new TextField();
|
||||||
private final ComboBox<String> languageCombo = new ComboBox<>();
|
private final ComboBox<String> languageCombo = new ComboBox<>();
|
||||||
private final ComboBox<ProjectLanguageTemplate.StdlibOption> stdlibCombo = new ComboBox<>();
|
private final ComboBox<ProjectLanguageTemplate.StdlibOption> stdlibCombo = new ComboBox<>();
|
||||||
private final ComboBox<String> sourceRootCombo = new ComboBox<>();
|
private final ComboBox<String> sourceRootCombo = new ComboBox<>();
|
||||||
|
private final ComboBox<Integer> indentationWidthCombo = new ComboBox<>();
|
||||||
private final Map<String, ProjectLanguageTemplate> languageTemplatesById = new LinkedHashMap<>();
|
private final Map<String, ProjectLanguageTemplate> languageTemplatesById = new LinkedHashMap<>();
|
||||||
private int stepIndex = 0;
|
private int stepIndex = 0;
|
||||||
|
|
||||||
@ -55,6 +57,9 @@ public final class NewProjectWizard {
|
|||||||
|
|
||||||
projectNameField.promptTextProperty().bind(Container.i18n().bind(I18n.LAUNCHER_PROJECT_NAME_PROMPT));
|
projectNameField.promptTextProperty().bind(Container.i18n().bind(I18n.LAUNCHER_PROJECT_NAME_PROMPT));
|
||||||
locationField.setText(projectCatalogService.projectsRoot().toString());
|
locationField.setText(projectCatalogService.projectsRoot().toString());
|
||||||
|
runtimePathField.promptTextProperty().bind(Container.i18n().bind(I18n.WIZARD_RUNTIME_PROMPT));
|
||||||
|
indentationWidthCombo.getItems().setAll(2, 4, 8);
|
||||||
|
indentationWidthCombo.getSelectionModel().select(Integer.valueOf(4));
|
||||||
initializeLanguageTemplates();
|
initializeLanguageTemplates();
|
||||||
|
|
||||||
renderStep();
|
renderStep();
|
||||||
@ -101,16 +106,17 @@ public final class NewProjectWizard {
|
|||||||
private void renderStep() {
|
private void renderStep() {
|
||||||
feedbackLabel.setText("");
|
feedbackLabel.setText("");
|
||||||
backButton.setDisable(stepIndex == 0);
|
backButton.setDisable(stepIndex == 0);
|
||||||
nextButton.setVisible(stepIndex < 3);
|
nextButton.setVisible(stepIndex < 4);
|
||||||
nextButton.setManaged(stepIndex < 3);
|
nextButton.setManaged(stepIndex < 4);
|
||||||
createButton.setVisible(stepIndex == 3);
|
createButton.setVisible(stepIndex == 4);
|
||||||
createButton.setManaged(stepIndex == 3);
|
createButton.setManaged(stepIndex == 4);
|
||||||
|
|
||||||
switch (stepIndex) {
|
switch (stepIndex) {
|
||||||
case 0 -> renderNameStep();
|
case 0 -> renderNameStep();
|
||||||
case 1 -> renderLanguageStep();
|
case 1 -> renderLanguageStep();
|
||||||
case 2 -> renderLocationStep();
|
case 2 -> renderDetailsStep();
|
||||||
case 3 -> renderConfirmStep();
|
case 3 -> renderLocationStep();
|
||||||
|
case 4 -> renderConfirmStep();
|
||||||
default -> throw new IllegalStateException("unknown wizard step: " + stepIndex);
|
default -> throw new IllegalStateException("unknown wizard step: " + stepIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,6 +177,8 @@ public final class NewProjectWizard {
|
|||||||
final Label projectStdlib = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_STDLIB, selectedStdlibLabel()));
|
final Label projectStdlib = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_STDLIB, selectedStdlibLabel()));
|
||||||
final Label projectSourceRoot = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_SOURCE_ROOT, selectedSourceRoot()));
|
final Label projectSourceRoot = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_SOURCE_ROOT, selectedSourceRoot()));
|
||||||
final Label projectLocation = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_LOCATION, locationField.getText().trim()));
|
final Label projectLocation = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_LOCATION, locationField.getText().trim()));
|
||||||
|
final Label projectIndentation = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_INDENTATION, selectedIndentationLabel()));
|
||||||
|
final Label projectRuntime = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_RUNTIME, selectedRuntimePathLabel()));
|
||||||
final Label projectRoot = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_ROOT, resolvedProjectRoot().toString()));
|
final Label projectRoot = new Label(Container.i18n().format(I18n.WIZARD_CONFIRM_ROOT, resolvedProjectRoot().toString()));
|
||||||
|
|
||||||
stepBody.getChildren().setAll(
|
stepBody.getChildren().setAll(
|
||||||
@ -178,10 +186,34 @@ public final class NewProjectWizard {
|
|||||||
projectLanguage,
|
projectLanguage,
|
||||||
projectStdlib,
|
projectStdlib,
|
||||||
projectSourceRoot,
|
projectSourceRoot,
|
||||||
|
projectIndentation,
|
||||||
|
projectRuntime,
|
||||||
projectLocation,
|
projectLocation,
|
||||||
projectRoot);
|
projectRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void renderDetailsStep() {
|
||||||
|
stepTitle.textProperty().unbind();
|
||||||
|
stepDescription.textProperty().unbind();
|
||||||
|
stepTitle.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_DETAILS_TITLE));
|
||||||
|
stepDescription.textProperty().bind(Container.i18n().bind(I18n.WIZARD_STEP_DETAILS_DESCRIPTION));
|
||||||
|
|
||||||
|
final Label indentationLabel = new Label(Container.i18n().text(I18n.WIZARD_INDENTATION_LABEL));
|
||||||
|
final Label runtimeLabel = new Label(Container.i18n().text(I18n.WIZARD_RUNTIME_LABEL));
|
||||||
|
final Button runtimeBrowseButton = new Button();
|
||||||
|
runtimeBrowseButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BROWSE));
|
||||||
|
runtimeBrowseButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||||
|
runtimeBrowseButton.setOnAction(ignored -> browseForRuntimePath());
|
||||||
|
indentationWidthCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
runtimePathField.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
final VBox indentationBox = new VBox(6, indentationLabel, indentationWidthCombo);
|
||||||
|
final HBox runtimeRow = new HBox(12, runtimePathField, runtimeBrowseButton);
|
||||||
|
runtimeRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
HBox.setHgrow(runtimePathField, Priority.ALWAYS);
|
||||||
|
final VBox runtimeBox = new VBox(6, runtimeLabel, runtimeRow);
|
||||||
|
stepBody.getChildren().setAll(indentationBox, runtimeBox);
|
||||||
|
}
|
||||||
|
|
||||||
private void goBack() {
|
private void goBack() {
|
||||||
if (stepIndex == 0) {
|
if (stepIndex == 0) {
|
||||||
return;
|
return;
|
||||||
@ -202,7 +234,8 @@ public final class NewProjectWizard {
|
|||||||
return switch (stepIndex) {
|
return switch (stepIndex) {
|
||||||
case 0 -> validateName();
|
case 0 -> validateName();
|
||||||
case 1 -> validateLanguageSettings();
|
case 1 -> validateLanguageSettings();
|
||||||
case 2 -> validateLocation();
|
case 2 -> validateDetails();
|
||||||
|
case 3 -> validateLocation();
|
||||||
default -> true;
|
default -> true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -246,6 +279,14 @@ public final class NewProjectWizard {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean validateDetails() {
|
||||||
|
if (selectedIndentationWidth() <= 0) {
|
||||||
|
feedbackLabel.setText(Container.i18n().text(I18n.WIZARD_ERROR_INDENTATION_REQUIRED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void finishCreate() {
|
private void finishCreate() {
|
||||||
if (!validateName() || !validateLanguageSettings() || !validateLocation()) {
|
if (!validateName() || !validateLanguageSettings() || !validateLocation()) {
|
||||||
return;
|
return;
|
||||||
@ -257,7 +298,9 @@ public final class NewProjectWizard {
|
|||||||
Path.of(locationField.getText().trim()),
|
Path.of(locationField.getText().trim()),
|
||||||
selectedLanguageId(),
|
selectedLanguageId(),
|
||||||
selectedStdlib().version(),
|
selectedStdlib().version(),
|
||||||
selectedSourceRoot())));
|
selectedSourceRoot(),
|
||||||
|
selectedIndentationWidth(),
|
||||||
|
selectedRuntimePath())));
|
||||||
stage.close();
|
stage.close();
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
feedbackLabel.setText(exception.getMessage());
|
feedbackLabel.setText(exception.getMessage());
|
||||||
@ -282,6 +325,24 @@ public final class NewProjectWizard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void browseForRuntimePath() {
|
||||||
|
final DirectoryChooser chooser = new DirectoryChooser();
|
||||||
|
chooser.setTitle(Container.i18n().text(I18n.WIZARD_RUNTIME_LABEL));
|
||||||
|
|
||||||
|
final String currentText = runtimePathField.getText().trim();
|
||||||
|
if (!currentText.isBlank()) {
|
||||||
|
final File currentDirectory = Path.of(currentText).toFile();
|
||||||
|
if (currentDirectory.isDirectory()) {
|
||||||
|
chooser.setInitialDirectory(currentDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final File selected = chooser.showDialog(stage);
|
||||||
|
if (selected != null) {
|
||||||
|
runtimePathField.setText(selected.toPath().toAbsolutePath().normalize().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String sanitizedName() {
|
private String sanitizedName() {
|
||||||
return projectNameField.getText() == null ? "" : projectNameField.getText().trim();
|
return projectNameField.getText() == null ? "" : projectNameField.getText().trim();
|
||||||
}
|
}
|
||||||
@ -330,6 +391,26 @@ public final class NewProjectWizard {
|
|||||||
return selectedStdlib == null ? "" : selectedStdlib.toString();
|
return selectedStdlib == null ? "" : selectedStdlib.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int selectedIndentationWidth() {
|
||||||
|
return indentationWidthCombo.getValue() == null ? 4 : indentationWidthCombo.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String selectedIndentationLabel() {
|
||||||
|
return "Spaces: " + selectedIndentationWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String selectedRuntimePath() {
|
||||||
|
if (runtimePathField.getText() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String trimmed = runtimePathField.getText().trim();
|
||||||
|
return trimmed.isBlank() ? null : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String selectedRuntimePathLabel() {
|
||||||
|
return selectedRuntimePath() == null ? "-" : selectedRuntimePath();
|
||||||
|
}
|
||||||
|
|
||||||
private Path resolvedProjectRoot() {
|
private Path resolvedProjectRoot() {
|
||||||
return Path.of(locationField.getText().trim()).toAbsolutePath().normalize().resolve(sanitizedName());
|
return Path.of(locationField.getText().trim()).toAbsolutePath().normalize().resolve(sanitizedName());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import java.nio.file.Path;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public final class EditorStatusBar extends HBox {
|
public final class EditorStatusBar extends HBox {
|
||||||
private static final String DEFAULT_INDENTATION = "Spaces: 4";
|
|
||||||
private final HBox breadcrumb = new HBox(6);
|
private final HBox breadcrumb = new HBox(6);
|
||||||
private final Label position = new Label();
|
private final Label position = new Label();
|
||||||
private final Label lineSeparator = new Label();
|
private final Label lineSeparator = new Label();
|
||||||
@ -59,11 +58,12 @@ public final class EditorStatusBar extends HBox {
|
|||||||
public void showFile(
|
public void showFile(
|
||||||
final ProjectReference projectReference,
|
final ProjectReference projectReference,
|
||||||
final EditorOpenFileBuffer fileBuffer,
|
final EditorOpenFileBuffer fileBuffer,
|
||||||
final EditorDocumentPresentation presentation) {
|
final EditorDocumentPresentation presentation,
|
||||||
|
final String indentationPolicyLabel) {
|
||||||
showBreadcrumb(projectReference, fileBuffer.path());
|
showBreadcrumb(projectReference, fileBuffer.path());
|
||||||
showMetadata(true);
|
showMetadata(true);
|
||||||
showPosition(fileBuffer.editable(), 1, 1);
|
showPosition(fileBuffer.editable(), 1, 1);
|
||||||
showDocumentFormatting(fileBuffer.lineSeparator(), fileBuffer.content());
|
showDocumentFormatting(fileBuffer.lineSeparator(), indentationPolicyLabel);
|
||||||
setText(language, fileBuffer.typeId());
|
setText(language, fileBuffer.typeId());
|
||||||
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
|
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
|
||||||
showAccessMode(fileBuffer.readOnly());
|
showAccessMode(fileBuffer.readOnly());
|
||||||
@ -88,9 +88,9 @@ public final class EditorStatusBar extends HBox {
|
|||||||
setText(position, line + ":" + column);
|
setText(position, line + ":" + column);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showDocumentFormatting(final String separator, final String content) {
|
public void showDocumentFormatting(final String separator, final String indentationPolicyLabel) {
|
||||||
setText(lineSeparator, separator);
|
setText(lineSeparator, separator);
|
||||||
setText(indentation, detectIndentation(content));
|
setText(indentation, indentationPolicyLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bindDefault(final Label label, final I18n key) {
|
private void bindDefault(final Label label, final I18n key) {
|
||||||
@ -215,58 +215,4 @@ public final class EditorStatusBar extends HBox {
|
|||||||
label.getStyleClass().add("editor-workspace-status-chip");
|
label.getStyleClass().add("editor-workspace-status-chip");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String detectIndentation(final String content) {
|
|
||||||
final String normalized = content == null ? "" : content.replace("\r\n", "\n").replace('\r', '\n');
|
|
||||||
int spaceGcd = 0;
|
|
||||||
boolean sawTabs = false;
|
|
||||||
for (final String line : normalized.split("\n", -1)) {
|
|
||||||
if (line.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int spaces = 0;
|
|
||||||
int tabs = 0;
|
|
||||||
while (spaces + tabs < line.length()) {
|
|
||||||
final char ch = line.charAt(spaces + tabs);
|
|
||||||
if (ch == ' ') {
|
|
||||||
if (tabs > 0) {
|
|
||||||
spaces = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
spaces++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ch == '\t') {
|
|
||||||
tabs++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (tabs > 0 && spaces == 0) {
|
|
||||||
sawTabs = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (spaces > 1) {
|
|
||||||
spaceGcd = spaceGcd == 0 ? spaces : gcd(spaceGcd, spaces);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sawTabs) {
|
|
||||||
return "Tabs: 1";
|
|
||||||
}
|
|
||||||
if (spaceGcd > 0) {
|
|
||||||
return "Spaces: " + spaceGcd;
|
|
||||||
}
|
|
||||||
return DEFAULT_INDENTATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int gcd(final int left, final int right) {
|
|
||||||
int a = Math.abs(left);
|
|
||||||
int b = Math.abs(right);
|
|
||||||
while (b != 0) {
|
|
||||||
final int next = a % b;
|
|
||||||
a = b;
|
|
||||||
b = next;
|
|
||||||
}
|
|
||||||
return a == 0 ? 1 : a;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import org.reactfx.Subscription;
|
|||||||
import p.studio.lsp.LspService;
|
import p.studio.lsp.LspService;
|
||||||
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
import p.studio.lsp.messages.LspAnalyzeDocumentRequest;
|
||||||
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
|
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
@ -45,6 +46,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
private final EditorDocumentPresentationRegistry presentationRegistry = new EditorDocumentPresentationRegistry();
|
private final EditorDocumentPresentationRegistry presentationRegistry = new EditorDocumentPresentationRegistry();
|
||||||
private final LspService prometeuLspService;
|
private final LspService prometeuLspService;
|
||||||
private final VfsProjectDocument vfsProjectDocument;
|
private final VfsProjectDocument vfsProjectDocument;
|
||||||
|
private final ProjectLocalStudioSetup.EditorIndentationSetup indentationSetup;
|
||||||
private final EditorOpenFileSession openFileSession = new EditorOpenFileSession();
|
private final EditorOpenFileSession openFileSession = new EditorOpenFileSession();
|
||||||
private final Subscription inlineHintChangeSubscription;
|
private final Subscription inlineHintChangeSubscription;
|
||||||
private final List<String> activePresentationStylesheets = new ArrayList<>();
|
private final List<String> activePresentationStylesheets = new ArrayList<>();
|
||||||
@ -66,10 +68,12 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
public EditorWorkspace(
|
public EditorWorkspace(
|
||||||
final ProjectReference projectReference,
|
final ProjectReference projectReference,
|
||||||
final VfsProjectDocument vfsProjectDocument,
|
final VfsProjectDocument vfsProjectDocument,
|
||||||
final LspService prometeuLspService) {
|
final LspService prometeuLspService,
|
||||||
|
final ProjectLocalStudioSetup.EditorIndentationSetup indentationSetup) {
|
||||||
super(projectReference);
|
super(projectReference);
|
||||||
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
|
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
|
||||||
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
|
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
|
||||||
|
this.indentationSetup = Objects.requireNonNull(indentationSetup, "indentationSetup");
|
||||||
root.getStyleClass().add("editor-workspace");
|
root.getStyleClass().add("editor-workspace");
|
||||||
refreshParagraphGraphics();
|
refreshParagraphGraphics();
|
||||||
codeArea.setEditable(false);
|
codeArea.setEditable(false);
|
||||||
@ -231,7 +235,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
codeArea.setEditable(fileBuffer.editable());
|
codeArea.setEditable(fileBuffer.editable());
|
||||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
||||||
refreshCommandSurfaces(fileBuffer);
|
refreshCommandSurfaces(fileBuffer);
|
||||||
statusBar.showFile(projectReference, fileBuffer, presentation);
|
statusBar.showFile(projectReference, fileBuffer, presentation, indentationSetup.statusLabel());
|
||||||
refreshStatusBarCaret();
|
refreshStatusBarCaret();
|
||||||
refreshSemanticOutline(fileBuffer, analysis);
|
refreshSemanticOutline(fileBuffer, analysis);
|
||||||
}
|
}
|
||||||
@ -374,7 +378,8 @@ 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));
|
||||||
statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), updatedDocument.content());
|
refreshEditableHighlighting(updatedDocument);
|
||||||
|
statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), indentationSetup.statusLabel());
|
||||||
tabStrip.showOpenFiles(
|
tabStrip.showOpenFiles(
|
||||||
openFileSession.openFiles(),
|
openFileSession.openFiles(),
|
||||||
openFileSession.activeFile().map(EditorOpenFileBuffer::path).orElse(null));
|
openFileSession.activeFile().map(EditorOpenFileBuffer::path).orElse(null));
|
||||||
@ -382,6 +387,15 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshEditableHighlighting(final VfsDocumentOpenResult.VfsTextDocument updatedDocument) {
|
||||||
|
final EditorDocumentPresentation presentation = presentationRegistry.resolve(updatedDocument.typeId());
|
||||||
|
inlineHintProjection = EditorInlineHintProjection.create(
|
||||||
|
updatedDocument.content(),
|
||||||
|
presentation.highlight(updatedDocument.content()),
|
||||||
|
List.of());
|
||||||
|
codeArea.setStyleSpans(0, inlineHintProjection.displayStyles());
|
||||||
|
}
|
||||||
|
|
||||||
private void saveActiveFile() {
|
private void saveActiveFile() {
|
||||||
openFileSession.activeFile()
|
openFileSession.activeFile()
|
||||||
.filter(EditorOpenFileBuffer::saveEnabled)
|
.filter(EditorOpenFileBuffer::saveEnabled)
|
||||||
@ -505,6 +519,11 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.getCode() == KeyCode.TAB) {
|
||||||
|
event.consume();
|
||||||
|
codeArea.replaceSelection(indentationSetup.tabInsertion());
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.getCode() == KeyCode.BACK_SPACE && inlineHintProjection.containsOffset(Math.max(0, caret - 1))) {
|
if (event.getCode() == KeyCode.BACK_SPACE && inlineHintProjection.containsOffset(Math.max(0, caret - 1))) {
|
||||||
event.consume();
|
event.consume();
|
||||||
codeArea.moveTo(inlineHintProjection.clampCaret(Math.max(0, caret - 1), false));
|
codeArea.moveTo(inlineHintProjection.clampCaret(Math.max(0, caret - 1), false));
|
||||||
|
|||||||
@ -10,15 +10,17 @@ public class EditorDocumentSyntaxHighlightingBash {
|
|||||||
+ "|(?<COMMENT>(?m)(?<!\\S)#[^\\n]*)"
|
+ "|(?<COMMENT>(?m)(?<!\\S)#[^\\n]*)"
|
||||||
+ "|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')"
|
+ "|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')"
|
||||||
+ "|(?<VARIABLE>\\$\\{?[A-Za-z_][A-Za-z0-9_]*}?|\\$[0-9@*#?!$-])"
|
+ "|(?<VARIABLE>\\$\\{?[A-Za-z_][A-Za-z0-9_]*}?|\\$[0-9@*#?!$-])"
|
||||||
|
+ "|(?<TESTBUILTIN>(?m)^[ \\t]*test\\b)"
|
||||||
+ "|(?<COMMAND>(?m)^[ \\t]*[A-Za-z_./-][A-Za-z0-9_./-]*)"
|
+ "|(?<COMMAND>(?m)^[ \\t]*[A-Za-z_./-][A-Za-z0-9_./-]*)"
|
||||||
+ "|(?<KEYWORD>\\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time)\\b)"
|
+ "|(?<KEYWORD>\\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time)\\b)"
|
||||||
+ "|(?<BUILTIN>\\b(?:echo|printf|read|cd|pwd|export|local|readonly|unset|return|shift|source|trap|exit|test)\\b)"
|
+ "|(?<BUILTIN>\\b(?:echo|printf|read|cd|pwd|export|local|readonly|unset|return|shift|source|trap|exit)\\b)"
|
||||||
+ "|(?<OPERATOR>\\|\\||&&|;;|<<-?|>>|[|&;<>~=(){}\\[\\]])"),
|
+ "|(?<OPERATOR>\\|\\||&&|;;|<<-?|>>|[|&;<>~=(){}\\[\\]])"),
|
||||||
List.of(
|
List.of(
|
||||||
new EditorDocumentHighlightToken("SHEBANG", "editor-syntax-bash-shebang"),
|
new EditorDocumentHighlightToken("SHEBANG", "editor-syntax-bash-shebang"),
|
||||||
new EditorDocumentHighlightToken("COMMENT", "editor-syntax-bash-comment"),
|
new EditorDocumentHighlightToken("COMMENT", "editor-syntax-bash-comment"),
|
||||||
new EditorDocumentHighlightToken("STRING", "editor-syntax-bash-string"),
|
new EditorDocumentHighlightToken("STRING", "editor-syntax-bash-string"),
|
||||||
new EditorDocumentHighlightToken("VARIABLE", "editor-syntax-bash-variable"),
|
new EditorDocumentHighlightToken("VARIABLE", "editor-syntax-bash-variable"),
|
||||||
|
new EditorDocumentHighlightToken("TESTBUILTIN", "editor-syntax-bash-builtin"),
|
||||||
new EditorDocumentHighlightToken("KEYWORD", "editor-syntax-bash-keyword"),
|
new EditorDocumentHighlightToken("KEYWORD", "editor-syntax-bash-keyword"),
|
||||||
new EditorDocumentHighlightToken("BUILTIN", "editor-syntax-bash-builtin"),
|
new EditorDocumentHighlightToken("BUILTIN", "editor-syntax-bash-builtin"),
|
||||||
new EditorDocumentHighlightToken("COMMAND", "editor-syntax-bash-command"),
|
new EditorDocumentHighlightToken("COMMAND", "editor-syntax-bash-command"),
|
||||||
|
|||||||
@ -41,22 +41,30 @@ wizard.step.language.title=Language and Project Layout
|
|||||||
wizard.step.language.description=Choose the language, stdlib version, and default source root for the new project.
|
wizard.step.language.description=Choose the language, stdlib version, and default source root for the new project.
|
||||||
wizard.step.location.title=Project Location
|
wizard.step.location.title=Project Location
|
||||||
wizard.step.location.description=Choose the parent directory where the project will be created.
|
wizard.step.location.description=Choose the parent directory where the project will be created.
|
||||||
|
wizard.step.details.title=Extra Details
|
||||||
|
wizard.step.details.description=Configure project-local editor and runtime details before choosing the project location.
|
||||||
wizard.step.confirm.title=Confirm Project Creation
|
wizard.step.confirm.title=Confirm Project Creation
|
||||||
wizard.step.confirm.description=Review the project details before creating it.
|
wizard.step.confirm.description=Review the project details before creating it.
|
||||||
wizard.language.label=Language
|
wizard.language.label=Language
|
||||||
wizard.stdlib.label=Stdlib Version
|
wizard.stdlib.label=Stdlib Version
|
||||||
wizard.sourceRoot.label=Source Root
|
wizard.sourceRoot.label=Source Root
|
||||||
|
wizard.indentation.label=Indentation Width
|
||||||
|
wizard.runtime.label=Prometeu Runtime Path
|
||||||
|
wizard.runtime.prompt=/opt/prometeu/runtime
|
||||||
wizard.confirm.name=Name: {0}
|
wizard.confirm.name=Name: {0}
|
||||||
wizard.confirm.language=Language: {0}
|
wizard.confirm.language=Language: {0}
|
||||||
wizard.confirm.stdlib=Stdlib: {0}
|
wizard.confirm.stdlib=Stdlib: {0}
|
||||||
wizard.confirm.sourceRoot=Source Root: {0}
|
wizard.confirm.sourceRoot=Source Root: {0}
|
||||||
wizard.confirm.location=Location: {0}
|
wizard.confirm.location=Location: {0}
|
||||||
|
wizard.confirm.indentation=Indentation: {0}
|
||||||
|
wizard.confirm.runtime=Runtime: {0}
|
||||||
wizard.confirm.root=Project Root: {0}
|
wizard.confirm.root=Project Root: {0}
|
||||||
wizard.error.nameRequired=Project name is required.
|
wizard.error.nameRequired=Project name is required.
|
||||||
wizard.error.languageRequired=Project language is required.
|
wizard.error.languageRequired=Project language is required.
|
||||||
wizard.error.stdlibRequired=Stdlib major must be a positive integer.
|
wizard.error.stdlibRequired=Stdlib major must be a positive integer.
|
||||||
wizard.error.sourceRootRequired=Source root is required.
|
wizard.error.sourceRootRequired=Source root is required.
|
||||||
wizard.error.locationRequired=Project location is required.
|
wizard.error.locationRequired=Project location is required.
|
||||||
|
wizard.error.indentationRequired=Indentation width must be selected.
|
||||||
|
|
||||||
toolbar.play=Play
|
toolbar.play=Play
|
||||||
toolbar.stop=Stop
|
toolbar.stop=Stop
|
||||||
|
|||||||
@ -40,11 +40,20 @@ final class ProjectCatalogServiceTest {
|
|||||||
assertEquals(1, project.stdlibVersion());
|
assertEquals(1, project.stdlibVersion());
|
||||||
assertTrue(Files.isDirectory(project.rootPath()));
|
assertTrue(Files.isDirectory(project.rootPath()));
|
||||||
assertTrue(Files.isRegularFile(project.rootPath().resolve("prometeu.json")));
|
assertTrue(Files.isRegularFile(project.rootPath().resolve("prometeu.json")));
|
||||||
|
assertTrue(Files.isRegularFile(project.rootPath().resolve(".gitignore")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve(".workspace")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve(".workspace")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve(".studio")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve(".studio")));
|
||||||
|
assertTrue(Files.isRegularFile(project.rootPath().resolve(".studio").resolve("setup.json")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve("src")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve("src")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve("build")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve("build")));
|
||||||
assertTrue(Files.isDirectory(project.rootPath().resolve("cartridge")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve("cartridge")));
|
||||||
|
final String gitIgnore = assertDoesNotThrow(() -> Files.readString(project.rootPath().resolve(".gitignore")));
|
||||||
|
assertTrue(gitIgnore.contains(".studio/"));
|
||||||
|
assertTrue(gitIgnore.contains(".workspace/"));
|
||||||
|
assertTrue(gitIgnore.contains("build/"));
|
||||||
|
assertTrue(gitIgnore.contains(".DS_Store"));
|
||||||
|
assertTrue(gitIgnore.contains("Thumbs.db"));
|
||||||
|
assertTrue(gitIgnore.contains(".directory"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -75,7 +84,9 @@ final class ProjectCatalogServiceTest {
|
|||||||
tempDir,
|
tempDir,
|
||||||
"pbs",
|
"pbs",
|
||||||
7,
|
7,
|
||||||
"code"));
|
"code",
|
||||||
|
8,
|
||||||
|
"/opt/prometeu/runtime"));
|
||||||
|
|
||||||
assertEquals("Wizard Layout Project", project.name());
|
assertEquals("Wizard Layout Project", project.name());
|
||||||
assertEquals("1.0.0", project.version());
|
assertEquals("1.0.0", project.version());
|
||||||
@ -84,8 +95,12 @@ final class ProjectCatalogServiceTest {
|
|||||||
assertTrue(Files.isDirectory(project.rootPath().resolve("code")));
|
assertTrue(Files.isDirectory(project.rootPath().resolve("code")));
|
||||||
assertTrue(Files.notExists(project.rootPath().resolve("src")));
|
assertTrue(Files.notExists(project.rootPath().resolve("src")));
|
||||||
final String manifestJson = Files.readString(project.rootPath().resolve("prometeu.json"));
|
final String manifestJson = Files.readString(project.rootPath().resolve("prometeu.json"));
|
||||||
|
final String setupJson = Files.readString(project.rootPath().resolve(".studio").resolve("setup.json"));
|
||||||
assertTrue(manifestJson.contains("\"language\": \"pbs\""));
|
assertTrue(manifestJson.contains("\"language\": \"pbs\""));
|
||||||
assertTrue(manifestJson.contains("\"stdlib\": \"7\""));
|
assertTrue(manifestJson.contains("\"stdlib\": \"7\""));
|
||||||
|
assertTrue(setupJson.contains("\"prometeuRuntimePath\" : \"/opt/prometeu/runtime\""));
|
||||||
|
assertTrue(setupJson.contains("\"mode\" : \"spaces\""));
|
||||||
|
assertTrue(setupJson.contains("\"width\" : 8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
|
|||||||
import p.studio.lsp.messages.LspProjectContext;
|
import p.studio.lsp.messages.LspProjectContext;
|
||||||
import p.studio.lsp.LspService;
|
import p.studio.lsp.LspService;
|
||||||
import p.studio.lsp.LspServiceFactory;
|
import p.studio.lsp.LspServiceFactory;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetupService;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projectstate.ProjectLocalStudioStateService;
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.lsp.dtos.LspSessionStateDTO;
|
import p.studio.lsp.dtos.LspSessionStateDTO;
|
||||||
@ -30,7 +32,12 @@ final class StudioProjectSessionFactoryTest {
|
|||||||
final RecordingLspFactory lspFactory = new RecordingLspFactory();
|
final RecordingLspFactory lspFactory = new RecordingLspFactory();
|
||||||
final RecordingVfsFactory vfsFactory = new RecordingVfsFactory();
|
final RecordingVfsFactory vfsFactory = new RecordingVfsFactory();
|
||||||
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService();
|
||||||
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(lspFactory, vfsFactory, stateService);
|
final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService();
|
||||||
|
final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory(
|
||||||
|
lspFactory,
|
||||||
|
vfsFactory,
|
||||||
|
stateService,
|
||||||
|
setupService);
|
||||||
final ProjectReference projectReference = new ProjectReference(
|
final ProjectReference projectReference = new ProjectReference(
|
||||||
"Example",
|
"Example",
|
||||||
"1.0.0",
|
"1.0.0",
|
||||||
@ -50,7 +57,9 @@ final class StudioProjectSessionFactoryTest {
|
|||||||
assertEquals("pbs", lspFactory.capturedContext.languageId());
|
assertEquals("pbs", lspFactory.capturedContext.languageId());
|
||||||
assertSame(vfsFactory.vfs, lspFactory.capturedVfs);
|
assertSame(vfsFactory.vfs, lspFactory.capturedVfs);
|
||||||
assertSame(stateService.loadedState, session.projectLocalStudioState());
|
assertSame(stateService.loadedState, session.projectLocalStudioState());
|
||||||
|
assertSame(setupService.loadedSetup, session.projectLocalStudioSetup());
|
||||||
assertSame(projectReference, stateService.loadedProjectReference);
|
assertSame(projectReference, stateService.loadedProjectReference);
|
||||||
|
assertSame(projectReference, setupService.loadedProjectReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class RecordingProjectLocalStudioStateService extends ProjectLocalStudioStateService {
|
private static final class RecordingProjectLocalStudioStateService extends ProjectLocalStudioStateService {
|
||||||
@ -68,6 +77,19 @@ final class StudioProjectSessionFactoryTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class RecordingProjectLocalStudioSetupService extends ProjectLocalStudioSetupService {
|
||||||
|
private ProjectReference loadedProjectReference;
|
||||||
|
private final ProjectLocalStudioSetup loadedSetup = new ProjectLocalStudioSetup(
|
||||||
|
null,
|
||||||
|
new ProjectLocalStudioSetup.EditorIndentationSetup("spaces", 8));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProjectLocalStudioSetup load(final ProjectReference projectReference) {
|
||||||
|
this.loadedProjectReference = projectReference;
|
||||||
|
return loadedSetup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class RecordingVfsFactory implements ProjectDocumentVfsFactory {
|
private static final class RecordingVfsFactory implements ProjectDocumentVfsFactory {
|
||||||
private VfsProjectContext capturedContext;
|
private VfsProjectContext capturedContext;
|
||||||
private final VfsProjectDocument vfs = new NoOpVfsProjectDocument();
|
private final VfsProjectDocument vfs = new NoOpVfsProjectDocument();
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package p.studio.projectsessions;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import p.studio.lsp.messages.LspProjectContext;
|
import p.studio.lsp.messages.LspProjectContext;
|
||||||
import p.studio.lsp.LspService;
|
import p.studio.lsp.LspService;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
import p.studio.projectstate.ProjectLocalStudioStateService;
|
import p.studio.projectstate.ProjectLocalStudioStateService;
|
||||||
import p.studio.lsp.dtos.LspSessionStateDTO;
|
import p.studio.lsp.dtos.LspSessionStateDTO;
|
||||||
@ -33,6 +34,7 @@ final class StudioProjectSessionTest {
|
|||||||
lsp,
|
lsp,
|
||||||
vfs,
|
vfs,
|
||||||
stateService,
|
stateService,
|
||||||
|
ProjectLocalStudioSetup.defaults(),
|
||||||
ProjectLocalStudioState.defaults());
|
ProjectLocalStudioState.defaults());
|
||||||
|
|
||||||
session.close();
|
session.close();
|
||||||
@ -54,6 +56,7 @@ final class StudioProjectSessionTest {
|
|||||||
lsp,
|
lsp,
|
||||||
vfs,
|
vfs,
|
||||||
stateService,
|
stateService,
|
||||||
|
ProjectLocalStudioSetup.defaults(),
|
||||||
ProjectLocalStudioState.defaults());
|
ProjectLocalStudioState.defaults());
|
||||||
final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults()
|
final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults()
|
||||||
.withOpenShellState(new ProjectLocalStudioState.OpenShellState("EDITOR"));
|
.withOpenShellState(new ProjectLocalStudioState.OpenShellState("EDITOR"));
|
||||||
|
|||||||
@ -23,11 +23,19 @@ final class ProjectLocalStudioSetupServiceTest {
|
|||||||
ProjectStudioPaths.setupPath(project),
|
ProjectStudioPaths.setupPath(project),
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"prometeuRuntimePath": "/opt/prometeu/runtime"
|
"prometeuRuntimePath": "/opt/prometeu/runtime",
|
||||||
|
"editorIndentation": {
|
||||||
|
"mode": "spaces",
|
||||||
|
"width": 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
|
|
||||||
assertEquals(new ProjectLocalStudioSetup("/opt/prometeu/runtime"), service.load(project));
|
assertEquals(
|
||||||
|
new ProjectLocalStudioSetup(
|
||||||
|
"/opt/prometeu/runtime",
|
||||||
|
new ProjectLocalStudioSetup.EditorIndentationSetup("spaces", 2)),
|
||||||
|
service.load(project));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -47,6 +55,24 @@ final class ProjectLocalStudioSetupServiceTest {
|
|||||||
assertEquals(ProjectLocalStudioSetup.defaults(), service.load(project));
|
assertEquals(ProjectLocalStudioSetup.defaults(), service.load(project));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveWritesNormalizedDedicatedProjectSetupFile() throws Exception {
|
||||||
|
final ProjectLocalStudioSetupService service = new ProjectLocalStudioSetupService();
|
||||||
|
final ProjectReference project = project("main");
|
||||||
|
|
||||||
|
service.save(
|
||||||
|
project,
|
||||||
|
new ProjectLocalStudioSetup(
|
||||||
|
" /opt/prometeu/runtime ",
|
||||||
|
new ProjectLocalStudioSetup.EditorIndentationSetup("spaces", 8)));
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
new ProjectLocalStudioSetup(
|
||||||
|
"/opt/prometeu/runtime",
|
||||||
|
new ProjectLocalStudioSetup.EditorIndentationSetup("spaces", 8)),
|
||||||
|
service.load(project));
|
||||||
|
}
|
||||||
|
|
||||||
private ProjectReference project(final String name) {
|
private ProjectReference project(final String name) {
|
||||||
final Path projectRoot = tempDir.resolve(name);
|
final Path projectRoot = tempDir.resolve(name);
|
||||||
return new ProjectReference("Main", "1.0.0", "pbs", 1, projectRoot);
|
return new ProjectReference("Main", "1.0.0", "pbs", 1, projectRoot);
|
||||||
|
|||||||
@ -260,6 +260,41 @@ final class EditorDocumentHighlightingRouterTest {
|
|||||||
assertTrue(result.inlineHints().isEmpty());
|
assertTrue(result.inlineHints().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bashBuiltinTestOnlyHighlightsInCommandPosition() {
|
||||||
|
final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry();
|
||||||
|
final EditorOpenFileBuffer commandBuffer = new EditorOpenFileBuffer(
|
||||||
|
Path.of("/tmp/example/run.sh"),
|
||||||
|
"run.sh",
|
||||||
|
"bash",
|
||||||
|
"test -f ./file\n",
|
||||||
|
"LF",
|
||||||
|
false,
|
||||||
|
VfsDocumentAccessMode.EDITABLE,
|
||||||
|
false);
|
||||||
|
final EditorOpenFileBuffer plainTextBuffer = new EditorOpenFileBuffer(
|
||||||
|
Path.of("/tmp/example/run.sh"),
|
||||||
|
"run.sh",
|
||||||
|
"bash",
|
||||||
|
"echo \"$test\"\n",
|
||||||
|
"LF",
|
||||||
|
false,
|
||||||
|
VfsDocumentAccessMode.EDITABLE,
|
||||||
|
false);
|
||||||
|
|
||||||
|
final EditorDocumentHighlightingResult commandResult = EditorDocumentHighlightingRouter.route(
|
||||||
|
commandBuffer,
|
||||||
|
registry.resolve("bash"),
|
||||||
|
null);
|
||||||
|
final EditorDocumentHighlightingResult plainTextResult = EditorDocumentHighlightingRouter.route(
|
||||||
|
plainTextBuffer,
|
||||||
|
registry.resolve("bash"),
|
||||||
|
null);
|
||||||
|
|
||||||
|
assertTrue(containsStyle(commandResult.styleSpans(), "editor-syntax-bash-builtin"));
|
||||||
|
assertFalse(containsStyle(plainTextResult.styleSpans(), "editor-syntax-bash-builtin"));
|
||||||
|
}
|
||||||
|
|
||||||
private boolean containsStyle(
|
private boolean containsStyle(
|
||||||
final org.fxmisc.richtext.model.StyleSpans<Collection<String>> styleSpans,
|
final org.fxmisc.richtext.model.StyleSpans<Collection<String>> styleSpans,
|
||||||
final String styleClass) {
|
final String styleClass) {
|
||||||
|
|||||||
16
test-projects/fragments/.gitignore
vendored
Normal file
16
test-projects/fragments/.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.studio/
|
||||||
|
.workspace/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Linux / Unix desktop metadata
|
||||||
|
.directory
|
||||||
|
*~
|
||||||
16
test-projects/main/.gitignore
vendored
Normal file
16
test-projects/main/.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.studio/
|
||||||
|
.workspace/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Linux / Unix desktop metadata
|
||||||
|
.directory
|
||||||
|
*~
|
||||||
Loading…
x
Reference in New Issue
Block a user