From f368ed94d229059376ff0e4cd01c4f96dbf5409c Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 3 Apr 2026 11:19:45 +0100 Subject: [PATCH] PBS highlight colors --- .../pbs/parser/PbsDeclarationParser.java | 2 +- .../pbs/parser/PbsExprParserContext.java | 2 +- .../main/resources/stdlib/1/sdk/log/main.pbs | 4 +- .../compiler/pbs/parser/PbsParserTest.java | 13 ++--- .../java/p/studio/lsp/LspServiceImplTest.java | 6 +++ .../EditorDocumentHighlightingRouter.java | 7 ++- .../EditorDocumentPresentationRegistry.java | 48 ++++++++++++++----- .../EditorDocumentSemanticHighlighting.java | 21 ++++++++ .../EditorDocumentSyntaxHighlighting.java | 4 ++ .../EditorDocumentHighlightingRouterTest.java | 45 +++++++++++++++-- ...ditorDocumentPresentationRegistryTest.java | 6 ++- test-projects/main/src/main.pbs | 8 ++-- 12 files changed, 129 insertions(+), 37 deletions(-) diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsDeclarationParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsDeclarationParser.java index aa246a27..11fc83ee 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsDeclarationParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsDeclarationParser.java @@ -574,7 +574,7 @@ final class PbsDeclarationParser { } private PbsToken consumeCallableName() { - if (cursor.check(PbsTokenKind.IDENTIFIER) || cursor.check(PbsTokenKind.ERROR)) { + if (cursor.check(PbsTokenKind.IDENTIFIER)) { return cursor.advance(); } final var token = cursor.peek(); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParserContext.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParserContext.java index be841110..e81cfb02 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParserContext.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParserContext.java @@ -39,7 +39,7 @@ final class PbsExprParserContext { } PbsToken consumeMemberName(final String message) { - if (cursor.check(PbsTokenKind.IDENTIFIER) || cursor.check(PbsTokenKind.ERROR)) { + if (cursor.check(PbsTokenKind.IDENTIFIER)) { return cursor.advance(); } final var token = cursor.peek(); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/main.pbs b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/main.pbs index a25e4c69..1d7ee6e7 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/main.pbs +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/main.pbs @@ -30,7 +30,7 @@ declare service Log LowLog.write(3, msg); } - fn error(msg: str) -> void + fn failure(msg: str) -> void { LowLog.write(4, msg); } @@ -55,7 +55,7 @@ declare service Log LowLog.write_tag(3, tag, msg); } - fn error(tag: int, msg: str) -> void + fn failure(tag: int, msg: str) -> void { LowLog.write_tag(4, tag, msg); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java index cacd1f16..0c428e4a 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java @@ -173,7 +173,7 @@ class PbsParserTest { } @Test - void shouldParseServiceAndMemberNamesUsingErrorKeyword() { + void shouldRejectErrorKeywordAsCallableAndMemberName() { final var source = """ declare service Log { fn error(msg: str) -> void { return; } @@ -181,20 +181,17 @@ class PbsParserTest { } fn run() -> void { - Log.error("oops"); - Log.error(7, "oops"); + Log.failure("oops"); + Log.failure(7, "oops"); return; } """; final var diagnostics = DiagnosticSink.empty(); final var fileId = new FileId(0); - final PbsAst.File ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); - assertTrue(diagnostics.isEmpty(), "Parser should accept 'error' as callable/member name"); - final var service = assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().getFirst()); - assertEquals(2, service.methods().size()); - assertEquals("error", service.methods().getFirst().name()); + assertFalse(diagnostics.isEmpty(), "Parser should reject reserved keyword 'error' as callable/member name"); } @Test diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java index 175ef67f..55ffc30b 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java @@ -219,6 +219,7 @@ final class LspServiceImplTest { pub fn helper() -> void; pub fn helper_result() -> result int; pub error MyError; + pub service Log; """); Files.writeString(src.resolve("helper.pbs"), "fn helper() -> void {}\n"); Files.writeString(src.resolve("helper_result.pbs"), """ @@ -230,6 +231,11 @@ final class LspServiceImplTest { return ok(1); } """); + Files.writeString(src.resolve("log.pbs"), """ + declare service Log { + fn failure(message: string) -> void; + } + """); return tempDir; } 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 18b3ce73..a8c3f5b5 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 @@ -11,16 +11,19 @@ final class EditorDocumentHighlightingRouter { final EditorOpenFileBuffer fileBuffer, final EditorDocumentPresentation presentation, final LspAnalyzeDocumentResult analysis) { + final var localHighlighting = presentation.highlight(fileBuffer.content()); if (fileBuffer.frontendDocument() && analysis != null && presentation.supportsSemanticHighlighting() && !analysis.semanticHighlights().isEmpty()) { return new EditorDocumentHighlightingResult( EditorDocumentHighlightOwner.LSP, - EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights())); + EditorDocumentSemanticHighlighting.overlay( + localHighlighting, + EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights()))); } return new EditorDocumentHighlightingResult( EditorDocumentHighlightOwner.LOCAL, - presentation.highlight(fileBuffer.content())); + localHighlighting); } } 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 e02060cb..f50976be 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,6 +1,7 @@ package p.studio.workspaces.editor; import p.studio.compiler.FrontendRegistryService; +import p.studio.compiler.models.FrontendSemanticPresentationSpec; import p.studio.lsp.dtos.LspSemanticPresentationDTO; import p.studio.vfs.messages.VfsDocumentTypeIds; import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting; @@ -55,21 +56,19 @@ final class EditorDocumentPresentationRegistry { 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()); - } + final var frontendSpec = FrontendRegistryService.getFrontendSpec(normalizedTypeId).orElseThrow(); + final FrontendSemanticPresentationSpec frontendPresentationSpec = frontendSpec.getSemanticPresentation(); + final java.util.List stylesheetUrls = resolveFrontendStylesheets( + semanticPresentation == null ? java.util.List.of() : semanticPresentation.resources(), + frontendPresentationSpec.resources()); + final java.util.List semanticKeys = semanticPresentation == null + ? frontendPresentationSpec.semanticKeys() + : semanticPresentation.semanticKeys(); return new EditorDocumentPresentation( normalizedTypeId, - semanticPresentation.resources().stream() - .map(EditorDocumentPresentationRegistry::resourceStylesheet) - .flatMap(Optional::stream) - .toList(), - semanticPresentation.semanticKeys(), - EditorDocumentSyntaxHighlighting.plainText()); + stylesheetUrls, + semanticKeys, + frontendSyntaxHighlighting(normalizedTypeId)); } private String normalize(final String typeId) { @@ -93,6 +92,29 @@ final class EditorDocumentPresentationRegistry { return Optional.of(resource.toExternalForm()); } + private static java.util.List resolveFrontendStylesheets( + final java.util.List primaryResources, + final java.util.List fallbackResources) { + final java.util.List primary = primaryResources.stream() + .map(EditorDocumentPresentationRegistry::resourceStylesheet) + .flatMap(Optional::stream) + .toList(); + if (!primary.isEmpty()) { + return primary; + } + return fallbackResources.stream() + .map(EditorDocumentPresentationRegistry::resourceStylesheet) + .flatMap(Optional::stream) + .toList(); + } + + private static EditorDocumentSyntaxHighlighting frontendSyntaxHighlighting(final String normalizedTypeId) { + return switch (normalizedTypeId) { + case "pbs" -> EditorDocumentSyntaxHighlighting.pbs(); + default -> EditorDocumentSyntaxHighlighting.plainText(); + }; + } + private static String normalizeResourcePath(final String resourcePath) { final String normalized = Objects.requireNonNull(resourcePath, "resourcePath").trim(); if (normalized.isEmpty()) { 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 2b03a50f..89dd1487 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 @@ -7,6 +7,7 @@ import p.studio.lsp.dtos.LspHighlightSpanDTO; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; public final class EditorDocumentSemanticHighlighting { @@ -40,4 +41,24 @@ public final class EditorDocumentSemanticHighlighting { } return builder.create(); } + + public static StyleSpans> overlay( + final StyleSpans> baseHighlighting, + final StyleSpans> semanticHighlighting) { + return baseHighlighting.overlay(semanticHighlighting, EditorDocumentSemanticHighlighting::mergeStyles); + } + + private static Collection mergeStyles( + final Collection baseStyles, + final Collection semanticStyles) { + if (baseStyles.isEmpty()) { + return semanticStyles; + } + if (semanticStyles.isEmpty()) { + return baseStyles; + } + final LinkedHashSet merged = new LinkedHashSet<>(baseStyles); + merged.addAll(semanticStyles); + return List.copyOf(merged); + } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java index cb59a654..50af86cd 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java @@ -36,6 +36,10 @@ public record EditorDocumentSyntaxHighlighting( return EditorDocumentSyntaxHighlightingBash.BASH; } + public static EditorDocumentSyntaxHighlighting pbs() { + return EditorDocumentSyntaxHighlightingPbs.PBS; + } + public StyleSpans> highlight(final String content) { final StyleSpansBuilder> builder = new StyleSpansBuilder<>(); if (tokenPattern == null) { 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 92ecc737..04bf2713 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 @@ -46,7 +46,8 @@ final class EditorDocumentHighlightingRouterTest { analysis); assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertEquals(List.of("editor-semantic-pbs-keyword"), List.copyOf(result.styleSpans().getStyleSpan(0).getStyle())); + assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); + assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); } @Test @@ -80,7 +81,7 @@ final class EditorDocumentHighlightingRouterTest { } @Test - void frontendDocumentsDegradeToLocalWhenSemanticPresentationResourcesAreUnavailable() { + void frontendDocumentsFallbackToLocalPbsSyntaxWhenSemanticPresentationResourcesAreUnavailable() { final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( Path.of("/tmp/example/src/main.pbs"), @@ -108,8 +109,9 @@ final class EditorDocumentHighlightingRouterTest { registry.resolve("pbs", analysis.semanticPresentation()), analysis); - assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); - assertTrue(result.styleSpans().getStyleSpan(0).getStyle().isEmpty()); + assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); + assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); + assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); } @Test @@ -145,6 +147,41 @@ final class EditorDocumentHighlightingRouterTest { assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); } + @Test + void frontendDocumentsKeepLocalSyntaxForRangesWithoutSemanticCoverage() { + final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); + final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( + Path.of("/tmp/example/src/main.pbs"), + "main.pbs", + "pbs", + "fn main() -> void { Game.tick(1); }", + "LF", + true, + VfsDocumentAccessMode.READ_ONLY, + false); + + final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( + new LspSessionStateDTO(true, List.of("highlight")), + new LspSemanticPresentationDTO( + List.of("pbs-service"), + List.of("/themes/pbs/semantic-highlighting.css")), + List.of(), + List.of(new LspHighlightSpanDTO(new LspRangeDTO(19, 23), "pbs-service")), + List.of(), + List.of(), + List.of()); + + final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( + fileBuffer, + registry.resolve("pbs", analysis.semanticPresentation()), + analysis); + + assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); + assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); + assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); + assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); + } + private boolean containsStyle( final org.fxmisc.richtext.model.StyleSpans> styleSpans, final String styleClass) { 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 2f88576a..458e9f4d 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 @@ -4,6 +4,7 @@ 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.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; final class EditorDocumentPresentationRegistryTest { @@ -24,7 +25,7 @@ final class EditorDocumentPresentationRegistryTest { } @Test - void missingFrontendResourcesDegradeToPlainFrontendPresentation() { + void missingFrontendResourcesFallbackToFrontendOwnedPresentationResources() { final EditorDocumentPresentation presentation = registry.resolve( "pbs", new LspSemanticPresentationDTO( @@ -33,7 +34,8 @@ final class EditorDocumentPresentationRegistryTest { assertEquals("pbs", presentation.styleKey()); assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys()); - assertTrue(presentation.stylesheetUrls().isEmpty()); + assertEquals(1, presentation.stylesheetUrls().size()); + assertFalse(presentation.highlight("fn main() -> void {}").getStyleSpan(0).getStyle().isEmpty()); } @Test diff --git a/test-projects/main/src/main.pbs b/test-projects/main/src/main.pbs index 516207ff..57ed553e 100644 --- a/test-projects/main/src/main.pbs +++ b/test-projects/main/src/main.pbs @@ -28,7 +28,7 @@ fn frame() -> void if (loading_handle == -1) { let t = Assets.load(assets.ui.atlas2, 3); if (t.status != 0) { - Log.error("load failed"); + Log.failure("load failed"); } else { loading_handle = t.loading_handle; Log.info("state: loading"); @@ -38,12 +38,12 @@ fn frame() -> void if (s == 2) { let commit_status = Assets.commit(loading_handle); if (commit_status != 0) { - Log.error("commit failed"); + Log.failure("commit failed"); } } else if (s == 3) { let sprite_status = Gfx.set_sprite(3, 10, 150, 150, 0, 0, true, false, false, 1); if (sprite_status != 0) { - Log.error("set_sprite failed"); + Log.failure("set_sprite failed"); } } else { Log.info("state: waiting"); @@ -100,7 +100,7 @@ fn frame() -> void } else if (total == 50) { - Log.error("50 is the magic number!"); + Log.failure("50 is the magic number!"); } else if (total == 15) {