implements PLN-0021
This commit is contained in:
parent
f9a47bbdbf
commit
b3097cfaf7
@ -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-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-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-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":[]}
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
id: PLN-0021
|
id: PLN-0021
|
||||||
ticket: studio-editor-write-wave-supported-non-frontend-files
|
ticket: studio-editor-write-wave-supported-non-frontend-files
|
||||||
title: Integrate DEC-0010 editor write UI and workflow in Studio
|
title: Integrate DEC-0010 editor write UI and workflow in Studio
|
||||||
status: review
|
status: done
|
||||||
created: 2026-03-31
|
created: 2026-03-31
|
||||||
completed:
|
completed: 2026-03-31
|
||||||
tags: [studio, editor, ui, save, read-only, write-wave]
|
tags: [studio, editor, ui, save, read-only, write-wave]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -87,7 +87,11 @@ public enum I18n {
|
|||||||
CODE_EDITOR_STATUS_LINE_SEPARATOR("codeEditor.status.lineSeparator"),
|
CODE_EDITOR_STATUS_LINE_SEPARATOR("codeEditor.status.lineSeparator"),
|
||||||
CODE_EDITOR_STATUS_INDENTATION("codeEditor.status.indentation"),
|
CODE_EDITOR_STATUS_INDENTATION("codeEditor.status.indentation"),
|
||||||
CODE_EDITOR_STATUS_LANGUAGE("codeEditor.status.language"),
|
CODE_EDITOR_STATUS_LANGUAGE("codeEditor.status.language"),
|
||||||
|
CODE_EDITOR_STATUS_EDITABLE("codeEditor.status.editable"),
|
||||||
CODE_EDITOR_STATUS_READ_ONLY("codeEditor.status.readOnly"),
|
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_TITLE("codeEditor.unsupportedFile.title"),
|
||||||
CODE_EDITOR_UNSUPPORTED_FILE_MESSAGE("codeEditor.unsupportedFile.message"),
|
CODE_EDITOR_UNSUPPORTED_FILE_MESSAGE("codeEditor.unsupportedFile.message"),
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package p.studio.workspaces.editor;
|
package p.studio.workspaces.editor;
|
||||||
|
|
||||||
|
import p.studio.vfs.VfsDocumentAccessMode;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -8,12 +10,28 @@ public record EditorOpenFileBuffer(
|
|||||||
String tabLabel,
|
String tabLabel,
|
||||||
String typeId,
|
String typeId,
|
||||||
String content,
|
String content,
|
||||||
String lineSeparator) {
|
String lineSeparator,
|
||||||
|
boolean frontendDocument,
|
||||||
|
VfsDocumentAccessMode accessMode,
|
||||||
|
boolean dirty) {
|
||||||
public EditorOpenFileBuffer {
|
public EditorOpenFileBuffer {
|
||||||
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||||
tabLabel = Objects.requireNonNull(tabLabel, "tabLabel");
|
tabLabel = Objects.requireNonNull(tabLabel, "tabLabel");
|
||||||
typeId = Objects.requireNonNull(typeId, "typeId");
|
typeId = Objects.requireNonNull(typeId, "typeId");
|
||||||
content = Objects.requireNonNull(content, "content");
|
content = Objects.requireNonNull(content, "content");
|
||||||
lineSeparator = Objects.requireNonNull(lineSeparator, "lineSeparator");
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,14 @@ public final class EditorOpenFileSession {
|
|||||||
return find(activePath);
|
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() {
|
public boolean isEmpty() {
|
||||||
return openFiles.isEmpty();
|
return openFiles.isEmpty();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,7 @@ public final class EditorStatusBar extends HBox {
|
|||||||
bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION);
|
bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION);
|
||||||
setText(language, fileBuffer.typeId());
|
setText(language, fileBuffer.typeId());
|
||||||
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
|
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
|
||||||
bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY);
|
showAccessMode(fileBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showPlaceholder(final EditorDocumentPresentation presentation) {
|
public void showPlaceholder(final EditorDocumentPresentation presentation) {
|
||||||
@ -78,6 +78,10 @@ public final class EditorStatusBar extends HBox {
|
|||||||
bindDefault(language, I18n.CODE_EDITOR_STATUS_LANGUAGE);
|
bindDefault(language, I18n.CODE_EDITOR_STATUS_LANGUAGE);
|
||||||
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
|
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
|
||||||
bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY);
|
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) {
|
private void bindDefault(final Label label, final I18n key) {
|
||||||
@ -174,6 +178,23 @@ public final class EditorStatusBar extends HBox {
|
|||||||
setVisibleManaged(readOnly, visible);
|
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) {
|
private void setVisibleManaged(final Label label, final boolean visible) {
|
||||||
label.setVisible(visible);
|
label.setVisible(visible);
|
||||||
label.setManaged(visible);
|
label.setManaged(visible);
|
||||||
|
|||||||
@ -85,8 +85,11 @@ public final class EditorTabStrip extends HBox {
|
|||||||
tabButton.getStyleClass().addAll(
|
tabButton.getStyleClass().addAll(
|
||||||
"studio-button",
|
"studio-button",
|
||||||
"studio-button-secondary",
|
"studio-button-secondary",
|
||||||
"editor-workspace-tab-button",
|
"editor-workspace-tab-button");
|
||||||
"editor-workspace-tab-button-read-only");
|
tabButton.getStyleClass().add(
|
||||||
|
fileBuffer.readOnly()
|
||||||
|
? "editor-workspace-tab-button-read-only"
|
||||||
|
: "editor-workspace-tab-button-editable");
|
||||||
applyTabMetrics(tabButton);
|
applyTabMetrics(tabButton);
|
||||||
if (fileBuffer.path().equals(activePath)) {
|
if (fileBuffer.path().equals(activePath)) {
|
||||||
tabButton.getStyleClass().add("editor-workspace-tab-button-active");
|
tabButton.getStyleClass().add("editor-workspace-tab-button-active");
|
||||||
|
|||||||
@ -1,22 +1,27 @@
|
|||||||
package p.studio.workspaces.editor;
|
package p.studio.workspaces.editor;
|
||||||
|
|
||||||
import javafx.scene.control.Alert;
|
|
||||||
import javafx.scene.Node;
|
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.control.SplitPane;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.fxmisc.richtext.CodeArea;
|
import org.fxmisc.richtext.CodeArea;
|
||||||
import org.fxmisc.richtext.LineNumberFactory;
|
import org.fxmisc.richtext.LineNumberFactory;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
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.ProjectDocumentVfs;
|
||||||
import p.studio.vfs.VfsDocumentOpenResult;
|
import p.studio.vfs.VfsDocumentOpenResult;
|
||||||
import p.studio.vfs.VfsProjectNode;
|
import p.studio.vfs.VfsProjectNode;
|
||||||
import p.studio.vfs.VfsTextDocument;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -24,6 +29,9 @@ import java.util.Objects;
|
|||||||
public final class EditorWorkspace extends Workspace {
|
public final class EditorWorkspace extends Workspace {
|
||||||
private final BorderPane root = new BorderPane();
|
private final BorderPane root = new BorderPane();
|
||||||
private final CodeArea codeArea = new CodeArea();
|
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 EditorProjectNavigatorPanel navigatorPanel = new EditorProjectNavigatorPanel();
|
||||||
private final EditorOutlinePanel outlinePanel = new EditorOutlinePanel();
|
private final EditorOutlinePanel outlinePanel = new EditorOutlinePanel();
|
||||||
private final EditorHelperPanel helperPanel = new EditorHelperPanel();
|
private final EditorHelperPanel helperPanel = new EditorHelperPanel();
|
||||||
@ -33,6 +41,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
private final ProjectDocumentVfs projectDocumentVfs;
|
private final ProjectDocumentVfs projectDocumentVfs;
|
||||||
private final EditorOpenFileSession openFileSession = new EditorOpenFileSession();
|
private final EditorOpenFileSession openFileSession = new EditorOpenFileSession();
|
||||||
private final List<String> activePresentationStylesheets = new ArrayList<>();
|
private final List<String> activePresentationStylesheets = new ArrayList<>();
|
||||||
|
private boolean syncingEditor;
|
||||||
|
|
||||||
public EditorWorkspace(
|
public EditorWorkspace(
|
||||||
final ProjectReference projectReference,
|
final ProjectReference projectReference,
|
||||||
@ -43,8 +52,11 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
|
codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
|
||||||
codeArea.setEditable(false);
|
codeArea.setEditable(false);
|
||||||
codeArea.setWrapText(false);
|
codeArea.setWrapText(false);
|
||||||
|
codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current));
|
||||||
codeArea.getStyleClass().add("editor-workspace-code-area");
|
codeArea.getStyleClass().add("editor-workspace-code-area");
|
||||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentationRegistry.resolve("text"));
|
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentationRegistry.resolve("text"));
|
||||||
|
configureCommandBar();
|
||||||
|
configureWarning();
|
||||||
showEditorPlaceholder();
|
showEditorPlaceholder();
|
||||||
navigatorPanel.setRefreshAction(this::refreshNavigator);
|
navigatorPanel.setRefreshAction(this::refreshNavigator);
|
||||||
navigatorPanel.setRevealActiveFileAction(this::revealActiveFileInNavigator);
|
navigatorPanel.setRevealActiveFileAction(this::revealActiveFileInNavigator);
|
||||||
@ -88,12 +100,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void openFile(final VfsTextDocument textDocument) {
|
private void openFile(final VfsTextDocument textDocument) {
|
||||||
openFileSession.open(new EditorOpenFileBuffer(
|
openFileSession.open(bufferFrom(textDocument));
|
||||||
textDocument.path(),
|
|
||||||
textDocument.documentName(),
|
|
||||||
textDocument.typeId(),
|
|
||||||
textDocument.content(),
|
|
||||||
textDocument.lineSeparator()));
|
|
||||||
renderSession();
|
renderSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,9 +122,16 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
final var fileBuffer = activeFile.orElseThrow();
|
final var fileBuffer = activeFile.orElseThrow();
|
||||||
final EditorDocumentPresentation presentation = presentationRegistry.resolve(fileBuffer.typeId());
|
final EditorDocumentPresentation presentation = presentationRegistry.resolve(fileBuffer.typeId());
|
||||||
applyPresentationStylesheets(presentation);
|
applyPresentationStylesheets(presentation);
|
||||||
|
syncingEditor = true;
|
||||||
|
try {
|
||||||
codeArea.replaceText(fileBuffer.content());
|
codeArea.replaceText(fileBuffer.content());
|
||||||
codeArea.setStyleSpans(0, presentation.highlight(fileBuffer.content()));
|
codeArea.setStyleSpans(0, presentation.highlight(fileBuffer.content()));
|
||||||
|
} finally {
|
||||||
|
syncingEditor = false;
|
||||||
|
}
|
||||||
|
codeArea.setEditable(fileBuffer.editable());
|
||||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
||||||
|
refreshCommandSurfaces(fileBuffer);
|
||||||
statusBar.showFile(projectReference, fileBuffer, presentation);
|
statusBar.showFile(projectReference, fileBuffer, presentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +141,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
.ifPresent(navigatorPanel::revealPath);
|
.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);
|
final var alert = new Alert(Alert.AlertType.INFORMATION);
|
||||||
if (root.getScene() != null) {
|
if (root.getScene() != null) {
|
||||||
alert.initOwner(root.getScene().getWindow());
|
alert.initOwner(root.getScene().getWindow());
|
||||||
@ -141,13 +155,23 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
private void showEditorPlaceholder() {
|
private void showEditorPlaceholder() {
|
||||||
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
|
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
|
||||||
final String placeholder = """
|
final String placeholder = """
|
||||||
// Read-only first wave
|
// Controlled write wave
|
||||||
// Open a supported text file from the project navigator.
|
// Open a supported project document from the navigator.
|
||||||
""";
|
""";
|
||||||
applyPresentationStylesheets(presentation);
|
applyPresentationStylesheets(presentation);
|
||||||
|
syncingEditor = true;
|
||||||
|
try {
|
||||||
codeArea.replaceText(placeholder);
|
codeArea.replaceText(placeholder);
|
||||||
codeArea.setStyleSpans(0, presentation.highlight(placeholder));
|
codeArea.setStyleSpans(0, presentation.highlight(placeholder));
|
||||||
|
} finally {
|
||||||
|
syncingEditor = false;
|
||||||
|
}
|
||||||
|
codeArea.setEditable(false);
|
||||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
||||||
|
saveButton.setDisable(true);
|
||||||
|
saveAllButton.setDisable(true);
|
||||||
|
readOnlyWarning.setVisible(false);
|
||||||
|
readOnlyWarning.setManaged(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyPresentationStylesheets(final EditorDocumentPresentation presentation) {
|
private void applyPresentationStylesheets(final EditorDocumentPresentation presentation) {
|
||||||
@ -175,7 +199,7 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private VBox buildCenterColumn() {
|
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");
|
centerColumn.getStyleClass().add("editor-workspace-center-column");
|
||||||
VBox.setVgrow(codeArea, Priority.ALWAYS);
|
VBox.setVgrow(codeArea, Priority.ALWAYS);
|
||||||
return centerColumn;
|
return centerColumn;
|
||||||
@ -184,4 +208,103 @@ public final class EditorWorkspace extends Workspace {
|
|||||||
private SplitPane buildRightColumn() {
|
private SplitPane buildRightColumn() {
|
||||||
return helperPanel.createBottomDockLayout(buildCenterColumn(), "editor-workspace-right-split");
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,7 +78,11 @@ codeEditor.status.position=L:C
|
|||||||
codeEditor.status.lineSeparator=LF
|
codeEditor.status.lineSeparator=LF
|
||||||
codeEditor.status.indentation=Spaces: 4
|
codeEditor.status.indentation=Spaces: 4
|
||||||
codeEditor.status.language=Text
|
codeEditor.status.language=Text
|
||||||
|
codeEditor.status.editable=Editable
|
||||||
codeEditor.status.readOnly=Read-only
|
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.title=Unsupported file
|
||||||
codeEditor.unsupportedFile.message=This file is not supported in this wave: {0}
|
codeEditor.unsupportedFile.message=This file is not supported in this wave: {0}
|
||||||
workspace.shipper=Shipper
|
workspace.shipper=Shipper
|
||||||
|
|||||||
@ -587,6 +587,31 @@
|
|||||||
-fx-border-color: #a7d7ff #5c738b #5c738b #5c738b;
|
-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 {
|
.editor-workspace-tab-overflow {
|
||||||
-fx-background-radius: 0;
|
-fx-background-radius: 0;
|
||||||
-fx-border-radius: 0;
|
-fx-border-radius: 0;
|
||||||
@ -632,6 +657,26 @@
|
|||||||
-fx-fill: #f2f6fb;
|
-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 {
|
.workspace-dock-pane {
|
||||||
-fx-collapsible: true;
|
-fx-collapsible: true;
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
@ -782,6 +827,12 @@
|
|||||||
-fx-max-width: 88;
|
-fx-max-width: 88;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-workspace-status-chip-editable {
|
||||||
|
-fx-background-color: #203226;
|
||||||
|
-fx-border-color: #5a8567;
|
||||||
|
-fx-text-fill: #ebfff0;
|
||||||
|
}
|
||||||
|
|
||||||
.assets-workspace-split {
|
.assets-workspace-split {
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package p.studio.workspaces.editor;
|
package p.studio.workspaces.editor;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import p.studio.vfs.VfsDocumentAccessMode;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ final class EditorOpenFileSessionTest {
|
|||||||
@Test
|
@Test
|
||||||
void openAddsNewFileAndMarksItActive() {
|
void openAddsNewFileAndMarksItActive() {
|
||||||
final var session = new EditorOpenFileSession();
|
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);
|
session.open(file);
|
||||||
|
|
||||||
@ -22,8 +23,8 @@ final class EditorOpenFileSessionTest {
|
|||||||
@Test
|
@Test
|
||||||
void openDoesNotDuplicateExistingTab() {
|
void openDoesNotDuplicateExistingTab() {
|
||||||
final var session = new EditorOpenFileSession();
|
final var session = new EditorOpenFileSession();
|
||||||
final var first = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "pbs", "a", "LF");
|
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
||||||
final var second = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "pbs", "b", "LF");
|
final var second = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "b");
|
||||||
|
|
||||||
session.open(first);
|
session.open(first);
|
||||||
session.open(second);
|
session.open(second);
|
||||||
@ -36,8 +37,8 @@ final class EditorOpenFileSessionTest {
|
|||||||
@Test
|
@Test
|
||||||
void activateSwitchesTheActiveTabWithinTheCurrentSession() {
|
void activateSwitchesTheActiveTabWithinTheCurrentSession() {
|
||||||
final var session = new EditorOpenFileSession();
|
final var session = new EditorOpenFileSession();
|
||||||
final var first = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "pbs", "a", "LF");
|
final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "a");
|
||||||
final var second = new EditorOpenFileBuffer(Path.of("src/other.pbs"), "other.pbs", "pbs", "b", "LF");
|
final var second = fileBuffer(Path.of("src/other.pbs"), "other.pbs", VfsDocumentAccessMode.READ_ONLY, false, true, "b");
|
||||||
|
|
||||||
session.open(first);
|
session.open(first);
|
||||||
session.open(second);
|
session.open(second);
|
||||||
@ -45,4 +46,42 @@ final class EditorOpenFileSessionTest {
|
|||||||
|
|
||||||
assertEquals(first.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user