/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /// 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICommonCodeEditor } from 'vs/editor/common/editorCommon'; import { EditorAction, ServicesAccessor, IActionOptions, ICommandKeybindingsOptions } from 'vs/editor/common/editorCommonExtensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { grammarsExtPoint, ITMSyntaxExtensionPoint } from 'vs/workbench/services/textMate/electron-browser/TMGrammars'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EditorAccessor, IGrammarContributions } from 'vs/workbench/parts/emmet/electron-browser/editorAccessor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionService, ExtensionPointContribution } from 'vs/platform/extensions/common/extensions'; import { IMessageService } from 'vs/platform/message/common/message'; import * as emmet from 'emmet'; import * as path from 'path'; import * as pfs from 'vs/base/node/pfs'; import Severity from 'vs/base/common/severity'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; interface IEmmetConfiguration { emmet: { preferences: any; syntaxProfiles: any; triggerExpansionOnTab: boolean, excludeLanguages: string[], extensionsPath: string, useNewEmmet: boolean }; } interface ModeScopeMap { [key: string]: string; } class GrammarContributions implements IGrammarContributions { private static _grammars: ModeScopeMap = null; constructor(contributions: ExtensionPointContribution[]) { if (GrammarContributions._grammars === null) { this.fillModeScopeMap(contributions); } } private fillModeScopeMap(contributions: ExtensionPointContribution[]) { GrammarContributions._grammars = {}; contributions.forEach((contribution) => { contribution.value.forEach((grammar) => { if (grammar.language && grammar.scopeName) { GrammarContributions._grammars[grammar.language] = grammar.scopeName; } }); }); } public getGrammar(mode): string { return GrammarContributions._grammars[mode]; } } class LazyEmmet { private static _INSTANCE = new LazyEmmet(); private static extensionsPath = ''; private static snippetsFromFile = {}; private static syntaxProfilesFromFile = {}; private static preferencesFromFile = {}; private static workspaceRoot = ''; private static emmetSupportedModes: string[]; public static withConfiguredEmmet(configurationService: IConfigurationService, messageService: IMessageService, telemetryService: ITelemetryService, emmetSupportedModes: string[], workspaceRoot: string, callback: (_emmet: typeof emmet) => void): TPromise { LazyEmmet.workspaceRoot = workspaceRoot; LazyEmmet.emmetSupportedModes = emmetSupportedModes; return LazyEmmet._INSTANCE.withEmmetPreferences(configurationService, messageService, telemetryService, callback); } private _emmetPromise: TPromise; private _messageService: IMessageService; constructor() { this._emmetPromise = null; } public withEmmetPreferences(configurationService: IConfigurationService, messageService: IMessageService, telemetryService: ITelemetryService, callback: (_emmet: typeof emmet) => void): TPromise { return this._loadEmmet().then((_emmet: typeof emmet) => { this._messageService = messageService; this._withEmmetPreferences(configurationService, telemetryService, _emmet, callback); }, (e) => { callback(null); }); } private _loadEmmet(): TPromise { if (!this._emmetPromise) { this._emmetPromise = new TPromise((c, e) => { require(['emmet'], c, e); }); } return this._emmetPromise; } private updateEmmetPreferences(configurationService: IConfigurationService, telemetryService: ITelemetryService, _emmet: typeof emmet): TPromise { let emmetPreferences = configurationService.getConfiguration().emmet; let loadEmmetSettings = () => { let syntaxProfiles = { ...LazyEmmet.syntaxProfilesFromFile, ...emmetPreferences.syntaxProfiles }; let preferences = { ...LazyEmmet.preferencesFromFile, ...emmetPreferences.preferences }; let snippets = LazyEmmet.snippetsFromFile; let mappedModes = []; let outputProfileFromSettings = false; for (let key in emmetPreferences.syntaxProfiles) { if (LazyEmmet.emmetSupportedModes.indexOf(key) === -1) { mappedModes.push(key); } else { outputProfileFromSettings = true; } } try { _emmet.loadPreferences(preferences); _emmet.loadProfiles(syntaxProfiles); _emmet.loadSnippets(snippets); 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); } 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 { if (extPath !== LazyEmmet.extensionsPath) { LazyEmmet.extensionsPath = extPath; LazyEmmet.snippetsFromFile = {}; LazyEmmet.preferencesFromFile = {}; LazyEmmet.syntaxProfilesFromFile = {}; if (extPath && extPath.trim()) { 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 => { if (exists) { 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); return TPromise.join([snippetsPromise, profilesPromise, preferencesPromise]); } this._messageService.show(Severity.Error, `The path set in emmet.extensionsPath "${LazyEmmet.extensionsPath}" does not exist.`); return undefined; }); } } return TPromise.as(void 0); } private getEmmetCustomization(filePath: string): TPromise { 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 {}; }); } private _withEmmetPreferences(configurationService: IConfigurationService, telemetryService: ITelemetryService, _emmet: typeof emmet, callback: (_emmet: typeof emmet) => void): void { this.updateEmmetPreferences(configurationService, telemetryService, _emmet).then(() => { try { callback(_emmet); } finally { _emmet.resetUserData(); } }); } } export class EmmetActionContext { editor: ICommonCodeEditor; emmet: typeof emmet; editorAccessor: EditorAccessor; constructor(editor: ICommonCodeEditor, _emmet: typeof emmet, editorAccessor: EditorAccessor) { this.editor = editor; this.emmet = _emmet; this.editorAccessor = editorAccessor; } } export interface IEmmetActionOptions extends IActionOptions { actionName: string; } export abstract class EmmetEditorAction extends EditorAction { 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', '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', 'editor.emmet.action.splitJoinTag': 'emmet.splitJoinTag', 'editor.emmet.action.toggleComment': 'emmet.toggleComment', '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' }; protected emmetActionName: string; constructor(opts: IEmmetActionOptions) { super(opts); this.emmetActionName = opts.actionName; } abstract runEmmetAction(accessor: ServicesAccessor, ctx: EmmetActionContext); protected noExpansionOccurred(editor: ICommonCodeEditor) { // default do nothing } private _lastGrammarContributions: TPromise = null; private _lastExtensionService: IExtensionService = null; private _withGrammarContributions(extensionService: IExtensionService): TPromise { if (this._lastExtensionService !== extensionService) { this._lastExtensionService = extensionService; this._lastGrammarContributions = extensionService.readExtensionPointContributions(grammarsExtPoint).then((contributions) => { return new GrammarContributions(contributions); }); } return this._lastGrammarContributions; } public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): TPromise { const configurationService = accessor.get(IConfigurationService); const instantiationService = accessor.get(IInstantiationService); const extensionService = accessor.get(IExtensionService); const modeService = accessor.get(IModeService); const messageService = accessor.get(IMessageService); const contextService = accessor.get(IWorkspaceContextService); const workspaceRoot = contextService.hasWorkspace() ? contextService.getWorkspace().resource.fsPath : ''; // TODO@Ramya (https://github.com/Microsoft/vscode/issues/29244) const telemetryService = accessor.get(ITelemetryService); const commandService = accessor.get(ICommandService); let mappedCommand = this.actionMap[this.id]; if (mappedCommand && mappedCommand !== 'emmet.expandAbbreviation' && mappedCommand !== 'emmet.wrapWithAbbreviation') { return commandService.executeCommand(mappedCommand); } return this._withGrammarContributions(extensionService).then((grammarContributions) => { let editorAccessor = new EditorAccessor( modeService, editor, configurationService.getConfiguration().emmet.syntaxProfiles, configurationService.getConfiguration().emmet.excludeLanguages, grammarContributions, this.emmetActionName ); if (configurationService.getConfiguration().emmet.useNewEmmet && (mappedCommand === 'emmet.expandAbbreviation' || mappedCommand === 'emmet.wrapWithAbbreviation')) { return commandService.executeCommand(mappedCommand, editorAccessor.getLanguage()); } if (!editorAccessor.isEmmetEnabledMode()) { this.noExpansionOccurred(editor); return undefined; } return LazyEmmet.withConfiguredEmmet(configurationService, messageService, telemetryService, editorAccessor.getEmmetSupportedModes(), workspaceRoot, (_emmet) => { if (!_emmet) { this.noExpansionOccurred(editor); return undefined; } editorAccessor.onBeforeEmmetAction(); instantiationService.invokeFunction((accessor) => { this.runEmmetAction(accessor, new EmmetActionContext(editor, _emmet, editorAccessor)); }); editorAccessor.onAfterEmmetAction(); }); }); } } export class BasicEmmetEditorAction extends EmmetEditorAction { constructor(id: string, label: string, alias: string, actionName: string, kbOpts?: ICommandKeybindingsOptions) { super({ id, label, alias, precondition: EditorContextKeys.writable, kbOpts, actionName }); } public runEmmetAction(accessor: ServicesAccessor, ctx: EmmetActionContext) { const telemetryService = accessor.get(ITelemetryService); try { if (!ctx.emmet.run(this.emmetActionName, ctx.editorAccessor)) { this.noExpansionOccurred(ctx.editor); } else if (this.emmetActionName === 'expand_abbreviation') { telemetryService.publicLog('emmetActionSucceeded', { action: this.emmetActionName }); } } catch (err) { this.noExpansionOccurred(ctx.editor); } } }