Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@ -0,0 +1,712 @@
/*---------------------------------------------------------------------------------------------
* 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 { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetNode';
import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util';
const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/;
const hexColorRegex = /^#[\da-fA-F]{0,6}$/;
const inlineElements = ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo',
'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i',
'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q',
's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup',
'textarea', 'tt', 'u', 'var'];
interface ExpandAbbreviationInput {
syntax: string;
abbreviation: string;
rangeToReplace: vscode.Range;
textToWrap?: string[];
filter?: string;
}
interface PreviewRangesWithContent {
previewRange: vscode.Range;
originalRange: vscode.Range;
originalContent: string;
textToWrapInPreview: string[];
}
export function wrapWithAbbreviation(args: any) {
return doWrapping(false, args);
}
export function wrapIndividualLinesWithAbbreviation(args: any) {
return doWrapping(true, args);
}
function doWrapping(individualLines: boolean, args: any) {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
if (individualLines) {
if (editor.selections.length === 1 && editor.selection.isEmpty) {
vscode.window.showInformationMessage('Select more than 1 line and try again.');
return;
}
if (editor.selections.find(x => x.isEmpty)) {
vscode.window.showInformationMessage('Select more than 1 line in each selection and try again.');
return;
}
}
args = args || {};
if (!args['language']) {
args['language'] = editor.document.languageId;
}
const syntax = getSyntaxFromArgs(args) || 'html';
const rootNode = parseDocument(editor.document, false);
let inPreview = false;
let currentValue = '';
const helper = getEmmetHelper();
// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
const rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => {
let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
const previousLine = rangeToReplace.end.line - 1;
const lastChar = editor.document.lineAt(previousLine).text.length;
rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar));
} else if (rangeToReplace.isEmpty) {
const { active } = selection;
const currentNode = getNode(rootNode, active, true);
if (currentNode && (currentNode.start.line === active.line || currentNode.end.line === active.line)) {
rangeToReplace = new vscode.Range(currentNode.start, currentNode.end);
} else {
rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
}
}
const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character);
const matches = firstLineOfSelection.match(/^(\s*)/);
const extraWhitespaceSelected = matches ? matches[1].length : 0;
rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character);
let textToWrapInPreview: string[];
const textToReplace = editor.document.getText(rangeToReplace);
if (individualLines) {
textToWrapInPreview = textToReplace.split('\n').map(x => x.trim());
} else {
const wholeFirstLine = editor.document.lineAt(rangeToReplace.start).text;
const otherMatches = wholeFirstLine.match(/^(\s*)/);
const precedingWhitespace = otherMatches ? otherMatches[1] : '';
textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + precedingWhitespace).join('\n\t') + '\n'];
}
textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1'));
return {
previewRange: rangeToReplace,
originalRange: rangeToReplace,
originalContent: textToReplace,
textToWrapInPreview
};
});
function revertPreview(): Thenable<any> {
return editor.edit(builder => {
for (const rangeToReplace of rangesToReplace) {
builder.replace(rangeToReplace.previewRange, rangeToReplace.originalContent);
rangeToReplace.previewRange = rangeToReplace.originalRange;
}
}, { undoStopBefore: false, undoStopAfter: false });
}
function applyPreview(expandAbbrList: ExpandAbbreviationInput[]): Thenable<boolean> {
let lastOldPreviewRange = new vscode.Range(0, 0, 0, 0);
let lastNewPreviewRange = new vscode.Range(0, 0, 0, 0);
let totalLinesInserted = 0;
return editor.edit(builder => {
for (let i = 0; i < rangesToReplace.length; i++) {
const expandedText = expandAbbr(expandAbbrList[i]) || '';
if (!expandedText) {
// Failed to expand text. We already showed an error inside expandAbbr.
break;
}
const oldPreviewRange = rangesToReplace[i].previewRange;
const preceedingText = editor.document.getText(new vscode.Range(oldPreviewRange.start.line, 0, oldPreviewRange.start.line, oldPreviewRange.start.character));
const indentPrefix = (preceedingText.match(/^(\s*)/) || ['', ''])[1];
let newText = expandedText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text
newText = newText.replace(/\$\{[\d]*\}/g, '|'); // Removing Tabstops
newText = newText.replace(/\$\{[\d]*(:[^}]*)?\}/g, (match) => { // Replacing Placeholders
return match.replace(/^\$\{[\d]*:/, '').replace('}', '');
});
builder.replace(oldPreviewRange, newText);
const expandedTextLines = newText.split('\n');
const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1;
const newLinesInserted = expandedTextLines.length - oldPreviewLines;
const newPreviewLineStart = oldPreviewRange.start.line + totalLinesInserted;
let newPreviewStart = oldPreviewRange.start.character;
const newPreviewLineEnd = oldPreviewRange.end.line + totalLinesInserted + newLinesInserted;
let newPreviewEnd = expandedTextLines[expandedTextLines.length - 1].length;
if (i > 0 && newPreviewLineEnd === lastNewPreviewRange.end.line) {
// If newPreviewLineEnd is equal to the previous expandedText lineEnd,
// set newPreviewStart to the length of the previous expandedText in that line
// plus the number of characters between both selections.
newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
newPreviewEnd += newPreviewStart;
}
else if (i > 0 && newPreviewLineStart === lastNewPreviewRange.end.line) {
// Same as above but expandedTextLines.length > 1 so newPreviewEnd keeps its value.
newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
}
else if (expandedTextLines.length === 1) {
// If the expandedText is single line, add the length of preceeding text as it will not be included in line length.
newPreviewEnd += oldPreviewRange.start.character;
}
lastOldPreviewRange = rangesToReplace[i].previewRange;
rangesToReplace[i].previewRange = lastNewPreviewRange = new vscode.Range(newPreviewLineStart, newPreviewStart, newPreviewLineEnd, newPreviewEnd);
totalLinesInserted += newLinesInserted;
}
}, { undoStopBefore: false, undoStopAfter: false });
}
function makeChanges(inputAbbreviation: string | undefined, definitive: boolean): Thenable<boolean> {
if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) {
return inPreview ? revertPreview().then(() => { return false; }) : Promise.resolve(inPreview);
}
const extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
if (!extractedResults) {
return Promise.resolve(inPreview);
} else if (extractedResults.abbreviation !== inputAbbreviation) {
// Not clear what should we do in this case. Warn the user? How?
}
const { abbreviation, filter } = extractedResults;
if (definitive) {
const revertPromise = inPreview ? revertPreview() : Promise.resolve();
return revertPromise.then(() => {
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
const rangeToReplace = rangesAndContent.originalRange;
let textToWrap: string[];
if (individualLines) {
textToWrap = rangesAndContent.textToWrapInPreview;
} else {
textToWrap = rangeToReplace.isSingleLine ? ['$TM_SELECTED_TEXT'] : ['\n\t$TM_SELECTED_TEXT\n'];
}
return { syntax: syntax || '', abbreviation, rangeToReplace, textToWrap, filter };
});
return expandAbbreviationInRange(editor, expandAbbrList, !individualLines).then(() => { return true; });
});
}
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
return { syntax: syntax || '', abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter };
});
return applyPreview(expandAbbrList);
}
function inputChanged(value: string): string {
if (value !== currentValue) {
currentValue = value;
makeChanges(value, false).then((out) => {
if (typeof out === 'boolean') {
inPreview = out;
}
});
}
return '';
}
const abbreviationPromise: Thenable<string | undefined> = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation', validateInput: inputChanged });
return abbreviationPromise.then(inputAbbreviation => {
return makeChanges(inputAbbreviation, true);
});
}
export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> {
if (!validate() || !vscode.window.activeTextEditor) {
return fallbackTab();
}
/**
* Short circuit the parsing. If previous character is space, do not expand.
*/
if (vscode.window.activeTextEditor.selections.length === 1 &&
vscode.window.activeTextEditor.selection.isEmpty
) {
const anchor = vscode.window.activeTextEditor.selection.anchor;
if (anchor.character === 0) {
return fallbackTab();
}
const prevPositionAnchor = anchor.translate(0, -1);
const prevText = vscode.window.activeTextEditor.document.getText(new vscode.Range(prevPositionAnchor, anchor));
if (prevText === ' ' || prevText === '\t') {
return fallbackTab();
}
}
args = args || {};
if (!args['language']) {
args['language'] = vscode.window.activeTextEditor.document.languageId;
} else {
const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
if (excludedLanguages.indexOf(vscode.window.activeTextEditor.document.languageId) > -1) {
return fallbackTab();
}
}
const syntax = getSyntaxFromArgs(args);
if (!syntax) {
return fallbackTab();
}
const editor = vscode.window.activeTextEditor;
// 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();
}
const abbreviationList: ExpandAbbreviationInput[] = [];
let firstAbbreviation: string;
let allAbbreviationsSame: boolean = true;
const helper = getEmmetHelper();
const getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string] => {
position = document.validatePosition(position);
let rangeToReplace: vscode.Range = selection;
let abbr = document.getText(rangeToReplace);
if (!rangeToReplace.isEmpty) {
const extractedResults = helper.extractAbbreviationFromText(abbr);
if (extractedResults) {
return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter];
}
return [null, '', ''];
}
const currentLine = editor.document.lineAt(position.line).text;
const textTillPosition = currentLine.substr(0, position.character);
// Expand cases like <div to <div></div> explicitly
// else we will end up with <<div></div>
if (syntax === 'html') {
const matches = textTillPosition.match(/<(\w+)$/);
if (matches) {
abbr = matches[1];
rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position);
return [rangeToReplace, abbr, ''];
}
}
const extractedResults = helper.extractAbbreviation(toLSTextDocument(editor.document), position, { lookAhead: false });
if (!extractedResults) {
return [null, '', ''];
}
const { abbreviationRange, abbreviation, filter } = extractedResults;
return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter];
};
const selectionsInReverseOrder = editor.selections.slice(0);
selectionsInReverseOrder.sort((a, b) => {
const posA = a.isReversed ? a.anchor : a.active;
const posB = b.isReversed ? b.anchor : b.active;
return posA.compareTo(posB) * -1;
});
let rootNode: Node | undefined;
function getRootNode() {
if (rootNode) {
return rootNode;
}
const usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;
if (editor.selections.length === 1 && isStyleSheet(editor.document.languageId) && usePartialParsing && editor.document.lineCount > 1000) {
rootNode = parsePartialStylesheet(editor.document, editor.selection.isReversed ? editor.selection.anchor : editor.selection.active);
} else {
rootNode = parseDocument(editor.document, false);
}
return rootNode;
}
selectionsInReverseOrder.forEach(selection => {
const position = selection.isReversed ? selection.anchor : selection.active;
const [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax);
if (!rangeToReplace) {
return;
}
if (!helper.isAbbreviationValid(syntax, abbreviation)) {
return;
}
let currentNode = getNode(getRootNode(), position, true);
let validateLocation = true;
let syntaxToUse = syntax;
if (editor.document.languageId === 'html') {
if (isStyleAttribute(currentNode, position)) {
syntaxToUse = 'css';
validateLocation = false;
} else {
const embeddedCssNode = getEmbeddedCssNodeIfAny(editor.document, currentNode, position);
if (embeddedCssNode) {
currentNode = getNode(embeddedCssNode, position, true);
syntaxToUse = 'css';
}
}
}
if (validateLocation && !isValidLocationForEmmetAbbreviation(editor.document, getRootNode(), currentNode, syntaxToUse, position, rangeToReplace)) {
return;
}
if (!firstAbbreviation) {
firstAbbreviation = abbreviation;
} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
allAbbreviationsSame = false;
}
abbreviationList.push({ syntax: syntaxToUse, abbreviation, rangeToReplace, filter });
});
return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
return success ? Promise.resolve(undefined) : fallbackTab();
});
}
function fallbackTab(): Thenable<boolean | undefined> {
if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true) {
return vscode.commands.executeCommand('tab');
}
return Promise.resolve(true);
}
/**
* Checks if given position is a valid location to expand emmet abbreviation.
* Works only on html and css/less/scss syntax
* @param document current Text Document
* @param rootNode parsed document
* @param currentNode current node in the parsed document
* @param syntax syntax of the abbreviation
* @param position position to validate
* @param abbreviationRange The range of the abbreviation for which given position is being validated
*/
export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean {
if (isStyleSheet(syntax)) {
const stylesheet = <Stylesheet>rootNode;
if (stylesheet && (stylesheet.comments || []).some(x => position.isAfterOrEqual(x.start) && position.isBeforeOrEqual(x.end))) {
return false;
}
// Continue validation only if the file was parse-able and the currentNode has been found
if (!currentNode) {
return true;
}
// Fix for https://github.com/microsoft/vscode/issues/34162
// Other than sass, stylus, we can make use of the terminator tokens to validate position
if (syntax !== 'sass' && syntax !== 'stylus' && currentNode.type === 'property') {
// Fix for upstream issue https://github.com/emmetio/css-parser/issues/3
if (currentNode.parent
&& currentNode.parent.type !== 'rule'
&& currentNode.parent.type !== 'at-rule') {
return false;
}
const abbreviation = document.getText(new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character));
const propertyNode = <Property>currentNode;
if (propertyNode.terminatorToken
&& propertyNode.separator
&& position.isAfterOrEqual(propertyNode.separatorToken.end)
&& position.isBeforeOrEqual(propertyNode.terminatorToken.start)
&& abbreviation.indexOf(':') === -1) {
return hexColorRegex.test(abbreviation) || abbreviation === '!';
}
if (!propertyNode.terminatorToken
&& propertyNode.separator
&& position.isAfterOrEqual(propertyNode.separatorToken.end)
&& abbreviation.indexOf(':') === -1) {
return hexColorRegex.test(abbreviation) || abbreviation === '!';
}
if (hexColorRegex.test(abbreviation) || abbreviation === '!') {
return false;
}
}
// 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') {
return true;
}
const currentCssNode = <Rule>currentNode;
// Position is valid if it occurs after the `{` that marks beginning of rule contents
if (position.isAfter(currentCssNode.contentStartToken.end)) {
return true;
}
// Workaround for https://github.com/microsoft/vscode/30188
// 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
if (currentCssNode.parent
&& (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule')
&& currentCssNode.selectorToken
&& position.line !== currentCssNode.selectorToken.end.line
&& currentCssNode.selectorToken.start.character === abbreviationRange.start.character
&& currentCssNode.selectorToken.start.line === abbreviationRange.start.line
) {
return true;
}
return false;
}
const startAngle = '<';
const endAngle = '>';
const escape = '\\';
const question = '?';
const currentHtmlNode = <HtmlNode>currentNode;
let start = new vscode.Position(0, 0);
if (currentHtmlNode) {
if (currentHtmlNode.name === 'script') {
const typeAttribute = (currentHtmlNode.attributes || []).filter(x => x.name.toString() === 'type')[0];
const typeValue = typeAttribute ? typeAttribute.value.toString() : '';
if (allowedMimeTypesInScriptTag.indexOf(typeValue) > -1) {
return true;
}
const isScriptJavascriptType = !typeValue || typeValue === 'application/javascript' || typeValue === 'text/javascript';
if (isScriptJavascriptType) {
return !!getSyntaxFromArgs({ language: 'javascript' });
}
return false;
}
const innerRange = getInnerRange(currentHtmlNode);
// Fix for https://github.com/microsoft/vscode/issues/28829
if (!innerRange || !innerRange.contains(position)) {
return false;
}
// Fix for https://github.com/microsoft/vscode/issues/35128
// Find the position up till where we will backtrack looking for unescaped < or >
// to decide if current position is valid for emmet expansion
start = innerRange.start;
let lastChildBeforePosition = currentHtmlNode.firstChild;
while (lastChildBeforePosition) {
if (lastChildBeforePosition.end.isAfter(position)) {
break;
}
start = lastChildBeforePosition.end;
lastChildBeforePosition = lastChildBeforePosition.nextSibling;
}
}
let textToBackTrack = document.getText(new vscode.Range(start.line, start.character, abbreviationRange.start.line, abbreviationRange.start.character));
// Worse case scenario is when cursor is inside a big chunk of text which needs to backtracked
// Backtrack only 500 offsets to ensure we dont waste time doing this
if (textToBackTrack.length > 500) {
textToBackTrack = textToBackTrack.substr(textToBackTrack.length - 500);
}
if (!textToBackTrack.trim()) {
return true;
}
let valid = true;
let foundSpace = false; // If < is found before finding whitespace, then its valid abbreviation. E.g.: <div|
let i = textToBackTrack.length - 1;
if (textToBackTrack[i] === startAngle) {
return false;
}
while (i >= 0) {
const char = textToBackTrack[i];
i--;
if (!foundSpace && /\s/.test(char)) {
foundSpace = true;
continue;
}
if (char === question && textToBackTrack[i] === startAngle) {
i--;
continue;
}
// Fix for https://github.com/microsoft/vscode/issues/55411
// A space is not a valid character right after < in a tag name.
if (/\s/.test(char) && textToBackTrack[i] === startAngle) {
i--;
continue;
}
if (char !== startAngle && char !== endAngle) {
continue;
}
if (i >= 0 && textToBackTrack[i] === escape) {
i--;
continue;
}
if (char === endAngle) {
if (i >= 0 && textToBackTrack[i] === '=') {
continue; // False alarm of cases like =>
} else {
break;
}
}
if (char === startAngle) {
valid = !foundSpace;
break;
}
}
return valid;
}
/**
* Expands abbreviations as detailed in expandAbbrList in the editor
*
* @returns false if no snippet can be inserted.
*/
function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
if (!expandAbbrList || expandAbbrList.length === 0) {
return Promise.resolve(false);
}
// 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
const insertPromises: Thenable<boolean>[] = [];
if (!insertSameSnippet) {
expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); }).forEach((expandAbbrInput: ExpandAbbreviationInput) => {
let expandedText = expandAbbr(expandAbbrInput);
if (expandedText) {
insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false }));
}
});
if (insertPromises.length === 0) {
return Promise.resolve(false);
}
return Promise.all(insertPromises).then(() => Promise.resolve(true));
}
// Snippet 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
const anyExpandAbbrInput = expandAbbrList[0];
const expandedText = expandAbbr(anyExpandAbbrInput);
const allRanges = expandAbbrList.map(value => {
return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
});
if (expandedText) {
return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
}
return Promise.resolve(false);
}
/*
* Walks the tree rooted at root and apply function fn on each node.
* if fn return false at any node, the further processing of tree is stopped.
*/
function walk(root: any, fn: ((node: any) => boolean)): boolean {
let ctx = root;
while (ctx) {
const next = ctx.next;
if (fn(ctx) === false || walk(ctx.firstChild, fn) === false) {
return false;
}
ctx = next;
}
return true;
}
/**
* Expands abbreviation as detailed in given input.
*/
function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
const helper = getEmmetHelper();
const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter);
if (input.textToWrap) {
if (input.filter && input.filter.indexOf('t') > -1) {
input.textToWrap = input.textToWrap.map(line => {
return line.replace(trimRegex, '').trim();
});
}
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;
}
}
let expandedText;
try {
// Expand the abbreviation
if (input.textToWrap) {
const parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions);
if (input.rangeToReplace.isSingleLine && input.textToWrap.length === 1) {
// Fetch rightmost element in the parsed abbreviation (i.e the element that will contain the wrapped text).
let wrappingNode = parsedAbbr;
while (wrappingNode && wrappingNode.children && wrappingNode.children.length > 0) {
wrappingNode = wrappingNode.children[wrappingNode.children.length - 1];
}
// If wrapping with a block element, insert newline in the text to wrap.
if (wrappingNode && inlineElements.indexOf(wrappingNode.name) === -1 && (expandOptions['profile'].hasOwnProperty('format') ? expandOptions['profile'].format : true)) {
wrappingNode.value = '\n\t' + wrappingNode.value + '\n';
}
}
// Below fixes https://github.com/microsoft/vscode/issues/78219
// walk the tree and remove tags for empty values
walk(parsedAbbr, node => {
if (node.name !== null && node.value === '' && !node.isSelfClosing && node.children.length === 0) {
node.name = '';
node.value = '\n';
}
return true;
});
expandedText = helper.expandAbbreviation(parsedAbbr, expandOptions);
// 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');
} else {
expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
}
} catch (e) {
vscode.window.showErrorMessage('Failed to expand abbreviation');
}
return expandedText;
}
export function getSyntaxFromArgs(args: { [x: string]: string }): string | undefined {
const mappedModes = getMappingForIncludedLanguages();
const language: string = args['language'];
const parentMode: string = args['parentMode'];
const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
if (excludedLanguages.indexOf(language) > -1) {
return;
}
let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
if (!syntax) {
syntax = getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
}
return syntax;
}

View File

@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* 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 { HtmlNode } from 'EmmetNode';
import { getHtmlNode, parseDocument, validate } from './util';
let balanceOutStack: Array<vscode.Selection[]> = [];
let lastOut = false;
let lastBalancedSelections: vscode.Selection[] = [];
export function balanceOut() {
balance(true);
}
export function balanceIn() {
balance(false);
}
function balance(out: boolean) {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
let rootNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) {
return;
}
let getRangeFunction = out ? getRangeToBalanceOut : getRangeToBalanceIn;
let newSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
let range = getRangeFunction(editor.document, selection, rootNode);
newSelections.push(range);
});
if (areSameSelections(newSelections, editor.selections)) {
return;
}
if (areSameSelections(lastBalancedSelections, editor.selections)) {
if (out) {
if (!balanceOutStack.length) {
balanceOutStack.push(editor.selections);
}
balanceOutStack.push(newSelections);
} else {
if (lastOut) {
balanceOutStack.pop();
}
newSelections = balanceOutStack.pop() || newSelections;
}
} else {
balanceOutStack = out ? [editor.selections, newSelections] : [];
}
lastOut = out;
lastBalancedSelections = editor.selections = newSelections;
}
function getRangeToBalanceOut(document: vscode.TextDocument, selection: vscode.Selection, rootNode: HtmlNode): vscode.Selection {
let nodeToBalance = getHtmlNode(document, rootNode, selection.start, false);
if (!nodeToBalance) {
return selection;
}
if (!nodeToBalance.close) {
return new vscode.Selection(nodeToBalance.start, nodeToBalance.end);
}
let innerSelection = new vscode.Selection(nodeToBalance.open.end, nodeToBalance.close.start);
let outerSelection = new vscode.Selection(nodeToBalance.start, nodeToBalance.end);
if (innerSelection.contains(selection) && !innerSelection.isEqual(selection)) {
return innerSelection;
}
if (outerSelection.contains(selection) && !outerSelection.isEqual(selection)) {
return outerSelection;
}
return selection;
}
function getRangeToBalanceIn(document: vscode.TextDocument, selection: vscode.Selection, rootNode: HtmlNode): vscode.Selection {
let nodeToBalance = getHtmlNode(document, rootNode, selection.start, true);
if (!nodeToBalance) {
return selection;
}
if (nodeToBalance.close) {
const entireNodeSelected = selection.start.isEqual(nodeToBalance.start) && selection.end.isEqual(nodeToBalance.end);
const startInOpenTag = selection.start.isAfter(nodeToBalance.open.start) && selection.start.isBefore(nodeToBalance.open.end);
const startInCloseTag = selection.start.isAfter(nodeToBalance.close.start) && selection.start.isBefore(nodeToBalance.close.end);
if (entireNodeSelected || startInOpenTag || startInCloseTag) {
return new vscode.Selection(nodeToBalance.open.end, nodeToBalance.close.start);
}
}
if (!nodeToBalance.firstChild) {
return selection;
}
if (selection.start.isEqual(nodeToBalance.firstChild.start)
&& selection.end.isEqual(nodeToBalance.firstChild.end)
&& nodeToBalance.firstChild.close) {
return new vscode.Selection(nodeToBalance.firstChild.open.end, nodeToBalance.firstChild.close.start);
}
return new vscode.Selection(nodeToBalance.firstChild.start, nodeToBalance.firstChild.end);
}
function areSameSelections(a: vscode.Selection[], b: vscode.Selection[]): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!a[i].isEqual(b[i])) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* 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 { activateEmmetExtension } from '../emmetCommon';
export function activate(context: vscode.ExtensionContext) {
activateEmmetExtension(context);
}

View File

@ -0,0 +1,172 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Based on @sergeche's work in his emmet plugin */
import { TextDocument, Position, Range, EndOfLine } from 'vscode';
/**
* A stream reader for VSCode's `TextDocument`
* Based on @emmetio/stream-reader and @emmetio/atom-plugin
*/
export class DocumentStreamReader {
private document: TextDocument;
private start: Position;
private _eof: Position;
private _sof: Position;
public pos: Position;
private _eol: string;
constructor(document: TextDocument, pos?: Position, limit?: Range) {
this.document = document;
this.start = this.pos = pos ? pos : new Position(0, 0);
this._sof = limit ? limit.start : new Position(0, 0);
this._eof = limit ? limit.end : new Position(this.document.lineCount - 1, this._lineLength(this.document.lineCount - 1));
this._eol = this.document.eol === EndOfLine.LF ? '\n' : '\r\n';
}
/**
* Returns true only if the stream is at the start of the file.
*/
sof(): boolean {
return this.pos.isBeforeOrEqual(this._sof);
}
/**
* Returns true only if the stream is at the end of the file.
*/
eof(): boolean {
return this.pos.isAfterOrEqual(this._eof);
}
/**
* Creates a new stream instance which is limited to given range for given document
*/
limit(start: Position, end: Position): DocumentStreamReader {
return new DocumentStreamReader(this.document, start, new Range(start, end));
}
/**
* Returns the next character code in the stream without advancing it.
* Will return NaN at the end of the file.
*/
peek(): number {
if (this.eof()) {
return NaN;
}
const line = this.document.lineAt(this.pos.line).text;
return this.pos.character < line.length ? line.charCodeAt(this.pos.character) : this._eol.charCodeAt(this.pos.character - line.length);
}
/**
* Returns the next character in the stream and advances it.
* Also returns NaN when no more characters are available.
*/
next(): number {
if (this.eof()) {
return NaN;
}
const line = this.document.lineAt(this.pos.line).text;
let code: number;
if (this.pos.character < line.length) {
code = line.charCodeAt(this.pos.character);
this.pos = this.pos.translate(0, 1);
} else {
code = this._eol.charCodeAt(this.pos.character - line.length);
this.pos = new Position(this.pos.line + 1, 0);
}
if (this.eof()) {
// restrict pos to eof, if in case it got moved beyond eof
this.pos = new Position(this._eof.line, this._eof.character);
}
return code;
}
/**
* Backs up the stream n characters. Backing it up further than the
* start of the current token will cause things to break, so be careful.
*/
backUp(n: number) {
let row = this.pos.line;
let column = this.pos.character;
column -= (n || 1);
while (row >= 0 && column < 0) {
row--;
column += this._lineLength(row);
}
this.pos = row < 0 || column < 0
? new Position(0, 0)
: new Position(row, column);
return this.peek();
}
/**
* Get the string between the start of the current token and the
* current stream position.
*/
current(): string {
return this.substring(this.start, this.pos);
}
/**
* Returns contents for given range
*/
substring(from: Position, to: Position): string {
return this.document.getText(new Range(from, to));
}
/**
* Creates error object with current stream state
*/
error(message: string): Error {
const err = new Error(`${message} at row ${this.pos.line}, column ${this.pos.character}`);
return err;
}
/**
* Returns line length of given row, including line ending
*/
_lineLength(row: number): number {
if (row === this.document.lineCount - 1) {
return this.document.lineAt(row).text.length;
}
return this.document.lineAt(row).text.length + this._eol.length;
}
/**
* `match` can be a character code or a function that takes a character code
* and returns a boolean. If the next character in the stream 'matches'
* the given argument, it is consumed and returned.
* Otherwise, `false` is returned.
*/
eat(match: number | Function): boolean {
const ch = this.peek();
const ok = typeof match === 'function' ? match(ch) : ch === match;
if (ok) {
this.next();
}
return ok;
}
/**
* Repeatedly calls <code>eat</code> with the given argument, until it
* fails. Returns <code>true</code> if any characters were eaten.
*/
eatWhile(match: number | Function): boolean {
const start = this.pos;
while (!this.eof() && this.eat(match)) { }
return !this.pos.isEqual(start);
}
}

View File

@ -0,0 +1,221 @@
/*---------------------------------------------------------------------------------------------
* 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 { Node, Stylesheet } from 'EmmetNode';
import { isValidLocationForEmmetAbbreviation, getSyntaxFromArgs } from './abbreviationActions';
import { getEmmetHelper, getMappingForIncludedLanguages, parsePartialStylesheet, getEmmetConfiguration, getEmmetMode, isStyleSheet, parseDocument, getNode, allowedMimeTypesInScriptTag, trimQuotes, toLSTextDocument } from './util';
import { getLanguageService, TokenType, Range as LSRange } from 'vscode-html-languageservice';
export class DefaultCompletionItemProvider implements vscode.CompletionItemProvider {
private lastCompletionType: string | undefined;
private htmlLS = getLanguageService();
public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, _: vscode.CancellationToken, context: vscode.CompletionContext): Thenable<vscode.CompletionList | undefined> | undefined {
const completionResult = this.provideCompletionItemsInternal(document, position, context);
if (!completionResult) {
this.lastCompletionType = undefined;
return;
}
return completionResult.then(completionList => {
if (!completionList || !completionList.items.length) {
this.lastCompletionType = undefined;
return completionList;
}
const item = completionList.items[0];
const expandedText = item.documentation ? item.documentation.toString() : '';
if (expandedText.startsWith('<')) {
this.lastCompletionType = 'html';
} else if (expandedText.indexOf(':') > 0 && expandedText.endsWith(';')) {
this.lastCompletionType = 'css';
} else {
this.lastCompletionType = undefined;
}
return completionList;
});
}
private provideCompletionItemsInternal(document: vscode.TextDocument, position: vscode.Position, context: vscode.CompletionContext): Thenable<vscode.CompletionList | undefined> | undefined {
const emmetConfig = vscode.workspace.getConfiguration('emmet');
const excludedLanguages = emmetConfig['excludeLanguages'] ? emmetConfig['excludeLanguages'] : [];
if (excludedLanguages.indexOf(document.languageId) > -1) {
return;
}
const mappedLanguages = getMappingForIncludedLanguages();
const isSyntaxMapped = mappedLanguages[document.languageId] ? true : false;
let emmetMode = getEmmetMode((isSyntaxMapped ? mappedLanguages[document.languageId] : document.languageId), excludedLanguages);
if (!emmetMode
|| emmetConfig['showExpandedAbbreviation'] === 'never'
|| ((isSyntaxMapped || emmetMode === 'jsx') && emmetConfig['showExpandedAbbreviation'] !== 'always')) {
return;
}
let syntax = emmetMode;
const helper = getEmmetHelper();
let validateLocation = syntax === 'html' || syntax === 'jsx' || syntax === 'xml';
let rootNode: Node | undefined = undefined;
let currentNode: Node | null = null;
const lsDoc = toLSTextDocument(document);
position = document.validatePosition(position);
if (document.languageId === 'html') {
if (context.triggerKind === vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) {
switch (this.lastCompletionType) {
case 'html':
validateLocation = false;
break;
case 'css':
validateLocation = false;
syntax = 'css';
break;
default:
break;
}
}
if (validateLocation) {
const parsedLsDoc = this.htmlLS.parseHTMLDocument(lsDoc);
const positionOffset = document.offsetAt(position);
const node = parsedLsDoc.findNodeAt(positionOffset);
if (node.tag === 'script') {
if (node.attributes && 'type' in node.attributes) {
const rawTypeAttrValue = node.attributes['type'];
if (rawTypeAttrValue) {
const typeAttrValue = trimQuotes(rawTypeAttrValue);
if (typeAttrValue === 'application/javascript' || typeAttrValue === 'text/javascript') {
if (!getSyntaxFromArgs({ language: 'javascript' })) {
return;
} else {
validateLocation = false;
}
}
else if (allowedMimeTypesInScriptTag.indexOf(trimQuotes(rawTypeAttrValue)) > -1) {
validateLocation = false;
}
}
} else {
return;
}
}
else if (node.tag === 'style') {
syntax = 'css';
validateLocation = false;
} else {
if (node.attributes && node.attributes['style']) {
const scanner = this.htmlLS.createScanner(document.getText(), node.start);
let tokenType = scanner.scan();
let prevAttr = undefined;
let styleAttrValueRange: [number, number] | undefined = undefined;
while (tokenType !== TokenType.EOS && (scanner.getTokenEnd() <= positionOffset)) {
tokenType = scanner.scan();
if (tokenType === TokenType.AttributeName) {
prevAttr = scanner.getTokenText();
}
else if (tokenType === TokenType.AttributeValue && prevAttr === 'style') {
styleAttrValueRange = [scanner.getTokenOffset(), scanner.getTokenEnd()];
}
}
if (prevAttr === 'style' && styleAttrValueRange && positionOffset > styleAttrValueRange[0] && positionOffset < styleAttrValueRange[1]) {
syntax = 'css';
validateLocation = false;
}
}
}
}
}
const expandOptions = isStyleSheet(syntax) ?
{ lookAhead: false, syntax: 'stylesheet' } :
{ lookAhead: true, syntax: 'markup' };
const extractAbbreviationResults = helper.extractAbbreviation(lsDoc, position, expandOptions);
if (!extractAbbreviationResults || !helper.isAbbreviationValid(syntax, extractAbbreviationResults.abbreviation)) {
return;
}
if (isStyleSheet(document.languageId) && context.triggerKind !== vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) {
validateLocation = true;
let usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;
rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : <Stylesheet>parseDocument(document, false);
if (!rootNode) {
return;
}
currentNode = getNode(rootNode, position, true);
}
if (validateLocation && !isValidLocationForEmmetAbbreviation(document, rootNode, currentNode, syntax, position, toRange(extractAbbreviationResults.abbreviationRange))) {
return;
}
let noiseCheckPromise: Thenable<any> = Promise.resolve();
// Fix for https://github.com/microsoft/vscode/issues/32647
// Check for document symbols in js/ts/jsx/tsx and avoid triggering emmet for abbreviations of the form symbolName.sometext
// Presence of > or * or + in the abbreviation denotes valid abbreviation that should trigger emmet
if (!isStyleSheet(syntax) && (document.languageId === 'javascript' || document.languageId === 'javascriptreact' || document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {
let abbreviation: string = extractAbbreviationResults.abbreviation;
if (abbreviation.startsWith('this.')) {
noiseCheckPromise = Promise.resolve(true);
} else {
noiseCheckPromise = vscode.commands.executeCommand<vscode.SymbolInformation[]>('vscode.executeDocumentSymbolProvider', document.uri).then((symbols: vscode.SymbolInformation[] | undefined) => {
return symbols && symbols.find(x => abbreviation === x.name || (abbreviation.startsWith(x.name + '.') && !/>|\*|\+/.test(abbreviation)));
});
}
}
return noiseCheckPromise.then((noise): vscode.CompletionList | undefined => {
if (noise) {
return;
}
let result = helper.doComplete(toLSTextDocument(document), position, syntax, getEmmetConfiguration(syntax!));
// https://github.com/microsoft/vscode/issues/86941
if (result && result.items && result.items.length === 1) {
if (result.items[0].label === 'widows: ;') {
return undefined;
}
}
let newItems: vscode.CompletionItem[] = [];
if (result && result.items) {
result.items.forEach((item: any) => {
let newItem = new vscode.CompletionItem(item.label);
newItem.documentation = item.documentation;
newItem.detail = item.detail;
newItem.insertText = new vscode.SnippetString(item.textEdit.newText);
let oldrange = item.textEdit.range;
newItem.range = new vscode.Range(oldrange.start.line, oldrange.start.character, oldrange.end.line, oldrange.end.character);
newItem.filterText = item.filterText;
newItem.sortText = item.sortText;
if (emmetConfig['showSuggestionsAsSnippets'] === true) {
newItem.kind = vscode.CompletionItemKind.Snippet;
}
newItems.push(newItem);
});
}
return new vscode.CompletionList(newItems, true);
});
}
}
function toRange(lsRange: LSRange) {
return new vscode.Range(lsRange.start.line, lsRange.start.character, lsRange.end.line, lsRange.end.character);
}

View File

@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* 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 { validate } from './util';
export function fetchEditPoint(direction: string): void {
if (!validate() || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
let newSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
let updatedSelection = direction === 'next' ? nextEditPoint(selection, editor) : prevEditPoint(selection, editor);
newSelections.push(updatedSelection);
});
editor.selections = newSelections;
editor.revealRange(editor.selections[editor.selections.length - 1]);
}
function nextEditPoint(selection: vscode.Selection, editor: vscode.TextEditor): vscode.Selection {
for (let lineNum = selection.anchor.line; lineNum < editor.document.lineCount; lineNum++) {
let updatedSelection = findEditPoint(lineNum, editor, selection.anchor, 'next');
if (updatedSelection) {
return updatedSelection;
}
}
return selection;
}
function prevEditPoint(selection: vscode.Selection, editor: vscode.TextEditor): vscode.Selection {
for (let lineNum = selection.anchor.line; lineNum >= 0; lineNum--) {
let updatedSelection = findEditPoint(lineNum, editor, selection.anchor, 'prev');
if (updatedSelection) {
return updatedSelection;
}
}
return selection;
}
function findEditPoint(lineNum: number, editor: vscode.TextEditor, position: vscode.Position, direction: string): vscode.Selection | undefined {
let line = editor.document.lineAt(lineNum);
let lineContent = line.text;
if (lineNum !== position.line && line.isEmptyOrWhitespace) {
return new vscode.Selection(lineNum, lineContent.length, lineNum, lineContent.length);
}
if (lineNum === position.line && direction === 'prev') {
lineContent = lineContent.substr(0, position.character);
}
let emptyAttrIndex = direction === 'next' ? lineContent.indexOf('""', lineNum === position.line ? position.character : 0) : lineContent.lastIndexOf('""');
let emptyTagIndex = direction === 'next' ? lineContent.indexOf('><', lineNum === position.line ? position.character : 0) : lineContent.lastIndexOf('><');
let winner = -1;
if (emptyAttrIndex > -1 && emptyTagIndex > -1) {
winner = direction === 'next' ? Math.min(emptyAttrIndex, emptyTagIndex) : Math.max(emptyAttrIndex, emptyTagIndex);
} else if (emptyAttrIndex > -1) {
winner = emptyAttrIndex;
} else {
winner = emptyTagIndex;
}
if (winner > -1) {
return new vscode.Selection(lineNum, winner + 1, lineNum, winner + 1);
}
return;
}

View File

@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* 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 { DefaultCompletionItemProvider } from './defaultCompletionProvider';
import { expandEmmetAbbreviation, wrapWithAbbreviation, wrapIndividualLinesWithAbbreviation } from './abbreviationActions';
import { removeTag } from './removeTag';
import { updateTag } from './updateTag';
import { matchTag } from './matchTag';
import { balanceOut, balanceIn } from './balance';
import { splitJoinTag } from './splitJoinTag';
import { mergeLines } from './mergeLines';
import { toggleComment } from './toggleComment';
import { fetchEditPoint } from './editPoint';
import { fetchSelectItem } from './selectItem';
import { evaluateMathExpression } from './evaluateMathExpression';
import { incrementDecrement } from './incrementDecrement';
import { LANGUAGE_MODES, getMappingForIncludedLanguages, updateEmmetExtensionsPath, getPathBaseName } from './util';
import { reflectCssValue } from './reflectCssValue';
export function activateEmmetExtension(context: vscode.ExtensionContext) {
registerCompletionProviders(context);
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.wrapWithAbbreviation', (args) => {
wrapWithAbbreviation(args);
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.wrapIndividualLinesWithAbbreviation', (args) => {
wrapIndividualLinesWithAbbreviation(args);
}));
context.subscriptions.push(vscode.commands.registerCommand('emmet.expandAbbreviation', (args) => {
expandEmmetAbbreviation(args);
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.removeTag', () => {
return removeTag();
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.updateTag', (inputTag) => {
if (inputTag && typeof inputTag === 'string') {
return updateTag(inputTag);
}
return vscode.window.showInputBox({ prompt: 'Enter Tag' }).then(tagName => {
if (tagName) {
const update = updateTag(tagName);
return update ? update : false;
}
return false;
});
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.matchTag', () => {
matchTag();
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.balanceOut', () => {
balanceOut();
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.balanceIn', () => {
balanceIn();
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.splitJoinTag', () => {
return splitJoinTag();
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.mergeLines', () => {
mergeLines();
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.toggleComment', () => {
toggleComment();
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.nextEditPoint', () => {
fetchEditPoint('next');
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.prevEditPoint', () => {
fetchEditPoint('prev');
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.selectNextItem', () => {
fetchSelectItem('next');
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.selectPrevItem', () => {
fetchSelectItem('prev');
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.evaluateMathExpression', () => {
evaluateMathExpression();
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.incrementNumberByOneTenth', () => {
return incrementDecrement(0.1);
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.incrementNumberByOne', () => {
return incrementDecrement(1);
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.incrementNumberByTen', () => {
return incrementDecrement(10);
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.decrementNumberByOneTenth', () => {
return incrementDecrement(-0.1);
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.decrementNumberByOne', () => {
return incrementDecrement(-1);
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.decrementNumberByTen', () => {
return incrementDecrement(-10);
}));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.reflectCSSValue', () => {
return reflectCssValue();
}));
updateEmmetExtensionsPath();
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('emmet.includeLanguages')) {
registerCompletionProviders(context);
}
if (e.affectsConfiguration('emmet.extensionsPath')) {
updateEmmetExtensionsPath();
}
}));
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument((e) => {
const basefileName: string = getPathBaseName(e.fileName);
if (basefileName.startsWith('snippets') && basefileName.endsWith('.json')) {
updateEmmetExtensionsPath(true);
}
}));
}
/**
* Holds any registered completion providers by their language strings
*/
const languageMappingForCompletionProviders: Map<string, string> = new Map<string, string>();
const completionProvidersMapping: Map<string, vscode.Disposable> = new Map<string, vscode.Disposable>();
function registerCompletionProviders(context: vscode.ExtensionContext) {
let completionProvider = new DefaultCompletionItemProvider();
let includedLanguages = getMappingForIncludedLanguages();
Object.keys(includedLanguages).forEach(language => {
if (languageMappingForCompletionProviders.has(language) && languageMappingForCompletionProviders.get(language) === includedLanguages[language]) {
return;
}
if (languageMappingForCompletionProviders.has(language)) {
const mapping = completionProvidersMapping.get(language);
if (mapping) {
mapping.dispose();
}
languageMappingForCompletionProviders.delete(language);
completionProvidersMapping.delete(language);
}
const provider = vscode.languages.registerCompletionItemProvider({ language, scheme: '*' }, completionProvider, ...LANGUAGE_MODES[includedLanguages[language]]);
context.subscriptions.push(provider);
languageMappingForCompletionProviders.set(language, includedLanguages[language]);
completionProvidersMapping.set(language, provider);
});
Object.keys(LANGUAGE_MODES).forEach(language => {
if (!languageMappingForCompletionProviders.has(language)) {
const provider = vscode.languages.registerCompletionItemProvider({ language, scheme: '*' }, completionProvider, ...LANGUAGE_MODES[language]);
context.subscriptions.push(provider);
languageMappingForCompletionProviders.set(language, language);
completionProvidersMapping.set(language, provider);
}
});
}
export function deactivate() {
}

View File

@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Based on @sergeche's work in his emmet plugin */
import * as vscode from 'vscode';
import evaluate from '@emmetio/math-expression';
import { DocumentStreamReader } from './bufferStream';
export function evaluateMathExpression() {
if (!vscode.window.activeTextEditor) {
vscode.window.showInformationMessage('No editor is active');
return;
}
const editor = vscode.window.activeTextEditor;
const stream = new DocumentStreamReader(editor.document);
editor.edit(editBuilder => {
editor.selections.forEach(selection => {
const pos = selection.isReversed ? selection.anchor : selection.active;
stream.pos = pos;
try {
const result = String(evaluate(stream, true));
editBuilder.replace(new vscode.Range(stream.pos, pos), result);
} catch (err) {
vscode.window.showErrorMessage('Could not evaluate expression');
// Ignore error since most likely its because of non-math expression
console.warn('Math evaluation error', err);
}
});
});
}

View File

@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Based on @sergeche's work on the emmet plugin for atom
// TODO: Move to https://github.com/emmetio/image-size
import * as path from 'path';
import * as http from 'http';
import * as https from 'https';
import { parse as parseUrl } from 'url';
import * as sizeOf from 'image-size';
const reUrl = /^https?:/;
/**
* Get size of given image file. Supports files from local filesystem,
* as well as URLs
*/
export function getImageSize(file: string) {
file = file.replace(/^file:\/\//, '');
return reUrl.test(file) ? getImageSizeFromURL(file) : getImageSizeFromFile(file);
}
/**
* Get image size from file on local file system
*/
function getImageSizeFromFile(file: string) {
return new Promise((resolve, reject) => {
const isDataUrl = file.match(/^data:.+?;base64,/);
if (isDataUrl) {
// NB should use sync version of `sizeOf()` for buffers
try {
const data = Buffer.from(file.slice(isDataUrl[0].length), 'base64');
return resolve(sizeForFileName('', sizeOf(data)));
} catch (err) {
return reject(err);
}
}
sizeOf(file, (err: any, size: any) => {
if (err) {
reject(err);
} else {
resolve(sizeForFileName(path.basename(file), size));
}
});
});
}
/**
* Get image size from given remove URL
*/
function getImageSizeFromURL(urlStr: string) {
return new Promise((resolve, reject) => {
const url = parseUrl(urlStr);
const getTransport = url.protocol === 'https:' ? https.get : http.get;
if (!url.pathname) {
return reject('Given url doesnt have pathname property');
}
const urlPath: string = url.pathname;
getTransport(url as any, resp => {
const chunks: Buffer[] = [];
let bufSize = 0;
const trySize = (chunks: Buffer[]) => {
try {
const size = sizeOf(Buffer.concat(chunks, bufSize));
resp.removeListener('data', onData);
resp.destroy(); // no need to read further
resolve(sizeForFileName(path.basename(urlPath), size));
} catch (err) {
// might not have enough data, skip error
}
};
const onData = (chunk: Buffer) => {
bufSize += chunk.length;
chunks.push(chunk);
trySize(chunks);
};
resp
.on('data', onData)
.on('end', () => trySize(chunks))
.once('error', err => {
resp.removeListener('data', onData);
reject(err);
});
})
.once('error', reject);
});
}
/**
* Returns size object for given file name. If file name contains `@Nx` token,
* the final dimentions will be downscaled by N
*/
function sizeForFileName(fileName: string, size: any) {
const m = fileName.match(/@(\d+)x\./);
const scale = m ? +m[1] : 1;
return {
realWidth: size.width,
realHeight: size.height,
width: Math.floor(size.width / scale),
height: Math.floor(size.height / scale)
};
}

View File

@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Based on @sergeche's work in his emmet plugin */
import * as vscode from 'vscode';
const reNumber = /[0-9]/;
/**
* Incerement number under caret of given editor
*/
export function incrementDecrement(delta: number): Thenable<boolean> | undefined {
if (!vscode.window.activeTextEditor) {
vscode.window.showInformationMessage('No editor is active');
return;
}
const editor = vscode.window.activeTextEditor;
return editor.edit(editBuilder => {
editor.selections.forEach(selection => {
let rangeToReplace = locate(editor.document, selection.isReversed ? selection.anchor : selection.active);
if (!rangeToReplace) {
return;
}
const text = editor.document.getText(rangeToReplace);
if (isValidNumber(text)) {
editBuilder.replace(rangeToReplace, update(text, delta));
}
});
});
}
/**
* Updates given number with `delta` and returns string formatted according
* to original string format
*/
export function update(numString: string, delta: number): string {
let m: RegExpMatchArray | null;
let decimals = (m = numString.match(/\.(\d+)$/)) ? m[1].length : 1;
let output = String((parseFloat(numString) + delta).toFixed(decimals)).replace(/\.0+$/, '');
if (m = numString.match(/^\-?(0\d+)/)) {
// padded number: preserve padding
output = output.replace(/^(\-?)(\d+)/, (_, minus, prefix) =>
minus + '0'.repeat(Math.max(0, (m ? m[1].length : 0) - prefix.length)) + prefix);
}
if (/^\-?\./.test(numString)) {
// omit integer part
output = output.replace(/^(\-?)0+/, '$1');
}
return output;
}
/**
* Locates number from given position in the document
*
* @return Range of number or `undefined` if not found
*/
export function locate(document: vscode.TextDocument, pos: vscode.Position): vscode.Range | undefined {
const line = document.lineAt(pos.line).text;
let start = pos.character;
let end = pos.character;
let hadDot = false, hadMinus = false;
let ch;
while (start > 0) {
ch = line[--start];
if (ch === '-') {
hadMinus = true;
break;
} else if (ch === '.' && !hadDot) {
hadDot = true;
} else if (!reNumber.test(ch)) {
start++;
break;
}
}
if (line[end] === '-' && !hadMinus) {
end++;
}
while (end < line.length) {
ch = line[end++];
if (ch === '.' && !hadDot && reNumber.test(line[end])) {
// A dot must be followed by a number. Otherwise stop parsing
hadDot = true;
} else if (!reNumber.test(ch)) {
end--;
break;
}
}
// ensure that found range contains valid number
if (start !== end && isValidNumber(line.slice(start, end))) {
return new vscode.Range(pos.line, start, pos.line, end);
}
return;
}
/**
* Check if given string contains valid number
*/
function isValidNumber(str: string): boolean {
return str ? !isNaN(parseFloat(str)) : false;
}

View File

@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Based on @sergeche's work on the emmet plugin for atom
// TODO: Move to https://github.com/emmetio/file-utils
import * as path from 'path';
import * as fs from 'fs';
const reAbsolutePosix = /^\/+/;
const reAbsoluteWin32 = /^\\+/;
const reAbsolute = path.sep === '/' ? reAbsolutePosix : reAbsoluteWin32;
/**
* Locates given `filePath` on users file system and returns absolute path to it.
* This method expects either URL, or relative/absolute path to resource
* @param basePath Base path to use if filePath is not absoulte
* @param filePath File to locate.
*/
export function locateFile(base: string, filePath: string): Promise<string> {
if (/^\w+:/.test(filePath)) {
// path with protocol, already absolute
return Promise.resolve(filePath);
}
filePath = path.normalize(filePath);
return reAbsolute.test(filePath)
? resolveAbsolute(base, filePath)
: resolveRelative(base, filePath);
}
/**
* Resolves relative file path
*/
function resolveRelative(basePath: string, filePath: string): Promise<string> {
return tryFile(path.resolve(basePath, filePath));
}
/**
* Resolves absolute file path agaist given editor: tries to find file in every
* parent of editors file
*/
function resolveAbsolute(basePath: string, filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
filePath = filePath.replace(reAbsolute, '');
const next = (ctx: string) => {
tryFile(path.resolve(ctx, filePath))
.then(resolve, () => {
const dir = path.dirname(ctx);
if (!dir || dir === ctx) {
return reject(`Unable to locate absolute file ${filePath}`);
}
next(dir);
});
};
next(basePath);
});
}
/**
* Check if given file exists and its a file, not directory
*/
function tryFile(file: string): Promise<string> {
return new Promise((resolve, reject) => {
fs.stat(file, (err, stat) => {
if (err) {
return reject(err);
}
if (!stat.isFile()) {
return reject(new Error(`${file} is not a file`));
}
resolve(file);
});
});
}

View File

@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* 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 { HtmlNode } from 'EmmetNode';
import { getHtmlNode, parseDocument, validate } from './util';
export function matchTag() {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
let rootNode: HtmlNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) { return; }
let updatedSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
let updatedSelection = getUpdatedSelections(editor, selection.start, rootNode);
if (updatedSelection) {
updatedSelections.push(updatedSelection);
}
});
if (updatedSelections.length > 0) {
editor.selections = updatedSelections;
editor.revealRange(editor.selections[updatedSelections.length - 1]);
}
}
function getUpdatedSelections(editor: vscode.TextEditor, position: vscode.Position, rootNode: HtmlNode): vscode.Selection | undefined {
let currentNode = getHtmlNode(editor.document, rootNode, position, true);
if (!currentNode) { return; }
// If no closing tag or cursor is between open and close tag, then no-op
if (!currentNode.close || (position.isAfter(currentNode.open.end) && position.isBefore(currentNode.close.start))) {
return;
}
// Place cursor inside the close tag if cursor is inside the open tag, else place it inside the open tag
let finalPosition = position.isBeforeOrEqual(currentNode.open.end) ? currentNode.close.start.translate(0, 2) : currentNode.open.start.translate(0, 1);
return new vscode.Selection(finalPosition, finalPosition);
}

View File

@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* 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 { Node } from 'EmmetNode';
import { getNode, parseDocument, validate } from './util';
export function mergeLines() {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
let rootNode = parseDocument(editor.document);
if (!rootNode) {
return;
}
return editor.edit(editBuilder => {
editor.selections.reverse().forEach(selection => {
let textEdit = getRangesToReplace(editor.document, selection, rootNode!);
if (textEdit) {
editBuilder.replace(textEdit.range, textEdit.newText);
}
});
});
}
function getRangesToReplace(document: vscode.TextDocument, selection: vscode.Selection, rootNode: Node): vscode.TextEdit | undefined {
let startNodeToUpdate: Node | null;
let endNodeToUpdate: Node | null;
if (selection.isEmpty) {
startNodeToUpdate = endNodeToUpdate = getNode(rootNode, selection.start, true);
} else {
startNodeToUpdate = getNode(rootNode, selection.start, true);
endNodeToUpdate = getNode(rootNode, selection.end, true);
}
if (!startNodeToUpdate || !endNodeToUpdate || startNodeToUpdate.start.line === endNodeToUpdate.end.line) {
return;
}
let rangeToReplace = new vscode.Range(startNodeToUpdate.start, endNodeToUpdate.end);
let textToReplaceWith = document.lineAt(startNodeToUpdate.start.line).text.substr(startNodeToUpdate.start.character);
for (let i = startNodeToUpdate.start.line + 1; i <= endNodeToUpdate.end.line; i++) {
textToReplaceWith += document.lineAt(i).text.trim();
}
return new vscode.TextEdit(rangeToReplace, textToReplaceWith);
}

View File

@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* 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 { homedir } from 'os';
import { activateEmmetExtension } from '../emmetCommon';
import { setHomeDir } from '../util';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.updateImageSize', () => {
return import('../updateImageSize').then(uis => uis.updateImageSize());
}));
setHomeDir(vscode.Uri.file(homedir()));
activateEmmetExtension(context);
}

View File

@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Range, window, TextEditor } from 'vscode';
import { getCssPropertyFromRule, getCssPropertyFromDocument } from './util';
import { Property, Rule } from 'EmmetNode';
const vendorPrefixes = ['-webkit-', '-moz-', '-ms-', '-o-', ''];
export function reflectCssValue(): Thenable<boolean> | undefined {
let editor = window.activeTextEditor;
if (!editor) {
window.showInformationMessage('No editor is active.');
return;
}
let node = getCssPropertyFromDocument(editor, editor.selection.active);
if (!node) {
return;
}
return updateCSSNode(editor, node);
}
function updateCSSNode(editor: TextEditor, property: Property): Thenable<boolean> {
const rule: Rule = property.parent;
let currentPrefix = '';
// Find vendor prefix of given property node
for (const prefix of vendorPrefixes) {
if (property.name.startsWith(prefix)) {
currentPrefix = prefix;
break;
}
}
const propertyName = property.name.substr(currentPrefix.length);
const propertyValue = property.value;
return editor.edit(builder => {
// Find properties with vendor prefixes, update each
vendorPrefixes.forEach(prefix => {
if (prefix === currentPrefix) {
return;
}
let vendorProperty = getCssPropertyFromRule(rule, prefix + propertyName);
if (vendorProperty) {
builder.replace(new Range(vendorProperty.valueToken.start, vendorProperty.valueToken.end), propertyValue);
}
});
});
}

View File

@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* 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 { parseDocument, validate, getHtmlNode } from './util';
import { HtmlNode } from 'EmmetNode';
export function removeTag() {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
let rootNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) {
return;
}
let indentInSpaces = '';
const tabSize: number = editor.options.tabSize ? +editor.options.tabSize : 0;
for (let i = 0; i < tabSize || 0; i++) {
indentInSpaces += ' ';
}
let rangesToRemove: vscode.Range[] = [];
editor.selections.reverse().forEach(selection => {
rangesToRemove = rangesToRemove.concat(getRangeToRemove(editor, rootNode, selection, indentInSpaces));
});
return editor.edit(editBuilder => {
rangesToRemove.forEach(range => {
editBuilder.replace(range, '');
});
});
}
function getRangeToRemove(editor: vscode.TextEditor, rootNode: HtmlNode, selection: vscode.Selection, indentInSpaces: string): vscode.Range[] {
let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true);
if (!nodeToUpdate) {
return [];
}
let openRange = new vscode.Range(nodeToUpdate.open.start, nodeToUpdate.open.end);
let closeRange: vscode.Range | null = null;
if (nodeToUpdate.close) {
closeRange = new vscode.Range(nodeToUpdate.close.start, nodeToUpdate.close.end);
}
let ranges = [openRange];
if (closeRange) {
for (let i = openRange.start.line + 1; i <= closeRange.start.line; i++) {
let lineContent = editor.document.lineAt(i).text;
if (lineContent.startsWith('\t')) {
ranges.push(new vscode.Range(i, 0, i, 1));
} else if (lineContent.startsWith(indentInSpaces)) {
ranges.push(new vscode.Range(i, 0, i, indentInSpaces.length));
}
}
ranges.push(closeRange);
}
return ranges;
}

View File

@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* 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 { validate, parseDocument, isStyleSheet } from './util';
import { nextItemHTML, prevItemHTML } from './selectItemHTML';
import { nextItemStylesheet, prevItemStylesheet } from './selectItemStylesheet';
import { HtmlNode, CssNode } from 'EmmetNode';
export function fetchSelectItem(direction: string): void {
if (!validate() || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
let rootNode = parseDocument(editor.document);
if (!rootNode) {
return;
}
let newSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
const selectionStart = selection.isReversed ? selection.active : selection.anchor;
const selectionEnd = selection.isReversed ? selection.anchor : selection.active;
let updatedSelection;
if (isStyleSheet(editor.document.languageId)) {
updatedSelection = direction === 'next' ? nextItemStylesheet(selectionStart, selectionEnd, <CssNode>rootNode!) : prevItemStylesheet(selectionStart, selectionEnd, <CssNode>rootNode!);
} else {
updatedSelection = direction === 'next' ? nextItemHTML(selectionStart, selectionEnd, editor, <HtmlNode>rootNode!) : prevItemHTML(selectionStart, selectionEnd, editor, <HtmlNode>rootNode!);
}
newSelections.push(updatedSelection ? updatedSelection : selection);
});
editor.selections = newSelections;
editor.revealRange(editor.selections[editor.selections.length - 1]);
}

View File

@ -0,0 +1,211 @@
/*---------------------------------------------------------------------------------------------
* 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 { getDeepestNode, findNextWord, findPrevWord, getHtmlNode, isNumber } from './util';
import { HtmlNode } from 'EmmetNode';
export function nextItemHTML(selectionStart: vscode.Position, selectionEnd: vscode.Position, editor: vscode.TextEditor, rootNode: HtmlNode): vscode.Selection | undefined {
let currentNode = getHtmlNode(editor.document, rootNode, selectionEnd, false);
let nextNode: HtmlNode | undefined = undefined;
if (!currentNode) {
return;
}
if (currentNode.type !== 'comment') {
// If cursor is in the tag name, select tag
if (selectionEnd.isBefore(currentNode.open.start.translate(0, currentNode.name.length))) {
return getSelectionFromNode(currentNode);
}
// If cursor is in the open tag, look for attributes
if (selectionEnd.isBefore(currentNode.open.end)) {
let attrSelection = getNextAttribute(selectionStart, selectionEnd, currentNode);
if (attrSelection) {
return attrSelection;
}
}
// Get the first child of current node which is right after the cursor and is not a comment
nextNode = currentNode.firstChild;
while (nextNode && (selectionEnd.isAfterOrEqual(nextNode.end) || nextNode.type === 'comment')) {
nextNode = nextNode.nextSibling;
}
}
// Get next sibling of current node which is not a comment. If none is found try the same on the parent
while (!nextNode && currentNode) {
if (currentNode.nextSibling) {
if (currentNode.nextSibling.type !== 'comment') {
nextNode = currentNode.nextSibling;
} else {
currentNode = currentNode.nextSibling;
}
} else {
currentNode = currentNode.parent;
}
}
return nextNode && getSelectionFromNode(nextNode);
}
export function prevItemHTML(selectionStart: vscode.Position, selectionEnd: vscode.Position, editor: vscode.TextEditor, rootNode: HtmlNode): vscode.Selection | undefined {
let currentNode = getHtmlNode(editor.document, rootNode, selectionStart, false);
let prevNode: HtmlNode | undefined = undefined;
if (!currentNode) {
return;
}
if (currentNode.type !== 'comment' && selectionStart.translate(0, -1).isAfter(currentNode.open.start)) {
if (selectionStart.isBefore(currentNode.open.end) || !currentNode.firstChild || selectionEnd.isBeforeOrEqual(currentNode.firstChild.start)) {
prevNode = currentNode;
} else {
// Select the child that appears just before the cursor and is not a comment
prevNode = currentNode.firstChild;
let oldOption: HtmlNode | undefined = undefined;
while (prevNode.nextSibling && selectionStart.isAfterOrEqual(prevNode.nextSibling.end)) {
if (prevNode && prevNode.type !== 'comment') {
oldOption = prevNode;
}
prevNode = prevNode.nextSibling;
}
prevNode = <HtmlNode>getDeepestNode((prevNode && prevNode.type !== 'comment') ? prevNode : oldOption);
}
}
// Select previous sibling which is not a comment. If none found, then select parent
while (!prevNode && currentNode) {
if (currentNode.previousSibling) {
if (currentNode.previousSibling.type !== 'comment') {
prevNode = <HtmlNode>getDeepestNode(currentNode.previousSibling);
} else {
currentNode = currentNode.previousSibling;
}
} else {
prevNode = currentNode.parent;
}
}
if (!prevNode) {
return undefined;
}
let attrSelection = getPrevAttribute(selectionStart, selectionEnd, prevNode);
return attrSelection ? attrSelection : getSelectionFromNode(prevNode);
}
function getSelectionFromNode(node: HtmlNode): vscode.Selection | undefined {
if (node && node.open) {
let selectionStart = (<vscode.Position>node.open.start).translate(0, 1);
let selectionEnd = selectionStart.translate(0, node.name.length);
return new vscode.Selection(selectionStart, selectionEnd);
}
return undefined;
}
function getNextAttribute(selectionStart: vscode.Position, selectionEnd: vscode.Position, node: HtmlNode): vscode.Selection | undefined {
if (!node.attributes || node.attributes.length === 0 || node.type === 'comment') {
return;
}
for (const attr of node.attributes) {
if (selectionEnd.isBefore(attr.start)) {
// select full attr
return new vscode.Selection(attr.start, attr.end);
}
if (!attr.value || (<vscode.Position>attr.value.start).isEqual(attr.value.end)) {
// No attr value to select
continue;
}
if ((selectionStart.isEqual(attr.start) && selectionEnd.isEqual(attr.end)) || selectionEnd.isBefore(attr.value.start)) {
// cursor is in attr name, so select full attr value
return new vscode.Selection(attr.value.start, attr.value.end);
}
// Fetch the next word in the attr value
if (attr.value.toString().indexOf(' ') === -1) {
// attr value does not have space, so no next word to find
continue;
}
let pos: number | undefined = undefined;
if (selectionStart.isEqual(attr.value.start) && selectionEnd.isEqual(attr.value.end)) {
pos = -1;
}
if (pos === undefined && selectionEnd.isBefore(attr.end)) {
pos = selectionEnd.character - attr.value.start.character - 1;
}
if (pos !== undefined) {
let [newSelectionStartOffset, newSelectionEndOffset] = findNextWord(attr.value.toString(), pos);
if (!isNumber(newSelectionStartOffset) || !isNumber(newSelectionEndOffset)) {
return;
}
if (newSelectionStartOffset >= 0 && newSelectionEndOffset >= 0) {
const newSelectionStart = (<vscode.Position>attr.value.start).translate(0, newSelectionStartOffset);
const newSelectionEnd = (<vscode.Position>attr.value.start).translate(0, newSelectionEndOffset);
return new vscode.Selection(newSelectionStart, newSelectionEnd);
}
}
}
return;
}
function getPrevAttribute(selectionStart: vscode.Position, selectionEnd: vscode.Position, node: HtmlNode): vscode.Selection | undefined {
if (!node.attributes || node.attributes.length === 0 || node.type === 'comment') {
return;
}
for (let i = node.attributes.length - 1; i >= 0; i--) {
let attr = node.attributes[i];
if (selectionStart.isBeforeOrEqual(attr.start)) {
continue;
}
if (!attr.value || (<vscode.Position>attr.value.start).isEqual(attr.value.end) || selectionStart.isBefore(attr.value.start)) {
// select full attr
return new vscode.Selection(attr.start, attr.end);
}
if (selectionStart.isEqual(attr.value.start)) {
if (selectionEnd.isAfterOrEqual(attr.value.end)) {
// select full attr
return new vscode.Selection(attr.start, attr.end);
}
// select attr value
return new vscode.Selection(attr.value.start, attr.value.end);
}
// Fetch the prev word in the attr value
let pos = selectionStart.isAfter(attr.value.end) ? attr.value.toString().length : selectionStart.character - attr.value.start.character;
let [newSelectionStartOffset, newSelectionEndOffset] = findPrevWord(attr.value.toString(), pos);
if (!isNumber(newSelectionStartOffset) || !isNumber(newSelectionEndOffset)) {
return;
}
if (newSelectionStartOffset >= 0 && newSelectionEndOffset >= 0) {
const newSelectionStart = (<vscode.Position>attr.value.start).translate(0, newSelectionStartOffset);
const newSelectionEnd = (<vscode.Position>attr.value.start).translate(0, newSelectionEndOffset);
return new vscode.Selection(newSelectionStart, newSelectionEnd);
}
}
return;
}

View File

@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* 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 { getDeepestNode, findNextWord, findPrevWord, getNode } from './util';
import { Node, CssNode, Rule, Property } from 'EmmetNode';
export function nextItemStylesheet(startOffset: vscode.Position, endOffset: vscode.Position, rootNode: Node): vscode.Selection | undefined {
let currentNode = <CssNode>getNode(rootNode, endOffset, true);
if (!currentNode) {
currentNode = <CssNode>rootNode;
}
if (!currentNode) {
return;
}
// Full property is selected, so select full property value next
if (currentNode.type === 'property' && startOffset.isEqual(currentNode.start) && endOffset.isEqual(currentNode.end)) {
return getSelectionFromProperty(currentNode, startOffset, endOffset, true, 'next');
}
// Part or whole of propertyValue is selected, so select the next word in the propertyValue
if (currentNode.type === 'property' && startOffset.isAfterOrEqual((<Property>currentNode).valueToken.start) && endOffset.isBeforeOrEqual((<Property>currentNode).valueToken.end)) {
let singlePropertyValue = getSelectionFromProperty(currentNode, startOffset, endOffset, false, 'next');
if (singlePropertyValue) {
return singlePropertyValue;
}
}
// Cursor is in the selector or in a property
if ((currentNode.type === 'rule' && endOffset.isBefore((<Rule>currentNode).selectorToken.end))
|| (currentNode.type === 'property' && endOffset.isBefore((<Property>currentNode).valueToken.end))) {
return getSelectionFromNode(currentNode);
}
// Get the first child of current node which is right after the cursor
let nextNode = currentNode.firstChild;
while (nextNode && endOffset.isAfterOrEqual(nextNode.end)) {
nextNode = nextNode.nextSibling;
}
// Get next sibling of current node or the parent
while (!nextNode && currentNode) {
nextNode = currentNode.nextSibling;
currentNode = currentNode.parent;
}
return getSelectionFromNode(nextNode);
}
export function prevItemStylesheet(startOffset: vscode.Position, endOffset: vscode.Position, rootNode: CssNode): vscode.Selection | undefined {
let currentNode = <CssNode>getNode(rootNode, startOffset, false);
if (!currentNode) {
currentNode = rootNode;
}
if (!currentNode) {
return;
}
// Full property value is selected, so select the whole property next
if (currentNode.type === 'property' && startOffset.isEqual((<Property>currentNode).valueToken.start) && endOffset.isEqual((<Property>currentNode).valueToken.end)) {
return getSelectionFromNode(currentNode);
}
// Part of propertyValue is selected, so select the prev word in the propertyValue
if (currentNode.type === 'property' && startOffset.isAfterOrEqual((<Property>currentNode).valueToken.start) && endOffset.isBeforeOrEqual((<Property>currentNode).valueToken.end)) {
let singlePropertyValue = getSelectionFromProperty(currentNode, startOffset, endOffset, false, 'prev');
if (singlePropertyValue) {
return singlePropertyValue;
}
}
if (currentNode.type === 'property' || !currentNode.firstChild || (currentNode.type === 'rule' && startOffset.isBeforeOrEqual(currentNode.firstChild.start))) {
return getSelectionFromNode(currentNode);
}
// Select the child that appears just before the cursor
let prevNode = currentNode.firstChild;
while (prevNode.nextSibling && startOffset.isAfterOrEqual(prevNode.nextSibling.end)) {
prevNode = prevNode.nextSibling;
}
prevNode = <CssNode>getDeepestNode(prevNode);
return getSelectionFromProperty(prevNode, startOffset, endOffset, false, 'prev');
}
function getSelectionFromNode(node: Node): vscode.Selection | undefined {
if (!node) {
return;
}
let nodeToSelect = node.type === 'rule' ? (<Rule>node).selectorToken : node;
return new vscode.Selection(nodeToSelect.start, nodeToSelect.end);
}
function getSelectionFromProperty(node: Node, selectionStart: vscode.Position, selectionEnd: vscode.Position, selectFullValue: boolean, direction: string): vscode.Selection | undefined {
if (!node || node.type !== 'property') {
return;
}
const propertyNode = <Property>node;
let propertyValue = propertyNode.valueToken.stream.substring(propertyNode.valueToken.start, propertyNode.valueToken.end);
selectFullValue = selectFullValue || (direction === 'prev' && selectionStart.isEqual(propertyNode.valueToken.start) && selectionEnd.isBefore(propertyNode.valueToken.end));
if (selectFullValue) {
return new vscode.Selection(propertyNode.valueToken.start, propertyNode.valueToken.end);
}
let pos: number = -1;
if (direction === 'prev') {
if (selectionStart.isEqual(propertyNode.valueToken.start)) {
return;
}
pos = selectionStart.isAfter(propertyNode.valueToken.end) ? propertyValue.length : selectionStart.character - propertyNode.valueToken.start.character;
}
if (direction === 'next') {
if (selectionEnd.isEqual(propertyNode.valueToken.end) && (selectionStart.isAfter(propertyNode.valueToken.start) || propertyValue.indexOf(' ') === -1)) {
return;
}
pos = selectionEnd.isEqual(propertyNode.valueToken.end) ? -1 : selectionEnd.character - propertyNode.valueToken.start.character - 1;
}
let [newSelectionStartOffset, newSelectionEndOffset] = direction === 'prev' ? findPrevWord(propertyValue, pos) : findNextWord(propertyValue, pos);
if (!newSelectionStartOffset && !newSelectionEndOffset) {
return;
}
const newSelectionStart = (<vscode.Position>propertyNode.valueToken.start).translate(0, newSelectionStartOffset);
const newSelectionEnd = (<vscode.Position>propertyNode.valueToken.start).translate(0, newSelectionEndOffset);
return new vscode.Selection(newSelectionStart, newSelectionEnd);
}

View File

@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* 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 { HtmlNode } from 'EmmetNode';
import { getHtmlNode, parseDocument, validate, getEmmetMode, getEmmetConfiguration } from './util';
export function splitJoinTag() {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
let rootNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) {
return;
}
return editor.edit(editBuilder => {
editor.selections.reverse().forEach(selection => {
let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true);
if (nodeToUpdate) {
let textEdit = getRangesToReplace(editor.document, nodeToUpdate);
editBuilder.replace(textEdit.range, textEdit.newText);
}
});
});
}
function getRangesToReplace(document: vscode.TextDocument, nodeToUpdate: HtmlNode): vscode.TextEdit {
let rangeToReplace: vscode.Range;
let textToReplaceWith: string;
if (!nodeToUpdate.close) {
// Split Tag
let nodeText = document.getText(new vscode.Range(nodeToUpdate.start, nodeToUpdate.end));
let m = nodeText.match(/(\s*\/)?>$/);
let end = <vscode.Position>nodeToUpdate.end;
let start = m ? end.translate(0, -m[0].length) : end;
rangeToReplace = new vscode.Range(start, end);
textToReplaceWith = `></${nodeToUpdate.name}>`;
} else {
// Join Tag
let start = (<vscode.Position>nodeToUpdate.open.end).translate(0, -1);
let end = <vscode.Position>nodeToUpdate.end;
rangeToReplace = new vscode.Range(start, end);
textToReplaceWith = '/>';
const emmetMode = getEmmetMode(document.languageId, []) || '';
const emmetConfig = getEmmetConfiguration(emmetMode);
if (emmetMode && emmetConfig.syntaxProfiles[emmetMode] &&
(emmetConfig.syntaxProfiles[emmetMode]['selfClosingStyle'] === 'xhtml' || emmetConfig.syntaxProfiles[emmetMode]['self_closing_tag'] === 'xhtml')) {
textToReplaceWith = ' ' + textToReplaceWith;
}
}
return new vscode.TextEdit(rangeToReplace, textToReplaceWith);
}

View File

@ -0,0 +1,555 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { Selection, workspace, CancellationTokenSource, CompletionTriggerKind, ConfigurationTarget } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { expandEmmetAbbreviation } from '../abbreviationActions';
import { DefaultCompletionItemProvider } from '../defaultCompletionProvider';
const completionProvider = new DefaultCompletionItemProvider();
const htmlContents = `
<body class="header">
<ul class="nav main">
<li class="item1">img</li>
<li class="item2">hithere</li>
ul>li
ul>li*2
ul>li.item$*2
ul>li.item$@44*2
<div i
</ul>
<style>
.boo {
display: dn; m10
}
</style>
<span></span>
(ul>li.item$)*2
(ul>li.item$)*2+span
(div>dl>(dt+dd)*2)
<script type="text/html">
span.hello
</script>
<script type="text/javascript">
span.bye
</script>
</body>
`;
suite('Tests for Expand Abbreviations (HTML)', () => {
const oldValueForExcludeLanguages = workspace.getConfiguration('emmet').inspect('excludeLanguages');
const oldValueForInlcudeLanguages = workspace.getConfiguration('emmet').inspect('includeLanguages');
teardown(() => {
// close all editors
return closeAllEditors;
});
test('Expand snippets (HTML)', () => {
return testExpandAbbreviation('html', new Selection(3, 23, 3, 23), 'img', '<img src=\"\" alt=\"\">');
});
test('Expand snippets in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(3, 23, 3, 23), 'img', '<img src=\"\" alt=\"\">');
});
test('Expand snippets when no parent node (HTML)', () => {
return withRandomFileEditor('img', 'html', async (editor, _doc) => {
editor.selection = new Selection(0, 3, 0, 3);
await expandEmmetAbbreviation(null);
assert.strictEqual(editor.document.getText(), '<img src=\"\" alt=\"\">');
return Promise.resolve();
});
});
test('Expand snippets when no parent node in completion list (HTML)', () => {
return withRandomFileEditor('img', 'html', async (editor, _doc) => {
editor.selection = new Selection(0, 3, 0, 3);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
assert.strictEqual(!completionPromise, false, `Got unexpected undefined instead of a completion promise`);
return Promise.resolve();
}
const completionList = await completionPromise;
assert.strictEqual(completionList && completionList.items && completionList.items.length > 0, true);
if (completionList) {
assert.strictEqual(completionList.items[0].label, 'img');
assert.strictEqual(((<string>completionList.items[0].documentation) || '').replace(/\|/g, ''), '<img src=\"\" alt=\"\">');
}
return Promise.resolve();
});
});
test('Expand abbreviation (HTML)', () => {
return testExpandAbbreviation('html', new Selection(5, 25, 5, 25), 'ul>li', '<ul>\n\t\t\t<li></li>\n\t\t</ul>');
});
test('Expand abbreviation in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(5, 25, 5, 25), 'ul>li', '<ul>\n\t<li></li>\n</ul>');
});
test('Expand text that is neither an abbreviation nor a snippet to tags (HTML)', () => {
return testExpandAbbreviation('html', new Selection(4, 20, 4, 27), 'hithere', '<hithere></hithere>');
});
test('Do not Expand text that is neither an abbreviation nor a snippet to tags in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(4, 20, 4, 27), 'hithere', '<hithere></hithere>', true);
});
test('Expand abbreviation with repeaters (HTML)', () => {
return testExpandAbbreviation('html', new Selection(6, 27, 6, 27), 'ul>li*2', '<ul>\n\t\t\t<li></li>\n\t\t\t<li></li>\n\t\t</ul>');
});
test('Expand abbreviation with repeaters in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(6, 27, 6, 27), 'ul>li*2', '<ul>\n\t<li></li>\n\t<li></li>\n</ul>');
});
test('Expand abbreviation with numbered repeaters (HTML)', () => {
return testExpandAbbreviation('html', new Selection(7, 33, 7, 33), 'ul>li.item$*2', '<ul>\n\t\t\t<li class="item1"></li>\n\t\t\t<li class="item2"></li>\n\t\t</ul>');
});
test('Expand abbreviation with numbered repeaters in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(7, 33, 7, 33), 'ul>li.item$*2', '<ul>\n\t<li class="item1"></li>\n\t<li class="item2"></li>\n</ul>');
});
test('Expand abbreviation with numbered repeaters with offset (HTML)', () => {
return testExpandAbbreviation('html', new Selection(8, 36, 8, 36), 'ul>li.item$@44*2', '<ul>\n\t\t\t<li class="item44"></li>\n\t\t\t<li class="item45"></li>\n\t\t</ul>');
});
test('Expand abbreviation with numbered repeaters with offset in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(8, 36, 8, 36), 'ul>li.item$@44*2', '<ul>\n\t<li class="item44"></li>\n\t<li class="item45"></li>\n</ul>');
});
test('Expand abbreviation with numbered repeaters in groups (HTML)', () => {
return testExpandAbbreviation('html', new Selection(17, 16, 17, 16), '(ul>li.item$)*2', '<ul>\n\t\t<li class="item1"></li>\n\t</ul>\n\t<ul>\n\t\t<li class="item2"></li>\n\t</ul>');
});
test('Expand abbreviation with numbered repeaters in groups in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(17, 16, 17, 16), '(ul>li.item$)*2', '<ul>\n\t<li class="item1"></li>\n</ul>\n<ul>\n\t<li class="item2"></li>\n</ul>');
});
test('Expand abbreviation with numbered repeaters in groups with sibling in the end (HTML)', () => {
return testExpandAbbreviation('html', new Selection(18, 21, 18, 21), '(ul>li.item$)*2+span', '<ul>\n\t\t<li class="item1"></li>\n\t</ul>\n\t<ul>\n\t\t<li class="item2"></li>\n\t</ul>\n\t<span></span>');
});
test('Expand abbreviation with numbered repeaters in groups with sibling in the end in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(18, 21, 18, 21), '(ul>li.item$)*2+span', '<ul>\n\t<li class="item1"></li>\n</ul>\n<ul>\n\t<li class="item2"></li>\n</ul>\n<span></span>');
});
test('Expand abbreviation with nested groups (HTML)', () => {
return testExpandAbbreviation('html', new Selection(19, 19, 19, 19), '(div>dl>(dt+dd)*2)', '<div>\n\t\t<dl>\n\t\t\t<dt></dt>\n\t\t\t<dd></dd>\n\t\t\t<dt></dt>\n\t\t\t<dd></dd>\n\t\t</dl>\n\t</div>');
});
test('Expand abbreviation with nested groups in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(19, 19, 19, 19), '(div>dl>(dt+dd)*2)', '<div>\n\t<dl>\n\t\t<dt></dt>\n\t\t<dd></dd>\n\t\t<dt></dt>\n\t\t<dd></dd>\n\t</dl>\n</div>');
});
test('Expand tag that is opened, but not closed (HTML)', () => {
return testExpandAbbreviation('html', new Selection(9, 6, 9, 6), '<div', '<div></div>');
});
test('Do not Expand tag that is opened, but not closed in completion list (HTML)', () => {
return testHtmlCompletionProvider(new Selection(9, 6, 9, 6), '<div', '<div></div>', true);
});
test('No expanding text inside open tag (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(2, 4, 2, 4);
await expandEmmetAbbreviation(null);
assert.strictEqual(editor.document.getText(), htmlContents);
return Promise.resolve();
});
});
test('No expanding text inside open tag in completion list (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, _doc) => {
editor.selection = new Selection(2, 4, 2, 4);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`);
return Promise.resolve();
});
});
test('No expanding text inside open tag when there is no closing tag (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(9, 8, 9, 8);
await expandEmmetAbbreviation(null);
assert.strictEqual(editor.document.getText(), htmlContents);
return Promise.resolve();
});
});
test('No expanding text inside open tag when there is no closing tag in completion list (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, _doc) => {
editor.selection = new Selection(9, 8, 9, 8);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`);
return Promise.resolve();
});
});
test('No expanding text inside open tag when there is no closing tag when there is no parent node (HTML)', () => {
const fileContents = '<img s';
return withRandomFileEditor(fileContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(0, 6, 0, 6);
await expandEmmetAbbreviation(null);
assert.strictEqual(editor.document.getText(), fileContents);
return Promise.resolve();
});
});
test('No expanding text in completion list inside open tag when there is no closing tag when there is no parent node (HTML)', () => {
const fileContents = '<img s';
return withRandomFileEditor(fileContents, 'html', (editor, _doc) => {
editor.selection = new Selection(0, 6, 0, 6);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`);
return Promise.resolve();
});
});
test('Expand css when inside style tag (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(13, 16, 13, 19);
const expandPromise = expandEmmetAbbreviation({ language: 'css' });
if (!expandPromise) {
return Promise.resolve();
}
await expandPromise;
assert.strictEqual(editor.document.getText(), htmlContents.replace('m10', 'margin: 10px;'));
return Promise.resolve();
});
});
test('Expand css when inside style tag in completion list (HTML)', () => {
const abbreviation = 'm10';
const expandedText = 'margin: 10px;';
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(13, 16, 13, 19);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
assert.strictEqual(1, 2, `Problem with expanding m10`);
return Promise.resolve();
}
const completionList = await completionPromise;
if (!completionList || !completionList.items || !completionList.items.length) {
assert.strictEqual(1, 2, `Problem with expanding m10`);
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.strictEqual(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.strictEqual(((<string>emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
assert.strictEqual(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`);
return Promise.resolve();
});
});
test('No expanding text inside style tag if position is not for property name (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(13, 14, 13, 14);
await expandEmmetAbbreviation(null);
assert.strictEqual(editor.document.getText(), htmlContents);
return Promise.resolve();
});
});
test('Expand css when inside style attribute (HTML)', () => {
const styleAttributeContent = '<div style="m10" class="hello"></div>';
return withRandomFileEditor(styleAttributeContent, 'html', async (editor, _doc) => {
editor.selection = new Selection(0, 15, 0, 15);
const expandPromise = expandEmmetAbbreviation(null);
if (!expandPromise) {
return Promise.resolve();
}
await expandPromise;
assert.strictEqual(editor.document.getText(), styleAttributeContent.replace('m10', 'margin: 10px;'));
return Promise.resolve();
});
});
test('Expand css when inside style attribute in completion list (HTML)', () => {
const abbreviation = 'm10';
const expandedText = 'margin: 10px;';
return withRandomFileEditor('<div style="m10" class="hello"></div>', 'html', async (editor, _doc) => {
editor.selection = new Selection(0, 15, 0, 15);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
assert.strictEqual(1, 2, `Problem with expanding m10`);
return Promise.resolve();
}
const completionList = await completionPromise;
if (!completionList || !completionList.items || !completionList.items.length) {
assert.strictEqual(1, 2, `Problem with expanding m10`);
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.strictEqual(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.strictEqual(((<string>emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
assert.strictEqual(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`);
return Promise.resolve();
});
});
test('Expand html when inside script tag with html type (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(21, 12, 21, 12);
const expandPromise = expandEmmetAbbreviation(null);
if (!expandPromise) {
return Promise.resolve();
}
await expandPromise;
assert.strictEqual(editor.document.getText(), htmlContents.replace('span.hello', '<span class="hello"></span>'));
return Promise.resolve();
});
});
test('Expand html in completion list when inside script tag with html type (HTML)', () => {
const abbreviation = 'span.hello';
const expandedText = '<span class="hello"></span>';
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(21, 12, 21, 12);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
assert.strictEqual(1, 2, `Problem with expanding span.hello`);
return Promise.resolve();
}
const completionList = await completionPromise;
if (!completionList || !completionList.items || !completionList.items.length) {
assert.strictEqual(1, 2, `Problem with expanding span.hello`);
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`);
assert.strictEqual(((<string>emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
return Promise.resolve();
});
});
test('No expanding text inside script tag with javascript type (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(24, 12, 24, 12);
await expandEmmetAbbreviation(null);
assert.strictEqual(editor.document.getText(), htmlContents);
return Promise.resolve();
});
});
test('No expanding text in completion list inside script tag with javascript type (HTML)', () => {
return withRandomFileEditor(htmlContents, 'html', (editor, _doc) => {
editor.selection = new Selection(24, 12, 24, 12);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`);
return Promise.resolve();
});
});
test('Expand html when inside script tag with javascript type if js is mapped to html (HTML)', async () => {
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': 'html' }, ConfigurationTarget.Global);
await withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(24, 10, 24, 10);
const expandPromise = expandEmmetAbbreviation(null);
if (!expandPromise) {
return Promise.resolve();
}
await expandPromise;
assert.strictEqual(editor.document.getText(), htmlContents.replace('span.bye', '<span class="bye"></span>'));
});
return workspace.getConfiguration('emmet').update('includeLanguages', oldValueForInlcudeLanguages || {}, ConfigurationTarget.Global);
});
test('Expand html in completion list when inside script tag with javascript type if js is mapped to html (HTML)', async () => {
const abbreviation = 'span.bye';
const expandedText = '<span class="bye"></span>';
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': 'html' }, ConfigurationTarget.Global);
await withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(24, 10, 24, 10);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
assert.strictEqual(1, 2, `Problem with expanding span.bye`);
return Promise.resolve();
}
const completionList = await completionPromise;
if (!completionList || !completionList.items || !completionList.items.length) {
assert.strictEqual(1, 2, `Problem with expanding span.bye`);
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item (${emmetCompletionItem.label}) doesnt match.`);
assert.strictEqual(((<string>emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
return Promise.resolve();
});
return workspace.getConfiguration('emmet').update('includeLanguages', oldValueForInlcudeLanguages || {}, ConfigurationTarget.Global);
});
// test('No expanding when html is excluded in the settings', () => {
// return workspace.getConfiguration('emmet').update('excludeLanguages', ['html'], ConfigurationTarget.Global).then(() => {
// return testExpandAbbreviation('html', new Selection(9, 6, 9, 6), '', '', true).then(() => {
// return workspace.getConfiguration('emmet').update('excludeLanguages', oldValueForExcludeLanguages ? oldValueForExcludeLanguages.globalValue : undefined, ConfigurationTarget.Global);
// });
// });
// });
test('No expanding when html is excluded in the settings in completion list', async () => {
await workspace.getConfiguration('emmet').update('excludeLanguages', ['html'], ConfigurationTarget.Global);
await testHtmlCompletionProvider(new Selection(9, 6, 9, 6), '', '', true);
return workspace.getConfiguration('emmet').update('excludeLanguages', oldValueForExcludeLanguages ? oldValueForExcludeLanguages.globalValue : undefined, ConfigurationTarget.Global);
});
// test('No expanding when php (mapped syntax) is excluded in the settings', () => {
// return workspace.getConfiguration('emmet').update('excludeLanguages', ['php'], ConfigurationTarget.Global).then(() => {
// return testExpandAbbreviation('php', new Selection(9, 6, 9, 6), '', '', true).then(() => {
// return workspace.getConfiguration('emmet').update('excludeLanguages', oldValueForExcludeLanguages ? oldValueForExcludeLanguages.globalValue : undefined, ConfigurationTarget.Global);
// });
// });
// });
});
suite('Tests for jsx, xml and xsl', () => {
const oldValueForSyntaxProfiles = workspace.getConfiguration('emmet').inspect('syntaxProfiles');
teardown(closeAllEditors);
test('Expand abbreviation with className instead of class in jsx', () => {
return withRandomFileEditor('ul.nav', 'javascriptreact', async (editor, _doc) => {
editor.selection = new Selection(0, 6, 0, 6);
await expandEmmetAbbreviation({ language: 'javascriptreact' });
assert.strictEqual(editor.document.getText(), '<ul className="nav"></ul>');
return Promise.resolve();
});
});
test('Expand abbreviation with self closing tags for jsx', () => {
return withRandomFileEditor('img', 'javascriptreact', async (editor, _doc) => {
editor.selection = new Selection(0, 6, 0, 6);
await expandEmmetAbbreviation({ language: 'javascriptreact' });
assert.strictEqual(editor.document.getText(), '<img src="" alt=""/>');
return Promise.resolve();
});
});
test('Expand abbreviation with single quotes for jsx', async () => {
await workspace.getConfiguration('emmet').update('syntaxProfiles', { jsx: { 'attr_quotes': 'single' } }, ConfigurationTarget.Global);
return withRandomFileEditor('img', 'javascriptreact', async (editor, _doc) => {
editor.selection = new Selection(0, 6, 0, 6);
await expandEmmetAbbreviation({ language: 'javascriptreact' });
assert.strictEqual(editor.document.getText(), '<img src=\'\' alt=\'\'/>');
return workspace.getConfiguration('emmet').update('syntaxProfiles', oldValueForSyntaxProfiles ? oldValueForSyntaxProfiles.globalValue : undefined, ConfigurationTarget.Global);
});
});
test('Expand abbreviation with self closing tags for xml', () => {
return withRandomFileEditor('img', 'xml', async (editor, _doc) => {
editor.selection = new Selection(0, 6, 0, 6);
await expandEmmetAbbreviation({ language: 'xml' });
assert.strictEqual(editor.document.getText(), '<img src="" alt=""/>');
return Promise.resolve();
});
});
test('Expand abbreviation with no self closing tags for html', () => {
return withRandomFileEditor('img', 'html', async (editor, _doc) => {
editor.selection = new Selection(0, 6, 0, 6);
await expandEmmetAbbreviation({ language: 'html' });
assert.strictEqual(editor.document.getText(), '<img src="" alt="">');
return Promise.resolve();
});
});
test('Expand abbreviation with condition containing less than sign for jsx', () => {
return withRandomFileEditor('if (foo < 10) { span.bar', 'javascriptreact', async (editor, _doc) => {
editor.selection = new Selection(0, 27, 0, 27);
await expandEmmetAbbreviation({ language: 'javascriptreact' });
assert.strictEqual(editor.document.getText(), 'if (foo < 10) { <span className="bar"></span>');
return Promise.resolve();
});
});
test('No expanding text inside open tag in completion list (jsx)', () => {
return testNoCompletion('jsx', htmlContents, new Selection(2, 4, 2, 4));
});
test('No expanding tag that is opened, but not closed in completion list (jsx)', () => {
return testNoCompletion('jsx', htmlContents, new Selection(9, 6, 9, 6));
});
test('No expanding text inside open tag when there is no closing tag in completion list (jsx)', () => {
return testNoCompletion('jsx', htmlContents, new Selection(9, 8, 9, 8));
});
test('No expanding text in completion list inside open tag when there is no closing tag when there is no parent node (jsx)', () => {
return testNoCompletion('jsx', '<img s', new Selection(0, 6, 0, 6));
});
});
function testExpandAbbreviation(syntax: string, selection: Selection, abbreviation: string, expandedText: string, shouldFail?: boolean): Thenable<any> {
return withRandomFileEditor(htmlContents, syntax, async (editor, _doc) => {
editor.selection = selection;
const expandPromise = expandEmmetAbbreviation(null);
if (!expandPromise) {
if (!shouldFail) {
assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`);
}
return Promise.resolve();
}
await expandPromise;
assert.strictEqual(editor.document.getText(), htmlContents.replace(abbreviation, expandedText));
return Promise.resolve();
});
}
function testHtmlCompletionProvider(selection: Selection, abbreviation: string, expandedText: string, shouldFail?: boolean): Thenable<any> {
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = selection;
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise) {
if (!shouldFail) {
assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`);
}
return Promise.resolve();
}
const completionList = await completionPromise;
if (!completionList || !completionList.items || !completionList.items.length) {
if (!shouldFail) {
assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`);
}
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`);
assert.strictEqual(((<string>emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
return Promise.resolve();
});
}
function testNoCompletion(syntax: string, fileContents: string, selection: Selection): Thenable<any> {
return withRandomFileEditor(fileContents, syntax, (editor, _doc) => {
editor.selection = selection;
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`);
return Promise.resolve();
});
}

View File

@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { CancellationTokenSource, CompletionTriggerKind, Selection } from 'vscode';
import { DefaultCompletionItemProvider } from '../defaultCompletionProvider';
import { closeAllEditors, withRandomFileEditor } from './testUtils';
const completionProvider = new DefaultCompletionItemProvider();
suite('Tests for completion in CSS embedded in HTML', () => {
teardown(() => {
// close all editors
return closeAllEditors;
});
test('style attribute & attribute value in html', async () => {
await testHtmlCompletionProvider('<div style="|"', [{ label: 'padding: ;' }]);
await testHtmlCompletionProvider(`<div style='|'`, [{ label: 'padding: ;' }]);
await testHtmlCompletionProvider(`<div style='p|'`, [{ label: 'padding: ;' }]);
await testHtmlCompletionProvider(`<div style='color: #0|'`, [{ label: '#000000' }]);
});
// https://github.com/microsoft/vscode/issues/79766
test('#79766, correct region determination', async () => {
await testHtmlCompletionProvider(`<div style="color: #000">di|</div>`, [
{ label: 'div', documentation: `<div>|</div>` }
]);
});
// https://github.com/microsoft/vscode/issues/86941
test('#86941, widows should not be completed', async () => {
await testCssCompletionProvider(`.foo { wi| }`, [
{ label: 'widows: ;', documentation: `widows: ;` }
]);
});
});
interface TestCompletionItem {
label: string;
documentation?: string;
}
function testHtmlCompletionProvider(contents: string, expectedItems: TestCompletionItem[]): Thenable<any> {
const cursorPos = contents.indexOf('|');
const htmlContents = contents.slice(0, cursorPos) + contents.slice(cursorPos + 1);
return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
const selection = new Selection(editor.document.positionAt(cursorPos), editor.document.positionAt(cursorPos));
editor.selection = selection;
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(
editor.document,
editor.selection.active,
cancelSrc.token,
{ triggerKind: CompletionTriggerKind.Invoke }
);
if (!completionPromise) {
return Promise.resolve();
}
const completionList = await completionPromise;
if (!completionList || !completionList.items || !completionList.items.length) {
return Promise.resolve();
}
expectedItems.forEach(eItem => {
const matches = completionList.items.filter(i => i.label === eItem.label);
const match = matches && matches.length > 0 ? matches[0] : undefined;
assert.ok(match, `Didn't find completion item with label ${eItem.label}`);
if (match) {
assert.equal(match.detail, 'Emmet Abbreviation', `Match needs to come from Emmet`);
if (eItem.documentation) {
assert.equal(match.documentation, eItem.documentation, `Emmet completion Documentation doesn't match`);
}
}
});
return Promise.resolve();
});
}
function testCssCompletionProvider(contents: string, expectedItems: TestCompletionItem[]): Thenable<any> {
const cursorPos = contents.indexOf('|');
const cssContents = contents.slice(0, cursorPos) + contents.slice(cursorPos + 1);
return withRandomFileEditor(cssContents, 'css', async (editor, _doc) => {
const selection = new Selection(editor.document.positionAt(cursorPos), editor.document.positionAt(cursorPos));
editor.selection = selection;
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(
editor.document,
editor.selection.active,
cancelSrc.token,
{ triggerKind: CompletionTriggerKind.Invoke }
);
if (!completionPromise) {
return Promise.resolve();
}
const completionList = await completionPromise;
if (!completionList || !completionList.items || !completionList.items.length) {
return Promise.resolve();
}
expectedItems.forEach(eItem => {
const matches = completionList.items.filter(i => i.label === eItem.label);
const match = matches && matches.length > 0 ? matches[0] : undefined;
assert.ok(match, `Didn't find completion item with label ${eItem.label}`);
if (match) {
assert.equal(match.detail, 'Emmet Abbreviation', `Match needs to come from Emmet`);
if (eItem.documentation) {
assert.equal(match.documentation, eItem.documentation, `Emmet completion Documentation doesn't match`);
}
}
});
return Promise.resolve();
});
}

View File

@ -0,0 +1,503 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { Selection, CompletionList, CancellationTokenSource, Position, CompletionTriggerKind } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { expandEmmetAbbreviation } from '../abbreviationActions';
import { DefaultCompletionItemProvider } from '../defaultCompletionProvider';
const completionProvider = new DefaultCompletionItemProvider();
const cssContents = `
.boo {
margin: 20px 10px;
pos:f
background-image: url('tryme.png');
pos:f
}
.boo .hoo {
margin: 10px;
ind
}
`;
const scssContents = `
.boo {
margin: 10px;
p10
.hoo {
p20
}
}
@include b(alert) {
margin: 10px;
p30
@include b(alert) {
p40
}
}
.foo {
margin: 10px;
margin: a
.hoo {
color: #000;
}
}
`;
suite('Tests for Expand Abbreviations (CSS)', () => {
teardown(closeAllEditors);
test('Expand abbreviation (CSS)', () => {
return withRandomFileEditor(cssContents, 'css', (editor, _) => {
editor.selections = [new Selection(3, 1, 3, 6), new Selection(5, 1, 5, 6)];
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), cssContents.replace(/pos:f/g, 'position: fixed;'));
return Promise.resolve();
});
});
});
test('No emmet when cursor inside comment (CSS)', () => {
const testContent = `
.foo {
/*margin: 10px;
m10
padding: 10px;*/
display: auto;
}
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
editor.selection = new Selection(3, 4, 3, 4);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, new Position(2, 10), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `Invalid completion at property value`);
}
return Promise.resolve();
});
});
});
test('No emmet when cursor in selector of a rule (CSS)', () => {
const testContent = `
.foo {
margin: 10px;
}
nav#
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
editor.selection = new Selection(5, 4, 5, 4);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, new Position(2, 10), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `Invalid completion at property value`);
}
return Promise.resolve();
});
});
});
test('Skip when typing property values when there is a property in the next line (CSS)', () => {
const testContent = `
.foo {
margin: a
margin: 10px;
}
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
editor.selection = new Selection(2, 10, 2, 10);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, new Position(2, 10), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `Invalid completion at property value`);
}
return Promise.resolve();
});
});
});
test('Skip when typing the last property value in single line rules (CSS)', () => {
const testContent = `.foo {padding: 10px; margin: a}`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
editor.selection = new Selection(0, 30, 0, 30);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, new Position(0, 30), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `Invalid completion at property value`);
}
return Promise.resolve();
});
});
});
test('Allow hex color or !important when typing property values when there is a property in the next line (CSS)', () => {
const testContent = `
.foo {
margin: #12 !
margin: 10px;
}
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
const cancelSrc = new CancellationTokenSource();
const completionPromise1 = completionProvider.provideCompletionItems(editor.document, new Position(2, 12), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
const completionPromise2 = completionProvider.provideCompletionItems(editor.document, new Position(2, 14), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise1 || !completionPromise2) {
assert.equal(1, 2, `Completion promise wasnt returned`);
return Promise.resolve();
}
const callBack = (completionList: CompletionList, expandedText: string) => {
if (!completionList.items || !completionList.items.length) {
assert.equal(1, 2, `Empty Completions`);
return;
}
const emmetCompletionItem = completionList.items[0];
assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
};
return Promise.all<CompletionList>([completionPromise1, completionPromise2]).then(([result1, result2]) => {
callBack(result1, '#121212');
callBack(result2, '!important');
editor.selections = [new Selection(2, 12, 2, 12), new Selection(2, 14, 2, 14)];
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent.replace('#12', '#121212').replace('!', '!important'));
});
});
});
});
test('Skip when typing property values when there is a property in the previous line (CSS)', () => {
const testContent = `
.foo {
margin: 10px;
margin: a
}
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
editor.selection = new Selection(3, 10, 3, 10);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, new Position(3, 10), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `Invalid completion at property value`);
}
return Promise.resolve();
});
});
});
test('Allow hex color or !important when typing property values when there is a property in the previous line (CSS)', () => {
const testContent = `
.foo {
margin: 10px;
margin: #12 !
}
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
const cancelSrc = new CancellationTokenSource();
const completionPromise1 = completionProvider.provideCompletionItems(editor.document, new Position(3, 12), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
const completionPromise2 = completionProvider.provideCompletionItems(editor.document, new Position(3, 14), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise1 || !completionPromise2) {
assert.equal(1, 2, `Completion promise wasnt returned`);
return Promise.resolve();
}
const callBack = (completionList: CompletionList, expandedText: string) => {
if (!completionList.items || !completionList.items.length) {
assert.equal(1, 2, `Empty Completions`);
return;
}
const emmetCompletionItem = completionList.items[0];
assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
};
return Promise.all<CompletionList>([completionPromise1, completionPromise2]).then(([result1, result2]) => {
callBack(result1, '#121212');
callBack(result2, '!important');
editor.selections = [new Selection(3, 12, 3, 12), new Selection(3, 14, 3, 14)];
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent.replace('#12', '#121212').replace('!', '!important'));
});
});
});
});
test('Skip when typing property values when it is the only property in the rule (CSS)', () => {
const testContent = `
.foo {
margin: a
}
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
editor.selection = new Selection(2, 10, 2, 10);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, new Position(2, 10), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `Invalid completion at property value`);
}
return Promise.resolve();
});
});
});
test('Allow hex colors or !important when typing property values when it is the only property in the rule (CSS)', () => {
const testContent = `
.foo {
margin: #12 !
}
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
const cancelSrc = new CancellationTokenSource();
const completionPromise1 = completionProvider.provideCompletionItems(editor.document, new Position(2, 12), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
const completionPromise2 = completionProvider.provideCompletionItems(editor.document, new Position(2, 14), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise1 || !completionPromise2) {
assert.equal(1, 2, `Completion promise wasnt returned`);
return Promise.resolve();
}
const callBack = (completionList: CompletionList, expandedText: string) => {
if (!completionList.items || !completionList.items.length) {
assert.equal(1, 2, `Empty Completions`);
return;
}
const emmetCompletionItem = completionList.items[0];
assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
};
return Promise.all<CompletionList>([completionPromise1, completionPromise2]).then(([result1, result2]) => {
callBack(result1, '#121212');
callBack(result2, '!important');
editor.selections = [new Selection(2, 12, 2, 12), new Selection(2, 14, 2, 14)];
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent.replace('#12', '#121212').replace('!', '!important'));
});
});
});
});
test('# shouldnt expand to hex color when in selector (CSS)', () => {
const testContent = `
.foo {
#
}
`;
return withRandomFileEditor(testContent, 'css', (editor, _) => {
editor.selection = new Selection(2, 2, 2, 2);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), testContent);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, new Position(2, 2), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `Invalid completion of hex color at property name`);
}
return Promise.resolve();
});
});
});
test('Expand abbreviation in completion list (CSS)', () => {
const abbreviation = 'pos:f';
const expandedText = 'position: fixed;';
return withRandomFileEditor(cssContents, 'css', (editor, _) => {
editor.selection = new Selection(3, 1, 3, 6);
const cancelSrc = new CancellationTokenSource();
const completionPromise1 = completionProvider.provideCompletionItems(editor.document, new Position(3, 6), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
const completionPromise2 = completionProvider.provideCompletionItems(editor.document, new Position(5, 6), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise1 || !completionPromise2) {
assert.equal(1, 2, `Problem with expanding pos:f`);
return Promise.resolve();
}
const callBack = (completionList: CompletionList) => {
if (!completionList.items || !completionList.items.length) {
assert.equal(1, 2, `Problem with expanding pos:f`);
return;
}
const emmetCompletionItem = completionList.items[0];
assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`);
};
return Promise.all<CompletionList>([completionPromise1, completionPromise2]).then(([result1, result2]) => {
callBack(result1);
callBack(result2);
return Promise.resolve();
});
});
});
test('Expand abbreviation (SCSS)', () => {
return withRandomFileEditor(scssContents, 'scss', (editor, _) => {
editor.selections = [
new Selection(3, 4, 3, 4),
new Selection(5, 5, 5, 5),
new Selection(11, 4, 11, 4),
new Selection(14, 5, 14, 5)
];
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), scssContents.replace(/p(\d\d)/g, 'padding: $1px;'));
return Promise.resolve();
});
});
});
test('Expand abbreviation in completion list (SCSS)', () => {
return withRandomFileEditor(scssContents, 'scss', (editor, _) => {
editor.selection = new Selection(3, 4, 3, 4);
const cancelSrc = new CancellationTokenSource();
const completionPromise1 = completionProvider.provideCompletionItems(editor.document, new Position(3, 4), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
const completionPromise2 = completionProvider.provideCompletionItems(editor.document, new Position(5, 5), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
const completionPromise3 = completionProvider.provideCompletionItems(editor.document, new Position(11, 4), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
const completionPromise4 = completionProvider.provideCompletionItems(editor.document, new Position(14, 5), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (!completionPromise1) {
assert.equal(1, 2, `Problem with expanding padding abbreviations at line 3 col 4`);
}
if (!completionPromise2) {
assert.equal(1, 2, `Problem with expanding padding abbreviations at line 5 col 5`);
}
if (!completionPromise3) {
assert.equal(1, 2, `Problem with expanding padding abbreviations at line 11 col 4`);
}
if (!completionPromise4) {
assert.equal(1, 2, `Problem with expanding padding abbreviations at line 14 col 5`);
}
if (!completionPromise1 || !completionPromise2 || !completionPromise3 || !completionPromise4) {
return Promise.resolve();
}
const callBack = (completionList: CompletionList, abbreviation: string, expandedText: string) => {
if (!completionList.items || !completionList.items.length) {
assert.equal(1, 2, `Problem with expanding m10`);
return;
}
const emmetCompletionItem = completionList.items[0];
assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`);
assert.equal((<string>emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`);
};
return Promise.all<CompletionList>([completionPromise1, completionPromise2, completionPromise3, completionPromise4]).then(([result1, result2, result3, result4]) => {
callBack(result1, 'p10', 'padding: 10px;');
callBack(result2, 'p20', 'padding: 20px;');
callBack(result3, 'p30', 'padding: 30px;');
callBack(result4, 'p40', 'padding: 40px;');
return Promise.resolve();
});
});
});
test('Invalid locations for abbreviations in scss', () => {
const scssContentsNoExpand = `
m10
.boo {
margin: 10px;
.hoo {
background:
}
}
`;
return withRandomFileEditor(scssContentsNoExpand, 'scss', (editor, _) => {
editor.selections = [
new Selection(1, 3, 1, 3), // outside rule
new Selection(5, 15, 5, 15) // in the value part of property value
];
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), scssContentsNoExpand);
return Promise.resolve();
});
});
});
test('Invalid locations for abbreviations in scss in completion list', () => {
const scssContentsNoExpand = `
m10
.boo {
margin: 10px;
.hoo {
background:
}
}
`;
return withRandomFileEditor(scssContentsNoExpand, 'scss', (editor, _) => {
editor.selection = new Selection(1, 3, 1, 3); // outside rule
const cancelSrc = new CancellationTokenSource();
let completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `m10 gets expanded in invalid location (outside rule)`);
}
editor.selection = new Selection(5, 15, 5, 15); // in the value part of property value
completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
return completionPromise.then((completionList: CompletionList | undefined) => {
if (completionList && completionList.items && completionList.items.length > 0) {
assert.equal(1, 2, `m10 gets expanded in invalid location (n the value part of property value)`);
}
return Promise.resolve();
});
}
return Promise.resolve();
});
});
});
test('Skip when typing property values when there is a nested rule in the next line (SCSS)', () => {
return withRandomFileEditor(scssContents, 'scss', (editor, _) => {
editor.selection = new Selection(19, 10, 19, 10);
return expandEmmetAbbreviation(null).then(() => {
assert.equal(editor.document.getText(), scssContents);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, new Position(19, 10), cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke });
if (completionPromise) {
assert.equal(1, 2, `Invalid completion at property value`);
}
return Promise.resolve();
});
});
});

View File

@ -0,0 +1,368 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { Selection } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { fetchEditPoint } from '../editPoint';
import { fetchSelectItem } from '../selectItem';
import { balanceOut, balanceIn } from '../balance';
suite('Tests for Next/Previous Select/Edit point and Balance actions', () => {
teardown(closeAllEditors);
const cssContents = `
.boo {
margin: 20px 10px;
background-image: url('tryme.png');
}
.boo .hoo {
margin: 10px;
}
`;
const scssContents = `
.boo {
margin: 20px 10px;
background-image: url('tryme.png');
.boo .hoo {
margin: 10px;
}
}
`;
const htmlContents = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<div>
\t\t
</div>
<div class="header">
<ul class="nav main">
<li class="item1">Item 1</li>
<li class="item2">Item 2</li>
</ul>
</div>
</body>
</html>
`;
test('Emmet Next/Prev Edit point in html file', function (): any {
return withRandomFileEditor(htmlContents, '.html', (editor, _) => {
editor.selections = [new Selection(1, 5, 1, 5)];
let expectedNextEditPoints: [number, number][] = [[4, 16], [6, 8], [10, 2], [20, 0]];
expectedNextEditPoints.forEach(([line, col]) => {
fetchEditPoint('next');
testSelection(editor.selection, col, line);
});
let expectedPrevEditPoints = [[10, 2], [6, 8], [4, 16], [0, 0]];
expectedPrevEditPoints.forEach(([line, col]) => {
fetchEditPoint('prev');
testSelection(editor.selection, col, line);
});
return Promise.resolve();
});
});
test('Emmet Select Next/Prev Item in html file', function (): any {
return withRandomFileEditor(htmlContents, '.html', (editor, _) => {
editor.selections = [new Selection(2, 2, 2, 2)];
let expectedNextItemPoints: [number, number, number][] = [
[2, 1, 5], // html
[2, 6, 15], // lang="en"
[2, 12, 14], // en
[3, 1, 5], // head
[4, 2, 6], // meta
[4, 7, 17], // charset=""
[5, 2, 6], // meta
[5, 7, 22], // name="viewport"
[5, 13, 21], // viewport
[5, 23, 70], // content="width=device-width, initial-scale=1.0"
[5, 32, 69], // width=device-width, initial-scale=1.0
[5, 32, 51], // width=device-width,
[5, 52, 69], // initial-scale=1.0
[6, 2, 7] // title
];
expectedNextItemPoints.forEach(([line, colstart, colend]) => {
fetchSelectItem('next');
testSelection(editor.selection, colstart, line, colend);
});
editor.selections = [new Selection(6, 15, 6, 15)];
expectedNextItemPoints.reverse().forEach(([line, colstart, colend]) => {
fetchSelectItem('prev');
testSelection(editor.selection, colstart, line, colend);
});
return Promise.resolve();
});
});
test('Emmet Select Next/Prev item at boundary', function(): any {
return withRandomFileEditor(htmlContents, '.html', (editor, _) => {
editor.selections = [new Selection(4, 1, 4, 1)];
fetchSelectItem('next');
testSelection(editor.selection, 2, 4, 6);
editor.selections = [new Selection(4, 1, 4, 1)];
fetchSelectItem('prev');
testSelection(editor.selection, 1, 3, 5);
return Promise.resolve();
});
});
test('Emmet Next/Prev Item in html template', function (): any {
const templateContents = `
<script type="text/template">
<div class="header">
<ul class="nav main">
</ul>
</div>
</script>
`;
return withRandomFileEditor(templateContents, '.html', (editor, _) => {
editor.selections = [new Selection(2, 2, 2, 2)];
let expectedNextItemPoints: [number, number, number][] = [
[2, 2, 5], // div
[2, 6, 20], // class="header"
[2, 13, 19], // header
[3, 3, 5], // ul
[3, 6, 22], // class="nav main"
[3, 13, 21], // nav main
[3, 13, 16], // nav
[3, 17, 21], // main
];
expectedNextItemPoints.forEach(([line, colstart, colend]) => {
fetchSelectItem('next');
testSelection(editor.selection, colstart, line, colend);
});
editor.selections = [new Selection(4, 1, 4, 1)];
expectedNextItemPoints.reverse().forEach(([line, colstart, colend]) => {
fetchSelectItem('prev');
testSelection(editor.selection, colstart, line, colend);
});
return Promise.resolve();
});
});
test('Emmet Select Next/Prev Item in css file', function (): any {
return withRandomFileEditor(cssContents, '.css', (editor, _) => {
editor.selections = [new Selection(0, 0, 0, 0)];
let expectedNextItemPoints: [number, number, number][] = [
[1, 0, 4], // .boo
[2, 1, 19], // margin: 20px 10px;
[2, 9, 18], // 20px 10px
[2, 9, 13], // 20px
[2, 14, 18], // 10px
[3, 1, 36], // background-image: url('tryme.png');
[3, 19, 35], // url('tryme.png')
[6, 0, 9], // .boo .hoo
[7, 1, 14], // margin: 10px;
[7, 9, 13], // 10px
];
expectedNextItemPoints.forEach(([line, colstart, colend]) => {
fetchSelectItem('next');
testSelection(editor.selection, colstart, line, colend);
});
editor.selections = [new Selection(9, 0, 9, 0)];
expectedNextItemPoints.reverse().forEach(([line, colstart, colend]) => {
fetchSelectItem('prev');
testSelection(editor.selection, colstart, line, colend);
});
return Promise.resolve();
});
});
test('Emmet Select Next/Prev Item in scss file with nested rules', function (): any {
return withRandomFileEditor(scssContents, '.scss', (editor, _) => {
editor.selections = [new Selection(0, 0, 0, 0)];
let expectedNextItemPoints: [number, number, number][] = [
[1, 0, 4], // .boo
[2, 1, 19], // margin: 20px 10px;
[2, 9, 18], // 20px 10px
[2, 9, 13], // 20px
[2, 14, 18], // 10px
[3, 1, 36], // background-image: url('tryme.png');
[3, 19, 35], // url('tryme.png')
[5, 1, 10], // .boo .hoo
[6, 2, 15], // margin: 10px;
[6, 10, 14], // 10px
];
expectedNextItemPoints.forEach(([line, colstart, colend]) => {
fetchSelectItem('next');
testSelection(editor.selection, colstart, line, colend);
});
editor.selections = [new Selection(8, 0, 8, 0)];
expectedNextItemPoints.reverse().forEach(([line, colstart, colend]) => {
fetchSelectItem('prev');
testSelection(editor.selection, colstart, line, colend);
});
return Promise.resolve();
});
});
test('Emmet Balance Out in html file', function (): any {
return withRandomFileEditor(htmlContents, 'html', (editor, _) => {
editor.selections = [new Selection(14, 6, 14, 10)];
let expectedBalanceOutRanges: [number, number, number, number][] = [
[14, 3, 14, 32], // <li class="item1">Item 1</li>
[13, 23, 16, 2], // inner contents of <ul class="nav main">
[13, 2, 16, 7], // outer contents of <ul class="nav main">
[12, 21, 17, 1], // inner contents of <div class="header">
[12, 1, 17, 7], // outer contents of <div class="header">
[8, 6, 18, 0], // inner contents of <body>
[8, 0, 18, 7], // outer contents of <body>
[2, 16, 19, 0], // inner contents of <html>
[2, 0, 19, 7], // outer contents of <html>
];
expectedBalanceOutRanges.forEach(([linestart, colstart, lineend, colend]) => {
balanceOut();
testSelection(editor.selection, colstart, linestart, colend, lineend);
});
editor.selections = [new Selection(12, 7, 12, 7)];
let expectedBalanceInRanges: [number, number, number, number][] = [
[12, 21, 17, 1], // inner contents of <div class="header">
[13, 2, 16, 7], // outer contents of <ul class="nav main">
[13, 23, 16, 2], // inner contents of <ul class="nav main">
[14, 3, 14, 32], // <li class="item1">Item 1</li>
[14, 21, 14, 27] // Item 1
];
expectedBalanceInRanges.forEach(([linestart, colstart, lineend, colend]) => {
balanceIn();
testSelection(editor.selection, colstart, linestart, colend, lineend);
});
return Promise.resolve();
});
});
test('Emmet Balance In using the same stack as Balance out in html file', function (): any {
return withRandomFileEditor(htmlContents, 'html', (editor, _) => {
editor.selections = [new Selection(15, 6, 15, 10)];
let expectedBalanceOutRanges: [number, number, number, number][] = [
[15, 3, 15, 32], // <li class="item1">Item 2</li>
[13, 23, 16, 2], // inner contents of <ul class="nav main">
[13, 2, 16, 7], // outer contents of <ul class="nav main">
[12, 21, 17, 1], // inner contents of <div class="header">
[12, 1, 17, 7], // outer contents of <div class="header">
[8, 6, 18, 0], // inner contents of <body>
[8, 0, 18, 7], // outer contents of <body>
[2, 16, 19, 0], // inner contents of <html>
[2, 0, 19, 7], // outer contents of <html>
];
expectedBalanceOutRanges.forEach(([linestart, colstart, lineend, colend]) => {
balanceOut();
testSelection(editor.selection, colstart, linestart, colend, lineend);
});
expectedBalanceOutRanges.reverse().forEach(([linestart, colstart, lineend, colend]) => {
testSelection(editor.selection, colstart, linestart, colend, lineend);
balanceIn();
});
return Promise.resolve();
});
});
test('Emmet Balance In when selection doesnt span entire node or its inner contents', function (): any {
return withRandomFileEditor(htmlContents, 'html', (editor, _) => {
editor.selection = new Selection(13, 7, 13, 10); // Inside the open tag of <ul class="nav main">
balanceIn();
testSelection(editor.selection, 23, 13, 2, 16); // inner contents of <ul class="nav main">
editor.selection = new Selection(16, 4, 16, 5); // Inside the open close of <ul class="nav main">
balanceIn();
testSelection(editor.selection, 23, 13, 2, 16); // inner contents of <ul class="nav main">
editor.selection = new Selection(13, 7, 14, 2); // Inside the open tag of <ul class="nav main"> and the next line
balanceIn();
testSelection(editor.selection, 23, 13, 2, 16); // inner contents of <ul class="nav main">
return Promise.resolve();
});
});
test('Emmet Balance In/Out in html template', function (): any {
const htmlTemplate = `
<script type="text/html">
<div class="header">
<ul class="nav main">
<li class="item1">Item 1</li>
<li class="item2">Item 2</li>
</ul>
</div>
</script>`;
return withRandomFileEditor(htmlTemplate, 'html', (editor, _) => {
editor.selections = [new Selection(5, 24, 5, 24)];
let expectedBalanceOutRanges: [number, number, number, number][] = [
[5, 20, 5, 26], // <li class="item1">``Item 2''</li>
[5, 2, 5, 31], // ``<li class="item1">Item 2</li>''
[3, 22, 6, 1], // inner contents of ul
[3, 1, 6, 6], // outer contents of ul
[2, 20, 7, 0], // inner contents of div
[2, 0, 7, 6], // outer contents of div
];
expectedBalanceOutRanges.forEach(([linestart, colstart, lineend, colend]) => {
balanceOut();
testSelection(editor.selection, colstart, linestart, colend, lineend);
});
expectedBalanceOutRanges.pop();
expectedBalanceOutRanges.reverse().forEach(([linestart, colstart, lineend, colend]) => {
balanceIn();
testSelection(editor.selection, colstart, linestart, colend, lineend);
});
return Promise.resolve();
});
});
});
function testSelection(selection: Selection, startChar: number, startline: number, endChar?: number, endLine?: number) {
assert.equal(selection.anchor.line, startline);
assert.equal(selection.anchor.character, startChar);
if (!endLine && endLine !== 0) {
assert.equal(selection.isSingleLine, true);
} else {
assert.equal(selection.active.line, endLine);
}
if (!endChar && endChar !== 0) {
assert.equal(selection.isEmpty, true);
} else {
assert.equal(selection.active.character, endChar);
}
}

View File

@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { Selection } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { incrementDecrement as incrementDecrementImpl } from '../incrementDecrement';
function incrementDecrement(delta: number): Thenable<boolean> {
const result = incrementDecrementImpl(delta);
assert.ok(result);
return result!;
}
suite('Tests for Increment/Decrement Emmet Commands', () => {
teardown(closeAllEditors);
const contents = `
hello 123.43 there
hello 999.9 there
hello 100 there
`;
test('incrementNumberByOne', function (): any {
return withRandomFileEditor(contents, 'txt', async (editor, doc) => {
editor.selections = [new Selection(1, 7, 1, 10), new Selection(2, 7, 2, 10)];
await incrementDecrement(1);
assert.equal(doc.getText(), contents.replace('123', '124').replace('999', '1000'));
return Promise.resolve();
});
});
test('incrementNumberByTen', function (): any {
return withRandomFileEditor(contents, 'txt', async (editor, doc) => {
editor.selections = [new Selection(1, 7, 1, 10), new Selection(2, 7, 2, 10)];
await incrementDecrement(10);
assert.equal(doc.getText(), contents.replace('123', '133').replace('999', '1009'));
return Promise.resolve();
});
});
test('incrementNumberByOneTenth', function (): any {
return withRandomFileEditor(contents, 'txt', async (editor, doc) => {
editor.selections = [new Selection(1, 7, 1, 13), new Selection(2, 7, 2, 12)];
await incrementDecrement(0.1);
assert.equal(doc.getText(), contents.replace('123.43', '123.53').replace('999.9', '1000'));
return Promise.resolve();
});
});
test('decrementNumberByOne', function (): any {
return withRandomFileEditor(contents, 'txt', async (editor, doc) => {
editor.selections = [new Selection(1, 7, 1, 10), new Selection(3, 7, 3, 10)];
await incrementDecrement(-1);
assert.equal(doc.getText(), contents.replace('123', '122').replace('100', '99'));
return Promise.resolve();
});
});
test('decrementNumberByTen', function (): any {
return withRandomFileEditor(contents, 'txt', async (editor, doc) => {
editor.selections = [new Selection(1, 7, 1, 10), new Selection(3, 7, 3, 10)];
await incrementDecrement(-10);
assert.equal(doc.getText(), contents.replace('123', '113').replace('100', '90'));
return Promise.resolve();
});
});
test('decrementNumberByOneTenth', function (): any {
return withRandomFileEditor(contents, 'txt', async (editor, doc) => {
editor.selections = [new Selection(1, 7, 1, 13), new Selection(3, 7, 3, 10)];
await incrementDecrement(-0.1);
assert.equal(doc.getText(), contents.replace('123.43', '123.33').replace('100', '99.9'));
return Promise.resolve();
});
});
});

View File

@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const testRunner = require('vscode/lib/testrunner');
const options: any = {
ui: 'tdd',
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'),
timeout: 60000
};
// These integration tests is being run in multiple environments (electron, web, remote)
// so we need to set the suite name based on the environment as the suite name is used
// for the test results file name
let suite = '';
if (process.env.VSCODE_BROWSER) {
suite = `${process.env.VSCODE_BROWSER} Browser Integration Emmet Tests`;
} else if (process.env.REMOTE_VSCODE) {
suite = 'Remote Integration Emmet Tests';
} else {
suite = 'Integration Emmet Tests';
}
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(options);
export = testRunner;

View File

@ -0,0 +1,260 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { withRandomFileEditor } from './testUtils';
import * as vscode from 'vscode';
import { parsePartialStylesheet, getNode } from '../util';
import { isValidLocationForEmmetAbbreviation } from '../abbreviationActions';
suite('Tests for partial parse of Stylesheets', () => {
function isValid(doc: vscode.TextDocument, range: vscode.Range, syntax: string): boolean {
const rootNode = parsePartialStylesheet(doc, range.end);
const currentNode = getNode(rootNode, range.end, true);
return isValidLocationForEmmetAbbreviation(doc, rootNode, currentNode, syntax, range.end, range);
}
test('Ignore block comment inside rule', function (): any {
const cssContents = `
p {
margin: p ;
/*dn: none; p */ p
p
p.
} p
`;
return withRandomFileEditor(cssContents, '.css', (_, doc) => {
let rangesForEmmet = [
new vscode.Range(3, 18, 3, 19), // Same line after block comment
new vscode.Range(4, 1, 4, 2), // p after block comment
new vscode.Range(5, 1, 5, 3) // p. after block comment
];
let rangesNotEmmet = [
new vscode.Range(1, 0, 1, 1), // Selector
new vscode.Range(2, 9, 2, 10), // Property value
new vscode.Range(3, 3, 3, 5), // dn inside block comment
new vscode.Range(3, 13, 3, 14), // p just before ending of block comment
new vscode.Range(6, 2, 6, 3) // p after ending of block
];
rangesForEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'css'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'css'), false);
});
return Promise.resolve();
});
});
test('Ignore commented braces', function (): any {
const sassContents = `
.foo
// .foo { brs
/* .foo { op.3
dn {
*/
@
} bg
`;
return withRandomFileEditor(sassContents, '.scss', (_, doc) => {
let rangesNotEmmet = [
new vscode.Range(1, 0, 1, 4), // Selector
new vscode.Range(2, 3, 2, 7), // Line commented selector
new vscode.Range(3, 3, 3, 7), // Block commented selector
new vscode.Range(4, 0, 4, 2), // dn inside block comment
new vscode.Range(6, 1, 6, 2), // @ inside a rule whose opening brace is commented
new vscode.Range(7, 2, 7, 4) // bg after ending of badly constructed block
];
rangesNotEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
});
test('Block comment between selector and open brace', function (): any {
const cssContents = `
p
/* First line
of a multiline
comment */
{
margin: p ;
/*dn: none; p */ p
p
p.
} p
`;
return withRandomFileEditor(cssContents, '.css', (_, doc) => {
let rangesForEmmet = [
new vscode.Range(7, 18, 7, 19), // Same line after block comment
new vscode.Range(8, 1, 8, 2), // p after block comment
new vscode.Range(9, 1, 9, 3) // p. after block comment
];
let rangesNotEmmet = [
new vscode.Range(1, 2, 1, 3), // Selector
new vscode.Range(3, 3, 3, 4), // Inside multiline comment
new vscode.Range(5, 0, 5, 1), // Opening Brace
new vscode.Range(6, 9, 6, 10), // Property value
new vscode.Range(7, 3, 7, 5), // dn inside block comment
new vscode.Range(7, 13, 7, 14), // p just before ending of block comment
new vscode.Range(10, 2, 10, 3) // p after ending of block
];
rangesForEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'css'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'css'), false);
});
return Promise.resolve();
});
});
test('Nested and consecutive rulesets with errors', function (): any {
const sassContents = `
.foo{
a
a
}}{ p
}
.bar{
@
.rudi {
@
}
}}}
`;
return withRandomFileEditor(sassContents, '.scss', (_, doc) => {
let rangesForEmmet = [
new vscode.Range(2, 1, 2, 2), // Inside a ruleset before errors
new vscode.Range(3, 1, 3, 2), // Inside a ruleset after no serious error
new vscode.Range(7, 1, 7, 2), // @ inside a so far well structured ruleset
new vscode.Range(9, 2, 9, 3), // @ inside a so far well structured nested ruleset
];
let rangesNotEmmet = [
new vscode.Range(4, 4, 4, 5), // p inside ruleset without proper selector
new vscode.Range(6, 3, 6, 4) // In selector
];
rangesForEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
});
test('One liner sass', function (): any {
const sassContents = `
.foo{dn}.bar{.boo{dn}dn}.comd{/*{dn*/p{div{dn}} }.foo{.other{dn}} dn
`;
return withRandomFileEditor(sassContents, '.scss', (_, doc) => {
let rangesForEmmet = [
new vscode.Range(1, 5, 1, 7), // Inside a ruleset
new vscode.Range(1, 18, 1, 20), // Inside a nested ruleset
new vscode.Range(1, 21, 1, 23), // Inside ruleset after nested one.
new vscode.Range(1, 43, 1, 45), // Inside nested ruleset after comment
new vscode.Range(1, 61, 1, 63) // Inside nested ruleset
];
let rangesNotEmmet = [
new vscode.Range(1, 3, 1, 4), // In foo selector
new vscode.Range(1, 10, 1, 11), // In bar selector
new vscode.Range(1, 15, 1, 16), // In boo selector
new vscode.Range(1, 28, 1, 29), // In comd selector
new vscode.Range(1, 33, 1, 34), // In commented dn
new vscode.Range(1, 37, 1, 38), // In p selector
new vscode.Range(1, 39, 1, 42), // In div selector
new vscode.Range(1, 66, 1, 68) // Outside any ruleset
];
rangesForEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
});
test('Variables and interpolation', function (): any {
const sassContents = `
p.#{dn} {
p.3
#{$attr}-color: blue;
dn
} op
.foo{nes{ted}} {
dn
}
`;
return withRandomFileEditor(sassContents, '.scss', (_, doc) => {
let rangesForEmmet = [
new vscode.Range(2, 1, 2, 4), // p.3 inside a ruleset whose selector uses interpolation
new vscode.Range(4, 1, 4, 3) // dn inside ruleset after property with variable
];
let rangesNotEmmet = [
new vscode.Range(1, 0, 1, 1), // In p in selector
new vscode.Range(1, 2, 1, 3), // In # in selector
new vscode.Range(1, 4, 1, 6), // In dn inside variable in selector
new vscode.Range(3, 7, 3, 8), // r of attr inside variable
new vscode.Range(5, 2, 5, 4), // op after ruleset
new vscode.Range(7, 1, 7, 3), // dn inside ruleset whose selector uses nested interpolation
new vscode.Range(3, 1, 3, 2), // # inside ruleset
];
rangesForEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
});
test('Comments in sass', function (): any {
const sassContents = `
.foo{
/* p // p */ brs6-2p
dn
}
p
/* c
om
ment */{
m10
}
.boo{
op.3
}
`;
return withRandomFileEditor(sassContents, '.scss', (_, doc) => {
let rangesForEmmet = [
new vscode.Range(2, 14, 2, 21), // brs6-2p with a block commented line comment ('/* */' overrides '//')
new vscode.Range(3, 1, 3, 3), // dn after a line with combined comments inside a ruleset
new vscode.Range(9, 1, 9, 4), // m10 inside ruleset whose selector is before a comment
new vscode.Range(12, 1, 12, 5) // op3 inside a ruleset with commented extra braces
];
let rangesNotEmmet = [
new vscode.Range(2, 4, 2, 5), // In p inside block comment
new vscode.Range(2, 9, 2, 10), // In p inside block comment and after line comment
new vscode.Range(6, 3, 6, 4) // In c inside block comment
];
rangesForEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), true);
});
rangesNotEmmet.forEach(range => {
assert.equal(isValid(doc, range, 'scss'), false);
});
return Promise.resolve();
});
});
});

View File

@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { Selection } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { reflectCssValue as reflectCssValueImpl } from '../reflectCssValue';
function reflectCssValue(): Thenable<boolean> {
const result = reflectCssValueImpl();
assert.ok(result);
return result!;
}
suite('Tests for Emmet: Reflect CSS Value command', () => {
teardown(closeAllEditors);
const cssContents = `
.header {
margin: 10px;
padding: 10px;
transform: rotate(50deg);
-moz-transform: rotate(20deg);
-o-transform: rotate(50deg);
-webkit-transform: rotate(50deg);
-ms-transform: rotate(50deg);
}
`;
const htmlContents = `
<html>
<style>
.header {
margin: 10px;
padding: 10px;
transform: rotate(50deg);
-moz-transform: rotate(20deg);
-o-transform: rotate(50deg);
-webkit-transform: rotate(50deg);
-ms-transform: rotate(50deg);
}
</style>
</html>
`;
test('Reflect Css Value in css file', function (): any {
return withRandomFileEditor(cssContents, '.css', (editor, doc) => {
editor.selections = [new Selection(5, 10, 5, 10)];
return reflectCssValue().then(() => {
assert.equal(doc.getText(), cssContents.replace(/\(50deg\)/g, '(20deg)'));
return Promise.resolve();
});
});
});
test('Reflect Css Value in css file, selecting entire property', function (): any {
return withRandomFileEditor(cssContents, '.css', (editor, doc) => {
editor.selections = [new Selection(5, 2, 5, 32)];
return reflectCssValue().then(() => {
assert.equal(doc.getText(), cssContents.replace(/\(50deg\)/g, '(20deg)'));
return Promise.resolve();
});
});
});
test('Reflect Css Value in html file', function (): any {
return withRandomFileEditor(htmlContents, '.html', (editor, doc) => {
editor.selections = [new Selection(7, 20, 7, 20)];
return reflectCssValue().then(() => {
assert.equal(doc.getText(), htmlContents.replace(/\(50deg\)/g, '(20deg)'));
return Promise.resolve();
});
});
});
test('Reflect Css Value in html file, selecting entire property', function (): any {
return withRandomFileEditor(htmlContents, '.html', (editor, doc) => {
editor.selections = [new Selection(7, 4, 7, 34)];
return reflectCssValue().then(() => {
assert.equal(doc.getText(), htmlContents.replace(/\(50deg\)/g, '(20deg)'));
return Promise.resolve();
});
});
});
});

View File

@ -0,0 +1,404 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { Selection, workspace, ConfigurationTarget } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { removeTag } from '../removeTag';
import { updateTag } from '../updateTag';
import { matchTag } from '../matchTag';
import { splitJoinTag } from '../splitJoinTag';
import { mergeLines } from '../mergeLines';
suite('Tests for Emmet actions on html tags', () => {
teardown(() => {
// close all editors
return closeAllEditors;
});
const contents = `
<div class="hello">
<ul>
<li><span>Hello</span></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
</ul>
<span/>
</div>
`;
let contentsWithTemplate = `
<script type="text/template">
<ul>
<li><span>Hello</span></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
</ul>
<span/>
</script>
`;
test('update tag with multiple cursors', () => {
const expectedContents = `
<div class="hello">
<ul>
<li><section>Hello</section></li>
<section><span>There</span></section>
<section><li><span>Bye</span></li></section>
</ul>
<span/>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 17, 3, 17), // cursor inside tags
new Selection(4, 5, 4, 5), // cursor inside opening tag
new Selection(5, 35, 5, 35), // cursor inside closing tag
];
return updateTag('section')!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
// #region update tag
test('update tag with entire node selected', () => {
const expectedContents = `
<div class="hello">
<ul>
<li><section>Hello</section></li>
<li><span>There</span></li>
<section><li><span>Bye</span></li></section>
</ul>
<span/>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 7, 3, 25),
new Selection(5, 3, 5, 39),
];
return updateTag('section')!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('update tag with template', () => {
const expectedContents = `
<script type="text/template">
<section>
<li><span>Hello</span></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
</section>
<span/>
</script>
`;
return withRandomFileEditor(contentsWithTemplate, 'html', (editor, doc) => {
editor.selections = [
new Selection(2, 4, 2, 4), // cursor inside ul tag
];
return updateTag('section')!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
// #endregion
// #region remove tag
test('remove tag with mutliple cursors', () => {
const expectedContents = `
<div class="hello">
<ul>
<li>Hello</li>
<span>There</span>
<li><span>Bye</span></li>
</ul>
<span/>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 17, 3, 17), // cursor inside tags
new Selection(4, 5, 4, 5), // cursor inside opening tag
new Selection(5, 35, 5, 35), // cursor inside closing tag
];
return removeTag()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('remove tag with boundary conditions', () => {
const expectedContents = `
<div class="hello">
<ul>
<li>Hello</li>
<li><span>There</span></li>
<li><span>Bye</span></li>
</ul>
<span/>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 7, 3, 25),
new Selection(5, 3, 5, 39),
];
return removeTag()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('remove tag with template', () => {
const expectedContents = `
<script type="text/template">
\t\t
<li><span>Hello</span></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
\t
<span/>
</script>
`;
return withRandomFileEditor(contentsWithTemplate, 'html', (editor, doc) => {
editor.selections = [
new Selection(2, 4, 2, 4), // cursor inside ul tag
];
return removeTag()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
// #endregion
// #region split/join tag
test('split/join tag with mutliple cursors', () => {
const expectedContents = `
<div class="hello">
<ul>
<li><span/></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
</ul>
<span></span>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 17, 3, 17), // join tag
new Selection(7, 5, 7, 5), // split tag
];
return splitJoinTag()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('split/join tag with boundary selection', () => {
const expectedContents = `
<div class="hello">
<ul>
<li><span/></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
</ul>
<span></span>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 7, 3, 25), // join tag
new Selection(7, 2, 7, 9), // split tag
];
return splitJoinTag()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('split/join tag with templates', () => {
const expectedContents = `
<script type="text/template">
<ul>
<li><span/></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
</ul>
<span></span>
</script>
`;
return withRandomFileEditor(contentsWithTemplate, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 17, 3, 17), // join tag
new Selection(7, 5, 7, 5), // split tag
];
return splitJoinTag()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('split/join tag in jsx with xhtml self closing tag', () => {
const expectedContents = `
<div class="hello">
<ul>
<li><span /></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
</ul>
<span></span>
</div>
`;
const oldValueForSyntaxProfiles = workspace.getConfiguration('emmet').inspect('syntaxProfiles');
return workspace.getConfiguration('emmet').update('syntaxProfiles', { jsx: { selfClosingStyle: 'xhtml' } }, ConfigurationTarget.Global).then(() => {
return withRandomFileEditor(contents, 'jsx', (editor, doc) => {
editor.selections = [
new Selection(3, 17, 3, 17), // join tag
new Selection(7, 5, 7, 5), // split tag
];
return splitJoinTag()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return workspace.getConfiguration('emmet').update('syntaxProfiles', oldValueForSyntaxProfiles ? oldValueForSyntaxProfiles.globalValue : undefined, ConfigurationTarget.Global);
});
});
});
});
// #endregion
// #region match tag
test('match tag with mutliple cursors', () => {
return withRandomFileEditor(contents, 'html', (editor, _) => {
editor.selections = [
new Selection(1, 0, 1, 0), // just before tag starts, i.e before <
new Selection(1, 1, 1, 1), // just before tag name starts
new Selection(1, 2, 1, 2), // inside tag name
new Selection(1, 6, 1, 6), // after tag name but before opening tag ends
new Selection(1, 18, 1, 18), // just before opening tag ends
new Selection(1, 19, 1, 19), // just after opening tag ends
];
matchTag();
editor.selections.forEach(selection => {
assert.equal(selection.active.line, 8);
assert.equal(selection.active.character, 3);
assert.equal(selection.anchor.line, 8);
assert.equal(selection.anchor.character, 3);
});
return Promise.resolve();
});
});
test('match tag with template scripts', () => {
let templateScript = `
<script type="text/template">
<div>
Hello
</div>
</script>`;
return withRandomFileEditor(templateScript, 'html', (editor, _) => {
editor.selections = [
new Selection(2, 2, 2, 2), // just before div tag starts, i.e before <
];
matchTag();
editor.selections.forEach(selection => {
assert.equal(selection.active.line, 4);
assert.equal(selection.active.character, 4);
assert.equal(selection.anchor.line, 4);
assert.equal(selection.anchor.character, 4);
});
return Promise.resolve();
});
});
// #endregion
// #region merge lines
test('merge lines of tag with children when empty selection', () => {
const expectedContents = `
<div class="hello">
<ul><li><span>Hello</span></li><li><span>There</span></li><div><li><span>Bye</span></li></div></ul>
<span/>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(2, 3, 2, 3)
];
return mergeLines()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('merge lines of tag with children when full node selection', () => {
const expectedContents = `
<div class="hello">
<ul><li><span>Hello</span></li><li><span>There</span></li><div><li><span>Bye</span></li></div></ul>
<span/>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(2, 3, 6, 7)
];
return mergeLines()!.then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('merge lines is no-op when start and end nodes are on the same line', () => {
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 9, 3, 9), // cursor is inside the <span> in <li><span>Hello</span></li>
new Selection(4, 5, 4, 5), // cursor is inside the <li> in <li><span>Hello</span></li>
new Selection(5, 5, 5, 20) // selection spans multiple nodes in the same line
];
return mergeLines()!.then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
// #endregion
});

View File

@ -0,0 +1 @@
DO NOT DELETE, USED BY INTEGRATION TESTS

View File

@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* 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 * as fs from 'fs';
import * as os from 'os';
import { join } from 'path';
function rndName() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
}
export function createRandomFile(contents = '', fileExtension = 'txt'): Thenable<vscode.Uri> {
return new Promise((resolve, reject) => {
const tmpFile = join(os.tmpdir(), rndName() + '.' + fileExtension);
fs.writeFile(tmpFile, contents, (error) => {
if (error) {
return reject(error);
}
resolve(vscode.Uri.file(tmpFile));
});
});
}
export function pathEquals(path1: string, path2: string): boolean {
if (process.platform !== 'linux') {
path1 = path1.toLowerCase();
path2 = path2.toLowerCase();
}
return path1 === path2;
}
export function deleteFile(file: vscode.Uri): Thenable<boolean> {
return new Promise((resolve, reject) => {
fs.unlink(file.fsPath, (err) => {
if (err) {
reject(err);
} else {
resolve(true);
}
});
});
}
export function closeAllEditors(): Thenable<any> {
return vscode.commands.executeCommand('workbench.action.closeAllEditors');
}
export function withRandomFileEditor(initialContents: string, fileExtension: string = 'txt', run: (editor: vscode.TextEditor, doc: vscode.TextDocument) => Thenable<void>): Thenable<boolean> {
return createRandomFile(initialContents, fileExtension).then(file => {
return vscode.workspace.openTextDocument(file).then(doc => {
return vscode.window.showTextDocument(doc).then((editor) => {
return run(editor, doc).then(_ => {
if (doc.isDirty) {
return doc.save().then(() => {
return deleteFile(file);
});
} else {
return deleteFile(file);
}
});
});
});
});
}

View File

@ -0,0 +1,729 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { Selection } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { toggleComment as toggleCommentImpl } from '../toggleComment';
function toggleComment(): Thenable<boolean> {
const result = toggleCommentImpl();
assert.ok(result);
return result!;
}
suite('Tests for Toggle Comment action from Emmet (HTML)', () => {
teardown(closeAllEditors);
const contents = `
<div class="hello">
<ul>
<li><span>Hello</span></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
</ul>
<ul>
<!--<li>Previously Commented Node</li>-->
<li>Another Node</li>
</ul>
<span/>
<style>
.boo {
margin: 10px;
padding: 20px;
}
.hoo {
margin: 10px;
padding: 20px;
}
</style>
</div>
`;
test('toggle comment with multiple cursors, but no selection (HTML)', () => {
const expectedContents = `
<div class="hello">
<ul>
<li><!--<span>Hello</span>--></li>
<!--<li><span>There</span></li>-->
<!--<div><li><span>Bye</span></li></div>-->
</ul>
<!--<ul>
<li>Previously Commented Node</li>
<li>Another Node</li>
</ul>-->
<span/>
<style>
.boo {
/*margin: 10px;*/
padding: 20px;
}
/*.hoo {
margin: 10px;
padding: 20px;
}*/
</style>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 17, 3, 17), // cursor inside the inner span element
new Selection(4, 5, 4, 5), // cursor inside opening tag
new Selection(5, 35, 5, 35), // cursor inside closing tag
new Selection(7, 3, 7, 3), // cursor inside open tag of <ul> one of whose children is already commented
new Selection(14, 8, 14, 8), // cursor inside the css property inside the style tag
new Selection(18, 3, 18, 3) // cursor inside the css rule inside the style tag
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('toggle comment with multiple cursors and whole node selected (HTML)', () => {
const expectedContents = `
<div class="hello">
<ul>
<li><!--<span>Hello</span>--></li>
<!--<li><span>There</span></li>-->
<div><li><span>Bye</span></li></div>
</ul>
<!--<ul>
<li>Previously Commented Node</li>
<li>Another Node</li>
</ul>-->
<span/>
<style>
.boo {
/*margin: 10px;*/
padding: 20px;
}
/*.hoo {
margin: 10px;
padding: 20px;
}*/
</style>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 7, 3, 25), // <span>Hello</span><
new Selection(4, 3, 4, 30), // <li><span>There</span></li>
new Selection(7, 2, 10, 7), // The <ul> one of whose children is already commented
new Selection(14, 4, 14, 17), // css property inside the style tag
new Selection(17, 3, 20, 4) // the css rule inside the style tag
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('toggle comment when multiple nodes are completely under single selection (HTML)', () => {
const expectedContents = `
<div class="hello">
<ul>
<!--<li><span>Hello</span></li>
<li><span>There</span></li>-->
<div><li><span>Bye</span></li></div>
</ul>
<ul>
<!--<li>Previously Commented Node</li>-->
<li>Another Node</li>
</ul>
<span/>
<style>
.boo {
/*margin: 10px;
padding: 20px;*/
}
.hoo {
margin: 10px;
padding: 20px;
}
</style>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 4, 4, 30),
new Selection(14, 4, 15, 18) // 2 css properties inside the style tag
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('toggle comment when multiple nodes are partially under single selection (HTML)', () => {
const expectedContents = `
<div class="hello">
<ul>
<!--<li><span>Hello</span></li>
<li><span>There</span></li>-->
<div><li><span>Bye</span></li></div>
</ul>
<!--<ul>
<li>Previously Commented Node</li>
<li>Another Node</li>
</ul>-->
<span/>
<style>
.boo {
margin: 10px;
padding: 20px;
}
.hoo {
margin: 10px;
padding: 20px;
}
</style>
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 24, 4, 20),
new Selection(7, 2, 9, 10) // The <ul> one of whose children is already commented
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('toggle comment with multiple cursors selecting parent and child nodes', () => {
const expectedContents = `
<div class="hello">
<ul>
<li><!--<span>Hello</span>--></li>
<!--<li><span>There</span></li>-->
<div><li><span>Bye</span></li></div>
</ul>
<!--<ul>
<li>Previously Commented Node</li>
<li>Another Node</li>
</ul>-->
<span/>
<!--<style>
.boo {
margin: 10px;
padding: 20px;
}
.hoo {
margin: 10px;
padding: 20px;
}
</style>-->
</div>
`;
return withRandomFileEditor(contents, 'html', (editor, doc) => {
editor.selections = [
new Selection(3, 17, 3, 17), // cursor inside the inner span element
new Selection(4, 5, 4, 5), // two cursors: one inside opening tag
new Selection(4, 17, 4, 17), // and the second inside the inner span element
new Selection(7, 3, 7, 3), // two cursors: one inside open tag of <ul> one of whose children is already commented
new Selection(9, 10, 9, 10), // and the second inside inner li element, whose parent is selected
new Selection(12, 3, 12, 3), // four nested cursors: one inside the style open tag
new Selection(14, 8, 14, 8), // the second inside the css property inside the style tag
new Selection(18, 3, 18, 3), // the third inside the css rule inside the style tag
new Selection(19, 8, 19, 8) // and the fourth inside the css property inside the style tag
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
test('toggle comment within script template', () => {
const templateContents = `
<script type="text/template">
<li><span>Hello</span></li>
<li><!--<span>There</span>--></li>
<div><li><span>Bye</span></li></div>
<span/>
</script>
`;
const expectedContents = `
<script type="text/template">
<!--<li><span>Hello</span></li>-->
<li><span>There</span></li>
<div><li><!--<span>Bye</span>--></li></div>
<span/>
</script>
`;
return withRandomFileEditor(templateContents, 'html', (editor, doc) => {
editor.selections = [
new Selection(2, 2, 2, 28), // select entire li element
new Selection(3, 17, 3, 17), // cursor inside the commented span
new Selection(4, 18, 4, 18), // cursor inside the noncommented span
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return Promise.resolve();
});
});
});
});
suite('Tests for Toggle Comment action from Emmet (CSS)', () => {
teardown(closeAllEditors);
const contents = `
.one {
margin: 10px;
padding: 10px;
}
.two {
height: 42px;
display: none;
}
.three {
width: 42px;
}`;
test('toggle comment with multiple cursors, but no selection (CSS)', () => {
const expectedContents = `
.one {
/*margin: 10px;*/
padding: 10px;
}
/*.two {
height: 42px;
display: none;
}*/
.three {
width: 42px;
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 5, 2, 5), // cursor inside a property
new Selection(5, 4, 5, 4), // cursor inside selector
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment with multiple cursors and whole node selected (CSS)', () => {
const expectedContents = `
.one {
/*margin: 10px;*/
/*padding: 10px;*/
}
/*.two {
height: 42px;
display: none;
}*/
.three {
width: 42px;
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 2, 2, 15), // A property completely selected
new Selection(3, 0, 3, 16), // A property completely selected along with whitespace
new Selection(5, 1, 8, 2), // A rule completely selected
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
//return toggleComment().then(() => {
//assert.equal(doc.getText(), contents);
return Promise.resolve();
//});
});
});
});
test('toggle comment when multiple nodes of same parent are completely under single selection (CSS)', () => {
const expectedContents = `
.one {
/* margin: 10px;
padding: 10px;*/
}
/*.two {
height: 42px;
display: none;
}
.three {
width: 42px;
}*/`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 0, 3, 16), // 2 properties completely under a single selection along with whitespace
new Selection(5, 1, 11, 2), // 2 rules completely under a single selection
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment when start and end of selection is inside properties of separate rules (CSS)', () => {
const expectedContents = `
.one {
margin: 10px;
/*padding: 10px;
}
.two {
height: 42px;*/
display: none;
}
.three {
width: 42px;
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(3, 7, 6, 6)
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment when selection spans properties of separate rules, with start in whitespace and end inside the property (CSS)', () => {
const expectedContents = `
.one {
margin: 10px;
/*padding: 10px;
}
.two {
height: 42px;*/
display: none;
}
.three {
width: 42px;
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(3, 0, 6, 6)
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment when selection spans properties of separate rules, with end in whitespace and start inside the property (CSS)', () => {
const expectedContents = `
.one {
margin: 10px;
/*padding: 10px;
}
.two {
height: 42px;*/
display: none;
}
.three {
width: 42px;
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(3, 7, 7, 0)
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment when selection spans properties of separate rules, with both start and end in whitespace (CSS)', () => {
const expectedContents = `
.one {
margin: 10px;
/*padding: 10px;
}
.two {
height: 42px;*/
display: none;
}
.three {
width: 42px;
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(3, 0, 7, 0)
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment when multiple nodes of same parent are partially under single selection (CSS)', () => {
const expectedContents = `
.one {
/*margin: 10px;
padding: 10px;*/
}
/*.two {
height: 42px;
display: none;
}
.three {
width: 42px;
*/ }`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 7, 3, 10), // 2 properties partially under a single selection
new Selection(5, 2, 11, 0), // 2 rules partially under a single selection
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
});
suite('Tests for Toggle Comment action from Emmet in nested css (SCSS)', () => {
teardown(closeAllEditors);
const contents = `
.one {
height: 42px;
.two {
width: 42px;
}
.three {
padding: 10px;
}
}`;
test('toggle comment with multiple cursors selecting nested nodes (SCSS)', () => {
const expectedContents = `
.one {
/*height: 42px;*/
/*.two {
width: 42px;
}*/
.three {
/*padding: 10px;*/
}
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 5, 2, 5), // cursor inside a property
new Selection(4, 4, 4, 4), // two cursors: one inside a nested rule
new Selection(5, 5, 5, 5), // and the second one inside a nested property
new Selection(9, 5, 9, 5) // cursor inside a property inside a nested rule
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment with multiple cursors selecting several nested nodes (SCSS)', () => {
const expectedContents = `
/*.one {
height: 42px;
.two {
width: 42px;
}
.three {
padding: 10px;
}
}*/`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(1, 3, 1, 3), // cursor in the outside rule. And several cursors inside:
new Selection(2, 5, 2, 5), // cursor inside a property
new Selection(4, 4, 4, 4), // two cursors: one inside a nested rule
new Selection(5, 5, 5, 5), // and the second one inside a nested property
new Selection(9, 5, 9, 5) // cursor inside a property inside a nested rule
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment with multiple cursors, but no selection (SCSS)', () => {
const expectedContents = `
.one {
/*height: 42px;*/
/*.two {
width: 42px;
}*/
.three {
/*padding: 10px;*/
}
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 5, 2, 5), // cursor inside a property
new Selection(4, 4, 4, 4), // cursor inside a nested rule
new Selection(9, 5, 9, 5) // cursor inside a property inside a nested rule
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
//return toggleComment().then(() => {
// assert.equal(doc.getText(), contents);
return Promise.resolve();
//});
});
});
});
test('toggle comment with multiple cursors and whole node selected (CSS)', () => {
const expectedContents = `
.one {
/*height: 42px;*/
/*.two {
width: 42px;
}*/
.three {
/*padding: 10px;*/
}
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 2, 2, 15), // A property completely selected
new Selection(4, 2, 6, 3), // A rule completely selected
new Selection(9, 3, 9, 17) // A property inside a nested rule completely selected
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment when multiple nodes are completely under single selection (CSS)', () => {
const expectedContents = `
.one {
/*height: 42px;
.two {
width: 42px;
}*/
.three {
padding: 10px;
}
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 2, 6, 3), // A properties and a nested rule completely under a single selection
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
test('toggle comment when multiple nodes are partially under single selection (CSS)', () => {
const expectedContents = `
.one {
/*height: 42px;
.two {
width: 42px;
*/ }
.three {
padding: 10px;
}
}`;
return withRandomFileEditor(contents, 'css', (editor, doc) => {
editor.selections = [
new Selection(2, 6, 6, 1), // A properties and a nested rule partially under a single selection
];
return toggleComment().then(() => {
assert.equal(doc.getText(), expectedContents);
return toggleComment().then(() => {
assert.equal(doc.getText(), contents);
return Promise.resolve();
});
});
});
});
});

View File

@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// import 'mocha';
// import * as assert from 'assert';
// import { Selection } from 'vscode';
// import { withRandomFileEditor, closeAllEditors } from './testUtils';
// import { updateImageSize } from '../updateImageSize';
// suite('Tests for Emmet actions on html tags', () => {
// teardown(closeAllEditors);
// test('update image css with multiple cursors in css file', () => {
// const cssContents = `
// .one {
// margin: 10px;
// padding: 10px;
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// }
// .two {
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// height: 42px;
// }
// .three {
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// width: 42px;
// }
// `;
// const expectedContents = `
// .one {
// margin: 10px;
// padding: 10px;
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// width: 32px;
// height: 32px;
// }
// .two {
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// width: 32px;
// height: 32px;
// }
// .three {
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// height: 32px;
// width: 32px;
// }
// `;
// return withRandomFileEditor(cssContents, 'css', (editor, doc) => {
// editor.selections = [
// new Selection(4, 50, 4, 50),
// new Selection(7, 50, 7, 50),
// new Selection(11, 50, 11, 50)
// ];
// return updateImageSize()!.then(() => {
// assert.equal(doc.getText(), expectedContents);
// return Promise.resolve();
// });
// });
// });
// test('update image size in css in html file with multiple cursors', () => {
// const htmlWithCssContents = `
// <html>
// <style>
// .one {
// margin: 10px;
// padding: 10px;
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// }
// .two {
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// height: 42px;
// }
// .three {
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// width: 42px;
// }
// </style>
// </html>
// `;
// const expectedContents = `
// <html>
// <style>
// .one {
// margin: 10px;
// padding: 10px;
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// width: 32px;
// height: 32px;
// }
// .two {
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// width: 32px;
// height: 32px;
// }
// .three {
// background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png);
// height: 32px;
// width: 32px;
// }
// </style>
// </html>
// `;
// return withRandomFileEditor(htmlWithCssContents, 'html', (editor, doc) => {
// editor.selections = [
// new Selection(6, 50, 6, 50),
// new Selection(9, 50, 9, 50),
// new Selection(13, 50, 13, 50)
// ];
// return updateImageSize()!.then(() => {
// assert.equal(doc.getText(), expectedContents);
// return Promise.resolve();
// });
// });
// });
// test('update image size in img tag in html file with multiple cursors', () => {
// const htmlwithimgtag = `
// <html>
// <img id="one" src="https://github.com/microsoft/vscode/blob/master/resources/linux/code.png" />
// <img id="two" src="https://github.com/microsoft/vscode/blob/master/resources/linux/code.png" width="56" />
// <img id="three" src="https://github.com/microsoft/vscode/blob/master/resources/linux/code.png" height="56" />
// </html>
// `;
// const expectedContents = `
// <html>
// <img id="one" src="https://github.com/microsoft/vscode/blob/master/resources/linux/code.png" width="32" height="32" />
// <img id="two" src="https://github.com/microsoft/vscode/blob/master/resources/linux/code.png" width="32" height="32" />
// <img id="three" src="https://github.com/microsoft/vscode/blob/master/resources/linux/code.png" height="32" width="32" />
// </html>
// `;
// return withRandomFileEditor(htmlwithimgtag, 'html', (editor, doc) => {
// editor.selections = [
// new Selection(2, 50, 2, 50),
// new Selection(3, 50, 3, 50),
// new Selection(4, 50, 4, 50)
// ];
// return updateImageSize()!.then(() => {
// assert.equal(doc.getText(), expectedContents);
// return Promise.resolve();
// });
// });
// });
// });

View File

@ -0,0 +1,351 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { Selection, workspace, ConfigurationTarget } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { wrapWithAbbreviation, wrapIndividualLinesWithAbbreviation } from '../abbreviationActions';
const htmlContentsForWrapTests = `
<ul class="nav main">
<li class="item1">img</li>
<li class="item2">$hithere</li>
</ul>
`;
const wrapBlockElementExpected = `
<ul class="nav main">
<div>
<li class="item1">img</li>
</div>
<div>
<li class="item2">$hithere</li>
</div>
</ul>
`;
const wrapInlineElementExpected = `
<ul class="nav main">
<span><li class="item1">img</li></span>
<span><li class="item2">$hithere</li></span>
</ul>
`;
const wrapSnippetExpected = `
<ul class="nav main">
<a href=""><li class="item1">img</li></a>
<a href=""><li class="item2">$hithere</li></a>
</ul>
`;
const wrapMultiLineAbbrExpected = `
<ul class="nav main">
<ul>
<li>
<li class="item1">img</li>
</li>
</ul>
<ul>
<li>
<li class="item2">$hithere</li>
</li>
</ul>
</ul>
`;
const wrapInlineElementExpectedFormatFalse = `
<ul class="nav main">
<h1><li class="item1">img</li></h1>
<h1><li class="item2">$hithere</li></h1>
</ul>
`;
suite('Tests for Wrap with Abbreviations', () => {
teardown(closeAllEditors);
const multiCursors = [new Selection(2, 6, 2, 6), new Selection(3, 6, 3, 6)];
const multiCursorsWithSelection = [new Selection(2, 2, 2, 28), new Selection(3, 2, 3, 33)];
const multiCursorsWithFullLineSelection = [new Selection(2, 0, 2, 28), new Selection(3, 0, 4, 0)];
const oldValueForSyntaxProfiles = workspace.getConfiguration('emmet').inspect('syntaxProfile');
test('Wrap with block element using multi cursor', () => {
return testWrapWithAbbreviation(multiCursors, 'div', wrapBlockElementExpected);
});
test('Wrap with inline element using multi cursor', () => {
return testWrapWithAbbreviation(multiCursors, 'span', wrapInlineElementExpected);
});
test('Wrap with snippet using multi cursor', () => {
return testWrapWithAbbreviation(multiCursors, 'a', wrapSnippetExpected);
});
test('Wrap with multi line abbreviation using multi cursor', () => {
return testWrapWithAbbreviation(multiCursors, 'ul>li', wrapMultiLineAbbrExpected);
});
test('Wrap with block element using multi cursor selection', () => {
return testWrapWithAbbreviation(multiCursorsWithSelection, 'div', wrapBlockElementExpected);
});
test('Wrap with inline element using multi cursor selection', () => {
return testWrapWithAbbreviation(multiCursorsWithSelection, 'span', wrapInlineElementExpected);
});
test('Wrap with snippet using multi cursor selection', () => {
return testWrapWithAbbreviation(multiCursorsWithSelection, 'a', wrapSnippetExpected);
});
test('Wrap with multi line abbreviation using multi cursor selection', () => {
return testWrapWithAbbreviation(multiCursorsWithSelection, 'ul>li', wrapMultiLineAbbrExpected);
});
test('Wrap with block element using multi cursor full line selection', () => {
return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'div', wrapBlockElementExpected);
});
test('Wrap with inline element using multi cursor full line selection', () => {
return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'span', wrapInlineElementExpected);
});
test('Wrap with snippet using multi cursor full line selection', () => {
return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'a', wrapSnippetExpected);
});
test('Wrap with multi line abbreviation using multi cursor full line selection', () => {
return testWrapWithAbbreviation(multiCursorsWithFullLineSelection, 'ul>li', wrapMultiLineAbbrExpected);
});
test('Wrap with abbreviation and comment filter', () => {
const contents = `
<ul class="nav main">
line
</ul>
`;
const expectedContents = `
<ul class="nav main">
<li class="hello">
line
</li>
<!-- /.hello -->
</ul>
`;
return testWrapWithAbbreviation([new Selection(2, 0, 2, 0)], 'li.hello|c', expectedContents, contents);
});
test('Wrap with abbreviation entire node when cursor is on opening tag', () => {
const contents = `
<div class="nav main">
hello
</div>
`;
const expectedContents = `
<div>
<div class="nav main">
hello
</div>
</div>
`;
return testWrapWithAbbreviation([new Selection(1, 1, 1, 1)], 'div', expectedContents, contents);
});
test('Wrap with abbreviation entire node when cursor is on closing tag', () => {
const contents = `
<div class="nav main">
hello
</div>
`;
const expectedContents = `
<div>
<div class="nav main">
hello
</div>
</div>
`;
return testWrapWithAbbreviation([new Selection(3, 1, 3, 1)], 'div', expectedContents, contents);
});
test('Wrap with multiline abbreviation doesnt add extra spaces', () => {
// Issue #29898
const contents = `
hello
`;
const expectedContents = `
<ul>
<li><a href="">hello</a></li>
</ul>
`;
return testWrapWithAbbreviation([new Selection(1, 2, 1, 2)], 'ul>li>a', expectedContents, contents);
});
test('Wrap individual lines with abbreviation', () => {
const contents = `
<ul class="nav main">
<li class="item1">This $10 is not a tabstop</li>
<li class="item2">hi.there</li>
</ul>
`;
const wrapIndividualLinesExpected = `
<ul class="nav main">
<ul>
<li class="hello1"><li class="item1">This $10 is not a tabstop</li></li>
<li class="hello2"><li class="item2">hi.there</li></li>
</ul>
</ul>
`;
return testWrapIndividualLinesWithAbbreviation([new Selection(2, 2, 3, 33)], 'ul>li.hello$*', wrapIndividualLinesExpected, contents);
});
test('Wrap individual lines with abbreviation with extra space selected', () => {
const contents = `
<ul class="nav main">
<li class="item1">img</li>
<li class="item2">hi.there</li>
</ul>
`;
const wrapIndividualLinesExpected = `
<ul class="nav main">
<ul>
<li class="hello1"><li class="item1">img</li></li>
<li class="hello2"><li class="item2">hi.there</li></li>
</ul>
</ul>
`;
return testWrapIndividualLinesWithAbbreviation([new Selection(2, 1, 4, 0)], 'ul>li.hello$*', wrapIndividualLinesExpected, contents);
});
test('Wrap individual lines with abbreviation with comment filter', () => {
const contents = `
<ul class="nav main">
<li class="item1">img</li>
<li class="item2">hi.there</li>
</ul>
`;
const wrapIndividualLinesExpected = `
<ul class="nav main">
<ul>
<li class="hello"><li class="item1">img</li></li>
<!-- /.hello -->
<li class="hello"><li class="item2">hi.there</li></li>
<!-- /.hello -->
</ul>
</ul>
`;
return testWrapIndividualLinesWithAbbreviation([new Selection(2, 2, 3, 33)], 'ul>li.hello*|c', wrapIndividualLinesExpected, contents);
});
test('Wrap individual lines with abbreviation and trim', () => {
const contents = `
<ul class="nav main">
• lorem ipsum
• lorem ipsum
</ul>
`;
const wrapIndividualLinesExpected = `
<ul class="nav main">
<ul>
<li class="hello1">lorem ipsum</li>
<li class="hello2">lorem ipsum</li>
</ul>
</ul>
`;
return testWrapIndividualLinesWithAbbreviation([new Selection(2, 3, 3, 16)], 'ul>li.hello$*|t', wrapIndividualLinesExpected, contents);
});
test('Wrap with abbreviation and format set to false', () => {
return workspace.getConfiguration('emmet').update('syntaxProfiles',{ 'html' : { 'format': false } } , ConfigurationTarget.Global).then(() => {
return testWrapWithAbbreviation(multiCursors,'h1',wrapInlineElementExpectedFormatFalse).then(() => {
return workspace.getConfiguration('emmet').update('syntaxProfiles',oldValueForSyntaxProfiles ? oldValueForSyntaxProfiles.globalValue : undefined, ConfigurationTarget.Global);
});
});
});
test('Wrap multi line selections with abbreviation', () => {
const htmlContentsForWrapMultiLineTests = `
<ul class="nav main">
line1
line2
line3
line4
</ul>
`;
const wrapMultiLineExpected = `
<ul class="nav main">
<div>
line1
line2
</div>
<div>
line3
line4
</div>
</ul>
`;
return testWrapWithAbbreviation([new Selection(2, 4, 3, 9), new Selection(5, 4, 6, 9)], 'div', wrapMultiLineExpected, htmlContentsForWrapMultiLineTests);
});
test('Wrap multiline with abbreviation uses className for jsx files', () => {
const wrapMultiLineJsxExpected = `
<ul class="nav main">
<div className="hello">
<li class="item1">img</li>
<li class="item2">$hithere</li>
</div>
</ul>
`;
return testWrapWithAbbreviation([new Selection(2,2,3,33)], '.hello', wrapMultiLineJsxExpected, htmlContentsForWrapTests, 'jsx');
});
test('Wrap individual line with abbreviation uses className for jsx files', () => {
const wrapIndividualLinesJsxExpected = `
<ul class="nav main">
<div className="hello1"><li class="item1">img</li></div>
<div className="hello2"><li class="item2">$hithere</li></div>
</ul>
`;
return testWrapIndividualLinesWithAbbreviation([new Selection(2,2,3,33)], '.hello$*', wrapIndividualLinesJsxExpected, htmlContentsForWrapTests, 'jsx');
});
});
function testWrapWithAbbreviation(selections: Selection[], abbreviation: string, expectedContents: string, input: string = htmlContentsForWrapTests, fileExtension: string = 'html'): Thenable<any> {
return withRandomFileEditor(input, fileExtension, (editor, _) => {
editor.selections = selections;
const promise = wrapWithAbbreviation({ abbreviation });
if (!promise) {
assert.equal(1, 2, 'Wrap with Abbreviation returned undefined.');
return Promise.resolve();
}
return promise.then(() => {
assert.equal(editor.document.getText(), expectedContents);
return Promise.resolve();
});
});
}
function testWrapIndividualLinesWithAbbreviation(selections: Selection[], abbreviation: string, expectedContents: string, input: string = htmlContentsForWrapTests, fileExtension: string = 'html'): Thenable<any> {
return withRandomFileEditor(input, fileExtension, (editor, _) => {
editor.selections = selections;
const promise = wrapIndividualLinesWithAbbreviation({ abbreviation });
if (!promise) {
assert.equal(1, 2, 'Wrap individual lines with Abbreviation returned undefined.');
return Promise.resolve();
}
return promise.then(() => {
assert.equal(editor.document.getText(), expectedContents);
return Promise.resolve();
});
});
}

View File

@ -0,0 +1,218 @@
/*---------------------------------------------------------------------------------------------
* 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 { getNodesInBetween, getNode, getHtmlNode, parseDocument, sameNodes, isStyleSheet, validate } from './util';
import { Node, Stylesheet, Rule } from 'EmmetNode';
import parseStylesheet from '@emmetio/css-parser';
import { DocumentStreamReader } from './bufferStream';
const startCommentStylesheet = '/*';
const endCommentStylesheet = '*/';
const startCommentHTML = '<!--';
const endCommentHTML = '-->';
export function toggleComment(): Thenable<boolean> | undefined {
if (!validate() || !vscode.window.activeTextEditor) {
return;
}
const editor = vscode.window.activeTextEditor;
let rootNode = parseDocument(editor.document);
if (!rootNode) {
return;
}
return editor.edit(editBuilder => {
let allEdits: vscode.TextEdit[][] = [];
editor.selections.reverse().forEach(selection => {
let edits = isStyleSheet(editor.document.languageId) ? toggleCommentStylesheet(selection, <Stylesheet>rootNode) : toggleCommentHTML(editor.document, selection, rootNode!);
if (edits.length > 0) {
allEdits.push(edits);
}
});
// Apply edits in order so we can skip nested ones.
allEdits.sort((arr1, arr2) => {
let result = arr1[0].range.start.line - arr2[0].range.start.line;
return result === 0 ? arr1[0].range.start.character - arr2[0].range.start.character : result;
});
let lastEditPosition = new vscode.Position(0, 0);
for (const edits of allEdits) {
if (edits[0].range.end.isAfterOrEqual(lastEditPosition)) {
edits.forEach(x => {
editBuilder.replace(x.range, x.newText);
lastEditPosition = x.range.end;
});
}
}
});
}
function toggleCommentHTML(document: vscode.TextDocument, selection: vscode.Selection, rootNode: Node): vscode.TextEdit[] {
const selectionStart = selection.isReversed ? selection.active : selection.anchor;
const selectionEnd = selection.isReversed ? selection.anchor : selection.active;
let startNode = getHtmlNode(document, rootNode, selectionStart, true);
let endNode = getHtmlNode(document, rootNode, selectionEnd, true);
if (!startNode || !endNode) {
return [];
}
if (sameNodes(startNode, endNode) && startNode.name === 'style'
&& startNode.open.end.isBefore(selectionStart)
&& startNode.close.start.isAfter(selectionEnd)) {
let buffer = new DocumentStreamReader(document, startNode.open.end, new vscode.Range(startNode.open.end, startNode.close.start));
let cssRootNode = parseStylesheet(buffer);
return toggleCommentStylesheet(selection, cssRootNode);
}
let allNodes: Node[] = getNodesInBetween(startNode, endNode);
let edits: vscode.TextEdit[] = [];
allNodes.forEach(node => {
edits = edits.concat(getRangesToUnCommentHTML(node, document));
});
if (startNode.type === 'comment') {
return edits;
}
edits.push(new vscode.TextEdit(new vscode.Range(allNodes[0].start, allNodes[0].start), startCommentHTML));
edits.push(new vscode.TextEdit(new vscode.Range(allNodes[allNodes.length - 1].end, allNodes[allNodes.length - 1].end), endCommentHTML));
return edits;
}
function getRangesToUnCommentHTML(node: Node, document: vscode.TextDocument): vscode.TextEdit[] {
let unCommentTextEdits: vscode.TextEdit[] = [];
// If current node is commented, then uncomment and return
if (node.type === 'comment') {
unCommentTextEdits.push(new vscode.TextEdit(new vscode.Range(node.start, node.start.translate(0, startCommentHTML.length)), ''));
unCommentTextEdits.push(new vscode.TextEdit(new vscode.Range(node.end.translate(0, -endCommentHTML.length), node.end), ''));
return unCommentTextEdits;
}
// All children of current node should be uncommented
node.children.forEach(childNode => {
unCommentTextEdits = unCommentTextEdits.concat(getRangesToUnCommentHTML(childNode, document));
});
return unCommentTextEdits;
}
function toggleCommentStylesheet(selection: vscode.Selection, rootNode: Stylesheet): vscode.TextEdit[] {
let selectionStart = selection.isReversed ? selection.active : selection.anchor;
let selectionEnd = selection.isReversed ? selection.anchor : selection.active;
let startNode = getNode(rootNode, selectionStart, true);
let endNode = getNode(rootNode, selectionEnd, true);
if (!selection.isEmpty) {
selectionStart = adjustStartNodeCss(startNode, selectionStart, rootNode);
selectionEnd = adjustEndNodeCss(endNode, selectionEnd, rootNode);
selection = new vscode.Selection(selectionStart, selectionEnd);
} else if (startNode) {
selectionStart = startNode.start;
selectionEnd = startNode.end;
selection = new vscode.Selection(selectionStart, selectionEnd);
}
// Uncomment the comments that intersect with the selection.
let rangesToUnComment: vscode.Range[] = [];
let edits: vscode.TextEdit[] = [];
rootNode.comments.forEach(comment => {
let commentRange = new vscode.Range(comment.start, comment.end);
if (selection.intersection(commentRange)) {
rangesToUnComment.push(commentRange);
edits.push(new vscode.TextEdit(new vscode.Range(comment.start, comment.start.translate(0, startCommentStylesheet.length)), ''));
edits.push(new vscode.TextEdit(new vscode.Range(comment.end.translate(0, -endCommentStylesheet.length), comment.end), ''));
}
});
if (edits.length > 0) {
return edits;
}
return [
new vscode.TextEdit(new vscode.Range(selection.start, selection.start), startCommentStylesheet),
new vscode.TextEdit(new vscode.Range(selection.end, selection.end), endCommentStylesheet)
];
}
function adjustStartNodeCss(node: Node | null, pos: vscode.Position, rootNode: Stylesheet): vscode.Position {
for (const comment of rootNode.comments) {
let commentRange = new vscode.Range(comment.start, comment.end);
if (commentRange.contains(pos)) {
return pos;
}
}
if (!node) {
return pos;
}
if (node.type === 'property') {
return node.start;
}
const rule = <Rule>node;
if (pos.isBefore(rule.contentStartToken.end) || !rule.firstChild) {
return rule.start;
}
if (pos.isBefore(rule.firstChild.start)) {
return pos;
}
let newStartNode = rule.firstChild;
while (newStartNode.nextSibling && pos.isAfter(newStartNode.end)) {
newStartNode = newStartNode.nextSibling;
}
return newStartNode.start;
}
function adjustEndNodeCss(node: Node | null, pos: vscode.Position, rootNode: Stylesheet): vscode.Position {
for (const comment of rootNode.comments) {
let commentRange = new vscode.Range(comment.start, comment.end);
if (commentRange.contains(pos)) {
return pos;
}
}
if (!node) {
return pos;
}
if (node.type === 'property') {
return node.end;
}
const rule = <Rule>node;
if (pos.isEqual(rule.contentEndToken.end) || !rule.firstChild) {
return rule.end;
}
if (pos.isAfter(rule.children[rule.children.length - 1].end)) {
return pos;
}
let newEndNode = rule.children[rule.children.length - 1];
while (newEndNode.previousSibling && pos.isBefore(newEndNode.start)) {
newEndNode = newEndNode.previousSibling;
}
return newEndNode.end;
}

View File

@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'EmmetNode' {
import { Position } from 'vscode';
export interface Node {
start: Position
end: Position
type: string
parent: Node
firstChild: Node
nextSibling: Node
previousSibling: Node
children: Node[]
}
export interface Token {
start: Position
end: Position
stream: BufferStream
toString(): string
}
export interface CssToken extends Token {
size: number
item(number: number): any
type: string
}
export interface HtmlToken extends Token {
value: string
}
export interface Attribute extends Token {
name: Token
value: Token
}
export interface HtmlNode extends Node {
name: string
open: Token
close: Token
parent: HtmlNode
firstChild: HtmlNode
nextSibling: HtmlNode
previousSibling: HtmlNode
children: HtmlNode[]
attributes: Attribute[]
}
export interface CssNode extends Node {
name: string
parent: CssNode
firstChild: CssNode
nextSibling: CssNode
previousSibling: CssNode
children: CssNode[]
}
export interface Rule extends CssNode {
selectorToken: Token
contentStartToken: Token
contentEndToken: Token
}
export interface Property extends CssNode {
valueToken: Token
separator: string
parent: Rule
terminatorToken: Token
separatorToken: Token
value: string
}
export interface Stylesheet extends Node {
comments: Token[]
}
export interface BufferStream {
peek(): number
next(): number
backUp(n: number): number
current(): string
substring(from: Position, to: Position): string
eat(match: any): boolean
eatWhile(match: any): boolean
}
}

View File

@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module '@emmetio/css-parser' {
import { BufferStream, Stylesheet } from 'EmmetNode';
function parseStylesheet(stream: BufferStream): Stylesheet;
export default parseStylesheet;
}

View File

@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module '@emmetio/html-matcher' {
import { BufferStream, HtmlNode } from 'EmmetNode';
function parse(stream: BufferStream): HtmlNode;
export default parse;
}

View File

@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module '@emmetio/math-expression' {
import { BufferStream } from 'EmmetNode';
function index(stream: BufferStream, backward: boolean): number;
export default index;
}

View File

@ -0,0 +1,23 @@
// Type definitions for image-size
// Project: https://github.com/image-size/image-size
// Definitions by: Elisée MAURER <https://github.com/elisee>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
/// <reference types='@types/node'/>
declare module 'image-size' {
interface ImageInfo {
width: number;
height: number;
type: string;
}
function sizeOf(path: string): ImageInfo;
function sizeOf(path: string, callback: (err: Error, dimensions: ImageInfo) => void): void;
function sizeOf(buffer: Buffer): ImageInfo;
namespace sizeOf { }
export = sizeOf;
}

View File

@ -0,0 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference types='@types/node'/>

View File

@ -0,0 +1,289 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Based on @sergeche's work on the emmet plugin for atom
import { TextEditor, Range, Position, window, TextEdit } from 'vscode';
import * as path from 'path';
import { getImageSize } from './imageSizeHelper';
import { parseDocument, getNode, iterateCSSToken, getCssPropertyFromRule, isStyleSheet, validate } from './util';
import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetNode';
import { locateFile } from './locateFile';
import parseStylesheet from '@emmetio/css-parser';
import { DocumentStreamReader } from './bufferStream';
/**
* Updates size of context image in given editor
*/
export function updateImageSize() {
if (!validate() || !window.activeTextEditor) {
return;
}
const editor = window.activeTextEditor;
let allUpdatesPromise = editor.selections.reverse().map(selection => {
let position = selection.isReversed ? selection.active : selection.anchor;
if (!isStyleSheet(editor.document.languageId)) {
return updateImageSizeHTML(editor, position);
} else {
return updateImageSizeCSSFile(editor, position);
}
});
return Promise.all(allUpdatesPromise).then((updates) => {
return editor.edit(builder => {
updates.forEach(update => {
update.forEach((textEdit: TextEdit) => {
builder.replace(textEdit.range, textEdit.newText);
});
});
});
});
}
/**
* Updates image size of context tag of HTML model
*/
function updateImageSizeHTML(editor: TextEditor, position: Position): Promise<TextEdit[]> {
const imageNode = getImageHTMLNode(editor, position);
const src = imageNode && getImageSrcHTML(imageNode);
if (!src) {
return updateImageSizeStyleTag(editor, position);
}
return locateFile(path.dirname(editor.document.fileName), src)
.then(getImageSize)
.then((size: any) => {
// since this action is asynchronous, we have to ensure that editor wasnt
// changed and user didnt moved caret outside <img> node
const img = getImageHTMLNode(editor, position);
if (img && getImageSrcHTML(img) === src) {
return updateHTMLTag(editor, img, size.width, size.height);
}
return [];
})
.catch(err => { console.warn('Error while updating image size:', err); return []; });
}
function updateImageSizeStyleTag(editor: TextEditor, position: Position): Promise<TextEdit[]> {
const getPropertyInsiderStyleTag = (editor: TextEditor): Property | null => {
const rootNode = parseDocument(editor.document);
const currentNode = <HtmlNode>getNode(rootNode, position, true);
if (currentNode && currentNode.name === 'style'
&& currentNode.open.end.isBefore(position)
&& currentNode.close.start.isAfter(position)) {
let buffer = new DocumentStreamReader(editor.document, currentNode.open.end, new Range(currentNode.open.end, currentNode.close.start));
let rootNode = parseStylesheet(buffer);
const node = getNode(rootNode, position, true);
return (node && node.type === 'property') ? <Property>node : null;
}
return null;
};
return updateImageSizeCSS(editor, position, getPropertyInsiderStyleTag);
}
function updateImageSizeCSSFile(editor: TextEditor, position: Position): Promise<TextEdit[]> {
return updateImageSizeCSS(editor, position, getImageCSSNode);
}
/**
* Updates image size of context rule of stylesheet model
*/
function updateImageSizeCSS(editor: TextEditor, position: Position, fetchNode: (editor: TextEditor, position: Position) => Property | null): Promise<TextEdit[]> {
const node = fetchNode(editor, position);
const src = node && getImageSrcCSS(node, position);
if (!src) {
return Promise.reject(new Error('No valid image source'));
}
return locateFile(path.dirname(editor.document.fileName), src)
.then(getImageSize)
.then((size: any): TextEdit[] => {
// since this action is asynchronous, we have to ensure that editor wasnt
// changed and user didnt moved caret outside <img> node
const prop = fetchNode(editor, position);
if (prop && getImageSrcCSS(prop, position) === src) {
return updateCSSNode(editor, prop, size.width, size.height);
}
return [];
})
.catch(err => { console.warn('Error while updating image size:', err); return []; });
}
/**
* Returns <img> node under caret in given editor or `null` if such node cannot
* be found
*/
function getImageHTMLNode(editor: TextEditor, position: Position): HtmlNode | null {
const rootNode = parseDocument(editor.document);
const node = <HtmlNode>getNode(rootNode, position, true);
return node && node.name.toLowerCase() === 'img' ? node : null;
}
/**
* Returns css property under caret in given editor or `null` if such node cannot
* be found
*/
function getImageCSSNode(editor: TextEditor, position: Position): Property | null {
const rootNode = parseDocument(editor.document);
const node = getNode(rootNode, position, true);
return node && node.type === 'property' ? <Property>node : null;
}
/**
* Returns image source from given <img> node
*/
function getImageSrcHTML(node: HtmlNode): string | undefined {
const srcAttr = getAttribute(node, 'src');
if (!srcAttr) {
return;
}
return (<HtmlToken>srcAttr.value).value;
}
/**
* Returns image source from given `url()` token
*/
function getImageSrcCSS(node: Property | undefined, position: Position): string | undefined {
if (!node) {
return;
}
const urlToken = findUrlToken(node, position);
if (!urlToken) {
return;
}
// A stylesheet token may contain either quoted ('string') or unquoted URL
let urlValue = urlToken.item(0);
if (urlValue && urlValue.type === 'string') {
urlValue = urlValue.item(0);
}
return urlValue && urlValue.valueOf();
}
/**
* Updates size of given HTML node
*/
function updateHTMLTag(editor: TextEditor, node: HtmlNode, width: number, height: number): TextEdit[] {
const srcAttr = getAttribute(node, 'src');
const widthAttr = getAttribute(node, 'width');
const heightAttr = getAttribute(node, 'height');
const quote = getAttributeQuote(editor, srcAttr);
const endOfAttributes = node.attributes[node.attributes.length - 1].end;
let edits: TextEdit[] = [];
let textToAdd = '';
if (!widthAttr) {
textToAdd += ` width=${quote}${width}${quote}`;
} else {
edits.push(new TextEdit(new Range(widthAttr.value.start, widthAttr.value.end), String(width)));
}
if (!heightAttr) {
textToAdd += ` height=${quote}${height}${quote}`;
} else {
edits.push(new TextEdit(new Range(heightAttr.value.start, heightAttr.value.end), String(height)));
}
if (textToAdd) {
edits.push(new TextEdit(new Range(endOfAttributes, endOfAttributes), textToAdd));
}
return edits;
}
/**
* Updates size of given CSS rule
*/
function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, height: number): TextEdit[] {
const rule = srcProp.parent;
const widthProp = getCssPropertyFromRule(rule, 'width');
const heightProp = getCssPropertyFromRule(rule, 'height');
// Detect formatting
const separator = srcProp.separator || ': ';
const before = getPropertyDelimitor(editor, srcProp);
let edits: TextEdit[] = [];
if (!srcProp.terminatorToken) {
edits.push(new TextEdit(new Range(srcProp.end, srcProp.end), ';'));
}
let textToAdd = '';
if (!widthProp) {
textToAdd += `${before}width${separator}${width}px;`;
} else {
edits.push(new TextEdit(new Range(widthProp.valueToken.start, widthProp.valueToken.end), `${width}px`));
}
if (!heightProp) {
textToAdd += `${before}height${separator}${height}px;`;
} else {
edits.push(new TextEdit(new Range(heightProp.valueToken.start, heightProp.valueToken.end), `${height}px`));
}
if (textToAdd) {
edits.push(new TextEdit(new Range(srcProp.end, srcProp.end), textToAdd));
}
return edits;
}
/**
* Returns attribute object with `attrName` name from given HTML node
*/
function getAttribute(node: HtmlNode, attrName: string): Attribute {
attrName = attrName.toLowerCase();
return node && (node.open as any).attributes.find((attr: any) => attr.name.value.toLowerCase() === attrName);
}
/**
* Returns quote character, used for value of given attribute. May return empty
* string if attribute wasnt quoted
*/
function getAttributeQuote(editor: TextEditor, attr: any): string {
const range = new Range(attr.value ? attr.value.end : attr.end, attr.end);
return range.isEmpty ? '' : editor.document.getText(range);
}
/**
* Finds 'url' token for given `pos` point in given CSS property `node`
*/
function findUrlToken(node: Property, pos: Position): CssToken | undefined {
for (let i = 0, il = (node as any).parsedValue.length, url; i < il; i++) {
iterateCSSToken((node as any).parsedValue[i], (token: CssToken) => {
if (token.type === 'url' && token.start.isBeforeOrEqual(pos) && token.end.isAfterOrEqual(pos)) {
url = token;
return false;
}
return true;
});
if (url) {
return url;
}
}
return;
}
/**
* Returns a string that is used to delimit properties in current nodes rule
*/
function getPropertyDelimitor(editor: TextEditor, node: Property): string {
let anchor;
if (anchor = (node.previousSibling || node.parent.contentStartToken)) {
return editor.document.getText(new Range(anchor.end, node.start));
} else if (anchor = (node.nextSibling || node.parent.contentEndToken)) {
return editor.document.getText(new Range(node.end, anchor.start));
}
return '';
}

View File

@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* 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 { HtmlNode } from 'EmmetNode';
import { getHtmlNode, parseDocument, validate } from './util';
export function updateTag(tagName: string): Thenable<boolean> | undefined {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}
let editor = vscode.window.activeTextEditor;
let rootNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) {
return;
}
let rangesToUpdate: vscode.Range[] = [];
editor.selections.reverse().forEach(selection => {
rangesToUpdate = rangesToUpdate.concat(getRangesToUpdate(editor, selection, rootNode));
});
return editor.edit(editBuilder => {
rangesToUpdate.forEach(range => {
editBuilder.replace(range, tagName);
});
});
}
function getRangesToUpdate(editor: vscode.TextEditor, selection: vscode.Selection, rootNode: HtmlNode): vscode.Range[] {
let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true);
if (!nodeToUpdate) {
return [];
}
let openStart = nodeToUpdate.open.start.translate(0, 1);
let openEnd = openStart.translate(0, nodeToUpdate.name.length);
let ranges = [new vscode.Range(openStart, openEnd)];
if (nodeToUpdate.close) {
let closeStart = nodeToUpdate.close.start.translate(0, 2);
let closeEnd = nodeToUpdate.close.end.translate(0, -1);
ranges.push(new vscode.Range(closeStart, closeEnd));
}
return ranges;
}

View File

@ -0,0 +1,655 @@
/*---------------------------------------------------------------------------------------------
* 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 parse from '@emmetio/html-matcher';
import parseStylesheet from '@emmetio/css-parser';
import { Node, HtmlNode, CssToken, Property, Rule, Stylesheet } from 'EmmetNode';
import { DocumentStreamReader } from './bufferStream';
import * as EmmetHelper from 'vscode-emmet-helper';
import { TextDocument as LSTextDocument } from 'vscode-html-languageservice';
let _emmetHelper: typeof EmmetHelper;
let _currentExtensionsPath: string | undefined = undefined;
let _homeDir: vscode.Uri | undefined;
export function setHomeDir(homeDir: vscode.Uri) {
_homeDir = homeDir;
}
export function getEmmetHelper() {
// Lazy load vscode-emmet-helper instead of importing it
// directly to reduce the start-up time of the extension
if (!_emmetHelper) {
_emmetHelper = require('vscode-emmet-helper');
}
updateEmmetExtensionsPath();
return _emmetHelper;
}
/**
* Update Emmet Helper to use user snippets from the extensionsPath setting
*/
export function updateEmmetExtensionsPath(forceRefresh: boolean = false) {
if (!_emmetHelper) {
return;
}
let extensionsPath = vscode.workspace.getConfiguration('emmet')['extensionsPath'];
if (forceRefresh || _currentExtensionsPath !== extensionsPath) {
_currentExtensionsPath = extensionsPath;
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
return;
} else {
const rootPath = vscode.workspace.workspaceFolders[0].uri;
const fileSystem = vscode.workspace.fs;
_emmetHelper.updateExtensionsPath(extensionsPath, fileSystem, rootPath, _homeDir).then(null, (err: string) => vscode.window.showErrorMessage(err));
}
}
}
/**
* Mapping between languages that support Emmet and completion trigger characters
*/
export const LANGUAGE_MODES: { [id: string]: string[] } = {
'html': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'jade': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'slim': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'haml': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'xml': ['.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'xsl': ['!', '.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'css': [':', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'scss': [':', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'sass': [':', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'less': [':', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'stylus': [':', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'javascriptreact': ['!', '.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
'typescriptreact': ['!', '.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
};
export function isStyleSheet(syntax: string): boolean {
let stylesheetSyntaxes = ['css', 'scss', 'sass', 'less', 'stylus'];
return (stylesheetSyntaxes.indexOf(syntax) > -1);
}
export function validate(allowStylesheet: boolean = true): boolean {
let editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showInformationMessage('No editor is active');
return false;
}
if (!allowStylesheet && isStyleSheet(editor.document.languageId)) {
return false;
}
return true;
}
export function getMappingForIncludedLanguages(): any {
// Explicitly map languages that have built-in grammar in VS Code to their parent language
// to get emmet completion support
// For other languages, users will have to use `emmet.includeLanguages` or
// language specific extensions can provide emmet completion support
const MAPPED_MODES: Object = {
'handlebars': 'html',
'php': 'html'
};
const finalMappedModes = Object.create(null);
let includeLanguagesConfig = vscode.workspace.getConfiguration('emmet')['includeLanguages'];
let includeLanguages = Object.assign({}, MAPPED_MODES, includeLanguagesConfig ? includeLanguagesConfig : {});
Object.keys(includeLanguages).forEach(syntax => {
if (typeof includeLanguages[syntax] === 'string' && LANGUAGE_MODES[includeLanguages[syntax]]) {
finalMappedModes[syntax] = includeLanguages[syntax];
}
});
return finalMappedModes;
}
/**
* Get the corresponding emmet mode for given vscode language mode
* E.g.: jsx for typescriptreact/javascriptreact or pug for jade
* If the language is not supported by emmet or has been excluded via `excludeLanguages` setting,
* then nothing is returned
*
* @param excludedLanguages Array of language ids that user has chosen to exclude for emmet
*/
export function getEmmetMode(language: string, excludedLanguages: string[]): string | undefined {
if (!language || excludedLanguages.indexOf(language) > -1) {
return;
}
if (/\b(typescriptreact|javascriptreact|jsx-tags)\b/.test(language)) { // treat tsx like jsx
return 'jsx';
}
if (language === 'sass-indented') { // map sass-indented to sass
return 'sass';
}
if (language === 'jade') {
return 'pug';
}
const emmetModes = ['html', 'pug', 'slim', 'haml', 'xml', 'xsl', 'jsx', 'css', 'scss', 'sass', 'less', 'stylus'];
if (emmetModes.indexOf(language) > -1) {
return language;
}
return;
}
/**
* Parses the given document using emmet parsing modules
*/
export function parseDocument(document: vscode.TextDocument, showError: boolean = true): Node | undefined {
let parseContent = isStyleSheet(document.languageId) ? parseStylesheet : parse;
try {
return parseContent(new DocumentStreamReader(document));
} catch (e) {
if (showError) {
vscode.window.showErrorMessage('Emmet: Failed to parse the file');
}
}
return undefined;
}
const closeBrace = 125;
const openBrace = 123;
const slash = 47;
const star = 42;
/**
* Traverse the given document backward & forward from given position
* to find a complete ruleset, then parse just that to return a Stylesheet
* @param document vscode.TextDocument
* @param position vscode.Position
*/
export function parsePartialStylesheet(document: vscode.TextDocument, position: vscode.Position): Stylesheet | undefined {
const isCSS = document.languageId === 'css';
let startPosition = new vscode.Position(0, 0);
let endPosition = new vscode.Position(document.lineCount - 1, document.lineAt(document.lineCount - 1).text.length);
const limitCharacter = document.offsetAt(position) - 5000;
const limitPosition = limitCharacter > 0 ? document.positionAt(limitCharacter) : startPosition;
const stream = new DocumentStreamReader(document, position);
function findOpeningCommentBeforePosition(pos: vscode.Position): vscode.Position | undefined {
let text = document.getText(new vscode.Range(0, 0, pos.line, pos.character));
let offset = text.lastIndexOf('/*');
if (offset === -1) {
return;
}
return document.positionAt(offset);
}
function findClosingCommentAfterPosition(pos: vscode.Position): vscode.Position | undefined {
let text = document.getText(new vscode.Range(pos.line, pos.character, document.lineCount - 1, document.lineAt(document.lineCount - 1).text.length));
let offset = text.indexOf('*/');
if (offset === -1) {
return;
}
offset += 2 + document.offsetAt(pos);
return document.positionAt(offset);
}
function consumeLineCommentBackwards() {
if (!isCSS && currentLine !== stream.pos.line) {
currentLine = stream.pos.line;
let startLineComment = document.lineAt(currentLine).text.indexOf('//');
if (startLineComment > -1) {
stream.pos = new vscode.Position(currentLine, startLineComment);
}
}
}
function consumeBlockCommentBackwards() {
if (stream.peek() === slash) {
if (stream.backUp(1) === star) {
stream.pos = findOpeningCommentBeforePosition(stream.pos) || startPosition;
} else {
stream.next();
}
}
}
function consumeCommentForwards() {
if (stream.eat(slash)) {
if (stream.eat(slash) && !isCSS) {
stream.pos = new vscode.Position(stream.pos.line + 1, 0);
} else if (stream.eat(star)) {
stream.pos = findClosingCommentAfterPosition(stream.pos) || endPosition;
}
}
}
// Go forward until we find a closing brace.
while (!stream.eof() && !stream.eat(closeBrace)) {
if (stream.peek() === slash) {
consumeCommentForwards();
} else {
stream.next();
}
}
if (!stream.eof()) {
endPosition = stream.pos;
}
stream.pos = position;
let openBracesToFind = 1;
let currentLine = position.line;
let exit = false;
// Go back until we found an opening brace. If we find a closing one, consume its pair and continue.
while (!exit && openBracesToFind > 0 && !stream.sof()) {
consumeLineCommentBackwards();
switch (stream.backUp(1)) {
case openBrace:
openBracesToFind--;
break;
case closeBrace:
if (isCSS) {
stream.next();
startPosition = stream.pos;
exit = true;
} else {
openBracesToFind++;
}
break;
case slash:
consumeBlockCommentBackwards();
break;
default:
break;
}
if (position.line - stream.pos.line > 100 || stream.pos.isBeforeOrEqual(limitPosition)) {
exit = true;
}
}
// We are at an opening brace. We need to include its selector.
currentLine = stream.pos.line;
openBracesToFind = 0;
let foundSelector = false;
while (!exit && !stream.sof() && !foundSelector && openBracesToFind >= 0) {
consumeLineCommentBackwards();
const ch = stream.backUp(1);
if (/\s/.test(String.fromCharCode(ch))) {
continue;
}
switch (ch) {
case slash:
consumeBlockCommentBackwards();
break;
case closeBrace:
openBracesToFind++;
break;
case openBrace:
openBracesToFind--;
break;
default:
if (!openBracesToFind) {
foundSelector = true;
}
break;
}
if (!stream.sof() && foundSelector) {
startPosition = stream.pos;
}
}
try {
return parseStylesheet(new DocumentStreamReader(document, startPosition, new vscode.Range(startPosition, endPosition)));
} catch (e) {
return;
}
}
/**
* Returns node corresponding to given position in the given root node
*/
export function getNode(root: Node | undefined, position: vscode.Position, includeNodeBoundary: boolean) {
if (!root) {
return null;
}
let currentNode = root.firstChild;
let foundNode: Node | null = null;
while (currentNode) {
const nodeStart: vscode.Position = currentNode.start;
const nodeEnd: vscode.Position = currentNode.end;
if ((nodeStart.isBefore(position) && nodeEnd.isAfter(position))
|| (includeNodeBoundary && (nodeStart.isBeforeOrEqual(position) && nodeEnd.isAfterOrEqual(position)))) {
foundNode = currentNode;
// Dig deeper
currentNode = currentNode.firstChild;
} else {
currentNode = currentNode.nextSibling;
}
}
return foundNode;
}
export const allowedMimeTypesInScriptTag = ['text/html', 'text/plain', 'text/x-template', 'text/template', 'text/ng-template'];
/**
* Returns HTML node corresponding to given position in the given root node
* If position is inside a script tag of type template, then it will be parsed to find the inner HTML node as well
*/
export function getHtmlNode(document: vscode.TextDocument, root: Node | undefined, position: vscode.Position, includeNodeBoundary: boolean): HtmlNode | undefined {
let currentNode = <HtmlNode>getNode(root, position, includeNodeBoundary);
if (!currentNode) { return; }
const isTemplateScript = currentNode.name === 'script' &&
(currentNode.attributes &&
currentNode.attributes.some(x => x.name.toString() === 'type'
&& allowedMimeTypesInScriptTag.indexOf(x.value.toString()) > -1));
if (isTemplateScript && currentNode.close &&
(position.isAfter(currentNode.open.end) && position.isBefore(currentNode.close.start))) {
let buffer = new DocumentStreamReader(document, currentNode.open.end, new vscode.Range(currentNode.open.end, currentNode.close.start));
try {
let scriptInnerNodes = parse(buffer);
currentNode = <HtmlNode>getNode(scriptInnerNodes, position, includeNodeBoundary) || currentNode;
} catch (e) { }
}
return currentNode;
}
/**
* Returns inner range of an html node.
*/
export function getInnerRange(currentNode: HtmlNode): vscode.Range | undefined {
if (!currentNode.close) {
return undefined;
}
return new vscode.Range(currentNode.open.end, currentNode.close.start);
}
/**
* Returns the deepest non comment node under given node
*/
export function getDeepestNode(node: Node | undefined): Node | undefined {
if (!node || !node.children || node.children.length === 0 || !node.children.find(x => x.type !== 'comment')) {
return node;
}
for (let i = node.children.length - 1; i >= 0; i--) {
if (node.children[i].type !== 'comment') {
return getDeepestNode(node.children[i]);
}
}
return undefined;
}
export function findNextWord(propertyValue: string, pos: number): [number | undefined, number | undefined] {
let foundSpace = pos === -1;
let foundStart = false;
let foundEnd = false;
let newSelectionStart;
let newSelectionEnd;
while (pos < propertyValue.length - 1) {
pos++;
if (!foundSpace) {
if (propertyValue[pos] === ' ') {
foundSpace = true;
}
continue;
}
if (foundSpace && !foundStart && propertyValue[pos] === ' ') {
continue;
}
if (!foundStart) {
newSelectionStart = pos;
foundStart = true;
continue;
}
if (propertyValue[pos] === ' ') {
newSelectionEnd = pos;
foundEnd = true;
break;
}
}
if (foundStart && !foundEnd) {
newSelectionEnd = propertyValue.length;
}
return [newSelectionStart, newSelectionEnd];
}
export function findPrevWord(propertyValue: string, pos: number): [number | undefined, number | undefined] {
let foundSpace = pos === propertyValue.length;
let foundStart = false;
let foundEnd = false;
let newSelectionStart;
let newSelectionEnd;
while (pos > -1) {
pos--;
if (!foundSpace) {
if (propertyValue[pos] === ' ') {
foundSpace = true;
}
continue;
}
if (foundSpace && !foundEnd && propertyValue[pos] === ' ') {
continue;
}
if (!foundEnd) {
newSelectionEnd = pos + 1;
foundEnd = true;
continue;
}
if (propertyValue[pos] === ' ') {
newSelectionStart = pos + 1;
foundStart = true;
break;
}
}
if (foundEnd && !foundStart) {
newSelectionStart = 0;
}
return [newSelectionStart, newSelectionEnd];
}
export function getNodesInBetween(node1: Node, node2: Node): Node[] {
// Same node
if (sameNodes(node1, node2)) {
return [node1];
}
// Not siblings
if (!sameNodes(node1.parent, node2.parent)) {
// node2 is ancestor of node1
if (node2.start.isBefore(node1.start)) {
return [node2];
}
// node1 is ancestor of node2
if (node2.start.isBefore(node1.end)) {
return [node1];
}
// Get the highest ancestor of node1 that should be commented
while (node1.parent && node1.parent.end.isBefore(node2.start)) {
node1 = node1.parent;
}
// Get the highest ancestor of node2 that should be commented
while (node2.parent && node2.parent.start.isAfter(node1.start)) {
node2 = node2.parent;
}
}
const siblings: Node[] = [];
let currentNode = node1;
const position = node2.end;
while (currentNode && position.isAfter(currentNode.start)) {
siblings.push(currentNode);
currentNode = currentNode.nextSibling;
}
return siblings;
}
export function sameNodes(node1: Node, node2: Node): boolean {
if (!node1 || !node2) {
return false;
}
return (<vscode.Position>node1.start).isEqual(node2.start) && (<vscode.Position>node1.end).isEqual(node2.end);
}
export function getEmmetConfiguration(syntax: string) {
const emmetConfig = vscode.workspace.getConfiguration('emmet');
const syntaxProfiles = Object.assign({}, emmetConfig['syntaxProfiles'] || {});
const preferences = Object.assign({}, emmetConfig['preferences'] || {});
// jsx, xml and xsl syntaxes need to have self closing tags unless otherwise configured by user
if (syntax === 'jsx' || syntax === 'xml' || syntax === 'xsl') {
syntaxProfiles[syntax] = syntaxProfiles[syntax] || {};
if (typeof syntaxProfiles[syntax] === 'object'
&& !syntaxProfiles[syntax].hasOwnProperty('self_closing_tag') // Old Emmet format
&& !syntaxProfiles[syntax].hasOwnProperty('selfClosingStyle') // Emmet 2.0 format
) {
syntaxProfiles[syntax] = {
...syntaxProfiles[syntax],
selfClosingStyle: 'xml'
};
}
}
return {
preferences,
showExpandedAbbreviation: emmetConfig['showExpandedAbbreviation'],
showAbbreviationSuggestions: emmetConfig['showAbbreviationSuggestions'],
syntaxProfiles,
variables: emmetConfig['variables'],
excludeLanguages: emmetConfig['excludeLanguages'],
showSuggestionsAsSnippets: emmetConfig['showSuggestionsAsSnippets']
};
}
/**
* Itereates by each child, as well as nested child's children, in their order
* and invokes `fn` for each. If `fn` function returns `false`, iteration stops
*/
export function iterateCSSToken(token: CssToken, fn: (x: any) => any): boolean {
for (let i = 0, il = token.size; i < il; i++) {
if (fn(token.item(i)) === false || iterateCSSToken(token.item(i), fn) === false) {
return false;
}
}
return true;
}
/**
* Returns `name` CSS property from given `rule`
*/
export function getCssPropertyFromRule(rule: Rule, name: string): Property | undefined {
return rule.children.find(node => node.type === 'property' && node.name === name) as Property;
}
/**
* Returns css property under caret in given editor or `null` if such node cannot
* be found
*/
export function getCssPropertyFromDocument(editor: vscode.TextEditor, position: vscode.Position): Property | null {
const rootNode = parseDocument(editor.document);
const node = getNode(rootNode, position, true);
if (isStyleSheet(editor.document.languageId)) {
return node && node.type === 'property' ? <Property>node : null;
}
let htmlNode = <HtmlNode>node;
if (htmlNode
&& htmlNode.name === 'style'
&& htmlNode.open.end.isBefore(position)
&& htmlNode.close.start.isAfter(position)) {
let buffer = new DocumentStreamReader(editor.document, htmlNode.start, new vscode.Range(htmlNode.start, htmlNode.end));
let rootNode = parseStylesheet(buffer);
const node = getNode(rootNode, position, true);
return (node && node.type === 'property') ? <Property>node : null;
}
return null;
}
export function getEmbeddedCssNodeIfAny(document: vscode.TextDocument, currentNode: Node | null, position: vscode.Position): Node | undefined {
if (!currentNode) {
return;
}
const currentHtmlNode = <HtmlNode>currentNode;
if (currentHtmlNode && currentHtmlNode.close) {
const innerRange = getInnerRange(currentHtmlNode);
if (innerRange && innerRange.contains(position)) {
if (currentHtmlNode.name === 'style'
&& currentHtmlNode.open.end.isBefore(position)
&& currentHtmlNode.close.start.isAfter(position)
) {
let buffer = new DocumentStreamReader(document, currentHtmlNode.open.end, new vscode.Range(currentHtmlNode.open.end, currentHtmlNode.close.start));
return parseStylesheet(buffer);
}
}
}
return;
}
export function isStyleAttribute(currentNode: Node | null, position: vscode.Position): boolean {
if (!currentNode) {
return false;
}
const currentHtmlNode = <HtmlNode>currentNode;
const index = (currentHtmlNode.attributes || []).findIndex(x => x.name.toString() === 'style');
if (index === -1) {
return false;
}
const styleAttribute = currentHtmlNode.attributes[index];
return position.isAfterOrEqual(styleAttribute.value.start) && position.isBeforeOrEqual(styleAttribute.value.end);
}
export function trimQuotes(s: string) {
if (s.length <= 1) {
return s.replace(/['"]/, '');
}
if (s[0] === `'` || s[0] === `"`) {
s = s.slice(1);
}
if (s[s.length - 1] === `'` || s[s.length - 1] === `"`) {
s = s.slice(0, -1);
}
return s;
}
export function isNumber(obj: any): obj is number {
return typeof obj === 'number';
}
export function toLSTextDocument(doc: vscode.TextDocument): LSTextDocument {
return LSTextDocument.create(doc.uri.toString(), doc.languageId, doc.version, doc.getText());
}
export function getPathBaseName(path: string): string {
const pathAfterSlashSplit = path.split('/').pop();
const pathAfterBackslashSplit = pathAfterSlashSplit ? pathAfterSlashSplit.split('\\').pop() : '';
return pathAfterBackslashSplit ?? '';
}