diff --git a/apps/web/src/vscodeThemeImport.test.ts b/apps/web/src/vscodeThemeImport.test.ts index 59f732bf4a12975c65206d4fae3817e5b0597d6f..797346a6557b6c608d60c291c8b0e43536986f1a 100644 --- a/apps/web/src/vscodeThemeImport.test.ts +++ b/apps/web/src/vscodeThemeImport.test.ts @@ -112,6 +112,36 @@ describe("VS Code theme import", () => { ); }); + it("moves reserved VS Code names to a non-reserved id", () => { + const darkPlus = parseVsCodeThemeFile({ + name: "Dark+", + type: "dark", + colors: { "editor.background": "#1e1e1e" }, + }); + expect(darkPlus.label).toBe("Dark+"); + expect(darkPlus.id).not.toBe("dark"); + expect(darkPlus.id).toBe("dark-vscode"); + }); + + it("keeps Dark+ and Light+ as separate themes instead of pairing them as +", () => { + const themes = pairVsCodeThemes([ + parseVsCodeThemeFile({ + name: "Light+", + type: "light", + colors: { "editor.background": "#ffffff" }, + }), + parseVsCodeThemeFile({ + name: "Dark+", + type: "dark", + colors: { "editor.background": "#1e1e1e" }, + }), + ]); + expect(themes.map((theme) => [theme.id, theme.label])).toEqual([ + ["light-vscode", "Light+"], + ["dark-vscode", "Dark+"], + ]); + }); + it("fills every role the file omits with a readable derived value", () => { const theme = parseVsCodeThemeFile(VSCODE_DARK); const colors = getThemeColorsForMode(theme, "dark")!; diff --git a/apps/web/src/vscodeThemeImport.ts b/apps/web/src/vscodeThemeImport.ts index 186e03fb6a34d926373045f9020a75bcf3926073..fc0fc2bf8cbc5e7e1326d52b45910c0adbf18c48 100644 --- a/apps/web/src/vscodeThemeImport.ts +++ b/apps/web/src/vscodeThemeImport.ts @@ -1,8 +1,10 @@ import { createVividThemeColors, getThemeModes, + isReservedThemeId, parseThemeFile, themeColorToHex, + themeIdFromName, THEME_FILE_VERSION, type ThemeAppearance, type ThemeColorRole, @@ -321,9 +323,12 @@ export function parseVsCodeThemeFile(value: unknown): ThemeDefinition { // Reuse the theme-file parser so ids, names, and color values go through the // same validation as a hand-written file. + const name = resolveName(value); + const generatedId = themeIdFromName(name); return parseThemeFile({ version: THEME_FILE_VERSION, - name: resolveName(value), + ...(isReservedThemeId(generatedId) ? { id: `${generatedId}-vscode` } : {}), + name, appearance, colors: { ...derived, ...overrides }, }); @@ -352,9 +357,11 @@ export function pairVsCodeThemes( const passthrough: Array<{ theme: ThemeDefinition; order: number }> = []; themes.forEach((theme, order) => { // Only single-appearance themes with an appearance word in the name can - // pair; anything else is already what the user asked for. + // pair, and only when the rest of the name can still identify the pair + // ("Dark+" and "Light+" would pair as "+"); anything else is already + // what the user asked for. const key = stripAppearance(theme.label); - if (getThemeModes(theme).length !== 1 || key === theme.label || key.length === 0) { + if (getThemeModes(theme).length !== 1 || key === theme.label || !/[a-z0-9]/i.test(key)) { passthrough.push({ theme, order }); return; }