Allow to contribute to icon registry from icon contribution point. Fixes #114942

This commit is contained in:
Martin Aeschlimann 2021-02-04 15:16:42 +01:00
parent 082af46e66
commit 684f61b456
7 changed files with 364 additions and 35 deletions

View file

@ -1230,6 +1230,10 @@ export function asCSSUrl(uri: URI): string {
return `url('${FileAccess.asBrowserUri(uri).toString(true).replace(/'/g, '%27')}')`;
}
export function asCSSPropertyValue(value: string) {
return `'${value.replace(/'/g, '%27')}'`;
}
export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void {
// If the data is provided as Buffer, we create a

View file

@ -16,7 +16,7 @@ import { ColorIdentifier, Extensions, IColorRegistry } from 'vs/platform/theme/c
import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IThemingRegistry, ITokenStyle } from 'vs/platform/theme/common/themeService';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ColorScheme } from 'vs/platform/theme/common/theme';
import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry';
import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet';
const VS_THEME_NAME = 'vs';
const VS_DARK_THEME_NAME = 'vs-dark';
@ -214,9 +214,9 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon
this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME));
this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME));
const iconRegistry = getIconRegistry();
const iconsStyleSheet = getIconsStyleSheet();
this._codiconCSS = iconRegistry.getCSS();
this._codiconCSS = iconsStyleSheet.getCSS();
this._themeCSS = '';
this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
this._globalStyleElement = null;
@ -224,8 +224,8 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon
this._colorMapOverride = null;
this.setTheme(VS_THEME_NAME);
iconRegistry.onDidChange(() => {
this._codiconCSS = iconRegistry.getCSS();
iconsStyleSheet.onDidChange(() => {
this._codiconCSS = iconsStyleSheet.getCSS();
this._updateCSS();
});
}

View file

@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { getIconRegistry, IconContribution, IconFontContribution } from 'vs/platform/theme/common/iconRegistry';
import { asCSSPropertyValue, asCSSUrl } from 'vs/base/browser/dom';
import { Event, Emitter } from 'vs/base/common/event';
export interface IIconsStyleSheet {
getCSS(): string;
readonly onDidChange: Event<void>;
}
export function getIconsStyleSheet(): IIconsStyleSheet {
const onDidChangeEmmiter = new Emitter<void>();
const iconRegistry = getIconRegistry();
iconRegistry.onDidChange(() => onDidChangeEmmiter.fire());
return {
onDidChange: onDidChangeEmmiter.event,
getCSS() {
const usedFontIds: { [id: string]: IconFontContribution } = {};
const formatIconRule = (contribution: IconContribution): string | undefined => {
let definition = contribution.defaults;
while (ThemeIcon.isThemeIcon(definition)) {
const c = iconRegistry.getIcon(definition.id);
if (!c) {
return undefined;
}
definition = c.defaults;
}
const fontId = definition.fontId;
if (fontId) {
const fontContribution = iconRegistry.getIconFont(fontId);
if (fontContribution) {
usedFontIds[fontId] = fontContribution;
return `.codicon-${contribution.id}:before { content: '${definition.character}'; font-family: ${asCSSPropertyValue(fontId)}; }`;
}
}
return `.codicon-${contribution.id}:before { content: '${definition.character}'; }`;
};
const rules = [];
for (let contribution of iconRegistry.getIcons()) {
const rule = formatIconRule(contribution);
if (rule) {
rules.push(rule);
}
}
for (let id in usedFontIds) {
const fontContribution = usedFontIds[id];
const src = fontContribution.definition.src.map(l => `${asCSSUrl(l.location)} format('${l.format}')`).join(', ');
rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(id)}; }`);
}
return rules.join('\n');
}
};
}

View file

@ -11,11 +11,11 @@ import { localize } from 'vs/nls';
import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { RunOnceScheduler } from 'vs/base/common/async';
import * as Codicons from 'vs/base/common/codicons';
import { URI } from 'vs/base/common/uri';
// ------ API types
// color registry
// icon registry
export const Extensions = {
IconContribution: 'base.contributions.icons'
};
@ -34,6 +34,15 @@ export interface IconContribution {
defaults: IconDefaults;
}
export interface IconFontContribution {
id: string;
definition: IconFontDefinition;
}
export interface IconFontDefinition {
src: { location: URI, format: string; }[]
}
export interface IIconRegistry {
readonly onDidChange: Event<void>;
@ -42,12 +51,12 @@ export interface IIconRegistry {
* Register a icon to the registry.
* @param id The icon id
* @param defaults The default values
* @description the description
* @param description The description
*/
registerIcon(id: string, defaults: IconDefaults, description?: string): ThemeIcon;
/**
* Register a icon to the registry.
* Deregister a icon from the registry.
*/
deregisterIcon(id: string): void;
@ -62,7 +71,7 @@ export interface IIconRegistry {
getIcon(id: string): IconContribution | undefined;
/**
* JSON schema for an object to assign icon values to one of the color contributions.
* JSON schema for an object to assign icon values to one of the icon contributions.
*/
getIconSchema(): IJSONSchema;
@ -72,10 +81,26 @@ export interface IIconRegistry {
getIconReferenceSchema(): IJSONSchema;
/**
* The CSS for all icons
* Register a icon font to the registry.
* @param id The icon font id
* @param definition The iocn font definition
*/
getCSS(): string;
registerIconFont(id: string, definition: IconFontDefinition): IconFontContribution;
/**
* Deregister an icon font to the registry.
*/
deregisterIconFont(id: string): void;
/**
* Get all icon font contributions
*/
getIconFonts(): IconFontContribution[];
/**
* Get the icon font for the given id
*/
getIconFont(id: string): IconFontContribution | undefined;
}
class IconRegistry implements IIconRegistry {
@ -99,10 +124,13 @@ class IconRegistry implements IIconRegistry {
type: 'object',
properties: {}
};
private iconReferenceSchema: IJSONSchema & { enum: string[], enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] };
private iconReferenceSchema: IJSONSchema & { enum: string[], enumDescriptions: string[] } = { type: 'string', pattern: `^${Codicons.CSSIcon.iconNameExpression}$`, enum: [], enumDescriptions: [] };
private iconFontsById: { [key: string]: IconFontContribution };
constructor() {
this.iconsById = {};
this.iconFontsById = {};
}
public registerIcon(id: string, defaults: IconDefaults, description?: string, deprecationMessage?: string): ThemeIcon {
@ -164,27 +192,27 @@ class IconRegistry implements IIconRegistry {
return this.iconReferenceSchema;
}
public getCSS() {
const rules = [];
for (let id in this.iconsById) {
const rule = this.formatRule(id);
if (rule) {
rules.push(rule);
}
public registerIconFont(id: string, definition: IconFontDefinition): IconFontContribution {
const existing = this.iconFontsById[id];
if (existing) {
return existing;
}
return rules.join('\n');
let iconFontContribution: IconFontContribution = { id, definition };
this.iconFontsById[id] = iconFontContribution;
this._onDidChange.fire();
return iconFontContribution;
}
private formatRule(id: string): string | undefined {
let definition = this.iconsById[id].defaults;
while (ThemeIcon.isThemeIcon(definition)) {
const c = this.iconsById[definition.id];
if (!c) {
return undefined;
}
definition = c.defaults;
}
return `.codicon-${id}:before { content: '${definition.character}'; }`;
public deregisterIconFont(id: string): void {
delete this.iconFontsById[id];
}
public getIconFonts(): IconFontContribution[] {
return Object.keys(this.iconFontsById).map(id => this.iconFontsById[id]);
}
public getIconFont(id: string): IconFontContribution | undefined {
return this.iconFontsById[id];
}
public toString() {

View file

@ -11,6 +11,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
// --- other interested parties
import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValidationExtensionPoint';
import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint';
import { IconExtensionPoint, IconFontExtensionPoint } from 'vs/workbench/services/themes/common/iconExtensionPoint';
import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes/common/tokenClassificationExtensionPoint';
import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint';
@ -79,6 +80,8 @@ export class ExtensionPoints implements IWorkbenchContribution {
// Classes that handle extension points...
this.instantiationService.createInstance(JSONValidationExtensionPoint);
this.instantiationService.createInstance(ColorExtensionPoint);
this.instantiationService.createInstance(IconExtensionPoint);
this.instantiationService.createInstance(IconFontExtensionPoint);
this.instantiationService.createInstance(TokenClassificationExtensionPoints);
this.instantiationService.createInstance(LanguageConfigurationFileHandler);
}

View file

@ -38,7 +38,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme';
import { IHostColorSchemeService } from 'vs/workbench/services/themes/common/hostColorSchemeService';
import { RunOnceScheduler, Sequencer } from 'vs/base/common/async';
import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit';
import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry';
import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet';
// implementation
@ -183,13 +183,13 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
const codiconStyleSheet = createStyleSheet();
codiconStyleSheet.id = 'codiconStyles';
const iconRegistry = getIconRegistry();
const iconsStyleSheet = getIconsStyleSheet();
function updateAll() {
codiconStyleSheet.textContent = iconRegistry.getCSS();
codiconStyleSheet.textContent = iconsStyleSheet.getCSS();
}
const delayer = new RunOnceScheduler(updateAll, 0);
iconRegistry.onDidChange(() => delayer.schedule());
iconsStyleSheet.onDidChange(() => delayer.schedule());
delayer.schedule();
}

View file

@ -0,0 +1,233 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IIconRegistry, Extensions as IconRegistryExtensions, IconFontDefinition } from 'vs/platform/theme/common/iconRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { CSSIcon } from 'vs/base/common/codicons';
import { fontIdRegex } from 'vs/workbench/services/themes/common/productIconThemeSchema';
import * as resources from 'vs/base/common/resources';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
interface IIconExtensionPoint {
id: string;
description: string;
default: { iconFontId: string; character: string; } | string;
}
interface IIconFontExtensionPoint {
id: string;
src: {
path: string;
format: string;
}[];
}
const iconRegistry: IIconRegistry = Registry.as<IIconRegistry>(IconRegistryExtensions.IconContribution);
const iconReferenceSchema = iconRegistry.getIconReferenceSchema();
const iconIdPattern = `^${CSSIcon.iconNameExpression}$`;
const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IIconExtensionPoint[]>({
extensionPoint: 'icons',
jsonSchema: {
description: nls.localize('contributes.icons', 'Contributes extension defined themable icons'),
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
description: nls.localize('contributes.icon.id', 'The identifier of the themable icon'),
pattern: iconIdPattern,
patternErrorMessage: nls.localize('contributes.icon.id.format', 'Identifiers must only contain letters, digits and minus.'),
},
description: {
type: 'string',
description: nls.localize('contributes.icon.description', 'The description of the themable icon'),
},
default: {
anyOf: [
iconReferenceSchema,
{
type: 'object',
properties: {
iconFontId: {
description: nls.localize('contributes.default.fontId', 'The id of the icon font that defines the default icon.'),
type: 'string'
},
character: {
description: nls.localize('contributes.default.character', 'The character in the icon font.'),
type: 'string'
}
}
}
],
description: nls.localize('contributes.default', 'The default of the icon. Either a reference to a '),
}
}
}
}
});
const iconFontConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IIconFontExtensionPoint[]>({
extensionPoint: 'iconFonts',
jsonSchema: {
description: nls.localize('contributes.iconFonts', 'Contributes icons fonts to be used by contributed icons'),
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
description: nls.localize('contributes.iconFonts.id', 'The ID of the font.'),
pattern: fontIdRegex,
patternErrorMessage: nls.localize('contributes.iconFonts.id.formatError', 'The ID must only contain letters, numbers, underscore and minus.')
},
src: {
type: 'array',
description: nls.localize('contributes.iconFonts.src', 'The location of the font.'),
items: {
type: 'object',
properties: {
path: {
type: 'string',
description: nls.localize('contributes.iconFonts.font-path', 'The font path, relative to the current product icon theme file.'),
},
format: {
type: 'string',
description: nls.localize('contributes.iconFonts.font-format', 'The format of the font.'),
enum: ['woff', 'woff2', 'truetype', 'opentype', 'embedded-opentype', 'svg']
}
},
required: [
'path',
'format'
]
}
}
}
}
}
});
export class IconExtensionPoint {
constructor() {
iconConfigurationExtPoint.setHandler((extensions, delta) => {
for (const extension of delta.added) {
const extensionValue = <IIconExtensionPoint[]>extension.value;
const collector = extension.collector;
if (!extension.description.enableProposedApi) {
collector.error(nls.localize('invalid.icons.proposedAPI', "'configuration.icons is a proposed constribution point and only available when running out of dev or with the following command line switch: --enable-proposed-api {0}", extension.description.identifier.value));
return;
}
if (!extensionValue || !Array.isArray(extensionValue)) {
collector.error(nls.localize('invalid.icons.configuration', "'configuration.icons' must be a array"));
return;
}
for (const iconContribution of extensionValue) {
if (typeof iconContribution.id !== 'string' || iconContribution.id.length === 0) {
collector.error(nls.localize('invalid.icons.id', "'configuration.icons.id' must be defined and can not be empty"));
return;
}
if (!iconContribution.id.match(iconIdPattern)) {
collector.error(nls.localize('invalid.icons.id.format', "'configuration.icons.id' must only contain letters, digits and minuses"));
return;
}
if (typeof iconContribution.description !== 'string' || iconContribution.id.length === 0) {
collector.error(nls.localize('invalid.icons.description', "'configuration.icons.description' must be defined and can not be empty"));
return;
}
let defaultIcon = iconContribution.default;
if (typeof defaultIcon === 'string') {
iconRegistry.registerIcon(iconContribution.id, { id: defaultIcon }, iconContribution.description);
} else if (typeof defaultIcon === 'object' && typeof defaultIcon.iconFontId === 'string' && typeof defaultIcon.character === 'string') {
iconRegistry.registerIcon(iconContribution.id, {
fontId: getFontId(extension.description, defaultIcon.iconFontId),
character: defaultIcon.character,
}, iconContribution.description);
} else {
collector.error(nls.localize('invalid.icons.default', "'configuration.icons.default' must be either a reference to the id of an other theme icon (string) or a icon definition (object)"));
}
}
}
for (const extension of delta.removed) {
const extensionValue = <IIconExtensionPoint[]>extension.value;
for (const iconContribution of extensionValue) {
iconRegistry.deregisterIcon(iconContribution.id);
}
}
});
}
}
export class IconFontExtensionPoint {
constructor() {
iconFontConfigurationExtPoint.setHandler((_extensions, delta) => {
for (const extension of delta.added) {
const extensionValue = <IIconFontExtensionPoint[]>extension.value;
const collector = extension.collector;
if (!extension.description.enableProposedApi) {
collector.error(nls.localize('invalid.iconFonts.proposedAPI', "'configuration.iconFonts is a proposed constribution point and only available when running out of dev or with the following command line switch: --enable-proposed-api {0}", extension.description.identifier.value));
return;
}
if (!extensionValue || !Array.isArray(extensionValue)) {
collector.error(nls.localize('invalid.iconFonts.configuration', "'configuration.iconFonts' must be a array"));
return;
}
for (const iconFontContribution of extensionValue) {
if (typeof iconFontContribution.id !== 'string' || iconFontContribution.id.length === 0) {
collector.error(nls.localize('invalid.iconFonts.id', "'configuration.iconFonts.id' must be defined and can not be empty"));
return;
}
if (!iconFontContribution.id.match(fontIdRegex)) {
collector.error(nls.localize('invalid.iconFonts.id.format', "'configuration.iconFonts.id' must only contain letters, numbers, underscore and minus."));
return;
}
if (!Array.isArray(iconFontContribution.src) || !iconFontContribution.src.length) {
collector.error(nls.localize('invalid.iconFonts.src', "'configuration.iconFonts.src' must be an array with locations of the icon font."));
return;
}
const def: IconFontDefinition = { src: [] };
for (const src of iconFontContribution.src) {
if (typeof src === 'object' && typeof src.path === 'string' && typeof src.format === 'string') {
const extensionLocation = extension.description.extensionLocation;
const iconFontLocation = resources.joinPath(extensionLocation, src.path);
if (!resources.isEqualOrParent(iconFontLocation, extensionLocation)) {
collector.warn(nls.localize('invalid.iconFonts.src.path', "Expected `contributes.iconFonts.src.path` ({0}) to be included inside extension's folder ({0}). This might make the extension non-portable.", iconFontLocation.path, extensionLocation.path));
}
def.src.push({
location: iconFontLocation,
format: src.format,
});
} else {
collector.error(nls.localize('invalid.iconFonts.src.item', "Items of 'configuration.iconFonts.src' must be objects with properties 'path' and 'format'"));
}
}
iconRegistry.registerIconFont(getFontId(extension.description, iconFontContribution.id), def);
}
}
for (const extension of delta.removed) {
const extensionValue = <IIconFontExtensionPoint[]>extension.value;
for (const iconFontContribution of extensionValue) {
iconRegistry.deregisterIconFont(getFontId(extension.description, iconFontContribution.id));
}
}
});
}
}
function getFontId(description: IExtensionDescription, iconFontId: string) {
return `${description.identifier.value}/${iconFontId}`;
}