themeService.ts 15.5 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

B
Benjamin Pasero 已提交
7
import {TPromise} from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
8 9
import nls = require('vs/nls');
import Paths = require('vs/base/common/paths');
10
import Json = require('vs/base/common/json');
E
Erich Gamma 已提交
11
import {IThemeExtensionPoint} from 'vs/platform/theme/common/themeExtensionPoint';
A
Alex Dima 已提交
12
import {IExtensionService} from 'vs/platform/extensions/common/extensions';
13
import {ExtensionsRegistry, IExtensionMessageCollector} from 'vs/platform/extensions/common/extensionsRegistry';
M
Martin Aeschlimann 已提交
14 15 16 17 18 19
import {IThemeService, IThemeData} from 'vs/workbench/services/themes/common/themeService';
import {getBaseThemeId, getSyntaxThemeId} from 'vs/platform/theme/common/themes';
import {IWindowService} from 'vs/workbench/services/window/electron-browser/windowService';
import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage';
import {$} from 'vs/base/browser/builder';
import Event, {Emitter} from 'vs/base/common/event';
E
Erich Gamma 已提交
20 21 22 23 24 25

import plist = require('vs/base/node/plist');
import pfs = require('vs/base/node/pfs');

// implementation

M
Martin Aeschlimann 已提交
26 27 28
const DEFAULT_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-json';

const THEME_CHANNEL = 'vscode:changeTheme';
B
Benjamin Pasero 已提交
29
const THEME_PREF = 'workbench.theme';
M
Martin Aeschlimann 已提交
30 31

let defaultBaseTheme = getBaseThemeId(DEFAULT_THEME_ID);
E
Erich Gamma 已提交
32

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
const defaultThemeExtensionId = 'vscode-theme-defaults';
const oldDefaultThemeExtensionId = 'vscode-theme-colorful-defaults';

function validateThemeId(theme: string) : string {
	// migrations
	switch (theme) {
		case 'vs': return `vs ${defaultThemeExtensionId}-themes-light_vs-json`;
		case 'vs-dark': return `vs-dark ${defaultThemeExtensionId}-themes-dark_vs-json`;
		case 'hc-black': return `hc-black ${defaultThemeExtensionId}-themes-hc_black-json`;
		case `vs ${oldDefaultThemeExtensionId}-themes-light_plus-tmTheme`: return `vs ${defaultThemeExtensionId}-themes-light_plus-json`;
		case `vs-dark ${oldDefaultThemeExtensionId}-themes-dark_plus-tmTheme`: return `vs-dark ${defaultThemeExtensionId}-themes-dark_plus-json`;
	}
	return theme;
}

48
let themesExtPoint = ExtensionsRegistry.registerExtensionPoint<IThemeExtensionPoint[]>('themes', {
E
Erich Gamma 已提交
49 50
	description: nls.localize('vscode.extension.contributes.themes', 'Contributes textmate color themes.'),
	type: 'array',
51
	defaultSnippets: [{ body: [{ label: '{{label}}', uiTheme: 'vs-dark', path: './themes/{{id}}.tmTheme.' }] }],
E
Erich Gamma 已提交
52 53
	items: {
		type: 'object',
54
		defaultSnippets: [{ body: { label: '{{label}}', uiTheme: 'vs-dark', path: './themes/{{id}}.tmTheme.' } }],
E
Erich Gamma 已提交
55 56 57 58 59 60 61
		properties: {
			label: {
				description: nls.localize('vscode.extension.contributes.themes.label', 'Label of the color theme as shown in the UI.'),
				type: 'string'
			},
			uiTheme: {
				description: nls.localize('vscode.extension.contributes.themes.uiTheme', 'Base theme defining the colors around the editor: \'vs\' is the light color theme, \'vs-dark\' is the dark color theme.'),
62
				enum: ['vs', 'vs-dark', 'hc-black']
E
Erich Gamma 已提交
63 64 65 66 67 68 69 70 71
			},
			path: {
				description: nls.localize('vscode.extension.contributes.themes.path', 'Path of the tmTheme file. The path is relative to the extension folder and is typically \'./themes/themeFile.tmTheme\'.'),
				type: 'string'
			}
		}
	}
});

72 73 74 75 76 77 78 79 80 81 82
interface ThemeSettingStyle {
	background?: string;
	foreground?: string;
	fontStyle?: string;
	caret?: string;
	invisibles?: string;
	lineHighlight?: string;
	selection?: string;
}

interface ThemeSetting {
83 84
	name?: string;
	scope?: string | string[];
85 86 87 88 89 90 91 92 93
	settings: ThemeSettingStyle[];
}

interface ThemeDocument {
	name: string;
	include: string;
	settings: ThemeSetting[];
}

E
Erich Gamma 已提交
94 95 96
export class ThemeService implements IThemeService {
	serviceId = IThemeService;

97
	private knownThemes: IThemeData[];
M
Martin Aeschlimann 已提交
98 99 100
	private currentTheme: string;
	private container: HTMLElement;
	private onThemeChange: Emitter<string>;
E
Erich Gamma 已提交
101

M
Martin Aeschlimann 已提交
102 103 104 105
	constructor(
			private extensionService: IExtensionService,
			@IWindowService private windowService: IWindowService,
			@IStorageService private storageService: IStorageService) {
E
Erich Gamma 已提交
106
		this.knownThemes = [];
M
Martin Aeschlimann 已提交
107
		this.onThemeChange = new Emitter<string>();
E
Erich Gamma 已提交
108 109 110 111 112 113

		themesExtPoint.setHandler((extensions) => {
			for (let ext of extensions) {
				this.onThemes(ext.description.extensionFolderPath, ext.description.id, ext.value, ext.collector);
			}
		});
M
Martin Aeschlimann 已提交
114 115 116 117 118 119 120 121 122 123

		windowService.onBroadcast(e => {
			if (e.channel === THEME_CHANNEL && typeof e.payload === 'string') {
				this.setTheme(e.payload, false);
			}
		});
	}

	public get onDidThemeChange(): Event<string> {
		return this.onThemeChange.event;
E
Erich Gamma 已提交
124 125
	}

M
Martin Aeschlimann 已提交
126 127 128
	public initialize(container: HTMLElement): TPromise<boolean> {
		this.container = container;

B
Benjamin Pasero 已提交
129
		let themeId = this.storageService.get(THEME_PREF, StorageScope.GLOBAL, null);
M
Martin Aeschlimann 已提交
130 131
		if (!themeId) {
			themeId = DEFAULT_THEME_ID;
B
Benjamin Pasero 已提交
132
			this.storageService.store(THEME_PREF, themeId, StorageScope.GLOBAL);
M
Martin Aeschlimann 已提交
133 134 135 136 137 138 139 140 141 142 143 144
		}
		return this.setTheme(themeId, false);
	}

	public setTheme(themeId: string, broadcastToAllWindows: boolean) : TPromise<boolean> {
		if (!themeId) {
			return TPromise.as(false);
		}
		if (themeId === this.currentTheme) {
			if (broadcastToAllWindows) {
				this.windowService.broadcast({ channel: THEME_CHANNEL, payload: themeId });
			}
145
			return TPromise.as(true);
M
Martin Aeschlimann 已提交
146 147 148 149 150 151 152 153 154 155 156 157 158
		}

		themeId = validateThemeId(themeId); // migrate theme ids

		let onApply = (newThemeId) => {
			if (this.container) {
				if (this.currentTheme) {
					$(this.container).removeClass(this.currentTheme);
				}
				this.currentTheme = newThemeId;
				$(this.container).addClass(newThemeId);
			}

B
Benjamin Pasero 已提交
159
			this.storageService.store(THEME_PREF, newThemeId, StorageScope.GLOBAL);
M
Martin Aeschlimann 已提交
160 161 162 163 164 165 166 167 168 169
			if (broadcastToAllWindows) {
				this.windowService.broadcast({ channel: THEME_CHANNEL, payload: newThemeId });
			}
			this.onThemeChange.fire(newThemeId);
		};

		return this.applyThemeCSS(themeId, DEFAULT_THEME_ID, onApply);
	}

	public getTheme() {
B
Benjamin Pasero 已提交
170
		return this.currentTheme || this.storageService.get(THEME_PREF, StorageScope.GLOBAL, DEFAULT_THEME_ID);
M
Martin Aeschlimann 已提交
171 172 173
	}

	private loadTheme(themeId: string, defaultId?: string): TPromise<IThemeData> {
E
Erich Gamma 已提交
174
		return this.getThemes().then(allThemes => {
B
Benjamin Pasero 已提交
175
			let themes = allThemes.filter(t => t.id === themeId);
E
Erich Gamma 已提交
176 177 178
			if (themes.length > 0) {
				return themes[0];
			}
M
Martin Aeschlimann 已提交
179 180 181 182 183 184
			if (defaultId) {
				let themes = allThemes.filter(t => t.id === defaultId);
				if (themes.length > 0) {
					return themes[0];
				}
			}
E
Erich Gamma 已提交
185 186 187 188
			return null;
		});
	}

M
Martin Aeschlimann 已提交
189 190
	private applyThemeCSS(themeId: string, defaultId: string, onApply: (themeId:string) => void): TPromise<boolean> {
		return this.loadTheme(themeId, defaultId).then(theme => {
E
Erich Gamma 已提交
191
			if (theme) {
M
Martin Aeschlimann 已提交
192
				return applyTheme(theme, onApply);
E
Erich Gamma 已提交
193
			}
194
			return false;
B
Benjamin Pasero 已提交
195
		});
E
Erich Gamma 已提交
196 197
	}

198
	public getThemes(): TPromise<IThemeData[]> {
A
Alex Dima 已提交
199
		return this.extensionService.onReady().then(isReady => {
E
Erich Gamma 已提交
200 201 202 203
			return this.knownThemes;
		});
	}

204
	private onThemes(extensionFolderPath: string, extensionId: string, themes: IThemeExtensionPoint[], collector: IExtensionMessageCollector): void {
E
Erich Gamma 已提交
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
		if (!Array.isArray(themes)) {
			collector.error(nls.localize(
				'reqarray',
				"Extension point `{0}` must be an array.",
				themesExtPoint.name
			));
			return;
		}
		themes.forEach(theme => {
			if (!theme.path || (typeof theme.path !== 'string')) {
				collector.error(nls.localize(
					'reqpath',
					"Expected string in `contributes.{0}.path`. Provided value: {1}",
					themesExtPoint.name,
					String(theme.path)
				));
				return;
			}
			let normalizedAbsolutePath = Paths.normalize(Paths.join(extensionFolderPath, theme.path));

			if (normalizedAbsolutePath.indexOf(extensionFolderPath) !== 0) {
				collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", themesExtPoint.name, normalizedAbsolutePath, extensionFolderPath));
			}

B
Benjamin Pasero 已提交
229
			let themeSelector = toCssSelector(extensionId + '-' + Paths.normalize(theme.path));
E
Erich Gamma 已提交
230 231 232 233 234 235
			this.knownThemes.push({
				id: `${theme.uiTheme || defaultBaseTheme} ${themeSelector}`,
				label: theme.label || Paths.basename(theme.path),
				description: theme.description,
				path: normalizedAbsolutePath
			});
B
Benjamin Pasero 已提交
236
		});
E
Erich Gamma 已提交
237 238 239
	}
}

240

E
Erich Gamma 已提交
241 242 243 244
function toCssSelector(str: string) {
	return str.replace(/[^_\-a-zA-Z0-9]/g, '-');
}

M
Martin Aeschlimann 已提交
245
function applyTheme(theme: IThemeData, onApply: (themeId:string) => void): TPromise<boolean> {
E
Erich Gamma 已提交
246
	if (theme.styleSheetContent) {
B
Benjamin Pasero 已提交
247
		_applyRules(theme.styleSheetContent);
M
Martin Aeschlimann 已提交
248 249
		onApply(theme.id);
		return TPromise.as(true);
E
Erich Gamma 已提交
250
	}
251 252 253 254
	return _loadThemeDocument(theme.path).then(themeDocument => {
		let styleSheetContent = _processThemeObject(theme.id, themeDocument);
		theme.styleSheetContent = styleSheetContent;
		_applyRules(styleSheetContent);
M
Martin Aeschlimann 已提交
255
		onApply(theme.id);
256
		return true;
257 258 259 260
	}, error => {
		return TPromise.wrapError(nls.localize('error.cannotloadtheme', "Unable to load {0}", theme.path));
	});
}
E
Erich Gamma 已提交
261

262 263 264
function _loadThemeDocument(themePath: string) : TPromise<ThemeDocument> {
	return pfs.readFile(themePath).then(content => {
		if (Paths.extname(themePath) === '.json') {
265
			let errors: Json.ParseError[] = [];
266
			let contentValue = <ThemeDocument> Json.parse(content.toString(), errors);
267
			if (errors.length > 0) {
268
				return TPromise.wrapError(new Error(nls.localize('error.cannotparsejson', "Problems parsing JSON theme file: {0}", errors.map(e => Json.getParseErrorMessage(e.error)).join(', '))));
269 270 271 272 273 274
			}
			if (contentValue.include) {
				return _loadThemeDocument(Paths.join(Paths.dirname(themePath), contentValue.include)).then(includedValue => {
					contentValue.settings = includedValue.settings.concat(contentValue.settings);
					return TPromise.as(contentValue);
				});
275
			}
276
			return TPromise.as(contentValue);
277 278 279 280 281
		} else {
			let parseResult = plist.parse(content.toString());
			if (parseResult.errors && parseResult.errors.length) {
				return TPromise.wrapError(new Error(nls.localize('error.cannotparse', "Problems parsing plist file: {0}", parseResult.errors.join(', '))));
			}
282
			return TPromise.as(parseResult.value);
E
Erich Gamma 已提交
283 284 285 286
		}
	});
}

287
function _processThemeObject(themeId: string, themeDocument: ThemeDocument): string {
B
Benjamin Pasero 已提交
288
	let cssRules: string[] = [];
E
Erich Gamma 已提交
289

290 291
	let themeSettings : ThemeSetting[] = themeDocument.settings;
	let editorSettings : ThemeSettingStyle = {
E
Erich Gamma 已提交
292 293 294 295 296 297 298 299
		background: void 0,
		foreground: void 0,
		caret: void 0,
		invisibles: void 0,
		lineHighlight: void 0,
		selection: void 0
	};

M
Martin Aeschlimann 已提交
300
	let themeSelector = `${getBaseThemeId(themeId)}.${getSyntaxThemeId(themeId)}`;
E
Erich Gamma 已提交
301 302

	if (Array.isArray(themeSettings)) {
303
		themeSettings.forEach((s : ThemeSetting, index, arr) => {
E
Erich Gamma 已提交
304 305 306
			if (index === 0 && !s.scope) {
				editorSettings = s.settings;
			} else {
307
				let scope: string | string[] = s.scope;
308
				let settings = s.settings;
E
Erich Gamma 已提交
309
				if (scope && settings) {
310
					let rules = Array.isArray(scope) ? <string[]> scope : scope.split(',');
B
Benjamin Pasero 已提交
311
					let statements = _settingsToStatements(settings);
E
Erich Gamma 已提交
312 313 314 315 316 317 318 319 320 321 322
					rules.forEach(rule => {
						rule = rule.trim().replace(/ /g, '.'); // until we have scope hierarchy in the editor dom: replace spaces with .

						cssRules.push(`.monaco-editor.${themeSelector} .token.${rule} { ${statements} }`);
					});
				}
			}
		});
	}

	if (editorSettings.background) {
B
Benjamin Pasero 已提交
323
		let background = new Color(editorSettings.background);
E
Erich Gamma 已提交
324 325 326
		//cssRules.push(`.monaco-editor.${themeSelector} { background-color: ${background}; }`);
		cssRules.push(`.monaco-editor.${themeSelector} .monaco-editor-background { background-color: ${background}; }`);
		cssRules.push(`.monaco-editor.${themeSelector} .glyph-margin { background-color: ${background}; }`);
327
		cssRules.push(`.${themeSelector} .monaco-workbench .monaco-editor-background { background-color: ${background}; }`);
E
Erich Gamma 已提交
328 329
	}
	if (editorSettings.foreground) {
B
Benjamin Pasero 已提交
330
		let foreground = new Color(editorSettings.foreground);
E
Erich Gamma 已提交
331 332 333 334
		cssRules.push(`.monaco-editor.${themeSelector} { color: ${foreground}; }`);
		cssRules.push(`.monaco-editor.${themeSelector} .token { color: ${foreground}; }`);
	}
	if (editorSettings.selection) {
B
Benjamin Pasero 已提交
335
		let selection = new Color(editorSettings.selection);
E
Erich Gamma 已提交
336 337 338 339
		cssRules.push(`.monaco-editor.${themeSelector} .focused .selected-text { background-color: ${selection}; }`);
		cssRules.push(`.monaco-editor.${themeSelector} .selected-text { background-color: ${selection.transparent(0.5)}; }`);
	}
	if (editorSettings.lineHighlight) {
B
Benjamin Pasero 已提交
340
		let lineHighlight = new Color(editorSettings.lineHighlight);
341
		cssRules.push(`.monaco-editor.${themeSelector}.focused .current-line { background-color: ${lineHighlight}; border:0; }`);
E
Erich Gamma 已提交
342 343
	}
	if (editorSettings.caret) {
B
Benjamin Pasero 已提交
344
		let caret = new Color(editorSettings.caret);
345 346
		let oppositeCaret = caret.opposite();
		cssRules.push(`.monaco-editor.${themeSelector} .cursor { background-color: ${caret}; border-color: ${caret}; color: ${oppositeCaret}; }`);
E
Erich Gamma 已提交
347 348
	}
	if (editorSettings.invisibles) {
B
Benjamin Pasero 已提交
349
		let invisibles = new Color(editorSettings.invisibles);
E
Erich Gamma 已提交
350
		cssRules.push(`.monaco-editor.${themeSelector} .token.whitespace { color: ${invisibles} !important; }`);
351
		cssRules.push(`.monaco-editor.${themeSelector} .token.indent-guide { border-left: 1px solid ${invisibles}; }`);
E
Erich Gamma 已提交
352 353 354 355 356
	}

	return cssRules.join('\n');
}

357
function _settingsToStatements(settings: ThemeSettingStyle): string {
B
Benjamin Pasero 已提交
358
	let statements: string[] = [];
E
Erich Gamma 已提交
359

B
Benjamin Pasero 已提交
360
	for (let settingName in settings) {
E
Erich Gamma 已提交
361 362 363
		var value = settings[settingName];
		switch (settingName) {
			case 'foreground':
B
Benjamin Pasero 已提交
364
				let foreground = new Color(value);
E
Erich Gamma 已提交
365 366 367 368
				statements.push(`color: ${foreground};`);
				break;
			case 'background':
				// do not support background color for now, see bug 18924
B
Benjamin Pasero 已提交
369
				//let background = new Color(value);
E
Erich Gamma 已提交
370 371 372
				//statements.push(`background-color: ${background};`);
				break;
			case 'fontStyle':
B
Benjamin Pasero 已提交
373
				let segments = value.split(' ');
E
Erich Gamma 已提交
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
				segments.forEach(s => {
					switch (value) {
						case 'italic':
							statements.push(`font-style: italic;`);
							break;
						case 'bold':
							statements.push(`font-weight: bold;`);
							break;
						case 'underline':
							statements.push(`text-decoration: underline;`);
							break;
					}
				});
		}
	}
	return statements.join(' ');
}

B
Benjamin Pasero 已提交
392
let className = 'contributedColorTheme';
E
Erich Gamma 已提交
393 394

function _applyRules(styleSheetContent: string) {
B
Benjamin Pasero 已提交
395
	let themeStyles = document.head.getElementsByClassName(className);
E
Erich Gamma 已提交
396
	if (themeStyles.length === 0) {
B
Benjamin Pasero 已提交
397 398
		let elStyle = document.createElement('style');
		elStyle.type = 'text/css';
E
Erich Gamma 已提交
399 400 401 402
		elStyle.className = className;
		elStyle.innerHTML = styleSheetContent;
		document.head.appendChild(elStyle);
	} else {
B
Benjamin Pasero 已提交
403
		(<HTMLStyleElement>themeStyles[0]).innerHTML = styleSheetContent;
E
Erich Gamma 已提交
404 405 406
	}
}

B
Benjamin Pasero 已提交
407
interface RGBA { r: number; g: number; b: number; a: number; }
E
Erich Gamma 已提交
408 409 410

class Color {

B
Benjamin Pasero 已提交
411
	private parsed: RGBA;
E
Erich Gamma 已提交
412 413
	private str: string;

B
Benjamin Pasero 已提交
414
	constructor(arg: string | RGBA) {
E
Erich Gamma 已提交
415
		if (typeof arg === 'string') {
B
Benjamin Pasero 已提交
416
			this.parsed = Color.parse(<string>arg);
E
Erich Gamma 已提交
417
		} else {
B
Benjamin Pasero 已提交
418
			this.parsed = <RGBA>arg;
E
Erich Gamma 已提交
419 420 421 422
		}
		this.str = null;
	}

B
Benjamin Pasero 已提交
423
	private static parse(color: string): RGBA {
E
Erich Gamma 已提交
424 425 426 427 428
		function parseHex(str: string) {
			return parseInt('0x' + str);
		}

		if (color.charAt(0) === '#' && color.length >= 7) {
B
Benjamin Pasero 已提交
429 430 431 432
			let r = parseHex(color.substr(1, 2));
			let g = parseHex(color.substr(3, 2));
			let b = parseHex(color.substr(5, 2));
			let a = color.length === 9 ? parseHex(color.substr(7, 2)) / 0xff : 1;
E
Erich Gamma 已提交
433 434
			return { r, g, b, a };
		}
B
Benjamin Pasero 已提交
435
		return { r: 255, g: 0, b: 0, a: 1 };
E
Erich Gamma 已提交
436 437 438 439
	}

	public toString(): string {
		if (!this.str) {
B
Benjamin Pasero 已提交
440
			let p = this.parsed;
E
Erich Gamma 已提交
441 442 443 444 445
			this.str = `rgba(${p.r}, ${p.g}, ${p.b}, ${+p.a.toFixed(2)})`;
		}
		return this.str;
	}

B
Benjamin Pasero 已提交
446 447 448
	public transparent(factor: number): Color {
		let p = this.parsed;
		return new Color({ r: p.r, g: p.g, b: p.b, a: p.a * factor });
E
Erich Gamma 已提交
449
	}
450 451 452 453 454 455 456 457 458 459

	public opposite(): Color {
		return new Color({
			r: 255 - this.parsed.r,
			g: 255 - this.parsed.g,
			b: 255 - this.parsed.b,
			a : this.parsed.a
		});
	}
}