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.FrontendSpec;
import p.studio.compiler.pbs.PbsSemanticKind;
import p.studio.utilities.structures.ReadOnlySet;
import java.util.List;
@ -15,7 +14,6 @@ public class PBSDefinitions {
.sourceRoots(ReadOnlySet.from("src"))
.stdlibVersions(List.of(FrontendSpec.Stdlib.asDefault(1)))
.semanticPresentation(new FrontendSemanticPresentationSpec(
PbsSemanticKind.semanticKeys(),
List.of(PBSFrontendThemes.DEFAULT_PBS),
"pbs-default",
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.FrontendHostProjectionSpec;
import p.studio.compiler.pbs.PbsSemanticKind;
import java.util.Arrays;
import java.util.List;
import static p.studio.compiler.pbs.PbsSemanticKind.BUILTIN_TYPE;
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;
import static p.studio.compiler.pbs.PbsSemanticKind.*;
public final class PBSFrontendHostProjections {
public static final String VSCODE_HOST_ID = "vscode";
public static final FrontendHostProjectionSpec VSCODE = new FrontendHostProjectionSpec(
VSCODE_HOST_ID,
Arrays.stream(p.studio.compiler.pbs.PbsSemanticKind.values())
Arrays.stream(PbsSemanticKind.values())
.map(PBSFrontendHostProjections::mapVscodeProjection)
.toList());
private PBSFrontendHostProjections() {
}
private static FrontendHostProjectionEntrySpec mapVscodeProjection(final p.studio.compiler.pbs.PbsSemanticKind kind) {
private static FrontendHostProjectionEntrySpec mapVscodeProjection(final PbsSemanticKind kind) {
return switch (kind) {
case COMMENT -> projection(COMMENT.semanticKey(), "comment");
case STRING -> projection(STRING.semanticKey(), "string");
@ -72,14 +52,14 @@ public final class PBSFrontendHostProjections {
private static FrontendHostProjectionEntrySpec projection(
final String semanticKey,
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(
final String semanticKey,
final String hostTokenType,
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(
@ -90,8 +70,8 @@ public final class PBSFrontendHostProjections {
return new FrontendHostProjectionEntrySpec(
semanticKey,
hostTokenType,
java.util.List.of(modifier),
List.of(modifier),
fallbackTokenType,
java.util.List.of(modifier));
List.of(modifier));
}
}

View File

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

View File

@ -12,7 +12,6 @@ final class FrontendSemanticPresentationSpecTest {
@Test
void shouldExposeHostProjectionByHostId() {
final var presentation = new FrontendSemanticPresentationSpec(
List.of("demo-key"),
List.of(new FrontendVisualThemeSpec(
"demo-theme",
"Demo Theme",
@ -37,7 +36,6 @@ final class FrontendSemanticPresentationSpecTest {
@Test
void shouldRejectDuplicateHostIds() {
assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec(
List.of("demo-key"),
List.of(new FrontendVisualThemeSpec(
"demo-theme",
"Demo Theme",
@ -66,7 +64,6 @@ final class FrontendSemanticPresentationSpecTest {
@Test
void shouldRejectProjectionForUnknownSemanticKey() {
assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec(
List.of("demo-key"),
List.of(new FrontendVisualThemeSpec(
"demo-theme",
"Demo Theme",
@ -82,4 +79,22 @@ final class FrontendSemanticPresentationSpecTest {
"variable",
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-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": {

File diff suppressed because it is too large Load Diff

View File

@ -335,4 +335,4 @@ function readObject(value) {
}
return value;
}
//# sourceMappingURL=extension.js.map
//# sourceMappingURL=extension.js.map