abbreviationActions.ts 24.4 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';
7
import { Node, HtmlNode, Rule, Property } from 'EmmetNode';
8
import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode } from './util';
9

10
const trimRegex = /[\u00a0]*[\d|#|\-|\*|\u2022]+\.?/;
11
const hexColorRegex = /^#\d+$/;
12

13 14 15 16 17 18
const inlineElements = ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo',
	'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i',
	'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q',
	's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup',
	'textarea', 'tt', 'u', 'var'];

R
Ramya Achutha Rao 已提交
19
interface ExpandAbbreviationInput {
20
	syntax: string;
R
Ramya Achutha Rao 已提交
21 22
	abbreviation: string;
	rangeToReplace: vscode.Range;
23
	textToWrap?: string[];
24
	filter?: string;
R
Ramya Achutha Rao 已提交
25 26
}

27 28 29 30 31 32 33
interface PreviewRangesWithContent {
	previewRange: vscode.Range;
	originalRange: vscode.Range;
	originalContent: string;
	textToWrapInPreview: string[];
}

34 35
export function wrapWithAbbreviation(args: any) {
	if (!validate(false) || !vscode.window.activeTextEditor) {
36 37
		return;
	}
38 39

	const editor = vscode.window.activeTextEditor;
40
	let rootNode = parseDocument(editor.document, false);
41

42
	const syntax = getSyntaxFromArgs({ language: editor.document.languageId }) || '';
43 44 45 46
	if (!syntax) {
		return;
	}

47 48 49 50 51 52 53
	let inPreview = false;

	// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
	let rangesToReplace: PreviewRangesWithContent[] = [];

	editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.line - b.start.line; }).forEach(selection => {
		let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
54 55 56 57 58
		if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
			let previousLine = rangeToReplace.end.line - 1;
			let lastChar = editor.document.lineAt(previousLine).text.length;
			rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar));
		} else if (rangeToReplace.isEmpty) {
59 60 61 62 63 64 65 66 67
			let { active } = selection;
			let currentNode = getNode(rootNode, active, true);
			if (currentNode && (currentNode.start.line === active.line || currentNode.end.line === active.line)) {
				rangeToReplace = new vscode.Range(currentNode.start, currentNode.end);
			} else {
				rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
			}
		}

68
		rangeToReplace = ignoreExtraWhitespaceSelected(rangeToReplace, editor.document);
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93

		const wholeFirstLine = editor.document.lineAt(rangeToReplace.start).text;
		const otherMatches = wholeFirstLine.match(/^(\s*)/);
		const preceedingWhiteSpace = otherMatches ? otherMatches[1] : '';
		let textToReplace = editor.document.getText(rangeToReplace);
		let textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + preceedingWhiteSpace).join('\n\t') + '\n'];
		rangesToReplace.push({ previewRange: rangeToReplace, originalRange: rangeToReplace, originalContent: textToReplace, textToWrapInPreview });
	});

	let abbreviationPromise;
	let currentValue = '';

	function inputChanged(value: string): string {
		if (value !== currentValue) {
			currentValue = value;
			makeChanges(value, inPreview, false).then((out) => {
				if (typeof out === 'boolean') {
					inPreview = out;
				}
			});
		}
		return '';
	}

	abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation', validateInput: inputChanged });
94
	const helper = getEmmetHelper();
95

96 97 98 99
	function makeChanges(inputAbbreviation: string | undefined, inPreview: boolean, definitive: boolean): Thenable<boolean> {
		if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) {
			return inPreview ? revertPreview(editor, rangesToReplace).then(() => { return false; }) : Promise.resolve(inPreview);
		}
100 101 102

		let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
		if (!extractedResults) {
103 104 105
			return Promise.resolve(inPreview);
		} else if (extractedResults.abbreviation !== inputAbbreviation) {
			// Not clear what should we do in this case. Warn the user? How?
106
		}
107

108 109 110 111 112 113 114 115 116 117 118 119
		let { abbreviation, filter } = extractedResults;
		if (definitive) {
			const revertPromise = inPreview ? revertPreview(editor, rangesToReplace) : Promise.resolve();
			return revertPromise.then(() => {
				const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
					let rangeToReplace = rangesAndContent.originalRange;
					let textToWrap = rangeToReplace.isSingleLine ? ['$TM_SELECTED_TEXT'] : ['\n\t$TM_SELECTED_TEXT\n'];
					return { syntax, abbreviation, rangeToReplace, textToWrap, filter };
				});
				return expandAbbreviationInRange(editor, expandAbbrList, true).then(() => { return true; });
			});
		}
120

121 122
		const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
			return { syntax, abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter };
123
		});
124

125 126 127 128 129 130
		return applyPreview(editor, expandAbbrList, rangesToReplace);
	}

	// On inputBox closing
	return abbreviationPromise.then(inputAbbreviation => {
		return makeChanges(inputAbbreviation, inPreview, true);
131 132 133
	});
}

134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
function revertPreview(editor: vscode.TextEditor, rangesToReplace: PreviewRangesWithContent[]): Thenable<any> {
	return editor.edit(builder => {
		for (let i = 0; i < rangesToReplace.length; i++) {
			builder.replace(rangesToReplace[i].previewRange, rangesToReplace[i].originalContent);
			rangesToReplace[i].previewRange = rangesToReplace[i].originalRange;
		}
	}, { undoStopBefore: false, undoStopAfter: false });
}

function applyPreview(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], rangesToReplace: PreviewRangesWithContent[]): Thenable<boolean> {
	let totalLinesInserted = 0;

	return editor.edit(builder => {
		for (let i = 0; i < rangesToReplace.length; i++) {
			const expandedText = expandAbbr(expandAbbrList[i]) || '';
			if (!expandedText) {
				// Failed to expand text. We already showed an error inside expandAbbr.
				break;
			}

			const oldPreviewRange = rangesToReplace[i].previewRange;
			const preceedingText = editor.document.getText(new vscode.Range(oldPreviewRange.start.line, 0, oldPreviewRange.start.line, oldPreviewRange.start.character));
			const indentPrefix = (preceedingText.match(/^(\s*)/) || ['', ''])[1];

			let newText = expandedText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text
			newText = newText.replace(/\$\{[\d]*\}/g, '|'); // Removing Tabstops
			newText = newText.replace(/\$\{[\d]*(:[^}]*)?\}/g, (match) => {		// Replacing Placeholders
				return match.replace(/^\$\{[\d]*:/, '').replace('}', '');
			});
			builder.replace(oldPreviewRange, newText);

			const expandedTextLines = newText.split('\n');
			const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1;
			const newLinesInserted = expandedTextLines.length - oldPreviewLines;

			let lastLineEnd = expandedTextLines[expandedTextLines.length - 1].length;
			if (expandedTextLines.length === 1) {
				// If the expandedText is single line, add the length of preceeding whitespace as it will not be included in line length.
				lastLineEnd += oldPreviewRange.start.character;
			}

			rangesToReplace[i].previewRange = new vscode.Range(oldPreviewRange.start.line + totalLinesInserted, oldPreviewRange.start.character, oldPreviewRange.end.line + totalLinesInserted + newLinesInserted, lastLineEnd);
			totalLinesInserted += newLinesInserted;
		}
	}, { undoStopBefore: false, undoStopAfter: false });
}

181 182
export function wrapIndividualLinesWithAbbreviation(args: any) {
	if (!validate(false) || !vscode.window.activeTextEditor) {
183 184 185 186
		return;
	}

	const editor = vscode.window.activeTextEditor;
187
	if (editor.selections.length === 1 && editor.selection.isEmpty) {
188 189 190
		vscode.window.showInformationMessage('Select more than 1 line and try again.');
		return;
	}
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
	if (editor.selections.find(x => x.isEmpty)) {
		vscode.window.showInformationMessage('Select more than 1 line in each selection and try again.');
		return;
	}
	let rangesToReplace: vscode.Range[] = [];
	editor.selections.forEach(selection => {
		let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
		if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
			let previousLine = rangeToReplace.end.line - 1;
			let lastChar = editor.document.lineAt(previousLine).text.length;
			rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar));
		}

		rangeToReplace = ignoreExtraWhitespaceSelected(rangeToReplace, editor.document);
		rangesToReplace.push(rangeToReplace);
	});
207

208
	const syntax = getSyntaxFromArgs({ language: editor.document.languageId });
209 210 211 212 213
	if (!syntax) {
		return;
	}

	const abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation' });
214
	const helper = getEmmetHelper();
215

216
	return abbreviationPromise.then(inputAbbreviation => {
217
		let expandAbbrInput: ExpandAbbreviationInput[] = [];
218
		if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) { return false; }
219

220
		let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
221
		if (!extractedResults) {
222
			return false;
223
		}
224 225 226 227 228 229 230 231 232 233 234 235 236
		rangesToReplace.forEach(rangeToReplace => {
			let lines = editor.document.getText(rangeToReplace).split('\n').map(x => x.trim());

			let { abbreviation, filter } = extractedResults;
			let input: ExpandAbbreviationInput = {
				syntax,
				abbreviation,
				rangeToReplace,
				textToWrap: lines,
				filter
			};
			expandAbbrInput.push(input);
		});
237

238
		return expandAbbreviationInRange(editor, expandAbbrInput, false);
239 240 241 242
	});

}

243 244 245 246 247 248 249 250
function ignoreExtraWhitespaceSelected(range: vscode.Range, document: vscode.TextDocument): vscode.Range {
	const firstLineOfSelection = document.lineAt(range.start).text.substr(range.start.character);
	const matches = firstLineOfSelection.match(/^(\s*)/);
	const extraWhiteSpaceSelected = matches ? matches[1].length : 0;

	return new vscode.Range(range.start.line, range.start.character + extraWhiteSpaceSelected, range.end.line, range.end.character);
}

251 252 253 254 255 256 257 258
export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> {
	if (!validate() || !vscode.window.activeTextEditor) {
		return fallbackTab();
	}

	args = args || {};
	if (!args['language']) {
		args['language'] = vscode.window.activeTextEditor.document.languageId;
259 260 261 262 263
	} else {
		const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
		if (excludedLanguages.indexOf(vscode.window.activeTextEditor.document.languageId) > -1) {
			return fallbackTab();
		}
264
	}
265
	const syntax = getSyntaxFromArgs(args);
266
	if (!syntax) {
267
		return fallbackTab();
268
	}
269 270 271

	const editor = vscode.window.activeTextEditor;

272
	let rootNode = parseDocument(editor.document, false);
273

274 275 276 277 278
	// When tabbed on a non empty selection, do not treat it as an emmet abbreviation, and fallback to tab instead
	if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true && editor.selections.find(x => !x.isEmpty)) {
		return fallbackTab();
	}

R
Ramya Achutha Rao 已提交
279
	let abbreviationList: ExpandAbbreviationInput[] = [];
280 281
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;
282
	const helper = getEmmetHelper();
283

284
	let getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string] => {
R
Ramya Achutha Rao 已提交
285
		let rangeToReplace: vscode.Range = selection;
286
		let abbr = document.getText(rangeToReplace);
287
		if (!rangeToReplace.isEmpty) {
288
			let extractedResults = helper.extractAbbreviationFromText(abbr);
289
			if (extractedResults) {
290
				return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter];
291
			}
292
			return [null, '', ''];
293 294
		}

295 296 297
		const currentLine = editor.document.lineAt(position.line).text;
		const textTillPosition = currentLine.substr(0, position.character);

298 299
		// Expand cases like <div to <div></div> explicitly
		// else we will end up with <<div></div>
300
		if (syntax === 'html') {
301 302
			let matches = textTillPosition.match(/<(\w+)$/);
			if (matches) {
303 304
				abbr = matches[1];
				rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position);
305
				return [rangeToReplace, abbr, ''];
306
			}
307
		}
308
		let extractedResults = helper.extractAbbreviation(editor.document, position, false);
309
		if (!extractedResults) {
310
			return [null, '', ''];
311 312
		}

313 314
		let { abbreviationRange, abbreviation, filter } = extractedResults;
		return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter];
315 316
	};

J
jmdowns2 已提交
317 318 319 320 321 322 323 324
	let selectionsInReverseOrder = editor.selections.slice(0);
	selectionsInReverseOrder.sort((a, b) => {
		var posA = a.isReversed ? a.anchor : a.active;
		var posB = b.isReversed ? b.anchor : b.active;
		return posA.compareTo(posB) * -1;
	});

	selectionsInReverseOrder.forEach(selection => {
325
		let position = selection.isReversed ? selection.anchor : selection.active;
326
		let [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax);
327 328 329
		if (!rangeToReplace) {
			return;
		}
330
		if (!helper.isAbbreviationValid(syntax, abbreviation)) {
331 332
			return;
		}
333

334
		let currentNode = getNode(rootNode, position, true);
335
		if (!isValidLocationForEmmetAbbreviation(editor.document, currentNode, syntax, position, rangeToReplace)) {
336 337 338
			return;
		}

339 340 341 342
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
343
		}
344

345
		abbreviationList.push({ syntax, abbreviation, rangeToReplace, filter });
346 347
	});

348 349 350 351 352
	return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
		if (!success) {
			return fallbackTab();
		}
	});
353 354
}

355
function fallbackTab(): Thenable<boolean | undefined> {
356 357 358
	if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true) {
		return vscode.commands.executeCommand('tab');
	}
359
	return Promise.resolve(true);
360
}
361
/**
362 363
 * Checks if given position is a valid location to expand emmet abbreviation.
 * Works only on html and css/less/scss syntax
364
 * @param document current Text Document
365 366 367
 * @param currentNode parsed node at given position
 * @param syntax syntax of the abbreviation
 * @param position position to validate
368
 * @param abbreviationRange The range of the abbreviation for which given position is being validated
369
 */
370
export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean {
371
	if (isStyleSheet(syntax)) {
372 373 374 375 376
		// Continue validation only if the file was parse-able and the currentNode has been found
		if (!currentNode) {
			return true;
		}

377 378 379
		// Fix for https://github.com/Microsoft/vscode/issues/34162
		// Other than sass, stylus, we can make use of the terminator tokens to validate position
		if (syntax !== 'sass' && syntax !== 'stylus' && currentNode.type === 'property') {
380 381 382 383 384 385 386 387

			// Fix for upstream issue https://github.com/emmetio/css-parser/issues/3
			if (currentNode.parent
				&& currentNode.parent.type !== 'rule'
				&& currentNode.parent.type !== 'at-rule') {
				return false;
			}

388
			const abbreviation = document.getText(new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character));
389 390 391 392
			const propertyNode = <Property>currentNode;
			if (propertyNode.terminatorToken
				&& propertyNode.separator
				&& position.isAfterOrEqual(propertyNode.separatorToken.end)
393 394
				&& position.isBeforeOrEqual(propertyNode.terminatorToken.start)
				&& abbreviation.indexOf(':') === -1) {
395
				return hexColorRegex.test(abbreviation);
396 397 398
			}
			if (!propertyNode.terminatorToken
				&& propertyNode.separator
399 400
				&& position.isAfterOrEqual(propertyNode.separatorToken.end)
				&& abbreviation.indexOf(':') === -1) {
401
				return hexColorRegex.test(abbreviation);
402 403 404
			}
		}

405 406 407
		// If current node is a rule or at-rule, then perform additional checks to ensure
		// emmet suggestions are not provided in the rule selector
		if (currentNode.type !== 'rule' && currentNode.type !== 'at-rule') {
R
Ramya Achutha Rao 已提交
408 409
			return true;
		}
410

R
Ramya Achutha Rao 已提交
411
		const currentCssNode = <Rule>currentNode;
412

413 414 415 416 417
		// Position is valid if it occurs after the `{` that marks beginning of rule contents
		if (position.isAfter(currentCssNode.contentStartToken.end)) {
			return true;
		}

418
		// Workaround for https://github.com/Microsoft/vscode/30188
419 420
		// The line above the rule selector is considered as part of the selector by the css-parser
		// But we should assume it is a valid location for css properties under the parent rule
421
		if (currentCssNode.parent
422
			&& (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule')
423
			&& currentCssNode.selectorToken
424 425 426 427
			&& position.line !== currentCssNode.selectorToken.end.line
			&& currentCssNode.selectorToken.start.character === abbreviationRange.start.character
			&& currentCssNode.selectorToken.start.line === abbreviationRange.start.line
		) {
428 429 430
			return true;
		}

431
		return false;
432 433
	}

434 435 436
	const startAngle = '<';
	const endAngle = '>';
	const escape = '\\';
437
	const question = '?';
R
Ramya Achutha Rao 已提交
438
	const currentHtmlNode = <HtmlNode>currentNode;
439
	let start = new vscode.Position(0, 0);
440

441 442
	if (currentHtmlNode) {
		const innerRange = getInnerRange(currentHtmlNode);
443

444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
		// Fix for https://github.com/Microsoft/vscode/issues/28829
		if (!innerRange || !innerRange.contains(position)) {
			return false;
		}

		// Fix for https://github.com/Microsoft/vscode/issues/35128
		// Find the position up till where we will backtrack looking for unescaped < or >
		// to decide if current position is valid for emmet expansion
		start = innerRange.start;
		let lastChildBeforePosition = currentHtmlNode.firstChild;
		while (lastChildBeforePosition) {
			if (lastChildBeforePosition.end.isAfter(position)) {
				break;
			}
			start = lastChildBeforePosition.end;
			lastChildBeforePosition = lastChildBeforePosition.nextSibling;
460 461
		}
	}
462
	let textToBackTrack = document.getText(new vscode.Range(start.line, start.character, abbreviationRange.start.line, abbreviationRange.start.character));
463 464 465 466 467 468 469

	// Worse case scenario is when cursor is inside a big chunk of text which needs to backtracked
	// Backtrack only 500 offsets to ensure we dont waste time doing this
	if (textToBackTrack.length > 500) {
		textToBackTrack = textToBackTrack.substr(textToBackTrack.length - 500);
	}

470 471 472 473
	if (!textToBackTrack.trim()) {
		return true;
	}

474 475 476 477 478 479 480 481 482 483
	let valid = true;
	let foundSpace = false; // If < is found before finding whitespace, then its valid abbreviation. Eg: <div|
	let i = textToBackTrack.length - 1;
	while (i >= 0) {
		const char = textToBackTrack[i];
		i--;
		if (!foundSpace && /\s/.test(char)) {
			foundSpace = true;
			continue;
		}
484 485 486 487
		if (char === question && textToBackTrack[i] === startAngle) {
			i--;
			continue;
		}
488 489 490 491 492 493 494 495 496 497 498 499 500 501
		if (char !== startAngle && char !== endAngle) {
			continue;
		}
		if (i >= 0 && textToBackTrack[i] === escape) {
			i--;
			continue;
		}
		if (char === endAngle) {
			break;
		}
		if (char === startAngle) {
			valid = !foundSpace;
			break;
		}
502 503
	}

504
	return valid;
R
Ramya Achutha Rao 已提交
505 506
}

507 508
/**
 * Expands abbreviations as detailed in expandAbbrList in the editor
509 510 511
 * @param editor
 * @param expandAbbrList
 * @param insertSameSnippet
512
 * @returns false if no snippet can be inserted.
513
 */
514
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
R
Ramya Achutha Rao 已提交
515
	if (!expandAbbrList || expandAbbrList.length === 0) {
516
		return Promise.resolve(false);
R
Ramya Achutha Rao 已提交
517 518 519 520 521
	}

	// 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
522
	let insertPromises: Thenable<boolean>[] = [];
R
Ramya Achutha Rao 已提交
523
	if (!insertSameSnippet) {
524
		expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); }).forEach((expandAbbrInput: ExpandAbbreviationInput) => {
525
			let expandedText = expandAbbr(expandAbbrInput);
R
Ramya Achutha Rao 已提交
526
			if (expandedText) {
527
				insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false }));
R
Ramya Achutha Rao 已提交
528 529
			}
		});
530 531 532
		if (insertPromises.length === 0) {
			return Promise.resolve(false);
		}
533
		return Promise.all(insertPromises).then(() => Promise.resolve(true));
R
Ramya Achutha Rao 已提交
534 535 536
	}

	// Snippet to replace at all cursors are the same
537
	// We can pass all ranges to `editor.insertSnippet` in a single call so that
R
Ramya Achutha Rao 已提交
538 539
	// all cursors are maintained after snippet insertion
	const anyExpandAbbrInput = expandAbbrList[0];
540
	let expandedText = expandAbbr(anyExpandAbbrInput);
R
Ramya Achutha Rao 已提交
541
	let allRanges = expandAbbrList.map(value => {
542
		return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
R
Ramya Achutha Rao 已提交
543 544
	});
	if (expandedText) {
545
		return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
R
Ramya Achutha Rao 已提交
546
	}
547
	return Promise.resolve(false);
548 549
}

550
/**
551
 * Expands abbreviation as detailed in given input.
552
 */
553
function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
554 555
	const helper = getEmmetHelper();
	const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter);
556

557
	if (input.textToWrap) {
558
		if (input.filter && input.filter.indexOf('t') > -1) {
559 560 561 562
			input.textToWrap = input.textToWrap.map(line => {
				return line.replace(trimRegex, '').trim();
			});
		}
563 564 565 566 567 568 569 570
		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;
		}
571 572
	}

573
	try {
574
		// Expand the abbreviation
575
		let expandedText;
576

577
		if (input.textToWrap) {
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
			let parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions);
			if (input.rangeToReplace.isSingleLine && input.textToWrap.length === 1) {

				// Fetch rightmost element in the parsed abbreviation (i.e the element that will contain the wrapped text).
				let wrappingNode = parsedAbbr;
				while (wrappingNode && wrappingNode.children && wrappingNode.children.length > 0) {
					wrappingNode = wrappingNode.children[wrappingNode.children.length - 1];
				}

				// If wrapping with a block element, insert newline in the text to wrap.
				if (wrappingNode && inlineElements.indexOf(wrappingNode.name) === -1) {
					wrappingNode.value = '\n\t' + wrappingNode.value + '\n';
				}
			}
			expandedText = helper.expandAbbreviation(parsedAbbr, expandOptions);
593 594 595
			// All $anyword would have been escaped by the emmet helper.
			// Remove the escaping backslash from $TM_SELECTED_TEXT so that VS Code Snippet controller can treat it as a variable
			expandedText = expandedText.replace('\\$TM_SELECTED_TEXT', '$TM_SELECTED_TEXT');
596 597
		} else {
			expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
598
		}
599

600 601
		return expandedText;

602 603
	} catch (e) {
		vscode.window.showErrorMessage('Failed to expand abbreviation');
604 605 606
	}


607 608
}

609
function getSyntaxFromArgs(args: Object): string | undefined {
610
	const mappedModes = getMappingForIncludedLanguages();
611 612 613 614 615 616 617
	const language: string = args['language'];
	const parentMode: string = args['parentMode'];
	const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
	if (excludedLanguages.indexOf(language) > -1) {
		return;
	}

618
	let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
619 620
	if (!syntax) {
		syntax = getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
621 622
	}

623
	return syntax;
624
}