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() {
if (cursor.check(PbsTokenKind.IDENTIFIER) || cursor.check(PbsTokenKind.ERROR)) {
if (cursor.check(PbsTokenKind.IDENTIFIER)) {
return cursor.advance();
}
final var token = cursor.peek();

View File

@ -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();

View File

@ -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);
}

View File

@ -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

View File

@ -219,6 +219,7 @@ final class LspServiceImplTest {
pub fn helper() -> void;
pub fn helper_result() -> result<MyError> 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;
}

View File

@ -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);
}
}

View File

@ -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<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(
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<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) {
final String normalized = Objects.requireNonNull(resourcePath, "resourcePath").trim();
if (normalized.isEmpty()) {

View File

@ -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<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;
}
public static EditorDocumentSyntaxHighlighting pbs() {
return EditorDocumentSyntaxHighlightingPbs.PBS;
}
public StyleSpans<Collection<String>> highlight(final String content) {
final StyleSpansBuilder<Collection<String>> builder = new StyleSpansBuilder<>();
if (tokenPattern == null) {

View File

@ -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<Collection<String>> styleSpans,
final String styleClass) {

View File

@ -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

View File

@ -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)
{