diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css index 72186934..fa2b662c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css @@ -10,47 +10,47 @@ -fx-text-fill: #71859a; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-keyword { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-keyword { -fx-fill: #8dc7ff; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-function { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-function { -fx-fill: #f0cb79; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-type { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-type { -fx-fill: #9ddba8; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-binding { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-binding { -fx-fill: #ffb1c8; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-string { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-string { -fx-fill: #e2c48c; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-number { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-number { -fx-fill: #c4e58a; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-comment { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-comment { -fx-fill: #6f8192; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-literal { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-literal { -fx-fill: #c8a2ff; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-operator { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-operator { -fx-fill: #dbe6f1; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-punctuation { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-punctuation { -fx-fill: #adc1d4; } -.editor-workspace-code-area-type-pbs .text.editor-syntax-pbs-identifier { +.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-identifier { -fx-fill: #edf4fb; } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java index 4405af32..18b3ce73 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java @@ -13,6 +13,7 @@ final class EditorDocumentHighlightingRouter { final LspAnalyzeDocumentResult analysis) { if (fileBuffer.frontendDocument() && analysis != null + && presentation.supportsSemanticHighlighting() && !analysis.semanticHighlights().isEmpty()) { return new EditorDocumentHighlightingResult( EditorDocumentHighlightOwner.LSP, diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java index 40b53b13..2b7dd55f 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java @@ -10,15 +10,21 @@ import java.util.Objects; record EditorDocumentPresentation( String styleKey, List stylesheetUrls, + List semanticKeys, EditorDocumentSyntaxHighlighting syntaxHighlighting) { EditorDocumentPresentation { styleKey = Objects.requireNonNull(styleKey, "styleKey"); stylesheetUrls = List.copyOf(Objects.requireNonNull(stylesheetUrls, "stylesheetUrls")); + semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys")); syntaxHighlighting = Objects.requireNonNull(syntaxHighlighting, "syntaxHighlighting"); } StyleSpans> highlight(final String content) { return syntaxHighlighting.highlight(content); } + + boolean supportsSemanticHighlighting() { + return !semanticKeys.isEmpty() && !stylesheetUrls.isEmpty(); + } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java index 69f60d1b..e02060cb 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java @@ -1,36 +1,47 @@ package p.studio.workspaces.editor; import p.studio.compiler.FrontendRegistryService; +import p.studio.lsp.dtos.LspSemanticPresentationDTO; import p.studio.vfs.messages.VfsDocumentTypeIds; import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting; +import java.net.URL; import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Logger; final class EditorDocumentPresentationRegistry { + private static final Logger LOGGER = Logger.getLogger(EditorDocumentPresentationRegistry.class.getName()); private static final EditorDocumentPresentation TEXT_PRESENTATION = new EditorDocumentPresentation( "text", java.util.List.of(), + java.util.List.of(), EditorDocumentSyntaxHighlighting.plainText()); private static final EditorDocumentPresentation JSON_PRESENTATION = new EditorDocumentPresentation( "json", java.util.List.of(stylesheet("presentations/json.css")), + java.util.List.of(), EditorDocumentSyntaxHighlighting.json()); private static final EditorDocumentPresentation BASH_PRESENTATION = new EditorDocumentPresentation( "bash", java.util.List.of(stylesheet("presentations/bash.css")), + java.util.List.of(), EditorDocumentSyntaxHighlighting.bash()); - private static final EditorDocumentPresentation FRONTEND_PRESENTATION = new EditorDocumentPresentation( - "fe", - java.util.List.of(stylesheet("presentations/fe.css")), - EditorDocumentSyntaxHighlighting.plainText()); EditorDocumentPresentation resolve(final String typeId) { + return resolve(typeId, null); + } + + EditorDocumentPresentation resolve( + final String typeId, + final LspSemanticPresentationDTO semanticPresentation) { final String normalizedTypeId = normalize(typeId); if (normalizedTypeId.isBlank()) { return TEXT_PRESENTATION; } if (FrontendRegistryService.getFrontendSpec(normalizedTypeId).isPresent()) { - return FRONTEND_PRESENTATION; + return frontendPresentation(normalizedTypeId, semanticPresentation); } if (VfsDocumentTypeIds.JSON.equals(normalizedTypeId)) { return JSON_PRESENTATION; @@ -41,6 +52,26 @@ final class EditorDocumentPresentationRegistry { return TEXT_PRESENTATION; } + private EditorDocumentPresentation frontendPresentation( + final String normalizedTypeId, + final LspSemanticPresentationDTO semanticPresentation) { + if (semanticPresentation == null) { + return new EditorDocumentPresentation( + normalizedTypeId, + java.util.List.of(), + java.util.List.of(), + EditorDocumentSyntaxHighlighting.plainText()); + } + return new EditorDocumentPresentation( + normalizedTypeId, + semanticPresentation.resources().stream() + .map(EditorDocumentPresentationRegistry::resourceStylesheet) + .flatMap(Optional::stream) + .toList(), + semanticPresentation.semanticKeys(), + EditorDocumentSyntaxHighlighting.plainText()); + } + private String normalize(final String typeId) { return typeId == null ? "" : typeId.trim().toLowerCase(Locale.ROOT); } @@ -51,4 +82,22 @@ final class EditorDocumentPresentationRegistry { "missing editor presentation stylesheet: " + relativePath) .toExternalForm(); } + + private static Optional resourceStylesheet(final String resourcePath) { + final String normalizedPath = normalizeResourcePath(resourcePath); + final URL resource = EditorDocumentPresentationRegistry.class.getResource(normalizedPath); + if (resource == null) { + LOGGER.fine("missing frontend semantic presentation resource: " + resourcePath); + return Optional.empty(); + } + return Optional.of(resource.toExternalForm()); + } + + private static String normalizeResourcePath(final String resourcePath) { + final String normalized = Objects.requireNonNull(resourcePath, "resourcePath").trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("resourcePath cannot be blank"); + } + return normalized.startsWith("/") ? normalized : "/" + normalized; + } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java index 4ccf4d7a..228ad75c 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java @@ -124,10 +124,12 @@ public final class EditorWorkspace extends Workspace { } final var fileBuffer = activeFile.orElseThrow(); - final EditorDocumentPresentation presentation = presentationRegistry.resolve(fileBuffer.typeId()); final LspAnalyzeDocumentResult analysis = fileBuffer.frontendDocument() ? prometeuLspService.analyzeDocument(new LspAnalyzeDocumentRequest(fileBuffer.path())) : null; + final EditorDocumentPresentation presentation = presentationRegistry.resolve( + fileBuffer.typeId(), + analysis == null ? null : analysis.semanticPresentation()); final EditorDocumentHighlightingResult highlighting = EditorDocumentHighlightingRouter.route( fileBuffer, presentation, diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java index b100aada..2b03a50f 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java @@ -28,7 +28,7 @@ public final class EditorDocumentSemanticHighlighting { builder.add(Collections.emptyList(), start - cursor); } if (end > start) { - builder.add(List.of("editor-syntax-" + highlight.semanticKey()), end - start); + builder.add(List.of("editor-semantic-" + highlight.semanticKey()), end - start); cursor = end; } } diff --git a/prometeu-studio/src/main/resources/themes/default-prometeu.css b/prometeu-studio/src/main/resources/themes/default-prometeu.css index d0e9f2fb..adb389eb 100644 --- a/prometeu-studio/src/main/resources/themes/default-prometeu.css +++ b/prometeu-studio/src/main/resources/themes/default-prometeu.css @@ -708,10 +708,6 @@ -fx-fill: #eef3f8; } -.editor-workspace-code-area-type-fe .text { - -fx-fill: #f2f6fb; -} - .editor-workspace-command-bar { -fx-padding: 10 12 0 12; -fx-alignment: center-left; @@ -880,12 +876,6 @@ -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; diff --git a/prometeu-studio/src/main/resources/themes/editor/presentations/fe.css b/prometeu-studio/src/main/resources/themes/editor/presentations/fe.css deleted file mode 100644 index b2ee77a3..00000000 --- a/prometeu-studio/src/main/resources/themes/editor/presentations/fe.css +++ /dev/null @@ -1,61 +0,0 @@ -.editor-workspace-code-area-type-fe { - -fx-highlight-fill: #1b3244; -} - -.editor-workspace-code-area-type-fe .text { - -fx-fill: #edf4fb; -} - -.editor-workspace-code-area-type-fe .lineno { - -fx-text-fill: #71859a; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-keyword { - -fx-fill: #8dc7ff; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-callable { - -fx-fill: #f0cb79; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-type { - -fx-fill: #9ddba8; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-binding { - -fx-fill: #ffb1c8; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-string { - -fx-fill: #e2c48c; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-number { - -fx-fill: #c4e58a; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-comment { - -fx-fill: #6f8192; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-literal { - -fx-fill: #c8a2ff; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-operator { - -fx-fill: #dbe6f1; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-punctuation { - -fx-fill: #adc1d4; -} - -.editor-workspace-code-area-type-fe .text.editor-syntax-fe-identifier { - -fx-fill: #edf4fb; -} - -.editor-workspace-status-chip-type-fe { - -fx-background-color: #152432; - -fx-border-color: #4d8db9; - -fx-text-fill: #e8f5ff; -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java index 944ddfb9..3a48a7e2 100644 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java +++ b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java @@ -12,6 +12,7 @@ import java.nio.file.Path; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; final class EditorDocumentHighlightingRouterTest { @Test @@ -29,18 +30,21 @@ final class EditorDocumentHighlightingRouterTest { final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO(List.of(), List.of()), + new LspSemanticPresentationDTO( + List.of("pbs-keyword"), + List.of("/themes/pbs/semantic-highlighting.css")), List.of(), - List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "fe-keyword")), + List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword")), List.of(), List.of()); final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( fileBuffer, - registry.resolve("pbs"), + registry.resolve("pbs", analysis.semanticPresentation()), analysis); assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); + assertEquals(List.of("editor-semantic-pbs-keyword"), List.copyOf(result.styleSpans().getStyleSpan(0).getStyle())); } @Test @@ -71,4 +75,36 @@ final class EditorDocumentHighlightingRouterTest { assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); } + + @Test + void frontendDocumentsDegradeToLocalWhenSemanticPresentationResourcesAreUnavailable() { + final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); + final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( + Path.of("/tmp/example/src/main.pbs"), + "main.pbs", + "pbs", + "fn main() -> void {}", + "LF", + true, + VfsDocumentAccessMode.READ_ONLY, + false); + + final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( + new LspSessionStateDTO(true, List.of("highlight")), + new LspSemanticPresentationDTO( + List.of("pbs-keyword"), + List.of("/themes/pbs/missing.css")), + List.of(), + List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword")), + List.of(), + List.of()); + + final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( + fileBuffer, + registry.resolve("pbs", analysis.semanticPresentation()), + analysis); + + assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); + assertTrue(result.styleSpans().getStyleSpan(0).getStyle().isEmpty()); + } } diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java index 7f6379e8..ab5d78e4 100644 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java +++ b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java @@ -1,15 +1,39 @@ package p.studio.workspaces.editor; import org.junit.jupiter.api.Test; +import p.studio.lsp.dtos.LspSemanticPresentationDTO; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; final class EditorDocumentPresentationRegistryTest { private final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); @Test void resolvesFrontendTypeIdsToFrontendPresentation() { - assertEquals("fe", registry.resolve("pbs").styleKey()); + final EditorDocumentPresentation presentation = registry.resolve( + "pbs", + new LspSemanticPresentationDTO( + java.util.List.of("pbs-keyword"), + java.util.List.of("/themes/pbs/semantic-highlighting.css"))); + + assertEquals("pbs", presentation.styleKey()); + assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys()); + assertEquals(1, presentation.stylesheetUrls().size()); + assertTrue(presentation.stylesheetUrls().getFirst().endsWith("/themes/pbs/semantic-highlighting.css")); + } + + @Test + void missingFrontendResourcesDegradeToPlainFrontendPresentation() { + final EditorDocumentPresentation presentation = registry.resolve( + "pbs", + new LspSemanticPresentationDTO( + java.util.List.of("pbs-keyword"), + java.util.List.of("/themes/pbs/missing.css"))); + + assertEquals("pbs", presentation.styleKey()); + assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys()); + assertTrue(presentation.stylesheetUrls().isEmpty()); } @Test