abbreviationActions.ts 10.5 KB
Newer Older
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.
 *--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
R
Ramya Achutha Rao 已提交
7
import { Node, HtmlNode, Rule } from 'EmmetNode';
8
import { getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate } from './util';
9
import { getExpandOptions, extractAbbreviation, extractAbbreviationFromText, isStyleSheet, isAbbreviationValid, getEmmetMode, expandAbbreviation } from 'vscode-emmet-helper';
10

R
Ramya Achutha Rao 已提交
11
interface ExpandAbbreviationInput {
12
	syntax: string;
R
Ramya Achutha Rao 已提交
13 14
	abbreviation: string;
	rangeToReplace: vscode.Range;
15
	textToWrap?: string | string[];
16
	filters?: string[];
R
Ramya Achutha Rao 已提交
17 18
}

19
export function wrapWithAbbreviation(args) {
20
	if (!validate(false)) {
21 22
		return;
	}
23 24

	const editor = vscode.window.activeTextEditor;
25
	const abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation' });
26
	const syntax = getSyntaxFromArgs({ language: editor.document.languageId }) || 'html';
27 28

	return abbreviationPromise.then(abbreviation => {
29
		if (!abbreviation || !abbreviation.trim() || !isAbbreviationValid(syntax, abbreviation)) { return; }
30

R
Ramya Achutha Rao 已提交
31
		let expandAbbrList: ExpandAbbreviationInput[] = [];
32

33
		editor.selections.forEach(selection => {
34
			let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
35 36 37
			if (rangeToReplace.isEmpty) {
				rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
			}
38

39 40 41
			const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character);
			const matches = firstLineOfSelection.match(/^(\s*)/);
			const preceedingWhiteSpace = matches ? matches[1].length : 0;
42

43 44
			rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + preceedingWhiteSpace, rangeToReplace.end.line, rangeToReplace.end.character);
			expandAbbrList.push({ syntax, abbreviation, rangeToReplace, textToWrap: '\n\t\$TM_SELECTED_TEXT\n' });
45
		});
46

47
		return expandAbbreviationInRange(editor, expandAbbrList, true);
48 49 50
	});
}

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
export function wrapIndividualLinesWithAbbreviation(args) {
	if (!validate(false)) {
		return;
	}

	const editor = vscode.window.activeTextEditor;
	if (editor.selection.isEmpty) {
		vscode.window.showInformationMessage('Select more than 1 line and try again.');
		return;
	}

	const abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation' });
	const syntax = getSyntaxFromArgs({ language: editor.document.languageId }) || 'html';
	const lines = editor.document.getText(editor.selection).split('\n').map(x => x.trim());

	return abbreviationPromise.then(abbreviation => {
		if (!abbreviation || !abbreviation.trim() || !isAbbreviationValid(syntax, abbreviation)) { return; }

		let input: ExpandAbbreviationInput = {
			syntax,
			abbreviation,
			rangeToReplace: editor.selection,
			textToWrap: lines
		};

		return expandAbbreviationInRange(editor, [input], true);
	});

}

81
export function expandEmmetAbbreviation(args) {
82
	const syntax = getSyntaxFromArgs(args);
83
	if (!syntax || !validate()) {
84 85
		return;
	}
86 87 88

	const editor = vscode.window.activeTextEditor;

89
	let rootNode = parseDocument(editor.document);
90 91 92
	if (!rootNode) {
		return;
	}
93

R
Ramya Achutha Rao 已提交
94
	let abbreviationList: ExpandAbbreviationInput[] = [];
95 96 97
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;

98
	let getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, isHtml: boolean): [vscode.Range, string, string[]] => {
R
Ramya Achutha Rao 已提交
99
		let rangeToReplace: vscode.Range = selection;
100
		let abbr = document.getText(rangeToReplace);
101
		if (!rangeToReplace.isEmpty) {
102 103
			let { abbreviation, filters } = extractAbbreviationFromText(abbr);
			return [rangeToReplace, abbreviation, filters];
104 105 106 107 108 109 110 111 112
		}

		// Expand cases like <div to <div></div> explicitly
		// else we will end up with <<div></div>
		if (isHtml) {
			const currentLine = editor.document.lineAt(position.line).text;
			const textTillPosition = currentLine.substr(0, position.character);
			let matches = textTillPosition.match(/<(\w+)$/);
			if (matches) {
113 114 115
				abbr = matches[1];
				rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position);
				return [rangeToReplace, abbr, []];
116
			}
117
		}
118 119
		let { abbreviationRange, abbreviation, filters } = extractAbbreviation(editor.document, position);
		return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filters];
120 121 122 123
	};

	editor.selections.forEach(selection => {
		let position = selection.isReversed ? selection.anchor : selection.active;
124
		let [rangeToReplace, abbreviation, filters] = getAbbreviation(editor.document, selection, position, syntax === 'html');
125
		if (!isAbbreviationValid(syntax, abbreviation)) {
126
			vscode.window.showErrorMessage('Emmet: Invalid abbreviation');
127 128
			return;
		}
129

130 131 132 133 134
		let currentNode = getNode(rootNode, position);
		if (!isValidLocationForEmmetAbbreviation(currentNode, syntax, position)) {
			return;
		}

135 136 137 138
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
139
		}
140

141
		abbreviationList.push({ syntax, abbreviation, rangeToReplace, filters });
142 143
	});

144
	return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame);
145 146 147 148
}


/**
149 150
 * Checks if given position is a valid location to expand emmet abbreviation.
 * Works only on html and css/less/scss syntax
151 152 153 154
 * @param currentNode parsed node at given position
 * @param syntax syntax of the abbreviation
 * @param position position to validate
 */
155
export function isValidLocationForEmmetAbbreviation(currentNode: Node, syntax: string, position: vscode.Position): boolean {
156
	if (!currentNode) {
157
		return !isStyleSheet(syntax);
158 159 160
	}

	if (isStyleSheet(syntax)) {
R
Ramya Achutha Rao 已提交
161 162 163 164
		if (currentNode.type !== 'rule') {
			return true;
		}
		const currentCssNode = <Rule>currentNode;
165 166 167 168 169 170 171 172 173 174

		// Workaround for https://github.com/Microsoft/vscode/30188
		if (currentCssNode.parent
			&& currentCssNode.parent.type === 'rule'
			&& currentCssNode.selectorToken
			&& currentCssNode.selectorToken.start.line !== currentCssNode.selectorToken.end.line) {
			return true;
		}

		// Position is valid if it occurs after the `{` that marks beginning of rule contents
R
Ramya Achutha Rao 已提交
175
		return currentCssNode.selectorToken && position.isAfter(currentCssNode.selectorToken.end);
176 177
	}

R
Ramya Achutha Rao 已提交
178 179 180
	const currentHtmlNode = <HtmlNode>currentNode;
	if (currentHtmlNode.close) {
		return getInnerRange(currentHtmlNode).contains(position);
181 182 183
	}

	return false;
R
Ramya Achutha Rao 已提交
184 185
}

186 187
/**
 * Expands abbreviations as detailed in expandAbbrList in the editor
188 189 190
 * @param editor
 * @param expandAbbrList
 * @param insertSameSnippet
191
 */
192
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
R
Ramya Achutha Rao 已提交
193 194 195 196 197 198 199
	if (!expandAbbrList || expandAbbrList.length === 0) {
		return;
	}

	// Snippet to replace at multiple cursors are not the same
	// `editor.insertSnippet` will have to be called for each instance separately
	// We will not be able to maintain multiple cursors after snippet insertion
200
	let insertPromises = [];
R
Ramya Achutha Rao 已提交
201 202
	if (!insertSameSnippet) {
		expandAbbrList.forEach((expandAbbrInput: ExpandAbbreviationInput) => {
203
			let expandedText = expandAbbr(expandAbbrInput);
R
Ramya Achutha Rao 已提交
204
			if (expandedText) {
205
				insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace));
R
Ramya Achutha Rao 已提交
206 207
			}
		});
208
		return Promise.all(insertPromises).then(() => Promise.resolve(true));
R
Ramya Achutha Rao 已提交
209 210 211
	}

	// Snippet to replace at all cursors are the same
212
	// We can pass all ranges to `editor.insertSnippet` in a single call so that
R
Ramya Achutha Rao 已提交
213 214
	// all cursors are maintained after snippet insertion
	const anyExpandAbbrInput = expandAbbrList[0];
215
	let expandedText = expandAbbr(anyExpandAbbrInput);
R
Ramya Achutha Rao 已提交
216
	let allRanges = expandAbbrList.map(value => {
217
		return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
R
Ramya Achutha Rao 已提交
218 219
	});
	if (expandedText) {
220
		return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
R
Ramya Achutha Rao 已提交
221
	}
222 223
}

224
/**
225
 * Expands abbreviation as detailed in given input.
226
 */
227
function expandAbbr(input: ExpandAbbreviationInput): string {
228
	const emmetConfig = vscode.workspace.getConfiguration('emmet');
229
	const expandOptions = getExpandOptions(input.syntax, emmetConfig['syntaxProfiles'], emmetConfig['variables'], input.filters);
230

231 232 233 234 235 236 237 238 239
	if (input.textToWrap) {
		expandOptions['text'] = input.textToWrap;

		// Below fixes https://github.com/Microsoft/vscode/issues/29898
		// With this, Emmet formats inline elements as block elements
		// ensuring the wrapped multi line text does not get merged to a single line
		if (!input.rangeToReplace.isSingleLine) {
			expandOptions.profile['inlineBreak'] = 1;
		}
240 241
	}

242
	try {
243
		// Expand the abbreviation
244
		let expandedText = expandAbbreviation(input.abbreviation, expandOptions);
245

246 247 248 249
		// If the expanded text is single line then we dont need the \t we added to $TM_SELECTED_TEXT earlier
		if (input.textToWrap && expandedText.indexOf('\n') === -1) {
			expandedText = expandedText.replace(/\s*\$TM_SELECTED_TEXT\s*/, '\$TM_SELECTED_TEXT');
		}
250 251
		return expandedText;

252 253
	} catch (e) {
		vscode.window.showErrorMessage('Failed to expand abbreviation');
254 255 256
	}


257 258 259 260 261 262 263 264
}

function getSyntaxFromArgs(args: any): string {
	let editor = vscode.window.activeTextEditor;
	if (!editor) {
		vscode.window.showInformationMessage('No editor is active.');
		return;
	}
265 266

	const mappedModes = getMappingForIncludedLanguages();
267 268
	let language: string = (!args || typeof args !== 'object' || !args['language']) ? editor.document.languageId : args['language'];
	let parentMode: string = (args && typeof args === 'object') ? args['parentMode'] : undefined;
269
	let excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
270
	let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
271 272 273 274
	if (syntax) {
		return syntax;
	}

275
	return getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
276
}