diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index 299f58f480b..fae02e600cd 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -181,7 +181,7 @@ export function expandEmmetAbbreviation(args: any): Thenable { /** * Checks if given position is a valid location to expand emmet abbreviation. * Works only on html and css/less/scss syntax + * @param document current Text Document * @param currentNode parsed node at given position * @param syntax syntax of the abbreviation * @param position position to validate + * @param abbreviationRange The range of the abbreviation for which given position is being validated */ -export function isValidLocationForEmmetAbbreviation(currentNode: Node | null, syntax: string, position: vscode.Position): boolean { +export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean { // Continue validation only if the file was parse-able and the currentNode has been found if (!currentNode) { return true; @@ -247,13 +249,64 @@ export function isValidLocationForEmmetAbbreviation(currentNode: Node | null, sy return false; } + const startAngle = '<'; + const endAngle = '>'; + const escape = '\\'; const currentHtmlNode = currentNode; - if (currentHtmlNode.close) { - const innerRange = getInnerRange(currentHtmlNode); - return !!innerRange && innerRange.contains(position); + const innerRange = getInnerRange(currentHtmlNode); + + // Fix for https://github.com/Microsoft/vscode/issues/28829 + if (!innerRange || !innerRange.contains(position)) { + return false; } - 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 + let start = innerRange.start; + let lastChildBeforePosition = currentHtmlNode.firstChild; + while (lastChildBeforePosition) { + if (lastChildBeforePosition.end.isAfter(position)) { + break; + } + start = lastChildBeforePosition.end; + lastChildBeforePosition = lastChildBeforePosition.nextSibling; + } + let textToBackTrack = document.getText(new vscode.Range(start, abbreviationRange.start)); + + // 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 + if (textToBackTrack.length > 500) { + textToBackTrack = textToBackTrack.substr(textToBackTrack.length - 500); + } + + let valid = true; + let foundSpace = false; // If < is found before finding whitespace, then its valid abbreviation. Eg: = 0) { + const char = textToBackTrack[i]; + i--; + if (!foundSpace && /\s/.test(char)) { + foundSpace = true; + continue; + } + if (char !== startAngle && char !== endAngle) { + continue; + } + if (i >= 0 && textToBackTrack[i] === escape) { + i--; + continue; + } + if (char === endAngle) { + break; + } + if (char === startAngle) { + valid = !foundSpace; + break; + } + } + + return valid; } /** diff --git a/extensions/emmet/src/defaultCompletionProvider.ts b/extensions/emmet/src/defaultCompletionProvider.ts index d0ac43eced7..7d331f575b7 100644 --- a/extensions/emmet/src/defaultCompletionProvider.ts +++ b/extensions/emmet/src/defaultCompletionProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { HtmlNode } from 'EmmetNode'; +import { HtmlNode, Node } from 'EmmetNode'; import { isValidLocationForEmmetAbbreviation } from './abbreviationActions'; import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, getEmmetConfiguration, getEmmetMode, isStyleSheet } from './util'; @@ -23,10 +23,27 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi const isSyntaxMapped = mappedLanguages[document.languageId] ? true : false; let syntax = getEmmetMode((isSyntaxMapped ? mappedLanguages[document.languageId] : document.languageId), excludedLanguages); + const helper = getEmmetHelper(); + const extractAbbreviationResults = helper.extractAbbreviation(document, position); + if (!extractAbbreviationResults) { + return; + } + + // If document can be html/css parsed, validate syntax and location if (document.languageId === 'html' || isStyleSheet(document.languageId)) { - // Document can be html/css parsed - // Use syntaxHelper to parse file, validate location and update sytnax if needed - syntax = this.syntaxHelper(syntax, document, position); + const rootNode = parseDocument(document, false); + if (!rootNode) { + return; + } + + // Use syntaxHelper to update sytnax if needed + const currentNode = getNode(rootNode, position, true); + syntax = this.syntaxHelper(syntax, currentNode, position); + + // Validate location + if (!syntax || !isValidLocationForEmmetAbbreviation(document, currentNode, syntax, position, extractAbbreviationResults.abbreviationRange)) { + return; + } } if (!syntax @@ -35,23 +52,19 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi return; } - const helper = getEmmetHelper(); let noiseCheckPromise: Thenable = Promise.resolve(); // Fix for https://github.com/Microsoft/vscode/issues/32647 // Check for document symbols in js/ts/jsx/tsx and avoid triggering emmet for abbreviations of the form symbolName.sometext // Presence of > or * or + in the abbreviation denotes valid abbreviation that should trigger emmet if (!isStyleSheet(syntax) && (document.languageId === 'javascript' || document.languageId === 'javascriptreact' || document.languageId === 'typescript' || document.languageId === 'typescriptreact')) { - let extractAbbreviationResults = helper.extractAbbreviation(document, position); - if (extractAbbreviationResults) { - let abbreviation: string = extractAbbreviationResults.abbreviation; - if (abbreviation.startsWith('this.')) { - noiseCheckPromise = Promise.resolve(true); - } else { - noiseCheckPromise = vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri).then((symbols: vscode.SymbolInformation[] | undefined) => { - return symbols && symbols.find(x => abbreviation === x.name || (abbreviation.startsWith(x.name + '.') && !/>|\*|\+/.test(abbreviation))); - }); - } + let abbreviation: string = extractAbbreviationResults.abbreviation; + if (abbreviation.startsWith('this.')) { + noiseCheckPromise = Promise.resolve(true); + } else { + noiseCheckPromise = vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri).then((symbols: vscode.SymbolInformation[] | undefined) => { + return symbols && symbols.find(x => abbreviation === x.name || (abbreviation.startsWith(x.name + '.') && !/>|\*|\+/.test(abbreviation))); + }); } } @@ -88,21 +101,11 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi /** * Parses given document to check whether given position is valid for emmet abbreviation and returns appropriate syntax * @param syntax string language mode of current document - * @param document vscode.Textdocument + * @param currentNode node in the document that contains the position * @param position vscode.Position position of the abbreviation that needs to be expanded */ - private syntaxHelper(syntax: string | undefined, document: vscode.TextDocument, position: vscode.Position): string | undefined { - if (!syntax) { - return syntax; - } - let rootNode = parseDocument(document, false); - if (!rootNode) { - return; - } - - let currentNode = getNode(rootNode, position, true); - - if (!isStyleSheet(syntax)) { + private syntaxHelper(syntax: string | undefined, currentNode: Node | null, position: vscode.Position): string | undefined { + if (syntax && !isStyleSheet(syntax)) { const currentHtmlNode = currentNode; if (currentHtmlNode && currentHtmlNode.close) { const innerRange = getInnerRange(currentHtmlNode); @@ -121,9 +124,6 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi } } - if (!isValidLocationForEmmetAbbreviation(currentNode, syntax, position)) { - return; - } return syntax; } diff --git a/extensions/emmet/src/test/abbreviationAction.test.ts b/extensions/emmet/src/test/abbreviationAction.test.ts index c9e14c800d5..3bce3029492 100644 --- a/extensions/emmet/src/test/abbreviationAction.test.ts +++ b/extensions/emmet/src/test/abbreviationAction.test.ts @@ -51,7 +51,7 @@ const htmlContents = ` ul>li*2 ul>li.item$*2 ul>li.item$@44*2 -