emmetActions.ts 14.0 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
/// <reference path="emmet.d.ts" />
'use strict';

J
Johannes Rieken 已提交
8
import { TPromise } from 'vs/base/common/winjs.base';
9
import { ICommonCodeEditor } from 'vs/editor/common/editorCommon';
10
import { EditorAction, ServicesAccessor, IActionOptions, ICommandKeybindingsOptions } from 'vs/editor/common/editorCommonExtensions';
J
Johannes Rieken 已提交
11
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
12
import { grammarsExtPoint, ITMSyntaxExtensionPoint } from 'vs/workbench/services/textMate/electron-browser/TMGrammars';
A
Alex Dima 已提交
13
import { IModeService } from 'vs/editor/common/services/modeService';
14
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
15
import { EditorAccessor, IGrammarContributions } from 'vs/workbench/parts/emmet/electron-browser/editorAccessor';
J
Johannes Rieken 已提交
16
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
17
import { IExtensionService, ExtensionPointContribution } from 'vs/platform/extensions/common/extensions';
18
import { IMessageService } from 'vs/platform/message/common/message';
19
import * as emmet from 'emmet';
20 21 22
import * as path from 'path';
import * as pfs from 'vs/base/node/pfs';
import Severity from 'vs/base/common/severity';
23
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
24
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
25
import { ICommandService } from 'vs/platform/commands/common/commands';
E
Erich Gamma 已提交
26

E
Erich Gamma 已提交
27 28 29 30
interface IEmmetConfiguration {
	emmet: {
		preferences: any;
		syntaxProfiles: any;
31
		triggerExpansionOnTab: boolean,
32
		excludeLanguages: string[],
33
		extensionsPath: string,
R
Ramya Achutha Rao 已提交
34
		useNewEmmet: boolean
E
Erich Gamma 已提交
35 36 37
	};
}

E
Erich Gamma 已提交
38 39 40 41 42 43 44 45
interface ModeScopeMap {
	[key: string]: string;
}

class GrammarContributions implements IGrammarContributions {

	private static _grammars: ModeScopeMap = null;

46
	constructor(contributions: ExtensionPointContribution<ITMSyntaxExtensionPoint[]>[]) {
E
Erich Gamma 已提交
47
		if (GrammarContributions._grammars === null) {
48
			this.fillModeScopeMap(contributions);
E
Erich Gamma 已提交
49 50 51
		}
	}

52
	private fillModeScopeMap(contributions: ExtensionPointContribution<ITMSyntaxExtensionPoint[]>[]) {
E
Erich Gamma 已提交
53
		GrammarContributions._grammars = {};
54 55 56 57
		contributions.forEach((contribution) => {
			contribution.value.forEach((grammar) => {
				if (grammar.language && grammar.scopeName) {
					GrammarContributions._grammars[grammar.language] = grammar.scopeName;
E
Erich Gamma 已提交
58
				}
59
			});
E
Erich Gamma 已提交
60 61 62 63 64 65 66 67
		});
	}

	public getGrammar(mode): string {
		return GrammarContributions._grammars[mode];
	}
}

A
Alex Dima 已提交
68
class LazyEmmet {
E
Erich Gamma 已提交
69

A
Alex Dima 已提交
70
	private static _INSTANCE = new LazyEmmet();
71 72 73 74
	private static extensionsPath = '';
	private static snippetsFromFile = {};
	private static syntaxProfilesFromFile = {};
	private static preferencesFromFile = {};
75
	private static workspaceRoot = '';
76
	private static emmetSupportedModes: string[];
77 78 79

	public static withConfiguredEmmet(configurationService: IConfigurationService,
		messageService: IMessageService,
80 81
		telemetryService: ITelemetryService,
		emmetSupportedModes: string[],
82
		workspaceRoot: string,
83
		callback: (_emmet: typeof emmet) => void): TPromise<void> {
84
		LazyEmmet.workspaceRoot = workspaceRoot;
85 86
		LazyEmmet.emmetSupportedModes = emmetSupportedModes;
		return LazyEmmet._INSTANCE.withEmmetPreferences(configurationService, messageService, telemetryService, callback);
A
Alex Dima 已提交
87 88 89
	}

	private _emmetPromise: TPromise<typeof emmet>;
90
	private _messageService: IMessageService;
A
Alex Dima 已提交
91 92 93 94 95

	constructor() {
		this._emmetPromise = null;
	}

96 97
	public withEmmetPreferences(configurationService: IConfigurationService,
		messageService: IMessageService,
98
		telemetryService: ITelemetryService,
99
		callback: (_emmet: typeof emmet) => void): TPromise<void> {
A
Alex Dima 已提交
100
		return this._loadEmmet().then((_emmet: typeof emmet) => {
101
			this._messageService = messageService;
102
			this._withEmmetPreferences(configurationService, telemetryService, _emmet, callback);
103 104
		}, (e) => {
			callback(null);
A
Alex Dima 已提交
105 106 107 108 109 110 111 112 113 114
		});
	}

	private _loadEmmet(): TPromise<typeof emmet> {
		if (!this._emmetPromise) {
			this._emmetPromise = new TPromise<typeof emmet>((c, e) => {
				require(['emmet'], c, e);
			});
		}
		return this._emmetPromise;
115 116
	}

117 118 119
	private updateEmmetPreferences(configurationService: IConfigurationService,
		telemetryService: ITelemetryService,
		_emmet: typeof emmet): TPromise<any> {
120
		let emmetPreferences = configurationService.getConfiguration<IEmmetConfiguration>().emmet;
121
		let loadEmmetSettings = () => {
122 123
			let syntaxProfiles = { ...LazyEmmet.syntaxProfilesFromFile, ...emmetPreferences.syntaxProfiles };
			let preferences = { ...LazyEmmet.preferencesFromFile, ...emmetPreferences.preferences };
124
			let snippets = LazyEmmet.snippetsFromFile;
125 126 127 128 129 130 131 132 133
			let mappedModes = [];
			let outputProfileFromSettings = false;
			for (let key in emmetPreferences.syntaxProfiles) {
				if (LazyEmmet.emmetSupportedModes.indexOf(key) === -1) {
					mappedModes.push(key);
				} else {
					outputProfileFromSettings = true;
				}
			}
134 135 136 137 138

			try {
				_emmet.loadPreferences(preferences);
				_emmet.loadProfiles(syntaxProfiles);
				_emmet.loadSnippets(snippets);
139 140 141 142 143 144 145 146 147 148

				let emmetCustomizationTelemetry = {
					emmetPreferencesFromFile: Object.keys(LazyEmmet.preferencesFromFile).length > 0,
					emmetSyntaxProfilesFromFile: Object.keys(LazyEmmet.syntaxProfilesFromFile).length > 0,
					emmetSnippetsFromFile: Object.keys(LazyEmmet.snippetsFromFile).length > 0,
					emmetPreferencesFromSettings: Object.keys(emmetPreferences.preferences).length > 0,
					emmetSyntaxProfilesFromSettings: outputProfileFromSettings,
					emmetMappedModes: mappedModes
				};
				telemetryService.publicLog('emmetCustomizations', emmetCustomizationTelemetry);
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
			} catch (err) {
				// ignore
			}
		};

		// Whether loading the files was a success or not, we load emmet with what we have
		return this.updateFromExtensionsPath(emmetPreferences.extensionsPath).then(loadEmmetSettings, (err) => {
			// Errors from all the promises used to fetch/read dir/files would bubble up here
			console.log(err);
			loadEmmetSettings();
		});
	}

	private updateFromExtensionsPath(extPath: string): TPromise<any> {
		if (extPath !== LazyEmmet.extensionsPath) {
			LazyEmmet.extensionsPath = extPath;
			LazyEmmet.snippetsFromFile = {};
			LazyEmmet.preferencesFromFile = {};
			LazyEmmet.syntaxProfilesFromFile = {};

			if (extPath && extPath.trim()) {
170 171 172 173 174
				let dirPath = path.isAbsolute(extPath) ? extPath : path.join(LazyEmmet.workspaceRoot, extPath);
				let snippetsPath = path.join(dirPath, 'snippets.json');
				let syntaxProfilesPath = path.join(dirPath, 'syntaxProfiles.json');
				let preferencesPath = path.join(dirPath, 'preferences.json');
				return pfs.dirExists(dirPath).then(exists => {
175
					if (exists) {
176 177 178
						let snippetsPromise = this.getEmmetCustomization(snippetsPath).then(value => LazyEmmet.snippetsFromFile = value);
						let profilesPromise = this.getEmmetCustomization(syntaxProfilesPath).then(value => LazyEmmet.syntaxProfilesFromFile = value);
						let preferencesPromise = this.getEmmetCustomization(preferencesPath).then(value => LazyEmmet.preferencesFromFile = value);
179 180 181 182

						return TPromise.join([snippetsPromise, profilesPromise, preferencesPromise]);
					}
					this._messageService.show(Severity.Error, `The path set in emmet.extensionsPath "${LazyEmmet.extensionsPath}" does not exist.`);
M
Matt Bierner 已提交
183
					return undefined;
184 185
				});
			}
E
Erich Gamma 已提交
186
		}
187
		return TPromise.as(void 0);
188 189
	}

190
	private getEmmetCustomization(filePath: string): TPromise<any> {
191 192 193 194 195 196 197 198 199 200 201 202 203 204
		return pfs.fileExists(filePath).then(fileExists => {
			if (fileExists) {
				return pfs.readFile(filePath).then(buff => {
					let parsedData = {};
					try {
						parsedData = JSON.parse(buff.toString());
					} catch (err) {
						this._messageService.show(Severity.Error, `Error while parsing "${filePath}": ${err}`);
					}
					return parsedData;
				});
			}
			return {};
		});
205 206
	}

207 208 209 210 211
	private _withEmmetPreferences(configurationService: IConfigurationService,
		telemetryService: ITelemetryService,
		_emmet: typeof emmet,
		callback: (_emmet: typeof emmet) => void): void {
		this.updateEmmetPreferences(configurationService, telemetryService, _emmet).then(() => {
212 213 214 215 216 217
			try {
				callback(_emmet);
			} finally {
				_emmet.resetUserData();
			}
		});
A
Alex Dima 已提交
218 219 220
	}
}

A
Alex Dima 已提交
221 222 223 224
export class EmmetActionContext {
	editor: ICommonCodeEditor;
	emmet: typeof emmet;
	editorAccessor: EditorAccessor;
A
Alex Dima 已提交
225

A
Alex Dima 已提交
226 227 228 229 230 231 232
	constructor(editor: ICommonCodeEditor, _emmet: typeof emmet, editorAccessor: EditorAccessor) {
		this.editor = editor;
		this.emmet = _emmet;
		this.editorAccessor = editorAccessor;
	}
}

233 234 235 236
export interface IEmmetActionOptions extends IActionOptions {
	actionName: string;
}

A
Alex Dima 已提交
237
export abstract class EmmetEditorAction extends EditorAction {
A
Alex Dima 已提交
238

239 240 241 242 243
	private actionMap = {
		'editor.emmet.action.removeTag': 'emmet.removeTag',
		'editor.emmet.action.updateTag': 'emmet.updateTag',
		'editor.emmet.action.matchingPair': 'emmet.matchTag',
		'editor.emmet.action.wrapWithAbbreviation': 'emmet.wrapWithAbbreviation',
244 245 246 247 248 249 250 251
		'editor.emmet.action.expandAbbreviation': 'emmet.expandAbbreviation',
		'editor.emmet.action.balanceInward': 'emmet.balanceIn',
		'editor.emmet.action.balanceOutward': 'emmet.balanceOut',
		'editor.emmet.action.previousEditPoint': 'emmet.prevEditPoint',
		'editor.emmet.action.nextEditPoint': 'emmet.nextEditPoint',
		'editor.emmet.action.mergeLines': 'emmet.mergeLines',
		'editor.emmet.action.selectPreviousItem': 'emmet.selectPrevItem',
		'editor.emmet.action.selectNextItem': 'emmet.selectNextItem',
252
		'editor.emmet.action.splitJoinTag': 'emmet.splitJoinTag',
253
		'editor.emmet.action.toggleComment': 'emmet.toggleComment',
254 255 256 257 258 259 260
		'editor.emmet.action.evaluateMath': 'emmet.evaluateMathExpression',
		'editor.emmet.action.incrementNumberByOneTenth': 'emmet.incrementNumberByOneTenth',
		'editor.emmet.action.incrementNumberByOne': 'emmet.incrementNumberByOne',
		'editor.emmet.action.incrementNumberByTen': 'emmet.incrementNumberByTen',
		'editor.emmet.action.decrementNumberByOneTenth': 'emmet.decrementNumberByOneTenth',
		'editor.emmet.action.decrementNumberByOne': 'emmet.decrementNumberByOne',
		'editor.emmet.action.decrementNumberByTen': 'emmet.decrementNumberByTen'
261 262
	};

263 264 265 266 267 268 269
	protected emmetActionName: string;

	constructor(opts: IEmmetActionOptions) {
		super(opts);
		this.emmetActionName = opts.actionName;
	}

E
Erich Gamma 已提交
270
	abstract runEmmetAction(accessor: ServicesAccessor, ctx: EmmetActionContext);
271

E
Erich Gamma 已提交
272
	protected noExpansionOccurred(editor: ICommonCodeEditor) {
E
Erich Gamma 已提交
273 274 275
		// default do nothing
	}

276 277 278 279 280
	private _lastGrammarContributions: TPromise<GrammarContributions> = null;
	private _lastExtensionService: IExtensionService = null;
	private _withGrammarContributions(extensionService: IExtensionService): TPromise<GrammarContributions> {
		if (this._lastExtensionService !== extensionService) {
			this._lastExtensionService = extensionService;
281 282
			this._lastGrammarContributions = extensionService.readExtensionPointContributions(grammarsExtPoint).then((contributions) => {
				return new GrammarContributions(contributions);
283 284 285 286 287
			});
		}
		return this._lastGrammarContributions;
	}

E
Erich Gamma 已提交
288
	public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): TPromise<void> {
A
Alex Dima 已提交
289 290
		const configurationService = accessor.get(IConfigurationService);
		const instantiationService = accessor.get(IInstantiationService);
291
		const extensionService = accessor.get(IExtensionService);
A
Alex Dima 已提交
292
		const modeService = accessor.get(IModeService);
293
		const messageService = accessor.get(IMessageService);
294
		const contextService = accessor.get(IWorkspaceContextService);
B
Benjamin Pasero 已提交
295
		const workspaceRoot = contextService.hasWorkspace() ? contextService.getWorkspace().resource.fsPath : ''; // TODO@Ramya (https://github.com/Microsoft/vscode/issues/29244)
296
		const telemetryService = accessor.get(ITelemetryService);
297 298
		const commandService = accessor.get(ICommandService);

299
		let mappedCommand = this.actionMap[this.id];
300
		if (mappedCommand && mappedCommand !== 'emmet.expandAbbreviation' && mappedCommand !== 'emmet.wrapWithAbbreviation') {
301 302
			return commandService.executeCommand<void>(mappedCommand);
		}
A
Alex Dima 已提交
303

304
		return this._withGrammarContributions(extensionService).then((grammarContributions) => {
E
Erich Gamma 已提交
305

306
			let editorAccessor = new EditorAccessor(
A
Alex Dima 已提交
307
				modeService,
308 309 310
				editor,
				configurationService.getConfiguration<IEmmetConfiguration>().emmet.syntaxProfiles,
				configurationService.getConfiguration<IEmmetConfiguration>().emmet.excludeLanguages,
311 312
				grammarContributions,
				this.emmetActionName
313 314
			);

315 316
			if (configurationService.getConfiguration<IEmmetConfiguration>().emmet.useNewEmmet
				&& (mappedCommand === 'emmet.expandAbbreviation' || mappedCommand === 'emmet.wrapWithAbbreviation')) {
317
				return commandService.executeCommand<void>(mappedCommand, editorAccessor.getLanguage());
318 319
			}

320 321
			if (!editorAccessor.isEmmetEnabledMode()) {
				this.noExpansionOccurred(editor);
M
Matt Bierner 已提交
322
				return undefined;
323
			}
324

325
			return LazyEmmet.withConfiguredEmmet(configurationService, messageService, telemetryService, editorAccessor.getEmmetSupportedModes(), workspaceRoot, (_emmet) => {
326 327 328 329
				if (!_emmet) {
					this.noExpansionOccurred(editor);
					return undefined;
				}
330 331 332 333 334
				editorAccessor.onBeforeEmmetAction();
				instantiationService.invokeFunction((accessor) => {
					this.runEmmetAction(accessor, new EmmetActionContext(editor, _emmet, editorAccessor));
				});
				editorAccessor.onAfterEmmetAction();
A
Alex Dima 已提交
335
			});
E
Erich Gamma 已提交
336
		});
337

E
Erich Gamma 已提交
338
	}
339
}
E
Erich Gamma 已提交
340 341 342

export class BasicEmmetEditorAction extends EmmetEditorAction {

E
Erich Gamma 已提交
343
	constructor(id: string, label: string, alias: string, actionName: string, kbOpts?: ICommandKeybindingsOptions) {
344
		super({
345 346 347
			id,
			label,
			alias,
348
			precondition: EditorContextKeys.writable,
349 350
			kbOpts,
			actionName
351
		});
E
Erich Gamma 已提交
352 353
	}

E
Erich Gamma 已提交
354
	public runEmmetAction(accessor: ServicesAccessor, ctx: EmmetActionContext) {
355
		const telemetryService = accessor.get(ITelemetryService);
356 357 358
		try {
			if (!ctx.emmet.run(this.emmetActionName, ctx.editorAccessor)) {
				this.noExpansionOccurred(ctx.editor);
359 360
			} else if (this.emmetActionName === 'expand_abbreviation') {
				telemetryService.publicLog('emmetActionSucceeded', { action: this.emmetActionName });
361 362
			}
		} catch (err) {
A
Alex Dima 已提交
363
			this.noExpansionOccurred(ctx.editor);
E
Erich Gamma 已提交
364
		}
365

E
Erich Gamma 已提交
366 367
	}
}