setup process correct behavior

This commit is contained in:
bQUARKz 2026-04-04 10:22:18 +01:00
parent e82d3e90b4
commit fc96e45435
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
25 changed files with 528 additions and 87 deletions

View File

@ -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"}]}

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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`.

View File

@ -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) {
} }

View File

@ -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) {
} }

View File

@ -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");
} }

View File

@ -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));
} }

View File

@ -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;
}
}
} }

View File

@ -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);
}
}
} }

View File

@ -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"),

View File

@ -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);

View File

@ -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());
} }

View File

@ -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;
}
} }

View File

@ -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));

View File

@ -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"),

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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"));

View File

@ -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);

View File

@ -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
View 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
View File

@ -0,0 +1,16 @@
.studio/
.workspace/
build/
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
Desktop.ini
# Linux / Unix desktop metadata
.directory
*~