implements PLN-0074

This commit is contained in:
bQUARKz 2026-05-06 15:28:37 +01:00
parent e5ab4cd021
commit 7ae2e94d63
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 170 additions and 134 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":"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-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":"done","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-0074
ticket: frontend-semantic-host-projection-flexibility
title: VS Code Semantic Host Projection Adapter
status: open
status: done
created: 2026-05-06
completed:
completed: 2026-05-06
tags: [vscode, extension, frontend, semantics, host-projection, adapter]
---

View File

@ -45,6 +45,7 @@ let reconnectContext;
let stoppingClient = false;
let lastAppliedThemeSignature;
const RECONNECT_DELAY_MS = 2000;
const VSCODE_HOST_ID = "vscode";
function activate(context) {
reconnectContext = context;
output = vscode.window.createOutputChannel("Prometeu LSP");
@ -207,21 +208,30 @@ function clearReconnectTimer() {
reconnectTimer = undefined;
}
async function applyPrometeuVisualTheme(initializeResult) {
const payload = extractPrometeuVisualThemesPayload(initializeResult);
if (!payload) {
const visualThemesPayload = extractPrometeuVisualThemesPayload(initializeResult);
if (!visualThemesPayload) {
output.appendLine("Prometeu visual theme payload not present in initialize result.");
return;
}
const activeTheme = payload.visualThemes.find((theme) => theme.themeId === payload.activeVisualThemeId);
const semanticProjectionPayload = extractPrometeuSemanticHostProjectionsPayload(initializeResult);
const vscodeProjection = selectHostProjection(semanticProjectionPayload, VSCODE_HOST_ID);
if (!semanticProjectionPayload) {
output.appendLine("Prometeu semantic host projection payload not present in initialize result.");
}
else if (!vscodeProjection) {
output.appendLine("Prometeu semantic host projection payload does not declare a vscode projection.");
}
const activeTheme = visualThemesPayload.visualThemes.find((theme) => theme.themeId === visualThemesPayload.activeVisualThemeId);
if (!activeTheme) {
output.appendLine(`Prometeu visual theme '${payload.activeVisualThemeId}' was not declared by the server.`);
output.appendLine(`Prometeu visual theme '${visualThemesPayload.activeVisualThemeId}' was not declared by the server.`);
return;
}
const themeSignature = JSON.stringify({
languageId: payload.frontendLanguageId,
languageId: visualThemesPayload.frontendLanguageId,
themeId: activeTheme.themeId,
editorPalette: activeTheme.editorPalette,
tokenStyles: activeTheme.tokenStyles
tokenStyles: activeTheme.tokenStyles,
semanticProjection: vscodeProjection ?? null
});
if (themeSignature === lastAppliedThemeSignature) {
output.appendLine(`Prometeu visual theme '${activeTheme.themeId}' is already applied.`);
@ -234,7 +244,7 @@ async function applyPrometeuVisualTheme(initializeResult) {
const existingSemanticCustomizations = configuration.get("editor.semanticTokenColorCustomizations") ?? {};
const semanticRules = {
...(readObject(existingSemanticCustomizations.rules)),
...buildSemanticTokenRules(payload.frontendLanguageId, activeTheme)
...buildSemanticTokenRules(visualThemesPayload.frontendLanguageId, activeTheme, vscodeProjection)
};
await configuration.update("editor.semanticTokenColorCustomizations", {
...existingSemanticCustomizations,
@ -249,7 +259,7 @@ async function applyPrometeuVisualTheme(initializeResult) {
"editorLineNumber.foreground": activeTheme.editorPalette.lineNumberForeground
}, configurationTarget);
lastAppliedThemeSignature = themeSignature;
output.appendLine(`Applied Prometeu visual theme '${activeTheme.themeId}' for language '${payload.frontendLanguageId}'.`);
output.appendLine(`Applied Prometeu visual theme '${activeTheme.themeId}' for language '${visualThemesPayload.frontendLanguageId}'.`);
}
function extractPrometeuVisualThemesPayload(initializeResult) {
if (!initializeResult || typeof initializeResult !== "object") {
@ -266,23 +276,59 @@ function extractPrometeuVisualThemesPayload(initializeResult) {
}
return payload;
}
function buildSemanticTokenRules(languageId, theme) {
function extractPrometeuSemanticHostProjectionsPayload(initializeResult) {
if (!initializeResult || typeof initializeResult !== "object") {
return undefined;
}
const capabilities = readObject(initializeResult.capabilities);
const experimental = readObject(capabilities.experimental);
const payload = readObject(experimental.prometeuSemanticHostProjections);
if (typeof payload.frontendLanguageId !== "string" || !Array.isArray(payload.semanticKeys) || !Array.isArray(payload.hostProjections)) {
return undefined;
}
return payload;
}
function selectHostProjection(payload, hostId) {
if (!payload) {
return undefined;
}
return payload.hostProjections.find((projection) => projection.hostId === hostId);
}
function buildSemanticTokenRules(languageId, theme, hostProjection) {
const rules = {};
const projectionBySemanticKey = new Map((hostProjection?.tokenProjections ?? []).map((projection) => [projection.semanticKey, projection]));
for (const tokenStyle of theme.tokenStyles) {
const selector = `${tokenStyle.semanticKey}:${languageId}`;
if (!tokenStyle.italic && !tokenStyle.bold && !tokenStyle.underline) {
rules[selector] = tokenStyle.foreground;
const styleValue = !tokenStyle.italic && !tokenStyle.bold && !tokenStyle.underline
? tokenStyle.foreground
: {
foreground: tokenStyle.foreground,
...(tokenStyle.italic ? { italic: true } : {}),
...(tokenStyle.bold ? { bold: true } : {}),
...(tokenStyle.underline ? { underline: true } : {})
};
rules[`${tokenStyle.semanticKey}:${languageId}`] = styleValue;
const projection = projectionBySemanticKey.get(tokenStyle.semanticKey);
if (!projection) {
continue;
}
rules[selector] = {
foreground: tokenStyle.foreground,
...(tokenStyle.italic ? { italic: true } : {}),
...(tokenStyle.bold ? { bold: true } : {}),
...(tokenStyle.underline ? { underline: true } : {})
};
const projectedSelector = buildSemanticSelector(languageId, projection.hostTokenType, projection.hostTokenModifiers);
if (!(projectedSelector in rules)) {
rules[projectedSelector] = styleValue;
}
const fallbackSelector = buildSemanticSelector(languageId, projection.fallbackTokenType, projection.fallbackTokenModifiers);
if (!(fallbackSelector in rules)) {
rules[fallbackSelector] = styleValue;
}
}
return rules;
}
function buildSemanticSelector(languageId, tokenType, modifiers) {
const normalizedModifiers = [...modifiers].sort();
if (normalizedModifiers.length === 0) {
return `${tokenType}:${languageId}`;
}
return `${tokenType}.${normalizedModifiers.join(".")}:${languageId}`;
}
function readObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};

View File

@ -53,100 +53,6 @@
}
}
},
"semanticTokenTypes": [
{
"id": "pbs-comment",
"description": "PBS comment token"
},
{
"id": "pbs-string",
"description": "PBS string token"
},
{
"id": "pbs-number",
"description": "PBS number token"
},
{
"id": "pbs-literal",
"description": "PBS literal token"
},
{
"id": "pbs-lifecycle",
"description": "PBS lifecycle token"
},
{
"id": "pbs-keyword",
"description": "PBS keyword token"
},
{
"id": "pbs-operator",
"description": "PBS operator token"
},
{
"id": "pbs-punctuation",
"description": "PBS punctuation token"
},
{
"id": "pbs-function",
"description": "PBS function token"
},
{
"id": "pbs-method",
"description": "PBS method token"
},
{
"id": "pbs-constructor",
"description": "PBS constructor token"
},
{
"id": "pbs-struct",
"description": "PBS struct token"
},
{
"id": "pbs-contract",
"description": "PBS contract token"
},
{
"id": "pbs-host",
"description": "PBS host token"
},
{
"id": "pbs-builtin-type",
"description": "PBS builtin type token"
},
{
"id": "pbs-service",
"description": "PBS service token"
},
{
"id": "pbs-error",
"description": "PBS error token"
},
{
"id": "pbs-enum",
"description": "PBS enum token"
},
{
"id": "pbs-callback",
"description": "PBS callback token"
},
{
"id": "pbs-global",
"description": "PBS global token"
},
{
"id": "pbs-const",
"description": "PBS const token"
},
{
"id": "pbs-implements",
"description": "PBS implements token"
},
{
"id": "pbs-identifier",
"description": "PBS identifier token"
}
],
"configurationDefaults": {
"editor.semanticHighlighting.enabled": true
}

View File

@ -51,6 +51,27 @@ type PrometeuVisualThemesPayload = {
visualThemes: PrometeuVisualTheme[];
};
type PrometeuSemanticHostProjectionEntry = {
semanticKey: string;
hostTokenType: string;
hostTokenModifiers: string[];
fallbackTokenType: string;
fallbackTokenModifiers: string[];
};
type PrometeuSemanticHostProjection = {
hostId: string;
tokenProjections: PrometeuSemanticHostProjectionEntry[];
};
type PrometeuSemanticHostProjectionsPayload = {
frontendLanguageId: string;
semanticKeys: string[];
hostProjections: PrometeuSemanticHostProjection[];
};
const VSCODE_HOST_ID = "vscode";
export function activate(context: vscode.ExtensionContext): void {
reconnectContext = context;
output = vscode.window.createOutputChannel("Prometeu LSP");
@ -255,23 +276,31 @@ function clearReconnectTimer(): void {
}
async function applyPrometeuVisualTheme(initializeResult: unknown): Promise<void> {
const payload = extractPrometeuVisualThemesPayload(initializeResult);
if (!payload) {
const visualThemesPayload = extractPrometeuVisualThemesPayload(initializeResult);
if (!visualThemesPayload) {
output.appendLine("Prometeu visual theme payload not present in initialize result.");
return;
}
const semanticProjectionPayload = extractPrometeuSemanticHostProjectionsPayload(initializeResult);
const vscodeProjection = selectHostProjection(semanticProjectionPayload, VSCODE_HOST_ID);
if (!semanticProjectionPayload) {
output.appendLine("Prometeu semantic host projection payload not present in initialize result.");
} else if (!vscodeProjection) {
output.appendLine("Prometeu semantic host projection payload does not declare a vscode projection.");
}
const activeTheme = payload.visualThemes.find((theme) => theme.themeId === payload.activeVisualThemeId);
const activeTheme = visualThemesPayload.visualThemes.find((theme) => theme.themeId === visualThemesPayload.activeVisualThemeId);
if (!activeTheme) {
output.appendLine(`Prometeu visual theme '${payload.activeVisualThemeId}' was not declared by the server.`);
output.appendLine(`Prometeu visual theme '${visualThemesPayload.activeVisualThemeId}' was not declared by the server.`);
return;
}
const themeSignature = JSON.stringify({
languageId: payload.frontendLanguageId,
languageId: visualThemesPayload.frontendLanguageId,
themeId: activeTheme.themeId,
editorPalette: activeTheme.editorPalette,
tokenStyles: activeTheme.tokenStyles
tokenStyles: activeTheme.tokenStyles,
semanticProjection: vscodeProjection ?? null
});
if (themeSignature === lastAppliedThemeSignature) {
output.appendLine(`Prometeu visual theme '${activeTheme.themeId}' is already applied.`);
@ -286,7 +315,7 @@ async function applyPrometeuVisualTheme(initializeResult: unknown): Promise<void
const existingSemanticCustomizations = configuration.get<Record<string, unknown>>("editor.semanticTokenColorCustomizations") ?? {};
const semanticRules = {
...(readObject(existingSemanticCustomizations.rules)),
...buildSemanticTokenRules(payload.frontendLanguageId, activeTheme)
...buildSemanticTokenRules(visualThemesPayload.frontendLanguageId, activeTheme, vscodeProjection)
};
await configuration.update(
@ -312,7 +341,7 @@ async function applyPrometeuVisualTheme(initializeResult: unknown): Promise<void
);
lastAppliedThemeSignature = themeSignature;
output.appendLine(`Applied Prometeu visual theme '${activeTheme.themeId}' for language '${payload.frontendLanguageId}'.`);
output.appendLine(`Applied Prometeu visual theme '${activeTheme.themeId}' for language '${visualThemesPayload.frontendLanguageId}'.`);
}
function extractPrometeuVisualThemesPayload(initializeResult: unknown): PrometeuVisualThemesPayload | undefined {
@ -334,27 +363,82 @@ function extractPrometeuVisualThemesPayload(initializeResult: unknown): Prometeu
return payload as unknown as PrometeuVisualThemesPayload;
}
function extractPrometeuSemanticHostProjectionsPayload(initializeResult: unknown): PrometeuSemanticHostProjectionsPayload | undefined {
if (!initializeResult || typeof initializeResult !== "object") {
return undefined;
}
const capabilities = readObject((initializeResult as { capabilities?: unknown }).capabilities);
const experimental = readObject(capabilities.experimental);
const payload = readObject(experimental.prometeuSemanticHostProjections);
if (typeof payload.frontendLanguageId !== "string" || !Array.isArray(payload.semanticKeys) || !Array.isArray(payload.hostProjections)) {
return undefined;
}
return payload as unknown as PrometeuSemanticHostProjectionsPayload;
}
function selectHostProjection(
payload: PrometeuSemanticHostProjectionsPayload | undefined,
hostId: string
): PrometeuSemanticHostProjection | undefined {
if (!payload) {
return undefined;
}
return payload.hostProjections.find((projection) => projection.hostId === hostId);
}
function buildSemanticTokenRules(
languageId: string,
theme: PrometeuVisualTheme
theme: PrometeuVisualTheme,
hostProjection: PrometeuSemanticHostProjection | undefined
): Record<string, string | { foreground: string; italic?: boolean; bold?: boolean; underline?: boolean }> {
const rules: Record<string, string | { foreground: string; italic?: boolean; bold?: boolean; underline?: boolean }> = {};
const projectionBySemanticKey = new Map<string, PrometeuSemanticHostProjectionEntry>(
(hostProjection?.tokenProjections ?? []).map((projection) => [projection.semanticKey, projection])
);
for (const tokenStyle of theme.tokenStyles) {
const selector = `${tokenStyle.semanticKey}:${languageId}`;
if (!tokenStyle.italic && !tokenStyle.bold && !tokenStyle.underline) {
rules[selector] = tokenStyle.foreground;
const styleValue = !tokenStyle.italic && !tokenStyle.bold && !tokenStyle.underline
? tokenStyle.foreground
: {
foreground: tokenStyle.foreground,
...(tokenStyle.italic ? { italic: true } : {}),
...(tokenStyle.bold ? { bold: true } : {}),
...(tokenStyle.underline ? { underline: true } : {})
};
rules[`${tokenStyle.semanticKey}:${languageId}`] = styleValue;
const projection = projectionBySemanticKey.get(tokenStyle.semanticKey);
if (!projection) {
continue;
}
rules[selector] = {
foreground: tokenStyle.foreground,
...(tokenStyle.italic ? { italic: true } : {}),
...(tokenStyle.bold ? { bold: true } : {}),
...(tokenStyle.underline ? { underline: true } : {})
};
const projectedSelector = buildSemanticSelector(languageId, projection.hostTokenType, projection.hostTokenModifiers);
if (!(projectedSelector in rules)) {
rules[projectedSelector] = styleValue;
}
const fallbackSelector = buildSemanticSelector(languageId, projection.fallbackTokenType, projection.fallbackTokenModifiers);
if (!(fallbackSelector in rules)) {
rules[fallbackSelector] = styleValue;
}
}
return rules;
}
function buildSemanticSelector(
languageId: string,
tokenType: string,
modifiers: string[]
): string {
const normalizedModifiers = [...modifiers].sort();
if (normalizedModifiers.length === 0) {
return `${tokenType}:${languageId}`;
}
return `${tokenType}.${normalizedModifiers.join(".")}:${languageId}`;
}
function readObject(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};