implements PLN-0073

This commit is contained in:
bQUARKz 2026-05-06 15:26:24 +01:00
parent 9d75f2f0e2
commit e5ab4cd021
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 193 additions and 7 deletions

View File

@ -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":[]}

View File

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

View File

@ -0,0 +1,27 @@
package p.studio.lsp.messages;
import java.util.List;
import java.util.Objects;
public record BaselineSemanticHostProjection(
String hostId,
List<BaselineSemanticHostProjectionEntry> 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;
}
}

View File

@ -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<String> hostTokenModifiers,
String fallbackTokenType,
List<String> 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<String> normalizeList(
final List<String> 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;
}
}

View File

@ -9,6 +9,7 @@ public record BaselineServerDescription(
String frontendLanguageId,
boolean hoverSupported,
List<String> semanticTokenTypes,
List<BaselineSemanticHostProjection> semanticHostProjections,
List<BaselineVisualTheme> 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<String>();
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");
}

View File

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

View File

@ -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<String, Object> mapSemanticHostProjectionPayload(final BaselineSemanticHostProjection hostProjection) {
final Map<String, Object> 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;
}
}

View File

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

View File

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

View File

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