From 3141796f345997708b4c6858a9ef5cd8e68941bc Mon Sep 17 00:00:00 2001 From: RedCMD Date: Sat, 18 Nov 2023 20:33:48 +1300 Subject: [PATCH 1/8] Contribute to json language server with a custom language. --- .../client/src/browser/jsonClientMain.ts | 19 ++-- .../client/src/jsonClient.ts | 59 ++++++++++-- .../client/src/languageParticipants.ts | 89 +++++++++++++++++++ .../client/src/languageStatus.ts | 5 +- .../client/src/node/jsonClientMain.ts | 19 ++-- .../json-language-features/package.json | 3 +- .../server/src/jsonServer.ts | 2 +- 7 files changed, 175 insertions(+), 21 deletions(-) create mode 100644 extensions/json-language-features/client/src/languageParticipants.ts diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index fb0da9c44fa61..f7c87fbf9fa5b 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext, Uri, l10n } from 'vscode'; -import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor, SchemaRequestService } from '../jsonClient'; +import { Disposable, ExtensionContext, Uri, l10n } from 'vscode'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; declare const Worker: { @@ -14,7 +14,7 @@ declare const Worker: { declare function fetch(uri: string, options: any): any; -let client: BaseLanguageClient | undefined; +let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { @@ -36,7 +36,14 @@ export async function activate(context: ExtensionContext) { } }; - client = await startClient(context, newLanguageClient, { schemaRequests }); + const timer = { + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { + const handle = setTimeout(callback, ms, ...args); + return { dispose: () => clearTimeout(handle) }; + } + }; + + client = await startClient(context, newLanguageClient, { schemaRequests, timer }); } catch (e) { console.log(e); @@ -45,7 +52,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/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 3f191f165cfaa..ce81dcb4c9ee2 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -6,7 +6,7 @@ export type JSONLanguageStatus = { schemas: string[] }; import { - workspace, window, languages, commands, ExtensionContext, extensions, Uri, ColorInformation, + workspace, window, languages, commands, OutputChannel, ExtensionContext, extensions, Uri, ColorInformation, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n } from 'vscode'; @@ -19,6 +19,7 @@ import { import { hash } from './utils/hash'; import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus'; +import { getLanguageParticipants, LanguageParticipants } from './languageParticipants'; namespace VSCodeContentRequest { export const type: RequestType = new RequestType('vscode/content'); @@ -126,6 +127,9 @@ export type LanguageClientConstructor = (name: string, description: string, clie export interface Runtime { schemaRequests: SchemaRequestService; telemetry?: TelemetryReporter; + readonly timer: { + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; + }; } export interface SchemaRequestService { @@ -141,13 +145,51 @@ let jsoncFoldingLimit = 5000; let jsonColorDecoratorLimit = 5000; let jsoncColorDecoratorLimit = 5000; -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(context, languageParticipants, newLanguageClient, outputChannel, runtime); + + let restartTrigger: Disposable | undefined; + languageParticipants.onDidChange(() => { + if (restartTrigger) { + restartTrigger.dispose(); + } + restartTrigger = runtime.timer.setTimeout(async () => { + if (client) { + outputChannel.appendLine('Extensions have changed, restarting JSON server...'); + outputChannel.appendLine(''); + const oldClient = client; + client = undefined; + await oldClient.dispose(); + client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + } + }, 2000); + }); + + return { + dispose: async () => { + restartTrigger?.dispose(); + await client?.dispose(); + outputChannel.dispose(); + } + }; +} + +async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise { - const toDispose = context.subscriptions; + const toDispose: Disposable[] = []; let rangeFormatting: Disposable | undefined = undefined; - const documentSelector = ['json', 'jsonc']; + const documentSelector = languageParticipants.documentSelector; const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0); schemaResolutionErrorStatusBarItem.name = l10n.t('JSON: Schema Resolution Error'); @@ -306,6 +348,7 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } }; + clientOptions.outputChannel = outputChannel; // Create the language client and start the client. const client = newLanguageClient('json', languageServerDescription, clientOptions); client.registerProposedFeatures(); @@ -490,7 +533,13 @@ export async function startClient(context: ExtensionContext, newLanguageClient: }); } - return client; + return { + dispose: async () => { + await client.stop(); + toDispose.forEach(d => d.dispose()); + rangeFormatting?.dispose(); + } + }; } function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] { diff --git a/extensions/json-language-features/client/src/languageParticipants.ts b/extensions/json-language-features/client/src/languageParticipants.ts new file mode 100644 index 0000000000000..fd244fb3a2c58 --- /dev/null +++ b/extensions/json-language-features/client/src/languageParticipants.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +/** + * JSON language participant contribution. + */ +interface LanguageParticipantContribution { + /** + * The id of the language which participates with the JSON language server. + */ + languageId: string; + /** + * true if the language allows comments and false otherwise. + * TODO: implement server side setting + */ + comments?: boolean; +} + +export interface LanguageParticipants { + readonly onDidChange: Event; + readonly documentSelector: DocumentSelector; + hasLanguage(languageId: string): boolean; + useComments(languageId: string): boolean; + dispose(): void; +} + +export function getLanguageParticipants(): LanguageParticipants { + const onDidChangeEmmiter = new EventEmitter(); + let languages = new Set(); + let comments = new Set(); + + function update() { + const oldLanguages = languages, oldComments = comments; + + languages = new Set(); + languages.add('json'); + languages.add('jsonc'); + comments = new Set(); + comments.add('jsonc'); + + for (const extension of extensions.all) { + const jsonLanguageParticipants = extension.packageJSON?.contributes?.jsonLanguageParticipants as LanguageParticipantContribution[]; + if (Array.isArray(jsonLanguageParticipants)) { + for (const jsonLanguageParticipant of jsonLanguageParticipants) { + const languageId = jsonLanguageParticipant.languageId; + if (typeof languageId === 'string') { + languages.add(languageId); + if (jsonLanguageParticipant.comments === true) { + comments.add(languageId); + } + } + } + } + } + return !isEqualSet(languages, oldLanguages) || !isEqualSet(oldLanguages, oldComments); + } + 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); }, + useComments(languageId: string) { return comments.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; +} \ No newline at end of file diff --git a/extensions/json-language-features/client/src/languageStatus.ts b/extensions/json-language-features/client/src/languageStatus.ts index f2c8b923c3024..4aead6f99f22c 100644 --- a/extensions/json-language-features/client/src/languageStatus.ts +++ b/extensions/json-language-features/client/src/languageStatus.ts @@ -9,6 +9,7 @@ import { ThemeIcon, TextDocument, LanguageStatusSeverity, l10n } from 'vscode'; import { JSONLanguageStatus, JSONSchemaSettings } from './jsonClient'; +import { DocumentSelector } from 'vscode-languageclient'; type ShowSchemasInput = { schemas: string[]; @@ -163,7 +164,7 @@ function showSchemaList(input: ShowSchemasInput) { }); } -export function createLanguageStatusItem(documentSelector: string[], statusRequest: (uri: string) => Promise): Disposable { +export function createLanguageStatusItem(documentSelector: DocumentSelector, statusRequest: (uri: string) => Promise): Disposable { const statusItem = languages.createLanguageStatusItem('json.projectStatus', documentSelector); statusItem.name = l10n.t('JSON Validation Status'); statusItem.severity = LanguageStatusSeverity.Information; @@ -268,7 +269,7 @@ export function createLimitStatusItem(newItem: (limit: number) => Disposable) { const openSettingsCommand = 'workbench.action.openSettings'; const configureSettingsLabel = l10n.t('Configure'); -export function createDocumentSymbolsLimitItem(documentSelector: string[], settingId: string, limit: number): Disposable { +export function createDocumentSymbolsLimitItem(documentSelector: DocumentSelector, settingId: string, limit: number): Disposable { const statusItem = languages.createLanguageStatusItem('json.documentSymbolsStatus', documentSelector); statusItem.name = l10n.t('JSON Outline Status'); statusItem.severity = LanguageStatusSeverity.Warning; diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index 457a40f6a740d..79d66e32ddafb 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode'; -import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription } from '../jsonClient'; -import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; +import { Disposable, ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode'; +import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../jsonClient'; +import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; import { promises as fs } from 'fs'; import * as path from 'path'; @@ -15,7 +15,7 @@ import TelemetryReporter from '@vscode/extension-telemetry'; import { JSONSchemaCache } from './schemaCache'; 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) { @@ -44,17 +44,24 @@ export async function activate(context: ExtensionContext) { const log = getLog(outputChannel); context.subscriptions.push(log); + const timer = { + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { + const handle = setTimeout(callback, ms, ...args); + return { dispose: () => clearTimeout(handle) }; + } + }; + // pass the location of the localization bundle to the server process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; const schemaRequests = await getSchemaRequestService(context, log); - client = await startClient(context, newLanguageClient, { schemaRequests, telemetry }); + client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer }); } export async function deactivate(): Promise { if (client) { - await client.stop(); + await client.dispose(); client = undefined; } telemetry?.dispose(); diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 1af141703386d..ace3821f5e357 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -13,7 +13,8 @@ "icon": "icons/json.png", "activationEvents": [ "onLanguage:json", - "onLanguage:jsonc" + "onLanguage:jsonc", + "onLanguage:jsontm" ], "main": "./client/out/node/jsonClientMain", "browser": "./client/dist/browser/jsonClientMain", diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index 36ca0dc591d72..6238cdd4cd6e1 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -360,7 +360,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) return []; // ignore empty documents } const jsonDocument = getJSONDocument(textDocument); - const documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'warning' } : { comments: 'error', trailingCommas: 'error' }; + const documentSettings: DocumentLanguageSettings = (textDocument.languageId === 'jsonc' || textDocument.languageId === 'snippets') ? { comments: 'ignore', trailingCommas: 'warning' } : { comments: 'error', trailingCommas: 'error' }; return await languageService.doValidation(textDocument, jsonDocument, documentSettings); } From 317ce19dddeab66dd117611cc6775cb29e736621 Mon Sep 17 00:00:00 2001 From: RedCMD Date: Sat, 18 Nov 2023 21:13:47 +1300 Subject: [PATCH 2/8] Add `snippets` to `"activationEvents"` --- extensions/json-language-features/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index ace3821f5e357..871c80faf0158 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -14,7 +14,7 @@ "activationEvents": [ "onLanguage:json", "onLanguage:jsonc", - "onLanguage:jsontm" + "onLanguage:snippets" ], "main": "./client/out/node/jsonClientMain", "browser": "./client/dist/browser/jsonClientMain", From f06a081a8b2cbc63e69a8ede6c21254e5d84311b Mon Sep 17 00:00:00 2001 From: RedCMD <33529441+RedCMD@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:24:02 +1300 Subject: [PATCH 3/8] Remove hardcoded `snippets` from `documentSettings` --- extensions/json-language-features/server/src/jsonServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index 6238cdd4cd6e1..36ca0dc591d72 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -360,7 +360,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) return []; // ignore empty documents } const jsonDocument = getJSONDocument(textDocument); - const documentSettings: DocumentLanguageSettings = (textDocument.languageId === 'jsonc' || textDocument.languageId === 'snippets') ? { comments: 'ignore', trailingCommas: 'warning' } : { comments: 'error', trailingCommas: 'error' }; + const documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'warning' } : { comments: 'error', trailingCommas: 'error' }; return await languageService.doValidation(textDocument, jsonDocument, documentSettings); } From a94a4bc6a73d0617c7c31d679bab9ccd10dbce34 Mon Sep 17 00:00:00 2001 From: RedCMD <33529441+RedCMD@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:24:22 +1300 Subject: [PATCH 4/8] Fix wrong variable in `!isEqualSet()` --- .../json-language-features/client/src/languageParticipants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/json-language-features/client/src/languageParticipants.ts b/extensions/json-language-features/client/src/languageParticipants.ts index fd244fb3a2c58..02f4ef2110759 100644 --- a/extensions/json-language-features/client/src/languageParticipants.ts +++ b/extensions/json-language-features/client/src/languageParticipants.ts @@ -57,7 +57,7 @@ export function getLanguageParticipants(): LanguageParticipants { } } } - return !isEqualSet(languages, oldLanguages) || !isEqualSet(oldLanguages, oldComments); + return !isEqualSet(languages, oldLanguages) || !isEqualSet(comments, oldComments); } update(); @@ -86,4 +86,4 @@ function isEqualSet(s1: Set, s2: Set) { } } return true; -} \ No newline at end of file +} From 8b325fbc4a68d4936a688d494221ac9116dee1a1 Mon Sep 17 00:00:00 2001 From: RedCMD <33529441+RedCMD@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:29:44 +1300 Subject: [PATCH 5/8] Use `extensions.allAcrossExtensionHosts` instead of `extensions.all` --- .../json-language-features/client/src/languageParticipants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/json-language-features/client/src/languageParticipants.ts b/extensions/json-language-features/client/src/languageParticipants.ts index 02f4ef2110759..6bd1a086e0a49 100644 --- a/extensions/json-language-features/client/src/languageParticipants.ts +++ b/extensions/json-language-features/client/src/languageParticipants.ts @@ -43,7 +43,7 @@ export function getLanguageParticipants(): LanguageParticipants { comments = new Set(); comments.add('jsonc'); - for (const extension of extensions.all) { + for (const extension of extensions.allAcrossExtensionHosts) { const jsonLanguageParticipants = extension.packageJSON?.contributes?.jsonLanguageParticipants as LanguageParticipantContribution[]; if (Array.isArray(jsonLanguageParticipants)) { for (const jsonLanguageParticipant of jsonLanguageParticipants) { From d58e2ac3d894f8d455fac0b3add9807399a6b773 Mon Sep 17 00:00:00 2001 From: RedCMD <33529441+RedCMD@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:33:11 +1300 Subject: [PATCH 6/8] enable `"enabledApiProposals"` for `extensions.allAcrossExtensionHosts` --- extensions/json-language-features/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 871c80faf0158..0b3a487515df8 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -9,7 +9,9 @@ "engines": { "vscode": "^1.77.0" }, - "enabledApiProposals": [], + "enabledApiProposals": [ + "extensionsAny" + ], "icon": "icons/json.png", "activationEvents": [ "onLanguage:json", From 1c7a2c72ca5928316e5feaf180cecc55fb560a42 Mon Sep 17 00:00:00 2001 From: RedCMD Date: Fri, 12 Jan 2024 17:58:30 +1300 Subject: [PATCH 7/8] Fix error: `Property 'allAcrossExtensionHosts' does not exist on type 'typeof extensions'` --- extensions/json-language-features/client/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/json-language-features/client/tsconfig.json b/extensions/json-language-features/client/tsconfig.json index 4254a37490ea7..aa51e4d0157e1 100644 --- a/extensions/json-language-features/client/tsconfig.json +++ b/extensions/json-language-features/client/tsconfig.json @@ -7,5 +7,6 @@ "src/**/*", "../../../src/vscode-dts/vscode.d.ts", "../../../src/vscode-dts/vscode.proposed.languageStatus.d.ts", + "../../../src/vscode-dts/vscode.proposed.extensionsAny.d.ts" ] } From 70d626af5fc0d57fd5e032bf7f7ee5c1ff968eb5 Mon Sep 17 00:00:00 2001 From: RedCMD <33529441+RedCMD@users.noreply.github.com> Date: Wed, 24 Jan 2024 06:32:28 +1300 Subject: [PATCH 8/8] Remove `snippets` --- extensions/json-language-features/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 0b3a487515df8..ba1f3806cb40b 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -15,8 +15,7 @@ "icon": "icons/json.png", "activationEvents": [ "onLanguage:json", - "onLanguage:jsonc", - "onLanguage:snippets" + "onLanguage:jsonc" ], "main": "./client/out/node/jsonClientMain", "browser": "./client/dist/browser/jsonClientMain",