- 遵循了 VSCode 的 Theme 标准,可参考:Color Theme
- Color Theme 包含 Workbench colors 和 Syntax colors
- 语法高亮(Syntax Highlight)由两局部组成:Tokenization 和 Theming
- 基于 CSS Custom Properties 实现主题切换
- 通过插件加载 / 切换主题的形式值得借鉴
- 语法高亮基于:vscode-textmate 和 vscode-oniguruma
Theming
在 Visual Studio Code 中,主题有三种类型:
- Color Theme:从 UI Component Identifier 和 Text Token Identifier 的色彩映射
- File Icon Theme:从文件类型 / 文件名到图像的映射
- Product Icon Theme:UI 的 icon,包含 Side bar, Activity bar, status bar 到 editor glyph margin
从 Contributes Theme 配置来看:
- colors 管制 UI 组件的色彩
- tokenColors 定于 editor 的代码高亮款式,具体可查看:Syntax Highlight Guide
- semanticTokenColors 作为 semanticHighlighting 的设置,容许加强编辑器中的高亮显示,具体可参考:Semantic Highlight Guide
自定义主题
能够参考:Create a new Color Theme
语法高亮
moanco-editor 和 VSCode 的高亮不太一样,比拟简陋很不难受,一番搜寻发现 monaco-editor 的语言反对应用的是内置的 Monarch 这个语法高亮反对。
官网的解释 Why doesn’t the editor support TextMate grammars?
次要就是因为 Textmate 语法解析依赖的 Oniguruma 是一个 C 语言下的解析性能,VSCode 能够应用 node 环境来调用原生的模块,然而在 web 环境下无奈实现,即便通过 asm.js 转换后,性能仍然会有 100-1000 倍的损失(16 年 9 月的阐明),而且 IE 不反对。
起初呈现了 WebAssembly,于是就有了 vscode-oniguruma。vscode-oniguruma 就是 Oniguruma 的 WASM 编译版,以便于在浏览器环境运行。
monaco-editor 语言的反对也只有通过 worker 的 js ts html css json 这些。然而业内更通用、生态更丰盛的是 Textmate,包含 VSCode 也是用的 Textmate。
Theia 将 monaco-editor
, vscode-oniguruma
and vscode-textmate
整合到一起,在 Editor 中获取 TM grammar 反对。
默认配置
在 Theia 利用入口的 package.json
文件的 "theia": {}
字段中配置。如:examples/browser/package.json
。
// dev-packages/application-package/src/application-props.ts
export const DEFAULT: ApplicationProps = {
...NpmRegistryProps.DEFAULT,
target: 'browser',
backend: {config: {}
},
frontend: {
config: {
applicationName: 'Eclipse Theia',
defaultTheme: 'dark',
defaultIconTheme: 'none'
}
},
generator: {
config: {preloadTemplate: ''}
}
};
而后在 Theming 中有 defaultTheme:
/**
* The default theme. If that is not applicable, returns with the fallback theme.
*/
get defaultTheme(): Theme {return this.themes[FrontendApplicationConfigProvider.get().defaultTheme] || this.themes[ApplicationProps.DEFAULT.frontend.config.defaultTheme];
}
通过 API 设置主题
能够在插件中通过 VSCode API 设置主题。
获取 settings 配置,并更新 workbench.colorTheme
字段。
export async function start(context: theia.PluginContext): Promise<void> {const configuration = theia.workspace.getConfiguration()
configuration.update('workbench.colorTheme', 'tide-dark-blue')
}
源码
Contributes 配置
PluginContributionHandler 负责插件 Contributes 配置的解决。
// packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts
@injectable()
export class PluginContributionHandler {@inject(MonacoThemingService)
protected readonly monacoThemingService: MonacoThemingService;
@inject(ColorRegistry)
protected readonly colors: ColorRegistry;
@inject(PluginIconThemeService)
protected readonly iconThemeService: PluginIconThemeService;
/**
* Always synchronous in order to simplify handling disconnections.
* @throws never, loading of each contribution should handle errors
* in order to avoid preventing loading of other contributions or extensions
*/
handleContributions(clientId: string, plugin: DeployedPlugin): Disposable {if (contributions.themes && contributions.themes.length) {const pending = {};
for (const theme of contributions.themes) {pushContribution(`themes.${theme.uri}`, () => this.monacoThemingService.register(theme, pending));
}
}
if (contributions.iconThemes && contributions.iconThemes.length) {for (const iconTheme of contributions.iconThemes) {pushContribution(`iconThemes.${iconTheme.uri}`, () => this.iconThemeService.register(iconTheme, plugin));
}
}
if (contributions.colors) {pushContribution('colors', () => this.colors.register(...contributions.colors));
}
}
}
MonacoThemingService 解决
MonacoThemingService.loadTheme 读取 json 文件并解析。
MonacoThemingService.register 注册主题。
// packages/monaco/src/browser/monaco-theming-service.ts
@injectable()
export class MonacoThemingService {
protected async doRegister(theme: MonacoTheme,
pending: {[uri: string]: Promise<any> },
toDispose: DisposableCollection
): Promise<void> {
try {const includes = {};
const json = await this.loadTheme(theme.uri, includes, pending, toDispose);
if (toDispose.disposed) {return;}
const label = theme.label || new URI(theme.uri).path.base;
const {id, description, uiTheme} = theme;
toDispose.push(MonacoThemingService.register({ id, label, description, uiTheme: uiTheme, json, includes}));
} catch (e) {console.error('Failed to load theme from' + theme.uri, e);
}
}
protected async loadTheme(
uri: string,
includes: {[include: string]: any },
pending: {[uri: string]: Promise<any> },
toDispose: DisposableCollection
): Promise<any> {const result = await this.fileService.read(new URI(uri));
const content = result.value;
if (toDispose.disposed) {return;}
const themeUri = new URI(uri);
if (themeUri.path.ext !== '.json') {const value = plistparser.parse(content);
if (value && 'settings' in value && Array.isArray(value.settings)) {return { tokenColors: value.settings};
}
throw new Error(`Problem parsing tmTheme file: ${uri}. 'settings' is not array.`);
}
const json = jsoncparser.parse(content, undefined, { disallowComments: false});
if ('tokenColors' in json && typeof json.tokenColors === 'string') {const value = await this.doLoadTheme(themeUri, json.tokenColors, includes, pending, toDispose);
if (toDispose.disposed) {return;}
json.tokenColors = value.tokenColors;
}
if (json.include) {includes[json.include] = await this.doLoadTheme(themeUri, json.include, includes, pending, toDispose);
if (toDispose.disposed) {return;}
}
return json;
}
static register(theme: MonacoThemeJson): Disposable {
const uiTheme = theme.uiTheme || 'vs-dark';
const {label, description, json, includes} = theme;
const id = theme.id || label;
const cssSelector = MonacoThemingService.toCssSelector(id);
const data = MonacoThemeRegistry.SINGLETON.register(json, includes, cssSelector, uiTheme); // 注册 MonacoTheme
return MonacoThemingService.doRegister({id, label, description, uiTheme, data});
}
protected static doRegister(state: MonacoThemeState): Disposable {const { id, label, description, uiTheme, data} = state;
const type = uiTheme === 'vs' ? 'light' : uiTheme === 'vs-dark' ? 'dark' : 'hc';
const builtInTheme = uiTheme === 'vs' ? BuiltinThemeProvider.lightCss : BuiltinThemeProvider.darkCss;
return new DisposableCollection(ThemeService.get().register({ // 注册 uiTheme
type,
id,
label,
description: description,
editorTheme: data.name!,
activate(): void {builtInTheme.use();
},
deactivate(): void {builtInTheme.unuse();
}
}),
putTheme(state)
);
}
}
MonacoThemeRegistry Monaco 主题注册
MonacoThemeRegistry 将主题与 monaco
关联,基于 vscode-textmate
实现 Editor 代码高亮。
通过 vscode-textmate
的 Registry 获取 encodedTokensColors。
monaco
是全局引入的。
// packages/monaco/src/browser/textmate/monaco-theme-registry.ts
import {IRawTheme, Registry, IRawThemeSetting} from 'vscode-textmate';
@injectable()
export class MonacoThemeRegistry {
/**
* Register VS Code compatible themes
*/
register(json: any, includes?: { [includePath: string]: any }, givenName?: string, monacoBase?: monaco.editor.BuiltinTheme): ThemeMix {
const name = givenName || json.name!;
const result: ThemeMix = {
name,
base: monacoBase || 'vs',
inherit: true,
colors: {},
rules: [],
settings: []};
if (typeof json.include !== 'undefined') {if (!includes || !includes[json.include]) {console.error(`Couldn't resolve includes theme ${json.include}.`);
} else {const parentTheme = this.register(includes[json.include], includes);
Object.assign(result.colors, parentTheme.colors);
result.rules.push(...parentTheme.rules);
result.settings.push(...parentTheme.settings);
}
}
const tokenColors: Array<IRawThemeSetting> = json.tokenColors;
if (Array.isArray(tokenColors)) {for (const tokenColor of tokenColors) {if (tokenColor.scope && tokenColor.settings) {
result.settings.push({
scope: tokenColor.scope,
settings: {foreground: this.normalizeColor(tokenColor.settings.foreground),
background: this.normalizeColor(tokenColor.settings.background),
fontStyle: tokenColor.settings.fontStyle
}
});
}
}
}
// colors 解决
if (json.colors) {Object.assign(result.colors, json.colors);
result.encodedTokensColors = Object.keys(result.colors).map(key => result.colors[key]);
}
if (monacoBase && givenName) {for (const setting of result.settings) {this.transform(setting, rule => result.rules.push(rule));
}
// the default rule (scope empty) is always the first rule. Ignore all other default rules.
const defaultTheme = monaco.services.StaticServices.standaloneThemeService.get()._knownThemes.get(result.base)!;
const foreground = result.colors['editor.foreground'] || defaultTheme.getColor('editor.foreground');
const background = result.colors['editor.background'] || defaultTheme.getColor('editor.background');
result.settings.unshift({
settings: {foreground: this.normalizeColor(foreground),
background: this.normalizeColor(background)
}
});
const reg = new Registry();
reg.setTheme(result);
// 获取 encodedTokensColors
result.encodedTokensColors = reg.getColorMap();
// index 0 has to be set to null as it is 'undefined' by default, but monaco code expects it to be null
// eslint-disable-next-line no-null/no-null
result.encodedTokensColors[0] = null!;
this.setTheme(givenName, result);
}
return result;
}
setTheme(name: string, data: ThemeMix): void {
// monaco auto refreshes a theme with new data
monaco.editor.defineTheme(name, data);
}
}
ThemeService UI 主题注册
挂载在全局对象中。
// packages/core/src/browser/theming.ts
export class ThemeService {private themes: { [id: string]: Theme } = {};
private activeTheme: Theme | undefined;
private readonly themeChange = new Emitter<ThemeChangeEvent>();
readonly onThemeChange: Event<ThemeChangeEvent> = this.themeChange.event; // onThemeChange 事件
static get(): ThemeService {
const global = window as any; // eslint-disable-line @typescript-eslint/no-explicit-any
return global[ThemeServiceSymbol] || new ThemeService();}
register(...themes: Theme[]): Disposable {for (const theme of themes) {this.themes[theme.id] = theme;
}
this.validateActiveTheme();
return Disposable.create(() => {for (const theme of themes) {delete this.themes[theme.id];
}
this.validateActiveTheme();});
}
/**
* The default theme. If that is not applicable, returns with the fallback theme.
*/
get defaultTheme(): Theme {return this.themes[FrontendApplicationConfigProvider.get().defaultTheme] || this.themes[ApplicationProps.DEFAULT.frontend.config.defaultTheme];
}
}
ColorContribution
其余中央能够通过继承 ColorContribution 接口实现 registerColors 办法来注册主题色彩的配置。
export const ColorContribution = Symbol('ColorContribution');
export interface ColorContribution {registerColors(colors: ColorRegistry): void;
}
如 TerminalFrontendContribution 的 terminal.background 实现:
// packages/terminal/src/browser/terminal-frontend-contribution.ts
registerColors(colors: ColorRegistry): void {
colors.register({
id: 'terminal.background',
defaults: {
dark: 'panel.background',
light: 'panel.background',
hc: 'panel.background'
},
description: 'The background color of the terminal, this allows coloring the terminal differently to the panel.'
});
}
ColorRegistry
注册主题色彩的配置。
/**
* It should be implemented by an extension, e.g. by the monaco extension.
*/
@injectable()
export class ColorRegistry {protected readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected fireDidChange(): void {this.onDidChangeEmitter.fire(undefined);
}
*getColors(): IterableIterator<string> {}
getCurrentCssVariable(id: string): ColorCssVariable | undefined {const value = this.getCurrentColor(id);
if (!value) {return undefined;}
const name = this.toCssVariableName(id);
return {name, value};
}
toCssVariableName(id: string, prefix = 'theia'): string { // 将配置转换为 CSS Custom Property
return `--${prefix}-${id.replace(/\./g, '-')}`;
}
getCurrentColor(id: string): string | undefined {return undefined;}
register(...definitions: ColorDefinition[]): Disposable {const result = new DisposableCollection(...definitions.map(definition => this.doRegister(definition)));
this.fireDidChange();
return result;
}
protected doRegister(definition: ColorDefinition): Disposable {return Disposable.NULL;}
}
ColorApplicationContribution
在 onStart 生命周期中,顺次调用 registerColors 办法注册。
在切换主题时,通过 documentElement.style.setProperty(name, value)
顺次设置 CSS Custom Property,从而变换主题。
// packages/core/src/browser/color-application-contribution.ts
@injectable()
export class ColorApplicationContribution implements FrontendApplicationContribution {protected readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange = this.onDidChangeEmitter.event;
@inject(ColorRegistry)
protected readonly colors: ColorRegistry;
@inject(ContributionProvider) @named(ColorContribution)
protected readonly colorContributions: ContributionProvider<ColorContribution>;
private static themeBackgroundId = 'theme.background';
onStart(): void {for (const contribution of this.colorContributions.getContributions()) {contribution.registerColors(this.colors);
}
this.updateThemeBackground();
ThemeService.get().onThemeChange(() => this.updateThemeBackground());
this.update();
ThemeService.get().onThemeChange(() => this.update());
this.colors.onDidChange(() => this.update());
}
protected update(): void { // 更新主题
if (!document) {return;}
this.toUpdate.dispose();
const theme = 'theia-' + ThemeService.get().getCurrentTheme().type;
document.body.classList.add(theme);
this.toUpdate.push(Disposable.create(() => document.body.classList.remove(theme)));
const documentElement = document.documentElement;
if (documentElement) {for (const id of this.colors.getColors()) {const variable = this.colors.getCurrentCssVariable(id);
if (variable) {const { name, value} = variable;
documentElement.style.setProperty(name, value); // 顺次设置 CSS Custom Property
this.toUpdate.push(Disposable.create(() => documentElement.style.removeProperty(name)));
}
}
}
this.onDidChangeEmitter.fire(undefined);
}
static initBackground(): void {const value = window.localStorage.getItem(this.themeBackgroundId) || '#1d1d1d';
const documentElement = document.documentElement;
documentElement.style.setProperty('--theia-editor-background', value);
}
}
参考
- VSCode Extension API – Color Theme
- vscode-theme-defaults
- WebIDE 的开发记录其五