abbreviationActions.ts 5.8 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 10
import parseStylesheet from '@emmetio/css-parser';
import parse from '@emmetio/html-matcher';
import Node from '@emmetio/node';
11
import { getSyntax, getNode, getInnerRange } from './util';
12
import { getExpandOptions, extractAbbreviation, isStyleSheet } from 'vscode-emmet-helper';
13
import { DocumentStreamReader } from './bufferStream';
14 15 16 17 18 19 20

export function wrapWithAbbreviation() {
	let editor = vscode.window.activeTextEditor;
	if (!editor) {
		vscode.window.showInformationMessage('No editor is active');
		return;
	}
21
	let syntax = getSyntax(editor.document);
22 23 24

	vscode.window.showInputBox({ prompt: 'Enter Abbreviation' }).then(abbr => {
		if (!abbr || !abbr.trim()) { return; }
25 26 27 28 29

		let textToReplaceList: [string, vscode.Range][] = [];
		let firstTextToReplace: string;
		let allTextToReplaceSame: boolean = true;

30 31 32 33 34 35
		editor.selections.forEach(selection => {
			let rangeToReplace: vscode.Range = selection;
			if (rangeToReplace.isEmpty) {
				rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
			}
			let textToReplace = editor.document.getText(rangeToReplace);
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

			if (!firstTextToReplace) {
				firstTextToReplace = textToReplace;
			} else if (allTextToReplaceSame && firstTextToReplace !== textToReplace) {
				allTextToReplaceSame = false;
			}

			textToReplaceList.push([textToReplace, rangeToReplace]);
		});

		if (textToReplaceList.length === 0) {
			return;
		}

		// Text 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 (!allTextToReplaceSame) {
			textToReplaceList.forEach(([textToReplace, rangeToReplace]) => {
				let expandedText = expand(abbr, getExpandOptions(syntax, textToReplace));
				if (expandedText) {
					editor.insertSnippet(new vscode.SnippetString(expandedText), rangeToReplace);
				}
			});
			return;
		}

		// Text to replace at all cursors are the same
		// We can pass all ranges to `editor.insertSnippet` in a single call so that 
		// all cursors are maintained after snippet insertion
		let expandedText = expand(abbr, getExpandOptions(syntax, textToReplaceList[0][0]));
		let allRanges = textToReplaceList.map(value => {
			return value[1];
69
		});
70 71 72 73
		if (expandedText) {
			editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
		}

74 75 76
	});
}

77 78
export function expandAbbreviation(args) {

79 80 81 82 83
	let editor = vscode.window.activeTextEditor;
	if (!editor) {
		vscode.window.showInformationMessage('No editor is active');
		return;
	}
84
	if (typeof args !== 'object' || !args['syntax']) {
85 86
		return;
	}
87
	let syntax = args['syntax'];
88
	let parseContent = isStyleSheet(syntax) ? parseStylesheet : parse;
89
	let rootNode: Node = parseContent(new DocumentStreamReader(editor.document));
90

91 92 93 94
	let abbreviationList: [string, vscode.Range][] = [];
	let firstAbbreviation: string;
	let allAbbreviationsSame: boolean = true;

95 96 97 98 99 100 101
	editor.selections.forEach(selection => {
		let abbreviationRange: vscode.Range = selection;
		let position = selection.isReversed ? selection.anchor : selection.active;
		let abbreviation = editor.document.getText(abbreviationRange);
		if (abbreviationRange.isEmpty) {
			[abbreviationRange, abbreviation] = extractAbbreviation(editor.document, position);
		}
102

103 104 105 106 107
		let currentNode = getNode(rootNode, position);
		if (!isValidLocationForEmmetAbbreviation(currentNode, syntax, position)) {
			return;
		}

108 109 110 111
		if (!firstAbbreviation) {
			firstAbbreviation = abbreviation;
		} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
			allAbbreviationsSame = false;
112
		}
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139

		abbreviationList.push([abbreviation, abbreviationRange]);
	});

	if (abbreviationList.length === 0) {
		return;
	}

	// Abbreviations at multiple cursors are not the same
	// `editor.insertSnippet` will have to be called for each abbreviation separately
	// We will not be able to maintain multiple cursors after snippet insertion
	if (!allAbbreviationsSame) {
		abbreviationList.forEach(([abbreviation, abbreviationRange]) => {
			let expandedText = expand(abbreviation, getExpandOptions(syntax));
			if (expandedText) {
				editor.insertSnippet(new vscode.SnippetString(expandedText), abbreviationRange);
			}
		});
		return;
	}

	// Abbreviations at all cursors are the same
	// We can pass all ranges to `editor.insertSnippet` in a single call so that 
	// all cursors are maintained after snippet insertion
	let expandedText = expand(abbreviationList[0][0], getExpandOptions(syntax));
	let allRanges = abbreviationList.map(value => {
		return value[1];
140
	});
141 142 143 144 145
	if (expandedText) {
		editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
	}


146 147 148 149
}


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

	if (isStyleSheet(syntax)) {
		return currentNode.type !== 'rule'
			|| (currentNode.selectorToken && position.isAfter(currentNode.selectorToken.end));
	}

	if (currentNode.close) {
		return getInnerRange(currentNode).contains(position);
	}

	return false;
171
}