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
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/**/*"
]
}