From 15ba9aee0e973b80a8cca63ffc4879858eab0dad Mon Sep 17 00:00:00 2001 From: Raymond Zhao Date: Tue, 29 Dec 2020 16:15:55 -0800 Subject: [PATCH] Emmet improve Expand Abbreviation perf (#113558) --- extensions/emmet/src/abbreviationActions.ts | 85 +++++++----- .../emmet/src/defaultCompletionProvider.ts | 18 +-- .../src/test/partialParsingStylesheet.test.ts | 9 +- extensions/emmet/src/util.ts | 125 +++--------------- 4 files changed, 82 insertions(+), 155 deletions(-) diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index a37741b8e86..a76a46cef23 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetNode'; -import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util'; +import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetFlatNode'; +import { getEmmetHelper, getFlatNode, getMappingForIncludedLanguages, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util'; +import { getRootNode as parseDocument } from './parseDocument'; const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/; const hexColorRegex = /^#[\da-fA-F]{0,6}$/; @@ -59,7 +60,7 @@ function doWrapping(individualLines: boolean, args: any) { args['language'] = editor.document.languageId; } const syntax = getSyntaxFromArgs(args) || 'html'; - const rootNode = parseDocument(editor.document, false); + const rootNode = parseDocument(editor.document, true); let inPreview = false; let currentValue = ''; @@ -68,31 +69,40 @@ function doWrapping(individualLines: boolean, args: any) { // Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents const rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => { let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection; + const document = editor.document; if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) { const previousLine = rangeToReplace.end.line - 1; - const lastChar = editor.document.lineAt(previousLine).text.length; + const lastChar = document.lineAt(previousLine).text.length; rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar)); } else if (rangeToReplace.isEmpty) { const { active } = selection; - const currentNode = getNode(rootNode, active, true); - if (currentNode && (currentNode.start.line === active.line || currentNode.end.line === active.line)) { - rangeToReplace = new vscode.Range(currentNode.start, currentNode.end); + const activeOffset = document.offsetAt(active); + const currentNode = getFlatNode(rootNode, activeOffset, true); + if (currentNode) { + const currentNodeStart = document.positionAt(currentNode.start); + const currentNodeEnd = document.positionAt(currentNode.end); + if (currentNodeStart.line === active.line || currentNodeEnd.line === active.line) { + rangeToReplace = new vscode.Range(currentNodeStart, currentNodeEnd); + } + else { + rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, document.lineAt(rangeToReplace.start.line).text.length); + } } else { - rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length); + rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, document.lineAt(rangeToReplace.start.line).text.length); } } - const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character); + const firstLineOfSelection = document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character); const matches = firstLineOfSelection.match(/^(\s*)/); const extraWhitespaceSelected = matches ? matches[1].length : 0; rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character); let textToWrapInPreview: string[]; - const textToReplace = editor.document.getText(rangeToReplace); + const textToReplace = document.getText(rangeToReplace); if (individualLines) { textToWrapInPreview = textToReplace.split('\n').map(x => x.trim()); } else { - const wholeFirstLine = editor.document.lineAt(rangeToReplace.start).text; + const wholeFirstLine = document.lineAt(rangeToReplace.start).text; const otherMatches = wholeFirstLine.match(/^(\s*)/); const precedingWhitespace = otherMatches ? otherMatches[1] : ''; textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + precedingWhitespace).join('\n\t') + '\n']; @@ -327,7 +337,7 @@ export function expandEmmetAbbreviation(args: any): Thenable 1000) { rootNode = parsePartialStylesheet(editor.document, editor.selection.isReversed ? editor.selection.anchor : editor.selection.active); } else { - rootNode = parseDocument(editor.document, false); + rootNode = parseDocument(editor.document, true); } return rootNode; @@ -342,24 +352,25 @@ export function expandEmmetAbbreviation(args: any): Thenable { * @param position position to validate * @param abbreviationRange The range of the abbreviation for which given position is being validated */ -export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean { +export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | undefined, syntax: string, offset: number, abbreviationRange: vscode.Range): boolean { if (isStyleSheet(syntax)) { const stylesheet = rootNode; - if (stylesheet && (stylesheet.comments || []).some(x => position.isAfterOrEqual(x.start) && position.isBeforeOrEqual(x.end))) { + if (stylesheet && (stylesheet.comments || []).some(x => offset >= x.start && offset <= x.end)) { return false; } // Continue validation only if the file was parse-able and the currentNode has been found @@ -419,14 +430,14 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen const propertyNode = currentNode; if (propertyNode.terminatorToken && propertyNode.separator - && position.isAfterOrEqual(propertyNode.separatorToken.end) - && position.isBeforeOrEqual(propertyNode.terminatorToken.start) + && offset >= propertyNode.separatorToken.end + && offset <= propertyNode.terminatorToken.start && abbreviation.indexOf(':') === -1) { return hexColorRegex.test(abbreviation) || abbreviation === '!'; } if (!propertyNode.terminatorToken && propertyNode.separator - && position.isAfterOrEqual(propertyNode.separatorToken.end) + && offset >= propertyNode.separatorToken.end && abbreviation.indexOf(':') === -1) { return hexColorRegex.test(abbreviation) || abbreviation === '!'; } @@ -444,7 +455,7 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen const currentCssNode = currentNode; // Position is valid if it occurs after the `{` that marks beginning of rule contents - if (position.isAfter(currentCssNode.contentStartToken.end)) { + if (offset > currentCssNode.contentStartToken.end) { return true; } @@ -453,12 +464,16 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen // But we should assume it is a valid location for css properties under the parent rule if (currentCssNode.parent && (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule') - && currentCssNode.selectorToken - && position.line !== currentCssNode.selectorToken.end.line - && currentCssNode.selectorToken.start.character === abbreviationRange.start.character - && currentCssNode.selectorToken.start.line === abbreviationRange.start.line - ) { - return true; + && currentCssNode.selectorToken) { + const position = document.positionAt(offset); + const tokenStartPos = document.positionAt(currentCssNode.selectorToken.start); + const tokenEndPos = document.positionAt(currentCssNode.selectorToken.end); + if (position.line !== tokenEndPos.line + && tokenStartPos.character === abbreviationRange.start.character + && tokenStartPos.line === abbreviationRange.start.line + ) { + return true; + } } return false; @@ -469,7 +484,7 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen const escape = '\\'; const question = '?'; const currentHtmlNode = currentNode; - let start = new vscode.Position(0, 0); + let start = 0; if (currentHtmlNode) { if (currentHtmlNode.name === 'script') { @@ -487,27 +502,27 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen return false; } - const innerRange = getInnerRange(currentHtmlNode); - // Fix for https://github.com/microsoft/vscode/issues/28829 - if (!innerRange || !innerRange.contains(position)) { + if (!currentHtmlNode.open || !currentHtmlNode.close || + !(currentHtmlNode.open.end <= offset && offset <= currentHtmlNode.close.start)) { return false; } // Fix for https://github.com/microsoft/vscode/issues/35128 // Find the position up till where we will backtrack looking for unescaped < or > // to decide if current position is valid for emmet expansion - start = innerRange.start; + start = currentHtmlNode.open.end; let lastChildBeforePosition = currentHtmlNode.firstChild; while (lastChildBeforePosition) { - if (lastChildBeforePosition.end.isAfter(position)) { + if (lastChildBeforePosition.end > offset) { break; } start = lastChildBeforePosition.end; lastChildBeforePosition = lastChildBeforePosition.nextSibling; } } - let textToBackTrack = document.getText(new vscode.Range(start.line, start.character, abbreviationRange.start.line, abbreviationRange.start.character)); + const startPos = document.positionAt(start); + let textToBackTrack = document.getText(new vscode.Range(startPos.line, startPos.character, abbreviationRange.start.line, abbreviationRange.start.character)); // Worse case scenario is when cursor is inside a big chunk of text which needs to backtracked // Backtrack only 500 offsets to ensure we dont waste time doing this diff --git a/extensions/emmet/src/defaultCompletionProvider.ts b/extensions/emmet/src/defaultCompletionProvider.ts index 9ac469af615..a4f31e5f8c4 100644 --- a/extensions/emmet/src/defaultCompletionProvider.ts +++ b/extensions/emmet/src/defaultCompletionProvider.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Node, Stylesheet } from 'EmmetNode'; +import { Node, Stylesheet } from 'EmmetFlatNode'; import { isValidLocationForEmmetAbbreviation, getSyntaxFromArgs } from './abbreviationActions'; -import { getEmmetHelper, getMappingForIncludedLanguages, parsePartialStylesheet, getEmmetConfiguration, getEmmetMode, isStyleSheet, parseDocument, getNode, allowedMimeTypesInScriptTag, trimQuotes, toLSTextDocument } from './util'; +import { getEmmetHelper, getMappingForIncludedLanguages, parsePartialStylesheet, getEmmetConfiguration, getEmmetMode, isStyleSheet, getFlatNode, allowedMimeTypesInScriptTag, trimQuotes, toLSTextDocument } from './util'; import { getLanguageService, TokenType, Range as LSRange } from 'vscode-html-languageservice'; +import { getRootNode } from './parseDocument'; export class DefaultCompletionItemProvider implements vscode.CompletionItemProvider { @@ -62,8 +63,8 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi const helper = getEmmetHelper(); let validateLocation = syntax === 'html' || syntax === 'jsx' || syntax === 'xml'; - let rootNode: Node | undefined = undefined; - let currentNode: Node | null = null; + let rootNode: Node | undefined; + let currentNode: Node | undefined; const lsDoc = toLSTextDocument(document); position = document.validatePosition(position); @@ -145,19 +146,18 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi return; } + const offset = document.offsetAt(position); if (isStyleSheet(document.languageId) && context.triggerKind !== vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) { validateLocation = true; let usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true; - rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : parseDocument(document, false); + rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : getRootNode(document, true); if (!rootNode) { return; } - currentNode = getNode(rootNode, position, true); + currentNode = getFlatNode(rootNode, offset, true); } - - - if (validateLocation && !isValidLocationForEmmetAbbreviation(document, rootNode, currentNode, syntax, position, toRange(extractAbbreviationResults.abbreviationRange))) { + if (validateLocation && !isValidLocationForEmmetAbbreviation(document, rootNode, currentNode, syntax, offset, toRange(extractAbbreviationResults.abbreviationRange))) { return; } diff --git a/extensions/emmet/src/test/partialParsingStylesheet.test.ts b/extensions/emmet/src/test/partialParsingStylesheet.test.ts index 0c19d06b645..b0928e1a32f 100644 --- a/extensions/emmet/src/test/partialParsingStylesheet.test.ts +++ b/extensions/emmet/src/test/partialParsingStylesheet.test.ts @@ -7,15 +7,16 @@ import 'mocha'; import * as assert from 'assert'; import { withRandomFileEditor } from './testUtils'; import * as vscode from 'vscode'; -import { parsePartialStylesheet, getNode } from '../util'; +import { parsePartialStylesheet, getFlatNode } from '../util'; import { isValidLocationForEmmetAbbreviation } from '../abbreviationActions'; suite('Tests for partial parse of Stylesheets', () => { function isValid(doc: vscode.TextDocument, range: vscode.Range, syntax: string): boolean { const rootNode = parsePartialStylesheet(doc, range.end); - const currentNode = getNode(rootNode, range.end, true); - return isValidLocationForEmmetAbbreviation(doc, rootNode, currentNode, syntax, range.end, range); + const endOffset = doc.offsetAt(range.end); + const currentNode = getFlatNode(rootNode, endOffset, true); + return isValidLocationForEmmetAbbreviation(doc, rootNode, currentNode, syntax, endOffset, range); } test('Ignore block comment inside rule', function (): any { @@ -257,4 +258,4 @@ ment */{ }); -}); \ No newline at end of file +}); diff --git a/extensions/emmet/src/util.ts b/extensions/emmet/src/util.ts index e5653659e01..97e829ef259 100644 --- a/extensions/emmet/src/util.ts +++ b/extensions/emmet/src/util.ts @@ -6,8 +6,7 @@ import * as vscode from 'vscode'; import parse from '@emmetio/html-matcher'; import parseStylesheet from '@emmetio/css-parser'; -import { Node, HtmlNode, Stylesheet } from 'EmmetNode'; -import { Node as FlatNode, HtmlNode as HtmlFlatNode, Property as FlatProperty, Rule as FlatRule, CssToken as FlatCssToken } from 'EmmetFlatNode'; +import { Node as FlatNode, HtmlNode as HtmlFlatNode, Property as FlatProperty, Rule as FlatRule, CssToken as FlatCssToken, Stylesheet as FlatStylesheet } from 'EmmetFlatNode'; import { DocumentStreamReader } from './bufferStream'; import * as EmmetHelper from 'vscode-emmet-helper'; import { TextDocument as LSTextDocument } from 'vscode-languageserver-textdocument'; @@ -138,21 +137,6 @@ export function getEmmetMode(language: string, excludedLanguages: string[]): str return; } -/** - * Parses the given document using emmet parsing modules - */ -export function parseDocument(document: vscode.TextDocument, showError: boolean = true): Node | undefined { - let parseContent = isStyleSheet(document.languageId) ? parseStylesheet : parse; - try { - return parseContent(new DocumentStreamReader(document)); - } catch (e) { - if (showError) { - vscode.window.showErrorMessage('Emmet: Failed to parse the file'); - } - } - return undefined; -} - const closeBrace = 125; const openBrace = 123; const slash = 47; @@ -164,7 +148,7 @@ const star = 42; * @param document vscode.TextDocument * @param position vscode.Position */ -export function parsePartialStylesheet(document: vscode.TextDocument, position: vscode.Position): Stylesheet | undefined { +export function parsePartialStylesheet(document: vscode.TextDocument, position: vscode.Position): FlatStylesheet | undefined { const isCSS = document.languageId === 'css'; let startPosition = new vscode.Position(0, 0); let endPosition = new vscode.Position(document.lineCount - 1, document.lineAt(document.lineCount - 1).text.length); @@ -304,7 +288,10 @@ export function parsePartialStylesheet(document: vscode.TextDocument, position: } try { - return parseStylesheet(new DocumentStreamReader(document, startPosition, new vscode.Range(startPosition, endPosition))); + const startOffset = document.offsetAt(startPosition); + const endOffset = document.offsetAt(endPosition); + const buffer = ' '.repeat(startOffset) + document.getText().substring(startOffset, endOffset); + return parseStylesheet(buffer); } catch (e) { return; } @@ -313,31 +300,6 @@ export function parsePartialStylesheet(document: vscode.TextDocument, position: /** * Returns node corresponding to given position in the given root node */ -export function getNode(root: Node | undefined, position: vscode.Position, includeNodeBoundary: boolean): Node | null { - if (!root) { - return null; - } - - let currentNode = root.firstChild; - let foundNode: Node | null = null; - - while (currentNode) { - const nodeStart: vscode.Position = currentNode.start; - const nodeEnd: vscode.Position = currentNode.end; - if ((nodeStart.isBefore(position) && nodeEnd.isAfter(position)) - || (includeNodeBoundary && (nodeStart.isBeforeOrEqual(position) && nodeEnd.isAfterOrEqual(position)))) { - - foundNode = currentNode; - // Dig deeper - currentNode = currentNode.firstChild; - } else { - currentNode = currentNode.nextSibling; - } - } - - return foundNode; -} - export function getFlatNode(root: FlatNode | undefined, offset: number, includeNodeBoundary: boolean): FlatNode | undefined { if (!root) { return; @@ -380,35 +342,9 @@ export function getFlatNode(root: FlatNode | undefined, offset: number, includeN 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; } - - const isTemplateScript = currentNode.name === 'script' && - (currentNode.attributes && - currentNode.attributes.some(x => x.name.toString() === 'type' - && allowedMimeTypesInScriptTag.indexOf(x.value.toString()) > -1)); - - if (isTemplateScript && currentNode.close && - (position.isAfter(currentNode.open.end) && position.isBefore(currentNode.close.start))) { - - let buffer = new DocumentStreamReader(document, currentNode.open.end, new vscode.Range(currentNode.open.end, currentNode.close.start)); - - try { - let scriptInnerNodes = parse(buffer); - currentNode = getNode(scriptInnerNodes, position, includeNodeBoundary) || currentNode; - } catch (e) { } - } - - return currentNode; -} - /** * Finds the HTML node within an HTML document at a given position + * 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 getHtmlFlatNode(documentText: string, root: FlatNode | undefined, offset: number, includeNodeBoundary: boolean): HtmlFlatNode | undefined { const currentNode: HtmlFlatNode | undefined = getFlatNode(root, offset, includeNodeBoundary); @@ -449,31 +385,6 @@ export function offsetRangeToVsRange(document: vscode.TextDocument, start: numbe return new vscode.Range(startPos, endPos); } -/** - * Returns inner range of an html node. - */ -export function getInnerRange(currentNode: HtmlNode): vscode.Range | undefined { - if (!currentNode.close) { - return undefined; - } - return new vscode.Range(currentNode.open.end, currentNode.close.start); -} - -/** - * Returns the deepest non comment node under given node - */ -export function getDeepestNode(node: Node | undefined): Node | undefined { - if (!node || !node.children || node.children.length === 0 || !node.children.find(x => x.type !== 'comment')) { - return node; - } - for (let i = node.children.length - 1; i >= 0; i--) { - if (node.children[i].type !== 'comment') { - return getDeepestNode(node.children[i]); - } - } - return undefined; -} - /** * Returns the deepest non comment node under given node */ @@ -696,18 +607,18 @@ export function getCssPropertyFromDocument(editor: vscode.TextEditor, position: } -export function getEmbeddedCssNodeIfAny(document: vscode.TextDocument, currentNode: Node | null, position: vscode.Position): Node | undefined { +export function getEmbeddedCssNodeIfAny(document: vscode.TextDocument, currentNode: FlatNode | undefined, position: vscode.Position): FlatNode | undefined { if (!currentNode) { return; } - const currentHtmlNode = currentNode; - if (currentHtmlNode && currentHtmlNode.close) { - const innerRange = getInnerRange(currentHtmlNode); - if (innerRange && innerRange.contains(position)) { + const currentHtmlNode = currentNode; + if (currentHtmlNode && currentHtmlNode.open && currentHtmlNode.close) { + const offset = document.offsetAt(position); + if (currentHtmlNode.open.end <= offset && offset <= currentHtmlNode.close.start) { if (currentHtmlNode.name === 'style' - && currentHtmlNode.open.end.isBefore(position) - && currentHtmlNode.close.start.isAfter(position)) { - let buffer = new DocumentStreamReader(document, currentHtmlNode.open.end, new vscode.Range(currentHtmlNode.open.end, currentHtmlNode.close.start)); + && currentHtmlNode.open.end < offset + && currentHtmlNode.close.start > offset) { + const buffer = ' '.repeat(currentHtmlNode.open.end) + document.getText().substring(currentHtmlNode.open.end, currentHtmlNode.close.start); return parseStylesheet(buffer); } } @@ -715,17 +626,17 @@ export function getEmbeddedCssNodeIfAny(document: vscode.TextDocument, currentNo return; } -export function isStyleAttribute(currentNode: Node | null, position: vscode.Position): boolean { +export function isStyleAttribute(currentNode: FlatNode | undefined, offset: number): boolean { if (!currentNode) { return false; } - const currentHtmlNode = currentNode; + const currentHtmlNode = currentNode; const index = (currentHtmlNode.attributes || []).findIndex(x => x.name.toString() === 'style'); if (index === -1) { return false; } const styleAttribute = currentHtmlNode.attributes[index]; - return position.isAfterOrEqual(styleAttribute.value.start) && position.isBeforeOrEqual(styleAttribute.value.end); + return offset >= styleAttribute.value.start && offset <= styleAttribute.value.end; } -- GitLab