import { doComplete as doEmmetComplete } from 'vscode-emmet-helper' ; import { getLanguageService , HTMLDocument , CompletionItem as HtmlCompletionItem , Node } from 'vscode-html-languageservice' ; import { CompletionList , Hover , Position , SymbolInformation , CompletionItem , CompletionItemKind , TextEdit , Range , WorkspaceEdit , LinkedEditingRanges } from 'vscode-languageserver' ; import { DocumentManager , Document , isInTag , getNodeIfIsInComponentStartTag } from '../../lib/documents' ; import { LSConfigManager , LSHTMLConfig } from '../../ls-config' ; import { svelteHtmlDataProvider } from './dataProvider' ; import { HoverProvider , CompletionsProvider , RenameProvider , LinkedEditingRangesProvider } from '../interfaces' ; import { isInsideMoustacheTag , toRange } from '../../lib/documents/utils' ; import { possiblyComponent } from '../../utils' ; export class HTMLPlugin implements HoverProvider , CompletionsProvider , RenameProvider , LinkedEditingRangesProvider { private configManager : LSConfigManager ; private lang = getLanguageService ( { customDataProviders : [ svelteHtmlDataProvider ] , useDefaultDataProvider : false } ) ; private documents = new WeakMap < Document , HTMLDocument > ( ) ; private styleScriptTemplate = new Set ( [ 'template' , 'style' , 'script' ] ) ; constructor ( docManager , configManager ) { this . configManager = configManager ; docManager . on ( 'documentChange' , ( document ) => { this . documents . set ( document , document . html ) ; } ) ; } doHover ( document , position ) { if ( ! this . featureEnabled ( 'hover' ) ) { return null ; } const html = this . documents . get ( document ) ; if ( ! html ) { return null ; } const node = html . findNodeAt ( document . offsetAt ( position ) ) ; if ( ! node || possiblyComponent ( node ) ) { return null ; } return this . lang . doHover ( document , position , html ) ; } getCompletions ( document , position ) { if ( ! this . featureEnabled ( 'completions' ) ) { return null ; } const html = this . documents . get ( document ) ; if ( ! html ) { return null ; } if ( this . isInsideMoustacheTag ( html , document , position ) || isInTag ( position , document . scriptInfo ) || isInTag ( position , document . moduleScriptInfo ) ) { return null ; } let emmetResults = { isIncomplete : false , items : [ ] } ; if ( this . configManager . getConfig ( ) . html . completions . emmet && this . configManager . getEmmetConfig ( ) . showExpandedAbbreviation !== 'never' ) { this . lang . setCompletionParticipants ( [ { onHtmlContent : ( ) => ( emmetResults = doEmmetComplete ( document , position , 'html' , this . configManager . getEmmetConfig ( ) ) || emmetResults ) } ] ) ; } const results = this . isInComponentTag ( html , document , position ) ? CompletionList . create ( [ ] ) : this . lang . doComplete ( document , position , html ) ; const items = this . toCompletionItems ( results . items ) ; items . forEach ( ( item ) => { if ( item . label . startsWith ( 'on:' ) && item . textEdit ) { item . textEdit = { ... item . textEdit , newText : item . textEdit . newText . replace ( '="$1"' , '$2="$1"' ) } ; } } ) ; return CompletionList . create ( [ ... this . toCompletionItems ( items ) , ... this . getLangCompletions ( items ) , ... emmetResults . items ] , emmetResults . items . length > 0 ) ; } /**
     * The HTML language service uses newer types which clash
     * without the stable ones. Transform to the stable types.
     */ private toCompletionItems ( items ) { return items . map ( ( item ) => { if ( ! item . textEdit || TextEdit . is ( item . textEdit ) ) { return < CompletionItem > item ; } return { ... item , textEdit : TextEdit . replace ( item . textEdit . replace , item . textEdit . newText ) } ; } ) ; } private isInComponentTag ( html , document , position ) { return ! ! getNodeIfIsInComponentStartTag ( html , document . offsetAt ( position ) ) ; } private getLangCompletions ( completions ) { const styleScriptTemplateCompletions = completions . filter ( ( completion ) => completion . kind === CompletionItemKind . Property && this . styleScriptTemplate . has ( completion . label ) ) ; const langCompletions = [ ] ; addLangCompletion ( 'script' , [ 'ts' ] ) ; addLangCompletion ( 'style' , [ 'less' , 'scss' ] ) ; addLangCompletion ( 'template' , [ 'pug' ] ) ; return langCompletions ; function addLangCompletion ( tag , languages ) { const existingCompletion = styleScriptTemplateCompletions . find ( ( completion ) => completion . label === tag ) ; if ( ! existingCompletion ) { return ; } languages . forEach ( ( lang ) => langCompletions . push ( { ... existingCompletion , label : `${ tag } (lang="${ lang }")` , insertText : existingCompletion . insertText && `${ existingCompletion . insertText } lang="${ lang }"` , textEdit : existingCompletion . textEdit && TextEdit . is ( existingCompletion . textEdit ) ? { range : existingCompletion . textEdit . range , newText : `${ existingCompletion . textEdit . newText } lang="${ lang }"` } : undefined } ) ) ; } } doTagComplete ( document , position ) { if ( ! this . featureEnabled ( 'tagComplete' ) ) { return null ; } const html = this . documents . get ( document ) ; if ( ! html ) { return null ; } if ( this . isInsideMoustacheTag ( html , document , position ) ) { return null ; } return this . lang . doTagComplete ( document , position , html ) ; } private isInsideMoustacheTag ( html , document , position ) { const offset = document . offsetAt ( position ) ; const node = html . findNodeAt ( offset ) ; return isInsideMoustacheTag ( document . getText ( ) , node . start , offset ) ; } getDocumentSymbols ( document ) { if ( ! this . featureEnabled ( 'documentSymbols' ) ) { return [ ] ; } const html = this . documents . get ( document ) ; if ( ! html ) { return [ ] ; } return this . lang . findDocumentSymbols ( document , html ) ; } rename ( document , position , newName ) { if ( ! this . featureEnabled ( 'renameTags' ) ) { return null ; } const html = this . documents . get ( document ) ; if ( ! html ) { return null ; } const node = html . findNodeAt ( document . offsetAt ( position ) ) ; if ( ! node || possiblyComponent ( node ) ) { return null ; } return this . lang . doRename ( document , position , newName , html ) ; } prepareRename ( document , position ) { if ( ! this . featureEnabled ( 'renameTags' ) ) { return null ; } const html = this . documents . get ( document ) ; if ( ! html ) { return null ; } const offset = document . offsetAt ( position ) ; const node = html . findNodeAt ( offset ) ; if ( ! node || possiblyComponent ( node ) || ! node . tag || ! this . isRenameAtTag ( node , offset ) ) { return null ; } const tagNameStart = node . start + '<' . length ; return toRange ( document . getText ( ) , tagNameStart , tagNameStart + node . tag . length ) ; } getLinkedEditingRanges ( document , position ) { if ( ! this . featureEnabled ( 'linkedEditing' ) ) { return null ; } const html = this . documents . get ( document ) ; if ( ! html ) { return null ; } const ranges = this . lang . findLinkedEditingRanges ( document , position , html ) ; if ( ! ranges ) { return null ; } return { ranges } ; } /**
     * Returns true if rename happens at the tag name, not anywhere inbetween.
     */ private isRenameAtTag ( node , offset ) { if ( ! node . tag ) { return false ; } const startTagNameEnd = node . start + `<${ node . tag }` . length ; const isAtStartTag = offset > node . start && offset <= startTagNameEnd ; const isAtEndTag = node . endTagStart !== undefined && offset >= node . endTagStart && offset < node . end ; return isAtStartTag || isAtEndTag ; } private featureEnabled ( feature ) { return ( this . configManager . enabled ( 'html.enable' ) && this . configManager . enabled ( `html.${ feature }.enable` ) ) ; } }