abbreviationActions.ts 30.8 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
Raymond Zhao 已提交
7
import * as nls from 'vscode-nls';
8 9 10
import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetFlatNode';
import { getEmmetHelper, getFlatNode, getMappingForIncludedLanguages, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util';
import { getRootNode as parseDocument } from './parseDocument';
R
Raymond Zhao 已提交
11 12
import { MarkupAbbreviation } from 'emmet';
// import { AbbreviationNode } from '@emmetio/abbreviation';
13

R
Raymond Zhao 已提交
14
const localize = nls.loadMessageBundle();
U
Ubuntu 已提交
15 16
const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/;
const hexColorRegex = /^#[\da-fA-F]{0,6}$/;
R
Raymond Zhao 已提交
17 18 19 20 21
// 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'];
22

R
Ramya Achutha Rao 已提交
23
interface ExpandAbbreviationInput {
24
	syntax: string;
R
Ramya Achutha Rao 已提交
25 26
	abbreviation: string;
	rangeToReplace: vscode.Range;
27
	textToWrap?: string[];
28
	filter?: string;
R
Ramya Achutha Rao 已提交
29 30
}

31 32 33 34 35 36 37
interface PreviewRangesWithContent {
	previewRange: vscode.Range;
	originalRange: vscode.Range;
	originalContent: string;
	textToWrapInPreview: string[];
}

38
export function wrapWithAbbreviation(args: any) {
R
Raymond Zhao 已提交
39
	return doWrapping(true, args);
40 41 42 43 44 45
}

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

R
Raymond Zhao 已提交
46
function doWrapping(_: boolean, args: any) {
47
	if (!validate(false) || !vscode.window.activeTextEditor) {
48 49
		return;
	}
50 51

	const editor = vscode.window.activeTextEditor;
R
Raymond Zhao 已提交
52
	const document = editor.document;
R
Raymond Zhao 已提交
53

54 55
	args = args || {};
	if (!args['language']) {
R
Raymond Zhao 已提交
56
		args['language'] = document.languageId;
57
	}
R
Raymond Zhao 已提交
58
	// we know it's not stylesheet due to the validate(false) call above
59
	const syntax = getSyntaxFromArgs(args) || 'html';
R
Raymond Zhao 已提交
60
	const rootNode = parseDocument(document, true);
61

62
	let inPreview = false;
63 64
	let currentValue = '';
	const helper = getEmmetHelper();
65 66

	// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
67
	const rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => {
68
		let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
69
		if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
R
Raymond Zhao 已提交
70
			// in case of multi-line, exclude last empty line from rangeToReplace
71
			const previousLine = rangeToReplace.end.line - 1;
72
			const lastChar = document.lineAt(previousLine).text.length;
73 74
			rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar));
		} else if (rangeToReplace.isEmpty) {
75
			const { active } = selection;
76 77 78 79 80 81
			const activeOffset = document.offsetAt(active);
			const currentNode = getFlatNode(rootNode, activeOffset, true);
			if (currentNode) {
				const currentNodeStart = document.positionAt(currentNode.start);
				const currentNodeEnd = document.positionAt(currentNode.end);
				if (currentNodeStart.line === active.line || currentNodeEnd.line === active.line) {
R
Raymond Zhao 已提交
82
					// wrap around entire node
83 84 85
					rangeToReplace = new vscode.Range(currentNodeStart, currentNodeEnd);
				}
				else {
R
Raymond Zhao 已提交
86
					// wrap line that cursor is on
87 88
					rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, document.lineAt(rangeToReplace.start.line).text.length);
				}
89
			} else {
R
Raymond Zhao 已提交
90
				// wrap line that cursor is on
91
				rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, document.lineAt(rangeToReplace.start.line).text.length);
92 93 94
			}
		}

95
		const firstLineOfSelection = document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character);
96
		const matches = firstLineOfSelection.match(/^(\s*)/);
97 98
		const extraWhitespaceSelected = matches ? matches[1].length : 0;
		rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character);
99

100
		let textToWrapInPreview: string[];
101
		const textToReplace = document.getText(rangeToReplace);
R
Raymond Zhao 已提交
102 103 104

		// the following assumes all the lines are indented the same way as the first
		// this assumption helps with applyPreview later
R
Raymond Zhao 已提交
105 106 107
		const wholeFirstLine = document.lineAt(rangeToReplace.start).text;
		const otherMatches = wholeFirstLine.match(/^(\s*)/);
		const precedingWhitespace = otherMatches ? otherMatches[1] : '';
R
Raymond Zhao 已提交
108 109 110 111 112
		textToWrapInPreview = rangeToReplace.isSingleLine ?
			[textToReplace] :
			textToReplace.split('\n' + precedingWhitespace).map(x => x.trimEnd());

		// escape $ characters, fixes #52640
113
		textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1'));
114 115 116 117 118 119 120

		return {
			previewRange: rangeToReplace,
			originalRange: rangeToReplace,
			originalContent: textToReplace,
			textToWrapInPreview
		};
121 122
	});

R
Raymond Zhao 已提交
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
	// if a selection falls on a node, it could interfere with linked editing,
	// so back up the selections, and change selections to wrap around the node
	const oldSelections = editor.selections;
	const newSelections: vscode.Selection[] = [];
	editor.selections.forEach(selection => {
		let { start, end } = selection;
		const startOffset = document.offsetAt(start);
		const startNode = <HtmlNode>getFlatNode(rootNode, startOffset, true);
		const endOffset = document.offsetAt(end);
		const endNode = <HtmlNode>getFlatNode(rootNode, endOffset, true);
		if (startNode) {
			start = document.positionAt(startNode.start);
		}
		if (endNode) {
			end = document.positionAt(endNode.end);
		}
		// don't need to preserve active/anchor order since the selection changes
		// after wrapping anyway
		newSelections.push(new vscode.Selection(start, end));
	});
	editor.selections = newSelections;

R
Raymond Zhao 已提交
145
	function revertPreview(): Thenable<boolean> {
146
		return editor.edit(builder => {
147 148 149
			for (const rangeToReplace of rangesToReplace) {
				builder.replace(rangeToReplace.previewRange, rangeToReplace.originalContent);
				rangeToReplace.previewRange = rangeToReplace.originalRange;
150 151 152
			}
		}, { undoStopBefore: false, undoStopAfter: false });
	}
153

154
	function applyPreview(expandAbbrList: ExpandAbbreviationInput[]): Thenable<boolean> {
J
Jean Pierre 已提交
155 156
		let lastOldPreviewRange = new vscode.Range(0, 0, 0, 0);
		let lastNewPreviewRange = new vscode.Range(0, 0, 0, 0);
R
Raymond Zhao 已提交
157
		let totalNewLinesInserted = 0;
158 159

		return editor.edit(builder => {
R
Raymond Zhao 已提交
160
			// the edits are applied in order top-down
161 162 163 164 165
			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;
166 167
				}

R
Raymond Zhao 已提交
168 169
				// get the current preview range, format the new wrapped text, and then replace
				// the text in the preview range with that new text
170 171 172 173
				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];

R
Raymond Zhao 已提交
174 175
				let newText = expandedText;
				newText = newText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text
176 177 178 179
				newText = newText.replace(/\$\{[\d]*\}/g, '|'); // Removing Tabstops
				newText = newText.replace(/\$\{[\d]*(:[^}]*)?\}/g, (match) => {		// Replacing Placeholders
					return match.replace(/^\$\{[\d]*:/, '').replace('}', '');
				});
R
Raymond Zhao 已提交
180
				newText = newText.replace(/\\\$/g, '$'); // Remove backslashes before $
181 182
				builder.replace(oldPreviewRange, newText);

R
Raymond Zhao 已提交
183 184 185 186
				// calculate the new preview range to use for future previews
				// we also have to take into account that the previous expansions could:
				// - cause new lines to appear
				// - be on the same line as other expansions
187 188 189 190
				const expandedTextLines = newText.split('\n');
				const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1;
				const newLinesInserted = expandedTextLines.length - oldPreviewLines;

R
Raymond Zhao 已提交
191
				const newPreviewLineStart = oldPreviewRange.start.line + totalNewLinesInserted;
J
Jean Pierre 已提交
192
				let newPreviewStart = oldPreviewRange.start.character;
R
Raymond Zhao 已提交
193
				const newPreviewLineEnd = oldPreviewRange.end.line + totalNewLinesInserted + newLinesInserted;
J
Jean Pierre 已提交
194 195 196 197 198 199 200 201 202 203 204
				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);
205
				}
J
Jean Pierre 已提交
206 207 208 209 210 211
				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;
R
Raymond Zhao 已提交
212 213 214
				lastNewPreviewRange = new vscode.Range(newPreviewLineStart, newPreviewStart, newPreviewLineEnd, newPreviewEnd);
				rangesToReplace[i].previewRange = lastNewPreviewRange;
				totalNewLinesInserted += newLinesInserted;
215 216 217
			}
		}, { undoStopBefore: false, undoStopAfter: false });
	}
218

219
	function makeChanges(inputAbbreviation: string | undefined, definitive: boolean): Thenable<boolean> {
220
		if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) {
221
			return inPreview ? revertPreview().then(() => { return false; }) : Promise.resolve(inPreview);
222
		}
223

224
		const extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
225
		if (!extractedResults) {
226 227 228
			return Promise.resolve(inPreview);
		} else if (extractedResults.abbreviation !== inputAbbreviation) {
			// Not clear what should we do in this case. Warn the user? How?
229
		}
230

231
		const { abbreviation, filter } = extractedResults;
232
		if (definitive) {
R
Raymond Zhao 已提交
233
			const revertPromise = inPreview ? revertPreview() : Promise.resolve(true);
234 235
			return revertPromise.then(() => {
				const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
236
					const rangeToReplace = rangesAndContent.originalRange;
237
					let textToWrap: string[];
R
Raymond Zhao 已提交
238 239 240 241 242 243 244
					// if (individualLines) {
					textToWrap = rangesAndContent.textToWrapInPreview;
					// } else {
					// 	// use the p tag as a dummy element to get Emmet to wrap the expression properly
					// 	textToWrap = rangeToReplace.isSingleLine ?
					// 		['$TM_SELECTED_TEXT'] : ['<p>$TM_SELECTED_TEXT</p>'];
					// }
245
					return { syntax: syntax || '', abbreviation, rangeToReplace, textToWrap, filter };
246
				});
R
Raymond Zhao 已提交
247
				return expandAbbreviationInRange(editor, expandAbbrList, false).then(() => { return true; });
248 249
			});
		}
250

251
		const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
252
			return { syntax: syntax || '', abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter };
253
		});
254

255
		return applyPreview(expandAbbrList);
256 257
	}

258 259 260 261
	function inputChanged(value: string): string {
		if (value !== currentValue) {
			currentValue = value;
			makeChanges(value, false).then((out) => {
R
Raymond Zhao 已提交
262
				inPreview = out;
263 264
			});
		}
265
		return '';
266
	}
R
Raymond Zhao 已提交
267

R
Raymond Zhao 已提交
268
	const prompt = localize('wrapWithAbbreviationPrompt', "Enter Abbreviation");
R
Raymond Zhao 已提交
269 270
	const abbreviationPromise: Thenable<string | undefined> = (args && args['abbreviation']) ?
		Promise.resolve(args['abbreviation']) :
R
Raymond Zhao 已提交
271
		vscode.window.showInputBox({ prompt, validateInput: inputChanged });
R
Raymond Zhao 已提交
272 273 274 275 276 277
	return abbreviationPromise.then(async (inputAbbreviation) => {
		const changesWereMade = await makeChanges(inputAbbreviation, true);
		if (!changesWereMade) {
			editor.selections = oldSelections;
		}
		return changesWereMade;
278
	});
279 280
}

281 282 283 284 285
export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> {
	if (!validate() || !vscode.window.activeTextEditor) {
		return fallbackTab();
	}

P
Pine Wu 已提交
286 287 288
	/**
	 * Short circuit the parsing. If previous character is space, do not expand.
	 */
P
Pine Wu 已提交
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
	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();
		}
	}

304 305 306
	args = args || {};
	if (!args['language']) {
		args['language'] = vscode.window.activeTextEditor.document.languageId;
307 308 309 310 311
	} else {
		const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
		if (excludedLanguages.indexOf(vscode.window.activeTextEditor.document.languageId) > -1) {
			return fallbackTab();
		}
312
	}
313
	const syntax = getSyntaxFromArgs(args);
314
	if (!syntax) {
315
		return fallbackTab();
316
	}
317 318

	const editor = vscode.window.activeTextEditor;
319

320 321 322 323 324
	// 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();
	}

325
	const abbreviationList: ExpandAbbreviationInput[] = [];
326 327
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;
328
	const helper = getEmmetHelper();
329

330
	const getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string] => {
M
Martin Aeschlimann 已提交
331
		position = document.validatePosition(position);
R
Ramya Achutha Rao 已提交
332
		let rangeToReplace: vscode.Range = selection;
333
		let abbr = document.getText(rangeToReplace);
334
		if (!rangeToReplace.isEmpty) {
335
			const extractedResults = helper.extractAbbreviationFromText(abbr);
336
			if (extractedResults) {
337
				return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter];
338
			}
339
			return [null, '', ''];
340 341
		}

342 343 344
		const currentLine = editor.document.lineAt(position.line).text;
		const textTillPosition = currentLine.substr(0, position.character);

345 346
		// Expand cases like <div to <div></div> explicitly
		// else we will end up with <<div></div>
347
		if (syntax === 'html') {
348
			const matches = textTillPosition.match(/<(\w+)$/);
349
			if (matches) {
350 351
				abbr = matches[1];
				rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position);
352
				return [rangeToReplace, abbr, ''];
353
			}
354
		}
355
		const extractedResults = helper.extractAbbreviation(toLSTextDocument(editor.document), position, { lookAhead: false });
356
		if (!extractedResults) {
357
			return [null, '', ''];
358 359
		}

360
		const { abbreviationRange, abbreviation, filter } = extractedResults;
361
		return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter];
362 363
	};

364
	const selectionsInReverseOrder = editor.selections.slice(0);
J
jmdowns2 已提交
365
	selectionsInReverseOrder.sort((a, b) => {
M
Matt Bierner 已提交
366 367
		const posA = a.isReversed ? a.anchor : a.active;
		const posB = b.isReversed ? b.anchor : b.active;
J
jmdowns2 已提交
368 369 370
		return posA.compareTo(posB) * -1;
	});

P
Pine Wu 已提交
371 372 373 374 375 376
	let rootNode: Node | undefined;
	function getRootNode() {
		if (rootNode) {
			return rootNode;
		}

377
		const usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;
P
Pine Wu 已提交
378 379 380
		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 {
381
			rootNode = parseDocument(editor.document, true);
P
Pine Wu 已提交
382 383 384 385 386
		}

		return rootNode;
	}

J
jmdowns2 已提交
387
	selectionsInReverseOrder.forEach(selection => {
388 389
		const position = selection.isReversed ? selection.anchor : selection.active;
		const [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax);
390 391 392
		if (!rangeToReplace) {
			return;
		}
393
		if (!helper.isAbbreviationValid(syntax, abbreviation)) {
394 395
			return;
		}
396 397
		const offset = editor.document.offsetAt(position);
		let currentNode = getFlatNode(getRootNode(), offset, true);
398 399 400 401
		let validateLocation = true;
		let syntaxToUse = syntax;

		if (editor.document.languageId === 'html') {
402
			if (isStyleAttribute(currentNode, offset)) {
403 404 405 406 407
				syntaxToUse = 'css';
				validateLocation = false;
			} else {
				const embeddedCssNode = getEmbeddedCssNodeIfAny(editor.document, currentNode, position);
				if (embeddedCssNode) {
408
					currentNode = getFlatNode(embeddedCssNode, offset, true);
409 410 411 412
					syntaxToUse = 'css';
				}
			}
		}
413

414
		if (validateLocation && !isValidLocationForEmmetAbbreviation(editor.document, getRootNode(), currentNode, syntaxToUse, offset, rangeToReplace)) {
415 416 417
			return;
		}

418 419 420 421
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
422
		}
423

424
		abbreviationList.push({ syntax: syntaxToUse, abbreviation, rangeToReplace, filter });
425 426
	});

427
	return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
428
		return success ? Promise.resolve(undefined) : fallbackTab();
429
	});
430 431
}

432
function fallbackTab(): Thenable<boolean | undefined> {
433 434 435
	if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true) {
		return vscode.commands.executeCommand('tab');
	}
436
	return Promise.resolve(true);
437
}
438
/**
439 440
 * Checks if given position is a valid location to expand emmet abbreviation.
 * Works only on html and css/less/scss syntax
441
 * @param document current Text Document
442
 * @param rootNode parsed document
443
 * @param currentNode current node in the parsed document
444 445
 * @param syntax syntax of the abbreviation
 * @param position position to validate
446
 * @param abbreviationRange The range of the abbreviation for which given position is being validated
447
 */
448
export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | undefined, syntax: string, offset: number, abbreviationRange: vscode.Range): boolean {
449
	if (isStyleSheet(syntax)) {
450
		const stylesheet = <Stylesheet>rootNode;
451
		if (stylesheet && (stylesheet.comments || []).some(x => offset >= x.start && offset <= x.end)) {
452 453
			return false;
		}
454 455 456 457 458
		// Continue validation only if the file was parse-able and the currentNode has been found
		if (!currentNode) {
			return true;
		}

C
ChaseKnowlden 已提交
459
		// Fix for https://github.com/microsoft/vscode/issues/34162
460 461
		// Other than sass, stylus, we can make use of the terminator tokens to validate position
		if (syntax !== 'sass' && syntax !== 'stylus' && currentNode.type === 'property') {
462 463 464 465 466 467 468 469

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

470
			const abbreviation = document.getText(new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character));
471 472 473
			const propertyNode = <Property>currentNode;
			if (propertyNode.terminatorToken
				&& propertyNode.separator
474 475
				&& offset >= propertyNode.separatorToken.end
				&& offset <= propertyNode.terminatorToken.start
476
				&& abbreviation.indexOf(':') === -1) {
477
				return hexColorRegex.test(abbreviation) || abbreviation === '!';
478 479 480
			}
			if (!propertyNode.terminatorToken
				&& propertyNode.separator
481
				&& offset >= propertyNode.separatorToken.end
482
				&& abbreviation.indexOf(':') === -1) {
483
				return hexColorRegex.test(abbreviation) || abbreviation === '!';
484
			}
485 486 487
			if (hexColorRegex.test(abbreviation) || abbreviation === '!') {
				return false;
			}
488 489
		}

490 491 492
		// 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 已提交
493 494
			return true;
		}
495

R
Ramya Achutha Rao 已提交
496
		const currentCssNode = <Rule>currentNode;
497

498
		// Position is valid if it occurs after the `{` that marks beginning of rule contents
499
		if (offset > currentCssNode.contentStartToken.end) {
500 501 502
			return true;
		}

C
ChaseKnowlden 已提交
503
		// Workaround for https://github.com/microsoft/vscode/30188
504 505
		// 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
506
		if (currentCssNode.parent
507
			&& (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule')
508 509 510 511 512 513 514 515 516 517
			&& currentCssNode.selectorToken) {
			const position = document.positionAt(offset);
			const tokenStartPos = document.positionAt(currentCssNode.selectorToken.start);
			const tokenEndPos = document.positionAt(currentCssNode.selectorToken.end);
			if (position.line !== tokenEndPos.line
				&& tokenStartPos.character === abbreviationRange.start.character
				&& tokenStartPos.line === abbreviationRange.start.line
			) {
				return true;
			}
518 519
		}

520
		return false;
521 522
	}

523 524 525
	const startAngle = '<';
	const endAngle = '>';
	const escape = '\\';
526
	const question = '?';
R
Ramya Achutha Rao 已提交
527
	const currentHtmlNode = <HtmlNode>currentNode;
528
	let start = 0;
529

530
	if (currentHtmlNode) {
531
		if (currentHtmlNode.name === 'script') {
532 533 534 535 536 537 538 539 540 541 542 543
			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;
544 545
		}

C
ChaseKnowlden 已提交
546
		// Fix for https://github.com/microsoft/vscode/issues/28829
547 548
		if (!currentHtmlNode.open || !currentHtmlNode.close ||
			!(currentHtmlNode.open.end <= offset && offset <= currentHtmlNode.close.start)) {
549 550 551
			return false;
		}

C
ChaseKnowlden 已提交
552
		// Fix for https://github.com/microsoft/vscode/issues/35128
553 554
		// Find the position up till where we will backtrack looking for unescaped < or >
		// to decide if current position is valid for emmet expansion
555
		start = currentHtmlNode.open.end;
556 557
		let lastChildBeforePosition = currentHtmlNode.firstChild;
		while (lastChildBeforePosition) {
558
			if (lastChildBeforePosition.end > offset) {
559 560 561 562
				break;
			}
			start = lastChildBeforePosition.end;
			lastChildBeforePosition = lastChildBeforePosition.nextSibling;
563 564
		}
	}
565 566
	const startPos = document.positionAt(start);
	let textToBackTrack = document.getText(new vscode.Range(startPos.line, startPos.character, abbreviationRange.start.line, abbreviationRange.start.character));
567 568 569 570 571 572 573

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

574 575 576 577
	if (!textToBackTrack.trim()) {
		return true;
	}

578
	let valid = true;
579
	let foundSpace = false; // If < is found before finding whitespace, then its valid abbreviation. E.g.: <div|
580
	let i = textToBackTrack.length - 1;
R
Ramya Achutha Rao 已提交
581 582 583 584
	if (textToBackTrack[i] === startAngle) {
		return false;
	}

585 586 587 588 589 590 591
	while (i >= 0) {
		const char = textToBackTrack[i];
		i--;
		if (!foundSpace && /\s/.test(char)) {
			foundSpace = true;
			continue;
		}
592 593 594 595
		if (char === question && textToBackTrack[i] === startAngle) {
			i--;
			continue;
		}
C
ChaseKnowlden 已提交
596
		// Fix for https://github.com/microsoft/vscode/issues/55411
597 598 599 600 601
		// A space is not a valid character right after < in a tag name.
		if (/\s/.test(char) && textToBackTrack[i] === startAngle) {
			i--;
			continue;
		}
602 603 604 605 606 607 608 609
		if (char !== startAngle && char !== endAngle) {
			continue;
		}
		if (i >= 0 && textToBackTrack[i] === escape) {
			i--;
			continue;
		}
		if (char === endAngle) {
610 611 612 613 614
			if (i >= 0 && textToBackTrack[i] === '=') {
				continue; // False alarm of cases like =>
			} else {
				break;
			}
615 616 617 618 619
		}
		if (char === startAngle) {
			valid = !foundSpace;
			break;
		}
620 621
	}

622
	return valid;
R
Ramya Achutha Rao 已提交
623 624
}

625 626
/**
 * Expands abbreviations as detailed in expandAbbrList in the editor
627
 *
628
 * @returns false if no snippet can be inserted.
629
 */
630
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
R
Ramya Achutha Rao 已提交
631
	if (!expandAbbrList || expandAbbrList.length === 0) {
632
		return Promise.resolve(false);
R
Ramya Achutha Rao 已提交
633 634 635 636 637
	}

	// 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
638
	const insertPromises: Thenable<boolean>[] = [];
R
Ramya Achutha Rao 已提交
639
	if (!insertSameSnippet) {
640
		expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); }).forEach((expandAbbrInput: ExpandAbbreviationInput) => {
R
Raymond Zhao 已提交
641
			const expandedText = expandAbbr(expandAbbrInput);
R
Ramya Achutha Rao 已提交
642
			if (expandedText) {
643
				insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false }));
R
Ramya Achutha Rao 已提交
644 645
			}
		});
646 647 648
		if (insertPromises.length === 0) {
			return Promise.resolve(false);
		}
649
		return Promise.all(insertPromises).then(() => Promise.resolve(true));
R
Ramya Achutha Rao 已提交
650 651 652
	}

	// Snippet to replace at all cursors are the same
653
	// We can pass all ranges to `editor.insertSnippet` in a single call so that
R
Ramya Achutha Rao 已提交
654 655
	// all cursors are maintained after snippet insertion
	const anyExpandAbbrInput = expandAbbrList[0];
656 657
	const expandedText = expandAbbr(anyExpandAbbrInput);
	const allRanges = expandAbbrList.map(value => {
658
		return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
R
Ramya Achutha Rao 已提交
659 660
	});
	if (expandedText) {
661
		return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
R
Ramya Achutha Rao 已提交
662
	}
663
	return Promise.resolve(false);
664 665
}

R
Raymond Zhao 已提交
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
// /*
// * 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: AbbreviationNode, fn: ((node: AbbreviationNode) => boolean)): boolean {
// 	if (fn(root) === false || walkChildren(root.children, fn) === false) {
// 		return false;
// 	}
// 	return true;
// }

// function walkChildren(children: AbbreviationNode[], fn: ((node: AbbreviationNode) => boolean)): boolean {
// 	for (let i = 0; i < children.length; i++) {
// 		const child = children[i];
// 		if (walk(child, fn) === false) {
// 			return false;
// 		}
// 	}
// 	return true;
// }
A
Aman Gupta 已提交
686

687
/**
688
 * Expands abbreviation as detailed in given input.
689
 */
M
Martin Aeschlimann 已提交
690
function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
691 692
	const helper = getEmmetHelper();
	const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter);
693

694
	if (input.textToWrap) {
R
Raymond Zhao 已提交
695
		if (input.filter && input.filter.includes('t')) {
696 697 698 699
			input.textToWrap = input.textToWrap.map(line => {
				return line.replace(trimRegex, '').trim();
			});
		}
700 701
		expandOptions['text'] = input.textToWrap;

C
ChaseKnowlden 已提交
702
		// Below fixes https://github.com/microsoft/vscode/issues/29898
703 704
		// With this, Emmet formats inline elements as block elements
		// ensuring the wrapped multi line text does not get merged to a single line
R
Raymond Zhao 已提交
705 706
		if (!input.rangeToReplace.isSingleLine && expandOptions.options) {
			expandOptions.options['output.inlineBreak'] = 1;
707
		}
708 709
	}

710
	let expandedText;
711
	try {
712
		// Expand the abbreviation
R
Raymond Zhao 已提交
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
		if (input.textToWrap && !isStyleSheet(input.syntax)) {
			const parsedAbbr = <MarkupAbbreviation>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).
			// 	const wrappingNodeChildren = parsedAbbr.children;
			// 	let wrappingNode = wrappingNodeChildren[wrappingNodeChildren.length - 1];
			// 	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.
			// 	// const format = expandOptions.options ? (expandOptions.options['output.format'] ?? true) : true;
			// 	// if (wrappingNode && wrappingNode.name && wrappingNode.value
			// 	// 	&& inlineElements.indexOf(wrappingNode.name) === -1
			// 	// 	&& format) {
			// 	// 	wrappingNode.value[0] = '\n\t' + wrappingNode.value[0] + '\n';
			// 	// }
			// }
A
Aman Gupta 已提交
731 732 733

			// Below fixes https://github.com/microsoft/vscode/issues/78219
			// walk the tree and remove tags for empty values
R
Raymond Zhao 已提交
734 735 736 737 738 739 740
			// walkChildren(parsedAbbr.children, node => {
			// 	if (node.name !== null && node.value && node.value[0] === '' && !node.selfClosing && node.children.length === 0) {
			// 		node.name = '';
			// 		node.value[0] = '\n';
			// 	}
			// 	return true;
			// });
A
Aman Gupta 已提交
741

742
			expandedText = helper.expandAbbreviation(parsedAbbr, expandOptions);
743 744
			// 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
R
Raymond Zhao 已提交
745
			expandedText = expandedText.replace('<p>\\$TM_SELECTED_TEXT</p>', '$TM_SELECTED_TEXT');
746 747
		} else {
			expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
748
		}
749

750 751
	} catch (e) {
		vscode.window.showErrorMessage('Failed to expand abbreviation');
752 753
	}

754
	return expandedText;
755 756
}

P
Pine Wu 已提交
757
export function getSyntaxFromArgs(args: { [x: string]: string }): string | undefined {
758
	const mappedModes = getMappingForIncludedLanguages();
759 760 761 762 763 764 765
	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;
	}

766
	let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
767 768
	if (!syntax) {
		syntax = getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
769 770
	}

771
	return syntax;
P
Pine Wu 已提交
772
}