abbreviationActions.ts 5.6 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 11 12 13 14
import * as extract from '@emmetio/extract-abbreviation';
import parseStylesheet from '@emmetio/css-parser';
import parse from '@emmetio/html-matcher';
import Node from '@emmetio/node';

import { getSyntax, getProfile, isStyleSheet, getNode, getInnerRange } from './util';
import { DocumentStreamReader } from './bufferStream';
15 16 17 18 19 20 21 22 23 24 25 26 27 28

const field = (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}`;

export function wrapWithAbbreviation() {
	let editor = vscode.window.activeTextEditor;
	if (!editor) {
		vscode.window.showInformationMessage('No editor is active');
		return;
	}
	let rangeToReplace: vscode.Range = editor.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);
29
	let syntax = getSyntax(editor.document);
30 31
	let options = {
		field: field,
32
		syntax: syntax,
33
		profile: getProfile(getSyntax(editor.document)),
34 35
		text: textToReplace,
		addons: syntax === 'jsx' ? { 'jsx': syntax === 'jsx' } : null
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
	};

	vscode.window.showInputBox({ prompt: 'Enter Abbreviation' }).then(abbr => {
		if (!abbr || !abbr.trim()) { return; }
		let expandedText = expand(abbr, options);
		editor.insertSnippet(new vscode.SnippetString(expandedText), rangeToReplace);
	});
}

export function expandAbbreviation() {
	let editor = vscode.window.activeTextEditor;
	if (!editor) {
		vscode.window.showInformationMessage('No editor is active');
		return;
	}
51
	let syntax = getSyntax(editor.document);
52 53 54 55 56 57 58 59 60 61
	let mappedSyntax = false;
	let emmetConfig = vscode.workspace.getConfiguration('emmet');
	if (emmetConfig && emmetConfig['syntaxProfiles']) {
		let syntaxProfiles = emmetConfig['syntaxProfiles'];
		if (typeof syntaxProfiles[syntax] === 'string') {
			syntax = syntaxProfiles[syntax];
			mappedSyntax = true;
		}
	}
	let output = expandAbbreviationHelper(syntax, editor.document, editor.selection, mappedSyntax);
62
	if (output) {
63
		editor.insertSnippet(new vscode.SnippetString(output.expandedText), output.abbreviationRange);
64 65 66 67 68
	}
}

export interface ExpandAbbreviationHelperOutput {
	expandedText: string;
69
	abbreviationRange: vscode.Range;
70 71 72 73 74 75
	abbreviation: string;
	syntax: string;
}

/**
 * Expands abbreviation at given range in the given document
76 77 78 79 80 81 82 83 84 85
 * @param syntax string syntax to be used for expanding abbreviations
 * @param document vscode.TextDocument
 * @param abbreviationRange vscode.Range range of the abbreviation that needs to be expanded
 * @param mappedSyntax Boolean Pass true if given document language was mapped to given syntax to get emmet abbreviation expansions.
 * */
export function expandAbbreviationHelper(syntax: string, document: vscode.TextDocument, abbreviationRange: vscode.Range, mappedSyntax: boolean): ExpandAbbreviationHelperOutput {
	if (!mappedSyntax) {
		let parseContent = isStyleSheet(syntax) ? parseStylesheet : parse;
		let rootNode: Node = parseContent(new DocumentStreamReader(document));
		let currentNode = getNode(rootNode, abbreviationRange.end);
86

87 88 89 90 91
		if (forceCssSyntax(syntax, currentNode, abbreviationRange.end)) {
			syntax = 'css';
		} else if (!isValidLocationForEmmetAbbreviation(currentNode, syntax, abbreviationRange.end)) {
			return;
		}
92 93
	}

94 95 96
	let abbreviation = document.getText(abbreviationRange);
	if (abbreviationRange.isEmpty) {
		[abbreviationRange, abbreviation] = extractAbbreviation(document, abbreviationRange.start);
97
	}
98

99 100
	let options = {
		field: field,
101
		syntax: syntax,
102
		profile: getProfile(syntax),
103
		addons: syntax === 'jsx' ? { 'jsx': true } : null
104 105
	};

106
	let expandedText = expand(abbreviation, options);
107
	return { expandedText, abbreviationRange, abbreviation, syntax };
108 109 110 111 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 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
}

/**
 * Extracts abbreviation from the given position in the given document
 */
function extractAbbreviation(document: vscode.TextDocument, position: vscode.Position): [vscode.Range, string] {
	let currentLine = document.lineAt(position.line).text;
	let result = extract(currentLine, position.character, true);
	if (!result) {
		return [null, ''];
	}

	let rangeToReplace = new vscode.Range(position.line, result.location, position.line, result.location + result.abbreviation.length);
	return [rangeToReplace, result.abbreviation];
}

/**
 * Inside <style> tag, force use of css abbreviations
 */
function forceCssSyntax(syntax: string, currentNode: Node, position: vscode.Position): boolean {
	return !isStyleSheet(syntax)
		&& currentNode
		&& currentNode.close
		&& currentNode.name === 'style'
		&& getInnerRange(currentNode).contains(position);
}

/**
 * Checks if given position is a valid location to expand emmet abbreviation
 * @param currentNode parsed node at given position
 * @param syntax syntax of the abbreviation
 * @param position position to validate
 */
function isValidLocationForEmmetAbbreviation(currentNode: Node, syntax: string, position: vscode.Position): boolean {
	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;
156
}