diff --git a/src/vs/base/browser/cssValue.ts b/src/vs/base/browser/cssValue.ts index 975edb7cfa4..f099ead8ce0 100644 --- a/src/vs/base/browser/cssValue.ts +++ b/src/vs/base/browser/cssValue.ts @@ -27,10 +27,26 @@ export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt return dflt; } -export function value(value: string): CssFragment { +export function sizeValue(value: string): CssFragment { + const out = value.replaceAll(/[^\w.%+-]/gi, ''); + if (out !== value) { + console.warn(`CSS size ${value} modified to ${out} to be safe for CSS`); + } + return asFragment(out); +} + +export function hexColorValue(value: string): CssFragment { + const out = value.replaceAll(/[^[0-9a-fA-F#]]/gi, ''); + if (out !== value) { + console.warn(`CSS hex color ${value} modified to ${out} to be safe for CSS`); + } + return asFragment(out); +} + +export function identValue(value: string): CssFragment { const out = value.replaceAll(/[^_\-a-z0-9]/gi, ''); if (out !== value) { - console.warn(`CSS value ${value} modified to ${out} to be safe for CSS`); + console.warn(`CSS ident value ${value} modified to ${out} to be safe for CSS`); } return asFragment(out); } diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 61fd63dc827..cb60d8b7bf5 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -35,13 +35,13 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } else { const match = resource.path.match(fileIconDirectoryRegex); if (match) { - name = cssEscape(match[2].toLowerCase()); + name = fileIconSelectorEscape(match[2].toLowerCase()); if (match[1]) { - classes.push(`${cssEscape(match[1].toLowerCase())}-name-dir-icon`); // parent directory + classes.push(`${fileIconSelectorEscape(match[1].toLowerCase())}-name-dir-icon`); // parent directory } } else { - name = cssEscape(resource.authority.toLowerCase()); + name = fileIconSelectorEscape(resource.authority.toLowerCase()); } } @@ -77,7 +77,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // Detected Mode const detectedLanguageId = detectLanguageId(modelService, languageService, resource); if (detectedLanguageId) { - classes.push(`${cssEscape(detectedLanguageId)}-lang-file-icon`); + classes.push(`${fileIconSelectorEscape(detectedLanguageId)}-lang-file-icon`); } } } @@ -85,7 +85,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } export function getIconClassesForLanguageId(languageId: string): string[] { - return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; + return ['file-icon', `${fileIconSelectorEscape(languageId)}-lang-file-icon`]; } function detectLanguageId(modelService: IModelService, languageService: ILanguageService, resource: uri): string | null { @@ -122,6 +122,6 @@ function detectLanguageId(modelService: IModelService, languageService: ILanguag return languageService.guessLanguageIdByFilepathOrFirstLine(resource); } -function cssEscape(str: string): string { +export function fileIconSelectorEscape(str: string): string { return str.replace(/[\s]/g, '/'); // HTML class names can not contain certain whitespace characters (https://dom.spec.whatwg.org/#interface-domtokenlist), use / instead, which doesn't exist in file names. } diff --git a/src/vs/platform/theme/browser/iconsStyleSheet.ts b/src/vs/platform/theme/browser/iconsStyleSheet.ts index 42c2d3b3bab..7215f066d45 100644 --- a/src/vs/platform/theme/browser/iconsStyleSheet.ts +++ b/src/vs/platform/theme/browser/iconsStyleSheet.ts @@ -58,8 +58,8 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc for (const id in usedFontIds) { const definition = usedFontIds[id]; - const fontWeight = definition.weight ? css.inline`font-weight: ${css.value(definition.weight)};` : css.inline``; - const fontStyle = definition.style ? css.inline`font-style: ${css.value(definition.style)};` : css.inline``; + const fontWeight = definition.weight ? css.inline`font-weight: ${css.identValue(definition.weight)};` : css.inline``; + const fontStyle = definition.style ? css.inline`font-style: ${css.identValue(definition.style)};` : css.inline``; const src = new css.Builder(); for (const l of definition.src) { diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index 279b30ba113..5dee6ce44d6 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -26,15 +26,15 @@ export const Extensions = { export type IconDefaults = ThemeIcon | IconDefinition; export interface IconDefinition { - font?: IconFontContribution; // undefined for the default font (codicon) - fontCharacter: string; + readonly font?: IconFontContribution; // undefined for the default font (codicon) + readonly fontCharacter: string; } export interface IconContribution { readonly id: string; description: string | undefined; - deprecationMessage?: string; + readonly deprecationMessage?: string; readonly defaults: IconDefaults; } diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 4aefe5336d2..79bebd3ad1c 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -10,11 +10,12 @@ import * as resources from '../../../../base/common/resources.js'; import * as Json from '../../../../base/common/json.js'; import { ExtensionData, IThemeExtensionPoint, IWorkbenchFileIconTheme } from '../common/workbenchThemeService.js'; import { getParseErrorMessage } from '../../../../base/common/jsonErrorMessages.js'; -import { asCSSUrl } from '../../../../base/browser/cssValue.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionResourceLoaderService } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { mainWindow } from '../../../../base/browser/window.js'; +import { fontCharacterRegex, fontColorRegex, fontSizeRegex } from '../common/productIconThemeSchema.js'; +import * as css from '../../../../base/browser/cssValue.js'; +import { fileIconSelectorEscape } from '../../../../editor/common/services/getIconClasses.js'; export class FileIconThemeData implements IWorkbenchFileIconTheme { @@ -236,7 +237,7 @@ export class FileIconThemeLoader { if (!iconThemeDocument.iconDefinitions) { return result; } - const selectorByDefinitionId: { [def: string]: string[] } = {}; + const selectorByDefinitionId: { [def: string]: css.Builder } = {}; const coveredLanguages: { [languageId: string]: boolean } = {}; const iconThemeDocumentLocationDirname = resources.dirname(iconThemeDocumentLocation); @@ -244,32 +245,32 @@ export class FileIconThemeLoader { return resources.joinPath(iconThemeDocumentLocationDirname, path); } - function collectSelectors(associations: IconsAssociation | undefined, baseThemeClassName?: string) { - function addSelector(selector: string, defId: string) { + function collectSelectors(associations: IconsAssociation | undefined, baseThemeClassName?: css.CssFragment) { + function addSelector(selector: css.CssFragment, defId: string) { if (defId) { let list = selectorByDefinitionId[defId]; if (!list) { - list = selectorByDefinitionId[defId] = []; + list = selectorByDefinitionId[defId] = new css.Builder(); } list.push(selector); } } if (associations) { - let qualifier = '.show-file-icons'; + let qualifier = css.inline`.show-file-icons`; if (baseThemeClassName) { - qualifier = baseThemeClassName + ' ' + qualifier; + qualifier = css.inline`${baseThemeClassName} ${qualifier}`; } - const expanded = '.monaco-tl-twistie.collapsible:not(.collapsed) + .monaco-tl-contents'; + const expanded = css.inline`.monaco-tl-twistie.collapsible:not(.collapsed) + .monaco-tl-contents`; if (associations.folder) { - addSelector(`${qualifier} .folder-icon::before`, associations.folder); + addSelector(css.inline`${qualifier} .folder-icon::before`, associations.folder); result.hasFolderIcons = true; } if (associations.folderExpanded) { - addSelector(`${qualifier} ${expanded} .folder-icon::before`, associations.folderExpanded); + addSelector(css.inline`${qualifier} ${expanded} .folder-icon::before`, associations.folderExpanded); result.hasFolderIcons = true; } @@ -277,37 +278,37 @@ export class FileIconThemeLoader { const rootFolderExpanded = associations.rootFolderExpanded || associations.folderExpanded; if (rootFolder) { - addSelector(`${qualifier} .rootfolder-icon::before`, rootFolder); + addSelector(css.inline`${qualifier} .rootfolder-icon::before`, rootFolder); result.hasFolderIcons = true; } if (rootFolderExpanded) { - addSelector(`${qualifier} ${expanded} .rootfolder-icon::before`, rootFolderExpanded); + addSelector(css.inline`${qualifier} ${expanded} .rootfolder-icon::before`, rootFolderExpanded); result.hasFolderIcons = true; } if (associations.file) { - addSelector(`${qualifier} .file-icon::before`, associations.file); + addSelector(css.inline`${qualifier} .file-icon::before`, associations.file); result.hasFileIcons = true; } const folderNames = associations.folderNames; if (folderNames) { for (const key in folderNames) { - const selectors: string[] = []; + const selectors = new css.Builder(); const name = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(`.${escapeCSS(name)}-name-folder-icon`); - addSelector(`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]); + selectors.push(css.inline`.${classSelectorPart(name)}-name-folder-icon`); + addSelector(css.inline`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]); result.hasFolderIcons = true; } } const folderNamesExpanded = associations.folderNamesExpanded; if (folderNamesExpanded) { for (const key in folderNamesExpanded) { - const selectors: string[] = []; + const selectors = new css.Builder(); const name = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(`.${escapeCSS(name)}-name-folder-icon`); - addSelector(`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]); + selectors.push(css.inline`.${classSelectorPart(name)}-name-folder-icon`); + addSelector(css.inline`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]); result.hasFolderIcons = true; } } @@ -316,7 +317,7 @@ export class FileIconThemeLoader { if (rootFolderNames) { for (const key in rootFolderNames) { const name = key.toLowerCase(); - addSelector(`${qualifier} .${escapeCSS(name)}-root-name-folder-icon.rootfolder-icon::before`, rootFolderNames[key]); + addSelector(css.inline`${qualifier} .${classSelectorPart(name)}-root-name-folder-icon.rootfolder-icon::before`, rootFolderNames[key]); result.hasFolderIcons = true; } } @@ -324,7 +325,7 @@ export class FileIconThemeLoader { if (rootFolderNamesExpanded) { for (const key in rootFolderNamesExpanded) { const name = key.toLowerCase(); - addSelector(`${qualifier} ${expanded} .${escapeCSS(name)}-root-name-folder-icon.rootfolder-icon::before`, rootFolderNamesExpanded[key]); + addSelector(css.inline`${qualifier} ${expanded} .${classSelectorPart(name)}-root-name-folder-icon.rootfolder-icon::before`, rootFolderNamesExpanded[key]); result.hasFolderIcons = true; } } @@ -335,7 +336,7 @@ export class FileIconThemeLoader { languageIds.jsonc = languageIds.json; } for (const languageId in languageIds) { - addSelector(`${qualifier} .${escapeCSS(languageId)}-lang-file-icon.file-icon::before`, languageIds[languageId]); + addSelector(css.inline`${qualifier} .${classSelectorPart(languageId)}-lang-file-icon.file-icon::before`, languageIds[languageId]); result.hasFileIcons = true; hasSpecificFileIcons = true; coveredLanguages[languageId] = true; @@ -344,16 +345,16 @@ export class FileIconThemeLoader { const fileExtensions = associations.fileExtensions; if (fileExtensions) { for (const key in fileExtensions) { - const selectors: string[] = []; + const selectors = new css.Builder(); const name = handleParentFolder(key.toLowerCase(), selectors); const segments = name.split('.'); if (segments.length) { for (let i = 0; i < segments.length; i++) { - selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`); + selectors.push(css.inline`.${classSelectorPart(segments.slice(i).join('.'))}-ext-file-icon`); } - selectors.push('.ext-file-icon'); // extra segment to increase file-ext score + selectors.push(css.inline`.ext-file-icon`); // extra segment to increase file-ext score } - addSelector(`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[key]); + addSelector(css.inline`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[key]); result.hasFileIcons = true; hasSpecificFileIcons = true; } @@ -361,18 +362,18 @@ export class FileIconThemeLoader { const fileNames = associations.fileNames; if (fileNames) { for (const key in fileNames) { - const selectors: string[] = []; + const selectors = new css.Builder(); const fileName = handleParentFolder(key.toLowerCase(), selectors); - selectors.push(`.${escapeCSS(fileName)}-name-file-icon`); - selectors.push('.name-file-icon'); // extra segment to increase file-name score + selectors.push(css.inline`.${classSelectorPart(fileName)}-name-file-icon`); + selectors.push(css.inline`.name-file-icon`); // extra segment to increase file-name score const segments = fileName.split('.'); if (segments.length) { for (let i = 1; i < segments.length; i++) { - selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`); + selectors.push(css.inline`.${classSelectorPart(segments.slice(i).join('.'))}-ext-file-icon`); } - selectors.push('.ext-file-icon'); // extra segment to increase file-ext score + selectors.push(css.inline`.ext-file-icon`); // extra segment to increase file-ext score } - addSelector(`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[key]); + addSelector(css.inline`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[key]); result.hasFileIcons = true; hasSpecificFileIcons = true; } @@ -380,9 +381,9 @@ export class FileIconThemeLoader { } } collectSelectors(iconThemeDocument); - collectSelectors(iconThemeDocument.light, '.vs'); - collectSelectors(iconThemeDocument.highContrast, '.hc-black'); - collectSelectors(iconThemeDocument.highContrast, '.hc-light'); + collectSelectors(iconThemeDocument.light, css.inline`.vs`); + collectSelectors(iconThemeDocument.highContrast, css.inline`.hc-black`); + collectSelectors(iconThemeDocument.highContrast, css.inline`.hc-light`); if (!result.hasFileIcons && !result.hasFolderIcons) { return result; @@ -390,52 +391,53 @@ export class FileIconThemeLoader { const showLanguageModeIcons = iconThemeDocument.showLanguageModeIcons === true || (hasSpecificFileIcons && iconThemeDocument.showLanguageModeIcons !== false); - const cssRules: string[] = []; + const cssRules = new css.Builder(); const fonts = iconThemeDocument.fonts; const fontSizes = new Map(); if (Array.isArray(fonts)) { const defaultFontSize = this.tryNormalizeFontSize(fonts[0].size) || '150%'; fonts.forEach(font => { - const src = font.src.map(l => `${asCSSUrl(resolvePath(l.path))} format('${l.format}')`).join(', '); - cssRules.push(`@font-face { src: ${src}; font-family: '${font.id}'; font-weight: ${font.weight}; font-style: ${font.style}; font-display: block; }`); + const fontSrcs = new css.Builder(); + fontSrcs.push(...font.src.map(l => css.inline`${css.asCSSUrl(resolvePath(l.path))} format(${css.stringValue(l.format)})`)); + cssRules.push(css.inline`@font-face { src: ${fontSrcs.join(', ')}; font-family: ${css.stringValue(font.id)}; font-weight: ${css.identValue(font.weight)}; font-style: ${css.identValue(font.style)}; font-display: block; }`); const fontSize = this.tryNormalizeFontSize(font.size); if (fontSize !== undefined && fontSize !== defaultFontSize) { fontSizes.set(font.id, fontSize); } }); - cssRules.push(`.show-file-icons .file-icon::before, .show-file-icons .folder-icon::before, .show-file-icons .rootfolder-icon::before { font-family: '${fonts[0].id}'; font-size: ${defaultFontSize}; }`); + cssRules.push(css.inline`.show-file-icons .file-icon::before, .show-file-icons .folder-icon::before, .show-file-icons .rootfolder-icon::before { font-family: ${css.stringValue(fonts[0].id)}; font-size: ${css.identValue(defaultFontSize)}; }`); } // Use emQuads to prevent the icon from collapsing to zero height for image icons - const emQuad = '\\2001'; + const emQuad = css.stringValue('\\2001'); for (const defId in selectorByDefinitionId) { const selectors = selectorByDefinitionId[defId]; const definition = iconThemeDocument.iconDefinitions[defId]; if (definition) { if (definition.iconPath) { - cssRules.push(`${selectors.join(', ')} { content: '${emQuad}'; background-image: ${asCSSUrl(resolvePath(definition.iconPath))}; }`); + cssRules.push(css.inline`${selectors.join(', ')} { content: ${emQuad}; background-image: ${css.asCSSUrl(resolvePath(definition.iconPath))}; }`); } else if (definition.fontCharacter || definition.fontColor) { - const body = []; - if (definition.fontColor) { - body.push(`color: ${definition.fontColor};`); + const body = new css.Builder(); + if (definition.fontColor && definition.fontColor.match(fontColorRegex)) { + body.push(css.inline`color: ${css.hexColorValue(definition.fontColor)};`); } - if (definition.fontCharacter) { - body.push(`content: '${definition.fontCharacter}';`); + if (definition.fontCharacter && definition.fontCharacter.match(fontCharacterRegex)) { + body.push(css.inline`content: ${css.stringValue(definition.fontCharacter)};`); } const fontSize = definition.fontSize ?? (definition.fontId ? fontSizes.get(definition.fontId) : undefined); - if (fontSize) { - body.push(`font-size: ${fontSize};`); + if (fontSize && fontSize.match(fontSizeRegex)) { + body.push(css.inline`font-size: ${css.sizeValue(fontSize)};`); } if (definition.fontId) { - body.push(`font-family: ${definition.fontId};`); + body.push(css.inline`font-family: ${css.stringValue(definition.fontId)};`); } if (showLanguageModeIcons) { - body.push(`background-image: unset;`); // potentially set by the language default + body.push(css.inline`background-image: unset;`); // potentially set by the language default } - cssRules.push(`${selectors.join(', ')} { ${body.join(' ')} }`); + cssRules.push(css.inline`${selectors.join(', ')} { ${body.join(' ')} }`); } } } @@ -445,9 +447,9 @@ export class FileIconThemeLoader { if (!coveredLanguages[languageId]) { const icon = this.languageService.getIcon(languageId); if (icon) { - const selector = `.show-file-icons .${escapeCSS(languageId)}-lang-file-icon.file-icon::before`; - cssRules.push(`${selector} { content: '${emQuad}'; background-image: ${asCSSUrl(icon.dark)}; }`); - cssRules.push(`.vs ${selector} { content: '${emQuad}'; background-image: ${asCSSUrl(icon.light)}; }`); + const selector = css.inline`.show-file-icons .${classSelectorPart(languageId)}-lang-file-icon.file-icon::before`; + cssRules.push(css.inline`${selector} { content: ${emQuad}; background-image: ${css.asCSSUrl(icon.dark)}; }`); + cssRules.push(css.inline`.vs ${selector} { content: ${emQuad}; background-image: ${css.asCSSUrl(icon.light)}; }`); } } } @@ -480,17 +482,17 @@ export class FileIconThemeLoader { } } -function handleParentFolder(key: string, selectors: string[]): string { +function handleParentFolder(key: string, selectors: css.Builder): string { const lastIndexOfSlash = key.lastIndexOf('/'); if (lastIndexOfSlash >= 0) { const parentFolder = key.substring(0, lastIndexOfSlash); - selectors.push(`.${escapeCSS(parentFolder)}-name-dir-icon`); + selectors.push(css.inline`.${classSelectorPart(parentFolder)}-name-dir-icon`); return key.substring(lastIndexOfSlash + 1); } return key; } -function escapeCSS(str: string) { - str = str.replace(/[\s]/g, '/'); // HTML class names can not contain certain whitespace characters (https://dom.spec.whatwg.org/#interface-domtokenlist), use / instead, which doesn't exist in file names. - return mainWindow.CSS.escape(str); +function classSelectorPart(str: string): css.CssFragment { + str = fileIconSelectorEscape(str); + return css.className(str); } diff --git a/src/vs/workbench/services/themes/browser/productIconThemeData.ts b/src/vs/workbench/services/themes/browser/productIconThemeData.ts index 0781c91a3a0..8961bfe3b2b 100644 --- a/src/vs/workbench/services/themes/browser/productIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/productIconThemeData.ts @@ -11,7 +11,7 @@ import * as Json from '../../../../base/common/json.js'; import { ExtensionData, IThemeExtensionPoint, IWorkbenchProductIconTheme, ThemeSettingDefaults } from '../common/workbenchThemeService.js'; import { getParseErrorMessage } from '../../../../base/common/jsonErrorMessages.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { fontIdRegex, fontWeightRegex, fontStyleRegex, fontFormatRegex } from '../common/productIconThemeSchema.js'; +import { fontIdRegex, fontWeightRegex, fontStyleRegex, fontFormatRegex, fontCharacterRegex } from '../common/productIconThemeSchema.js'; import { isObject, isString } from '../../../../base/common/types.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IconDefinition, getIconRegistry, IconContribution, IconFontDefinition, IconFontSource } from '../../../../platform/theme/common/iconRegistry.js'; @@ -200,8 +200,8 @@ function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderServ const sanitizedFonts: Map = new Map(); for (const font of contentValue.fonts) { - if (isString(font.id) && font.id.match(fontIdRegex)) { - const fontId = font.id; + const fontId = font.id; + if (isString(fontId) && fontId.match(fontIdRegex)) { let fontWeight = undefined; if (isString(font.weight) && font.weight.match(fontWeightRegex)) { @@ -245,7 +245,7 @@ function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderServ for (const iconId in contentValue.iconDefinitions) { const definition = contentValue.iconDefinitions[iconId]; - if (isString(definition.fontCharacter)) { + if (isString(definition.fontCharacter) && definition.fontCharacter.match(fontCharacterRegex)) { const fontId = definition.fontId ?? primaryFontId; const fontDefinition = sanitizedFonts.get(fontId); if (fontDefinition) { @@ -256,7 +256,7 @@ function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderServ warnings.push(nls.localize('error.icon.font', 'Skipping icon definition \'{0}\'. Unknown font.', iconId)); } } else { - warnings.push(nls.localize('error.icon.fontCharacter', 'Skipping icon definition \'{0}\'. Unknown fontCharacter.', iconId)); + warnings.push(nls.localize('error.icon.fontCharacter', 'Skipping icon definition \'{0}\'. Unknown fontCharacter. Must use a sing; character or a \\ followed by a Unicode code points in hexadecimal.', iconId)); } } return { iconDefinitions }; diff --git a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts index 4d18093f41e..685c3375f00 100644 --- a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts @@ -7,7 +7,7 @@ import * as nls from '../../../../nls.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; -import { fontWeightRegex, fontStyleRegex, fontSizeRegex, fontIdRegex } from './productIconThemeSchema.js'; +import { fontWeightRegex, fontStyleRegex, fontSizeRegex, fontIdRegex, fontCharacterRegex, fontColorRegex } from './productIconThemeSchema.js'; const schemaId = 'vscode://schemas/icon-theme'; const schema: IJSONSchema = { @@ -208,12 +208,15 @@ const schema: IJSONSchema = { }, fontCharacter: { type: 'string', - description: nls.localize('schema.fontCharacter', 'When using a glyph font: The character in the font to use.') + description: nls.localize('schema.fontCharacter', 'When using a glyph font: The character in the font to use.'), + pattern: fontCharacterRegex, + patternErrorMessage: nls.localize('schema.fontCharacter.formatError', 'The fontCharacter must be a single letter or a backslash and followed by unicode code points in hexadecimal.') }, fontColor: { type: 'string', format: 'color-hex', - description: nls.localize('schema.fontColor', 'When using a glyph font: The color to use.') + description: nls.localize('schema.fontColor', 'When using a glyph font: The color to use.'), + pattern: fontColorRegex }, fontSize: { type: 'string', diff --git a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts index 29588b66234..0a58dbcdbbf 100644 --- a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts @@ -11,6 +11,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import * as resources from '../../../../base/common/resources.js'; import { IExtensionDescription } from '../../../../platform/extensions/common/extensions.js'; import { extname, posix } from '../../../../base/common/path.js'; +import { fontCharacterRegex } from './productIconThemeSchema.js'; interface IIconExtensionPoint { [id: string]: { @@ -53,7 +54,9 @@ const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint