abbreviationActions.ts 7.3 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';
8 9
import parseStylesheet from '@emmetio/css-parser';
import parse from '@emmetio/html-matcher';
R
Ramya Achutha Rao 已提交
10
import { Node, HtmlNode, Rule } from 'EmmetNode';
11
import { getNode, getInnerRange } from './util';
12
import { getExpandOptions, extractAbbreviation, isStyleSheet, isAbbreviationValid } from 'vscode-emmet-helper';
13
import { DocumentStreamReader } from './bufferStream';
14

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

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

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

R
Ramya Achutha Rao 已提交
31
	vscode.window.showInputBox({ prompt: 'Enter Abbreviation' }).then(abbreviation => {
32
		if (!abbreviation || !abbreviation.trim() || !isAbbreviationValid(syntax, abbreviation)) { return; }
33

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

39
		editor.selections.forEach(selection => {
40
			let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
41 42 43
			if (rangeToReplace.isEmpty) {
				rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
			}
44 45 46 47 48 49 50 51 52
			const firstLine = editor.document.lineAt(rangeToReplace.start).text;
			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);
			}

53
			let textToWrap = newLine;
54
			for (let i = rangeToReplace.start.line; i <= rangeToReplace.end.line; i++) {
55
				textToWrap += '\t' + editor.document.lineAt(i).text.substr(preceedingWhiteSpace.length) + newLine;
56
			}
57 58

			if (!firstTextToReplace) {
R
Ramya Achutha Rao 已提交
59 60
				firstTextToReplace = textToWrap;
			} else if (allTextToReplaceSame && firstTextToReplace !== textToWrap) {
61 62 63
				allTextToReplaceSame = false;
			}

64
			expandAbbrList.push({ syntax, abbreviation, rangeToReplace, textToWrap });
65
		});
66

67
		expandAbbreviationInRange(editor, expandAbbrList, syntax, allTextToReplaceSame, preceedingWhiteSpace);
68 69 70
	});
}

71
export function expandAbbreviation(args) {
72 73
	const syntax = getSyntaxFromArgs(args);
	if (!syntax) {
74 75
		return;
	}
76 77 78

	const editor = vscode.window.activeTextEditor;

79
	let parseContent = isStyleSheet(syntax) ? parseStylesheet : parse;
80
	let rootNode: Node = parseContent(new DocumentStreamReader(editor.document));
81

R
Ramya Achutha Rao 已提交
82
	let abbreviationList: ExpandAbbreviationInput[] = [];
83 84 85
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;

86
	editor.selections.forEach(selection => {
R
Ramya Achutha Rao 已提交
87
		let rangeToReplace: vscode.Range = selection;
88
		let position = selection.isReversed ? selection.anchor : selection.active;
R
Ramya Achutha Rao 已提交
89 90 91
		let abbreviation = editor.document.getText(rangeToReplace);
		if (rangeToReplace.isEmpty) {
			[rangeToReplace, abbreviation] = extractAbbreviation(editor.document, position);
92
		}
93 94 95
		if (!isAbbreviationValid(syntax, abbreviation)) {
			return;
		}
96

97 98 99 100 101
		let currentNode = getNode(rootNode, position);
		if (!isValidLocationForEmmetAbbreviation(currentNode, syntax, position)) {
			return;
		}

102 103 104 105
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
106
		}
107

108
		abbreviationList.push({ syntax, abbreviation, rangeToReplace });
109 110
	});

111
	expandAbbreviationInRange(editor, abbreviationList, syntax, allAbbreviationsSame);
112 113 114 115
}


/**
116 117
 * Checks if given position is a valid location to expand emmet abbreviation.
 * Works only on html and css/less/scss syntax
118 119 120 121
 * @param currentNode parsed node at given position
 * @param syntax syntax of the abbreviation
 * @param position position to validate
 */
122
export function isValidLocationForEmmetAbbreviation(currentNode: Node, syntax: string, position: vscode.Position): boolean {
123 124 125 126 127
	if (!currentNode) {
		return true;
	}

	if (isStyleSheet(syntax)) {
R
Ramya Achutha Rao 已提交
128 129 130 131 132
		if (currentNode.type !== 'rule') {
			return true;
		}
		const currentCssNode = <Rule>currentNode;
		return currentCssNode.selectorToken && position.isAfter(currentCssNode.selectorToken.end);
133 134
	}

R
Ramya Achutha Rao 已提交
135 136 137
	const currentHtmlNode = <HtmlNode>currentNode;
	if (currentHtmlNode.close) {
		return getInnerRange(currentHtmlNode).contains(position);
138 139 140
	}

	return false;
R
Ramya Achutha Rao 已提交
141 142
}

143 144
/**
 * Expands abbreviations as detailed in expandAbbrList in the editor
145 146 147 148 149
 * @param editor
 * @param expandAbbrList
 * @param syntax
 * @param insertSameSnippet
 * @param preceedingWhiteSpace
150
 */
151
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], syntax: string, insertSameSnippet: boolean, preceedingWhiteSpace: string = '') {
R
Ramya Achutha Rao 已提交
152 153 154
	if (!expandAbbrList || expandAbbrList.length === 0) {
		return;
	}
155
	const newLine = editor.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n';
R
Ramya Achutha Rao 已提交
156 157 158 159 160 161

	// 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
	if (!insertSameSnippet) {
		expandAbbrList.forEach((expandAbbrInput: ExpandAbbreviationInput) => {
162
			let expandedText = expandAbbr(expandAbbrInput, preceedingWhiteSpace, newLine);
R
Ramya Achutha Rao 已提交
163 164 165 166 167 168 169 170
			if (expandedText) {
				editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace);
			}
		});
		return;
	}

	// Snippet to replace at all cursors are the same
171
	// We can pass all ranges to `editor.insertSnippet` in a single call so that
R
Ramya Achutha Rao 已提交
172 173
	// all cursors are maintained after snippet insertion
	const anyExpandAbbrInput = expandAbbrList[0];
174
	let expandedText = expandAbbr(anyExpandAbbrInput, preceedingWhiteSpace, newLine);
R
Ramya Achutha Rao 已提交
175 176 177 178 179 180
	let allRanges = expandAbbrList.map(value => {
		return value.rangeToReplace;
	});
	if (expandedText) {
		editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
	}
181 182
}

183
/**
184
 * Expands abbreviation as detailed in given input.
185 186 187
 * If there is textToWrap, then given preceedingWhiteSpace is applied
 */
function expandAbbr(input: ExpandAbbreviationInput, preceedingWhiteSpace: string, newLine: string): string {
188

189
	let expandedText = expand(input.abbreviation, getExpandOptions(input.syntax, input.textToWrap));
190 191 192 193
	if (!expandedText) {
		return;
	}

194 195
	if (!input.textToWrap) {
		return expandedText;
196
	}
197 198

	return expandedText.split(newLine).map(line => preceedingWhiteSpace + line).join(newLine);
199 200 201 202 203 204 205 206 207 208 209 210 211
}

function getSyntaxFromArgs(args: any): string {
	let editor = vscode.window.activeTextEditor;
	if (!editor) {
		vscode.window.showInformationMessage('No editor is active.');
		return;
	}
	if (typeof args !== 'object' || !args['syntax']) {
		vscode.window.showInformationMessage('Cannot resolve language at cursor.');
		return;
	}
	return args['syntax'];
212
}