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-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"}]}
@ -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-0018","status":"done","ticket":"studio-project-local-studio-state-under-dot-studio","title":"Persist project-local Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","shell","layout","setup"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"discussion/lessons/DSC-0018-studio-project-local-studio-state-under-dot-studio/LSN-0031-project-local-studio-state-and-lifecycle-safe-layout-restoration.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"studio-project-local-setup-separate-from-main-studio-state","title":"Separate project-local setup from the main Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","setup","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"discussion/lessons/DSC-0019-studio-project-local-setup-separate-from-main-studio-state/LSN-0032-separate-project-config-from-session-restoration-state.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"studio-editor-indentation-policy-and-project-setup","title":"Indentation Policy, Status-Bar Semantics, and Project-Local Editor Setup","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","editor","indentation","tabs","setup","dot-studio","configuration","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}

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 existing or recent projects;
- 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.
## Workspace Model

View File

@ -200,6 +200,11 @@ Rules:
- the global shell `Save` menu item must not be the save surface for this wave;
- save intent must route through `prometeu-vfs`, which remains the owner of persistence policy;
- a frontend hard `read-only` tab must show a top warning that the file cannot be edited or saved in this wave;
- the active indentation policy for editable documents MUST come from project-local setup rather than from file-content heuristics;
- the status-bar indentation chip MUST display the active configured policy, not inferred file contents;
- pressing `Tab` in an editable document MUST insert spaces according to the active configured indentation width;
- 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.
## Outline Rules

View File

@ -61,6 +61,9 @@ Rules:
- 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 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;
- 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`.

View File

@ -2,6 +2,8 @@ package p.studio.projects;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioSetupService;
import java.io.IOException;
import java.io.UncheckedIOException;
@ -17,6 +19,7 @@ public final class ProjectCatalogService {
private static final String MANIFEST_FILE_NAME = "prometeu.json";
private static final ObjectMapper MAPPER = new ObjectMapper();
private final Path projectsRoot;
private final ProjectLocalStudioSetupService projectLocalStudioSetupService = new ProjectLocalStudioSetupService();
public ProjectCatalogService(Path projectsRoot) {
this.projectsRoot = Objects.requireNonNull(projectsRoot, "projectsRoot").toAbsolutePath().normalize();
@ -67,11 +70,11 @@ public final class ProjectCatalogService {
}
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) {
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) {
@ -87,6 +90,9 @@ public final class ProjectCatalogService {
if (request.stdlib() <= 0) {
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 normalizedParent = Objects.requireNonNull(request.parentLocation(), "request.parentLocation")
@ -104,17 +110,24 @@ public final class ProjectCatalogService {
try {
Files.createDirectories(normalizedParent);
Files.createDirectories(projectRoot.resolve(".workspace"));
Files.createDirectories(ProjectStudioPaths.studioRoot(new ProjectReference(
final ProjectReference projectReference = new ProjectReference(
displayName,
"1.0.0",
languageId,
request.stdlib(),
projectRoot)));
projectRoot);
Files.createDirectories(ProjectStudioPaths.studioRoot(projectReference));
Files.createDirectories(projectRoot.resolve(sourceRoot));
Files.createDirectories(projectRoot.resolve("assets"));
Files.createDirectories(projectRoot.resolve("build"));
Files.createDirectories(projectRoot.resolve("cartridge"));
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) {
throw new UncheckedIOException(ioException);
}
@ -188,6 +201,27 @@ public final class ProjectCatalogService {
""".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)
private record ProjectManifestSummary(String name, String version, String language, String stdlib) {
}

View File

@ -7,5 +7,7 @@ public record ProjectCreationRequest(
Path parentLocation,
String languageId,
int stdlib,
String sourceRoot) {
String sourceRoot,
int indentationWidth,
String runtimePath) {
}

View File

@ -1,6 +1,7 @@
package p.studio.projectsessions;
import p.studio.lsp.LspService;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projectstate.ProjectLocalStudioStateService;
import p.studio.projects.ProjectReference;
@ -13,6 +14,7 @@ public final class StudioProjectSession implements AutoCloseable {
private final LspService prometeuLspService;
private final VfsProjectDocument vfsProjectDocument;
private final ProjectLocalStudioStateService projectLocalStudioStateService;
private final ProjectLocalStudioSetup projectLocalStudioSetup;
private ProjectLocalStudioState projectLocalStudioState;
private boolean closed;
@ -25,6 +27,7 @@ public final class StudioProjectSession implements AutoCloseable {
prometeuLspService,
vfsProjectDocument,
new ProjectLocalStudioStateService(),
ProjectLocalStudioSetup.defaults(),
ProjectLocalStudioState.defaults());
}
@ -33,11 +36,13 @@ public final class StudioProjectSession implements AutoCloseable {
final LspService prometeuLspService,
final VfsProjectDocument vfsProjectDocument,
final ProjectLocalStudioStateService projectLocalStudioStateService,
final ProjectLocalStudioSetup projectLocalStudioSetup,
final ProjectLocalStudioState projectLocalStudioState) {
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup");
this.projectLocalStudioState = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState");
}
@ -57,6 +62,10 @@ public final class StudioProjectSession implements AutoCloseable {
return projectLocalStudioState;
}
public ProjectLocalStudioSetup projectLocalStudioSetup() {
return projectLocalStudioSetup;
}
public void replaceProjectLocalStudioState(final ProjectLocalStudioState 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.LspServiceFactory;
import p.studio.projectstate.ProjectLocalStudioSetupService;
import p.studio.projectstate.ProjectLocalStudioStateService;
import p.studio.projects.ProjectReference;
import p.studio.vfs.VfsProjectDocument;
@ -13,20 +14,27 @@ public final class StudioProjectSessionFactory {
private final LspServiceFactory lspServiceFactory;
private final ProjectDocumentVfsFactory projectDocumentVfsFactory;
private final ProjectLocalStudioStateService projectLocalStudioStateService;
private final ProjectLocalStudioSetupService projectLocalStudioSetupService;
public StudioProjectSessionFactory(
final LspServiceFactory lspServiceFactory,
final ProjectDocumentVfsFactory projectDocumentVfsFactory) {
this(lspServiceFactory, projectDocumentVfsFactory, new ProjectLocalStudioStateService());
this(
lspServiceFactory,
projectDocumentVfsFactory,
new ProjectLocalStudioStateService(),
new ProjectLocalStudioSetupService());
}
StudioProjectSessionFactory(
final LspServiceFactory lspServiceFactory,
final ProjectDocumentVfsFactory projectDocumentVfsFactory,
final ProjectLocalStudioStateService projectLocalStudioStateService) {
final ProjectLocalStudioStateService projectLocalStudioStateService,
final ProjectLocalStudioSetupService projectLocalStudioSetupService) {
this.lspServiceFactory = Objects.requireNonNull(lspServiceFactory, "prometeuLspServiceFactory");
this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory");
this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService");
this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService");
}
public StudioProjectSession open(final ProjectReference projectReference) {
@ -37,6 +45,7 @@ public final class StudioProjectSessionFactory {
lspServiceFactory.open(lspProjectContext(target), vfsProjectDocument),
vfsProjectDocument,
projectLocalStudioStateService,
projectLocalStudioSetupService.load(target),
projectLocalStudioStateService.load(target));
}

View File

@ -3,13 +3,18 @@ package p.studio.projectstate;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public record ProjectLocalStudioSetup(String prometeuRuntimePath) {
public record ProjectLocalStudioSetup(
String prometeuRuntimePath,
EditorIndentationSetup editorIndentation) {
public ProjectLocalStudioSetup {
prometeuRuntimePath = normalizeText(prometeuRuntimePath);
editorIndentation = editorIndentation == null
? EditorIndentationSetup.defaults()
: editorIndentation.normalize();
}
public static ProjectLocalStudioSetup defaults() {
return new ProjectLocalStudioSetup(null);
return new ProjectLocalStudioSetup(null, EditorIndentationSetup.defaults());
}
private static String normalizeText(final String value) {
@ -19,4 +24,57 @@ public record ProjectLocalStudioSetup(String prometeuRuntimePath) {
final String trimmed = value.trim();
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.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
public final class ProjectLocalStudioSetupService {
public class ProjectLocalStudioSetupService {
private static final ObjectMapper MAPPER = new ObjectMapper();
public ProjectLocalStudioSetup load(final ProjectReference projectReference) {
@ -24,4 +25,15 @@ public final class ProjectLocalStudioSetupService {
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_LOCATION_TITLE("wizard.step.location.title"),
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_DESCRIPTION("wizard.step.confirm.description"),
WIZARD_LANGUAGE_LABEL("wizard.language.label"),
WIZARD_STDLIB_LABEL("wizard.stdlib.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_LANGUAGE("wizard.confirm.language"),
WIZARD_CONFIRM_STDLIB("wizard.confirm.stdlib"),
WIZARD_CONFIRM_SOURCE_ROOT("wizard.confirm.sourceRoot"),
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_ERROR_NAME_REQUIRED("wizard.error.nameRequired"),
WIZARD_ERROR_LANGUAGE_REQUIRED("wizard.error.languageRequired"),
WIZARD_ERROR_STDLIB_REQUIRED("wizard.error.stdlibRequired"),
WIZARD_ERROR_SOURCE_ROOT_REQUIRED("wizard.error.sourceRootRequired"),
WIZARD_ERROR_LOCATION_REQUIRED("wizard.error.locationRequired"),
WIZARD_ERROR_INDENTATION_REQUIRED("wizard.error.indentationRequired"),
TOOLBAR_PLAY("toolbar.play"),
TOOLBAR_STOP("toolbar.stop"),

View File

@ -3,6 +3,7 @@ package p.studio.window;
import javafx.scene.layout.BorderPane;
import p.studio.Container;
import p.studio.controls.shell.*;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.lsp.events.StudioWorkspaceSelectedEvent;
import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projects.ProjectReference;
@ -31,6 +32,7 @@ public final class MainView extends BorderPane {
this.projectSession = projectSession;
this.projectReference = projectSession.projectReference();
final ProjectLocalStudioState persistedState = projectSession.projectLocalStudioState();
final ProjectLocalStudioSetup projectSetup = projectSession.projectLocalStudioSetup();
final var menuBar = new StudioShellMenuBarControl();
final var runSurface = new StudioRunSurfaceControl();
setTop(new StudioShellTopBarControl(menuBar));
@ -40,7 +42,8 @@ public final class MainView extends BorderPane {
editorWorkspace = new EditorWorkspace(
projectReference,
projectSession.projectDocumentVfs(),
projectSession.prometeuLspService());
projectSession.prometeuLspService(),
projectSetup.editorIndentation());
host.register(editorWorkspace);
assetWorkspace.restoreProjectLocalState(persistedState);
editorWorkspace.restoreProjectLocalState(persistedState);

View File

@ -37,9 +37,11 @@ public final class NewProjectWizard {
private final Button createButton = new Button();
private final TextField projectNameField = new TextField();
private final TextField locationField = new TextField();
private final TextField runtimePathField = new TextField();
private final ComboBox<String> languageCombo = new ComboBox<>();
private final ComboBox<ProjectLanguageTemplate.StdlibOption> stdlibCombo = 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 int stepIndex = 0;
@ -55,6 +57,9 @@ public final class NewProjectWizard {
projectNameField.promptTextProperty().bind(Container.i18n().bind(I18n.LAUNCHER_PROJECT_NAME_PROMPT));
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();
renderStep();
@ -101,16 +106,17 @@ public final class NewProjectWizard {
private void renderStep() {
feedbackLabel.setText("");
backButton.setDisable(stepIndex == 0);
nextButton.setVisible(stepIndex < 3);
nextButton.setManaged(stepIndex < 3);
createButton.setVisible(stepIndex == 3);
createButton.setManaged(stepIndex == 3);
nextButton.setVisible(stepIndex < 4);
nextButton.setManaged(stepIndex < 4);
createButton.setVisible(stepIndex == 4);
createButton.setManaged(stepIndex == 4);
switch (stepIndex) {
case 0 -> renderNameStep();
case 1 -> renderLanguageStep();
case 2 -> renderLocationStep();
case 3 -> renderConfirmStep();
case 2 -> renderDetailsStep();
case 3 -> renderLocationStep();
case 4 -> renderConfirmStep();
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 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 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()));
stepBody.getChildren().setAll(
@ -178,10 +186,34 @@ public final class NewProjectWizard {
projectLanguage,
projectStdlib,
projectSourceRoot,
projectIndentation,
projectRuntime,
projectLocation,
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() {
if (stepIndex == 0) {
return;
@ -202,7 +234,8 @@ public final class NewProjectWizard {
return switch (stepIndex) {
case 0 -> validateName();
case 1 -> validateLanguageSettings();
case 2 -> validateLocation();
case 2 -> validateDetails();
case 3 -> validateLocation();
default -> true;
};
}
@ -246,6 +279,14 @@ public final class NewProjectWizard {
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() {
if (!validateName() || !validateLanguageSettings() || !validateLocation()) {
return;
@ -257,7 +298,9 @@ public final class NewProjectWizard {
Path.of(locationField.getText().trim()),
selectedLanguageId(),
selectedStdlib().version(),
selectedSourceRoot())));
selectedSourceRoot(),
selectedIndentationWidth(),
selectedRuntimePath())));
stage.close();
} catch (IllegalArgumentException exception) {
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() {
return projectNameField.getText() == null ? "" : projectNameField.getText().trim();
}
@ -330,6 +391,26 @@ public final class NewProjectWizard {
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() {
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;
public final class EditorStatusBar extends HBox {
private static final String DEFAULT_INDENTATION = "Spaces: 4";
private final HBox breadcrumb = new HBox(6);
private final Label position = new Label();
private final Label lineSeparator = new Label();
@ -59,11 +58,12 @@ public final class EditorStatusBar extends HBox {
public void showFile(
final ProjectReference projectReference,
final EditorOpenFileBuffer fileBuffer,
final EditorDocumentPresentation presentation) {
final EditorDocumentPresentation presentation,
final String indentationPolicyLabel) {
showBreadcrumb(projectReference, fileBuffer.path());
showMetadata(true);
showPosition(fileBuffer.editable(), 1, 1);
showDocumentFormatting(fileBuffer.lineSeparator(), fileBuffer.content());
showDocumentFormatting(fileBuffer.lineSeparator(), indentationPolicyLabel);
setText(language, fileBuffer.typeId());
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
showAccessMode(fileBuffer.readOnly());
@ -88,9 +88,9 @@ public final class EditorStatusBar extends HBox {
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(indentation, detectIndentation(content));
setText(indentation, indentationPolicyLabel);
}
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");
}
}
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.messages.LspAnalyzeDocumentRequest;
import p.studio.lsp.messages.LspAnalyzeDocumentResult;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
@ -45,6 +46,7 @@ public final class EditorWorkspace extends Workspace {
private final EditorDocumentPresentationRegistry presentationRegistry = new EditorDocumentPresentationRegistry();
private final LspService prometeuLspService;
private final VfsProjectDocument vfsProjectDocument;
private final ProjectLocalStudioSetup.EditorIndentationSetup indentationSetup;
private final EditorOpenFileSession openFileSession = new EditorOpenFileSession();
private final Subscription inlineHintChangeSubscription;
private final List<String> activePresentationStylesheets = new ArrayList<>();
@ -66,10 +68,12 @@ public final class EditorWorkspace extends Workspace {
public EditorWorkspace(
final ProjectReference projectReference,
final VfsProjectDocument vfsProjectDocument,
final LspService prometeuLspService) {
final LspService prometeuLspService,
final ProjectLocalStudioSetup.EditorIndentationSetup indentationSetup) {
super(projectReference);
this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument");
this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService");
this.indentationSetup = Objects.requireNonNull(indentationSetup, "indentationSetup");
root.getStyleClass().add("editor-workspace");
refreshParagraphGraphics();
codeArea.setEditable(false);
@ -231,7 +235,7 @@ public final class EditorWorkspace extends Workspace {
codeArea.setEditable(fileBuffer.editable());
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
refreshCommandSurfaces(fileBuffer);
statusBar.showFile(projectReference, fileBuffer, presentation);
statusBar.showFile(projectReference, fileBuffer, presentation, indentationSetup.statusLabel());
refreshStatusBarCaret();
refreshSemanticOutline(fileBuffer, analysis);
}
@ -374,7 +378,8 @@ public final class EditorWorkspace extends Workspace {
final String sourceContent = inlineHintProjection.stripDecorations(content);
final VfsDocumentOpenResult.VfsTextDocument updatedDocument = vfsProjectDocument.updateDocument(activeFile.path(), sourceContent);
openFileSession.open(bufferFrom(updatedDocument));
statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), updatedDocument.content());
refreshEditableHighlighting(updatedDocument);
statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), indentationSetup.statusLabel());
tabStrip.showOpenFiles(
openFileSession.openFiles(),
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() {
openFileSession.activeFile()
.filter(EditorOpenFileBuffer::saveEnabled)
@ -505,6 +519,11 @@ public final class EditorWorkspace extends Workspace {
}
return;
}
if (event.getCode() == KeyCode.TAB) {
event.consume();
codeArea.replaceSelection(indentationSetup.tabInsertion());
return;
}
if (event.getCode() == KeyCode.BACK_SPACE && inlineHintProjection.containsOffset(Math.max(0, caret - 1))) {
event.consume();
codeArea.moveTo(inlineHintProjection.clampCaret(Math.max(0, caret - 1), false));

View File

@ -10,15 +10,17 @@ public class EditorDocumentSyntaxHighlightingBash {
+ "|(?<COMMENT>(?m)(?<!\\S)#[^\\n]*)"
+ "|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')"
+ "|(?<VARIABLE>\\$\\{?[A-Za-z_][A-Za-z0-9_]*}?|\\$[0-9@*#?!$-])"
+ "|(?<TESTBUILTIN>(?m)^[ \\t]*test\\b)"
+ "|(?<COMMAND>(?m)^[ \\t]*[A-Za-z_./-][A-Za-z0-9_./-]*)"
+ "|(?<KEYWORD>\\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time)\\b)"
+ "|(?<BUILTIN>\\b(?:echo|printf|read|cd|pwd|export|local|readonly|unset|return|shift|source|trap|exit|test)\\b)"
+ "|(?<BUILTIN>\\b(?:echo|printf|read|cd|pwd|export|local|readonly|unset|return|shift|source|trap|exit)\\b)"
+ "|(?<OPERATOR>\\|\\||&&|;;|<<-?|>>|[|&;<>~=(){}\\[\\]])"),
List.of(
new EditorDocumentHighlightToken("SHEBANG", "editor-syntax-bash-shebang"),
new EditorDocumentHighlightToken("COMMENT", "editor-syntax-bash-comment"),
new EditorDocumentHighlightToken("STRING", "editor-syntax-bash-string"),
new EditorDocumentHighlightToken("VARIABLE", "editor-syntax-bash-variable"),
new EditorDocumentHighlightToken("TESTBUILTIN", "editor-syntax-bash-builtin"),
new EditorDocumentHighlightToken("KEYWORD", "editor-syntax-bash-keyword"),
new EditorDocumentHighlightToken("BUILTIN", "editor-syntax-bash-builtin"),
new EditorDocumentHighlightToken("COMMAND", "editor-syntax-bash-command"),

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.location.title=Project Location
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.description=Review the project details before creating it.
wizard.language.label=Language
wizard.stdlib.label=Stdlib Version
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.language=Language: {0}
wizard.confirm.stdlib=Stdlib: {0}
wizard.confirm.sourceRoot=Source Root: {0}
wizard.confirm.location=Location: {0}
wizard.confirm.indentation=Indentation: {0}
wizard.confirm.runtime=Runtime: {0}
wizard.confirm.root=Project Root: {0}
wizard.error.nameRequired=Project name is required.
wizard.error.languageRequired=Project language is required.
wizard.error.stdlibRequired=Stdlib major must be a positive integer.
wizard.error.sourceRootRequired=Source root is required.
wizard.error.locationRequired=Project location is required.
wizard.error.indentationRequired=Indentation width must be selected.
toolbar.play=Play
toolbar.stop=Stop

View File

@ -40,11 +40,20 @@ final class ProjectCatalogServiceTest {
assertEquals(1, project.stdlibVersion());
assertTrue(Files.isDirectory(project.rootPath()));
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(".studio")));
assertTrue(Files.isRegularFile(project.rootPath().resolve(".studio").resolve("setup.json")));
assertTrue(Files.isDirectory(project.rootPath().resolve("src")));
assertTrue(Files.isDirectory(project.rootPath().resolve("build")));
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
@ -75,7 +84,9 @@ final class ProjectCatalogServiceTest {
tempDir,
"pbs",
7,
"code"));
"code",
8,
"/opt/prometeu/runtime"));
assertEquals("Wizard Layout Project", project.name());
assertEquals("1.0.0", project.version());
@ -84,8 +95,12 @@ final class ProjectCatalogServiceTest {
assertTrue(Files.isDirectory(project.rootPath().resolve("code")));
assertTrue(Files.notExists(project.rootPath().resolve("src")));
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("\"stdlib\": \"7\""));
assertTrue(setupJson.contains("\"prometeuRuntimePath\" : \"/opt/prometeu/runtime\""));
assertTrue(setupJson.contains("\"mode\" : \"spaces\""));
assertTrue(setupJson.contains("\"width\" : 8"));
}
@Test

View File

@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.LspService;
import p.studio.lsp.LspServiceFactory;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioSetupService;
import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projectstate.ProjectLocalStudioStateService;
import p.studio.lsp.dtos.LspSessionStateDTO;
@ -30,7 +32,12 @@ final class StudioProjectSessionFactoryTest {
final RecordingLspFactory lspFactory = new RecordingLspFactory();
final RecordingVfsFactory vfsFactory = new RecordingVfsFactory();
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(
"Example",
"1.0.0",
@ -50,7 +57,9 @@ final class StudioProjectSessionFactoryTest {
assertEquals("pbs", lspFactory.capturedContext.languageId());
assertSame(vfsFactory.vfs, lspFactory.capturedVfs);
assertSame(stateService.loadedState, session.projectLocalStudioState());
assertSame(setupService.loadedSetup, session.projectLocalStudioSetup());
assertSame(projectReference, stateService.loadedProjectReference);
assertSame(projectReference, setupService.loadedProjectReference);
}
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 VfsProjectContext capturedContext;
private final VfsProjectDocument vfs = new NoOpVfsProjectDocument();

View File

@ -3,6 +3,7 @@ package p.studio.projectsessions;
import org.junit.jupiter.api.Test;
import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.LspService;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioState;
import p.studio.projectstate.ProjectLocalStudioStateService;
import p.studio.lsp.dtos.LspSessionStateDTO;
@ -33,6 +34,7 @@ final class StudioProjectSessionTest {
lsp,
vfs,
stateService,
ProjectLocalStudioSetup.defaults(),
ProjectLocalStudioState.defaults());
session.close();
@ -54,6 +56,7 @@ final class StudioProjectSessionTest {
lsp,
vfs,
stateService,
ProjectLocalStudioSetup.defaults(),
ProjectLocalStudioState.defaults());
final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults()
.withOpenShellState(new ProjectLocalStudioState.OpenShellState("EDITOR"));

View File

@ -23,11 +23,19 @@ final class ProjectLocalStudioSetupServiceTest {
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
@ -47,6 +55,24 @@ final class ProjectLocalStudioSetupServiceTest {
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) {
final Path projectRoot = tempDir.resolve(name);
return new ProjectReference("Main", "1.0.0", "pbs", 1, projectRoot);

View File

@ -260,6 +260,41 @@ final class EditorDocumentHighlightingRouterTest {
assertTrue(result.inlineHints().isEmpty());
}
@Test
void bashBuiltinTestOnlyHighlightsInCommandPosition() {
final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry();
final EditorOpenFileBuffer commandBuffer = new EditorOpenFileBuffer(
Path.of("/tmp/example/run.sh"),
"run.sh",
"bash",
"test -f ./file\n",
"LF",
false,
VfsDocumentAccessMode.EDITABLE,
false);
final EditorOpenFileBuffer plainTextBuffer = new EditorOpenFileBuffer(
Path.of("/tmp/example/run.sh"),
"run.sh",
"bash",
"echo \"$test\"\n",
"LF",
false,
VfsDocumentAccessMode.EDITABLE,
false);
final EditorDocumentHighlightingResult commandResult = EditorDocumentHighlightingRouter.route(
commandBuffer,
registry.resolve("bash"),
null);
final EditorDocumentHighlightingResult plainTextResult = EditorDocumentHighlightingRouter.route(
plainTextBuffer,
registry.resolve("bash"),
null);
assertTrue(containsStyle(commandResult.styleSpans(), "editor-syntax-bash-builtin"));
assertFalse(containsStyle(plainTextResult.styleSpans(), "editor-syntax-bash-builtin"));
}
private boolean containsStyle(
final org.fxmisc.richtext.model.StyleSpans<Collection<String>> styleSpans,
final String styleClass) {

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
*~