abbreviationActions.ts 12.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';
7
import { Node, HtmlNode, Rule, Property } from 'EmmetNode';
8
import { getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration } from './util';
9
import { getExpandOptions, extractAbbreviation, extractAbbreviationFromText, isStyleSheet, isAbbreviationValid, getEmmetMode, expandAbbreviation } from 'vscode-emmet-helper';
10

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

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

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

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

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

R
Ramya Achutha Rao 已提交
33
		let expandAbbrList: ExpandAbbreviationInput[] = [];
34

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

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

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

49
		return expandAbbreviationInRange(editor, expandAbbrList, true);
50 51 52
	});
}

53 54 55 56 57 58 59 60 61 62 63 64
export function wrapIndividualLinesWithAbbreviation(args) {
	if (!validate(false)) {
		return;
	}

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

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

68 69
	return abbreviationPromise.then(inputAbbreviation => {
		if (!inputAbbreviation || !inputAbbreviation.trim() || !isAbbreviationValid(syntax, inputAbbreviation)) { return; }
70

71 72 73 74 75
		let extractedResults = extractAbbreviationFromText(inputAbbreviation);
		if (!extractedResults) {
			return;
		}

76
		let { abbreviation, filter } = extractedResults;
77 78 79 80
		let input: ExpandAbbreviationInput = {
			syntax,
			abbreviation,
			rangeToReplace: editor.selection,
81
			textToWrap: lines,
82
			filter
83 84 85 86 87 88 89
		};

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

}

90
export function expandEmmetAbbreviation(args): Thenable<boolean> {
91 92
	const syntax = getSyntaxFromArgs(args);
	if (!syntax || !validate()) {
93
		return fallbackTab();
94
	}
95 96 97

	const editor = vscode.window.activeTextEditor;

98
	let rootNode = parseDocument(editor.document, false);
99

100 101 102 103 104
	// 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 已提交
105
	let abbreviationList: ExpandAbbreviationInput[] = [];
106 107 108
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;

109
	let getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range, string, string] => {
R
Ramya Achutha Rao 已提交
110
		let rangeToReplace: vscode.Range = selection;
111
		let abbr = document.getText(rangeToReplace);
112
		if (!rangeToReplace.isEmpty) {
113 114
			let extractedResults = extractAbbreviationFromText(abbr);
			if (extractedResults) {
115
				return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter];
116
			}
117
			return [null, '', ''];
118 119
		}

120 121 122
		const currentLine = editor.document.lineAt(position.line).text;
		const textTillPosition = currentLine.substr(0, position.character);

123 124
		// Expand cases like <div to <div></div> explicitly
		// else we will end up with <<div></div>
125
		if (syntax === 'html') {
126 127
			let matches = textTillPosition.match(/<(\w+)$/);
			if (matches) {
128 129
				abbr = matches[1];
				rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position);
130
				return [rangeToReplace, abbr, ''];
131
			}
132
		}
133
		let extractedResults = extractAbbreviation(editor.document, position, false);
134
		if (!extractedResults) {
135
			return [null, '', ''];
136 137
		}

138 139
		let { abbreviationRange, abbreviation, filter } = extractedResults;
		return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter];
140 141 142 143
	};

	editor.selections.forEach(selection => {
		let position = selection.isReversed ? selection.anchor : selection.active;
144
		let [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax);
145 146 147
		if (!rangeToReplace) {
			return;
		}
148 149 150
		if (!isAbbreviationValid(syntax, abbreviation)) {
			return;
		}
151

152 153
		let currentNode = getNode(rootNode, position, true);
		if (!isValidLocationForEmmetAbbreviation(currentNode, syntax, position, abbreviation)) {
154 155 156
			return;
		}

157 158 159 160
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
161
		}
162

163
		abbreviationList.push({ syntax, abbreviation, rangeToReplace, filter });
164 165
	});

166 167 168 169 170
	return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
		if (!success) {
			return fallbackTab();
		}
	});
171 172
}

173 174 175 176 177
function fallbackTab(): Thenable<boolean> {
	if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true) {
		return vscode.commands.executeCommand('tab');
	}
}
178
/**
179 180
 * Checks if given position is a valid location to expand emmet abbreviation.
 * Works only on html and css/less/scss syntax
181 182 183 184
 * @param currentNode parsed node at given position
 * @param syntax syntax of the abbreviation
 * @param position position to validate
 */
185
export function isValidLocationForEmmetAbbreviation(currentNode: Node, syntax: string, position: vscode.Position, abbreviation: string): boolean {
186
	// Continue validation only if the file was parse-able and the currentNode has been found
187
	if (!currentNode) {
188
		return true;
189 190 191
	}

	if (isStyleSheet(syntax)) {
192

193 194 195 196 197 198 199 200
		// CSS Emmet snippets are for property-value or in case of colors, just value
		if (currentNode.type === 'property' && (<Property>currentNode).value) {
			if (position.isBefore((<Property>currentNode).valueToken.start)) {
				return false;
			}
			return /^#\d+$/.test(abbreviation);
		}

201 202 203
		// 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 已提交
204 205
			return true;
		}
206

R
Ramya Achutha Rao 已提交
207
		const currentCssNode = <Rule>currentNode;
208

209 210 211 212 213
		// Position is valid if it occurs after the `{` that marks beginning of rule contents
		if (position.isAfter(currentCssNode.contentStartToken.end)) {
			return true;
		}

214
		// Workaround for https://github.com/Microsoft/vscode/30188
215 216
		// 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
217
		if (currentCssNode.parent
218
			&& (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule')
219
			&& currentCssNode.selectorToken
220
			&& position.line !== currentCssNode.selectorToken.end.line) {
221 222 223
			return true;
		}

224
		return false;
225 226
	}

R
Ramya Achutha Rao 已提交
227 228 229
	const currentHtmlNode = <HtmlNode>currentNode;
	if (currentHtmlNode.close) {
		return getInnerRange(currentHtmlNode).contains(position);
230 231 232
	}

	return false;
R
Ramya Achutha Rao 已提交
233 234
}

235 236
/**
 * Expands abbreviations as detailed in expandAbbrList in the editor
237 238 239
 * @param editor
 * @param expandAbbrList
 * @param insertSameSnippet
240
 * @returns false if no snippet can be inserted.
241
 */
242
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
R
Ramya Achutha Rao 已提交
243
	if (!expandAbbrList || expandAbbrList.length === 0) {
244
		return Promise.resolve(false);
R
Ramya Achutha Rao 已提交
245 246 247 248 249
	}

	// 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
250
	let insertPromises = [];
R
Ramya Achutha Rao 已提交
251 252
	if (!insertSameSnippet) {
		expandAbbrList.forEach((expandAbbrInput: ExpandAbbreviationInput) => {
253
			let expandedText = expandAbbr(expandAbbrInput);
R
Ramya Achutha Rao 已提交
254
			if (expandedText) {
255
				insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace));
R
Ramya Achutha Rao 已提交
256 257
			}
		});
258 259 260
		if (insertPromises.length === 0) {
			return Promise.resolve(false);
		}
261
		return Promise.all(insertPromises).then(() => Promise.resolve(true));
R
Ramya Achutha Rao 已提交
262 263 264
	}

	// Snippet to replace at all cursors are the same
265
	// We can pass all ranges to `editor.insertSnippet` in a single call so that
R
Ramya Achutha Rao 已提交
266 267
	// all cursors are maintained after snippet insertion
	const anyExpandAbbrInput = expandAbbrList[0];
268
	let expandedText = expandAbbr(anyExpandAbbrInput);
R
Ramya Achutha Rao 已提交
269
	let allRanges = expandAbbrList.map(value => {
270
		return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
R
Ramya Achutha Rao 已提交
271 272
	});
	if (expandedText) {
273
		return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
R
Ramya Achutha Rao 已提交
274
	}
275
	return Promise.resolve(false);
276 277
}

278
/**
279
 * Expands abbreviation as detailed in given input.
280
 */
281
function expandAbbr(input: ExpandAbbreviationInput): string {
282
	const expandOptions = getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter);
283

284
	if (input.textToWrap) {
285
		if (input.filter && input.filter.indexOf('t') > -1) {
286 287 288 289
			input.textToWrap = input.textToWrap.map(line => {
				return line.replace(trimRegex, '').trim();
			});
		}
290 291 292 293 294 295 296 297
		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;
		}
298 299
	}

300
	try {
301
		// Expand the abbreviation
302
		let expandedText = expandAbbreviation(input.abbreviation, expandOptions);
303

304 305 306 307 308 309 310 311 312
		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');
			}
313
		}
314

315 316
		return expandedText;

317 318
	} catch (e) {
		vscode.window.showErrorMessage('Failed to expand abbreviation');
319 320 321
	}


322 323 324 325 326 327 328 329
}

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

	const mappedModes = getMappingForIncludedLanguages();
332 333
	let language: string = (!args || typeof args !== 'object' || !args['language']) ? editor.document.languageId : args['language'];
	let parentMode: string = (args && typeof args === 'object') ? args['parentMode'] : undefined;
334
	let excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
335
	let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
336 337
	if (!syntax) {
		syntax = getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
338 339
	}

340
	return syntax;
341
}