"Fossies" - the Fresh Open Source Software Archive

Member "vscode-1.49.1/extensions/emmet/src/abbreviationActions.ts" (16 Sep 2020, 28449 Bytes) of package /linux/misc/vscode-1.49.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "abbreviationActions.ts": 1.48.2_vs_1.49.0.

    1 /*---------------------------------------------------------------------------------------------
    2  *  Copyright (c) Microsoft Corporation. All rights reserved.
    3  *  Licensed under the MIT License. See License.txt in the project root for license information.
    4  *--------------------------------------------------------------------------------------------*/
    5 
    6 import * as vscode from 'vscode';
    7 import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetNode';
    8 import { getEmmetHelper, getNode, getInnerRange, getMappingForIncludedLanguages, parseDocument, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util';
    9 
   10 const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/;
   11 const hexColorRegex = /^#[\da-fA-F]{0,6}$/;
   12 const inlineElements = ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo',
   13     'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i',
   14     'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q',
   15     's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup',
   16     'textarea', 'tt', 'u', 'var'];
   17 
   18 interface ExpandAbbreviationInput {
   19     syntax: string;
   20     abbreviation: string;
   21     rangeToReplace: vscode.Range;
   22     textToWrap?: string[];
   23     filter?: string;
   24 }
   25 
   26 interface PreviewRangesWithContent {
   27     previewRange: vscode.Range;
   28     originalRange: vscode.Range;
   29     originalContent: string;
   30     textToWrapInPreview: string[];
   31 }
   32 
   33 export function wrapWithAbbreviation(args: any) {
   34     return doWrapping(false, args);
   35 }
   36 
   37 export function wrapIndividualLinesWithAbbreviation(args: any) {
   38     return doWrapping(true, args);
   39 }
   40 
   41 function doWrapping(individualLines: boolean, args: any) {
   42     if (!validate(false) || !vscode.window.activeTextEditor) {
   43         return;
   44     }
   45 
   46     const editor = vscode.window.activeTextEditor;
   47     if (individualLines) {
   48         if (editor.selections.length === 1 && editor.selection.isEmpty) {
   49             vscode.window.showInformationMessage('Select more than 1 line and try again.');
   50             return;
   51         }
   52         if (editor.selections.find(x => x.isEmpty)) {
   53             vscode.window.showInformationMessage('Select more than 1 line in each selection and try again.');
   54             return;
   55         }
   56     }
   57     args = args || {};
   58     if (!args['language']) {
   59         args['language'] = editor.document.languageId;
   60     }
   61     const syntax = getSyntaxFromArgs(args) || 'html';
   62     const rootNode = parseDocument(editor.document, false);
   63 
   64     let inPreview = false;
   65     let currentValue = '';
   66     const helper = getEmmetHelper();
   67 
   68     // Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
   69     let rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => {
   70         let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
   71         if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
   72             const previousLine = rangeToReplace.end.line - 1;
   73             const lastChar = editor.document.lineAt(previousLine).text.length;
   74             rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar));
   75         } else if (rangeToReplace.isEmpty) {
   76             const { active } = selection;
   77             const currentNode = getNode(rootNode, active, true);
   78             if (currentNode && (currentNode.start.line === active.line || currentNode.end.line === active.line)) {
   79                 rangeToReplace = new vscode.Range(currentNode.start, currentNode.end);
   80             } else {
   81                 rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
   82             }
   83         }
   84 
   85         const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character);
   86         const matches = firstLineOfSelection.match(/^(\s*)/);
   87         const extraWhitespaceSelected = matches ? matches[1].length : 0;
   88         rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character);
   89 
   90         let textToWrapInPreview: string[];
   91         let textToReplace = editor.document.getText(rangeToReplace);
   92         if (individualLines) {
   93             textToWrapInPreview = textToReplace.split('\n').map(x => x.trim());
   94         } else {
   95             const wholeFirstLine = editor.document.lineAt(rangeToReplace.start).text;
   96             const otherMatches = wholeFirstLine.match(/^(\s*)/);
   97             const precedingWhitespace = otherMatches ? otherMatches[1] : '';
   98             textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + precedingWhitespace).join('\n\t') + '\n'];
   99         }
  100         textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1'));
  101 
  102         return {
  103             previewRange: rangeToReplace,
  104             originalRange: rangeToReplace,
  105             originalContent: textToReplace,
  106             textToWrapInPreview
  107         };
  108     });
  109 
  110     function revertPreview(): Thenable<any> {
  111         return editor.edit(builder => {
  112             for (const rangeToReplace of rangesToReplace) {
  113                 builder.replace(rangeToReplace.previewRange, rangeToReplace.originalContent);
  114                 rangeToReplace.previewRange = rangeToReplace.originalRange;
  115             }
  116         }, { undoStopBefore: false, undoStopAfter: false });
  117     }
  118 
  119     function applyPreview(expandAbbrList: ExpandAbbreviationInput[]): Thenable<boolean> {
  120         let lastOldPreviewRange = new vscode.Range(0, 0, 0, 0);
  121         let lastNewPreviewRange = new vscode.Range(0, 0, 0, 0);
  122         let totalLinesInserted = 0;
  123 
  124         return editor.edit(builder => {
  125             for (let i = 0; i < rangesToReplace.length; i++) {
  126                 const expandedText = expandAbbr(expandAbbrList[i]) || '';
  127                 if (!expandedText) {
  128                     // Failed to expand text. We already showed an error inside expandAbbr.
  129                     break;
  130                 }
  131 
  132                 const oldPreviewRange = rangesToReplace[i].previewRange;
  133                 const preceedingText = editor.document.getText(new vscode.Range(oldPreviewRange.start.line, 0, oldPreviewRange.start.line, oldPreviewRange.start.character));
  134                 const indentPrefix = (preceedingText.match(/^(\s*)/) || ['', ''])[1];
  135 
  136                 let newText = expandedText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text
  137                 newText = newText.replace(/\$\{[\d]*\}/g, '|'); // Removing Tabstops
  138                 newText = newText.replace(/\$\{[\d]*(:[^}]*)?\}/g, (match) => {     // Replacing Placeholders
  139                     return match.replace(/^\$\{[\d]*:/, '').replace('}', '');
  140                 });
  141                 builder.replace(oldPreviewRange, newText);
  142 
  143                 const expandedTextLines = newText.split('\n');
  144                 const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1;
  145                 const newLinesInserted = expandedTextLines.length - oldPreviewLines;
  146 
  147                 let newPreviewLineStart = oldPreviewRange.start.line + totalLinesInserted;
  148                 let newPreviewStart = oldPreviewRange.start.character;
  149                 const newPreviewLineEnd = oldPreviewRange.end.line + totalLinesInserted + newLinesInserted;
  150                 let newPreviewEnd = expandedTextLines[expandedTextLines.length - 1].length;
  151                 if (i > 0 && newPreviewLineEnd === lastNewPreviewRange.end.line) {
  152                     // If newPreviewLineEnd is equal to the previous expandedText lineEnd,
  153                     // set newPreviewStart to the length of the previous expandedText in that line
  154                     // plus the number of characters between both selections.
  155                     newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
  156                     newPreviewEnd += newPreviewStart;
  157                 }
  158                 else if (i > 0 && newPreviewLineStart === lastNewPreviewRange.end.line) {
  159                     // Same as above but expandedTextLines.length > 1 so newPreviewEnd keeps its value.
  160                     newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
  161                 }
  162                 else if (expandedTextLines.length === 1) {
  163                     // If the expandedText is single line, add the length of preceeding text as it will not be included in line length.
  164                     newPreviewEnd += oldPreviewRange.start.character;
  165                 }
  166 
  167                 lastOldPreviewRange = rangesToReplace[i].previewRange;
  168                 rangesToReplace[i].previewRange = lastNewPreviewRange = new vscode.Range(newPreviewLineStart, newPreviewStart, newPreviewLineEnd, newPreviewEnd);
  169 
  170                 totalLinesInserted += newLinesInserted;
  171             }
  172         }, { undoStopBefore: false, undoStopAfter: false });
  173     }
  174 
  175     function makeChanges(inputAbbreviation: string | undefined, definitive: boolean): Thenable<boolean> {
  176         if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) {
  177             return inPreview ? revertPreview().then(() => { return false; }) : Promise.resolve(inPreview);
  178         }
  179 
  180         let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
  181         if (!extractedResults) {
  182             return Promise.resolve(inPreview);
  183         } else if (extractedResults.abbreviation !== inputAbbreviation) {
  184             // Not clear what should we do in this case. Warn the user? How?
  185         }
  186 
  187         let { abbreviation, filter } = extractedResults;
  188         if (definitive) {
  189             const revertPromise = inPreview ? revertPreview() : Promise.resolve();
  190             return revertPromise.then(() => {
  191                 const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
  192                     let rangeToReplace = rangesAndContent.originalRange;
  193                     let textToWrap: string[];
  194                     if (individualLines) {
  195                         textToWrap = rangesAndContent.textToWrapInPreview;
  196                     } else {
  197                         textToWrap = rangeToReplace.isSingleLine ? ['$TM_SELECTED_TEXT'] : ['\n\t$TM_SELECTED_TEXT\n'];
  198                     }
  199                     return { syntax: syntax || '', abbreviation, rangeToReplace, textToWrap, filter };
  200                 });
  201                 return expandAbbreviationInRange(editor, expandAbbrList, !individualLines).then(() => { return true; });
  202             });
  203         }
  204 
  205         const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
  206             return { syntax: syntax || '', abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter };
  207         });
  208 
  209         return applyPreview(expandAbbrList);
  210     }
  211 
  212     function inputChanged(value: string): string {
  213         if (value !== currentValue) {
  214             currentValue = value;
  215             makeChanges(value, false).then((out) => {
  216                 if (typeof out === 'boolean') {
  217                     inPreview = out;
  218                 }
  219             });
  220         }
  221         return '';
  222     }
  223     const abbreviationPromise: Thenable<string | undefined> = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation', validateInput: inputChanged });
  224     return abbreviationPromise.then(inputAbbreviation => {
  225         return makeChanges(inputAbbreviation, true);
  226     });
  227 }
  228 
  229 export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> {
  230     if (!validate() || !vscode.window.activeTextEditor) {
  231         return fallbackTab();
  232     }
  233 
  234     /**
  235      * Short circuit the parsing. If previous character is space, do not expand.
  236      */
  237     if (vscode.window.activeTextEditor.selections.length === 1 &&
  238         vscode.window.activeTextEditor.selection.isEmpty
  239     ) {
  240         const anchor = vscode.window.activeTextEditor.selection.anchor;
  241         if (anchor.character === 0) {
  242             return fallbackTab();
  243         }
  244 
  245         const prevPositionAnchor = anchor.translate(0, -1);
  246         const prevText = vscode.window.activeTextEditor.document.getText(new vscode.Range(prevPositionAnchor, anchor));
  247         if (prevText === ' ' || prevText === '\t') {
  248             return fallbackTab();
  249         }
  250     }
  251 
  252     args = args || {};
  253     if (!args['language']) {
  254         args['language'] = vscode.window.activeTextEditor.document.languageId;
  255     } else {
  256         const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
  257         if (excludedLanguages.indexOf(vscode.window.activeTextEditor.document.languageId) > -1) {
  258             return fallbackTab();
  259         }
  260     }
  261     const syntax = getSyntaxFromArgs(args);
  262     if (!syntax) {
  263         return fallbackTab();
  264     }
  265 
  266     const editor = vscode.window.activeTextEditor;
  267 
  268     // When tabbed on a non empty selection, do not treat it as an emmet abbreviation, and fallback to tab instead
  269     if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true && editor.selections.find(x => !x.isEmpty)) {
  270         return fallbackTab();
  271     }
  272 
  273     let abbreviationList: ExpandAbbreviationInput[] = [];
  274     let firstAbbreviation: string;
  275     let allAbbreviationsSame: boolean = true;
  276     const helper = getEmmetHelper();
  277 
  278     let getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string] => {
  279         position = document.validatePosition(position);
  280         let rangeToReplace: vscode.Range = selection;
  281         let abbr = document.getText(rangeToReplace);
  282         if (!rangeToReplace.isEmpty) {
  283             let extractedResults = helper.extractAbbreviationFromText(abbr);
  284             if (extractedResults) {
  285                 return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter];
  286             }
  287             return [null, '', ''];
  288         }
  289 
  290         const currentLine = editor.document.lineAt(position.line).text;
  291         const textTillPosition = currentLine.substr(0, position.character);
  292 
  293         // Expand cases like <div to <div></div> explicitly
  294         // else we will end up with <<div></div>
  295         if (syntax === 'html') {
  296             let matches = textTillPosition.match(/<(\w+)$/);
  297             if (matches) {
  298                 abbr = matches[1];
  299                 rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position);
  300                 return [rangeToReplace, abbr, ''];
  301             }
  302         }
  303         let extractedResults = helper.extractAbbreviation(toLSTextDocument(editor.document), position, false);
  304         if (!extractedResults) {
  305             return [null, '', ''];
  306         }
  307 
  308         let { abbreviationRange, abbreviation, filter } = extractedResults;
  309         return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter];
  310     };
  311 
  312     let selectionsInReverseOrder = editor.selections.slice(0);
  313     selectionsInReverseOrder.sort((a, b) => {
  314         const posA = a.isReversed ? a.anchor : a.active;
  315         const posB = b.isReversed ? b.anchor : b.active;
  316         return posA.compareTo(posB) * -1;
  317     });
  318 
  319     let rootNode: Node | undefined;
  320     function getRootNode() {
  321         if (rootNode) {
  322             return rootNode;
  323         }
  324 
  325         let usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;
  326         if (editor.selections.length === 1 && isStyleSheet(editor.document.languageId) && usePartialParsing && editor.document.lineCount > 1000) {
  327             rootNode = parsePartialStylesheet(editor.document, editor.selection.isReversed ? editor.selection.anchor : editor.selection.active);
  328         } else {
  329             rootNode = parseDocument(editor.document, false);
  330         }
  331 
  332         return rootNode;
  333     }
  334 
  335     selectionsInReverseOrder.forEach(selection => {
  336         let position = selection.isReversed ? selection.anchor : selection.active;
  337         let [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax);
  338         if (!rangeToReplace) {
  339             return;
  340         }
  341         if (!helper.isAbbreviationValid(syntax, abbreviation)) {
  342             return;
  343         }
  344         let currentNode = getNode(getRootNode(), position, true);
  345         let validateLocation = true;
  346         let syntaxToUse = syntax;
  347 
  348         if (editor.document.languageId === 'html') {
  349             if (isStyleAttribute(currentNode, position)) {
  350                 syntaxToUse = 'css';
  351                 validateLocation = false;
  352             } else {
  353                 const embeddedCssNode = getEmbeddedCssNodeIfAny(editor.document, currentNode, position);
  354                 if (embeddedCssNode) {
  355                     currentNode = getNode(embeddedCssNode, position, true);
  356                     syntaxToUse = 'css';
  357                 }
  358             }
  359         }
  360 
  361         if (validateLocation && !isValidLocationForEmmetAbbreviation(editor.document, getRootNode(), currentNode, syntaxToUse, position, rangeToReplace)) {
  362             return;
  363         }
  364 
  365         if (!firstAbbreviation) {
  366             firstAbbreviation = abbreviation;
  367         } else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
  368             allAbbreviationsSame = false;
  369         }
  370 
  371         abbreviationList.push({ syntax: syntaxToUse, abbreviation, rangeToReplace, filter });
  372     });
  373 
  374     return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
  375         return success ? Promise.resolve(undefined) : fallbackTab();
  376     });
  377 }
  378 
  379 function fallbackTab(): Thenable<boolean | undefined> {
  380     if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true) {
  381         return vscode.commands.executeCommand('tab');
  382     }
  383     return Promise.resolve(true);
  384 }
  385 /**
  386  * Checks if given position is a valid location to expand emmet abbreviation.
  387  * Works only on html and css/less/scss syntax
  388  * @param document current Text Document
  389  * @param rootNode parsed document
  390  * @param currentNode current node in the parsed document
  391  * @param syntax syntax of the abbreviation
  392  * @param position position to validate
  393  * @param abbreviationRange The range of the abbreviation for which given position is being validated
  394  */
  395 export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | null, syntax: string, position: vscode.Position, abbreviationRange: vscode.Range): boolean {
  396     if (isStyleSheet(syntax)) {
  397         const stylesheet = <Stylesheet>rootNode;
  398         if (stylesheet && (stylesheet.comments || []).some(x => position.isAfterOrEqual(x.start) && position.isBeforeOrEqual(x.end))) {
  399             return false;
  400         }
  401         // Continue validation only if the file was parse-able and the currentNode has been found
  402         if (!currentNode) {
  403             return true;
  404         }
  405 
  406         // Fix for https://github.com/Microsoft/vscode/issues/34162
  407         // Other than sass, stylus, we can make use of the terminator tokens to validate position
  408         if (syntax !== 'sass' && syntax !== 'stylus' && currentNode.type === 'property') {
  409 
  410             // Fix for upstream issue https://github.com/emmetio/css-parser/issues/3
  411             if (currentNode.parent
  412                 && currentNode.parent.type !== 'rule'
  413                 && currentNode.parent.type !== 'at-rule') {
  414                 return false;
  415             }
  416 
  417             const abbreviation = document.getText(new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character));
  418             const propertyNode = <Property>currentNode;
  419             if (propertyNode.terminatorToken
  420                 && propertyNode.separator
  421                 && position.isAfterOrEqual(propertyNode.separatorToken.end)
  422                 && position.isBeforeOrEqual(propertyNode.terminatorToken.start)
  423                 && abbreviation.indexOf(':') === -1) {
  424                 return hexColorRegex.test(abbreviation) || abbreviation === '!';
  425             }
  426             if (!propertyNode.terminatorToken
  427                 && propertyNode.separator
  428                 && position.isAfterOrEqual(propertyNode.separatorToken.end)
  429                 && abbreviation.indexOf(':') === -1) {
  430                 return hexColorRegex.test(abbreviation) || abbreviation === '!';
  431             }
  432             if (hexColorRegex.test(abbreviation) || abbreviation === '!') {
  433                 return false;
  434             }
  435         }
  436 
  437         // If current node is a rule or at-rule, then perform additional checks to ensure
  438         // emmet suggestions are not provided in the rule selector
  439         if (currentNode.type !== 'rule' && currentNode.type !== 'at-rule') {
  440             return true;
  441         }
  442 
  443         const currentCssNode = <Rule>currentNode;
  444 
  445         // Position is valid if it occurs after the `{` that marks beginning of rule contents
  446         if (position.isAfter(currentCssNode.contentStartToken.end)) {
  447             return true;
  448         }
  449 
  450         // Workaround for https://github.com/Microsoft/vscode/30188
  451         // The line above the rule selector is considered as part of the selector by the css-parser
  452         // But we should assume it is a valid location for css properties under the parent rule
  453         if (currentCssNode.parent
  454             && (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule')
  455             && currentCssNode.selectorToken
  456             && position.line !== currentCssNode.selectorToken.end.line
  457             && currentCssNode.selectorToken.start.character === abbreviationRange.start.character
  458             && currentCssNode.selectorToken.start.line === abbreviationRange.start.line
  459         ) {
  460             return true;
  461         }
  462 
  463         return false;
  464     }
  465 
  466     const startAngle = '<';
  467     const endAngle = '>';
  468     const escape = '\\';
  469     const question = '?';
  470     const currentHtmlNode = <HtmlNode>currentNode;
  471     let start = new vscode.Position(0, 0);
  472 
  473     if (currentHtmlNode) {
  474         if (currentHtmlNode.name === 'script') {
  475             const typeAttribute = (currentHtmlNode.attributes || []).filter(x => x.name.toString() === 'type')[0];
  476             const typeValue = typeAttribute ? typeAttribute.value.toString() : '';
  477 
  478             if (allowedMimeTypesInScriptTag.indexOf(typeValue) > -1) {
  479                 return true;
  480             }
  481 
  482             const isScriptJavascriptType = !typeValue || typeValue === 'application/javascript' || typeValue === 'text/javascript';
  483             if (isScriptJavascriptType) {
  484                 return !!getSyntaxFromArgs({ language: 'javascript' });
  485             }
  486             return false;
  487         }
  488 
  489         const innerRange = getInnerRange(currentHtmlNode);
  490 
  491         // Fix for https://github.com/Microsoft/vscode/issues/28829
  492         if (!innerRange || !innerRange.contains(position)) {
  493             return false;
  494         }
  495 
  496         // Fix for https://github.com/Microsoft/vscode/issues/35128
  497         // Find the position up till where we will backtrack looking for unescaped < or >
  498         // to decide if current position is valid for emmet expansion
  499         start = innerRange.start;
  500         let lastChildBeforePosition = currentHtmlNode.firstChild;
  501         while (lastChildBeforePosition) {
  502             if (lastChildBeforePosition.end.isAfter(position)) {
  503                 break;
  504             }
  505             start = lastChildBeforePosition.end;
  506             lastChildBeforePosition = lastChildBeforePosition.nextSibling;
  507         }
  508     }
  509     let textToBackTrack = document.getText(new vscode.Range(start.line, start.character, abbreviationRange.start.line, abbreviationRange.start.character));
  510 
  511     // Worse case scenario is when cursor is inside a big chunk of text which needs to backtracked
  512     // Backtrack only 500 offsets to ensure we dont waste time doing this
  513     if (textToBackTrack.length > 500) {
  514         textToBackTrack = textToBackTrack.substr(textToBackTrack.length - 500);
  515     }
  516 
  517     if (!textToBackTrack.trim()) {
  518         return true;
  519     }
  520 
  521     let valid = true;
  522     let foundSpace = false; // If < is found before finding whitespace, then its valid abbreviation. E.g.: <div|
  523     let i = textToBackTrack.length - 1;
  524     if (textToBackTrack[i] === startAngle) {
  525         return false;
  526     }
  527 
  528     while (i >= 0) {
  529         const char = textToBackTrack[i];
  530         i--;
  531         if (!foundSpace && /\s/.test(char)) {
  532             foundSpace = true;
  533             continue;
  534         }
  535         if (char === question && textToBackTrack[i] === startAngle) {
  536             i--;
  537             continue;
  538         }
  539         // Fix for https://github.com/Microsoft/vscode/issues/55411
  540         // A space is not a valid character right after < in a tag name.
  541         if (/\s/.test(char) && textToBackTrack[i] === startAngle) {
  542             i--;
  543             continue;
  544         }
  545         if (char !== startAngle && char !== endAngle) {
  546             continue;
  547         }
  548         if (i >= 0 && textToBackTrack[i] === escape) {
  549             i--;
  550             continue;
  551         }
  552         if (char === endAngle) {
  553             if (i >= 0 && textToBackTrack[i] === '=') {
  554                 continue; // False alarm of cases like =>
  555             } else {
  556                 break;
  557             }
  558         }
  559         if (char === startAngle) {
  560             valid = !foundSpace;
  561             break;
  562         }
  563     }
  564 
  565     return valid;
  566 }
  567 
  568 /**
  569  * Expands abbreviations as detailed in expandAbbrList in the editor
  570  *
  571  * @returns false if no snippet can be inserted.
  572  */
  573 function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Thenable<boolean> {
  574     if (!expandAbbrList || expandAbbrList.length === 0) {
  575         return Promise.resolve(false);
  576     }
  577 
  578     // Snippet to replace at multiple cursors are not the same
  579     // `editor.insertSnippet` will have to be called for each instance separately
  580     // We will not be able to maintain multiple cursors after snippet insertion
  581     let insertPromises: Thenable<boolean>[] = [];
  582     if (!insertSameSnippet) {
  583         expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); }).forEach((expandAbbrInput: ExpandAbbreviationInput) => {
  584             let expandedText = expandAbbr(expandAbbrInput);
  585             if (expandedText) {
  586                 insertPromises.push(editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false }));
  587             }
  588         });
  589         if (insertPromises.length === 0) {
  590             return Promise.resolve(false);
  591         }
  592         return Promise.all(insertPromises).then(() => Promise.resolve(true));
  593     }
  594 
  595     // Snippet to replace at all cursors are the same
  596     // We can pass all ranges to `editor.insertSnippet` in a single call so that
  597     // all cursors are maintained after snippet insertion
  598     const anyExpandAbbrInput = expandAbbrList[0];
  599     let expandedText = expandAbbr(anyExpandAbbrInput);
  600     let allRanges = expandAbbrList.map(value => {
  601         return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
  602     });
  603     if (expandedText) {
  604         return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
  605     }
  606     return Promise.resolve(false);
  607 }
  608 
  609 /*
  610 * Walks the tree rooted at root and apply function fn on each node.
  611 * if fn return false at any node, the further processing of tree is stopped.
  612 */
  613 function walk(root: any, fn: ((node: any) => boolean)): boolean {
  614     let ctx = root;
  615     while (ctx) {
  616 
  617         let next = ctx.next;
  618         if (fn(ctx) === false || walk(ctx.firstChild, fn) === false) {
  619             return false;
  620         }
  621 
  622         ctx = next;
  623     }
  624 
  625     return true;
  626 }
  627 
  628 /**
  629  * Expands abbreviation as detailed in given input.
  630  */
  631 function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
  632     const helper = getEmmetHelper();
  633     const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter);
  634 
  635     if (input.textToWrap) {
  636         if (input.filter && input.filter.indexOf('t') > -1) {
  637             input.textToWrap = input.textToWrap.map(line => {
  638                 return line.replace(trimRegex, '').trim();
  639             });
  640         }
  641         expandOptions['text'] = input.textToWrap;
  642 
  643         // Below fixes https://github.com/Microsoft/vscode/issues/29898
  644         // With this, Emmet formats inline elements as block elements
  645         // ensuring the wrapped multi line text does not get merged to a single line
  646         if (!input.rangeToReplace.isSingleLine) {
  647             expandOptions.profile['inlineBreak'] = 1;
  648         }
  649     }
  650 
  651     let expandedText;
  652     try {
  653         // Expand the abbreviation
  654 
  655         if (input.textToWrap) {
  656             let parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions);
  657             if (input.rangeToReplace.isSingleLine && input.textToWrap.length === 1) {
  658 
  659                 // Fetch rightmost element in the parsed abbreviation (i.e the element that will contain the wrapped text).
  660                 let wrappingNode = parsedAbbr;
  661                 while (wrappingNode && wrappingNode.children && wrappingNode.children.length > 0) {
  662                     wrappingNode = wrappingNode.children[wrappingNode.children.length - 1];
  663                 }
  664 
  665                 // If wrapping with a block element, insert newline in the text to wrap.
  666                 if (wrappingNode && inlineElements.indexOf(wrappingNode.name) === -1 && (expandOptions['profile'].hasOwnProperty('format') ? expandOptions['profile'].format : true)) {
  667                     wrappingNode.value = '\n\t' + wrappingNode.value + '\n';
  668                 }
  669             }
  670 
  671             // Below fixes https://github.com/microsoft/vscode/issues/78219
  672             // walk the tree and remove tags for empty values
  673             walk(parsedAbbr, node => {
  674                 if (node.name !== null && node.value === '' && !node.isSelfClosing && node.children.length === 0) {
  675                     node.name = '';
  676                     node.value = '\n';
  677                 }
  678 
  679                 return true;
  680             });
  681 
  682             expandedText = helper.expandAbbreviation(parsedAbbr, expandOptions);
  683             // All $anyword would have been escaped by the emmet helper.
  684             // Remove the escaping backslash from $TM_SELECTED_TEXT so that VS Code Snippet controller can treat it as a variable
  685             expandedText = expandedText.replace('\\$TM_SELECTED_TEXT', '$TM_SELECTED_TEXT');
  686         } else {
  687             expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
  688         }
  689 
  690     } catch (e) {
  691         vscode.window.showErrorMessage('Failed to expand abbreviation');
  692     }
  693 
  694     return expandedText;
  695 }
  696 
  697 export function getSyntaxFromArgs(args: { [x: string]: string }): string | undefined {
  698     const mappedModes = getMappingForIncludedLanguages();
  699     const language: string = args['language'];
  700     const parentMode: string = args['parentMode'];
  701     const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
  702     if (excludedLanguages.indexOf(language) > -1) {
  703         return;
  704     }
  705 
  706     let syntax = getEmmetMode((mappedModes[language] ? mappedModes[language] : language), excludedLanguages);
  707     if (!syntax) {
  708         syntax = getEmmetMode((mappedModes[parentMode] ? mappedModes[parentMode] : parentMode), excludedLanguages);
  709     }
  710 
  711     return syntax;
  712 }