Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion scripts/e2e-settings-visual-theme.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ test('theme registry exposes the requested Monaco themes and safe fallback', ()
assert.equal(DEFAULT_VISUAL_THEME_ID, 'browser-cpp-dark');
assert.deepEqual(VISUAL_THEMES.map(({ id }) => id), [
'browser-cpp-dark', 'vs', 'vs-dark', 'hc-light', 'hc-black',
'monochrome-light', 'monochrome-dark',
]);
assert.equal(getVisualTheme('not-a-theme').id, DEFAULT_VISUAL_THEME_ID);
for (const theme of VISUAL_THEMES) {
Expand All @@ -34,10 +35,25 @@ test('theme registry exposes the requested Monaco themes and safe fallback', ()
}
assert.deepEqual(VISUAL_THEMES.map(({ id }) => id), [
'browser-cpp-dark', 'vs', 'vs-dark', 'hc-light', 'hc-black',
'monochrome-light', 'monochrome-dark',
]);
assert.deepEqual(VISUAL_THEMES.slice(1).map(({ monacoTheme }) => monacoTheme), [
'browser-cpp-vs', 'browser-cpp-vs-dark', 'browser-cpp-hc-light', 'browser-cpp-hc-black',
'browser-cpp-monochrome-light', 'browser-cpp-monochrome-dark',
]);

for (const theme of VISUAL_THEMES.slice(-2)) {
const colors = [
...Object.values(theme.cssTokens),
...Object.values(theme.terminalTheme),
...Object.values(theme.preview),
theme.selection.foreground,
theme.selection.background,
theme.selection.inactiveBackground,
].filter((value) => typeof value === 'string' && value.startsWith('#'));
assert.ok(colors.length > 0);
assert.ok(colors.every(isGrayscale), `${theme.id} contains a chromatic color`);
}
});

test('terminal viewport inherits the active visual theme background', async () => {
Expand All @@ -50,10 +66,31 @@ test('terminal viewport inherits the active visual theme background', async () =
);
assert.deepEqual(
[...css.matchAll(/--terminal-background:\s*(#[\da-f]{3,6});/gi)].map(([, color]) => color.toLowerCase()),
['#11111b', '#fff', '#181818', '#fff', '#000']
['#11111b', '#fff', '#181818', '#fff', '#000', '#ddd', '#222']
);
});

test('monochrome themes separate panels and outline buttons', async () => {
const css = await readFile(new URL('../src/ui/styles.css', import.meta.url), 'utf8');
assert.match(css, /--panel-explorer:#ddd/);
assert.match(css, /--panel-editor:#fff/);
assert.match(css, /--panel-terminal:#ddd/);
assert.match(css, /--panel-explorer:#222/);
assert.match(css, /--panel-editor:#000/);
assert.match(css, /--panel-terminal:#222/);
assert.match(css, /--button-border:#666/);
assert.match(css, /--button-border:#999/);
assert.match(css, /:root\[data-visual-theme="monochrome-light"\][\s\S]*button[\s\S]*border-color: var\(--button-border\);/);
assert.match(css, /:root\[data-visual-theme="monochrome-dark"\][\s\S]*button[\s\S]*border-color: var\(--button-border\);/);
assert.match(css, /\.panel-header-btn[\s\S]*border: none;/);
assert.match(css, /\.tab-close[\s\S]*border: none;/);
assert.match(css, /\.panel-controls button[\s\S]*border: none;/);
assert.match(css, /monochrome-light[^}]*\.panel-header-btn[\s\S]*border: 1px solid var\(--button-border\);/);
assert.match(css, /monochrome-light[^}]*\.tab[\s\S]*border: 1px solid var\(--panel-border\);/);
assert.match(css, /monochrome-dark[^}]*\.tab\.active[\s\S]*border: 1px solid var\(--panel-border\);/);
assert.match(css, /monochrome-dark[^}]*border-color: var\(--blue\);/);
});

function contrastRatio(foreground, background) {
const luminance = (color) => {
const channels = color.match(/[0-9a-f]{2}/gi).map((channel) => parseInt(channel, 16) / 255);
Expand All @@ -77,3 +114,20 @@ test('visual theme defaults, sanitizes legacy storage, and persists immediately'
assert.deepEqual(settings.get(), { memoryDebugMode: true, visualTheme: 'vs' });
assert.deepEqual(storage.value, { memoryDebugMode: true, visualTheme: 'vs' });
});

test('monochrome themes persist and unknown values still use the existing default', async () => {
const storage = createStorage({ memoryDebugMode: false, visualTheme: 'monochrome-light' });
const settings = createExtensionSettings({ storage });
assert.equal((await settings.load()).visualTheme, 'monochrome-light');
await settings.setVisualTheme('monochrome-dark');
assert.equal(storage.value.visualTheme, 'monochrome-dark');

const invalidStorage = createStorage({ visualTheme: 'not-a-theme' });
const invalidSettings = createExtensionSettings({ storage: invalidStorage });
assert.equal((await invalidSettings.load()).visualTheme, DEFAULT_VISUAL_THEME_ID);
});

function isGrayscale(color) {
const channels = color.slice(1).match(/.{2}/g)?.map((channel) => parseInt(channel, 16));
return channels?.length === 3 && channels[0] === channels[1] && channels[1] === channels[2];
}
59 changes: 56 additions & 3 deletions scripts/smoke-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,7 @@ int main() {
assert(helpOutput.includes('Run the last compiled binary'),
'Real help output is missing the run-command description');
let previousSelection = null;
for (const themeId of ['vs', 'hc-light', 'browser-cpp-dark', 'vs-dark', 'hc-black']) {
for (const themeId of ['vs', 'hc-light', 'browser-cpp-dark', 'vs-dark', 'hc-black', 'monochrome-light', 'monochrome-dark']) {
await evaluate(cdp, sessionId, `document.querySelector('input[name="visual-theme"][value="${themeId}"]')?.click()`);
await waitFor(async () => evaluate(
cdp,
Expand All @@ -1051,6 +1051,17 @@ int main() {
`${themeId} computed Monaco selection styles`, 5_000);
assert(monacoColors?.background && monacoColors.foreground && monacoColors.inactiveBackground,
`${themeId} Monaco selection styles were not computed`);
if (themeId.startsWith('monochrome-')) {
const editorTextColors = await evaluate(cdp, sessionId, `(() => {
const spans = [...document.querySelectorAll('.monaco-editor .view-lines .view-line span')];
return spans.map((element) => getComputedStyle(element).color).filter(Boolean);
})()`);
assert(editorTextColors.length > 0, `${themeId} rendered no Monaco text spans`);
assert(editorTextColors.every((color) => {
const channels = color.match(/[\\d.]+/g)?.map(Number);
return channels?.length >= 3 && channels[0] === channels[1] && channels[1] === channels[2];
}), `${themeId} Monaco text contains chromatic syntax or bracket colors`);
}
if (previousSelection) {
const retained = await evaluate(cdp, sessionId,
`document.getElementById('terminal-container')?.__browserCppReadTerminalSelection?.() || null`);
Expand Down Expand Up @@ -1167,7 +1178,7 @@ int main() {
assert(createdFileText === 'hello from fstream\n', `Expected output.txt to be created, got: ${JSON.stringify(createdFileText)}`);
assert(terminalText.includes('5 stream wrote output.txt'), `Expected stream-insertion output, got: ${JSON.stringify(terminalText)}`);

for (const themeId of ['vs', 'hc-light']) {
for (const themeId of ['vs', 'hc-light', 'monochrome-light', 'monochrome-dark']) {
await evaluate(cdp, sessionId, `(() => {
const input = document.querySelector('input[name="visual-theme"][value="${themeId}"]');
input?.click();
Expand All @@ -1179,14 +1190,56 @@ int main() {
`document.documentElement.dataset.visualTheme === '${themeId}'`
), `${themeId} theme selection`, 10_000);

if (themeId.startsWith('monochrome-')) {
const panelStyles = await evaluate(cdp, sessionId, `(() => {
const style = (selector) => {
const element = document.querySelector(selector);
const computed = element && getComputedStyle(element);
return computed && {
background: computed.backgroundColor,
border: computed.borderRightColor || computed.borderTopColor,
};
};
const button = document.querySelector('#toolbar button');
return {
explorer: style('#sidebar'),
editor: style('#editor-container'),
terminal: style('#terminal-panel'),
buttonBorder: button ? getComputedStyle(button).borderTopColor : null,
};
})()`);
const expectedPanels = themeId === 'monochrome-light'
? { explorer: '#dddddd', editor: '#ffffff', terminal: '#dddddd', border: '#666666' }
: { explorer: '#222222', editor: '#000000', terminal: '#222222', border: '#999999' };
assert(normalizeCssColor(panelStyles.explorer.background) === expectedPanels.explorer,
`${themeId} explorer panel has the wrong background`);
assert(normalizeCssColor(panelStyles.editor.background) === expectedPanels.editor,
`${themeId} editor panel has the wrong background`);
assert(normalizeCssColor(panelStyles.terminal.background) === expectedPanels.terminal,
`${themeId} terminal panel has the wrong background`);
assert(normalizeCssColor(panelStyles.buttonBorder) === expectedPanels.border,
`${themeId} buttons do not have a contrasting outline`);
}

const selection = await selectTerminalText(cdp, sessionId);
const expectedSelectionBackground = themeId === 'vs' ? '#0969da' : '#0037da';
const expectedSelectionBackground = {
vs: '#0969da',
'hc-light': '#0037da',
'monochrome-light': '#666666',
'monochrome-dark': '#999999',
}[themeId];
assert(selection.theme.selectionBackground, `${themeId} terminal selection background is missing`);
assert(selection.theme.selectionForeground, `${themeId} terminal selection foreground is missing`);
assert(selection.theme.selectionBackground === expectedSelectionBackground,
`${themeId} did not apply its configured terminal selection background`);
assert(selection.theme.selectionBackground !== selection.theme.background,
`${themeId} terminal selection is not distinct from its background`);
if (themeId.startsWith('monochrome-')) {
const colors = [selection.theme.background, selection.theme.foreground,
selection.theme.selectionBackground, selection.theme.selectionForeground];
assert(colors.every((color) => /^#([\da-f]{2})\1\1$/i.test(color)),
`${themeId} terminal colors are not grayscale: ${colors.join(', ')}`);
}
const currentTerminal = await evaluate(
cdp,
sessionId,
Expand Down
46 changes: 46 additions & 0 deletions src/ui/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ self.MonacoEnvironment = {
// ── Internal state ────────────────────────────────────────────────────────────
let _editor = null;

function isMonochromeTheme(theme) {
return theme === 'browser-cpp-monochrome-light' || theme === 'browser-cpp-monochrome-dark';
}

// ── Public API ────────────────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -73,6 +77,39 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) {

for (const visualTheme of VISUAL_THEMES) {
if (visualTheme.id === 'browser-cpp-dark') continue;
if (visualTheme.id.startsWith('monochrome-')) {
const { cssTokens: colors } = visualTheme;
const isLight = visualTheme.id === 'monochrome-light';
monaco.editor.defineTheme(visualTheme.monacoTheme, {
base: isLight ? 'vs' : 'vs-dark',
inherit: false,
rules: [],
colors: {
'editor.background': colors.base,
'editor.foreground': colors.text,
'editorLineNumber.foreground': colors.subtext0,
'editorLineNumber.activeForeground': colors.text,
'editor.lineHighlightBackground': colors.surface0,
'editorCursor.foreground': colors.text,
'editor.selectionForeground': visualTheme.selection.foreground,
'editor.selectionBackground': visualTheme.selection.background,
'editor.inactiveSelectionBackground': visualTheme.selection.inactiveBackground,
'editorIndentGuide.background': colors.surface1,
'editorWidget.background': colors.mantle,
'editorSuggestWidget.background': colors.mantle,
'editorSuggestWidget.border': colors.surface1,
'minimap.background': colors.mantle,
'editorBracketHighlight.foreground1': colors.text,
'editorBracketHighlight.foreground2': colors.text,
'editorBracketHighlight.foreground3': colors.text,
'editorBracketHighlight.foreground4': colors.text,
'editorBracketHighlight.foreground5': colors.text,
'editorBracketHighlight.foreground6': colors.text,
'editorBracketHighlight.unexpectedBracket.foreground': colors.text,
},
});
continue;
}
monaco.editor.defineTheme(visualTheme.monacoTheme, {
base: visualTheme.id,
inherit: true,
Expand Down Expand Up @@ -107,6 +144,11 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) {
automaticLayout: true, // resize with container
});

_editor.updateOptions({
bracketPairColorization: { enabled: !isMonochromeTheme(theme) },
guides: { bracketPairs: !isMonochromeTheme(theme), indentation: true },
});

if (new URLSearchParams(globalThis.location?.search || '').has('smoke-test')) {
container.__browserCppPrepareSelectionColorRead = () => {
return true;
Expand Down Expand Up @@ -140,6 +182,10 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) {

export function setTheme(theme) {
monaco.editor.setTheme(theme);
_editor?.updateOptions({
bracketPairColorization: { enabled: !isMonochromeTheme(theme) },
guides: { bracketPairs: !isMonochromeTheme(theme), indentation: true },
});
}

/** Get the full text of the current document. */
Expand Down
Loading
Loading