abbreviationActions.ts 11.1 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  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';
import { expand } from '@emmetio/expand-abbreviation';
R
Ramya Achutha Rao 已提交
8
import { Node, HtmlNode, Rule } from 'EmmetNode';
9
import { getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate } from './util';
10
import { getExpandOptions, extractAbbreviation, isStyleSheet, isAbbreviationValid, getEmmetMode } from 'vscode-emmet-helper';
11

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

20 21
const selectedTextToWrap = '\n\$TM_SELECTED_TEXT\n';

22 23
export function wrapWithAbbreviation(args) {
	const syntax = getSyntaxFromArgs(args);
24
	if (!syntax || !validate()) {
25 26
		return;
	}
27 28

	const editor = vscode.window.activeTextEditor;
29
	const newLine = editor.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n';
30

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

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

R
Ramya Achutha Rao 已提交
36
		let expandAbbrList: ExpandAbbreviationInput[] = [];
37 38 39
		let firstTextToReplace: string;
		let allTextToReplaceSame: boolean = true;

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
			const firstLine = editor.document.lineAt(rangeToReplace.start).text;
46
			const firstLineTillSelection = firstLine.substr(0, rangeToReplace.start.character);
47
			const whitespaceBeforeSelection = /^\s*$/.test(firstLineTillSelection);
48 49 50
			let textToWrap = '';
			let preceedingWhiteSpace = '';

51
			if (whitespaceBeforeSelection) {
52 53 54 55 56 57 58 59 60 61 62 63 64 65
				const matches = firstLine.match(/^(\s*)/);
				if (matches) {
					preceedingWhiteSpace = matches[1];
				}
				if (rangeToReplace.start.character <= preceedingWhiteSpace.length) {
					rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.end.line, rangeToReplace.end.character);
				}

				textToWrap = newLine;
				for (let i = rangeToReplace.start.line; i <= rangeToReplace.end.line; i++) {
					textToWrap += '\t' + editor.document.lineAt(i).text.substr(preceedingWhiteSpace.length) + newLine;
				}
			} else {
				textToWrap = editor.document.getText(rangeToReplace);
66
			}
67 68

			if (!firstTextToReplace) {
R
Ramya Achutha Rao 已提交
69 70
				firstTextToReplace = textToWrap;
			} else if (allTextToReplaceSame && firstTextToReplace !== textToWrap) {
71 72 73
				allTextToReplaceSame = false;
			}

74
			expandAbbrList.push({ syntax, abbreviation, rangeToReplace, textToWrap, preceedingWhiteSpace });
75
		});
76

77 78
		if (!allTextToReplaceSame) {
			expandAbbrList.forEach(input => {
79
				input.textToWrap = selectedTextToWrap;
80 81 82
			});
		}

83
		return expandAbbreviationInRange(editor, expandAbbrList, true);
84 85 86
	});
}

87
export function expandAbbreviation(args) {
88
	const syntax = getSyntaxFromArgs(args);
89
	if (!syntax || !validate()) {
90 91
		return;
	}
92 93 94

	const editor = vscode.window.activeTextEditor;

95
	let rootNode = parseDocument(editor.document);
96 97 98
	if (!rootNode) {
		return;
	}
99

R
Ramya Achutha Rao 已提交
100
	let abbreviationList: ExpandAbbreviationInput[] = [];
101 102 103
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;

R
Ramya Achutha Rao 已提交
104
	let getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, isHtml: boolean): [vscode.Range, string] => {
R
Ramya Achutha Rao 已提交
105
		let rangeToReplace: vscode.Range = selection;
R
Ramya Achutha Rao 已提交
106
		let abbreviation = document.getText(rangeToReplace);
107
		if (!rangeToReplace.isEmpty) {
R
Ramya Achutha Rao 已提交
108
			return [rangeToReplace, abbreviation];
109 110 111 112 113 114 115 116 117
		}

		// Expand cases like <div to <div></div> explicitly
		// else we will end up with <<div></div>
		if (isHtml) {
			const currentLine = editor.document.lineAt(position.line).text;
			const textTillPosition = currentLine.substr(0, position.character);
			let matches = textTillPosition.match(/<(\w+)$/);
			if (matches) {
R
Ramya Achutha Rao 已提交
118
				abbreviation = matches[1];
119
				rangeToReplace = new vscode.Range(position.translate(0, -(abbreviation.length + 1)), position);
R
Ramya Achutha Rao 已提交
120
				return [rangeToReplace, abbreviation];
121
			}
122
		}
123 124 125 126 127
		return extractAbbreviation(editor.document, position);
	};

	editor.selections.forEach(selection => {
		let position = selection.isReversed ? selection.anchor : selection.active;
R
Ramya Achutha Rao 已提交
128
		let [rangeToReplace, abbreviation] = getAbbreviation(editor.document, selection, position, syntax === 'html');
129
		if (!isAbbreviationValid(syntax, abbreviation)) {
130
			vscode.window.showErrorMessage('Emmet: Invalid abbreviation');
131 132
			return;
		}
133

134 135 136 137 138
		let currentNode = getNode(rootNode, position);
		if (!isValidLocationForEmmetAbbreviation(currentNode, syntax, position)) {
			return;
		}

139 140 141 142
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
143
		}
144

145
		abbreviationList.push({ syntax, abbreviation, rangeToReplace });
146 147
	});

148
	return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame);
149 150 151 152
}


/**
153 154
 * Checks if given position is a valid location to expand emmet abbreviation.
 * Works only on html and css/less/scss syntax
155 156 157 158
 * @param currentNode parsed node at given position
 * @param syntax syntax of the abbreviation
 * @param position position to validate
 */
159
export function isValidLocationForEmmetAbbreviation(currentNode: Node, syntax: string, position: vscode.Position): boolean {
160
	if (!currentNode) {
161
		return !isStyleSheet(syntax);
162 163 164
	}

	if (isStyleSheet(syntax)) {
R
Ramya Achutha Rao 已提交
165 166 167 168
		if (currentNode.type !== 'rule') {
			return true;
		}
		const currentCssNode = <Rule>currentNode;
169 170 171 172 173 174 175 176 177 178

		// Workaround for https://github.com/Microsoft/vscode/30188
		if (currentCssNode.parent
			&& currentCssNode.parent.type === 'rule'
			&& currentCssNode.selectorToken
			&& currentCssNode.selectorToken.start.line !== currentCssNode.selectorToken.end.line) {
			return true;
		}

		// Position is valid if it occurs after the `{` that marks beginning of rule contents
R
Ramya Achutha Rao 已提交
179
		return currentCssNode.selectorToken && position.isAfter(currentCssNode.selectorToken.end);
180 181
	}

R
Ramya Achutha Rao 已提交
182 183 184
	const currentHtmlNode = <HtmlNode>currentNode;
	if (currentHtmlNode.close) {
		return getInnerRange(currentHtmlNode).contains(position);
185 186 187
	}

	return false;
R
Ramya Achutha Rao 已提交
188 189
}

190 191
/**
 * Expands abbreviations as detailed in expandAbbrList in the editor
192 193 194
 * @param editor
 * @param expandAbbrList
 * @param insertSameSnippet
195
 */
196
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
R
Ramya Achutha Rao 已提交
197 198 199
	if (!expandAbbrList || expandAbbrList.length === 0) {
		return;
	}
200
	const newLine = editor.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n';
R
Ramya Achutha Rao 已提交
201 202 203 204

	// 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
205
	let insertPromises = [];
R
Ramya Achutha Rao 已提交
206 207
	if (!insertSameSnippet) {
		expandAbbrList.forEach((expandAbbrInput: ExpandAbbreviationInput) => {
208
			let expandedText = expandAbbr(expandAbbrInput, newLine);
R
Ramya Achutha Rao 已提交
209
			if (expandedText) {
210
				insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace));
R
Ramya Achutha Rao 已提交
211 212
			}
		});
213
		return Promise.all(insertPromises).then(() => Promise.resolve(true));
R
Ramya Achutha Rao 已提交
214 215 216
	}

	// Snippet to replace at all cursors are the same
217
	// We can pass all ranges to `editor.insertSnippet` in a single call so that
R
Ramya Achutha Rao 已提交
218 219
	// all cursors are maintained after snippet insertion
	const anyExpandAbbrInput = expandAbbrList[0];
220
	let expandedText = expandAbbr(anyExpandAbbrInput, newLine);
R
Ramya Achutha Rao 已提交
221
	let allRanges = expandAbbrList.map(value => {
222
		return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
R
Ramya Achutha Rao 已提交
223 224
	});
	if (expandedText) {
225
		return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
R
Ramya Achutha Rao 已提交
226
	}
227 228
}

229
/**
230
 * Expands abbreviation as detailed in given input.
231 232
 * If there is textToWrap, then given preceedingWhiteSpace is applied
 */
233
function expandAbbr(input: ExpandAbbreviationInput, newLine: string): string {
234 235
	const emmetConfig = vscode.workspace.getConfiguration('emmet');
	const expandOptions = getExpandOptions(emmetConfig['syntaxProfiles'], emmetConfig['variables'], input.syntax, input.textToWrap);
236 237

	// Below fixes https://github.com/Microsoft/vscode/issues/29898
238
	// With this, Emmet formats inline elements as block elements
239 240
	// ensuring the wrapped multi line text does not get merged to a single line
	if (input.textToWrap && !input.rangeToReplace.isSingleLine) {
241 242 243
		expandOptions.profile['inlineBreak'] = 1;
	}

244
	// Expand the abbreviation
245 246
	let expandedText;
	try {
247
		expandedText = expand(input.abbreviation, expandOptions);
248
		if (input.textToWrap && input.textToWrap !== selectedTextToWrap) {
249
			expandedText = expandedText.replace(/(\$[^\{])/g, '\\$&');
250
		}
251 252 253 254
	} catch (e) {
		vscode.window.showErrorMessage('Failed to expand abbreviation');
	}

255 256 257 258
	if (!expandedText) {
		return;
	}

259
	// If no text to wrap, then return the expanded text
260
	if (!input.textToWrap || !input.preceedingWhiteSpace) {
261
		return expandedText;
262
	}
263

264 265 266
	// There was text to wrap, and the final expanded text is multi line
	// So add the preceedingWhiteSpace to each line
	if (expandedText.indexOf('\n') > -1) {
267
		return expandedText.split(newLine).map(line => input.preceedingWhiteSpace + line).join(newLine);
268 269 270 271 272 273 274 275 276 277
	}

	// There was text to wrap and the final expanded text is single line
	// This can happen when the abbreviation was for an inline element
	// Remove the preceeding newLine + tab and the ending newLine, that was added to textToWrap
	// And re-expand the abbreviation
	let regex = newLine === '\n' ? /^\n\t(.*)\n$/ : /^\r\n\t(.*)\r\n$/;
	let matches = input.textToWrap.match(regex);
	if (matches) {
		input.textToWrap = matches[1];
278
		return expandAbbr(input, newLine);
279 280
	}

281
	return input.preceedingWhiteSpace + expandedText;
282 283 284 285 286 287 288 289
}

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

	const mappedModes = getMappingForIncludedLanguages();
292 293
	let language: string = (!args || typeof args !== 'object' || !args['language']) ? editor.document.languageId : args['language'];
	let parentMode: string = (args && typeof args === 'object') ? args['parentMode'] : undefined;
294 295
	let excludedLanguages = vscode.workspace.getConfiguration('emmet')['exlcudeLanguages'] ? vscode.workspace.getConfiguration('emmet')['exlcudeLanguages'] : [];
	let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
296 297 298 299
	if (syntax) {
		return syntax;
	}

300
	return getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
301
}