diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 4369e623..17cc4fc5 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":35,"AGD":38,"DEC":35,"PLN":75,"LSN":47,"CLSN":1}} -{"type":"discussion","id":"DSC-0034","status":"in_progress","ticket":"frontend-semantic-host-projection-flexibility","title":"Frontend semantic vocabulary flexibility and declarative host projection","created_at":"2026-05-06","updated_at":"2026-05-06","tags":["compiler","compiler-general","frontend","semantics","vscode","host-projection","lsp"],"agendas":[{"id":"AGD-0037","file":"AGD-0037-frontend-semantic-host-projection-flexibility.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06"}],"decisions":[{"id":"DEC-0034","file":"DEC-0034-frontend-semantic-host-projection-flexibility.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06","ref_agenda":"AGD-0037"}],"plans":[{"id":"PLN-0072","file":"PLN-0072-frontend-semantic-host-projection-contract.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]},{"id":"PLN-0073","file":"PLN-0073-lsp-semantic-host-projection-transport.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]},{"id":"PLN-0074","file":"PLN-0074-vscode-semantic-host-projection-adapter.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0034","status":"in_progress","ticket":"frontend-semantic-host-projection-flexibility","title":"Frontend semantic vocabulary flexibility and declarative host projection","created_at":"2026-05-06","updated_at":"2026-05-06","tags":["compiler","compiler-general","frontend","semantics","vscode","host-projection","lsp"],"agendas":[{"id":"AGD-0037","file":"AGD-0037-frontend-semantic-host-projection-flexibility.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06"}],"decisions":[{"id":"DEC-0034","file":"DEC-0034-frontend-semantic-host-projection-flexibility.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06","ref_agenda":"AGD-0037"}],"plans":[{"id":"PLN-0072","file":"PLN-0072-frontend-semantic-host-projection-contract.md","status":"done","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]},{"id":"PLN-0073","file":"PLN-0073-lsp-semantic-host-projection-transport.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]},{"id":"PLN-0074","file":"PLN-0074-vscode-semantic-host-projection-adapter.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]}],"lessons":[]} {"type":"discussion","id":"DSC-0033","status":"in_progress","ticket":"frontend-visual-theme-spec-and-css-retirement","title":"Frontend visual theme spec and retirement of host-consumed semantic CSS","created_at":"2026-05-06","updated_at":"2026-05-06","tags":["compiler","compiler-general","frontend","presentation","theming","studio","vscode","lsp","pbs"],"agendas":[{"id":"AGD-0036","file":"AGD-0036-frontend-visual-theme-spec-and-css-retirement.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06"}],"decisions":[{"id":"DEC-0033","file":"DEC-0033-frontend-visual-theme-spec-and-css-retirement.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06","ref_agenda":"AGD-0036"}],"plans":[{"id":"PLN-0068","file":"PLN-0068-frontend-visual-contract-and-pbs-migration.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0069","file":"PLN-0069-lsp-visual-theme-transport-and-selection.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0070","file":"PLN-0070-studio-visual-theme-adapter-and-css-retirement.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0071","file":"PLN-0071-vscode-dynamic-theme-translation-adapter.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]}],"lessons":[]} {"type":"discussion","id":"DSC-0032","status":"in_progress","ticket":"studio-new-lsp-api-and-v1-boundary","title":"Novo boundary entre lsp-api, lsp-v1 e a extensao VS Code","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","lsp","vscode","protocol","api","boundary"],"agendas":[{"id":"AGD-0035","file":"AGD-0035-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0035"}],"plans":[{"id":"PLN-0065","file":"PLN-0065-lsp-boundary-and-module-scaffolding.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0066","file":"PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0067","file":"PLN-0067-compiler-backed-dumb-lsp-server-baseline.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]}],"lessons":[]} {"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]} diff --git a/discussion/workflow/plans/PLN-0072-frontend-semantic-host-projection-contract.md b/discussion/workflow/plans/PLN-0072-frontend-semantic-host-projection-contract.md index a4d829ee..cd4ed79a 100644 --- a/discussion/workflow/plans/PLN-0072-frontend-semantic-host-projection-contract.md +++ b/discussion/workflow/plans/PLN-0072-frontend-semantic-host-projection-contract.md @@ -2,9 +2,9 @@ id: PLN-0072 ticket: frontend-semantic-host-projection-flexibility title: Frontend Semantic Host Projection Contract -status: open +status: done created: 2026-05-06 -completed: +completed: 2026-05-06 tags: [compiler, compiler-general, frontend, semantics, host-projection, vscode, pbs] --- diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java index 3c899177..4daf9129 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java @@ -17,6 +17,7 @@ public class PBSDefinitions { .semanticPresentation(new FrontendSemanticPresentationSpec( PbsSemanticKind.semanticKeys(), List.of(PBSFrontendThemes.DEFAULT_PBS), - "pbs-default")) + "pbs-default", + List.of(PBSFrontendHostProjections.VSCODE))) .build(); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendHostProjections.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendHostProjections.java new file mode 100644 index 00000000..995bfeac --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendHostProjections.java @@ -0,0 +1,97 @@ +package p.studio.compiler; + +import p.studio.compiler.models.FrontendHostProjectionEntrySpec; +import p.studio.compiler.models.FrontendHostProjectionSpec; + +import java.util.Arrays; + +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; + +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()) + .map(PBSFrontendHostProjections::mapVscodeProjection) + .toList()); + + private PBSFrontendHostProjections() { + } + + private static FrontendHostProjectionEntrySpec mapVscodeProjection(final p.studio.compiler.pbs.PbsSemanticKind kind) { + return switch (kind) { + case COMMENT -> projection(COMMENT.semanticKey(), "comment"); + case STRING -> projection(STRING.semanticKey(), "string"); + case NUMBER -> projection(NUMBER.semanticKey(), "number"); + case LITERAL -> projection(LITERAL.semanticKey(), "enumMember", "variable"); + case LIFECYCLE -> projection(LIFECYCLE.semanticKey(), "function", "variable", "async"); + case KEYWORD -> projection(KEYWORD.semanticKey(), "keyword"); + case OPERATOR -> projection(OPERATOR.semanticKey(), "operator", "keyword"); + case PUNCTUATION -> projection(PUNCTUATION.semanticKey(), "operator", "keyword"); + case FUNCTION -> projection(FUNCTION.semanticKey(), "function"); + case METHOD -> projection(METHOD.semanticKey(), "method", "function"); + case CONSTRUCTOR -> projection(CONSTRUCTOR.semanticKey(), "class", "type", "declaration"); + case STRUCT -> projection(STRUCT.semanticKey(), "struct", "type"); + case CONTRACT -> projection(CONTRACT.semanticKey(), "interface", "type"); + case HOST -> projection(HOST.semanticKey(), "class", "type", "defaultLibrary"); + case BUILTIN_TYPE -> projection(BUILTIN_TYPE.semanticKey(), "type", "variable", "defaultLibrary"); + case SERVICE -> projection(SERVICE.semanticKey(), "class", "type", "defaultLibrary"); + case ERROR -> projection(ERROR.semanticKey(), "enum", "type"); + case ENUM -> projection(ENUM.semanticKey(), "enum", "type"); + case CALLBACK -> projection(CALLBACK.semanticKey(), "interface", "type"); + case GLOBAL -> projection(GLOBAL.semanticKey(), "variable", "variable", "readonly"); + case CONST -> projection(CONST.semanticKey(), "variable", "variable", "readonly"); + case IMPLEMENTS -> projection(IMPLEMENTS.semanticKey(), "keyword"); + case IDENTIFIER -> projection(IDENTIFIER.semanticKey(), "variable"); + }; + } + + private static FrontendHostProjectionEntrySpec projection( + final String semanticKey, + final String hostTokenType) { + return new FrontendHostProjectionEntrySpec(semanticKey, hostTokenType, java.util.List.of(), hostTokenType, java.util.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()); + } + + private static FrontendHostProjectionEntrySpec projection( + final String semanticKey, + final String hostTokenType, + final String fallbackTokenType, + final String modifier) { + return new FrontendHostProjectionEntrySpec( + semanticKey, + hostTokenType, + java.util.List.of(modifier), + fallbackTokenType, + java.util.List.of(modifier)); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java index c78457c0..3e26eb5e 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java @@ -1,6 +1,7 @@ package p.studio.compiler.pbs; import org.junit.jupiter.api.Test; +import p.studio.compiler.PBSFrontendHostProjections; import p.studio.compiler.PBSDefinitions; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -21,6 +22,7 @@ final class PbsSemanticPresentationContractTest { assertEquals("#d9e2ec", presentation.defaultTheme().editorPalette().baseForeground()); assertEquals("#569cd6", presentation.defaultTheme().requireTokenStyle("pbs-keyword").foreground()); assertTrue(presentation.defaultTheme().requireTokenStyle("pbs-comment").italic()); + assertEquals(PBSFrontendHostProjections.VSCODE_HOST_ID, presentation.requireHostProjection("vscode").hostId()); } @Test @@ -32,4 +34,19 @@ final class PbsSemanticPresentationContractTest { PbsSemanticKind.semanticKeys(), defaultTheme.tokenStyles().stream().map(tokenStyle -> tokenStyle.semanticKey()).toList()); } + + @Test + void shouldCoverTheWholePbsSemanticVocabularyInTheVscodeProjection() { + final var presentation = PBSDefinitions.PBS.getSemanticPresentation(); + final var vscodeProjection = presentation.requireHostProjection(PBSFrontendHostProjections.VSCODE_HOST_ID); + + assertEquals( + PbsSemanticKind.semanticKeys(), + vscodeProjection.tokenProjections().stream().map(tokenProjection -> tokenProjection.semanticKey()).toList()); + assertEquals("keyword", vscodeProjection.requireProjection(PbsSemanticKind.KEYWORD.semanticKey()).hostTokenType()); + assertEquals("type", vscodeProjection.requireProjection(PbsSemanticKind.STRUCT.semanticKey()).fallbackTokenType()); + assertEquals( + java.util.List.of("readonly"), + vscodeProjection.requireProjection(PbsSemanticKind.GLOBAL.semanticKey()).hostTokenModifiers()); + } } diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionEntrySpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionEntrySpec.java new file mode 100644 index 00000000..4d66ff4a --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionEntrySpec.java @@ -0,0 +1,38 @@ +package p.studio.compiler.models; + +import java.util.List; +import java.util.Objects; + +public record FrontendHostProjectionEntrySpec( + String semanticKey, + String hostTokenType, + List hostTokenModifiers, + String fallbackTokenType, + List fallbackTokenModifiers) { + + public FrontendHostProjectionEntrySpec { + semanticKey = requireText(semanticKey, "semanticKey"); + hostTokenType = requireText(hostTokenType, "hostTokenType"); + hostTokenModifiers = normalizeList(hostTokenModifiers, "hostTokenModifiers"); + fallbackTokenType = requireText(fallbackTokenType, "fallbackTokenType"); + fallbackTokenModifiers = normalizeList(fallbackTokenModifiers, "fallbackTokenModifiers"); + } + + private static List normalizeList( + final List values, + final String field) { + return List.copyOf(Objects.requireNonNull(values, field).stream() + .map(value -> requireText(value, field + " entry")) + .toList()); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionSpec.java new file mode 100644 index 00000000..826ab68f --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionSpec.java @@ -0,0 +1,48 @@ +package p.studio.compiler.models; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public record FrontendHostProjectionSpec( + String hostId, + List tokenProjections) { + + public FrontendHostProjectionSpec { + hostId = requireText(hostId, "hostId"); + tokenProjections = List.copyOf(Objects.requireNonNull(tokenProjections, "tokenProjections")); + if (tokenProjections.isEmpty()) { + throw new IllegalArgumentException("tokenProjections must not be empty"); + } + final Map bySemanticKey = new LinkedHashMap<>(); + for (final FrontendHostProjectionEntrySpec tokenProjection : tokenProjections) { + final FrontendHostProjectionEntrySpec previous = bySemanticKey.put(tokenProjection.semanticKey(), tokenProjection); + if (previous != null) { + throw new IllegalArgumentException("duplicate host projection for semantic key: " + tokenProjection.semanticKey()); + } + } + } + + public Optional projectionFor(final String semanticKey) { + return tokenProjections.stream() + .filter(tokenProjection -> tokenProjection.semanticKey().equals(semanticKey)) + .findFirst(); + } + + public FrontendHostProjectionEntrySpec requireProjection(final String semanticKey) { + return projectionFor(semanticKey) + .orElseThrow(() -> new IllegalArgumentException("missing host projection for semantic key: " + semanticKey)); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java index 989f5959..5de32aa9 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java @@ -6,12 +6,21 @@ import java.util.Objects; public record FrontendSemanticPresentationSpec( List semanticKeys, List themes, - String defaultThemeId) { + String defaultThemeId, + List hostProjections) { + + public FrontendSemanticPresentationSpec( + final List semanticKeys, + final List themes, + final String defaultThemeId) { + this(semanticKeys, 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 (!defaultThemeId.isEmpty()) { throw new IllegalArgumentException("defaultThemeId must be blank when no themes are declared"); @@ -25,10 +34,27 @@ public record FrontendSemanticPresentationSpec( throw new IllegalArgumentException("defaultThemeId must reference one of the declared themes"); } } + final List normalizedSemanticKeys = semanticKeys.stream() + .map(key -> requireText(key, "semanticKeys entry")) + .toList(); + final var hostIds = new java.util.LinkedHashSet(); + 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())) { + 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(), ""); + return new FrontendSemanticPresentationSpec(List.of(), List.of(), "", List.of()); } public FrontendVisualThemeSpec defaultTheme() { @@ -42,7 +68,18 @@ public record FrontendSemanticPresentationSpec( } public boolean isEmpty() { - return semanticKeys.isEmpty() && themes.isEmpty() && defaultThemeId.isEmpty(); + return semanticKeys.isEmpty() && themes.isEmpty() && defaultThemeId.isEmpty() && hostProjections.isEmpty(); + } + + public java.util.Optional hostProjection(final String hostId) { + return hostProjections.stream() + .filter(hostProjection -> hostProjection.hostId().equals(requireText(hostId, "hostId"))) + .findFirst(); + } + + public FrontendHostProjectionSpec requireHostProjection(final String hostId) { + return hostProjection(hostId) + .orElseThrow(() -> new IllegalArgumentException("missing host projection for hostId: " + hostId)); } private static String requireText( diff --git a/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/models/FrontendSemanticPresentationSpecTest.java b/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/models/FrontendSemanticPresentationSpecTest.java new file mode 100644 index 00000000..ded116dd --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/models/FrontendSemanticPresentationSpecTest.java @@ -0,0 +1,85 @@ +package p.studio.compiler.models; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class FrontendSemanticPresentationSpecTest { + + @Test + void shouldExposeHostProjectionByHostId() { + final var presentation = new FrontendSemanticPresentationSpec( + List.of("demo-key"), + 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)))), + "demo-theme", + List.of(new FrontendHostProjectionSpec( + "vscode", + List.of(new FrontendHostProjectionEntrySpec( + "demo-key", + "keyword", + List.of("declaration"), + "variable", + List.of()))))); + + final var vscodeProjection = presentation.requireHostProjection("vscode").requireProjection("demo-key"); + assertEquals("keyword", vscodeProjection.hostTokenType()); + assertEquals(List.of("declaration"), vscodeProjection.hostTokenModifiers()); + assertEquals("variable", vscodeProjection.fallbackTokenType()); + } + + @Test + void shouldRejectDuplicateHostIds() { + assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec( + List.of("demo-key"), + 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)))), + "demo-theme", + List.of( + new FrontendHostProjectionSpec( + "vscode", + List.of(new FrontendHostProjectionEntrySpec( + "demo-key", + "keyword", + List.of(), + "variable", + List.of()))), + new FrontendHostProjectionSpec( + "vscode", + List.of(new FrontendHostProjectionEntrySpec( + "demo-key", + "type", + List.of(), + "variable", + List.of())))))); + } + + @Test + void shouldRejectProjectionForUnknownSemanticKey() { + assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec( + List.of("demo-key"), + 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)))), + "demo-theme", + List.of(new FrontendHostProjectionSpec( + "vscode", + List.of(new FrontendHostProjectionEntrySpec( + "unknown-key", + "keyword", + List.of(), + "variable", + List.of())))))); + } +}