abbreviationActions.ts 27.9 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, Stylesheet } from 'EmmetNode';
M
Martin Aeschlimann 已提交
8
import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util';
9

U
Ubuntu 已提交
10 11
const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/;
const hexColorRegex = /^#[\da-fA-F]{0,6}$/;
12 13 14 15 16 17
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 已提交
18
interface ExpandAbbreviationInput {
19
	syntax: string;
R
Ramya Achutha Rao 已提交
20 21
	abbreviation: string;
	rangeToReplace: vscode.Range;
22
	textToWrap?: string[];
23
	filter?: string;
R
Ramya Achutha Rao 已提交
24 25
}

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

33
export function wrapWithAbbreviation(args: any) {
34 35 36 37 38 39 40 41
	return doWrapping(false, args);
}

export function wrapIndividualLinesWithAbbreviation(args: any) {
	return doWrapping(true, args);
}

function doWrapping(individualLines: boolean, args: any) {
42
	if (!validate(false) || !vscode.window.activeTextEditor) {
43 44
		return;
	}
45 46

	const editor = vscode.window.activeTextEditor;
47 48 49 50 51 52 53 54 55 56
	if (individualLines) {
		if (editor.selections.length === 1 && editor.selection.isEmpty) {
			vscode.window.showInformationMessage('Select more than 1 line and try again.');
			return;
		}
		if (editor.selections.find(x => x.isEmpty)) {
			vscode.window.showInformationMessage('Select more than 1 line in each selection and try again.');
			return;
		}
	}
57 58 59 60 61
	args = args || {};
	if (!args['language']) {
		args['language'] = editor.document.languageId;
	}
	const syntax = getSyntaxFromArgs(args) || 'html';
62
	const rootNode = parseDocument(editor.document, false);
63

64
	let inPreview = false;
65 66
	let currentValue = '';
	const helper = getEmmetHelper();
67 68

	// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
69
	const rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => {
70
		let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
71
		if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
72 73
			const previousLine = rangeToReplace.end.line - 1;
			const lastChar = editor.document.lineAt(previousLine).text.length;
74 75
			rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar));
		} else if (rangeToReplace.isEmpty) {
76 77
			const { active } = selection;
			const currentNode = getNode(rootNode, active, true);
78 79 80 81 82 83 84
			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);
			}
		}

85 86
		const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character);
		const matches = firstLineOfSelection.match(/^(\s*)/);
87 88
		const extraWhitespaceSelected = matches ? matches[1].length : 0;
		rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character);
89

90
		let textToWrapInPreview: string[];
91
		const textToReplace = editor.document.getText(rangeToReplace);
92 93 94 95 96
		if (individualLines) {
			textToWrapInPreview = textToReplace.split('\n').map(x => x.trim());
		} else {
			const wholeFirstLine = editor.document.lineAt(rangeToReplace.start).text;
			const otherMatches = wholeFirstLine.match(/^(\s*)/);
M
Maira Wenzel 已提交
97 98
			const precedingWhitespace = otherMatches ? otherMatches[1] : '';
			textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + precedingWhitespace).join('\n\t') + '\n'];
99
		}
100
		textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1'));
101 102 103 104 105 106 107

		return {
			previewRange: rangeToReplace,
			originalRange: rangeToReplace,
			originalContent: textToReplace,
			textToWrapInPreview
		};
108 109
	});

110 111
	function revertPreview(): Thenable<any> {
		return editor.edit(builder => {
112 113 114
			for (const rangeToReplace of rangesToReplace) {
				builder.replace(rangeToReplace.previewRange, rangeToReplace.originalContent);
				rangeToReplace.previewRange = rangeToReplace.originalRange;
115 116 117
			}
		}, { undoStopBefore: false, undoStopAfter: false });
	}
118

119
	function applyPreview(expandAbbrList: ExpandAbbreviationInput[]): Thenable<boolean> {
J
Jean Pierre 已提交
120 121
		let lastOldPreviewRange = new vscode.Range(0, 0, 0, 0);
		let lastNewPreviewRange = new vscode.Range(0, 0, 0, 0);
122 123 124 125 126 127 128 129
		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;
130 131
				}

132 133 134 135 136 137 138 139 140
				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('}', '');
				});
R
Raymond Zhao 已提交
141
				newText = newText.replace(/\\\$/g, '$'); // Remove backslashes before $
142 143 144 145 146 147
				builder.replace(oldPreviewRange, newText);

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

148
				const newPreviewLineStart = oldPreviewRange.start.line + totalLinesInserted;
J
Jean Pierre 已提交
149 150 151 152 153 154 155 156 157 158 159 160 161
				let newPreviewStart = oldPreviewRange.start.character;
				const newPreviewLineEnd = oldPreviewRange.end.line + totalLinesInserted + newLinesInserted;
				let newPreviewEnd = expandedTextLines[expandedTextLines.length - 1].length;
				if (i > 0 && newPreviewLineEnd === lastNewPreviewRange.end.line) {
					// If newPreviewLineEnd is equal to the previous expandedText lineEnd,
					// set newPreviewStart to the length of the previous expandedText in that line
					// plus the number of characters between both selections.
					newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
					newPreviewEnd += newPreviewStart;
				}
				else if (i > 0 && newPreviewLineStart === lastNewPreviewRange.end.line) {
					// Same as above but expandedTextLines.length > 1 so newPreviewEnd keeps its value.
					newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
162
				}
J
Jean Pierre 已提交
163 164 165 166 167 168 169
				else if (expandedTextLines.length === 1) {
					// If the expandedText is single line, add the length of preceeding text as it will not be included in line length.
					newPreviewEnd += oldPreviewRange.start.character;
				}

				lastOldPreviewRange = rangesToReplace[i].previewRange;
				rangesToReplace[i].previewRange = lastNewPreviewRange = new vscode.Range(newPreviewLineStart, newPreviewStart, newPreviewLineEnd, newPreviewEnd);
170 171 172 173 174

				totalLinesInserted += newLinesInserted;
			}
		}, { undoStopBefore: false, undoStopAfter: false });
	}
175

176
	function makeChanges(inputAbbreviation: string | undefined, definitive: boolean): Thenable<boolean> {
177
		if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) {
178
			return inPreview ? revertPreview().then(() => { return false; }) : Promise.resolve(inPreview);
179
		}
180

181
		const extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
182
		if (!extractedResults) {
183 184 185
			return Promise.resolve(inPreview);
		} else if (extractedResults.abbreviation !== inputAbbreviation) {
			// Not clear what should we do in this case. Warn the user? How?
186
		}
187

188
		const { abbreviation, filter } = extractedResults;
189
		if (definitive) {
190
			const revertPromise = inPreview ? revertPreview() : Promise.resolve();
191 192
			return revertPromise.then(() => {
				const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
193
					const rangeToReplace = rangesAndContent.originalRange;
194 195 196 197 198 199 200
					let textToWrap: string[];
					if (individualLines) {
						textToWrap = rangesAndContent.textToWrapInPreview;
					} else {
						textToWrap = rangeToReplace.isSingleLine ? ['$TM_SELECTED_TEXT'] : ['\n\t$TM_SELECTED_TEXT\n'];
					}
					return { syntax: syntax || '', abbreviation, rangeToReplace, textToWrap, filter };
201
				});
202
				return expandAbbreviationInRange(editor, expandAbbrList, !individualLines).then(() => { return true; });
203 204
			});
		}
205

206
		const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
207
			return { syntax: syntax || '', abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter };
208
		});
209

210
		return applyPreview(expandAbbrList);
211 212
	}

213 214 215 216 217 218 219
	function inputChanged(value: string): string {
		if (value !== currentValue) {
			currentValue = value;
			makeChanges(value, false).then((out) => {
				if (typeof out === 'boolean') {
					inPreview = out;
				}
220 221
			});
		}
222
		return '';
223
	}
224
	const abbreviationPromise: Thenable<string | undefined> = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation', validateInput: inputChanged });
225
	return abbreviationPromise.then(inputAbbreviation => {
226
		return makeChanges(inputAbbreviation, true);
227
	});
228 229
}

230 231 232 233 234
export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> {
	if (!validate() || !vscode.window.activeTextEditor) {
		return fallbackTab();
	}

P
Pine Wu 已提交
235 236 237
	/**
	 * Short circuit the parsing. If previous character is space, do not expand.
	 */
P
Pine Wu 已提交
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
	if (vscode.window.activeTextEditor.selections.length === 1 &&
		vscode.window.activeTextEditor.selection.isEmpty
	) {
		const anchor = vscode.window.activeTextEditor.selection.anchor;
		if (anchor.character === 0) {
			return fallbackTab();
		}

		const prevPositionAnchor = anchor.translate(0, -1);
		const prevText = vscode.window.activeTextEditor.document.getText(new vscode.Range(prevPositionAnchor, anchor));
		if (prevText === ' ' || prevText === '\t') {
			return fallbackTab();
		}
	}

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

	const editor = vscode.window.activeTextEditor;
268

269 270 271 272 273
	// 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();
	}

274
	const abbreviationList: ExpandAbbreviationInput[] = [];
275 276
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;
277
	const helper = getEmmetHelper();
278

279
	const getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string] => {
M
Martin Aeschlimann 已提交
280
		position = document.validatePosition(position);
R
Ramya Achutha Rao 已提交
281
		let rangeToReplace: vscode.Range = selection;
282
		let abbr = document.getText(rangeToReplace);
283
		if (!rangeToReplace.isEmpty) {
284
			const extractedResults = helper.extractAbbreviationFromText(abbr);
285
			if (extractedResults) {
286
				return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter];
287
			}
288
			return [null, '', ''];
289 290
		}

291 292 293
		const currentLine = editor.document.lineAt(position.line).text;
		const textTillPosition = currentLine.substr(0, position.character);

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

309
		const { abbreviationRange, abbreviation, filter } = extractedResults;
310
		return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter];
311 312
	};

313
	const selectionsInReverseOrder = editor.selections.slice(0);
J
jmdowns2 已提交
314
	selectionsInReverseOrder.sort((a, b) => {
M
Matt Bierner 已提交
315 316
		const posA = a.isReversed ? a.anchor : a.active;
		const posB = b.isReversed ? b.anchor : b.active;
J
jmdowns2 已提交
317 318 319
		return posA.compareTo(posB) * -1;
	});

P
Pine Wu 已提交
320 321 322 323 324 325
	let rootNode: Node | undefined;
	function getRootNode() {
		if (rootNode) {
			return rootNode;
		}

326
		const usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;
P
Pine Wu 已提交
327 328 329 330 331 332 333 334 335
		if (editor.selections.length === 1 && isStyleSheet(editor.document.languageId) && usePartialParsing && editor.document.lineCount > 1000) {
			rootNode = parsePartialStylesheet(editor.document, editor.selection.isReversed ? editor.selection.anchor : editor.selection.active);
		} else {
			rootNode = parseDocument(editor.document, false);
		}

		return rootNode;
	}

J
jmdowns2 已提交
336
	selectionsInReverseOrder.forEach(selection => {
337 338
		const position = selection.isReversed ? selection.anchor : selection.active;
		const [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax);
339 340 341
		if (!rangeToReplace) {
			return;
		}
342
		if (!helper.isAbbreviationValid(syntax, abbreviation)) {
343 344
			return;
		}
P
Pine Wu 已提交
345
		let currentNode = getNode(getRootNode(), position, true);
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
		let validateLocation = true;
		let syntaxToUse = syntax;

		if (editor.document.languageId === 'html') {
			if (isStyleAttribute(currentNode, position)) {
				syntaxToUse = 'css';
				validateLocation = false;
			} else {
				const embeddedCssNode = getEmbeddedCssNodeIfAny(editor.document, currentNode, position);
				if (embeddedCssNode) {
					currentNode = getNode(embeddedCssNode, position, true);
					syntaxToUse = 'css';
				}
			}
		}
361

P
Pine Wu 已提交
362
		if (validateLocation && !isValidLocationForEmmetAbbreviation(editor.document, getRootNode(), currentNode, syntaxToUse, position, rangeToReplace)) {
363 364 365
			return;
		}

366 367 368 369
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
370
		}
371

372
		abbreviationList.push({ syntax: syntaxToUse, abbreviation, rangeToReplace, filter });
373 374
	});

375
	return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
376
		return success ? Promise.resolve(undefined) : fallbackTab();
377
	});
378 379
}

380
function fallbackTab(): Thenable<boolean | undefined> {
381 382 383
	if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true) {
		return vscode.commands.executeCommand('tab');
	}
384
	return Promise.resolve(true);
385
}
386
/**
387 388
 * Checks if given position is a valid location to expand emmet abbreviation.
 * Works only on html and css/less/scss syntax
389
 * @param document current Text Document
390
 * @param rootNode parsed document
391
 * @param currentNode current node in the parsed document
392 393
 * @param syntax syntax of the abbreviation
 * @param position position to validate
394
 * @param abbreviationRange The range of the abbreviation for which given position is being validated
395
 */
396
export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean {
397
	if (isStyleSheet(syntax)) {
398 399 400 401
		const stylesheet = <Stylesheet>rootNode;
		if (stylesheet && (stylesheet.comments || []).some(x => position.isAfterOrEqual(x.start) && position.isBeforeOrEqual(x.end))) {
			return false;
		}
402 403 404 405 406
		// Continue validation only if the file was parse-able and the currentNode has been found
		if (!currentNode) {
			return true;
		}

C
ChaseKnowlden 已提交
407
		// Fix for https://github.com/microsoft/vscode/issues/34162
408 409
		// Other than sass, stylus, we can make use of the terminator tokens to validate position
		if (syntax !== 'sass' && syntax !== 'stylus' && currentNode.type === 'property') {
410 411 412 413 414 415 416 417

			// 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;
			}

418
			const abbreviation = document.getText(new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character));
419 420 421 422
			const propertyNode = <Property>currentNode;
			if (propertyNode.terminatorToken
				&& propertyNode.separator
				&& position.isAfterOrEqual(propertyNode.separatorToken.end)
423 424
				&& position.isBeforeOrEqual(propertyNode.terminatorToken.start)
				&& abbreviation.indexOf(':') === -1) {
425
				return hexColorRegex.test(abbreviation) || abbreviation === '!';
426 427 428
			}
			if (!propertyNode.terminatorToken
				&& propertyNode.separator
429 430
				&& position.isAfterOrEqual(propertyNode.separatorToken.end)
				&& abbreviation.indexOf(':') === -1) {
431
				return hexColorRegex.test(abbreviation) || abbreviation === '!';
432
			}
433 434 435
			if (hexColorRegex.test(abbreviation) || abbreviation === '!') {
				return false;
			}
436 437
		}

438 439 440
		// 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 已提交
441 442
			return true;
		}
443

R
Ramya Achutha Rao 已提交
444
		const currentCssNode = <Rule>currentNode;
445

446 447 448 449 450
		// Position is valid if it occurs after the `{` that marks beginning of rule contents
		if (position.isAfter(currentCssNode.contentStartToken.end)) {
			return true;
		}

C
ChaseKnowlden 已提交
451
		// Workaround for https://github.com/microsoft/vscode/30188
452 453
		// 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
454
		if (currentCssNode.parent
455
			&& (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule')
456
			&& currentCssNode.selectorToken
457 458 459 460
			&& position.line !== currentCssNode.selectorToken.end.line
			&& currentCssNode.selectorToken.start.character === abbreviationRange.start.character
			&& currentCssNode.selectorToken.start.line === abbreviationRange.start.line
		) {
461 462 463
			return true;
		}

464
		return false;
465 466
	}

467 468 469
	const startAngle = '<';
	const endAngle = '>';
	const escape = '\\';
470
	const question = '?';
R
Ramya Achutha Rao 已提交
471
	const currentHtmlNode = <HtmlNode>currentNode;
472
	let start = new vscode.Position(0, 0);
473

474
	if (currentHtmlNode) {
475
		if (currentHtmlNode.name === 'script') {
476 477 478 479 480 481 482 483 484 485 486 487
			const typeAttribute = (currentHtmlNode.attributes || []).filter(x => x.name.toString() === 'type')[0];
			const typeValue = typeAttribute ? typeAttribute.value.toString() : '';

			if (allowedMimeTypesInScriptTag.indexOf(typeValue) > -1) {
				return true;
			}

			const isScriptJavascriptType = !typeValue || typeValue === 'application/javascript' || typeValue === 'text/javascript';
			if (isScriptJavascriptType) {
				return !!getSyntaxFromArgs({ language: 'javascript' });
			}
			return false;
488 489
		}

490
		const innerRange = getInnerRange(currentHtmlNode);
491

C
ChaseKnowlden 已提交
492
		// Fix for https://github.com/microsoft/vscode/issues/28829
493 494 495 496
		if (!innerRange || !innerRange.contains(position)) {
			return false;
		}

C
ChaseKnowlden 已提交
497
		// Fix for https://github.com/microsoft/vscode/issues/35128
498 499 500 501 502 503 504 505 506 507
		// 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;
508 509
		}
	}
510
	let textToBackTrack = document.getText(new vscode.Range(start.line, start.character, abbreviationRange.start.line, abbreviationRange.start.character));
511 512 513 514 515 516 517

	// 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);
	}

518 519 520 521
	if (!textToBackTrack.trim()) {
		return true;
	}

522
	let valid = true;
523
	let foundSpace = false; // If < is found before finding whitespace, then its valid abbreviation. E.g.: <div|
524
	let i = textToBackTrack.length - 1;
R
Ramya Achutha Rao 已提交
525 526 527 528
	if (textToBackTrack[i] === startAngle) {
		return false;
	}

529 530 531 532 533 534 535
	while (i >= 0) {
		const char = textToBackTrack[i];
		i--;
		if (!foundSpace && /\s/.test(char)) {
			foundSpace = true;
			continue;
		}
536 537 538 539
		if (char === question && textToBackTrack[i] === startAngle) {
			i--;
			continue;
		}
C
ChaseKnowlden 已提交
540
		// Fix for https://github.com/microsoft/vscode/issues/55411
541 542 543 544 545
		// A space is not a valid character right after < in a tag name.
		if (/\s/.test(char) && textToBackTrack[i] === startAngle) {
			i--;
			continue;
		}
546 547 548 549 550 551 552 553
		if (char !== startAngle && char !== endAngle) {
			continue;
		}
		if (i >= 0 && textToBackTrack[i] === escape) {
			i--;
			continue;
		}
		if (char === endAngle) {
554 555 556 557 558
			if (i >= 0 && textToBackTrack[i] === '=') {
				continue; // False alarm of cases like =>
			} else {
				break;
			}
559 560 561 562 563
		}
		if (char === startAngle) {
			valid = !foundSpace;
			break;
		}
564 565
	}

566
	return valid;
R
Ramya Achutha Rao 已提交
567 568
}

569 570
/**
 * Expands abbreviations as detailed in expandAbbrList in the editor
571
 *
572
 * @returns false if no snippet can be inserted.
573
 */
574
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
R
Ramya Achutha Rao 已提交
575
	if (!expandAbbrList || expandAbbrList.length === 0) {
576
		return Promise.resolve(false);
R
Ramya Achutha Rao 已提交
577 578 579 580 581
	}

	// 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
582
	const insertPromises: Thenable<boolean>[] = [];
R
Ramya Achutha Rao 已提交
583
	if (!insertSameSnippet) {
584
		expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); }).forEach((expandAbbrInput: ExpandAbbreviationInput) => {
585
			let expandedText = expandAbbr(expandAbbrInput);
R
Ramya Achutha Rao 已提交
586
			if (expandedText) {
587
				insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false }));
R
Ramya Achutha Rao 已提交
588 589
			}
		});
590 591 592
		if (insertPromises.length === 0) {
			return Promise.resolve(false);
		}
593
		return Promise.all(insertPromises).then(() => Promise.resolve(true));
R
Ramya Achutha Rao 已提交
594 595 596
	}

	// Snippet to replace at all cursors are the same
597
	// We can pass all ranges to `editor.insertSnippet` in a single call so that
R
Ramya Achutha Rao 已提交
598 599
	// all cursors are maintained after snippet insertion
	const anyExpandAbbrInput = expandAbbrList[0];
600 601
	const expandedText = expandAbbr(anyExpandAbbrInput);
	const allRanges = expandAbbrList.map(value => {
602
		return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
R
Ramya Achutha Rao 已提交
603 604
	});
	if (expandedText) {
605
		return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
R
Ramya Achutha Rao 已提交
606
	}
607
	return Promise.resolve(false);
608 609
}

A
Aman Gupta 已提交
610 611 612 613 614 615 616 617
/*
* Walks the tree rooted at root and apply function fn on each node.
* if fn return false at any node, the further processing of tree is stopped.
*/
function walk(root: any, fn: ((node: any) => boolean)): boolean {
	let ctx = root;
	while (ctx) {

618
		const next = ctx.next;
A
Aman Gupta 已提交
619 620 621 622 623 624 625 626 627 628
		if (fn(ctx) === false || walk(ctx.firstChild, fn) === false) {
			return false;
		}

		ctx = next;
	}

	return true;
}

629
/**
630
 * Expands abbreviation as detailed in given input.
631
 */
M
Martin Aeschlimann 已提交
632
function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
633 634
	const helper = getEmmetHelper();
	const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter);
635

636
	if (input.textToWrap) {
637
		if (input.filter && input.filter.indexOf('t') > -1) {
638 639 640 641
			input.textToWrap = input.textToWrap.map(line => {
				return line.replace(trimRegex, '').trim();
			});
		}
642 643
		expandOptions['text'] = input.textToWrap;

C
ChaseKnowlden 已提交
644
		// Below fixes https://github.com/microsoft/vscode/issues/29898
645 646 647 648 649
		// 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;
		}
650 651
	}

652
	let expandedText;
653
	try {
654
		// Expand the abbreviation
655

656
		if (input.textToWrap) {
657
			const parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions);
658 659 660 661 662 663 664 665 666
			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.
667
				if (wrappingNode && inlineElements.indexOf(wrappingNode.name) === -1 && (expandOptions['profile'].hasOwnProperty('format') ? expandOptions['profile'].format : true)) {
668 669 670
					wrappingNode.value = '\n\t' + wrappingNode.value + '\n';
				}
			}
A
Aman Gupta 已提交
671 672 673 674 675 676 677 678 679 680 681 682

			// Below fixes https://github.com/microsoft/vscode/issues/78219
			// walk the tree and remove tags for empty values
			walk(parsedAbbr, node => {
				if (node.name !== null && node.value === '' && !node.isSelfClosing && node.children.length === 0) {
					node.name = '';
					node.value = '\n';
				}

				return true;
			});

683
			expandedText = helper.expandAbbreviation(parsedAbbr, expandOptions);
684 685 686
			// 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');
687 688
		} else {
			expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
689
		}
690

691 692
	} catch (e) {
		vscode.window.showErrorMessage('Failed to expand abbreviation');
693 694
	}

695
	return expandedText;
696 697
}

P
Pine Wu 已提交
698
export function getSyntaxFromArgs(args: { [x: string]: string }): string | undefined {
699
	const mappedModes = getMappingForIncludedLanguages();
700 701 702 703 704 705 706
	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;
	}

707
	let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
708 709
	if (!syntax) {
		syntax = getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
710 711
	}

712
	return syntax;
P
Pine Wu 已提交
713
}