abstractTextMateService.ts 18.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/*---------------------------------------------------------------------------------------------
 *  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 * as dom from 'vs/base/browser/dom';
import { Color } from 'vs/base/common/color';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import * as resources from 'vs/base/common/resources';
import * as types from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/token';
A
Alex Dima 已提交
15
import { IState, ITokenizationSupport, LanguageId, TokenMetadata, TokenizationRegistry, StandardTokenType, LanguageIdentifier } from 'vs/editor/common/modes';
16 17 18 19 20
import { nullTokenize2 } from 'vs/editor/common/modes/nullMode';
import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
P
Peng Lyu 已提交
21 22
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
23
import { ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry';
A
Alex Dima 已提交
24
import { ITMSyntaxExtensionPoint, grammarsExtPoint } from 'vs/workbench/services/textMate/common/TMGrammars';
25
import { ITextMateService } from 'vs/workbench/services/textMate/common/textMateService';
A
Alex Dima 已提交
26
import { ITokenColorizationRule, IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
27
import { IGrammar, StackElement, IOnigLib, IRawTheme } from 'vscode-textmate';
28 29
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
30 31
import { IValidGrammarDefinition, IValidEmbeddedLanguagesMap, IValidTokenTypeMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry';
import { TMGrammarFactory } from 'vs/workbench/services/textMate/common/TMGrammarFactory';
32 33 34 35 36 37 38 39 40

export abstract class AbstractTextMateService extends Disposable implements ITextMateService {
	public _serviceBrand: any;

	private readonly _onDidEncounterLanguage: Emitter<LanguageId> = this._register(new Emitter<LanguageId>());
	public readonly onDidEncounterLanguage: Event<LanguageId> = this._onDidEncounterLanguage.event;

	private readonly _styleElement: HTMLStyleElement;
	private readonly _createdModes: string[];
A
Alex Dima 已提交
41
	private readonly _encounteredLanguages: boolean[];
42

A
Alex Dima 已提交
43 44
	private _grammarDefinitions: IValidGrammarDefinition[] | null;
	private _grammarFactory: TMGrammarFactory | null;
45 46 47 48 49 50
	private _tokenizersRegistrations: IDisposable[];
	private _currentTokenColors: ITokenColorizationRule[] | null;

	constructor(
		@IModeService private readonly _modeService: IModeService,
		@IWorkbenchThemeService private readonly _themeService: IWorkbenchThemeService,
A
Alex Dima 已提交
51
		@IFileService protected readonly _fileService: IFileService,
52 53
		@INotificationService private readonly _notificationService: INotificationService,
		@ILogService private readonly _logService: ILogService,
P
Peng Lyu 已提交
54 55
		@IConfigurationService private readonly _configurationService: IConfigurationService,
		@IStorageService private readonly _storageService: IStorageService
56 57 58 59 60
	) {
		super();
		this._styleElement = dom.createStyleSheet();
		this._styleElement.className = 'vscode-tokens-styles';
		this._createdModes = [];
61
		this._encounteredLanguages = [];
A
Alex Dima 已提交
62 63 64

		this._grammarDefinitions = null;
		this._grammarFactory = null;
65 66 67
		this._tokenizersRegistrations = [];

		grammarsExtPoint.setHandler((extensions) => {
A
Alex Dima 已提交
68 69 70 71
			this._grammarDefinitions = null;
			if (this._grammarFactory) {
				this._grammarFactory.dispose();
				this._grammarFactory = null;
72
				this._onDidDisposeGrammarFactory();
73
			}
A
Alex Dima 已提交
74
			this._tokenizersRegistrations = dispose(this._tokenizersRegistrations);
75

A
Alex Dima 已提交
76
			this._grammarDefinitions = [];
77
			for (const extension of extensions) {
A
Alex Dima 已提交
78
				const grammars = extension.value;
79
				for (const grammar of grammars) {
A
Alex Dima 已提交
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
					if (!this._validateGrammarExtensionPoint(extension.description.extensionLocation, grammar, extension.collector)) {
						continue;
					}
					const grammarLocation = resources.joinPath(extension.description.extensionLocation, grammar.path);

					const embeddedLanguages: IValidEmbeddedLanguagesMap = Object.create(null);
					if (grammar.embeddedLanguages) {
						let scopes = Object.keys(grammar.embeddedLanguages);
						for (let i = 0, len = scopes.length; i < len; i++) {
							let scope = scopes[i];
							let language = grammar.embeddedLanguages[scope];
							if (typeof language !== 'string') {
								// never hurts to be too careful
								continue;
							}
							let languageIdentifier = this._modeService.getLanguageIdentifier(language);
							if (languageIdentifier) {
								embeddedLanguages[scope] = languageIdentifier.id;
							}
						}
					}

					const tokenTypes: IValidTokenTypeMap = Object.create(null);
					if (grammar.tokenTypes) {
						const scopes = Object.keys(grammar.tokenTypes);
						for (const scope of scopes) {
							const tokenType = grammar.tokenTypes[scope];
							switch (tokenType) {
								case 'string':
									tokenTypes[scope] = StandardTokenType.String;
									break;
								case 'other':
									tokenTypes[scope] = StandardTokenType.Other;
									break;
								case 'comment':
									tokenTypes[scope] = StandardTokenType.Comment;
									break;
							}
						}
					}

					let languageIdentifier: LanguageIdentifier | null = null;
					if (grammar.language) {
						languageIdentifier = this._modeService.getLanguageIdentifier(grammar.language);
					}

					this._grammarDefinitions.push({
						location: grammarLocation,
						language: languageIdentifier ? languageIdentifier.id : undefined,
						scopeName: grammar.scopeName,
						embeddedLanguages: embeddedLanguages,
						tokenTypes: tokenTypes,
						injectTo: grammar.injectTo,
					});
134 135 136 137 138 139 140 141
				}
			}

			for (const createMode of this._createdModes) {
				this._registerDefinitionIfAvailable(createMode);
			}
		});

A
Alex Dima 已提交
142 143 144 145 146 147
		this._register(this._themeService.onDidColorThemeChange(() => {
			if (this._grammarFactory) {
				this._updateTheme(this._grammarFactory, this._themeService.getColorTheme(), false);
			}
		}));

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
		// Generate some color map until the grammar registry is loaded
		let colorTheme = this._themeService.getColorTheme();
		let defaultForeground: Color = Color.transparent;
		let defaultBackground: Color = Color.transparent;
		for (let i = 0, len = colorTheme.tokenColors.length; i < len; i++) {
			let rule = colorTheme.tokenColors[i];
			if (!rule.scope && rule.settings) {
				if (rule.settings.foreground) {
					defaultForeground = Color.fromHex(rule.settings.foreground);
				}
				if (rule.settings.background) {
					defaultBackground = Color.fromHex(rule.settings.background);
				}
			}
		}
		TokenizationRegistry.setColorMap([null!, defaultForeground, defaultBackground]);

		this._modeService.onDidCreateMode((mode) => {
			let modeId = mode.getId();
			this._createdModes.push(modeId);
			this._registerDefinitionIfAvailable(modeId);
		});
	}

A
Alex Dima 已提交
172 173 174
	private _canCreateGrammarFactory(): boolean {
		// Check if extension point is ready
		return (this._grammarDefinitions ? true : false);
175 176
	}

A
Alex Dima 已提交
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
	private async _getOrCreateGrammarFactory(): Promise<TMGrammarFactory> {
		if (this._grammarFactory) {
			return this._grammarFactory;
		}

		const vscodeTextmate = await this._loadVSCodeTextmate();

		// Avoid duplicate instantiations
		if (this._grammarFactory) {
			return this._grammarFactory;
		}

		this._grammarFactory = new TMGrammarFactory({
			logTrace: (msg: string) => this._logService.trace(msg),
			logError: (msg: string, err: any) => this._logService.error(msg, err),
			readFile: async (resource: URI) => {
				const content = await this._fileService.readFile(resource);
				return content.value.toString();
195
			}
A
Alex Dima 已提交
196
		}, this._grammarDefinitions || [], vscodeTextmate, this._loadOnigLib());
197
		this._onDidCreateGrammarFactory(this._grammarDefinitions || []);
A
Alex Dima 已提交
198 199

		this._updateTheme(this._grammarFactory, this._themeService.getColorTheme(), true);
200

A
Alex Dima 已提交
201
		return this._grammarFactory;
202 203
	}

A
Alex Dima 已提交
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
	private async _registerDefinitionIfAvailable(modeId: string): Promise<void> {
		const languageIdentifier = this._modeService.getLanguageIdentifier(modeId);
		if (!languageIdentifier) {
			return;
		}
		const languageId = languageIdentifier.id;
		try {
			if (!this._canCreateGrammarFactory()) {
				return;
			}
			const grammarFactory = await this._getOrCreateGrammarFactory();
			if (grammarFactory.has(languageId)) {
				const promise = grammarFactory.createGrammar(languageId).then((r) => {
					const tokenization = new TMTokenization(r.grammar, r.initialState, r.containsEmbeddedLanguages);
					tokenization.onDidEncounterLanguage((languageId) => {
						if (!this._encounteredLanguages[languageId]) {
							this._encounteredLanguages[languageId] = true;
							this._onDidEncounterLanguage.fire(languageId);
						}
					});
P
Peng Lyu 已提交
224
					return new TMTokenizationSupport(r.languageId, tokenization, this._notificationService, this._configurationService, this._storageService);
A
Alex Dima 已提交
225 226 227 228 229 230 231 232
				}, e => {
					onUnexpectedError(e);
					return null;
				});
				this._tokenizersRegistrations.push(TokenizationRegistry.registerPromise(modeId, promise));
			}
		} catch (err) {
			onUnexpectedError(err);
233 234 235 236 237 238 239 240 241 242 243
		}
	}

	private static _toColorMap(colorMap: string[]): Color[] {
		let result: Color[] = [null!];
		for (let i = 1, len = colorMap.length; i < len; i++) {
			result[i] = Color.fromHex(colorMap[i]);
		}
		return result;
	}

A
Alex Dima 已提交
244 245
	private _updateTheme(grammarFactory: TMGrammarFactory, colorTheme: IColorTheme, forceUpdate: boolean): void {
		if (!forceUpdate && AbstractTextMateService.equalsTokenRules(this._currentTokenColors, colorTheme.tokenColors)) {
246 247
			return;
		}
A
Alex Dima 已提交
248
		this._currentTokenColors = colorTheme.tokenColors;
249 250
		this._doUpdateTheme(grammarFactory, { name: colorTheme.label, settings: colorTheme.tokenColors });
	}
A
Alex Dima 已提交
251

252 253
	protected _doUpdateTheme(grammarFactory: TMGrammarFactory, theme: IRawTheme): void {
		grammarFactory.setTheme(theme);
A
Alex Dima 已提交
254
		let colorMap = AbstractTextMateService._toColorMap(grammarFactory.getColorMap());
255 256 257 258 259
		let cssRules = generateTokensCSSForColorMap(colorMap);
		this._styleElement.innerHTML = cssRules;
		TokenizationRegistry.setColorMap(colorMap);
	}

A
Alex Dima 已提交
260 261 262
	private static equalsTokenRules(a: ITokenColorizationRule[] | null, b: ITokenColorizationRule[] | null): boolean {
		if (!b || !a || b.length !== a.length) {
			return false;
263
		}
A
Alex Dima 已提交
264 265 266
		for (let i = b.length - 1; i >= 0; i--) {
			let r1 = b[i];
			let r2 = a[i];
267
			if (r1.scope !== r2.scope) {
A
Alex Dima 已提交
268
				return false;
269 270 271 272 273
			}
			let s1 = r1.settings;
			let s2 = r2.settings;
			if (s1 && s2) {
				if (s1.fontStyle !== s2.fontStyle || s1.foreground !== s2.foreground || s1.background !== s2.background) {
A
Alex Dima 已提交
274
					return false;
275 276
				}
			} else if (!s1 || !s2) {
A
Alex Dima 已提交
277
				return false;
278 279
			}
		}
A
Alex Dima 已提交
280
		return true;
281 282
	}

A
Alex Dima 已提交
283
	private _validateGrammarExtensionPoint(extensionLocation: URI, syntax: ITMSyntaxExtensionPoint, collector: ExtensionMessageCollector): boolean {
284 285
		if (syntax.language && ((typeof syntax.language !== 'string') || !this._modeService.isRegisteredMode(syntax.language))) {
			collector.error(nls.localize('invalid.language', "Unknown language in `contributes.{0}.language`. Provided value: {1}", grammarsExtPoint.name, String(syntax.language)));
A
Alex Dima 已提交
286
			return false;
287 288 289
		}
		if (!syntax.scopeName || (typeof syntax.scopeName !== 'string')) {
			collector.error(nls.localize('invalid.scopeName', "Expected string in `contributes.{0}.scopeName`. Provided value: {1}", grammarsExtPoint.name, String(syntax.scopeName)));
A
Alex Dima 已提交
290
			return false;
291 292 293
		}
		if (!syntax.path || (typeof syntax.path !== 'string')) {
			collector.error(nls.localize('invalid.path.0', "Expected string in `contributes.{0}.path`. Provided value: {1}", grammarsExtPoint.name, String(syntax.path)));
A
Alex Dima 已提交
294
			return false;
295 296 297
		}
		if (syntax.injectTo && (!Array.isArray(syntax.injectTo) || syntax.injectTo.some(scope => typeof scope !== 'string'))) {
			collector.error(nls.localize('invalid.injectTo', "Invalid value in `contributes.{0}.injectTo`. Must be an array of language scope names. Provided value: {1}", grammarsExtPoint.name, JSON.stringify(syntax.injectTo)));
A
Alex Dima 已提交
298
			return false;
299 300 301
		}
		if (syntax.embeddedLanguages && !types.isObject(syntax.embeddedLanguages)) {
			collector.error(nls.localize('invalid.embeddedLanguages', "Invalid value in `contributes.{0}.embeddedLanguages`. Must be an object map from scope name to language. Provided value: {1}", grammarsExtPoint.name, JSON.stringify(syntax.embeddedLanguages)));
A
Alex Dima 已提交
302
			return false;
303 304 305 306
		}

		if (syntax.tokenTypes && !types.isObject(syntax.tokenTypes)) {
			collector.error(nls.localize('invalid.tokenTypes', "Invalid value in `contributes.{0}.tokenTypes`. Must be an object map from scope name to token type. Provided value: {1}", grammarsExtPoint.name, JSON.stringify(syntax.tokenTypes)));
A
Alex Dima 已提交
307
			return false;
308 309 310 311 312 313
		}

		const grammarLocation = resources.joinPath(extensionLocation, syntax.path);
		if (!resources.isEqualOrParent(grammarLocation, extensionLocation)) {
			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.", grammarsExtPoint.name, grammarLocation.path, extensionLocation.path));
		}
A
Alex Dima 已提交
314 315 316 317 318 319 320 321 322
		return true;
	}

	public async createGrammar(modeId: string): Promise<IGrammar> {
		const grammarFactory = await this._getOrCreateGrammarFactory();
		const { grammar } = await grammarFactory.createGrammar(this._modeService.getLanguageIdentifier(modeId)!.id);
		return grammar;
	}

323 324 325 326 327 328
	protected _onDidCreateGrammarFactory(grammarDefinitions: IValidGrammarDefinition[]): void {
	}

	protected _onDidDisposeGrammarFactory(): void {
	}

A
Alex Dima 已提交
329 330 331 332
	protected abstract _loadVSCodeTextmate(): Promise<typeof import('vscode-textmate')>;
	protected abstract _loadOnigLib(): Promise<IOnigLib> | undefined;
}

P
Peng Lyu 已提交
333 334
const donotAskUpdateKey = 'editor.maxTokenizationLineLength.donotask';

335
class TMTokenizationSupport implements ITokenizationSupport {
336
	private readonly _languageId: LanguageId;
337
	private readonly _actual: TMTokenization;
338
	private _tokenizationWarningAlreadyShown: boolean;
339
	private _maxTokenizationLineLength: number;
340

341 342 343 344
	constructor(
		languageId: LanguageId,
		actual: TMTokenization,
		@INotificationService private readonly _notificationService: INotificationService,
P
Peng Lyu 已提交
345 346
		@IConfigurationService private readonly _configurationService: IConfigurationService,
		@IStorageService private readonly _storageService: IStorageService
347
	) {
348
		this._languageId = languageId;
349
		this._actual = actual;
P
Peng Lyu 已提交
350
		this._tokenizationWarningAlreadyShown = !!(this._storageService.getBoolean(donotAskUpdateKey, StorageScope.GLOBAL));
351 352
		this._maxTokenizationLineLength = this._configurationService.getValue<number>('editor.maxTokenizationLineLength');
		this._configurationService.onDidChangeConfiguration(e => {
353
			if (e.affectsConfiguration('editor.maxTokenizationLineLength')) {
354
				this._maxTokenizationLineLength = this._configurationService.getValue<number>('editor.maxTokenizationLineLength');
355 356
			}
		});
357 358
	}

359 360
	getInitialState(): IState {
		return this._actual.getInitialState();
361 362
	}

363
	tokenize(line: string, state: IState, offsetDelta: number): TokenizationResult {
364 365 366
		throw new Error('Not supported!');
	}

367
	tokenize2(line: string, state: StackElement, offsetDelta: number): TokenizationResult2 {
368 369 370 371 372 373 374 375
		if (offsetDelta !== 0) {
			throw new Error('Unexpected: offsetDelta should be 0.');
		}

		// Do not attempt to tokenize if a line is too long
		if (line.length >= this._maxTokenizationLineLength) {
			if (!this._tokenizationWarningAlreadyShown) {
				this._tokenizationWarningAlreadyShown = true;
P
Peng Lyu 已提交
376 377 378 379 380 381 382 383 384
				this._notificationService.prompt(
					Severity.Warning,
					nls.localize('too many characters', "Tokenization is skipped for long lines for performance reasons. The length of a long line can be configured via `editor.maxTokenizationLineLength`."),
					[{
						label: nls.localize('neverAgain', "Don't Show Again"),
						isSecondary: true,
						run: () => this._storageService.store(donotAskUpdateKey, true, StorageScope.GLOBAL)
					}]
				);
385 386 387 388 389
			}
			console.log(`Line (${line.substr(0, 15)}...): longer than ${this._maxTokenizationLineLength} characters, tokenization skipped.`);
			return nullTokenize2(this._languageId, line, state, offsetDelta);
		}

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
		return this._actual.tokenize2(line, state);
	}
}

class TMTokenization extends Disposable {

	private readonly _grammar: IGrammar;
	private readonly _containsEmbeddedLanguages: boolean;
	private readonly _seenLanguages: boolean[];
	private readonly _initialState: StackElement;

	private readonly _onDidEncounterLanguage: Emitter<LanguageId> = this._register(new Emitter<LanguageId>());
	public readonly onDidEncounterLanguage: Event<LanguageId> = this._onDidEncounterLanguage.event;

	constructor(grammar: IGrammar, initialState: StackElement, containsEmbeddedLanguages: boolean) {
		super();
		this._grammar = grammar;
		this._initialState = initialState;
		this._containsEmbeddedLanguages = containsEmbeddedLanguages;
		this._seenLanguages = [];
	}

	public getInitialState(): IState {
		return this._initialState;
	}

	public tokenize2(line: string, state: StackElement): TokenizationResult2 {
417 418 419 420 421 422 423 424 425 426 427 428 429
		let textMateResult = this._grammar.tokenizeLine2(line, state);

		if (this._containsEmbeddedLanguages) {
			let seenLanguages = this._seenLanguages;
			let tokens = textMateResult.tokens;

			// Must check if any of the embedded languages was hit
			for (let i = 0, len = (tokens.length >>> 1); i < len; i++) {
				let metadata = tokens[(i << 1) + 1];
				let languageId = TokenMetadata.getLanguageId(metadata);

				if (!seenLanguages[languageId]) {
					seenLanguages[languageId] = true;
430
					this._onDidEncounterLanguage.fire(languageId);
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
				}
			}
		}

		let endState: StackElement;
		// try to save an object if possible
		if (state.equals(textMateResult.ruleStack)) {
			endState = state;
		} else {
			endState = textMateResult.ruleStack;

		}

		return new TokenizationResult2(textMateResult.tokens, endState);
	}
}