Path: blob/main/extensions/emmet/src/defaultCompletionProvider.ts
4772 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as vscode from 'vscode';6import { Node, Stylesheet } from 'EmmetFlatNode';7import { isValidLocationForEmmetAbbreviation, getSyntaxFromArgs } from './abbreviationActions';8import { getEmmetHelper, getMappingForIncludedLanguages, parsePartialStylesheet, getEmmetConfiguration, getEmmetMode, isStyleSheet, getFlatNode, allowedMimeTypesInScriptTag, toLSTextDocument, getHtmlFlatNode, getEmbeddedCssNodeIfAny } from './util';9import { Range as LSRange } from 'vscode-languageserver-textdocument';10import { getRootNode } from './parseDocument';1112export class DefaultCompletionItemProvider implements vscode.CompletionItemProvider {1314private lastCompletionType: string | undefined;1516public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, _: vscode.CancellationToken, context: vscode.CompletionContext): Thenable<vscode.CompletionList | undefined> | undefined {17const completionResult = this.provideCompletionItemsInternal(document, position, context);18if (!completionResult) {19this.lastCompletionType = undefined;20return;21}2223return completionResult.then(completionList => {24if (!completionList || !completionList.items.length) {25this.lastCompletionType = undefined;26return completionList;27}28const item = completionList.items[0];29const expandedText = item.documentation ? item.documentation.toString() : '';3031if (expandedText.startsWith('<')) {32this.lastCompletionType = 'html';33} else if (expandedText.indexOf(':') > 0 && expandedText.endsWith(';')) {34this.lastCompletionType = 'css';35} else {36this.lastCompletionType = undefined;37}38return completionList;39});40}4142private provideCompletionItemsInternal(document: vscode.TextDocument, position: vscode.Position, context: vscode.CompletionContext): Thenable<vscode.CompletionList | undefined> | undefined {43const emmetConfig = vscode.workspace.getConfiguration('emmet');44const excludedLanguages = emmetConfig['excludeLanguages'] ? emmetConfig['excludeLanguages'] : [];45if (excludedLanguages.includes(document.languageId)) {46return;47}4849const mappedLanguages = getMappingForIncludedLanguages();50const isSyntaxMapped = mappedLanguages[document.languageId] ? true : false;51const emmetMode = getEmmetMode((isSyntaxMapped ? mappedLanguages[document.languageId] : document.languageId), mappedLanguages, excludedLanguages);5253if (!emmetMode54|| emmetConfig['showExpandedAbbreviation'] === 'never'55|| ((isSyntaxMapped || emmetMode === 'jsx') && emmetConfig['showExpandedAbbreviation'] !== 'always')) {56return;57}5859let syntax = emmetMode;6061let validateLocation = syntax === 'html' || syntax === 'jsx' || syntax === 'xml';62let rootNode: Node | undefined;63let currentNode: Node | undefined;6465const lsDoc = toLSTextDocument(document);66position = document.validatePosition(position);6768// Don't show completions if there's a comment at the beginning of the line69const lineRange = new vscode.Range(position.line, 0, position.line, position.character);70if (document.getText(lineRange).trimStart().startsWith('//')) {71return;72}7374const helper = getEmmetHelper();75if (syntax === 'html') {76if (context.triggerKind === vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) {77switch (this.lastCompletionType) {78case 'html':79validateLocation = false;80break;81case 'css':82validateLocation = false;83syntax = 'css';84break;85default:86break;87}88}89if (validateLocation) {90const positionOffset = document.offsetAt(position);91const emmetRootNode = getRootNode(document, true);92const foundNode = getHtmlFlatNode(document.getText(), emmetRootNode, positionOffset, false);93if (foundNode) {94if (foundNode.name === 'script') {95const typeNode = foundNode.attributes.find(attr => attr.name.toString() === 'type');96if (typeNode) {97const typeAttrValue = typeNode.value.toString();98if (typeAttrValue === 'application/javascript' || typeAttrValue === 'text/javascript') {99if (!getSyntaxFromArgs({ language: 'javascript' })) {100return;101} else {102validateLocation = false;103}104}105else if (allowedMimeTypesInScriptTag.includes(typeAttrValue)) {106validateLocation = false;107}108} else {109return;110}111}112else if (foundNode.name === 'style') {113syntax = 'css';114validateLocation = false;115} else {116const styleNode = foundNode.attributes.find(attr => attr.name.toString() === 'style');117if (styleNode && styleNode.value.start <= positionOffset && positionOffset <= styleNode.value.end) {118syntax = 'css';119validateLocation = false;120}121}122}123}124}125126const expandOptions = isStyleSheet(syntax) ?127{ lookAhead: false, syntax: 'stylesheet' } :128{ lookAhead: true, syntax: 'markup' };129const extractAbbreviationResults = helper.extractAbbreviation(lsDoc, position, expandOptions);130if (!extractAbbreviationResults || !helper.isAbbreviationValid(syntax, extractAbbreviationResults.abbreviation)) {131return;132}133134const offset = document.offsetAt(position);135if (isStyleSheet(document.languageId) && context.triggerKind !== vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) {136validateLocation = true;137const usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;138rootNode = usePartialParsing && document.lineCount > 1000 ? parsePartialStylesheet(document, position) : <Stylesheet>getRootNode(document, true);139if (!rootNode) {140return;141}142currentNode = getFlatNode(rootNode, offset, true);143}144145// Fix for https://github.com/microsoft/vscode/issues/107578146// Validate location if syntax is of styleSheet type to ensure that location is valid for emmet abbreviation.147// For an html document containing a <style> node, compute the embeddedCssNode and fetch the flattened node as currentNode.148if (!isStyleSheet(document.languageId) && isStyleSheet(syntax) && context.triggerKind !== vscode.CompletionTriggerKind.TriggerForIncompleteCompletions) {149validateLocation = true;150rootNode = getRootNode(document, true);151if (!rootNode) {152return;153}154const flatNode = getFlatNode(rootNode, offset, true);155const embeddedCssNode = getEmbeddedCssNodeIfAny(document, flatNode, position);156currentNode = getFlatNode(embeddedCssNode, offset, true);157}158159if (validateLocation && !isValidLocationForEmmetAbbreviation(document, rootNode, currentNode, syntax, offset, toRange(extractAbbreviationResults.abbreviationRange))) {160return;161}162163let isNoisePromise: Thenable<boolean> = Promise.resolve(false);164165// Fix for https://github.com/microsoft/vscode/issues/32647166// Check for document symbols in js/ts/jsx/tsx and avoid triggering emmet for abbreviations of the form symbolName.sometext167// Presence of > or * or + in the abbreviation denotes valid abbreviation that should trigger emmet168if (!isStyleSheet(syntax) && (document.languageId === 'javascript' || document.languageId === 'javascriptreact' || document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {169const abbreviation: string = extractAbbreviationResults.abbreviation;170// For the second condition, we don't want abbreviations that have [] characters but not ='s in them to expand171// In turn, users must explicitly expand abbreviations of the form Component[attr1 attr2], but it means we don't try to expand a[i].172if (abbreviation.startsWith('this.') || /\[[^\]=]*\]/.test(abbreviation)) {173isNoisePromise = Promise.resolve(true);174} else {175isNoisePromise = vscode.commands.executeCommand<vscode.SymbolInformation[] | undefined>('vscode.executeDocumentSymbolProvider', document.uri).then(symbols => {176return !!symbols && symbols.some(x => abbreviation === x.name || (abbreviation.startsWith(x.name + '.') && !/>|\*|\+/.test(abbreviation)));177});178}179}180181return isNoisePromise.then((isNoise): vscode.CompletionList | undefined => {182if (isNoise) {183return undefined;184}185186const config = getEmmetConfiguration(syntax!);187const result = helper.doComplete(toLSTextDocument(document), position, syntax, config);188189const newItems: vscode.CompletionItem[] = [];190if (result && result.items) {191result.items.forEach((item: any) => {192const newItem = new vscode.CompletionItem(item.label);193newItem.documentation = item.documentation;194newItem.detail = item.detail;195newItem.insertText = new vscode.SnippetString(item.textEdit.newText);196const oldrange = item.textEdit.range;197newItem.range = new vscode.Range(oldrange.start.line, oldrange.start.character, oldrange.end.line, oldrange.end.character);198199newItem.filterText = item.filterText;200newItem.sortText = item.sortText;201202if (emmetConfig['showSuggestionsAsSnippets'] === true) {203newItem.kind = vscode.CompletionItemKind.Snippet;204}205newItems.push(newItem);206});207}208209return new vscode.CompletionList(newItems, true);210});211}212}213214function toRange(lsRange: LSRange) {215return new vscode.Range(lsRange.start.line, lsRange.start.character, lsRange.end.line, lsRange.end.character);216}217218219