import { Autowired, Injectable } from '@opensumi/di'; import { URI, formatLocalize, getDebugLogger, parseWithComments } from '@opensumi/ide-core-browser'; import { StaticResourceService } from '@opensumi/ide-core-browser/lib/static-resource'; import { IFileServiceClient } from '@opensumi/ide-file-service'; import { IIconTheme } from '../common'; @Injectable({ multiple: true }) export class IconThemeData implements IIconTheme { hasFileIcons: boolean; hasFolderIcons: boolean; hidesExplorerArrows: boolean; styleSheetContent: string; @Autowired(IFileServiceClient) private readonly fileServiceClient: IFileServiceClient; @Autowired() private readonly staticResourceService: StaticResourceService; async load(location: URI) { const ret = await this.fileServiceClient.readFile(location.toString()); let contentValue = {} as IconThemeDocument; try { contentValue = parseWithComments(ret.content.toString()); } catch (error) { getDebugLogger().log(formatLocalize('error.cannotparseicontheme', location.codeUri.fsPath)); } const result = processIconThemeDocument(location, contentValue, this.staticResourceService); this.hasFileIcons = result.hasFileIcons; this.hasFolderIcons = result.hasFolderIcons; this.hidesExplorerArrows = result.hidesExplorerArrows; this.styleSheetContent = result.content; return result.content; } } interface IconDefinition { iconPath: string; fontColor: string; fontCharacter: string; fontSize: string; fontId: string; } interface FontDefinition { id: string; weight: string; style: string; size: string; src: { path: string; format: string }[]; } // 图标名与图标定义的映射关系 interface IconsAssociation { folder?: string; file?: string; folderExpanded?: string; rootFolder?: string; rootFolderExpanded?: string; folderNames?: { [folderName: string]: string }; folderNamesExpanded?: { [folderName: string]: string }; fileExtensions?: { [extension: string]: string }; fileNames?: { [fileName: string]: string }; languageIds?: { [languageId: string]: string }; } export interface IconThemeDocument extends IconsAssociation { iconDefinitions: { [key: string]: IconDefinition }; fonts: FontDefinition[]; light?: IconsAssociation; highContrast?: IconsAssociation; hidesExplorerArrows?: boolean; } /** * 将图标配置信息转成 CSS * @param iconThemeDocumentLocation * @param iconThemeDocument */ function processIconThemeDocument( iconThemeDocumentLocation: URI, iconThemeDocument: IconThemeDocument, staticResourceService: StaticResourceService, ): { content: string; hasFileIcons: boolean; hasFolderIcons: boolean; hidesExplorerArrows: boolean } { const result = { content: '', hasFileIcons: false, hasFolderIcons: false, hidesExplorerArrows: !!iconThemeDocument.hidesExplorerArrows, }; if (!iconThemeDocument.iconDefinitions) { return result; } const selectorByDefinitionId: { [def: string]: string[] } = {}; function resolvePath(path: string) { const targetPath = iconThemeDocumentLocation.path.dir.join(path.replace(/^\.\//, '')); const targetLocation = iconThemeDocumentLocation.withPath(targetPath); if (iconThemeDocumentLocation.scheme === 'https') { return targetLocation.toString(); } // file 协议的需要走 static-resource return staticResourceService.resolveStaticResource(targetLocation); } /** * 将图标关联关系转成css选择器数组,支持主题的light、hc传入 * @param associations * @param baseThemeClassName */ function collectSelectors(associations: IconsAssociation | undefined, baseThemeClassName?: string) { // 将定义的图标名插入对应的选择器列表 function addSelector(selector: string, defId: string) { if (defId) { let list = selectorByDefinitionId[defId]; if (!list) { list = selectorByDefinitionId[defId] = []; } list.push(selector); } } if (associations) { let qualifier = '.show-file-icons'; if (baseThemeClassName) { qualifier = baseThemeClassName + ' ' + qualifier; } if (associations.folder) { addSelector(`${qualifier} .folder-icon::before`, associations.folder); result.hasFolderIcons = true; } if (associations.folderExpanded) { addSelector(`${qualifier} .folder-icon.expanded::before`, associations.folderExpanded); result.hasFolderIcons = true; } const rootFolder = associations.rootFolder || associations.folder; const rootFolderExpanded = associations.rootFolderExpanded || associations.folderExpanded; if (rootFolder) { addSelector(`${qualifier} .rootfolder-icon::before`, rootFolder); result.hasFolderIcons = true; } if (rootFolderExpanded) { addSelector(`${qualifier} .rootfolder-icon.expanded::before`, rootFolderExpanded); result.hasFolderIcons = true; } if (associations.file) { addSelector(`${qualifier} .file-icon::before`, associations.file); result.hasFileIcons = true; } const folderNames = associations.folderNames; if (folderNames) { // eslint-disable-next-line guard-for-in for (const folderName in folderNames) { addSelector( `${qualifier} .${escapeCSS(folderName.toLowerCase())}-name-folder-icon.folder-icon::before`, folderNames[folderName], ); result.hasFolderIcons = true; } } const folderNamesExpanded = associations.folderNamesExpanded; if (folderNamesExpanded) { // eslint-disable-next-line guard-for-in for (const folderName in folderNamesExpanded) { addSelector( `${qualifier} .${escapeCSS(folderName.toLowerCase())}-name-folder-icon.folder-icon.expanded::before`, folderNamesExpanded[folderName], ); result.hasFolderIcons = true; } } const languageIds = associations.languageIds; if (languageIds) { if (!languageIds.jsonc && languageIds.json) { languageIds.jsonc = languageIds.json; } // eslint-disable-next-line guard-for-in for (const languageId in languageIds) { addSelector( `${qualifier} .${escapeCSS(languageId)}-lang-file-icon.file-icon::before`, languageIds[languageId], ); result.hasFileIcons = true; } } const fileExtensions = associations.fileExtensions; if (fileExtensions) { // eslint-disable-next-line guard-for-in for (const fileExtension in fileExtensions) { const selectors: string[] = []; const segments = fileExtension.toLowerCase().split('.'); if (segments.length) { for (let i = 0; i < segments.length; i++) { selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`); } selectors.push('.ext-file-icon'); // extra segment to increase file-ext score } addSelector(`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[fileExtension]); result.hasFileIcons = true; } } const fileNames = associations.fileNames; if (fileNames) { // eslint-disable-next-line guard-for-in for (let fileName in fileNames) { const selectors: string[] = []; fileName = fileName.toLowerCase(); selectors.push(`.${escapeCSS(fileName)}-name-file-icon`); 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('.ext-file-icon'); // extra segment to increase file-ext score } addSelector(`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[fileName]); result.hasFileIcons = true; } } } } collectSelectors(iconThemeDocument); collectSelectors(iconThemeDocument.light, '.vs'); collectSelectors(iconThemeDocument.highContrast, '.hc-black'); collectSelectors(iconThemeDocument.highContrast, '.hc-light'); if (!result.hasFileIcons && !result.hasFolderIcons) { return result; } const cssRules: string[] = []; const fonts = iconThemeDocument.fonts; if (Array.isArray(fonts)) { fonts.forEach((font) => { const src = font.src.map((l) => `url('${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}; }`, ); }); 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: ${fonts[0].size || '150%'}}`, ); } // eslint-disable-next-line guard-for-in for (const defId in selectorByDefinitionId) { const selectors = selectorByDefinitionId[defId]; const definition = iconThemeDocument.iconDefinitions[defId]; if (definition) { if (definition.iconPath) { cssRules.push( `${selectors.join(', ')} { content: ' '; background-image: url("${resolvePath(definition.iconPath)}"); }`, ); } if (definition.fontCharacter || definition.fontColor) { let body = ''; if (definition.fontColor) { body += ` color: ${definition.fontColor};`; } if (definition.fontCharacter) { body += ` content: '${definition.fontCharacter}';`; } if (definition.fontSize) { body += ` font-size: ${definition.fontSize};`; } if (definition.fontId) { body += ` font-family: ${definition.fontId};`; } cssRules.push(`${selectors.join(', ')} { ${body} }`); } } } result.content = cssRules.join('\n'); return result; } function escapeCSS(str: string) { str = str.replace(/[\x09\x0a\x0c\x0d\x20]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. return window.CSS.escape(str); }