diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 17cc4fc5..3052a833 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":"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-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":"done","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-0073-lsp-semantic-host-projection-transport.md b/discussion/workflow/plans/PLN-0073-lsp-semantic-host-projection-transport.md index 3c7ea505..038e6bad 100644 --- a/discussion/workflow/plans/PLN-0073-lsp-semantic-host-projection-transport.md +++ b/discussion/workflow/plans/PLN-0073-lsp-semantic-host-projection-transport.md @@ -2,9 +2,9 @@ id: PLN-0073 ticket: frontend-semantic-host-projection-flexibility title: LSP Semantic Host Projection Transport -status: open +status: done created: 2026-05-06 -completed: +completed: 2026-05-06 tags: [lsp, compiler, frontend, semantics, host-projection, vscode, protocol] --- diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjection.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjection.java new file mode 100644 index 00000000..ed617665 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjection.java @@ -0,0 +1,27 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSemanticHostProjection( + String hostId, + List tokenProjections) { + + public BaselineSemanticHostProjection { + hostId = requireText(hostId, "hostId"); + tokenProjections = List.copyOf(Objects.requireNonNull(tokenProjections, "tokenProjections")); + if (tokenProjections.isEmpty()) { + throw new IllegalArgumentException("tokenProjections must not be empty"); + } + } + + 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-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjectionEntry.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjectionEntry.java new file mode 100644 index 00000000..406332e8 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjectionEntry.java @@ -0,0 +1,38 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSemanticHostProjectionEntry( + String semanticKey, + String hostTokenType, + List hostTokenModifiers, + String fallbackTokenType, + List fallbackTokenModifiers) { + + public BaselineSemanticHostProjectionEntry { + 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-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index 9734f319..6af55458 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -9,6 +9,7 @@ public record BaselineServerDescription( String frontendLanguageId, boolean hoverSupported, List semanticTokenTypes, + List semanticHostProjections, List visualThemes, String activeVisualThemeId) { @@ -17,8 +18,15 @@ public record BaselineServerDescription( version = requireText(version, "version"); frontendLanguageId = requireText(frontendLanguageId, "frontendLanguageId"); semanticTokenTypes = List.copyOf(Objects.requireNonNull(semanticTokenTypes, "semanticTokenTypes")); + semanticHostProjections = List.copyOf(Objects.requireNonNull(semanticHostProjections, "semanticHostProjections")); visualThemes = List.copyOf(Objects.requireNonNull(visualThemes, "visualThemes")); activeVisualThemeId = requireText(activeVisualThemeId, "activeVisualThemeId"); + final var hostIds = new java.util.LinkedHashSet(); + for (final BaselineSemanticHostProjection hostProjection : semanticHostProjections) { + if (!hostIds.add(hostProjection.hostId())) { + throw new IllegalArgumentException("duplicate semantic host projection for hostId: " + hostProjection.hostId()); + } + } if (visualThemes.stream().map(BaselineVisualTheme::themeId).noneMatch(activeVisualThemeId::equals)) { throw new IllegalArgumentException("activeVisualThemeId must reference one of the declared visual themes"); } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index 534c3ffa..c85617d8 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -6,6 +6,8 @@ import p.studio.compiler.messages.BuildingIssue; import p.studio.compiler.models.AnalysisSnapshot; import p.studio.compiler.models.BuilderPipelineContext; import p.studio.compiler.models.FrontendEditorPaletteSpec; +import p.studio.compiler.models.FrontendHostProjectionEntrySpec; +import p.studio.compiler.models.FrontendHostProjectionSpec; import p.studio.compiler.PBSDefinitions; import p.studio.compiler.models.FrontendTokenStyleSpec; import p.studio.compiler.models.FrontendVisualThemeSpec; @@ -33,14 +35,16 @@ public record CompilerLanguageServiceBridge() implements LanguageServiceBridge { @Override public BaselineServerDescription describeServer(final LspProjectContext context) { Objects.requireNonNull(context, "context"); + final var presentation = PBSDefinitions.PBS.getSemanticPresentation(); return new BaselineServerDescription( "Prometeu Studio LSP", "0.1.0", context.languageId(), true, - PBSDefinitions.PBS.getSemanticPresentation().semanticKeys(), - PBSDefinitions.PBS.getSemanticPresentation().themes().stream().map(this::mapVisualTheme).toList(), - PBSDefinitions.PBS.getSemanticPresentation().defaultThemeId()); + presentation.semanticKeys(), + presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), + presentation.themes().stream().map(this::mapVisualTheme).toList(), + presentation.defaultThemeId()); } @Override @@ -205,6 +209,21 @@ public record CompilerLanguageServiceBridge() implements LanguageServiceBridge { theme.tokenStyles().stream().map(this::mapTokenStyle).toList()); } + private BaselineSemanticHostProjection mapSemanticHostProjection(final FrontendHostProjectionSpec hostProjection) { + return new BaselineSemanticHostProjection( + hostProjection.hostId(), + hostProjection.tokenProjections().stream().map(this::mapSemanticHostProjectionEntry).toList()); + } + + private BaselineSemanticHostProjectionEntry mapSemanticHostProjectionEntry(final FrontendHostProjectionEntrySpec tokenProjection) { + return new BaselineSemanticHostProjectionEntry( + tokenProjection.semanticKey(), + tokenProjection.hostTokenType(), + tokenProjection.hostTokenModifiers(), + tokenProjection.fallbackTokenType(), + tokenProjection.fallbackTokenModifiers()); + } + private BaselineEditorPalette mapEditorPalette(final FrontendEditorPaletteSpec palette) { return new BaselineEditorPalette( palette.baseForeground(), diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index aba2dd5f..48caa92a 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -33,7 +33,12 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { Map.of( "frontendLanguageId", description.frontendLanguageId(), "activeVisualThemeId", description.activeVisualThemeId(), - "visualThemes", description.visualThemes().stream().map(this::mapVisualThemePayload).toList()))); + "visualThemes", description.visualThemes().stream().map(this::mapVisualThemePayload).toList()), + "prometeuSemanticHostProjections", + Map.of( + "frontendLanguageId", description.frontendLanguageId(), + "semanticKeys", description.semanticTokenTypes(), + "hostProjections", description.semanticHostProjections().stream().map(this::mapSemanticHostProjectionPayload).toList()))); final InitializeResult result = new InitializeResult(capabilities); final ServerInfo serverInfo = new ServerInfo(); @@ -151,4 +156,16 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { "underline", tokenStyle.underline())).toList()); return payload; } + + private Map mapSemanticHostProjectionPayload(final BaselineSemanticHostProjection hostProjection) { + final Map payload = new LinkedHashMap<>(); + payload.put("hostId", hostProjection.hostId()); + payload.put("tokenProjections", hostProjection.tokenProjections().stream().map(tokenProjection -> Map.of( + "semanticKey", tokenProjection.semanticKey(), + "hostTokenType", tokenProjection.hostTokenType(), + "hostTokenModifiers", tokenProjection.hostTokenModifiers(), + "fallbackTokenType", tokenProjection.fallbackTokenType(), + "fallbackTokenModifiers", tokenProjection.fallbackTokenModifiers())).toList()); + return payload; + } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index e613bb54..1c3c0a61 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -64,6 +64,11 @@ class CompilerLanguageServiceBridgeTest { assertEquals("pbs", description.frontendLanguageId()); assertEquals("pbs-default", description.activeVisualThemeId()); + assertEquals(1, description.semanticHostProjections().size()); + assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); + assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() + .anyMatch(tokenProjection -> tokenProjection.semanticKey().equals("pbs-keyword") + && tokenProjection.hostTokenType().equals("keyword"))); assertEquals(1, description.visualThemes().size()); assertEquals("PBS Default", description.visualThemes().getFirst().displayName()); assertEquals("#d9e2ec", description.visualThemes().getFirst().editorPalette().baseForeground()); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index f59b745c..5b4fc14d 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -14,6 +14,8 @@ import org.junit.jupiter.api.Test; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineEditorPalette; import p.studio.lsp.messages.BaselineHover; +import p.studio.lsp.messages.BaselineSemanticHostProjection; +import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry; import p.studio.lsp.messages.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineServerDescription; import p.studio.lsp.messages.BaselineTokenStyle; @@ -89,6 +91,14 @@ class PrometeuLanguageServerTest { "pbs", true, List.of(), + List.of(new BaselineSemanticHostProjection( + "vscode", + List.of(new BaselineSemanticHostProjectionEntry( + "demo-keyword", + "keyword", + List.of(), + "variable", + List.of())))), List.of(new BaselineVisualTheme( "demo-default", "Demo Default", diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java new file mode 100644 index 00000000..bf6a9b24 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -0,0 +1,62 @@ +package p.studio.lsp.services.protocol.mapping; + +import org.eclipse.lsp4j.InitializeResult; +import org.junit.jupiter.api.Test; +import p.studio.lsp.messages.BaselineEditorPalette; +import p.studio.lsp.messages.BaselineSemanticHostProjection; +import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry; +import p.studio.lsp.messages.BaselineServerDescription; +import p.studio.lsp.messages.BaselineTokenStyle; +import p.studio.lsp.messages.BaselineVisualTheme; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +final class Lsp4jProtocolMessageMapperTest { + + @Test + void initializeResultPublishesCanonicalKeysAndHostProjectionPayload() { + final var mapper = new Lsp4jProtocolMessageMapper(); + final var description = new BaselineServerDescription( + "demo", + "1", + "pbs", + true, + List.of("demo-keyword"), + List.of(new BaselineSemanticHostProjection( + "vscode", + List.of(new BaselineSemanticHostProjectionEntry( + "demo-keyword", + "keyword", + List.of("declaration"), + "variable", + List.of())))), + List.of(new BaselineVisualTheme( + "demo-default", + "Demo Default", + new BaselineEditorPalette("#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777"), + List.of(new BaselineTokenStyle("demo-keyword", "#888888", false, false, false)))), + "demo-default"); + + final InitializeResult result = mapper.mapInitializeResult(description); + + final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); + final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); + assertEquals("pbs", semanticPayload.get("frontendLanguageId")); + assertEquals(List.of("demo-keyword"), semanticPayload.get("semanticKeys")); + + final var hostProjectionPayloads = assertInstanceOf(List.class, semanticPayload.get("hostProjections")); + final var firstHostProjection = assertInstanceOf(Map.class, hostProjectionPayloads.getFirst()); + assertEquals("vscode", firstHostProjection.get("hostId")); + + final var tokenProjectionPayloads = assertInstanceOf(List.class, firstHostProjection.get("tokenProjections")); + final var firstTokenProjection = assertInstanceOf(Map.class, tokenProjectionPayloads.getFirst()); + assertEquals("demo-keyword", firstTokenProjection.get("semanticKey")); + assertEquals("keyword", firstTokenProjection.get("hostTokenType")); + assertEquals(List.of("declaration"), firstTokenProjection.get("hostTokenModifiers")); + assertEquals("variable", firstTokenProjection.get("fallbackTokenType")); + } +}