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

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

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

20 21
export function wrapWithAbbreviation(args: any) {
	if (!validate(false) || !vscode.window.activeTextEditor) {
22 23
		return;
	}
24 25

	const editor = vscode.window.activeTextEditor;
26

27
	const syntax = getSyntaxFromArgs({ language: editor.document.languageId });
28 29 30 31 32
	if (!syntax) {
		return;
	}

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

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

R
Ramya Achutha Rao 已提交
38
		let expandAbbrList: ExpandAbbreviationInput[] = [];
39

40
		editor.selections.forEach(selection => {
41
			let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
42 43 44
			if (rangeToReplace.isEmpty) {
				rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
			}
45

46 47 48
			const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character);
			const matches = firstLineOfSelection.match(/^(\s*)/);
			const preceedingWhiteSpace = matches ? matches[1].length : 0;
49

50
			rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + preceedingWhiteSpace, rangeToReplace.end.line, rangeToReplace.end.character);
51
			expandAbbrList.push({ syntax, abbreviation, rangeToReplace, textToWrap: ['\n\t$TM_SELECTED_TEXT\n'] });
52
		});
53

54
		return expandAbbreviationInRange(editor, expandAbbrList, true);
55 56 57
	});
}

58 59
export function wrapIndividualLinesWithAbbreviation(args: any) {
	if (!validate(false) || !vscode.window.activeTextEditor) {
60 61 62 63 64 65 66 67 68
		return;
	}

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

69
	const syntax = getSyntaxFromArgs({ language: editor.document.languageId });
70 71 72 73 74
	if (!syntax) {
		return;
	}

	const abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation' });
75
	const lines = editor.document.getText(editor.selection).split('\n').map(x => x.trim());
76
	const helper = getEmmetHelper();
77

78
	return abbreviationPromise.then(inputAbbreviation => {
79
		if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) { return false; }
80

81
		let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
82
		if (!extractedResults) {
83
			return false;
84 85
		}

86
		let { abbreviation, filter } = extractedResults;
87 88 89 90
		let input: ExpandAbbreviationInput = {
			syntax,
			abbreviation,
			rangeToReplace: editor.selection,
91
			textToWrap: lines,
92
			filter
93 94 95 96 97 98 99
		};

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

}

100 101 102 103 104 105 106 107
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;
108 109 110 111 112
	} else {
		const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
		if (excludedLanguages.indexOf(vscode.window.activeTextEditor.document.languageId) > -1) {
			return fallbackTab();
		}
113
	}
114
	const syntax = getSyntaxFromArgs(args);
115
	if (!syntax) {
116
		return fallbackTab();
117
	}
118 119 120

	const editor = vscode.window.activeTextEditor;

121
	let rootNode = parseDocument(editor.document, false);
122

123 124 125 126 127
	// 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 已提交
128
	let abbreviationList: ExpandAbbreviationInput[] = [];
129 130
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;
131
	const helper = getEmmetHelper();
132

133
	let getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string] => {
R
Ramya Achutha Rao 已提交
134
		let rangeToReplace: vscode.Range = selection;
135
		let abbr = document.getText(rangeToReplace);
136
		if (!rangeToReplace.isEmpty) {
137
			let extractedResults = helper.extractAbbreviationFromText(abbr);
138
			if (extractedResults) {
139
				return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter];
140
			}
141
			return [null, '', ''];
142 143
		}

144 145 146
		const currentLine = editor.document.lineAt(position.line).text;
		const textTillPosition = currentLine.substr(0, position.character);

147 148
		// Expand cases like <div to <div></div> explicitly
		// else we will end up with <<div></div>
149
		if (syntax === 'html') {
150 151
			let matches = textTillPosition.match(/<(\w+)$/);
			if (matches) {
152 153
				abbr = matches[1];
				rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position);
154
				return [rangeToReplace, abbr, ''];
155
			}
156
		}
157
		let extractedResults = helper.extractAbbreviation(editor.document, position, false);
158
		if (!extractedResults) {
159
			return [null, '', ''];
160 161
		}

162 163
		let { abbreviationRange, abbreviation, filter } = extractedResults;
		return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter];
164 165
	};

J
jmdowns2 已提交
166 167 168 169 170 171 172 173
	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 => {
174
		let position = selection.isReversed ? selection.anchor : selection.active;
175
		let [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax);
176 177 178
		if (!rangeToReplace) {
			return;
		}
179
		if (!helper.isAbbreviationValid(syntax, abbreviation)) {
180 181
			return;
		}
182

183
		let currentNode = getNode(rootNode, position, true);
184
		if (!isValidLocationForEmmetAbbreviation(editor.document, currentNode, syntax, position, rangeToReplace)) {
185 186 187
			return;
		}

188 189 190 191
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
192
		}
193

194
		abbreviationList.push({ syntax, abbreviation, rangeToReplace, filter });
195 196
	});

197 198 199 200 201
	return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
		if (!success) {
			return fallbackTab();
		}
	});
202 203
}

204
function fallbackTab(): Thenable<boolean | undefined> {
205 206 207
	if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true) {
		return vscode.commands.executeCommand('tab');
	}
208
	return Promise.resolve(true);
209
}
210
/**
211 212
 * Checks if given position is a valid location to expand emmet abbreviation.
 * Works only on html and css/less/scss syntax
213
 * @param document current Text Document
214 215 216
 * @param currentNode parsed node at given position
 * @param syntax syntax of the abbreviation
 * @param position position to validate
217
 * @param abbreviationRange The range of the abbreviation for which given position is being validated
218
 */
219
export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean {
220
	// Continue validation only if the file was parse-able and the currentNode has been found
221
	if (!currentNode) {
222
		return true;
223 224 225
	}

	if (isStyleSheet(syntax)) {
226 227 228
		// 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 已提交
229 230
			return true;
		}
231

R
Ramya Achutha Rao 已提交
232
		const currentCssNode = <Rule>currentNode;
233

234 235 236 237 238
		// Position is valid if it occurs after the `{` that marks beginning of rule contents
		if (position.isAfter(currentCssNode.contentStartToken.end)) {
			return true;
		}

239
		// Workaround for https://github.com/Microsoft/vscode/30188
240 241
		// 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
242
		if (currentCssNode.parent
243
			&& (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule')
244
			&& currentCssNode.selectorToken
245
			&& position.line !== currentCssNode.selectorToken.end.line) {
246 247 248
			return true;
		}

249
		return false;
250 251
	}

252 253 254
	const startAngle = '<';
	const endAngle = '>';
	const escape = '\\';
R
Ramya Achutha Rao 已提交
255
	const currentHtmlNode = <HtmlNode>currentNode;
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
	const innerRange = getInnerRange(currentHtmlNode);

	// 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
	let start = innerRange.start;
	let lastChildBeforePosition = currentHtmlNode.firstChild;
	while (lastChildBeforePosition) {
		if (lastChildBeforePosition.end.isAfter(position)) {
			break;
		}
		start = lastChildBeforePosition.end;
		lastChildBeforePosition = lastChildBeforePosition.nextSibling;
	}
	let textToBackTrack = document.getText(new vscode.Range(start, abbreviationRange.start));

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

	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;
		}
		if (char !== startAngle && char !== endAngle) {
			continue;
		}
		if (i >= 0 && textToBackTrack[i] === escape) {
			i--;
			continue;
		}
		if (char === endAngle) {
			break;
		}
		if (char === startAngle) {
			valid = !foundSpace;
			break;
		}
307 308
	}

309
	return valid;
R
Ramya Achutha Rao 已提交
310 311
}

312 313
/**
 * Expands abbreviations as detailed in expandAbbrList in the editor
314 315 316
 * @param editor
 * @param expandAbbrList
 * @param insertSameSnippet
317
 * @returns false if no snippet can be inserted.
318
 */
319
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
R
Ramya Achutha Rao 已提交
320
	if (!expandAbbrList || expandAbbrList.length === 0) {
321
		return Promise.resolve(false);
R
Ramya Achutha Rao 已提交
322 323 324 325 326
	}

	// 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
327
	let insertPromises: Thenable<boolean>[] = [];
R
Ramya Achutha Rao 已提交
328 329
	if (!insertSameSnippet) {
		expandAbbrList.forEach((expandAbbrInput: ExpandAbbreviationInput) => {
330
			let expandedText = expandAbbr(expandAbbrInput);
R
Ramya Achutha Rao 已提交
331
			if (expandedText) {
332
				insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace));
R
Ramya Achutha Rao 已提交
333 334
			}
		});
335 336 337
		if (insertPromises.length === 0) {
			return Promise.resolve(false);
		}
338
		return Promise.all(insertPromises).then(() => Promise.resolve(true));
R
Ramya Achutha Rao 已提交
339 340 341
	}

	// Snippet to replace at all cursors are the same
342
	// We can pass all ranges to `editor.insertSnippet` in a single call so that
R
Ramya Achutha Rao 已提交
343 344
	// all cursors are maintained after snippet insertion
	const anyExpandAbbrInput = expandAbbrList[0];
345
	let expandedText = expandAbbr(anyExpandAbbrInput);
R
Ramya Achutha Rao 已提交
346
	let allRanges = expandAbbrList.map(value => {
347
		return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
R
Ramya Achutha Rao 已提交
348 349
	});
	if (expandedText) {
350
		return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
R
Ramya Achutha Rao 已提交
351
	}
352
	return Promise.resolve(false);
353 354
}

355
/**
356
 * Expands abbreviation as detailed in given input.
357
 */
358
function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
359 360
	const helper = getEmmetHelper();
	const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter);
361

362
	if (input.textToWrap) {
363
		if (input.filter && input.filter.indexOf('t') > -1) {
364 365 366 367
			input.textToWrap = input.textToWrap.map(line => {
				return line.replace(trimRegex, '').trim();
			});
		}
368 369 370 371 372 373 374 375
		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;
		}
376 377
	}

378
	try {
379
		// Expand the abbreviation
380
		let expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
381

382 383 384 385 386 387 388 389 390
		if (input.textToWrap) {
			// 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');

			// If the expanded text is single line then we dont need the \t and \n we added to $TM_SELECTED_TEXT earlier
			if (input.textToWrap.length === 1 && expandedText.indexOf('\n') === -1) {
				expandedText = expandedText.replace(/\s*\$TM_SELECTED_TEXT\s*/, '$TM_SELECTED_TEXT');
			}
391
		}
392

393 394
		return expandedText;

395 396
	} catch (e) {
		vscode.window.showErrorMessage('Failed to expand abbreviation');
397 398 399
	}


400 401
}

402
function getSyntaxFromArgs(args: Object): string | undefined {
403
	const mappedModes = getMappingForIncludedLanguages();
404 405 406 407 408 409 410
	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;
	}

411
	let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
412 413
	if (!syntax) {
		syntax = getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
414 415
	}

416
	return syntax;
417
}