pull latest master
This commit is contained in:
Коммит
9fc3c07980
|
@ -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
|
15
package.json
15
package.json
|
@ -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,93 +4,138 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
export interface EmmetConfiguration {
|
||||
useNewEmmet: boolean;
|
||||
showExpandedAbbreviation: string;
|
||||
showAbbreviationSuggestions: boolean;
|
||||
syntaxProfiles: object;
|
||||
variables: object;
|
||||
}
|
||||
|
||||
public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable<vscode.CompletionList> {
|
||||
export function doComplete(document: TextDocument, position: Position, syntax: string, emmetConfig: EmmetConfiguration): 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)) {
|
||||
if (!emmetConfig.useNewEmmet || emmetConfig.showExpandedAbbreviation === 'never' || emmetModes.indexOf(syntax) === -1) {
|
||||
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 (!isStyleSheet(syntax)) {
|
||||
if (!snippetKeyCache.has(syntax)) {
|
||||
let registry = createSnippetsRegistry(syntax);
|
||||
let snippetKeys: string[] = registry.all({ type: 'string' }).map(snippet => {
|
||||
let registry = customSnippetRegistry[syntax] ? customSnippetRegistry[syntax] : createSnippetsRegistry(syntax);
|
||||
markupSnippetKeys = registry.all({ type: 'string' }).map(snippet => {
|
||||
return snippet.key;
|
||||
});
|
||||
snippetKeyCache.set(syntax, snippetKeys);
|
||||
snippetKeyCache.set(syntax, markupSnippetKeys);
|
||||
} else {
|
||||
markupSnippetKeys = snippetKeyCache.get(syntax);
|
||||
}
|
||||
}
|
||||
|
||||
let snippetKeys = snippetKeyCache.get(syntax);
|
||||
let snippetCompletions: vscode.CompletionItem[] = [];
|
||||
snippetKeys.forEach(snippetKey => {
|
||||
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 = expand(currentAbbr, getExpandOptions(syntax));
|
||||
let expandedAbbr;
|
||||
try {
|
||||
expandedAbbr = expand(currentAbbr, expandOptions);
|
||||
} catch (e) {
|
||||
|
||||
let item = new vscode.CompletionItem(snippetKey);
|
||||
item.documentation = this.makeCursorsGorgeous(expandedAbbr);
|
||||
}
|
||||
|
||||
let item = CompletionItem.create(snippetKey);
|
||||
item.documentation = removeTabStops(expandedAbbr);
|
||||
item.detail = 'Emmet Abbreviation';
|
||||
item.insertText = new vscode.SnippetString(expandedAbbr);
|
||||
item.range = abbreviationRange;
|
||||
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;
|
||||
|
@ -100,33 +145,59 @@ export class EmmetCompletionItemProvider implements vscode.CompletionItemProvide
|
|||
|
||||
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);
|
||||
function getAbbreviationSuggestions(syntax: string, prefix: string, abbreviation: string, abbreviationRange: Range, expandOptions: object): CompletionItem[] {
|
||||
if (!prefix || isStyleSheet(syntax)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return currentWord;
|
||||
let snippetKeys = snippetKeyCache.has(syntax) ? snippetKeyCache.get(syntax) : snippetKeyCache.get('html');
|
||||
let snippetCompletions = [];
|
||||
|
||||
return makeSnippetSuggestion(snippetKeys, prefix, abbreviation, abbreviationRange, expandOptions);;
|
||||
}
|
||||
|
||||
private makeCursorsGorgeous(expandedWord: string): string {
|
||||
// add one cursor automatically at the end
|
||||
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'];
|
||||
|
@ -136,15 +207,54 @@ export function isStyleSheet(syntax): boolean {
|
|||
/**
|
||||
* 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);
|
||||
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, ''];
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -154,7 +264,17 @@ export function extractAbbreviation(document: vscode.TextDocument, position: vsc
|
|||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,14 +282,22 @@ export function isAbbreviationValid(syntax: string, abbreviation: string): boole
|
|||
* @param syntax
|
||||
* @param textToReplace
|
||||
*/
|
||||
export function getExpandOptions(syntax: string, textToReplace?: string) {
|
||||
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: field,
|
||||
field: emmetSnippetField,
|
||||
syntax: syntax,
|
||||
profile: getProfile(syntax),
|
||||
addons: syntax === 'jsx' ? { 'jsx': true } : null,
|
||||
variables: getVariables(),
|
||||
text: textToReplace ? textToReplace : null
|
||||
profile: getProfile(syntax, syntaxProfiles),
|
||||
addons: addons,
|
||||
variables: getVariables(variables),
|
||||
snippets: customSnippetRegistry[syntax]
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -177,8 +305,10 @@ export function getExpandOptions(syntax: string, textToReplace?: string) {
|
|||
* 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'] || {};
|
||||
function getProfile(syntax: string, profilesFromSettings: object): any {
|
||||
if (!profilesFromSettings) {
|
||||
profilesFromSettings = {};
|
||||
}
|
||||
let profilesConfig = Object.assign({}, profilesFromFile, profilesFromSettings);
|
||||
|
||||
let options = profilesConfig[syntax];
|
||||
|
@ -203,10 +333,7 @@ export function getProfile(syntax: string): any {
|
|||
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];
|
||||
newOptions['format'] = (options[key] === true || options[key] === false) ? options[key] : true;
|
||||
break;
|
||||
case 'inline_break':
|
||||
newOptions['inlineBreak'] = options[key];
|
||||
|
@ -231,54 +358,118 @@ export function getProfile(syntax: string): any {
|
|||
/**
|
||||
* Returns variables to be used while expanding snippets
|
||||
*/
|
||||
export function getVariables(): any {
|
||||
let variablesFromSettings = vscode.workspace.getConfiguration('emmet')['variables'];
|
||||
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;
|
||||
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 dirPath = emmetExtensionsPath.trim();
|
||||
let snippetsPath = path.join(dirPath, 'snippets.json');
|
||||
let profilesPath = path.join(dirPath, 'syntaxProfiles.json');
|
||||
if (dirExists(dirPath)) {
|
||||
|
||||
let snippetsPromise = new Promise<void>((resolve, reject) => {
|
||||
fs.readFile(snippetsPath, (err, snippetsData) => {
|
||||
if (err) {
|
||||
return;
|
||||
return resolve();
|
||||
}
|
||||
try {
|
||||
let snippetsJson = JSON.parse(snippetsData.toString());
|
||||
variablesFromFile = snippetsJson['variables'];
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
fs.readFile(profilesPath, (err, profilesData) => {
|
||||
if (err) {
|
||||
customSnippetRegistry = {};
|
||||
snippetKeyCache.clear();
|
||||
Object.keys(snippetsJson).forEach(syntax => {
|
||||
if (!snippetsJson[syntax]['snippets']) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
profilesFromFile = JSON.parse(profilesData.toString());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
})
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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/**/*"
|
||||
]
|
||||
}
|
Загрузка…
Ссылка в новой задаче