Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
712
lib/vscode/extensions/emmet/src/abbreviationActions.ts
Normal file
712
lib/vscode/extensions/emmet/src/abbreviationActions.ts
Normal 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;
|
||||
}
|
124
lib/vscode/extensions/emmet/src/balance.ts
Normal file
124
lib/vscode/extensions/emmet/src/balance.ts
Normal 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;
|
||||
}
|
11
lib/vscode/extensions/emmet/src/browser/emmetBrowserMain.ts
Normal file
11
lib/vscode/extensions/emmet/src/browser/emmetBrowserMain.ts
Normal 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);
|
||||
}
|
172
lib/vscode/extensions/emmet/src/bufferStream.ts
Normal file
172
lib/vscode/extensions/emmet/src/bufferStream.ts
Normal 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);
|
||||
}
|
||||
}
|
221
lib/vscode/extensions/emmet/src/defaultCompletionProvider.ts
Normal file
221
lib/vscode/extensions/emmet/src/defaultCompletionProvider.ts
Normal 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);
|
||||
}
|
73
lib/vscode/extensions/emmet/src/editPoint.ts
Normal file
73
lib/vscode/extensions/emmet/src/editPoint.ts
Normal 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;
|
||||
}
|
189
lib/vscode/extensions/emmet/src/emmetCommon.ts
Normal file
189
lib/vscode/extensions/emmet/src/emmetCommon.ts
Normal 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() {
|
||||
}
|
35
lib/vscode/extensions/emmet/src/evaluateMathExpression.ts
Normal file
35
lib/vscode/extensions/emmet/src/evaluateMathExpression.ts
Normal 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 it’s because of non-math expression
|
||||
console.warn('Math evaluation error', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
113
lib/vscode/extensions/emmet/src/imageSizeHelper.ts
Normal file
113
lib/vscode/extensions/emmet/src/imageSizeHelper.ts
Normal 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)
|
||||
};
|
||||
}
|
114
lib/vscode/extensions/emmet/src/incrementDecrement.ts
Normal file
114
lib/vscode/extensions/emmet/src/incrementDecrement.ts
Normal 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;
|
||||
}
|
85
lib/vscode/extensions/emmet/src/locateFile.ts
Normal file
85
lib/vscode/extensions/emmet/src/locateFile.ts
Normal 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 user’s 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 editor’s 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 it’s 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);
|
||||
});
|
||||
});
|
||||
}
|
45
lib/vscode/extensions/emmet/src/matchTag.ts
Normal file
45
lib/vscode/extensions/emmet/src/matchTag.ts
Normal 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);
|
||||
}
|
54
lib/vscode/extensions/emmet/src/mergeLines.ts
Normal file
54
lib/vscode/extensions/emmet/src/mergeLines.ts
Normal 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);
|
||||
}
|
19
lib/vscode/extensions/emmet/src/node/emmetNodeMain.ts
Normal file
19
lib/vscode/extensions/emmet/src/node/emmetNodeMain.ts
Normal 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);
|
||||
}
|
54
lib/vscode/extensions/emmet/src/reflectCssValue.ts
Normal file
54
lib/vscode/extensions/emmet/src/reflectCssValue.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
66
lib/vscode/extensions/emmet/src/removeTag.ts
Normal file
66
lib/vscode/extensions/emmet/src/removeTag.ts
Normal 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;
|
||||
}
|
||||
|
37
lib/vscode/extensions/emmet/src/selectItem.ts
Normal file
37
lib/vscode/extensions/emmet/src/selectItem.ts
Normal 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]);
|
||||
}
|
211
lib/vscode/extensions/emmet/src/selectItemHTML.ts
Normal file
211
lib/vscode/extensions/emmet/src/selectItemHTML.ts
Normal 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;
|
||||
}
|
142
lib/vscode/extensions/emmet/src/selectItemStylesheet.ts
Normal file
142
lib/vscode/extensions/emmet/src/selectItemStylesheet.ts
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
|
62
lib/vscode/extensions/emmet/src/splitJoinTag.ts
Normal file
62
lib/vscode/extensions/emmet/src/splitJoinTag.ts
Normal 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);
|
||||
}
|
555
lib/vscode/extensions/emmet/src/test/abbreviationAction.test.ts
Normal file
555
lib/vscode/extensions/emmet/src/test/abbreviationAction.test.ts
Normal 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();
|
||||
});
|
||||
}
|
128
lib/vscode/extensions/emmet/src/test/completion.test.ts
Normal file
128
lib/vscode/extensions/emmet/src/test/completion.test.ts
Normal 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();
|
||||
});
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
40
lib/vscode/extensions/emmet/src/test/index.ts
Normal file
40
lib/vscode/extensions/emmet/src/test/index.ts
Normal 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;
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
89
lib/vscode/extensions/emmet/src/test/reflectCssValue.test.ts
Normal file
89
lib/vscode/extensions/emmet/src/test/reflectCssValue.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
404
lib/vscode/extensions/emmet/src/test/tagActions.test.ts
Normal file
404
lib/vscode/extensions/emmet/src/test/tagActions.test.ts
Normal 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
|
||||
});
|
||||
|
@ -0,0 +1 @@
|
||||
DO NOT DELETE, USED BY INTEGRATION TESTS
|
70
lib/vscode/extensions/emmet/src/test/testUtils.ts
Normal file
70
lib/vscode/extensions/emmet/src/test/testUtils.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
729
lib/vscode/extensions/emmet/src/test/toggleComment.test.ts
Normal file
729
lib/vscode/extensions/emmet/src/test/toggleComment.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
150
lib/vscode/extensions/emmet/src/test/updateImageSize.test.ts
Normal file
150
lib/vscode/extensions/emmet/src/test/updateImageSize.test.ts
Normal 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();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// });
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
218
lib/vscode/extensions/emmet/src/toggleComment.ts
Normal file
218
lib/vscode/extensions/emmet/src/toggleComment.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
95
lib/vscode/extensions/emmet/src/typings/EmmetNode.d.ts
vendored
Normal file
95
lib/vscode/extensions/emmet/src/typings/EmmetNode.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
13
lib/vscode/extensions/emmet/src/typings/emmetio__css-parser.d.ts
vendored
Normal file
13
lib/vscode/extensions/emmet/src/typings/emmetio__css-parser.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
13
lib/vscode/extensions/emmet/src/typings/emmetio__html-matcher.d.ts
vendored
Normal file
13
lib/vscode/extensions/emmet/src/typings/emmetio__html-matcher.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
13
lib/vscode/extensions/emmet/src/typings/emmetio__math-expression.d.ts
vendored
Normal file
13
lib/vscode/extensions/emmet/src/typings/emmetio__math-expression.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
23
lib/vscode/extensions/emmet/src/typings/image-size.d.ts
vendored
Normal file
23
lib/vscode/extensions/emmet/src/typings/image-size.d.ts
vendored
Normal 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;
|
||||
}
|
7
lib/vscode/extensions/emmet/src/typings/refs.d.ts
vendored
Normal file
7
lib/vscode/extensions/emmet/src/typings/refs.d.ts
vendored
Normal 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'/>
|
289
lib/vscode/extensions/emmet/src/updateImageSize.ts
Normal file
289
lib/vscode/extensions/emmet/src/updateImageSize.ts
Normal 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 wasn’t
|
||||
// changed and user didn’t 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 wasn’t
|
||||
// changed and user didn’t 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 wasn’t 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 node’s 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 '';
|
||||
}
|
||||
|
50
lib/vscode/extensions/emmet/src/updateTag.ts
Normal file
50
lib/vscode/extensions/emmet/src/updateTag.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
655
lib/vscode/extensions/emmet/src/util.ts
Normal file
655
lib/vscode/extensions/emmet/src/util.ts
Normal 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 ?? '';
|
||||
}
|
Reference in New Issue
Block a user