PBS highlight colors

This commit is contained in:
bQUARKz 2026-04-03 11:19:45 +01:00
parent 5bbd753ba0
commit f368ed94d2
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
12 changed files with 129 additions and 37 deletions

View File

@ -574,7 +574,7 @@ final class PbsDeclarationParser {
} }
private PbsToken consumeCallableName() { private PbsToken consumeCallableName() {
if (cursor.check(PbsTokenKind.IDENTIFIER) || cursor.check(PbsTokenKind.ERROR)) { if (cursor.check(PbsTokenKind.IDENTIFIER)) {
return cursor.advance(); return cursor.advance();
} }
final var token = cursor.peek(); final var token = cursor.peek();

View File

@ -39,7 +39,7 @@ final class PbsExprParserContext {
} }
PbsToken consumeMemberName(final String message) { PbsToken consumeMemberName(final String message) {
if (cursor.check(PbsTokenKind.IDENTIFIER) || cursor.check(PbsTokenKind.ERROR)) { if (cursor.check(PbsTokenKind.IDENTIFIER)) {
return cursor.advance(); return cursor.advance();
} }
final var token = cursor.peek(); final var token = cursor.peek();

View File

@ -30,7 +30,7 @@ declare service Log
LowLog.write(3, msg); LowLog.write(3, msg);
} }
fn error(msg: str) -> void fn failure(msg: str) -> void
{ {
LowLog.write(4, msg); LowLog.write(4, msg);
} }
@ -55,7 +55,7 @@ declare service Log
LowLog.write_tag(3, tag, msg); 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); LowLog.write_tag(4, tag, msg);
} }

View File

@ -173,7 +173,7 @@ class PbsParserTest {
} }
@Test @Test
void shouldParseServiceAndMemberNamesUsingErrorKeyword() { void shouldRejectErrorKeywordAsCallableAndMemberName() {
final var source = """ final var source = """
declare service Log { declare service Log {
fn error(msg: str) -> void { return; } fn error(msg: str) -> void { return; }
@ -181,20 +181,17 @@ class PbsParserTest {
} }
fn run() -> void { fn run() -> void {
Log.error("oops"); Log.failure("oops");
Log.error(7, "oops"); Log.failure(7, "oops");
return; return;
} }
"""; """;
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0); 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"); assertFalse(diagnostics.isEmpty(), "Parser should reject reserved keyword '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());
} }
@Test @Test

View File

@ -219,6 +219,7 @@ final class LspServiceImplTest {
pub fn helper() -> void; pub fn helper() -> void;
pub fn helper_result() -> result<MyError> int; pub fn helper_result() -> result<MyError> int;
pub error MyError; pub error MyError;
pub service Log;
"""); """);
Files.writeString(src.resolve("helper.pbs"), "fn helper() -> void {}\n"); Files.writeString(src.resolve("helper.pbs"), "fn helper() -> void {}\n");
Files.writeString(src.resolve("helper_result.pbs"), """ Files.writeString(src.resolve("helper_result.pbs"), """
@ -230,6 +231,11 @@ final class LspServiceImplTest {
return ok(1); return ok(1);
} }
"""); """);
Files.writeString(src.resolve("log.pbs"), """
declare service Log {
fn failure(message: string) -> void;
}
""");
return tempDir; return tempDir;
} }

View File

@ -11,16 +11,19 @@ final class EditorDocumentHighlightingRouter {
final EditorOpenFileBuffer fileBuffer, final EditorOpenFileBuffer fileBuffer,
final EditorDocumentPresentation presentation, final EditorDocumentPresentation presentation,
final LspAnalyzeDocumentResult analysis) { final LspAnalyzeDocumentResult analysis) {
final var localHighlighting = presentation.highlight(fileBuffer.content());
if (fileBuffer.frontendDocument() if (fileBuffer.frontendDocument()
&& analysis != null && analysis != null
&& presentation.supportsSemanticHighlighting() && presentation.supportsSemanticHighlighting()
&& !analysis.semanticHighlights().isEmpty()) { && !analysis.semanticHighlights().isEmpty()) {
return new EditorDocumentHighlightingResult( return new EditorDocumentHighlightingResult(
EditorDocumentHighlightOwner.LSP, EditorDocumentHighlightOwner.LSP,
EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights())); EditorDocumentSemanticHighlighting.overlay(
localHighlighting,
EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights())));
} }
return new EditorDocumentHighlightingResult( return new EditorDocumentHighlightingResult(
EditorDocumentHighlightOwner.LOCAL, EditorDocumentHighlightOwner.LOCAL,
presentation.highlight(fileBuffer.content())); localHighlighting);
} }
} }

View File

@ -1,6 +1,7 @@
package p.studio.workspaces.editor; package p.studio.workspaces.editor;
import p.studio.compiler.FrontendRegistryService; import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.models.FrontendSemanticPresentationSpec;
import p.studio.lsp.dtos.LspSemanticPresentationDTO; import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import p.studio.vfs.messages.VfsDocumentTypeIds; import p.studio.vfs.messages.VfsDocumentTypeIds;
import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting; import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting;
@ -55,21 +56,19 @@ final class EditorDocumentPresentationRegistry {
private EditorDocumentPresentation frontendPresentation( private EditorDocumentPresentation frontendPresentation(
final String normalizedTypeId, final String normalizedTypeId,
final LspSemanticPresentationDTO semanticPresentation) { final LspSemanticPresentationDTO semanticPresentation) {
if (semanticPresentation == null) { final var frontendSpec = FrontendRegistryService.getFrontendSpec(normalizedTypeId).orElseThrow();
final FrontendSemanticPresentationSpec frontendPresentationSpec = frontendSpec.getSemanticPresentation();
final java.util.List<String> stylesheetUrls = resolveFrontendStylesheets(
semanticPresentation == null ? java.util.List.of() : semanticPresentation.resources(),
frontendPresentationSpec.resources());
final java.util.List<String> semanticKeys = semanticPresentation == null
? frontendPresentationSpec.semanticKeys()
: semanticPresentation.semanticKeys();
return new EditorDocumentPresentation( return new EditorDocumentPresentation(
normalizedTypeId, normalizedTypeId,
java.util.List.of(), stylesheetUrls,
java.util.List.of(), semanticKeys,
EditorDocumentSyntaxHighlighting.plainText()); frontendSyntaxHighlighting(normalizedTypeId));
}
return new EditorDocumentPresentation(
normalizedTypeId,
semanticPresentation.resources().stream()
.map(EditorDocumentPresentationRegistry::resourceStylesheet)
.flatMap(Optional::stream)
.toList(),
semanticPresentation.semanticKeys(),
EditorDocumentSyntaxHighlighting.plainText());
} }
private String normalize(final String typeId) { private String normalize(final String typeId) {
@ -93,6 +92,29 @@ final class EditorDocumentPresentationRegistry {
return Optional.of(resource.toExternalForm()); return Optional.of(resource.toExternalForm());
} }
private static java.util.List<String> resolveFrontendStylesheets(
final java.util.List<String> primaryResources,
final java.util.List<String> fallbackResources) {
final java.util.List<String> 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) { private static String normalizeResourcePath(final String resourcePath) {
final String normalized = Objects.requireNonNull(resourcePath, "resourcePath").trim(); final String normalized = Objects.requireNonNull(resourcePath, "resourcePath").trim();
if (normalized.isEmpty()) { if (normalized.isEmpty()) {

View File

@ -7,6 +7,7 @@ import p.studio.lsp.dtos.LspHighlightSpanDTO;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
public final class EditorDocumentSemanticHighlighting { public final class EditorDocumentSemanticHighlighting {
@ -40,4 +41,24 @@ public final class EditorDocumentSemanticHighlighting {
} }
return builder.create(); return builder.create();
} }
public static StyleSpans<Collection<String>> overlay(
final StyleSpans<Collection<String>> baseHighlighting,
final StyleSpans<Collection<String>> semanticHighlighting) {
return baseHighlighting.overlay(semanticHighlighting, EditorDocumentSemanticHighlighting::mergeStyles);
}
private static Collection<String> mergeStyles(
final Collection<String> baseStyles,
final Collection<String> semanticStyles) {
if (baseStyles.isEmpty()) {
return semanticStyles;
}
if (semanticStyles.isEmpty()) {
return baseStyles;
}
final LinkedHashSet<String> merged = new LinkedHashSet<>(baseStyles);
merged.addAll(semanticStyles);
return List.copyOf(merged);
}
} }

View File

@ -36,6 +36,10 @@ public record EditorDocumentSyntaxHighlighting(
return EditorDocumentSyntaxHighlightingBash.BASH; return EditorDocumentSyntaxHighlightingBash.BASH;
} }
public static EditorDocumentSyntaxHighlighting pbs() {
return EditorDocumentSyntaxHighlightingPbs.PBS;
}
public StyleSpans<Collection<String>> highlight(final String content) { public StyleSpans<Collection<String>> highlight(final String content) {
final StyleSpansBuilder<Collection<String>> builder = new StyleSpansBuilder<>(); final StyleSpansBuilder<Collection<String>> builder = new StyleSpansBuilder<>();
if (tokenPattern == null) { if (tokenPattern == null) {

View File

@ -46,7 +46,8 @@ final class EditorDocumentHighlightingRouterTest {
analysis); analysis);
assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); 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 @Test
@ -80,7 +81,7 @@ final class EditorDocumentHighlightingRouterTest {
} }
@Test @Test
void frontendDocumentsDegradeToLocalWhenSemanticPresentationResourcesAreUnavailable() { void frontendDocumentsFallbackToLocalPbsSyntaxWhenSemanticPresentationResourcesAreUnavailable() {
final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry();
final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer(
Path.of("/tmp/example/src/main.pbs"), Path.of("/tmp/example/src/main.pbs"),
@ -108,8 +109,9 @@ final class EditorDocumentHighlightingRouterTest {
registry.resolve("pbs", analysis.semanticPresentation()), registry.resolve("pbs", analysis.semanticPresentation()),
analysis); analysis);
assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); assertEquals(EditorDocumentHighlightOwner.LSP, result.owner());
assertTrue(result.styleSpans().getStyleSpan(0).getStyle().isEmpty()); assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword"));
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function"));
} }
@Test @Test
@ -145,6 +147,41 @@ final class EditorDocumentHighlightingRouterTest {
assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); 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( private boolean containsStyle(
final org.fxmisc.richtext.model.StyleSpans<Collection<String>> styleSpans, final org.fxmisc.richtext.model.StyleSpans<Collection<String>> styleSpans,
final String styleClass) { final String styleClass) {

View File

@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import p.studio.lsp.dtos.LspSemanticPresentationDTO; import p.studio.lsp.dtos.LspSemanticPresentationDTO;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
final class EditorDocumentPresentationRegistryTest { final class EditorDocumentPresentationRegistryTest {
@ -24,7 +25,7 @@ final class EditorDocumentPresentationRegistryTest {
} }
@Test @Test
void missingFrontendResourcesDegradeToPlainFrontendPresentation() { void missingFrontendResourcesFallbackToFrontendOwnedPresentationResources() {
final EditorDocumentPresentation presentation = registry.resolve( final EditorDocumentPresentation presentation = registry.resolve(
"pbs", "pbs",
new LspSemanticPresentationDTO( new LspSemanticPresentationDTO(
@ -33,7 +34,8 @@ final class EditorDocumentPresentationRegistryTest {
assertEquals("pbs", presentation.styleKey()); assertEquals("pbs", presentation.styleKey());
assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys()); 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 @Test

View File

@ -28,7 +28,7 @@ fn frame() -> void
if (loading_handle == -1) { if (loading_handle == -1) {
let t = Assets.load(assets.ui.atlas2, 3); let t = Assets.load(assets.ui.atlas2, 3);
if (t.status != 0) { if (t.status != 0) {
Log.error("load failed"); Log.failure("load failed");
} else { } else {
loading_handle = t.loading_handle; loading_handle = t.loading_handle;
Log.info("state: loading"); Log.info("state: loading");
@ -38,12 +38,12 @@ fn frame() -> void
if (s == 2) { if (s == 2) {
let commit_status = Assets.commit(loading_handle); let commit_status = Assets.commit(loading_handle);
if (commit_status != 0) { if (commit_status != 0) {
Log.error("commit failed"); Log.failure("commit failed");
} }
} else if (s == 3) { } else if (s == 3) {
let sprite_status = Gfx.set_sprite(3, 10, 150, 150, 0, 0, true, false, false, 1); let sprite_status = Gfx.set_sprite(3, 10, 150, 150, 0, 0, true, false, false, 1);
if (sprite_status != 0) { if (sprite_status != 0) {
Log.error("set_sprite failed"); Log.failure("set_sprite failed");
} }
} else { } else {
Log.info("state: waiting"); Log.info("state: waiting");
@ -100,7 +100,7 @@ fn frame() -> void
} }
else if (total == 50) else if (total == 50)
{ {
Log.error("50 is the magic number!"); Log.failure("50 is the magic number!");
} }
else if (total == 15) else if (total == 15)
{ {