Frontend Visual Theme Spec and Retirement of Host-Consumed Semantic CSS

This commit is contained in:
bQUARKz 2026-05-06 15:39:43 +01:00
parent 7ae2e94d63
commit fb13e166f1
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 505 additions and 453 deletions

View File

@ -2,7 +2,6 @@ package p.studio.compiler;
import p.studio.compiler.models.FrontendSemanticPresentationSpec; import p.studio.compiler.models.FrontendSemanticPresentationSpec;
import p.studio.compiler.models.FrontendSpec; import p.studio.compiler.models.FrontendSpec;
import p.studio.compiler.pbs.PbsSemanticKind;
import p.studio.utilities.structures.ReadOnlySet; import p.studio.utilities.structures.ReadOnlySet;
import java.util.List; import java.util.List;
@ -15,7 +14,6 @@ public class PBSDefinitions {
.sourceRoots(ReadOnlySet.from("src")) .sourceRoots(ReadOnlySet.from("src"))
.stdlibVersions(List.of(FrontendSpec.Stdlib.asDefault(1))) .stdlibVersions(List.of(FrontendSpec.Stdlib.asDefault(1)))
.semanticPresentation(new FrontendSemanticPresentationSpec( .semanticPresentation(new FrontendSemanticPresentationSpec(
PbsSemanticKind.semanticKeys(),
List.of(PBSFrontendThemes.DEFAULT_PBS), List.of(PBSFrontendThemes.DEFAULT_PBS),
"pbs-default", "pbs-default",
List.of(PBSFrontendHostProjections.VSCODE))) List.of(PBSFrontendHostProjections.VSCODE)))

View File

@ -2,46 +2,26 @@ package p.studio.compiler;
import p.studio.compiler.models.FrontendHostProjectionEntrySpec; import p.studio.compiler.models.FrontendHostProjectionEntrySpec;
import p.studio.compiler.models.FrontendHostProjectionSpec; import p.studio.compiler.models.FrontendHostProjectionSpec;
import p.studio.compiler.pbs.PbsSemanticKind;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import static p.studio.compiler.pbs.PbsSemanticKind.BUILTIN_TYPE; import static p.studio.compiler.pbs.PbsSemanticKind.*;
import static p.studio.compiler.pbs.PbsSemanticKind.CALLBACK;
import static p.studio.compiler.pbs.PbsSemanticKind.COMMENT;
import static p.studio.compiler.pbs.PbsSemanticKind.CONST;
import static p.studio.compiler.pbs.PbsSemanticKind.CONSTRUCTOR;
import static p.studio.compiler.pbs.PbsSemanticKind.CONTRACT;
import static p.studio.compiler.pbs.PbsSemanticKind.ENUM;
import static p.studio.compiler.pbs.PbsSemanticKind.ERROR;
import static p.studio.compiler.pbs.PbsSemanticKind.FUNCTION;
import static p.studio.compiler.pbs.PbsSemanticKind.GLOBAL;
import static p.studio.compiler.pbs.PbsSemanticKind.HOST;
import static p.studio.compiler.pbs.PbsSemanticKind.IDENTIFIER;
import static p.studio.compiler.pbs.PbsSemanticKind.IMPLEMENTS;
import static p.studio.compiler.pbs.PbsSemanticKind.KEYWORD;
import static p.studio.compiler.pbs.PbsSemanticKind.LIFECYCLE;
import static p.studio.compiler.pbs.PbsSemanticKind.LITERAL;
import static p.studio.compiler.pbs.PbsSemanticKind.METHOD;
import static p.studio.compiler.pbs.PbsSemanticKind.NUMBER;
import static p.studio.compiler.pbs.PbsSemanticKind.OPERATOR;
import static p.studio.compiler.pbs.PbsSemanticKind.PUNCTUATION;
import static p.studio.compiler.pbs.PbsSemanticKind.SERVICE;
import static p.studio.compiler.pbs.PbsSemanticKind.STRING;
import static p.studio.compiler.pbs.PbsSemanticKind.STRUCT;
public final class PBSFrontendHostProjections { public final class PBSFrontendHostProjections {
public static final String VSCODE_HOST_ID = "vscode"; public static final String VSCODE_HOST_ID = "vscode";
public static final FrontendHostProjectionSpec VSCODE = new FrontendHostProjectionSpec( public static final FrontendHostProjectionSpec VSCODE = new FrontendHostProjectionSpec(
VSCODE_HOST_ID, VSCODE_HOST_ID,
Arrays.stream(p.studio.compiler.pbs.PbsSemanticKind.values()) Arrays.stream(PbsSemanticKind.values())
.map(PBSFrontendHostProjections::mapVscodeProjection) .map(PBSFrontendHostProjections::mapVscodeProjection)
.toList()); .toList());
private PBSFrontendHostProjections() { private PBSFrontendHostProjections() {
} }
private static FrontendHostProjectionEntrySpec mapVscodeProjection(final p.studio.compiler.pbs.PbsSemanticKind kind) { private static FrontendHostProjectionEntrySpec mapVscodeProjection(final PbsSemanticKind kind) {
return switch (kind) { return switch (kind) {
case COMMENT -> projection(COMMENT.semanticKey(), "comment"); case COMMENT -> projection(COMMENT.semanticKey(), "comment");
case STRING -> projection(STRING.semanticKey(), "string"); case STRING -> projection(STRING.semanticKey(), "string");
@ -72,14 +52,14 @@ public final class PBSFrontendHostProjections {
private static FrontendHostProjectionEntrySpec projection( private static FrontendHostProjectionEntrySpec projection(
final String semanticKey, final String semanticKey,
final String hostTokenType) { final String hostTokenType) {
return new FrontendHostProjectionEntrySpec(semanticKey, hostTokenType, java.util.List.of(), hostTokenType, java.util.List.of()); return new FrontendHostProjectionEntrySpec(semanticKey, hostTokenType, List.of(), hostTokenType, List.of());
} }
private static FrontendHostProjectionEntrySpec projection( private static FrontendHostProjectionEntrySpec projection(
final String semanticKey, final String semanticKey,
final String hostTokenType, final String hostTokenType,
final String fallbackTokenType) { final String fallbackTokenType) {
return new FrontendHostProjectionEntrySpec(semanticKey, hostTokenType, java.util.List.of(), fallbackTokenType, java.util.List.of()); return new FrontendHostProjectionEntrySpec(semanticKey, hostTokenType, List.of(), fallbackTokenType, List.of());
} }
private static FrontendHostProjectionEntrySpec projection( private static FrontendHostProjectionEntrySpec projection(
@ -90,8 +70,8 @@ public final class PBSFrontendHostProjections {
return new FrontendHostProjectionEntrySpec( return new FrontendHostProjectionEntrySpec(
semanticKey, semanticKey,
hostTokenType, hostTokenType,
java.util.List.of(modifier), List.of(modifier),
fallbackTokenType, fallbackTokenType,
java.util.List.of(modifier)); List.of(modifier));
} }
} }

View File

@ -4,57 +4,80 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
public record FrontendSemanticPresentationSpec( public record FrontendSemanticPresentationSpec(
List<String> semanticKeys,
List<FrontendVisualThemeSpec> themes, List<FrontendVisualThemeSpec> themes,
String defaultThemeId, String defaultThemeId,
List<FrontendHostProjectionSpec> hostProjections) { List<FrontendHostProjectionSpec> hostProjections) {
public FrontendSemanticPresentationSpec( public FrontendSemanticPresentationSpec(
final List<String> semanticKeys,
final List<FrontendVisualThemeSpec> themes, final List<FrontendVisualThemeSpec> themes,
final String defaultThemeId) { final String defaultThemeId) {
this(semanticKeys, themes, defaultThemeId, List.of()); this(themes, defaultThemeId, List.of());
} }
public FrontendSemanticPresentationSpec { public FrontendSemanticPresentationSpec {
semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys"));
themes = List.copyOf(Objects.requireNonNull(themes, "themes")); themes = List.copyOf(Objects.requireNonNull(themes, "themes"));
defaultThemeId = Objects.requireNonNull(defaultThemeId, "defaultThemeId").trim(); defaultThemeId = Objects.requireNonNull(defaultThemeId, "defaultThemeId").trim();
hostProjections = List.copyOf(Objects.requireNonNull(hostProjections, "hostProjections")); hostProjections = List.copyOf(Objects.requireNonNull(hostProjections, "hostProjections"));
if (semanticKeys.isEmpty() && themes.isEmpty()) { if (themes.isEmpty()) {
if (!defaultThemeId.isEmpty()) { if (!defaultThemeId.isEmpty()) {
throw new IllegalArgumentException("defaultThemeId must be blank when no themes are declared"); throw new IllegalArgumentException("defaultThemeId must be blank when no themes are declared");
} }
if (!hostProjections.isEmpty()) {
throw new IllegalArgumentException("hostProjections must be empty when no themes are declared");
}
} else { } else {
defaultThemeId = requireText(defaultThemeId, "defaultThemeId"); defaultThemeId = requireText(defaultThemeId, "defaultThemeId");
if (themes.isEmpty()) { final String resolvedDefaultThemeId = defaultThemeId;
throw new IllegalArgumentException("themes must not be empty"); if (themes.stream().map(FrontendVisualThemeSpec::themeId).noneMatch(resolvedDefaultThemeId::equals)) {
}
if (themes.stream().map(FrontendVisualThemeSpec::themeId).noneMatch(defaultThemeId::equals)) {
throw new IllegalArgumentException("defaultThemeId must reference one of the declared themes"); throw new IllegalArgumentException("defaultThemeId must reference one of the declared themes");
} }
}
final List<String> normalizedSemanticKeys = semanticKeys.stream() final List<String> derivedSemanticKeys = themes.stream()
.map(key -> requireText(key, "semanticKeys entry")) .filter(theme -> theme.themeId().equals(resolvedDefaultThemeId))
.toList(); .findFirst()
final var hostIds = new java.util.LinkedHashSet<String>(); .orElseThrow(() -> new IllegalStateException("defaultThemeId must resolve to a declared theme"))
for (final FrontendHostProjectionSpec hostProjection : hostProjections) { .tokenStyles()
if (!hostIds.add(hostProjection.hostId())) { .stream()
throw new IllegalArgumentException("duplicate host projection for hostId: " + hostProjection.hostId()); .map(FrontendTokenStyleSpec::semanticKey)
} .toList();
for (final FrontendHostProjectionEntrySpec tokenProjection : hostProjection.tokenProjections()) {
if (!normalizedSemanticKeys.contains(tokenProjection.semanticKey())) { for (final FrontendVisualThemeSpec theme : themes) {
final List<String> themeSemanticKeys = theme.tokenStyles().stream()
.map(FrontendTokenStyleSpec::semanticKey)
.toList();
if (!themeSemanticKeys.equals(derivedSemanticKeys)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"host projection for hostId '" + hostProjection.hostId() + "' references unknown semantic key: " "theme '" + theme.themeId() + "' must publish the same semantic keys as the default theme");
+ tokenProjection.semanticKey()); }
}
final var hostIds = new java.util.LinkedHashSet<String>();
for (final FrontendHostProjectionSpec hostProjection : hostProjections) {
if (!hostIds.add(hostProjection.hostId())) {
throw new IllegalArgumentException(
"duplicate host projection for hostId: " + hostProjection.hostId());
}
final List<String> projectedSemanticKeys = hostProjection.tokenProjections().stream()
.map(FrontendHostProjectionEntrySpec::semanticKey)
.toList();
if (!projectedSemanticKeys.equals(derivedSemanticKeys)) {
throw new IllegalArgumentException(
"host projection for hostId '" + hostProjection.hostId()
+ "' must publish the same semantic keys as the default theme");
}
for (final FrontendHostProjectionEntrySpec tokenProjection : hostProjection.tokenProjections()) {
if (!derivedSemanticKeys.contains(tokenProjection.semanticKey())) {
throw new IllegalArgumentException(
"host projection for hostId '" + hostProjection.hostId() + "' references unknown semantic key: "
+ tokenProjection.semanticKey());
}
} }
} }
} }
semanticKeys = normalizedSemanticKeys;
} }
public static FrontendSemanticPresentationSpec empty() { public static FrontendSemanticPresentationSpec empty() {
return new FrontendSemanticPresentationSpec(List.of(), List.of(), "", List.of()); return new FrontendSemanticPresentationSpec(List.of(), "", List.of());
} }
public FrontendVisualThemeSpec defaultTheme() { public FrontendVisualThemeSpec defaultTheme() {
@ -68,7 +91,16 @@ public record FrontendSemanticPresentationSpec(
} }
public boolean isEmpty() { public boolean isEmpty() {
return semanticKeys.isEmpty() && themes.isEmpty() && defaultThemeId.isEmpty() && hostProjections.isEmpty(); return themes.isEmpty() && defaultThemeId.isEmpty() && hostProjections.isEmpty();
}
public List<String> semanticKeys() {
if (themes.isEmpty()) {
return List.of();
}
return defaultTheme().tokenStyles().stream()
.map(FrontendTokenStyleSpec::semanticKey)
.toList();
} }
public java.util.Optional<FrontendHostProjectionSpec> hostProjection(final String hostId) { public java.util.Optional<FrontendHostProjectionSpec> hostProjection(final String hostId) {

View File

@ -12,7 +12,6 @@ final class FrontendSemanticPresentationSpecTest {
@Test @Test
void shouldExposeHostProjectionByHostId() { void shouldExposeHostProjectionByHostId() {
final var presentation = new FrontendSemanticPresentationSpec( final var presentation = new FrontendSemanticPresentationSpec(
List.of("demo-key"),
List.of(new FrontendVisualThemeSpec( List.of(new FrontendVisualThemeSpec(
"demo-theme", "demo-theme",
"Demo Theme", "Demo Theme",
@ -37,7 +36,6 @@ final class FrontendSemanticPresentationSpecTest {
@Test @Test
void shouldRejectDuplicateHostIds() { void shouldRejectDuplicateHostIds() {
assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec( assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec(
List.of("demo-key"),
List.of(new FrontendVisualThemeSpec( List.of(new FrontendVisualThemeSpec(
"demo-theme", "demo-theme",
"Demo Theme", "Demo Theme",
@ -66,7 +64,6 @@ final class FrontendSemanticPresentationSpecTest {
@Test @Test
void shouldRejectProjectionForUnknownSemanticKey() { void shouldRejectProjectionForUnknownSemanticKey() {
assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec( assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec(
List.of("demo-key"),
List.of(new FrontendVisualThemeSpec( List.of(new FrontendVisualThemeSpec(
"demo-theme", "demo-theme",
"Demo Theme", "Demo Theme",
@ -82,4 +79,22 @@ final class FrontendSemanticPresentationSpecTest {
"variable", "variable",
List.of())))))); List.of()))))));
} }
@Test
void shouldRejectThemeSetThatDriftsFromDefaultThemeVocabulary() {
assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec(
List.of(
new FrontendVisualThemeSpec(
"demo-theme",
"Demo Theme",
new FrontendEditorPaletteSpec("#111111", "#eeeeee", "#333333", "#777777", "#111111", "#222222", "#eeeeee"),
List.of(new FrontendTokenStyleSpec("demo-key", "#eeeeee", false, false, false))),
new FrontendVisualThemeSpec(
"demo-theme-2",
"Demo Theme 2",
new FrontendEditorPaletteSpec("#111111", "#eeeeee", "#333333", "#777777", "#111111", "#222222", "#eeeeee"),
List.of(new FrontendTokenStyleSpec("other-key", "#eeeeee", false, false, false)))),
"demo-theme",
List.of()));
}
} }

View File

@ -30,7 +30,34 @@
}, },
"pbs-const:pbs": "#f78c6c", "pbs-const:pbs": "#f78c6c",
"pbs-implements:pbs": "#a1c181", "pbs-implements:pbs": "#a1c181",
"pbs-identifier:pbs": "#d4d4d4" "pbs-identifier:pbs": "#d4d4d4",
"comment:pbs": {
"foreground": "#c090b0",
"italic": true
},
"string:pbs": "#00c088",
"number:pbs": "#ff90b0",
"enumMember:pbs": "#4fc1ff",
"variable:pbs": "#4fc1ff",
"function.async:pbs": "#ef50c0",
"variable.async:pbs": "#ef50c0",
"keyword:pbs": "#569cd6",
"operator:pbs": "#d4d4d4",
"function:pbs": "#f2c14e",
"method:pbs": "#f2c14e",
"class.declaration:pbs": "#ecdcaa",
"type.declaration:pbs": "#ecdcaa",
"struct:pbs": "#4ec9b0",
"type:pbs": "#4ec9b0",
"interface:pbs": "#78dce8",
"class.defaultLibrary:pbs": "#b7a2fa",
"type.defaultLibrary:pbs": "#b7a2fa",
"variable.defaultLibrary:pbs": "#8be9fd",
"enum:pbs": "#ff5b5b",
"variable.readonly:pbs": {
"foreground": "#f790fc",
"italic": true
}
} }
}, },
"workbench.colorCustomizations": { "workbench.colorCustomizations": {

File diff suppressed because it is too large Load Diff