diff --git a/extensions/handlebars/package.json b/extensions/handlebars/package.json index 0eb6cb2eb2b6e..88976cf36ff61 100644 --- a/extensions/handlebars/package.json +++ b/extensions/handlebars/package.json @@ -36,6 +36,12 @@ "scopeName": "text.html.handlebars", "path": "./syntaxes/Handlebars.tmLanguage.json" } + ], + "htmlLanguageParticipants": [ + { + "languageId": "handlebars", + "autoInsert": true + } ] }, "repository": { diff --git a/extensions/html-language-features/client/src/autoInsertion.ts b/extensions/html-language-features/client/src/autoInsertion.ts index 170afa46c0280..e95e6a64a09db 100644 --- a/extensions/html-language-features/client/src/autoInsertion.ts +++ b/extensions/html-language-features/client/src/autoInsertion.ts @@ -5,8 +5,9 @@ import { window, workspace, Disposable, TextDocument, Position, SnippetString, TextDocumentChangeEvent, TextDocumentChangeReason, TextDocumentContentChangeEvent } from 'vscode'; import { Runtime } from './htmlClient'; +import { LanguageParticipants } from './languageParticipants'; -export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position) => Thenable, supportedLanguages: { [id: string]: boolean }, runtime: Runtime): Disposable { +export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position) => Thenable, languageParticipants: LanguageParticipants, runtime: Runtime): Disposable { const disposables: Disposable[] = []; workspace.onDidChangeTextDocument(onDidChangeTextDocument, null, disposables); @@ -33,7 +34,7 @@ export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose' return; } const document = editor.document; - if (!supportedLanguages[document.languageId]) { + if (!languageParticipants.useAutoInsert(document.languageId)) { return; } const configurations = workspace.getConfiguration(undefined, document.uri); diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts index ab23520fe79f1..fb8cc9071a8dc 100644 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, ExtensionContext, Uri } from 'vscode'; -import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor } from '../htmlClient'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; import { LanguageClient } from 'vscode-languageclient/browser'; declare const Worker: { @@ -15,7 +15,7 @@ declare const TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string }; }; -let client: BaseLanguageClient | undefined; +let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { @@ -42,7 +42,7 @@ export async function activate(context: ExtensionContext) { export async function deactivate(): Promise { if (client) { - await client.stop(); + await client.dispose(); client = undefined; } } diff --git a/extensions/html-language-features/client/src/customData.ts b/extensions/html-language-features/client/src/customData.ts index 80e5f2f04d981..71d847a4f391a 100644 --- a/extensions/html-language-features/client/src/customData.ts +++ b/extensions/html-language-features/client/src/customData.ts @@ -125,7 +125,7 @@ function collectInWorkspaces(workspaceUris: Set): Set { } function collectInExtensions(localExtensionUris: Set, externalUris: Set): void { - for (const extension of extensions.all) { + for (const extension of extensions.allAcrossExtensionHosts) { const customData = extension.packageJSON?.contributes?.html?.customData; if (Array.isArray(customData)) { for (const uriOrPath of customData) { diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index 60accb3870b13..d5cf527713f24 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -9,7 +9,7 @@ const localize = nls.loadMessageBundle(); import { languages, ExtensionContext, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions, Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend, - DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands + DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands, OutputChannel } from 'vscode'; import { LanguageClientOptions, RequestType, DocumentRangeFormattingParams, @@ -18,6 +18,7 @@ import { import { FileSystemProvider, serveFileSystemRequests } from './requests'; import { getCustomDataSource } from './customData'; import { activateAutoInsertion } from './autoInsertion'; +import { getLanguageParticipants, LanguageParticipants } from './languageParticipants'; namespace CustomDataChangedNotification { export const type: NotificationType = new NotificationType('html/customDataChanged'); @@ -74,6 +75,8 @@ export interface TelemetryReporter { export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; +export const languageServerDescription = localize('htmlserver.name', 'HTML Language Server'); + export interface Runtime { TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } }; fileFs?: FileSystemProvider; @@ -83,11 +86,69 @@ export interface Runtime { }; } -export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { +export interface AsyncDisposable { + dispose(): Promise; +} + +export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { + + const outputChannel = window.createOutputChannel(languageServerDescription); + + const languageParticipants = getLanguageParticipants(); + context.subscriptions.push(languageParticipants); + + let client: Disposable | undefined = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime); + + 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 && languageParticipants.hasLanguage(e.document.languageId)) { + 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); + } + } + }); + context.subscriptions.push(activeEditorListener); + } + } + + let restartTrigger: Disposable | undefined; + languageParticipants.onDidChange(() => { + if (restartTrigger) { + restartTrigger.dispose(); + } + restartTrigger = runtime.timer.setTimeout(async () => { + if (client) { + outputChannel.appendLine('Extensions have changed, restarting HTML server...'); + outputChannel.appendLine(''); + const oldClient = client; + client = undefined; + await oldClient.dispose(); + client = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime); + } + }, 2000); + }); + + return { + dispose: async () => { + restartTrigger?.dispose(); + await client?.dispose(); + outputChannel.dispose(); + } + }; +} + +async function startClientWithParticipants(languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise { - const toDispose = context.subscriptions; + const toDispose: Disposable[] = []; - const documentSelector = ['html', 'handlebars']; + const documentSelector = languageParticipants.documentSelector; const embeddedLanguages = { css: true, javascript: true }; let rangeFormatting: Disposable | undefined = undefined; @@ -129,22 +190,23 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } } }; + clientOptions.outputChannel = outputChannel; // Create the language client and start the client. - const client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions); + const client = newLanguageClient('html', languageServerDescription, clientOptions); client.registerProposedFeatures(); await client.start(); toDispose.push(serveFileSystemRequests(client, runtime)); - const customDataSource = getCustomDataSource(runtime, context.subscriptions); + const customDataSource = getCustomDataSource(runtime, toDispose); client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); customDataSource.onDidChange(() => { client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); - }); - client.onRequest(CustomDataContent.type, customDataSource.getContent); + }, undefined, toDispose); + toDispose.push(client.onRequest(CustomDataContent.type, customDataSource.getContent)); const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise => { @@ -155,7 +217,8 @@ export async function startClient(context: ExtensionContext, newLanguageClient: }; return client.sendRequest(AutoInsertRequest.type, param); }; - const disposable = activateAutoInsertion(insertRequestor, { html: true, handlebars: true }, runtime); + + const disposable = activateAutoInsertion(insertRequestor, languageParticipants, runtime); toDispose.push(disposable); const disposable2 = client.onTelemetry(e => { @@ -193,7 +256,6 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } }); - function updateFormatterRegistration() { const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable); if (!formatEnabled && rangeFormatting) { @@ -278,25 +340,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } })); - 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); + return { + dispose: async () => { + await client.stop(); + toDispose.forEach(d => d.dispose()); + rangeFormatting?.dispose(); } - } - - return client; + }; } diff --git a/extensions/html-language-features/client/src/languageParticipants.ts b/extensions/html-language-features/client/src/languageParticipants.ts new file mode 100644 index 0000000000000..6abe49237c806 --- /dev/null +++ b/extensions/html-language-features/client/src/languageParticipants.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DocumentSelector } from 'vscode-languageclient'; +import { Event, EventEmitter, extensions } from 'vscode'; + +/** + * HTML language participant contribution. + */ +interface LanguageParticipantContribution { + /** + * The id of the language which participates with the HTML language server. + */ + languageId: string; + /** + * true if the language activates the auto insertion and false otherwise. + */ + autoInsert?: boolean; +} + +export interface LanguageParticipants { + readonly onDidChange: Event; + readonly documentSelector: DocumentSelector; + hasLanguage(languageId: string): boolean; + useAutoInsert(languageId: string): boolean; + dispose(): void; +} + +export function getLanguageParticipants(): LanguageParticipants { + const onDidChangeEmmiter = new EventEmitter(); + let languages = new Set(); + let autoInsert = new Set(); + + function update() { + const oldLanguages = languages, oldAutoInsert = autoInsert; + + languages = new Set(); + languages.add('html'); + autoInsert = new Set(); + autoInsert.add('html'); + + for (const extension of extensions.allAcrossExtensionHosts) { + const htmlLanguageParticipants = extension.packageJSON?.contributes?.htmlLanguageParticipants as LanguageParticipantContribution[]; + if (Array.isArray(htmlLanguageParticipants)) { + for (const htmlLanguageParticipant of htmlLanguageParticipants) { + const languageId = htmlLanguageParticipant.languageId; + if (typeof languageId === 'string') { + languages.add(languageId); + if (htmlLanguageParticipant.autoInsert !== false) { + autoInsert.add(languageId); + } + } + } + } + } + return !isEqualSet(languages, oldLanguages) || !isEqualSet(oldLanguages, oldAutoInsert); + } + update(); + + const changeListener = extensions.onDidChange(_ => { + if (update()) { + onDidChangeEmmiter.fire(); + } + }); + + return { + onDidChange: onDidChangeEmmiter.event, + get documentSelector() { return Array.from(languages); }, + hasLanguage(languageId: string) { return languages.has(languageId); }, + useAutoInsert(languageId: string) { return autoInsert.has(languageId); }, + dispose: () => changeListener.dispose() + }; +} + +function isEqualSet(s1: Set, s2: Set) { + if (s1.size !== s2.size) { + return false; + } + for (const e of s1) { + if (!s2.has(e)) { + return false; + } + } + return true; +} diff --git a/extensions/html-language-features/client/src/node/htmlClientMain.ts b/extensions/html-language-features/client/src/node/htmlClientMain.ts index 11ca6f254f997..c3e8c85cf4fd9 100644 --- a/extensions/html-language-features/client/src/node/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/node/htmlClientMain.ts @@ -5,15 +5,15 @@ import { getNodeFileFS } from './nodeFs'; import { Disposable, ExtensionContext } from 'vscode'; -import { startClient, LanguageClientConstructor } from '../htmlClient'; -import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; +import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; +import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; import { TextDecoder } from 'util'; import * as fs from 'fs'; import TelemetryReporter from '@vscode/extension-telemetry'; let telemetry: TelemetryReporter | undefined; -let client: BaseLanguageClient | undefined; +let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { @@ -50,7 +50,7 @@ export async function activate(context: ExtensionContext) { export async function deactivate(): Promise { if (client) { - await client.stop(); + await client.dispose(); client = undefined; } } diff --git a/extensions/html-language-features/client/tsconfig.json b/extensions/html-language-features/client/tsconfig.json index 573b24b4aa636..8f5cef74fd3a3 100644 --- a/extensions/html-language-features/client/tsconfig.json +++ b/extensions/html-language-features/client/tsconfig.json @@ -5,6 +5,7 @@ }, "include": [ "src/**/*", - "../../../src/vscode-dts/vscode.d.ts" + "../../../src/vscode-dts/vscode.d.ts", + "../../../src/vscode-dts/vscode.proposed.extensionsAny.d.ts" ] } diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index b3731cd0acd3e..7e4120eeb05b6 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -14,6 +14,9 @@ "onLanguage:html", "onLanguage:handlebars" ], + "enabledApiProposals": [ + "extensionsAny" + ], "main": "./client/out/node/htmlClientMain", "browser": "./client/dist/browser/htmlClientMain", "capabilities": { diff --git a/extensions/html-language-features/schemas/package.schema.json b/extensions/html-language-features/schemas/package.schema.json index a4d8715b918f5..ef717dbd1d14d 100644 --- a/extensions/html-language-features/schemas/package.schema.json +++ b/extensions/html-language-features/schemas/package.schema.json @@ -13,6 +13,23 @@ "type": "string", "description": "Relative path to a HTML custom data file" } + }, + "htmlLanguageParticipants": { + "type": "array", + "description": "A list of languages that participate with the HTML language server.", + "items": { + "type": "object", + "properties": { + "languageId": { + "type": "string", + "description": "The id of the language that participates with HTML language server." + }, + "autoInsert": { + "type": "boolean", + "description": "Whether the language participates with HTML auto insertions. If not specified, defaults to true." + } + } + } } } }