diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 3052a833..f17e173d 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":"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":[]} diff --git a/discussion/workflow/plans/PLN-0074-vscode-semantic-host-projection-adapter.md b/discussion/workflow/plans/PLN-0074-vscode-semantic-host-projection-adapter.md index eab87eec..53bc1fc3 100644 --- a/discussion/workflow/plans/PLN-0074-vscode-semantic-host-projection-adapter.md +++ b/discussion/workflow/plans/PLN-0074-vscode-semantic-host-projection-adapter.md @@ -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] --- diff --git a/tools/vscode-extension/out/extension.js b/tools/vscode-extension/out/extension.js index 6e850aff..3baaba89 100644 --- a/tools/vscode-extension/out/extension.js +++ b/tools/vscode-extension/out/extension.js @@ -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,27 +276,63 @@ 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 {}; } return value; } -//# sourceMappingURL=extension.js.map \ No newline at end of file +//# sourceMappingURL=extension.js.map diff --git a/tools/vscode-extension/package.json b/tools/vscode-extension/package.json index f370837e..823a02a4 100644 --- a/tools/vscode-extension/package.json +++ b/tools/vscode-extension/package.json @@ -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 } diff --git a/tools/vscode-extension/src/extension.ts b/tools/vscode-extension/src/extension.ts index 19d7a1b5..090ae8d6 100644 --- a/tools/vscode-extension/src/extension.ts +++ b/tools/vscode-extension/src/extension.ts @@ -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 { - 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>("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 projection.hostId === hostId); +} + function buildSemanticTokenRules( languageId: string, - theme: PrometeuVisualTheme + theme: PrometeuVisualTheme, + hostProjection: PrometeuSemanticHostProjection | undefined ): Record { const rules: Record = {}; + 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: 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 { if (!value || typeof value !== "object" || Array.isArray(value)) { return {};