implements PLN-0021

This commit is contained in:
bQUARKz 2026-03-31 16:51:35 +01:00
parent f9a47bbdbf
commit b3097cfaf7
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 300 additions and 29 deletions

View File

@ -11,4 +11,4 @@
{"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]}
{"type":"discussion","id":"DSC-0012","status":"done","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
{"type":"discussion","id":"DSC-0013","status":"open","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[{"id":"AGD-0013","file":"AGD-0013-studio-editor-write-wave-supported-non-frontend-files.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"},{"id":"AGD-0014","file":"AGD-0014-studio-editor-frontend-edit-rights.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0010","file":"DEC-0010-studio-controlled-non-frontend-editor-write-wave.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0013"},{"id":"DEC-0011","file":"DEC-0011-studio-frontend-read-only-minimum-lsp-phase.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0014"}],"plans":[{"id":"PLN-0019","file":"PLN-0019-propagate-dec-0010-into-studio-and-vfs-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0020","file":"PLN-0020-build-dec-0010-vfs-access-policy-and-save-core.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0021","file":"PLN-0021-integrate-dec-0010-editor-write-ui-and-workflow.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0022","file":"PLN-0022-propagate-dec-0011-into-studio-vfs-and-lsp-specs.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0023","file":"PLN-0023-scaffold-flat-packed-prometeu-lsp-api-and-session-seams.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0024","file":"PLN-0024-implement-read-only-fe-diagnostics-symbols-and-definition.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0025","file":"PLN-0025-implement-fe-semantic-highlight-consumption.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0013","status":"open","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[{"id":"AGD-0013","file":"AGD-0013-studio-editor-write-wave-supported-non-frontend-files.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"},{"id":"AGD-0014","file":"AGD-0014-studio-editor-frontend-edit-rights.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0010","file":"DEC-0010-studio-controlled-non-frontend-editor-write-wave.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0013"},{"id":"DEC-0011","file":"DEC-0011-studio-frontend-read-only-minimum-lsp-phase.md","status":"accepted","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0014"}],"plans":[{"id":"PLN-0019","file":"PLN-0019-propagate-dec-0010-into-studio-and-vfs-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0020","file":"PLN-0020-build-dec-0010-vfs-access-policy-and-save-core.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0021","file":"PLN-0021-integrate-dec-0010-editor-write-ui-and-workflow.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0010"]},{"id":"PLN-0022","file":"PLN-0022-propagate-dec-0011-into-studio-vfs-and-lsp-specs.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0023","file":"PLN-0023-scaffold-flat-packed-prometeu-lsp-api-and-session-seams.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0024","file":"PLN-0024-implement-read-only-fe-diagnostics-symbols-and-definition.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]},{"id":"PLN-0025","file":"PLN-0025-implement-fe-semantic-highlight-consumption.md","status":"review","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0011"]}],"lessons":[]}

View File

@ -2,9 +2,9 @@
id: PLN-0021
ticket: studio-editor-write-wave-supported-non-frontend-files
title: Integrate DEC-0010 editor write UI and workflow in Studio
status: review
status: done
created: 2026-03-31
completed:
completed: 2026-03-31
tags: [studio, editor, ui, save, read-only, write-wave]
---

View File

@ -87,7 +87,11 @@ public enum I18n {
CODE_EDITOR_STATUS_LINE_SEPARATOR("codeEditor.status.lineSeparator"),
CODE_EDITOR_STATUS_INDENTATION("codeEditor.status.indentation"),
CODE_EDITOR_STATUS_LANGUAGE("codeEditor.status.language"),
CODE_EDITOR_STATUS_EDITABLE("codeEditor.status.editable"),
CODE_EDITOR_STATUS_READ_ONLY("codeEditor.status.readOnly"),
CODE_EDITOR_COMMAND_SAVE("codeEditor.command.save"),
CODE_EDITOR_COMMAND_SAVE_ALL("codeEditor.command.saveAll"),
CODE_EDITOR_WARNING_FRONTEND_READ_ONLY("codeEditor.warning.frontendReadOnly"),
CODE_EDITOR_UNSUPPORTED_FILE_TITLE("codeEditor.unsupportedFile.title"),
CODE_EDITOR_UNSUPPORTED_FILE_MESSAGE("codeEditor.unsupportedFile.message"),

View File

@ -1,5 +1,7 @@
package p.studio.workspaces.editor;
import p.studio.vfs.VfsDocumentAccessMode;
import java.nio.file.Path;
import java.util.Objects;
@ -8,12 +10,28 @@ public record EditorOpenFileBuffer(
String tabLabel,
String typeId,
String content,
String lineSeparator) {
String lineSeparator,
boolean frontendDocument,
VfsDocumentAccessMode accessMode,
boolean dirty) {
public EditorOpenFileBuffer {
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
tabLabel = Objects.requireNonNull(tabLabel, "tabLabel");
typeId = Objects.requireNonNull(typeId, "typeId");
content = Objects.requireNonNull(content, "content");
lineSeparator = Objects.requireNonNull(lineSeparator, "lineSeparator");
Objects.requireNonNull(accessMode, "accessMode");
}
public boolean editable() {
return accessMode == VfsDocumentAccessMode.EDITABLE;
}
public boolean readOnly() {
return !editable();
}
public boolean saveEnabled() {
return editable() && dirty;
}
}

View File

@ -39,6 +39,14 @@ public final class EditorOpenFileSession {
return find(activePath);
}
public boolean hasDirtyEditableFiles() {
return openFiles.stream().anyMatch(EditorOpenFileBuffer::saveEnabled);
}
public boolean hasSaveableActiveFile() {
return activeFile().map(EditorOpenFileBuffer::saveEnabled).orElse(false);
}
public boolean isEmpty() {
return openFiles.isEmpty();
}

View File

@ -66,7 +66,7 @@ public final class EditorStatusBar extends HBox {
bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION);
setText(language, fileBuffer.typeId());
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY);
showAccessMode(fileBuffer);
}
public void showPlaceholder(final EditorDocumentPresentation presentation) {
@ -78,6 +78,10 @@ public final class EditorStatusBar extends HBox {
bindDefault(language, I18n.CODE_EDITOR_STATUS_LANGUAGE);
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY);
readOnly.getStyleClass().remove("editor-workspace-status-chip-editable");
if (!readOnly.getStyleClass().contains("editor-workspace-status-chip-read-only")) {
readOnly.getStyleClass().add("editor-workspace-status-chip-read-only");
}
}
private void bindDefault(final Label label, final I18n key) {
@ -174,6 +178,23 @@ public final class EditorStatusBar extends HBox {
setVisibleManaged(readOnly, visible);
}
private void showAccessMode(final EditorOpenFileBuffer fileBuffer) {
if (fileBuffer.readOnly()) {
bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY);
readOnly.getStyleClass().remove("editor-workspace-status-chip-editable");
if (!readOnly.getStyleClass().contains("editor-workspace-status-chip-read-only")) {
readOnly.getStyleClass().add("editor-workspace-status-chip-read-only");
}
return;
}
bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_EDITABLE);
readOnly.getStyleClass().remove("editor-workspace-status-chip-read-only");
if (!readOnly.getStyleClass().contains("editor-workspace-status-chip-editable")) {
readOnly.getStyleClass().add("editor-workspace-status-chip-editable");
}
}
private void setVisibleManaged(final Label label, final boolean visible) {
label.setVisible(visible);
label.setManaged(visible);

View File

@ -85,8 +85,11 @@ public final class EditorTabStrip extends HBox {
tabButton.getStyleClass().addAll(
"studio-button",
"studio-button-secondary",
"editor-workspace-tab-button",
"editor-workspace-tab-button-read-only");
"editor-workspace-tab-button");
tabButton.getStyleClass().add(
fileBuffer.readOnly()
? "editor-workspace-tab-button-read-only"
: "editor-workspace-tab-button-editable");
applyTabMetrics(tabButton);
if (fileBuffer.path().equals(activePath)) {
tabButton.getStyleClass().add("editor-workspace-tab-button-active");

View File

@ -1,22 +1,27 @@
package p.studio.workspaces.editor;
import javafx.scene.control.Alert;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.Workspace;
import p.studio.workspaces.WorkspaceId;
import p.studio.vfs.ProjectDocumentVfs;
import p.studio.vfs.VfsDocumentOpenResult;
import p.studio.vfs.VfsProjectNode;
import p.studio.vfs.VfsTextDocument;
import p.studio.workspaces.Workspace;
import p.studio.workspaces.WorkspaceId;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@ -24,6 +29,9 @@ import java.util.Objects;
public final class EditorWorkspace extends Workspace {
private final BorderPane root = new BorderPane();
private final CodeArea codeArea = new CodeArea();
private final Button saveButton = new Button();
private final Button saveAllButton = new Button();
private final Label readOnlyWarning = new Label();
private final EditorProjectNavigatorPanel navigatorPanel = new EditorProjectNavigatorPanel();
private final EditorOutlinePanel outlinePanel = new EditorOutlinePanel();
private final EditorHelperPanel helperPanel = new EditorHelperPanel();
@ -33,6 +41,7 @@ public final class EditorWorkspace extends Workspace {
private final ProjectDocumentVfs projectDocumentVfs;
private final EditorOpenFileSession openFileSession = new EditorOpenFileSession();
private final List<String> activePresentationStylesheets = new ArrayList<>();
private boolean syncingEditor;
public EditorWorkspace(
final ProjectReference projectReference,
@ -43,8 +52,11 @@ public final class EditorWorkspace extends Workspace {
codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
codeArea.setEditable(false);
codeArea.setWrapText(false);
codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current));
codeArea.getStyleClass().add("editor-workspace-code-area");
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentationRegistry.resolve("text"));
configureCommandBar();
configureWarning();
showEditorPlaceholder();
navigatorPanel.setRefreshAction(this::refreshNavigator);
navigatorPanel.setRevealActiveFileAction(this::revealActiveFileInNavigator);
@ -88,12 +100,7 @@ public final class EditorWorkspace extends Workspace {
}
private void openFile(final VfsTextDocument textDocument) {
openFileSession.open(new EditorOpenFileBuffer(
textDocument.path(),
textDocument.documentName(),
textDocument.typeId(),
textDocument.content(),
textDocument.lineSeparator()));
openFileSession.open(bufferFrom(textDocument));
renderSession();
}
@ -115,9 +122,16 @@ public final class EditorWorkspace extends Workspace {
final var fileBuffer = activeFile.orElseThrow();
final EditorDocumentPresentation presentation = presentationRegistry.resolve(fileBuffer.typeId());
applyPresentationStylesheets(presentation);
syncingEditor = true;
try {
codeArea.replaceText(fileBuffer.content());
codeArea.setStyleSpans(0, presentation.highlight(fileBuffer.content()));
} finally {
syncingEditor = false;
}
codeArea.setEditable(fileBuffer.editable());
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
refreshCommandSurfaces(fileBuffer);
statusBar.showFile(projectReference, fileBuffer, presentation);
}
@ -127,7 +141,7 @@ public final class EditorWorkspace extends Workspace {
.ifPresent(navigatorPanel::revealPath);
}
private void showUnsupportedFileModal(final java.nio.file.Path path) {
private void showUnsupportedFileModal(final Path path) {
final var alert = new Alert(Alert.AlertType.INFORMATION);
if (root.getScene() != null) {
alert.initOwner(root.getScene().getWindow());
@ -141,13 +155,23 @@ public final class EditorWorkspace extends Workspace {
private void showEditorPlaceholder() {
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
final String placeholder = """
// Read-only first wave
// Open a supported text file from the project navigator.
// Controlled write wave
// Open a supported project document from the navigator.
""";
applyPresentationStylesheets(presentation);
syncingEditor = true;
try {
codeArea.replaceText(placeholder);
codeArea.setStyleSpans(0, presentation.highlight(placeholder));
} finally {
syncingEditor = false;
}
codeArea.setEditable(false);
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
saveButton.setDisable(true);
saveAllButton.setDisable(true);
readOnlyWarning.setVisible(false);
readOnlyWarning.setManaged(false);
}
private void applyPresentationStylesheets(final EditorDocumentPresentation presentation) {
@ -175,7 +199,7 @@ public final class EditorWorkspace extends Workspace {
}
private VBox buildCenterColumn() {
final var centerColumn = new VBox(12, tabStrip, codeArea);
final var centerColumn = new VBox(12, buildCommandBar(), readOnlyWarning, tabStrip, codeArea);
centerColumn.getStyleClass().add("editor-workspace-center-column");
VBox.setVgrow(codeArea, Priority.ALWAYS);
return centerColumn;
@ -184,4 +208,103 @@ public final class EditorWorkspace extends Workspace {
private SplitPane buildRightColumn() {
return helperPanel.createBottomDockLayout(buildCenterColumn(), "editor-workspace-right-split");
}
private void configureCommandBar() {
saveButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE));
saveAllButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE_ALL));
saveButton.getStyleClass().addAll("studio-button", "editor-workspace-command-button");
saveAllButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "editor-workspace-command-button");
saveButton.setFocusTraversable(false);
saveAllButton.setFocusTraversable(false);
saveButton.setDisable(true);
saveAllButton.setDisable(true);
saveButton.setOnAction(event -> saveActiveFile());
saveAllButton.setOnAction(event -> saveAllFiles());
}
private void configureWarning() {
readOnlyWarning.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_WARNING_FRONTEND_READ_ONLY));
readOnlyWarning.getStyleClass().add("editor-workspace-warning");
readOnlyWarning.setWrapText(true);
readOnlyWarning.setVisible(false);
readOnlyWarning.setManaged(false);
}
private HBox buildCommandBar() {
final var spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
final var commandBar = new HBox(8, saveButton, saveAllButton, spacer);
commandBar.getStyleClass().add("editor-workspace-command-bar");
return commandBar;
}
private void syncActiveDocumentToVfs(final String content) {
if (syncingEditor) {
return;
}
openFileSession.activeFile()
.filter(EditorOpenFileBuffer::editable)
.ifPresent(activeFile -> {
final VfsTextDocument updatedDocument = projectDocumentVfs.updateDocument(activeFile.path(), content);
openFileSession.open(bufferFrom(updatedDocument));
tabStrip.showOpenFiles(
openFileSession.openFiles(),
openFileSession.activeFile().map(EditorOpenFileBuffer::path).orElse(null));
refreshCommandSurfaces(openFileSession.activeFile().orElseThrow());
});
}
private void saveActiveFile() {
openFileSession.activeFile()
.filter(EditorOpenFileBuffer::saveEnabled)
.ifPresent(activeFile -> {
projectDocumentVfs.saveDocument(activeFile.path());
reloadOpenFilesFromVfs(activeFile.path());
renderSession();
});
}
private void saveAllFiles() {
if (!openFileSession.hasDirtyEditableFiles()) {
return;
}
projectDocumentVfs.saveAllDocuments();
reloadOpenFilesFromVfs(openFileSession.activeFile().map(EditorOpenFileBuffer::path).orElse(null));
renderSession();
}
private void reloadOpenFilesFromVfs(final Path activePath) {
final var openPaths = openFileSession.openFiles().stream()
.map(EditorOpenFileBuffer::path)
.toList();
for (final var path : openPaths) {
final var result = projectDocumentVfs.openDocument(path);
if (result instanceof VfsTextDocument textDocument) {
openFileSession.open(bufferFrom(textDocument));
}
}
if (activePath != null) {
openFileSession.activate(activePath);
}
}
private void refreshCommandSurfaces(final EditorOpenFileBuffer fileBuffer) {
saveButton.setDisable(!fileBuffer.saveEnabled());
saveAllButton.setDisable(!openFileSession.hasDirtyEditableFiles());
final boolean showWarning = fileBuffer.frontendDocument() && fileBuffer.readOnly();
readOnlyWarning.setVisible(showWarning);
readOnlyWarning.setManaged(showWarning);
}
private EditorOpenFileBuffer bufferFrom(final VfsTextDocument textDocument) {
return new EditorOpenFileBuffer(
textDocument.path(),
textDocument.documentName(),
textDocument.typeId(),
textDocument.content(),
textDocument.lineSeparator(),
textDocument.accessContext().frontendDocument(),
textDocument.accessContext().accessMode(),
textDocument.dirty());
}
}

View File

@ -78,7 +78,11 @@ codeEditor.status.position=L:C
codeEditor.status.lineSeparator=LF
codeEditor.status.indentation=Spaces: 4
codeEditor.status.language=Text
codeEditor.status.editable=Editable
codeEditor.status.readOnly=Read-only
codeEditor.command.save=Save
codeEditor.command.saveAll=Save All
codeEditor.warning.frontendReadOnly=This frontend file is read-only in this wave. It cannot be edited or saved yet.
codeEditor.unsupportedFile.title=Unsupported file
codeEditor.unsupportedFile.message=This file is not supported in this wave: {0}
workspace.shipper=Shipper

View File

@ -587,6 +587,31 @@
-fx-border-color: #a7d7ff #5c738b #5c738b #5c738b;
}
.editor-workspace-tab-button-editable {
-fx-background-color: #203226;
-fx-border-color: #4d6f58;
-fx-text-fill: #e8f6eb;
}
.editor-workspace-tab-button-editable:hover {
-fx-background-color: #29412f;
-fx-border-color: #6f957a;
-fx-text-fill: #f4fff5;
}
.editor-workspace-tab-button-editable.editor-workspace-tab-button-active {
-fx-background-color: #1d3a2a;
-fx-border-color: #8ad3a2 #587464 #587464 #587464;
-fx-border-width: 3 1 1 1;
-fx-text-fill: #ffffff;
-fx-font-weight: bold;
}
.editor-workspace-tab-button-editable.editor-workspace-tab-button-active:hover {
-fx-background-color: #234532;
-fx-border-color: #a5efbd #688676 #688676 #688676;
}
.editor-workspace-tab-overflow {
-fx-background-radius: 0;
-fx-border-radius: 0;
@ -632,6 +657,26 @@
-fx-fill: #f2f6fb;
}
.editor-workspace-command-bar {
-fx-padding: 10 12 0 12;
-fx-alignment: center-left;
}
.editor-workspace-command-button {
-fx-min-height: 34;
-fx-pref-height: 34;
-fx-max-height: 34;
-fx-padding: 0 14 0 14;
}
.editor-workspace-warning {
-fx-padding: 8 12 8 12;
-fx-background-color: #3b2a10;
-fx-border-color: #8f6730;
-fx-text-fill: #f7ddb0;
-fx-font-size: 12px;
}
.workspace-dock-pane {
-fx-collapsible: true;
-fx-background-color: transparent;
@ -782,6 +827,12 @@
-fx-max-width: 88;
}
.editor-workspace-status-chip-editable {
-fx-background-color: #203226;
-fx-border-color: #5a8567;
-fx-text-fill: #ebfff0;
}
.assets-workspace-split {
-fx-background-color: transparent;
}

View File

@ -1,6 +1,7 @@
package p.studio.workspaces.editor;
import org.junit.jupiter.api.Test;
import p.studio.vfs.VfsDocumentAccessMode;
import java.nio.file.Path;
@ -11,7 +12,7 @@ final class EditorOpenFileSessionTest {
@Test
void openAddsNewFileAndMarksItActive() {
final var session = new EditorOpenFileSession();
final var file = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "pbs", "fn main(): void\n", "LF");
final var file = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "fn main(): void\n");
session.open(file);
@ -22,8 +23,8 @@ final class EditorOpenFileSessionTest {
@Test
void openDoesNotDuplicateExistingTab() {
final var session = new EditorOpenFileSession();
final var first = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "pbs", "a", "LF");
final var second = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "pbs", "b", "LF");
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
final var second = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "b");
session.open(first);
session.open(second);
@ -36,8 +37,8 @@ final class EditorOpenFileSessionTest {
@Test
void activateSwitchesTheActiveTabWithinTheCurrentSession() {
final var session = new EditorOpenFileSession();
final var first = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "pbs", "a", "LF");
final var second = new EditorOpenFileBuffer(Path.of("src/other.pbs"), "other.pbs", "pbs", "b", "LF");
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
final var second = fileBuffer(Path.of("src/other.pbs"), "other.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "b");
session.open(first);
session.open(second);
@ -45,4 +46,42 @@ final class EditorOpenFileSessionTest {
assertEquals(first.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path());
}
@Test
void reportsSaveStateForDirtyEditableFilesOnly() {
final var session = new EditorOpenFileSession();
session.open(fileBuffer(Path.of("notes.txt"), "notes.txt", VfsDocumentAccessMode.EDITABLE, true, false, "alpha"));
assertTrue(session.hasSaveableActiveFile());
assertTrue(session.hasDirtyEditableFiles());
}
@Test
void frontendReadOnlyFilesDoNotEnableSaveActions() {
final var session = new EditorOpenFileSession();
session.open(fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "fn main(): void"));
assertFalse(session.hasSaveableActiveFile());
assertFalse(session.hasDirtyEditableFiles());
assertTrue(session.activeFile().orElseThrow().frontendDocument());
assertTrue(session.activeFile().orElseThrow().readOnly());
}
private EditorOpenFileBuffer fileBuffer(
final Path path,
final String tabLabel,
final VfsDocumentAccessMode accessMode,
final boolean dirty,
final boolean frontendDocument,
final String content) {
return new EditorOpenFileBuffer(
path,
tabLabel,
frontendDocument ? "pbs" : "text",
content,
"LF",
frontendDocument,
accessMode,
dirty);
}
}