/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions, Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend, DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands } from 'vscode'; import { LanguageClientOptions, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams, DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, NotificationType, CommonLanguageClient } from 'vscode-languageclient'; import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared'; import { activateTagClosing } from './tagClosing'; import { RequestService } from './requests'; import { getCustomDataSource } from './customData'; namespace CustomDataChangedNotification { export const type: NotificationType = new NotificationType('html/customDataChanged'); } namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); } // experimental: semantic tokens interface SemanticTokenParams { textDocument: TextDocumentIdentifier; ranges?: LspRange[]; } namespace SemanticTokenRequest { export const type: RequestType = new RequestType('html/semanticTokens'); } namespace SemanticTokenLegendRequest { export const type: RequestType0<{ types: string[]; modifiers: string[] } | null, any> = new RequestType0('html/semanticTokenLegend'); } namespace SettingIds { export const linkedEditing = 'editor.linkedEditing'; export const formatEnable = 'html.format.enable'; } export interface TelemetryReporter { sendTelemetryEvent(eventName: string, properties?: { [key: string]: string; }, measurements?: { [key: string]: number; }): void; } export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient; export interface Runtime { TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string; } }; fs?: RequestService; telemetry?: TelemetryReporter; } export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) { let toDispose = context.subscriptions; let documentSelector = ['html', 'handlebars']; let embeddedLanguages = { css: true, javascript: true }; let rangeFormatting: Disposable | undefined = undefined; const customDataSource = getCustomDataSource(context.subscriptions); // Options to control the language client let clientOptions: LanguageClientOptions = { documentSelector, synchronize: { configurationSection: ['html', 'css', 'javascript'], // the settings to synchronize }, initializationOptions: { embeddedLanguages, handledSchemas: ['file'], provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting. }, middleware: { // testing the replace / insert mode provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult { function updateRanges(item: CompletionItem) { const range = item.range; if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) { item.range = { inserting: new Range(range.start, position), replacing: range }; } } function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined { if (r) { (Array.isArray(r) ? r : r.items).forEach(updateRanges); } return r; } const isThenable = (obj: ProviderResult): obj is Thenable => obj && (obj)['then']; const r = next(document, position, context, token); if (isThenable(r)) { return r.then(updateProposals); } return updateProposals(r); } } }; // Create the language client and start the client. let client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions); client.registerProposedFeatures(); let disposable = client.start(); toDispose.push(disposable); client.onReady().then(() => { client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); customDataSource.onDidChange(() => { client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); }); let tagRequestor = (document: TextDocument, position: Position) => { let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position); return client.sendRequest(TagCloseRequest.type, param); }; disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true }, 'html.autoClosingTags'); toDispose.push(disposable); disposable = client.onTelemetry(e => { runtime.telemetry?.sendTelemetryEvent(e.key, e.data); }); toDispose.push(disposable); // manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652. updateFormatterRegistration(); toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(SettingIds.formatEnable) && updateFormatterRegistration())); client.sendRequest(SemanticTokenLegendRequest.type).then(legend => { if (legend) { const provider: DocumentSemanticTokensProvider & DocumentRangeSemanticTokensProvider = { provideDocumentSemanticTokens(doc) { const params: SemanticTokenParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc), }; return client.sendRequest(SemanticTokenRequest.type, params).then(data => { return data && new SemanticTokens(new Uint32Array(data)); }); }, provideDocumentRangeSemanticTokens(doc, range) { const params: SemanticTokenParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc), ranges: [client.code2ProtocolConverter.asRange(range)] }; return client.sendRequest(SemanticTokenRequest.type, params).then(data => { return data && new SemanticTokens(new Uint32Array(data)); }); } }; toDispose.push(languages.registerDocumentSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers))); } }); }); function updateFormatterRegistration() { const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable); if (!formatEnabled && rangeFormatting) { rangeFormatting.dispose(); rangeFormatting = undefined; } else if (formatEnabled && !rangeFormatting) { rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, { provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult { const filesConfig = workspace.getConfiguration('files', document); const fileFormattingOptions = { trimTrailingWhitespace: filesConfig.get('trimTrailingWhitespace'), trimFinalNewlines: filesConfig.get('trimFinalNewlines'), insertFinalNewline: filesConfig.get('insertFinalNewline'), }; let params: DocumentRangeFormattingParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range: client.code2ProtocolConverter.asRange(range), options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions) }; return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( client.protocol2CodeConverter.asTextEdits, (error) => { client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []); return Promise.resolve([]); } ); } }); } } languages.setLanguageConfiguration('html', { indentationRules: { increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|keygen|link|menuitem|meta|param|source|track|wbr)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|)|\{[^}"']*$/, decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/ }, wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, onEnterRules: [ { beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'), afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i, action: { indentAction: IndentAction.IndentOutdent } }, { beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'), action: { indentAction: IndentAction.Indent } } ], }); languages.setLanguageConfiguration('handlebars', { wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, onEnterRules: [ { beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'), afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i, action: { indentAction: IndentAction.IndentOutdent } }, { beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'), action: { indentAction: IndentAction.Indent } } ], }); const regionCompletionRegExpr = /^(\s*)(<(!(-(-\s*(#\w*)?)?)?)?)?$/; const htmlSnippetCompletionRegExpr = /^(\s*)(<(h(t(m(l)?)?)?)?)?$/; languages.registerCompletionItemProvider(documentSelector, { provideCompletionItems(doc, pos) { const results: CompletionItem[] = []; let lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos)); let match = lineUntilPos.match(regionCompletionRegExpr); if (match) { let range = new Range(new Position(pos.line, match[1].length), pos); let beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet); beginProposal.range = range; beginProposal.insertText = new SnippetString(''); beginProposal.documentation = localize('folding.start', 'Folding Region Start'); beginProposal.filterText = match[2]; beginProposal.sortText = 'za'; results.push(beginProposal); let endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet); endProposal.range = range; endProposal.insertText = new SnippetString(''); endProposal.documentation = localize('folding.end', 'Folding Region End'); endProposal.filterText = match[2]; endProposal.sortText = 'zb'; results.push(endProposal); } let match2 = lineUntilPos.match(htmlSnippetCompletionRegExpr); if (match2 && doc.getText(new Range(new Position(0, 0), pos)).match(htmlSnippetCompletionRegExpr)) { let range = new Range(new Position(pos.line, match2[1].length), pos); let snippetProposal = new CompletionItem('HTML sample', CompletionItemKind.Snippet); snippetProposal.range = range; const content = ['', '', '', '\t', '\t', '\t${1:Page Title}', '\t', '\t', '\t', '', '', '\t$0', '', ''].join('\n'); snippetProposal.insertText = new SnippetString(content); snippetProposal.documentation = localize('folding.html', 'Simple HTML5 starting point'); snippetProposal.filterText = match2[2]; snippetProposal.sortText = 'za'; results.push(snippetProposal); } return results; } }); const promptForLinkedEditingKey = 'html.promptForLinkedEditing'; if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) { const config = workspace.getConfiguration('editor', { languageId: 'html' }); if (!config.get('linkedEditing') && !config.get('renameOnType')) { const activeEditorListener = window.onDidChangeActiveTextEditor(async e => { if (e && documentSelector.indexOf(e.document.languageId) !== -1) { context.globalState.update(promptForLinkedEditingKey, false); activeEditorListener.dispose(); const configure = localize('configureButton', 'Configure'); const res = await window.showInformationMessage(localize('linkedEditingQuestion', 'VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure); if (res === configure) { commands.executeCommand('workbench.action.openSettings', SettingIds.linkedEditing); } } }); toDispose.push(activeEditorListener); } } toDispose.push(); }