prometeu-vsf
This commit is contained in:
parent
a13d0a54c2
commit
b7beef4c7e
@ -0,0 +1,24 @@
|
||||
package p.studio.workspaces.editor;
|
||||
|
||||
import org.fxmisc.richtext.model.StyleSpans;
|
||||
import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record EditorDocumentPresentation(
|
||||
String styleKey,
|
||||
List<String> stylesheetUrls,
|
||||
EditorDocumentSyntaxHighlighting syntaxHighlighting) {
|
||||
|
||||
EditorDocumentPresentation {
|
||||
styleKey = Objects.requireNonNull(styleKey, "styleKey");
|
||||
stylesheetUrls = List.copyOf(Objects.requireNonNull(stylesheetUrls, "stylesheetUrls"));
|
||||
syntaxHighlighting = Objects.requireNonNull(syntaxHighlighting, "syntaxHighlighting");
|
||||
}
|
||||
|
||||
StyleSpans<Collection<String>> highlight(final String content) {
|
||||
return syntaxHighlighting.highlight(content);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package p.studio.workspaces.editor;
|
||||
|
||||
import p.studio.compiler.FrontendRegistryService;
|
||||
import p.studio.vfs.VfsDocumentTypeIds;
|
||||
import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
final class EditorDocumentPresentationRegistry {
|
||||
private static final EditorDocumentPresentation TEXT_PRESENTATION = new EditorDocumentPresentation(
|
||||
"text",
|
||||
java.util.List.of(),
|
||||
EditorDocumentSyntaxHighlighting.plainText());
|
||||
private static final EditorDocumentPresentation JSON_PRESENTATION = new EditorDocumentPresentation(
|
||||
"json",
|
||||
java.util.List.of(stylesheet("presentations/json.css")),
|
||||
EditorDocumentSyntaxHighlighting.json());
|
||||
private static final EditorDocumentPresentation BASH_PRESENTATION = new EditorDocumentPresentation(
|
||||
"bash",
|
||||
java.util.List.of(stylesheet("presentations/bash.css")),
|
||||
EditorDocumentSyntaxHighlighting.bash());
|
||||
private static final EditorDocumentPresentation FRONTEND_PRESENTATION = new EditorDocumentPresentation(
|
||||
"fe",
|
||||
java.util.List.of(),
|
||||
EditorDocumentSyntaxHighlighting.plainText());
|
||||
|
||||
EditorDocumentPresentation resolve(final String typeId) {
|
||||
final String normalizedTypeId = normalize(typeId);
|
||||
if (normalizedTypeId.isBlank()) {
|
||||
return TEXT_PRESENTATION;
|
||||
}
|
||||
if (FrontendRegistryService.getFrontendSpec(normalizedTypeId).isPresent()) {
|
||||
return FRONTEND_PRESENTATION;
|
||||
}
|
||||
if (VfsDocumentTypeIds.JSON.equals(normalizedTypeId)) {
|
||||
return JSON_PRESENTATION;
|
||||
}
|
||||
if (VfsDocumentTypeIds.BASH.equals(normalizedTypeId)) {
|
||||
return BASH_PRESENTATION;
|
||||
}
|
||||
return TEXT_PRESENTATION;
|
||||
}
|
||||
|
||||
private String normalize(final String typeId) {
|
||||
return typeId == null ? "" : typeId.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static String stylesheet(final String relativePath) {
|
||||
return java.util.Objects.requireNonNull(
|
||||
EditorDocumentPresentationRegistry.class.getResource("/themes/editor/" + relativePath),
|
||||
"missing editor presentation stylesheet: " + relativePath)
|
||||
.toExternalForm();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package p.studio.workspaces.editor;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
|
||||
final class EditorDocumentPresentationStyles {
|
||||
private static final String CODE_AREA_TYPE_CLASS_PREFIX = "editor-workspace-code-area-type-";
|
||||
private static final String STATUS_CHIP_TYPE_CLASS_PREFIX = "editor-workspace-status-chip-type-";
|
||||
|
||||
private EditorDocumentPresentationStyles() {
|
||||
}
|
||||
|
||||
static void applyToCodeArea(final Node node, final EditorDocumentPresentation presentation) {
|
||||
replaceTypeClass(node, CODE_AREA_TYPE_CLASS_PREFIX, presentation.styleKey());
|
||||
}
|
||||
|
||||
static void applyToStatusChip(final Label label, final EditorDocumentPresentation presentation) {
|
||||
replaceTypeClass(label, STATUS_CHIP_TYPE_CLASS_PREFIX, presentation.styleKey());
|
||||
}
|
||||
|
||||
private static void replaceTypeClass(
|
||||
final Node node,
|
||||
final String prefix,
|
||||
final String styleKey) {
|
||||
node.getStyleClass().removeIf(styleClass -> styleClass.startsWith(prefix));
|
||||
node.getStyleClass().add(prefix + styleKey);
|
||||
}
|
||||
}
|
||||
@ -6,11 +6,13 @@ import java.util.Objects;
|
||||
public record EditorOpenFileBuffer(
|
||||
Path path,
|
||||
String tabLabel,
|
||||
String typeId,
|
||||
String content,
|
||||
String lineSeparator) {
|
||||
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");
|
||||
}
|
||||
|
||||
@ -53,26 +53,30 @@ public final class EditorStatusBar extends HBox {
|
||||
readOnly
|
||||
);
|
||||
|
||||
showPlaceholder();
|
||||
}
|
||||
|
||||
public void showFile(final ProjectReference projectReference, final EditorOpenFileBuffer fileBuffer) {
|
||||
public void showFile(
|
||||
final ProjectReference projectReference,
|
||||
final EditorOpenFileBuffer fileBuffer,
|
||||
final EditorDocumentPresentation presentation) {
|
||||
showBreadcrumb(projectReference, fileBuffer.path());
|
||||
showMetadata(true);
|
||||
bindDefault(position, I18n.CODE_EDITOR_STATUS_POSITION);
|
||||
setText(lineSeparator, fileBuffer.lineSeparator());
|
||||
bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION);
|
||||
setText(language, extensionText(fileBuffer.path()));
|
||||
setText(language, fileBuffer.typeId());
|
||||
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
|
||||
bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY);
|
||||
}
|
||||
|
||||
public void showPlaceholder() {
|
||||
public void showPlaceholder(final EditorDocumentPresentation presentation) {
|
||||
breadcrumb.getChildren().clear();
|
||||
showMetadata(false);
|
||||
bindDefault(position, I18n.CODE_EDITOR_STATUS_POSITION);
|
||||
bindDefault(lineSeparator, I18n.CODE_EDITOR_STATUS_LINE_SEPARATOR);
|
||||
bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION);
|
||||
bindDefault(language, I18n.CODE_EDITOR_STATUS_LANGUAGE);
|
||||
EditorDocumentPresentationStyles.applyToStatusChip(language, presentation);
|
||||
bindDefault(readOnly, I18n.CODE_EDITOR_STATUS_READ_ONLY);
|
||||
}
|
||||
|
||||
@ -180,12 +184,4 @@ public final class EditorStatusBar extends HBox {
|
||||
label.getStyleClass().add("editor-workspace-status-chip");
|
||||
}
|
||||
}
|
||||
|
||||
private String extensionText(final java.nio.file.Path path) {
|
||||
final var fileName = path.getFileName().toString();
|
||||
final var dot = fileName.lastIndexOf('.');
|
||||
return dot >= 0 && dot < fileName.length() - 1
|
||||
? fileName.substring(dot + 1)
|
||||
: fileName;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ import p.studio.vfs.VfsDocumentOpenResult;
|
||||
import p.studio.vfs.VfsProjectNode;
|
||||
import p.studio.vfs.VfsTextDocument;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class EditorWorkspace extends Workspace {
|
||||
@ -27,8 +29,10 @@ public final class EditorWorkspace extends Workspace {
|
||||
private final EditorHelperPanel helperPanel = new EditorHelperPanel();
|
||||
private final EditorStatusBar statusBar = new EditorStatusBar();
|
||||
private final EditorTabStrip tabStrip = new EditorTabStrip();
|
||||
private final EditorDocumentPresentationRegistry presentationRegistry = new EditorDocumentPresentationRegistry();
|
||||
private final ProjectDocumentVfs projectDocumentVfs;
|
||||
private final EditorOpenFileSession openFileSession = new EditorOpenFileSession();
|
||||
private final List<String> activePresentationStylesheets = new ArrayList<>();
|
||||
|
||||
public EditorWorkspace(
|
||||
final ProjectReference projectReference,
|
||||
@ -40,6 +44,7 @@ public final class EditorWorkspace extends Workspace {
|
||||
codeArea.setEditable(false);
|
||||
codeArea.setWrapText(false);
|
||||
codeArea.getStyleClass().add("editor-workspace-code-area");
|
||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentationRegistry.resolve("text"));
|
||||
showEditorPlaceholder();
|
||||
navigatorPanel.setRefreshAction(this::refreshNavigator);
|
||||
navigatorPanel.setRevealActiveFileAction(this::revealActiveFileInNavigator);
|
||||
@ -50,7 +55,7 @@ public final class EditorWorkspace extends Workspace {
|
||||
});
|
||||
|
||||
root.setCenter(buildLayout());
|
||||
statusBar.showPlaceholder();
|
||||
statusBar.showPlaceholder(presentationRegistry.resolve("text"));
|
||||
}
|
||||
|
||||
@Override public WorkspaceId workspaceId() { return WorkspaceId.EDITOR; }
|
||||
@ -86,6 +91,7 @@ public final class EditorWorkspace extends Workspace {
|
||||
openFileSession.open(new EditorOpenFileBuffer(
|
||||
textDocument.path(),
|
||||
textDocument.documentName(),
|
||||
textDocument.typeId(),
|
||||
textDocument.content(),
|
||||
textDocument.lineSeparator()));
|
||||
renderSession();
|
||||
@ -99,13 +105,20 @@ public final class EditorWorkspace extends Workspace {
|
||||
activeFile.map(EditorOpenFileBuffer::path).orElse(null));
|
||||
if (activeFile.isEmpty()) {
|
||||
showEditorPlaceholder();
|
||||
statusBar.showPlaceholder();
|
||||
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
|
||||
applyPresentationStylesheets(presentation);
|
||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
||||
statusBar.showPlaceholder(presentation);
|
||||
return;
|
||||
}
|
||||
|
||||
final var fileBuffer = activeFile.orElseThrow();
|
||||
final EditorDocumentPresentation presentation = presentationRegistry.resolve(fileBuffer.typeId());
|
||||
applyPresentationStylesheets(presentation);
|
||||
codeArea.replaceText(fileBuffer.content());
|
||||
statusBar.showFile(projectReference, fileBuffer);
|
||||
codeArea.setStyleSpans(0, presentation.highlight(fileBuffer.content()));
|
||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
||||
statusBar.showFile(projectReference, fileBuffer, presentation);
|
||||
}
|
||||
|
||||
private void revealActiveFileInNavigator() {
|
||||
@ -126,10 +139,22 @@ public final class EditorWorkspace extends Workspace {
|
||||
}
|
||||
|
||||
private void showEditorPlaceholder() {
|
||||
codeArea.replaceText("""
|
||||
final EditorDocumentPresentation presentation = presentationRegistry.resolve("text");
|
||||
final String placeholder = """
|
||||
// Read-only first wave
|
||||
// Open a supported text file from the project navigator.
|
||||
""");
|
||||
""";
|
||||
applyPresentationStylesheets(presentation);
|
||||
codeArea.replaceText(placeholder);
|
||||
codeArea.setStyleSpans(0, presentation.highlight(placeholder));
|
||||
EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation);
|
||||
}
|
||||
|
||||
private void applyPresentationStylesheets(final EditorDocumentPresentation presentation) {
|
||||
root.getStylesheets().removeAll(activePresentationStylesheets);
|
||||
activePresentationStylesheets.clear();
|
||||
activePresentationStylesheets.addAll(presentation.stylesheetUrls());
|
||||
root.getStylesheets().addAll(activePresentationStylesheets);
|
||||
}
|
||||
|
||||
private VBox buildLayout() {
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
package p.studio.workspaces.editor.syntaxhighlight;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record EditorDocumentHighlightToken(
|
||||
String groupName,
|
||||
Collection<String> styleClasses) {
|
||||
|
||||
public EditorDocumentHighlightToken(final String groupName, final String styleClass) {
|
||||
this(groupName, List.of(styleClass));
|
||||
}
|
||||
|
||||
public EditorDocumentHighlightToken {
|
||||
groupName = Objects.requireNonNull(groupName, "groupName");
|
||||
styleClasses = List.copyOf(Objects.requireNonNull(styleClasses, "styleClasses"));
|
||||
if (groupName.isBlank()) {
|
||||
throw new IllegalArgumentException("groupName cannot be blank");
|
||||
}
|
||||
if (styleClasses.isEmpty()) {
|
||||
throw new IllegalArgumentException("styleClasses cannot be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package p.studio.workspaces.editor.syntaxhighlight;
|
||||
|
||||
import org.fxmisc.richtext.model.StyleSpans;
|
||||
import org.fxmisc.richtext.model.StyleSpansBuilder;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public record EditorDocumentSyntaxHighlighting(
|
||||
Pattern tokenPattern,
|
||||
List<EditorDocumentHighlightToken> tokens) {
|
||||
|
||||
public EditorDocumentSyntaxHighlighting {
|
||||
tokens = List.copyOf(Objects.requireNonNull(tokens, "tokens"));
|
||||
if (tokenPattern == null && !tokens.isEmpty()) {
|
||||
throw new IllegalArgumentException("plain text highlighting cannot define tokens");
|
||||
}
|
||||
if (tokenPattern != null && tokens.isEmpty()) {
|
||||
throw new IllegalArgumentException("token-based highlighting requires tokens");
|
||||
}
|
||||
}
|
||||
|
||||
public static EditorDocumentSyntaxHighlighting plainText() {
|
||||
return EditorDocumentSyntaxHighlightingPlainText.PLAIN_TEXT;
|
||||
}
|
||||
|
||||
public static EditorDocumentSyntaxHighlighting json() {
|
||||
return EditorDocumentSyntaxHighlightingJson.JSON;
|
||||
}
|
||||
|
||||
public static EditorDocumentSyntaxHighlighting bash() {
|
||||
return EditorDocumentSyntaxHighlightingBash.BASH;
|
||||
}
|
||||
|
||||
public StyleSpans<Collection<String>> highlight(final String content) {
|
||||
final StyleSpansBuilder<Collection<String>> builder = new StyleSpansBuilder<>();
|
||||
if (tokenPattern == null) {
|
||||
builder.add(Collections.emptyList(), content.length());
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
final Matcher matcher = tokenPattern.matcher(content);
|
||||
int lastMatchEnd = 0;
|
||||
|
||||
while (matcher.find()) {
|
||||
builder.add(Collections.emptyList(), matcher.start() - lastMatchEnd);
|
||||
builder.add(styleClassFor(matcher), matcher.end() - matcher.start());
|
||||
lastMatchEnd = matcher.end();
|
||||
}
|
||||
|
||||
builder.add(Collections.emptyList(), content.length() - lastMatchEnd);
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
private Collection<String> styleClassFor(final Matcher matcher) {
|
||||
for (final EditorDocumentHighlightToken token : tokens) {
|
||||
if (matcher.group(token.groupName()) != null) {
|
||||
return token.styleClasses();
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("matched token without registered style");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package p.studio.workspaces.editor.syntaxhighlight;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EditorDocumentSyntaxHighlightingBash {
|
||||
public static final EditorDocumentSyntaxHighlighting BASH = new EditorDocumentSyntaxHighlighting(
|
||||
Pattern.compile(
|
||||
"(?<SHEBANG>^#![^\\n]*)"
|
||||
+ "|(?<COMMENT>(?m)(?<!\\S)#[^\\n]*)"
|
||||
+ "|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')"
|
||||
+ "|(?<VARIABLE>\\$\\{?[A-Za-z_][A-Za-z0-9_]*}?|\\$[0-9@*#?!$-])"
|
||||
+ "|(?<COMMAND>(?m)^[ \\t]*[A-Za-z_./-][A-Za-z0-9_./-]*)"
|
||||
+ "|(?<KEYWORD>\\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time)\\b)"
|
||||
+ "|(?<BUILTIN>\\b(?:echo|printf|read|cd|pwd|export|local|readonly|unset|return|shift|source|trap|exit|test)\\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("KEYWORD", "editor-syntax-bash-keyword"),
|
||||
new EditorDocumentHighlightToken("BUILTIN", "editor-syntax-bash-builtin"),
|
||||
new EditorDocumentHighlightToken("COMMAND", "editor-syntax-bash-command"),
|
||||
new EditorDocumentHighlightToken("OPERATOR", "editor-syntax-bash-operator")));
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.workspaces.editor.syntaxhighlight;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EditorDocumentSyntaxHighlightingJson {
|
||||
public static final EditorDocumentSyntaxHighlighting JSON = new EditorDocumentSyntaxHighlighting(
|
||||
Pattern.compile(
|
||||
"(?<KEY>\"(?:\\\\.|[^\"\\\\])*\"(?=\\s*:))"
|
||||
+ "|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\")"
|
||||
+ "|(?<NUMBER>-?\\b\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)"
|
||||
+ "|(?<BOOLEAN>\\b(?:true|false)\\b)"
|
||||
+ "|(?<NULL>\\bnull\\b)"
|
||||
+ "|(?<PUNCTUATION>[\\{\\}\\[\\],:])"),
|
||||
List.of(
|
||||
new EditorDocumentHighlightToken("KEY", "editor-syntax-json-key"),
|
||||
new EditorDocumentHighlightToken("STRING", "editor-syntax-json-string"),
|
||||
new EditorDocumentHighlightToken("NUMBER", "editor-syntax-json-number"),
|
||||
new EditorDocumentHighlightToken("BOOLEAN", "editor-syntax-json-boolean"),
|
||||
new EditorDocumentHighlightToken("NULL", "editor-syntax-json-null"),
|
||||
new EditorDocumentHighlightToken("PUNCTUATION", "editor-syntax-json-punctuation")));
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package p.studio.workspaces.editor.syntaxhighlight;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class EditorDocumentSyntaxHighlightingPlainText {
|
||||
public static final EditorDocumentSyntaxHighlighting PLAIN_TEXT = new EditorDocumentSyntaxHighlighting(
|
||||
null,
|
||||
List.of());
|
||||
}
|
||||
@ -536,6 +536,10 @@
|
||||
|
||||
.editor-workspace-code-area {
|
||||
-fx-background-color: #171c22;
|
||||
-fx-font-family: "JetBrains Mono", "Iosevka", "Cascadia Mono", "IBM Plex Mono", monospace;
|
||||
-fx-font-size: 14px;
|
||||
-fx-highlight-fill: #26405c;
|
||||
-fx-highlight-text-fill: #eef4fb;
|
||||
}
|
||||
|
||||
.editor-workspace-tab-button {
|
||||
@ -606,6 +610,28 @@
|
||||
-fx-background-color: #171c22;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area .paragraph-box {
|
||||
-fx-background-color: #171c22;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area .lineno {
|
||||
-fx-background-color: #12161c;
|
||||
-fx-text-fill: #6f7a86;
|
||||
-fx-padding: 0 12 0 12;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area .text {
|
||||
-fx-fill: #f2f6fb;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-text .text {
|
||||
-fx-fill: #eef3f8;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-fe .text {
|
||||
-fx-fill: #f2f6fb;
|
||||
}
|
||||
|
||||
.workspace-dock-pane {
|
||||
-fx-collapsible: true;
|
||||
-fx-background-color: transparent;
|
||||
@ -714,6 +740,18 @@
|
||||
-fx-max-height: 28;
|
||||
}
|
||||
|
||||
.editor-workspace-status-chip-type-text {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-border-color: #2a313c;
|
||||
-fx-text-fill: #c5d2de;
|
||||
}
|
||||
|
||||
.editor-workspace-status-chip-type-fe {
|
||||
-fx-background-color: #271a35;
|
||||
-fx-border-color: #8d67c7;
|
||||
-fx-text-fill: #f3ecff;
|
||||
}
|
||||
|
||||
.editor-workspace-status-chip-position {
|
||||
-fx-min-width: 44;
|
||||
-fx-pref-width: 44;
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
.editor-workspace-code-area-type-bash .text {
|
||||
-fx-fill: #edf2f7;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .lineno {
|
||||
-fx-text-fill: #788595;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .text.editor-syntax-bash-shebang {
|
||||
-fx-fill: #f0ae63;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .text.editor-syntax-bash-comment {
|
||||
-fx-fill: #7d8a98;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .text.editor-syntax-bash-string {
|
||||
-fx-fill: #dcbf88;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .text.editor-syntax-bash-variable {
|
||||
-fx-fill: #8fd4ff;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .text.editor-syntax-bash-keyword {
|
||||
-fx-fill: #d7a6ff;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .text.editor-syntax-bash-builtin {
|
||||
-fx-fill: #9fe2a0;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .text.editor-syntax-bash-command {
|
||||
-fx-fill: #7fc1ff;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-bash .text.editor-syntax-bash-operator {
|
||||
-fx-fill: #c7d3de;
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
.editor-workspace-code-area-type-json {
|
||||
-fx-highlight-fill: #204766;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-json .text {
|
||||
-fx-fill: #dff3ff;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-json .lineno {
|
||||
-fx-text-fill: #7e92a6;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-json .text.editor-syntax-json-key {
|
||||
-fx-fill: #8fd4ff;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-json .text.editor-syntax-json-string {
|
||||
-fx-fill: #d9c48f;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-json .text.editor-syntax-json-number {
|
||||
-fx-fill: #9ee39f;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-json .text.editor-syntax-json-boolean {
|
||||
-fx-fill: #f3a6d6;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-json .text.editor-syntax-json-null {
|
||||
-fx-fill: #c89cff;
|
||||
}
|
||||
|
||||
.editor-workspace-code-area-type-json .text.editor-syntax-json-punctuation {
|
||||
-fx-fill: #c8d5e2;
|
||||
}
|
||||
|
||||
.editor-workspace-status-chip-type-json {
|
||||
-fx-background-color: #132433;
|
||||
-fx-border-color: #4e88b8;
|
||||
-fx-text-fill: #e2f4ff;
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package p.studio.workspaces.editor;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
final class EditorDocumentPresentationRegistryTest {
|
||||
private final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry();
|
||||
|
||||
@Test
|
||||
void resolvesFrontendTypeIdsToFrontendPresentation() {
|
||||
assertEquals("fe", registry.resolve("pbs").styleKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesJsonTypeIdsToJsonPresentation() {
|
||||
final EditorDocumentPresentation presentation = registry.resolve("json");
|
||||
|
||||
assertEquals("json", presentation.styleKey());
|
||||
assertEquals(1, presentation.stylesheetUrls().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesBashTypeIdsToBashPresentation() {
|
||||
final EditorDocumentPresentation presentation = registry.resolve("bash");
|
||||
|
||||
assertEquals("bash", presentation.styleKey());
|
||||
assertEquals(1, presentation.stylesheetUrls().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToTextPresentationForUnknownTypeIds() {
|
||||
assertEquals("text", registry.resolve("markdown").styleKey());
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ final class EditorOpenFileSessionTest {
|
||||
@Test
|
||||
void openAddsNewFileAndMarksItActive() {
|
||||
final var session = new EditorOpenFileSession();
|
||||
final var file = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "fn main(): void\n", "LF");
|
||||
final var file = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "pbs", "fn main(): void\n", "LF");
|
||||
|
||||
session.open(file);
|
||||
|
||||
@ -22,8 +22,8 @@ final class EditorOpenFileSessionTest {
|
||||
@Test
|
||||
void openDoesNotDuplicateExistingTab() {
|
||||
final var session = new EditorOpenFileSession();
|
||||
final var first = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "a", "LF");
|
||||
final var second = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "b", "LF");
|
||||
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");
|
||||
|
||||
session.open(first);
|
||||
session.open(second);
|
||||
@ -36,8 +36,8 @@ final class EditorOpenFileSessionTest {
|
||||
@Test
|
||||
void activateSwitchesTheActiveTabWithinTheCurrentSession() {
|
||||
final var session = new EditorOpenFileSession();
|
||||
final var first = new EditorOpenFileBuffer(Path.of("src/main.pbs"), "main.pbs", "a", "LF");
|
||||
final var second = new EditorOpenFileBuffer(Path.of("src/other.pbs"), "other.pbs", "b", "LF");
|
||||
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");
|
||||
|
||||
session.open(first);
|
||||
session.open(second);
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package p.studio.vfs;
|
||||
|
||||
public final class VfsDocumentTypeIds {
|
||||
public static final String TEXT = "text";
|
||||
public static final String JSON = "json";
|
||||
public static final String BASH = "bash";
|
||||
|
||||
private VfsDocumentTypeIds() {
|
||||
}
|
||||
}
|
||||
@ -6,12 +6,14 @@ import java.util.Objects;
|
||||
public record VfsTextDocument(
|
||||
Path path,
|
||||
String documentName,
|
||||
String typeId,
|
||||
String content,
|
||||
String lineSeparator) implements VfsDocumentOpenResult {
|
||||
|
||||
public VfsTextDocument {
|
||||
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||
Objects.requireNonNull(documentName, "documentName");
|
||||
Objects.requireNonNull(typeId, "typeId");
|
||||
Objects.requireNonNull(content, "content");
|
||||
Objects.requireNonNull(lineSeparator, "lineSeparator");
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.vfs;
|
||||
|
||||
import p.studio.compiler.FrontendRegistryService;
|
||||
import p.studio.compiler.models.FrontendSpec;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
@ -12,6 +13,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@ -98,6 +100,7 @@ final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
||||
return new VfsTextDocument(
|
||||
normalizedPath,
|
||||
normalizedPath.getFileName().toString(),
|
||||
documentTypeId(normalizedPath, content),
|
||||
content,
|
||||
lineSeparator(content));
|
||||
}
|
||||
@ -118,6 +121,20 @@ final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private String documentTypeId(final Path path, final String content) {
|
||||
final String extension = extensionOf(path);
|
||||
if ("json".equals(extension) || "ndjson".equals(extension)) {
|
||||
return VfsDocumentTypeIds.JSON;
|
||||
}
|
||||
if (isBashDocument(path, extension, content)) {
|
||||
return VfsDocumentTypeIds.BASH;
|
||||
}
|
||||
if (isFrontendSourceDocument(extension)) {
|
||||
return projectContext.languageId();
|
||||
}
|
||||
return VfsDocumentTypeIds.TEXT;
|
||||
}
|
||||
|
||||
private VfsProjectNode buildNode(
|
||||
final Path path,
|
||||
final String displayName,
|
||||
@ -194,6 +211,39 @@ final class FilesystemProjectDocumentVfs implements ProjectDocumentVfs {
|
||||
return Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private boolean isFrontendSourceDocument(final String extension) {
|
||||
if (extension.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return FrontendRegistryService.getFrontendSpec(projectContext.languageId())
|
||||
.map(FrontendSpec::getAllowedExtensions)
|
||||
.stream()
|
||||
.flatMap(allowedExtensions -> allowedExtensions.stream())
|
||||
.anyMatch(allowedExtension -> allowedExtension.equalsIgnoreCase(extension));
|
||||
}
|
||||
|
||||
private boolean isBashDocument(final Path path, final String extension, final String content) {
|
||||
if (Set.of("sh", "bash", "bashrc", "bash_profile").contains(extension)) {
|
||||
return true;
|
||||
}
|
||||
final String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
|
||||
if (".bashrc".equals(fileName) || ".bash_profile".equals(fileName)) {
|
||||
return true;
|
||||
}
|
||||
final int firstLineBreak = content.indexOf('\n');
|
||||
final String firstLine = firstLineBreak >= 0 ? content.substring(0, firstLineBreak) : content;
|
||||
return firstLine.startsWith("#!") && (firstLine.contains("bash") || firstLine.contains("/sh"));
|
||||
}
|
||||
|
||||
private String extensionOf(final Path path) {
|
||||
final String fileName = path.getFileName().toString();
|
||||
final int dot = fileName.lastIndexOf('.');
|
||||
if (dot < 0 || dot == fileName.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(dot + 1).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private boolean containsNulByte(final byte[] bytes) {
|
||||
for (final byte value : bytes) {
|
||||
if (value == 0) {
|
||||
|
||||
@ -43,10 +43,50 @@ final class FilesystemProjectDocumentVfsTest {
|
||||
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
||||
|
||||
assertEquals("main.pbs", document.documentName());
|
||||
assertEquals("pbs", document.typeId());
|
||||
assertEquals("LF", document.lineSeparator());
|
||||
assertTrue(document.content().contains("fn main()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void openDocumentClassifiesJsonFilesWithJsonTypeIdentifier() throws Exception {
|
||||
final Path file = tempDir.resolve("prometeu.json");
|
||||
Files.writeString(file, "{\n \"name\": \"Example\"\n}\n");
|
||||
|
||||
final ProjectDocumentVfs vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||
|
||||
final VfsDocumentOpenResult result = vfs.openDocument(file);
|
||||
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
||||
|
||||
assertEquals(VfsDocumentTypeIds.JSON, document.typeId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void openDocumentClassifiesNdjsonFilesWithJsonTypeIdentifier() throws Exception {
|
||||
final Path file = tempDir.resolve("events.ndjson");
|
||||
Files.writeString(file, "{\"event\":\"start\"}\n{\"event\":\"done\"}\n");
|
||||
|
||||
final ProjectDocumentVfs vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||
|
||||
final VfsDocumentOpenResult result = vfs.openDocument(file);
|
||||
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
||||
|
||||
assertEquals(VfsDocumentTypeIds.JSON, document.typeId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void openDocumentClassifiesBashScriptsWithBashTypeIdentifier() throws Exception {
|
||||
final Path file = tempDir.resolve("script.sh");
|
||||
Files.writeString(file, "#!/usr/bin/env bash\nprintf \"hi\"\n");
|
||||
|
||||
final ProjectDocumentVfs vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext());
|
||||
|
||||
final VfsDocumentOpenResult result = vfs.openDocument(file);
|
||||
final VfsTextDocument document = assertInstanceOf(VfsTextDocument.class, result);
|
||||
|
||||
assertEquals(VfsDocumentTypeIds.BASH, document.typeId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void openDocumentRejectsBinaryLikeFiles() throws Exception {
|
||||
final Path file = tempDir.resolve("sprite.bin");
|
||||
|
||||
@ -208,6 +208,126 @@
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "0 assets loaded",
|
||||
"severity" : "SUCCESS",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Assets",
|
||||
"message" : "Asset scan started",
|
||||
"severity" : "INFO",
|
||||
"sticky" : false
|
||||
}, {
|
||||
"source" : "Studio",
|
||||
"message" : "Project ready",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user