diff --git a/extensions/emmet/src/extension.ts b/extensions/emmet/src/extension.ts index 5bdb9338975..fb7bcb22f0f 100644 --- a/extensions/emmet/src/extension.ts +++ b/extensions/emmet/src/extension.ts @@ -17,7 +17,7 @@ import { fetchEditPoint } from './editPoint'; import { fetchSelectItem } from './selectItem'; import { evaluateMathExpression } from './evaluateMathExpression'; import { incrementDecrement } from './incrementDecrement'; -import { LANGUAGE_MODES, getMappingForIncludedLanguages, resolveUpdateExtensionsPath } from './util'; +import { LANGUAGE_MODES, getMappingForIncludedLanguages, updateEmmetExtensionsPath } from './util'; import { updateImageSize } from './updateImageSize'; import { reflectCssValue } from './reflectCssValue'; @@ -129,11 +129,15 @@ export function activate(context: vscode.ExtensionContext) { return reflectCssValue(); })); - resolveUpdateExtensionsPath(); + updateEmmetExtensionsPath(); - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { - registerCompletionProviders(context); - resolveUpdateExtensionsPath(); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('emmet.includeLanguages')) { + registerCompletionProviders(context); + } + if (e.affectsConfiguration('emmet.extensionsPath')) { + updateEmmetExtensionsPath(); + } })); } diff --git a/extensions/emmet/src/util.ts b/extensions/emmet/src/util.ts index c74fb6511e3..6e2531c001a 100644 --- a/extensions/emmet/src/util.ts +++ b/extensions/emmet/src/util.ts @@ -13,14 +13,19 @@ let _emmetHelper: any; let _currentExtensionsPath: string | undefined = undefined; export function getEmmetHelper() { + // Lazy load vscode-emmet-helper instead of importing it + // directly to reduce the start-up time of the extension if (!_emmetHelper) { _emmetHelper = require('vscode-emmet-helper'); } - resolveUpdateExtensionsPath(); + updateEmmetExtensionsPath(); return _emmetHelper; } -export function resolveUpdateExtensionsPath() { +/** + * Update Emmet Helper to use user snippets from the extensionsPath setting + */ +export function updateEmmetExtensionsPath() { if (!_emmetHelper) { return; } @@ -31,7 +36,10 @@ export function resolveUpdateExtensionsPath() { } } -export const LANGUAGE_MODES: any = { +/** + * Mapping between languages that support Emmet and completion trigger characters + */ +export const LANGUAGE_MODES: { [id: string]: string[] } = { 'html': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], 'jade': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], 'slim': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], @@ -47,19 +55,6 @@ export const LANGUAGE_MODES: any = { 'typescriptreact': ['!', '.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] }; -export const allowedMimeTypesInScriptTag = ['text/html', 'text/plain', 'text/x-template', 'text/template', 'text/ng-template']; - -const emmetModes = ['html', 'pug', 'slim', 'haml', 'xml', 'xsl', 'jsx', 'css', 'scss', 'sass', 'less', 'stylus']; - -// Explicitly map languages that have built-in grammar in VS Code to their parent language -// to get emmet completion support -// For other languages, users will have to use `emmet.includeLanguages` or -// language specific extensions can provide emmet completion support -export const MAPPED_MODES: Object = { - 'handlebars': 'html', - 'php': 'html' -}; - export function isStyleSheet(syntax: string): boolean { let stylesheetSyntaxes = ['css', 'scss', 'sass', 'less', 'stylus']; return (stylesheetSyntaxes.indexOf(syntax) > -1); @@ -78,6 +73,15 @@ export function validate(allowStylesheet: boolean = true): boolean { } export function getMappingForIncludedLanguages(): any { + // Explicitly map languages that have built-in grammar in VS Code to their parent language + // to get emmet completion support + // For other languages, users will have to use `emmet.includeLanguages` or + // language specific extensions can provide emmet completion support + const MAPPED_MODES: Object = { + 'handlebars': 'html', + 'php': 'html' + }; + const finalMappedModes = Object.create(null); let includeLanguagesConfig = vscode.workspace.getConfiguration('emmet')['includeLanguages']; let includeLanguages = Object.assign({}, MAPPED_MODES, includeLanguagesConfig ? includeLanguagesConfig : {}); @@ -111,6 +115,7 @@ export function getEmmetMode(language: string, excludedLanguages: string[]): str if (language === 'jade') { return 'pug'; } + const emmetModes = ['html', 'pug', 'slim', 'haml', 'xml', 'xsl', 'jsx', 'css', 'scss', 'sass', 'less', 'stylus']; if (emmetModes.indexOf(language) > -1) { return language; } @@ -137,6 +142,12 @@ const openBrace = 123; const slash = 47; const star = 42; +/** + * Traverse the given document backward & forward from given position + * to find a complete ruleset, then parse just that to return a Stylesheet + * @param document vscode.TextDocument + * @param position vscode.Position + */ export function parsePartialStylesheet(document: vscode.TextDocument, position: vscode.Position): Stylesheet | undefined { const isCSS = document.languageId === 'css'; let startPosition = new vscode.Position(0, 0); @@ -145,6 +156,25 @@ export function parsePartialStylesheet(document: vscode.TextDocument, position: const limitPosition = limitCharacter > 0 ? document.positionAt(limitCharacter) : startPosition; const stream = new DocumentStreamReader(document, position); + function findOpeningCommentBeforePosition(pos: vscode.Position): vscode.Position | undefined { + let text = document.getText(new vscode.Range(0, 0, pos.line, pos.character)); + let offset = text.lastIndexOf('/*'); + if (offset === -1) { + return; + } + return document.positionAt(offset); + } + + function findClosingCommentAfterPosition(pos: vscode.Position): vscode.Position | undefined { + let text = document.getText(new vscode.Range(pos.line, pos.character, document.lineCount - 1, document.lineAt(document.lineCount - 1).text.length)); + let offset = text.indexOf('*/'); + if (offset === -1) { + return; + } + offset += 2 + document.offsetAt(pos); + return document.positionAt(offset); + } + function consumeLineCommentBackwards() { if (!isCSS && currentLine !== stream.pos.line) { currentLine = stream.pos.line; @@ -158,7 +188,7 @@ export function parsePartialStylesheet(document: vscode.TextDocument, position: function consumeBlockCommentBackwards() { if (stream.peek() === slash) { if (stream.backUp(1) === star) { - stream.pos = findOpeningCommentBeforePosition(document, stream.pos) || startPosition; + stream.pos = findOpeningCommentBeforePosition(stream.pos) || startPosition; } else { stream.next(); } @@ -170,7 +200,7 @@ export function parsePartialStylesheet(document: vscode.TextDocument, position: if (stream.eat(slash) && !isCSS) { stream.pos = new vscode.Position(stream.pos.line + 1, 0); } else if (stream.eat(star)) { - stream.pos = findClosingCommentAfterPosition(document, stream.pos) || endPosition; + stream.pos = findClosingCommentAfterPosition(stream.pos) || endPosition; } } } @@ -264,25 +294,6 @@ export function parsePartialStylesheet(document: vscode.TextDocument, position: } } -function findOpeningCommentBeforePosition(document: vscode.TextDocument, position: vscode.Position): vscode.Position | undefined { - let text = document.getText(new vscode.Range(0, 0, position.line, position.character)); - let offset = text.lastIndexOf('/*'); - if (offset === -1) { - return; - } - return document.positionAt(offset); -} - -function findClosingCommentAfterPosition(document: vscode.TextDocument, position: vscode.Position): vscode.Position | undefined { - let text = document.getText(new vscode.Range(position.line, position.character, document.lineCount - 1, document.lineAt(document.lineCount - 1).text.length)); - let offset = text.indexOf('*/'); - if (offset === -1) { - return; - } - offset += 2 + document.offsetAt(position); - return document.positionAt(offset); -} - /** * Returns node corresponding to given position in the given root node */ @@ -311,11 +322,22 @@ export function getNode(root: Node | undefined, position: vscode.Position, inclu return foundNode; } +export const allowedMimeTypesInScriptTag = ['text/html', 'text/plain', 'text/x-template', 'text/template', 'text/ng-template']; + +/** + * Returns HTML node corresponding to given position in the given root node + * If position is inside a script tag of type template, then it will be parsed to find the inner HTML node as well + */ export function getHtmlNode(document: vscode.TextDocument, root: Node | undefined, position: vscode.Position, includeNodeBoundary: boolean): HtmlNode | undefined { let currentNode = getNode(root, position, includeNodeBoundary); if (!currentNode) { return; } - if (isTemplateScript(currentNode) && currentNode.close && + const isTemplateScript = currentNode.name === 'script' && + (currentNode.attributes && + currentNode.attributes.some(x => x.name.toString() === 'type' + && allowedMimeTypesInScriptTag.indexOf(x.value.toString()) > -1)); + + if (isTemplateScript && currentNode.close && (position.isAfter(currentNode.open.end) && position.isBefore(currentNode.close.start))) { let buffer = new DocumentStreamReader(document, currentNode.open.end, new vscode.Range(currentNode.open.end, currentNode.close.start)); @@ -340,6 +362,10 @@ export function getInnerRange(currentNode: HtmlNode): vscode.Range | undefined { return new vscode.Range(currentNode.open.end, currentNode.close.start); } +/** + * Returns the deepest non comment node under given node + * @param node + */ export function getDeepestNode(node: Node | undefined): Node | undefined { if (!node || !node.children || node.children.length === 0 || !node.children.find(x => x.type !== 'comment')) { return node; @@ -434,37 +460,32 @@ export function getNodesInBetween(node1: Node, node2: Node): Node[] { return [node1]; } - // Same parent - if (sameNodes(node1.parent, node2.parent)) { - return getNextSiblingsTillPosition(node1, node2.end); + // Not siblings + if (!sameNodes(node1.parent, node2.parent)) { + // node2 is ancestor of node1 + if (node2.start.isBefore(node1.start)) { + return [node2]; + } + + // node1 is ancestor of node2 + if (node2.start.isBefore(node1.end)) { + return [node1]; + } + + // Get the highest ancestor of node1 that should be commented + while (node1.parent && node1.parent.end.isBefore(node2.start)) { + node1 = node1.parent; + } + + // Get the highest ancestor of node2 that should be commented + while (node2.parent && node2.parent.start.isAfter(node1.start)) { + node2 = node2.parent; + } } - // node2 is ancestor of node1 - if (node2.start.isBefore(node1.start)) { - return [node2]; - } - - // node1 is ancestor of node2 - if (node2.start.isBefore(node1.end)) { - return [node1]; - } - - // Get the highest ancestor of node1 that should be commented - while (node1.parent && node1.parent.end.isBefore(node2.start)) { - node1 = node1.parent; - } - - // Get the highest ancestor of node2 that should be commented - while (node2.parent && node2.parent.start.isAfter(node1.start)) { - node2 = node2.parent; - } - - return getNextSiblingsTillPosition(node1, node2.end); -} - -function getNextSiblingsTillPosition(node: Node, position: vscode.Position): Node[] { - let siblings: Node[] = []; - let currentNode = node; + const siblings: Node[] = []; + let currentNode = node1; + const position = node2.end; while (currentNode && position.isAfter(currentNode.start)) { siblings.push(currentNode); currentNode = currentNode.nextSibling; @@ -589,9 +610,4 @@ export function isStyleAttribute(currentNode: Node | null, position: vscode.Posi return position.isAfterOrEqual(styleAttribute.value.start) && position.isBeforeOrEqual(styleAttribute.value.end); } -export function isTemplateScript(currentNode: HtmlNode): boolean { - return currentNode.name === 'script' && - (currentNode.attributes && - currentNode.attributes.some(x => x.name.toString() === 'type' - && allowedMimeTypesInScriptTag.indexOf(x.value.toString()) > -1)); -} +