This commit is contained in:
jens1o 2017-07-29 09:26:54 +02:00
Родитель e291a4596d 1184a6c6d4
Коммит 9fc3c07980
9 изменённых файлов: 762 добавлений и 5309 удалений

22
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Mocha Tests",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"args": [
"-u",
"tdd",
"--timeout",
"999999",
"--colors",
"${workspaceRoot}/out/emmetHelperTest.js"
]
}
]
}

Просмотреть файл

@ -3,9 +3,9 @@ A helper module to use emmet modules with Visual Studio Code
Visual Studio Code extensions that provide language service and want to provide emmet abbreviation expansions
in auto-complete can include this module and use the EmmetCompletionProvider.
Just pass the one of the emmet supported syntaxes that you would like the completion provider to use.
in auto-complete can include this module and use the `doComplete` method.
Just pass the one of the emmet supported syntaxes that you would like the completion provider to use along with other parameters that you would generally pass to a completion provider.
If `emmet.syntaxPofiles` has a mapping for your language, then the builit-in emmet extension will provide
If `emmet.includeLanguages` has a mapping for your language, then the builit-in emmet extension will provide
html emmet abbreviations. Ask the user to remove the mapping, if your extension decides to provide
emmet completions using this module

Просмотреть файл

@ -1,27 +1,30 @@
{
"name": "vscode-emmet-helper",
"version": "0.0.10",
"version": "1.0.0",
"description": "Helper to use emmet modules in Visual Studio Code",
"main": "./out/emmetHelper.js",
"author": "Microsoft Corporation",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/vscode-emmet-helper"
"url": "https://github.com/ramya-rao-a/vscode-emmet-helper"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/Microsoft/vscode-emmet-helper"
"url": "https://github.com/ramya-rao-a/vscode-emmet-helper"
},
"devDependencies": {
"@types/node": "^6.0.46",
"typescript": "^2.1.5"
"typescript": "^2.1.5",
"mocha": "3.4.2"
},
"dependencies": {
"@emmetio/expand-abbreviation": "^0.5.8",
"@emmetio/extract-abbreviation": "^0.1.1"
"@emmetio/extract-abbreviation": "^0.1.1",
"vscode-languageserver-types": "^3.0.3"
},
"scripts": {
"prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./"
"compile": "tsc -watch -p ./",
"test": "mocha out/emmetHelperTest.js"
}
}

Просмотреть файл

@ -4,281 +4,472 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { TextDocument, Position, Range, CompletionItem, CompletionList, TextEdit, InsertTextFormat } from 'vscode-languageserver-types'
import { expand, createSnippetsRegistry } from '@emmetio/expand-abbreviation';
import * as extract from '@emmetio/extract-abbreviation';
import * as path from 'path';
import * as fs from 'fs';
const snippetKeyCache = new Map<string, string[]>();
const htmlAbbreviationRegex = /^[a-z,A-Z,!,(,[,#,\.]/;
let markupSnippetKeys: string[];
const htmlAbbreviationStartRegex = /^[a-z,A-Z,!,(,[,#,\.]/;
const htmlAbbreviationEndRegex = /[a-z,A-Z,!,),\],#,\.,},\d,*,$]$/;
const cssAbbreviationRegex = /^[a-z,A-Z,!,@,#]/;
const emmetModes = ['html', 'pug', 'slim', 'haml', 'xml', 'xsl', 'jsx', 'css', 'scss', 'sass', 'less', 'stylus'];
const commonlyUsedTags = ['div', 'span', 'p', 'b', 'i', 'body', 'html', 'ul', 'ol', 'li', 'head', 'script', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section'];
const bemFilterSuffix = 'bem';
const filterDelimitor = '|';
export class EmmetCompletionItemProvider implements vscode.CompletionItemProvider {
private _syntax: string;
constructor(syntax: string) {
if (syntax) {
this._syntax = syntax;
}
}
public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable<vscode.CompletionList> {
let emmetConfig = vscode.workspace.getConfiguration('emmet');
if (!emmetConfig['useNewEmmet'] || !emmetConfig['showExpandedAbbreviation']) {
return Promise.resolve(null);
}
let [abbreviationRange, abbreviation] = extractAbbreviation(document, position);
if (!isAbbreviationValid(this._syntax, abbreviation)) {
return;
}
let expandedText = expand(abbreviation, getExpandOptions(this._syntax));
if (!expandedText) {
return;
}
let expandedAbbr = new vscode.CompletionItem(abbreviation);
expandedAbbr.insertText = new vscode.SnippetString(expandedText);
expandedAbbr.documentation = this.makeCursorsGorgeous(expandedText);
expandedAbbr.range = abbreviationRange;
expandedAbbr.detail = 'Emmet Abbreviation';
let completionItems: vscode.CompletionItem[] = expandedAbbr ? [expandedAbbr] : [];
if (!isStyleSheet(this._syntax)) {
// Workaround for the main expanded abbr not appearing before the snippet suggestions
expandedAbbr.sortText = '0' + expandedAbbr.label;
let currentWord = this.getCurrentWord(document, position);
let abbreviationSuggestions = this.getAbbreviationSuggestions(this._syntax, currentWord, abbreviation, abbreviationRange);
completionItems = completionItems.concat(abbreviationSuggestions);
} else {
// Temporary fix for https://github.com/Microsoft/vscode/issues/28933
expandedAbbr.filterText = abbreviation;
expandedAbbr.sortText = expandedAbbr.documentation;
expandedAbbr.label = expandedAbbr.documentation;
}
return Promise.resolve(new vscode.CompletionList(completionItems, true));
}
getAbbreviationSuggestions(syntax: string, prefix: string, abbreviation: string, abbreviationRange: vscode.Range): vscode.CompletionItem[] {
if (!vscode.workspace.getConfiguration('emmet')['showAbbreviationSuggestions'] || !prefix || !abbreviation) {
return [];
}
if (!snippetKeyCache.has(syntax)) {
let registry = createSnippetsRegistry(syntax);
let snippetKeys: string[] = registry.all({ type: 'string' }).map(snippet => {
return snippet.key;
});
snippetKeyCache.set(syntax, snippetKeys);
}
let snippetKeys = snippetKeyCache.get(syntax);
let snippetCompletions: vscode.CompletionItem[] = [];
snippetKeys.forEach(snippetKey => {
if (!snippetKey.startsWith(prefix) || snippetKey === prefix) {
return;
}
let currentAbbr = abbreviation + snippetKey.substr(prefix.length);
let expandedAbbr = expand(currentAbbr, getExpandOptions(syntax));
let item = new vscode.CompletionItem(snippetKey);
item.documentation = this.makeCursorsGorgeous(expandedAbbr);
item.detail = 'Emmet Abbreviation';
item.insertText = new vscode.SnippetString(expandedAbbr);
item.range = abbreviationRange;
// Workaround for snippet suggestions items getting filtered out as the complete abbr does not start with snippetKey
item.filterText = abbreviation;
// Workaround for the main expanded abbr not appearing before the snippet suggestions
item.sortText = '9' + abbreviation;
snippetCompletions.push(item);
});
return snippetCompletions;
}
private getCurrentWord(document: vscode.TextDocument, position: vscode.Position): string {
let wordAtPosition = document.getWordRangeAtPosition(position);
let currentWord = '';
if (wordAtPosition && wordAtPosition.start.character < position.character) {
let word = document.getText(wordAtPosition);
currentWord = word.substr(0, position.character - wordAtPosition.start.character);
}
return currentWord;
}
private makeCursorsGorgeous(expandedWord: string): string {
// add one cursor automatically at the end
return expandedWord.replace(/\$\{\d+\}/g, '|').replace(/\$\{\d+:([^\}]+)\}/g, '_$1_') + '|';
}
export interface EmmetConfiguration {
useNewEmmet: boolean;
showExpandedAbbreviation: string;
showAbbreviationSuggestions: boolean;
syntaxProfiles: object;
variables: object;
}
export function doComplete(document: TextDocument, position: Position, syntax: string, emmetConfig: EmmetConfiguration): CompletionList {
if (!emmetConfig.useNewEmmet || emmetConfig.showExpandedAbbreviation === 'never' || emmetModes.indexOf(syntax) === -1) {
return;
}
if (!isStyleSheet(syntax)) {
if (!snippetKeyCache.has(syntax)) {
let registry = customSnippetRegistry[syntax] ? customSnippetRegistry[syntax] : createSnippetsRegistry(syntax);
markupSnippetKeys = registry.all({ type: 'string' }).map(snippet => {
return snippet.key;
});
snippetKeyCache.set(syntax, markupSnippetKeys);
} else {
markupSnippetKeys = snippetKeyCache.get(syntax);
}
}
let expandedAbbr: CompletionItem;
let { abbreviationRange, abbreviation, filters } = extractAbbreviation(document, position);
let expandOptions = getExpandOptions(syntax, emmetConfig.syntaxProfiles, emmetConfig.variables, filters);
if (isAbbreviationValid(syntax, abbreviation)) {
let expandedText;
// Skip non stylesheet abbreviations that are just letters/numbers unless they are valid snippets or commonly used tags
// This is to avoid noise where abc -> <abc>${1}</abc>
// Also skip abbreviations ending with `.` This will be noise when people are typing simple text and ending it with period.
if (isStyleSheet(syntax)
|| (!/^[a-z,A-Z,\d]*$/.test(abbreviation) && !abbreviation.endsWith('.'))
|| markupSnippetKeys.indexOf(abbreviation) > -1
|| commonlyUsedTags.indexOf(abbreviation) > -1) {
try {
expandedText = expand(abbreviation, expandOptions);
// Skip cases when abc -> abc: ; as this is noise
if (isStyleSheet(syntax) && expandedText === `${abbreviation}: \${1};`) {
expandedText = '';
}
} catch (e) {
}
}
if (expandedText) {
expandedAbbr = CompletionItem.create(abbreviation);
expandedAbbr.textEdit = TextEdit.replace(abbreviationRange, expandedText);
expandedAbbr.documentation = removeTabStops(expandedText);
expandedAbbr.insertTextFormat = InsertTextFormat.Snippet;
expandedAbbr.detail = 'Emmet Abbreviation';
if (filters.indexOf('bem') > -1) {
expandedAbbr.label = abbreviation + filterDelimitor + bemFilterSuffix;
}
if (isStyleSheet(syntax)) {
// See https://github.com/Microsoft/vscode/issues/28933#issuecomment-309236902
// Due to this we set filterText, sortText and label to expanded abbreviation
// - Label makes it clear to the user what their choice is
// - FilterText fixes the issue when user types in propertyname and emmet uses it to match with abbreviations
// - SortText will sort the choice in a way that is intutive to the user
expandedAbbr.filterText = expandedAbbr.documentation;
expandedAbbr.sortText = expandedAbbr.documentation;
expandedAbbr.label = expandedAbbr.documentation;
return CompletionList.create([expandedAbbr], true);
}
}
}
let completionItems: CompletionItem[] = expandedAbbr ? [expandedAbbr] : [];
if (!isStyleSheet(syntax)) {
if (expandedAbbr) {
// Workaround for the main expanded abbr not appearing before the snippet suggestions
expandedAbbr.sortText = '0' + expandedAbbr.label;
}
let currentWord = getCurrentWord(document, position);
let commonlyUsedTagSuggestions = makeSnippetSuggestion(commonlyUsedTags, currentWord, abbreviation, abbreviationRange, expandOptions);
completionItems = completionItems.concat(commonlyUsedTagSuggestions);
if (emmetConfig.showAbbreviationSuggestions) {
let abbreviationSuggestions = getAbbreviationSuggestions(syntax, currentWord, abbreviation, abbreviationRange, expandOptions);
completionItems = completionItems.concat(abbreviationSuggestions);
}
}
return CompletionList.create(completionItems, true);
}
function makeSnippetSuggestion(snippets: string[], prefix: string, abbreviation: string, abbreviationRange: Range, expandOptions: any): CompletionItem[] {
if (!prefix) {
return [];
}
let snippetCompletions = [];
snippets.forEach(snippetKey => {
if (!snippetKey.startsWith(prefix) || snippetKey === prefix) {
return;
}
let currentAbbr = abbreviation + snippetKey.substr(prefix.length);
let expandedAbbr;
try {
expandedAbbr = expand(currentAbbr, expandOptions);
} catch (e) {
}
let item = CompletionItem.create(snippetKey);
item.documentation = removeTabStops(expandedAbbr);
item.detail = 'Emmet Abbreviation';
item.textEdit = TextEdit.replace(abbreviationRange, expandedAbbr);
item.insertTextFormat = InsertTextFormat.Snippet;
// Workaround for snippet suggestions items getting filtered out as the complete abbr does not start with snippetKey
item.filterText = abbreviation;
// Workaround for the main expanded abbr not appearing before the snippet suggestions
item.sortText = '9' + abbreviation;
snippetCompletions.push(item);
});
return snippetCompletions;
}
function getAbbreviationSuggestions(syntax: string, prefix: string, abbreviation: string, abbreviationRange: Range, expandOptions: object): CompletionItem[] {
if (!prefix || isStyleSheet(syntax)) {
return [];
}
let snippetKeys = snippetKeyCache.has(syntax) ? snippetKeyCache.get(syntax) : snippetKeyCache.get('html');
let snippetCompletions = [];
return makeSnippetSuggestion(snippetKeys, prefix, abbreviation, abbreviationRange, expandOptions);;
}
function getCurrentWord(document: TextDocument, position: Position): string {
let currentLine = getCurrentLine(document, position);
if (currentLine) {
let matches = currentLine.match(/[\w,:]*$/);
if (matches) {
return matches[0];
}
}
}
function removeTabStops(expandedWord: string): string {
return expandedWord.replace(/\$\{\d+\}/g, '|').replace(/\$\{\d+:([^\}]+)\}/g, '_$1_') + '|';
}
function getCurrentLine(document: TextDocument, position: Position): string {
let offset = document.offsetAt(position);
let text = document.getText();
let start = 0;
let end = text.length;
for (let i = offset - 1; i >= 0; i--) {
if (text[i] === '\n') {
start = i + 1;
break;
}
}
for (let i = offset; i < text.length; i++) {
if (text[i] === '\n') {
end = i;
break;
}
}
return text.substring(start, end);
}
let customSnippetRegistry = {};
let variablesFromFile = {};
let profilesFromFile = {};
let emmetExtensionsPath = '';
const field = (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}`;
export const emmetSnippetField = (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}`;
export function isStyleSheet(syntax): boolean {
let stylesheetSyntaxes = ['css', 'scss', 'sass', 'less', 'stylus'];
return (stylesheetSyntaxes.indexOf(syntax) > -1);
let stylesheetSyntaxes = ['css', 'scss', 'sass', 'less', 'stylus'];
return (stylesheetSyntaxes.indexOf(syntax) > -1);
}
/**
* Extracts abbreviation from the given position in the given document
*/
export function extractAbbreviation(document: vscode.TextDocument, position: vscode.Position): [vscode.Range, string] {
let currentLine = document.lineAt(position.line).text;
let result = extract(currentLine, position.character, true);
if (!result) {
return [null, ''];
}
* Extracts abbreviation from the given position in the given document
*/
export function extractAbbreviation(document: TextDocument, position: Position) {
let filters = [];
let pos = position.character;
let currentLine = getCurrentLine(document, position);
let currentLineTillPosition = currentLine.substr(0, position.character);
let lengthOccupiedByFilter = 0;
if (currentLineTillPosition.endsWith(`${filterDelimitor}${bemFilterSuffix}`)) {
lengthOccupiedByFilter = 4;
pos -= lengthOccupiedByFilter;
filters.push(bemFilterSuffix)
}
let result;
try {
result = extract(currentLine, pos, true);
}
catch (e) {
}
if (!result) {
return null;
}
let rangeToReplace = Range.create(position.line, result.location, position.line, result.location + result.abbreviation.length + lengthOccupiedByFilter);
return {
abbreviationRange: rangeToReplace,
abbreviation: result.abbreviation,
filters
};
}
let rangeToReplace = new vscode.Range(position.line, result.location, position.line, result.location + result.abbreviation.length);
return [rangeToReplace, result.abbreviation];
export function extractAbbreviationFromText(text: string): any {
let filters = [];
let pos = text.length;
if (text.endsWith(`${filterDelimitor}${bemFilterSuffix}`)) {
pos -= 4;
filters.push(bemFilterSuffix)
}
let result;
try {
result = extract(text, pos, true);
}
catch (e) {
}
if (!result) {
return null;
}
return {
abbreviation: result.abbreviation,
filters
};
}
/**
* Returns a boolean denoting validity of given abbreviation in the context of given syntax
* Not needed once https://github.com/emmetio/atom-plugin/issues/22 is fixed
* @param syntax string
* @param abbreviation string
*/
* Returns a boolean denoting validity of given abbreviation in the context of given syntax
* Not needed once https://github.com/emmetio/atom-plugin/issues/22 is fixed
* @param syntax string
* @param abbreviation string
*/
export function isAbbreviationValid(syntax: string, abbreviation: string): boolean {
return isStyleSheet(syntax) ? htmlAbbreviationRegex.test(abbreviation) : cssAbbreviationRegex.test(abbreviation);
if (isStyleSheet(syntax)) {
return cssAbbreviationRegex.test(abbreviation);
}
if (abbreviation.startsWith('!') && /[^!]/.test(abbreviation)) {
return false;
}
// Its common for users to type (sometextinsidebrackets), this should not be treated as an abbreviation
if (abbreviation.startsWith('(') && abbreviation.endsWith(')') && !/^\(.+[>,+,*].+\)$/.test(abbreviation)) {
return false;
}
return (htmlAbbreviationStartRegex.test(abbreviation) && htmlAbbreviationEndRegex.test(abbreviation));
}
/**
* Returns options to be used by the expand module
* @param syntax
* @param textToReplace
*/
export function getExpandOptions(syntax: string, textToReplace?: string) {
return {
field: field,
syntax: syntax,
profile: getProfile(syntax),
addons: syntax === 'jsx' ? { 'jsx': true } : null,
variables: getVariables(),
text: textToReplace ? textToReplace : null
};
* Returns options to be used by the expand module
* @param syntax
* @param textToReplace
*/
export function getExpandOptions(syntax: string, syntaxProfiles?: object, variables?: object, filters?: string[], ) {
let baseSyntax = isStyleSheet(syntax) ? 'css' : 'html';
if (!customSnippetRegistry[syntax] && customSnippetRegistry[baseSyntax]) {
customSnippetRegistry[syntax] = customSnippetRegistry[baseSyntax];
}
let addons = syntax === 'jsx' ? { 'jsx': true } : {};
if (filters && filters.indexOf('bem') > -1) {
addons['bem'] = { element: '__' };
}
return {
field: emmetSnippetField,
syntax: syntax,
profile: getProfile(syntax, syntaxProfiles),
addons: addons,
variables: getVariables(variables),
snippets: customSnippetRegistry[syntax]
};
}
/**
* Maps and returns syntaxProfiles of previous format to ones compatible with new emmet modules
* @param syntax
*/
export function getProfile(syntax: string): any {
let profilesFromSettings = vscode.workspace.getConfiguration('emmet')['syntaxProfiles'] || {};
let profilesConfig = Object.assign({}, profilesFromFile, profilesFromSettings);
* Maps and returns syntaxProfiles of previous format to ones compatible with new emmet modules
* @param syntax
*/
function getProfile(syntax: string, profilesFromSettings: object): any {
if (!profilesFromSettings) {
profilesFromSettings = {};
}
let profilesConfig = Object.assign({}, profilesFromFile, profilesFromSettings);
let options = profilesConfig[syntax];
if (!options || typeof options === 'string') {
if (options === 'xhtml') {
return {
selfClosingStyle: 'xhtml'
};
}
return {};
}
let newOptions = {};
for (let key in options) {
switch (key) {
case 'tag_case':
newOptions['tagCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : '';
break;
case 'attr_case':
newOptions['attributeCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : '';
break;
case 'attr_quotes':
newOptions['attributeQuotes'] = options[key];
break;
case 'tag_nl':
newOptions['format'] = (options[key] === 'true' || options[key] === 'false') ? options[key] : 'true';
break;
case 'indent':
newOptions['attrCase'] = (options[key] === 'true' || options[key] === 'false') ? '\t' : options[key];
break;
case 'inline_break':
newOptions['inlineBreak'] = options[key];
break;
case 'self_closing_tag':
if (options[key] === true) {
newOptions['selfClosingStyle'] = 'xml'; break;
}
if (options[key] === false) {
newOptions['selfClosingStyle'] = 'html'; break;
}
newOptions['selfClosingStyle'] = options[key];
break;
default:
newOptions[key] = options[key];
break;
}
}
return newOptions;
let options = profilesConfig[syntax];
if (!options || typeof options === 'string') {
if (options === 'xhtml') {
return {
selfClosingStyle: 'xhtml'
};
}
return {};
}
let newOptions = {};
for (let key in options) {
switch (key) {
case 'tag_case':
newOptions['tagCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : '';
break;
case 'attr_case':
newOptions['attributeCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : '';
break;
case 'attr_quotes':
newOptions['attributeQuotes'] = options[key];
break;
case 'tag_nl':
newOptions['format'] = (options[key] === true || options[key] === false) ? options[key] : true;
break;
case 'inline_break':
newOptions['inlineBreak'] = options[key];
break;
case 'self_closing_tag':
if (options[key] === true) {
newOptions['selfClosingStyle'] = 'xml'; break;
}
if (options[key] === false) {
newOptions['selfClosingStyle'] = 'html'; break;
}
newOptions['selfClosingStyle'] = options[key];
break;
default:
newOptions[key] = options[key];
break;
}
}
return newOptions;
}
/**
* Returns variables to be used while expanding snippets
*/
export function getVariables(): any {
let variablesFromSettings = vscode.workspace.getConfiguration('emmet')['variables'];
return Object.assign({}, variablesFromFile, variablesFromSettings);
* Returns variables to be used while expanding snippets
*/
function getVariables(variablesFromSettings: object): any {
if (!variablesFromSettings) {
return variablesFromFile;
}
return Object.assign({}, variablesFromFile, variablesFromSettings);
}
/**
* Updates customizations from snippets.json and syntaxProfiles.json files in the directory configured in emmet.extensionsPath setting
*/
export function updateExtensionsPath() {
let currentEmmetExtensionsPath = vscode.workspace.getConfiguration('emmet')['extensionsPath'];
if (emmetExtensionsPath !== currentEmmetExtensionsPath) {
emmetExtensionsPath = currentEmmetExtensionsPath;
* Updates customizations from snippets.json and syntaxProfiles.json files in the directory configured in emmet.extensionsPath setting
*/
export function updateExtensionsPath(emmetExtensionsPath: string): Promise<void> {
if (!emmetExtensionsPath || !emmetExtensionsPath.trim() || !path.isAbsolute(emmetExtensionsPath.trim()) || !dirExists(emmetExtensionsPath.trim())) {
customSnippetRegistry = {};
snippetKeyCache.clear();
profilesFromFile = {};
variablesFromFile = {};
return Promise.resolve();
}
if (emmetExtensionsPath && emmetExtensionsPath.trim()) {
let dirPath = path.isAbsolute(emmetExtensionsPath) ? emmetExtensionsPath : path.join(vscode.workspace.rootPath, emmetExtensionsPath);
let snippetsPath = path.join(dirPath, 'snippets.json');
let profilesPath = path.join(dirPath, 'syntaxProfiles.json');
if (dirExists(dirPath)) {
fs.readFile(snippetsPath, (err, snippetsData) => {
if (err) {
return;
}
try {
let snippetsJson = JSON.parse(snippetsData.toString());
variablesFromFile = snippetsJson['variables'];
} catch (e) {
let dirPath = emmetExtensionsPath.trim();
let snippetsPath = path.join(dirPath, 'snippets.json');
let profilesPath = path.join(dirPath, 'syntaxProfiles.json');
}
});
fs.readFile(profilesPath, (err, profilesData) => {
if (err) {
return;
}
try {
profilesFromFile = JSON.parse(profilesData.toString());
} catch (e) {
let snippetsPromise = new Promise<void>((resolve, reject) => {
fs.readFile(snippetsPath, (err, snippetsData) => {
if (err) {
return resolve();
}
try {
let snippetsJson = JSON.parse(snippetsData.toString());
variablesFromFile = snippetsJson['variables'];
customSnippetRegistry = {};
snippetKeyCache.clear();
Object.keys(snippetsJson).forEach(syntax => {
if (!snippetsJson[syntax]['snippets']) {
return;
}
let baseSyntax = isStyleSheet(syntax) ? 'css' : 'html';
let customSnippets = snippetsJson[syntax]['snippets'];
if (snippetsJson[baseSyntax]['snippets'] && baseSyntax !== syntax) {
customSnippets = Object.assign({}, snippetsJson[baseSyntax]['snippets'], snippetsJson[syntax]['snippets'])
}
customSnippetRegistry[syntax] = createSnippetsRegistry(syntax, customSnippets);
let snippetKeys: string[] = customSnippetRegistry[syntax].all({ type: 'string' }).map(snippet => {
return snippet.key;
});
snippetKeyCache.set(syntax, snippetKeys);
});
} catch (e) {
}
return resolve();
});
});
let variablesPromise = new Promise<void>((resolve, reject) => {
fs.readFile(profilesPath, (err, profilesData) => {
try {
if (!err) {
profilesFromFile = JSON.parse(profilesData.toString());
}
} catch (e) {
}
return resolve();
});
});
return Promise.all([snippetsPromise, variablesFromFile]).then(() => Promise.resolve());
}
});
}
}
}
}
function dirExists(dirPath: string): boolean {
try {
return fs.statSync(dirPath).isDirectory();
} catch (e) {
return false;
}
try {
return fs.statSync(dirPath).isDirectory();
} catch (e) {
return false;
}
}
/**
* Get the corresponding emmet mode for given vscode language mode
* Eg: jsx for typescriptreact/javascriptreact or pug for jade
* If the language is not supported by emmet or has been exlcuded via `exlcudeLanguages` setting,
* then nothing is returned
*
* @param language
* @param exlcudedLanguages Array of language ids that user has chosen to exlcude for emmet
*/
export function getEmmetMode(language: string, excludedLanguages: string[]): string {
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';
}
if (emmetModes.indexOf(language) > -1) {
return language;
}
}

286
src/emmetHelperTest.ts Normal file
Просмотреть файл

@ -0,0 +1,286 @@
import { TextDocument, Position } from 'vscode-languageserver-types'
import { isAbbreviationValid, extractAbbreviation, extractAbbreviationFromText, getExpandOptions, emmetSnippetField, updateExtensionsPath, doComplete } from './emmetHelper';
import { describe, it } from 'mocha';
import * as assert from 'assert';
import * as path from 'path';
const extensionsPath = path.join(path.normalize(path.join(__dirname, '..')), 'testData');
describe('Validate Abbreviations', () => {
it('should return true for valid abbreivations', () => {
const htmlAbbreviations = ['ul>li', 'ul', 'h1', 'ul>li*3', '(ul>li)+div', '.hello', '!', '#hello', '.item[id=ok]'];
htmlAbbreviations.forEach(abbr => {
assert(isAbbreviationValid('html', abbr));
});
htmlAbbreviations.forEach(abbr => {
assert(isAbbreviationValid('haml', abbr));
});
});
it('should return false for invalid abbreivations', () => {
const htmlAbbreviations = ['!ul!', '(hello)'];
const cssAbbreviations = ['123'];
htmlAbbreviations.forEach(abbr => {
assert(!isAbbreviationValid('html', abbr));
});
htmlAbbreviations.forEach(abbr => {
assert(!isAbbreviationValid('haml', abbr));
});
cssAbbreviations.forEach(abbr => {
assert(!isAbbreviationValid('css', abbr));
});
cssAbbreviations.forEach(abbr => {
assert(!isAbbreviationValid('scss', abbr));
});
})
});
describe('Extract Abbreviations', () => {
it('should extract abbreviations from document', () => {
const testCases: [string, number, number, string, number, number, number, number, string[]][] = [
['<div>ul>li*3</div>', 0, 7, 'ul', 0, 5, 0, 7,[]],
['<div>ul>li*3</div>', 0, 10, 'ul>li', 0, 5, 0, 10,[]],
['<div>ul>li*3</div>', 0, 12, 'ul>li*3', 0, 5, 0, 12,[]],
['ul>li', 0, 5, 'ul>li', 0, 0, 0, 5,[]],
['ul>li|bem', 0, 9, 'ul>li', 0, 0, 0, 9,['bem']]
]
testCases.forEach(([content, positionLine, positionChar, expectedAbbr, expectedRangeStartLine, expectedRangeStartChar, expectedRangeEndLine, expectedRangeEndChar, expectedFilters]) => {
const document = TextDocument.create('test://test/test.html', 'html', 0, content);
const position = Position.create(positionLine, positionChar);
const {abbreviationRange, abbreviation, filters} = extractAbbreviation(document, position);
assert.equal(expectedAbbr, abbreviation);
assert.equal(expectedRangeStartLine, abbreviationRange.start.line);
assert.equal(expectedRangeStartChar, abbreviationRange.start.character);
assert.equal(expectedRangeEndLine, abbreviationRange.end.line);
assert.equal(expectedRangeEndChar, abbreviationRange.end.character);
assert.equal(filters.length, expectedFilters.length);
for(let i = 0; i < filters.length; i++) {
assert.equal(filters[i], expectedFilters[i]);
}
});
});
it('should extract abbreviations from text', () => {
const testCases: [string, string, string[]][] = [
['ul', 'ul', []],
['ul>li', 'ul>li', []],
['ul>li*3', 'ul>li*3', []],
['ul>li|bem', 'ul>li', ['bem']]
]
testCases.forEach(([content, expectedAbbr, expectedFilters]) => {
const {abbreviation, filters} = extractAbbreviationFromText(content);
assert.equal(expectedAbbr, abbreviation);
assert.equal(filters.length, expectedFilters.length);
for(let i = 0; i < filters.length; i++) {
assert.equal(filters[i], expectedFilters[i]);
}
});
});
});
describe('Test Basic Expand Options', () => {
it('should check for basic expand options', () => {
const textToReplace = 'textToReplace';
const syntax = 'anythingreally';
let expandOptions = getExpandOptions(syntax);
assert.equal(expandOptions.field, emmetSnippetField)
assert.equal(expandOptions.syntax, syntax);
});
});
describe('Test output profile settings', () => {
it('should convert output profile from old format to new', () => {
const profile = {
tag_case: 'lower',
attr_case: 'lower',
attr_quotes: 'single',
tag_nl: true,
inline_break: 2,
self_closing_tag: 'xhtml'
}
const expandOptions = getExpandOptions('html', { html: profile });
assert.equal(profile['tag_case'], expandOptions.profile['tagCase']);
assert.equal(profile['attr_case'], expandOptions.profile['attributeCase']);
assert.equal(profile['attr_quotes'], expandOptions.profile['attributeQuotes']);
assert.equal(profile['tag_nl'], expandOptions.profile['format']);
assert.equal(profile['inline_break'], expandOptions.profile['inlineBreak']);
assert.equal(profile['self_closing_tag'], expandOptions.profile['selfClosingStyle']);
});
it('should convert self_closing_style', () => {
const testCases = [true, false, 'xhtml'];
const expectedValue = ['xml', 'html', 'xhtml'];
for (let i = 0; i < testCases.length; i++) {
const expandOptions = getExpandOptions('html', { html: { self_closing_tag: testCases[i] } });
assert.equal(expandOptions.profile['selfClosingStyle'], expectedValue[i]);
}
});
it('should convert tag_nl', () => {
const testCases = [true, false, 'decide'];
const expectedValue = [true, false, true];
for (let i = 0; i < testCases.length; i++) {
const expandOptions = getExpandOptions('html', { html: { tag_nl: testCases[i] } });
assert.equal(expandOptions.profile['format'], expectedValue[i]);
}
});
it('shoud use output profile in new format as is', () => {
const profile = {
tagCase: 'lower',
attributeCase: 'lower',
attributeQuotes: 'single',
format: true,
inlineBreak: 2,
selfClosingStyle: 'xhtml'
};
const expandOptions = getExpandOptions('html', { html: profile });
Object.keys(profile).forEach(key => {
assert.equal(expandOptions.profile[key], profile[key]);
});
});
it('should use profile from extensionsPath', () => {
updateExtensionsPath(extensionsPath).then(() => {
const profile = {
tag_case: 'lower',
attr_case: 'lower',
attr_quotes: 'single',
tag_nl: true,
inline_break: 2,
self_closing_tag: 'xhtml'
}
const expandOptions = getExpandOptions('html', { html: profile });
assert.equal(expandOptions.profile['tagCase'], 'upper');
assert.equal(profile['tag_case'], 'lower');
});
});
});
describe('Test variables settings', () => {
it('should take in variables as is', () => {
const variables = {
lang: 'de',
charset: 'UTF-8'
}
const expandOptions = getExpandOptions('html', {}, variables);
Object.keys(variables).forEach(key => {
assert.equal(expandOptions.variables[key], variables[key]);
});
});
it('should use variables from extensionsPath', () => {
updateExtensionsPath(extensionsPath).then(() => {
const variables = {
lang: 'en',
charset: 'UTF-8'
}
const expandOptions = getExpandOptions('html', {}, variables);
assert.equal(expandOptions.variables['lang'], 'fr');
assert.equal(variables['lang'], 'en');
});
});
});
describe('Test custom snippets', () => {
it('should use custom snippets from extensionsPath', () => {
const customSnippetKey = 'ch';
updateExtensionsPath(null).then(() => {
const expandOptionsWithoutCustomSnippets = getExpandOptions('css');
assert(!expandOptionsWithoutCustomSnippets.snippets);
// Use custom snippets from extensionsPath
updateExtensionsPath(extensionsPath).then(() => {
let foundCustomSnippet = false;
let foundCustomSnippetInInhertitedSyntax = false;
const expandOptionsWithCustomSnippets = getExpandOptions('css');
const expandOptionsWithCustomSnippetsInhertedSytnax = getExpandOptions('scss');
expandOptionsWithoutCustomSnippets.snippets.all({ type: 'string' }).forEach(snippet => {
if (snippet.key === customSnippetKey) {
foundCustomSnippet = true;
}
});
expandOptionsWithCustomSnippetsInhertedSytnax.snippets.all({ type: 'string' }).forEach(snippet => {
if (snippet.key === customSnippetKey) {
foundCustomSnippet = true;
}
});
assert.equal(foundCustomSnippet, true);
assert.equal(foundCustomSnippetInInhertitedSyntax, true);
});
});
});
});
describe('Test completions', () => {
it('should provide completions', () => {
updateExtensionsPath(null).then(() => {
const testCases: [string, number, number, string, string, number, number, number, number][] = [
['<div>ul>li*3</div>', 0, 7, 'ul', '<ul></ul>', 0, 5, 0, 7],
['<div>ul>li*3</div>', 0, 10, 'ul>li', '<ul>\n\t<li></li>\n</ul>', 0, 5, 0, 10]
];
testCases.forEach(([content, positionLine, positionChar, expectedAbbr, expectedExpansion, expectedRangeStartLine, expectedRangeStartChar, expectedRangeEndLine, expectedRangeEndChar]) => {
const document = TextDocument.create('test://test/test.html', 'html', 0, content);
const position = Position.create(positionLine, positionChar);
const completionList = doComplete(document, position, 'html', {
useNewEmmet: true,
showExpandedAbbreviation: 'always',
showAbbreviationSuggestions: false,
syntaxProfiles: {},
variables: {}
});
assert.equal(completionList.items[0].label, expectedAbbr);
assert.equal(completionList.items[0].documentation, expectedExpansion);
});
});
});
it('should not provide completions', () => {
updateExtensionsPath(null).then(() => {
const testCases: [string, number, number][] = [
['<div>abc</div>', 0, 8],
['<div>abc12</div>', 0, 10],
['<div>abc.</div>', 0, 9],
['<div>(div)</div>', 0, 10]
];
testCases.forEach(([content, positionLine, positionChar]) => {
const document = TextDocument.create('test://test/test.html', 'html', 0, content);
const position = Position.create(positionLine, positionChar);
const completionList = doComplete(document, position, 'html', {
useNewEmmet: true,
showExpandedAbbreviation: 'always',
showAbbreviationSuggestions: false,
syntaxProfiles: {},
variables: {}
});
assert.equal(completionList.items.length, 0);
});
});
});
})

5061
src/typings/vscode.d.ts поставляемый

Разница между файлами не показана из-за своего большого размера Загрузить разницу

10
testData/snippets.json Normal file
Просмотреть файл

@ -0,0 +1,10 @@
{
"css": {
"snippets": {
"ch": "color:hsl(${1:0}, ${2:100}%, ${3:50}%);"
}
},
"variables": {
"lang": "fr"
}
}

Просмотреть файл

@ -0,0 +1,5 @@
{
"html": {
"tag_case": "upper"
}
}

Просмотреть файл

@ -10,8 +10,5 @@
},
"exclude": [
"node_modules"
],
"include": [
"src/**/*"
]
}