未验证 提交 15ba9aee 编写于 作者: R Raymond Zhao 提交者: GitHub

Emmet improve Expand Abbreviation perf (#113558)

上级 b5cd082c
......@@ -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<boolean | undefined
if (editor.selections.length === 1 && isStyleSheet(editor.document.languageId) && usePartialParsing && editor.document.lineCount > 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<boolean | undefined
if (!helper.isAbbreviationValid(syntax, abbreviation)) {
return;
}
let currentNode = getNode(getRootNode(), position, true);
const offset = editor.document.offsetAt(position);
let currentNode = getFlatNode(getRootNode(), offset, true);
let validateLocation = true;
let syntaxToUse = syntax;
if (editor.document.languageId === 'html') {
if (isStyleAttribute(currentNode, position)) {
if (isStyleAttribute(currentNode, offset)) {
syntaxToUse = 'css';
validateLocation = false;
} else {
const embeddedCssNode = getEmbeddedCssNodeIfAny(editor.document, currentNode, position);
if (embeddedCssNode) {
currentNode = getNode(embeddedCssNode, position, true);
currentNode = getFlatNode(embeddedCssNode, offset, true);
syntaxToUse = 'css';
}
}
}
if (validateLocation && !isValidLocationForEmmetAbbreviation(editor.document, getRootNode(), currentNode, syntaxToUse, position, rangeToReplace)) {
if (validateLocation && !isValidLocationForEmmetAbbreviation(editor.document, getRootNode(), currentNode, syntaxToUse, offset, rangeToReplace)) {
return;
}
......@@ -393,10 +404,10 @@ function fallbackTab(): Thenable<boolean | undefined> {
* @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 = <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 = <Property>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 = <Rule>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,13 +464,17 @@ 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
&& 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 = <HtmlNode>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
......
......@@ -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) : <Stylesheet>parseDocument(document, false);
rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : <Stylesheet>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;
}
......
......@@ -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 {
......
......@@ -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 = <HtmlNode>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 = <HtmlNode>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 = <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 = <HtmlNode>currentNode;
if (currentHtmlNode && currentHtmlNode.close) {
const innerRange = getInnerRange(currentHtmlNode);
if (innerRange && innerRange.contains(position)) {
const currentHtmlNode = <HtmlFlatNode>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 = <HtmlNode>currentNode;
const currentHtmlNode = <HtmlFlatNode>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;
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册