Contribute to json language server with a custom language. (#198583)

* Contribute to json language server with a custom language.

* Add `snippets` to `"activationEvents"`

* Remove hardcoded `snippets` from `documentSettings`

* Fix wrong variable in `!isEqualSet()`

* Use `extensions.allAcrossExtensionHosts` instead of `extensions.all`

* enable `"enabledApiProposals"` for `extensions.allAcrossExtensionHosts`

* Fix error: `Property 'allAcrossExtensionHosts' does not exist on type 'typeof extensions'`

* Remove `snippets`
This commit is contained in:
RedCMD
2024-01-31 00:10:23 +13:00
committed by GitHub
parent 6215ccd0e7
commit eee4b5fc50
7 changed files with 176 additions and 20 deletions

View File

@@ -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<void> {
if (client) {
await client.stop();
await client.dispose();
client = undefined;
}
}

View File

@@ -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<string, string, any> = 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<BaseLanguageClient> {
export interface AsyncDisposable {
dispose(): Promise<void>;
}
const toDispose = context.subscriptions;
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
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<AsyncDisposable> {
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[] {

View File

@@ -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<void>;
readonly documentSelector: DocumentSelector;
hasLanguage(languageId: string): boolean;
useComments(languageId: string): boolean;
dispose(): void;
}
export function getLanguageParticipants(): LanguageParticipants {
const onDidChangeEmmiter = new EventEmitter<void>();
let languages = new Set<string>();
let comments = new Set<string>();
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.allAcrossExtensionHosts) {
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(comments, 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<T>(s1: Set<T>, s2: Set<T>) {
if (s1.size !== s2.size) {
return false;
}
for (const e of s1) {
if (!s2.has(e)) {
return false;
}
}
return true;
}

View File

@@ -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<JSONLanguageStatus>): Disposable {
export function createLanguageStatusItem(documentSelector: DocumentSelector, statusRequest: (uri: string) => Promise<JSONLanguageStatus>): 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;

View File

@@ -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<any> {
if (client) {
await client.stop();
await client.dispose();
client = undefined;
}
telemetry?.dispose();

View File

@@ -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"
]
}

View File

@@ -9,7 +9,9 @@
"engines": {
"vscode": "^1.77.0"
},
"enabledApiProposals": [],
"enabledApiProposals": [
"extensionsAny"
],
"icon": "icons/json.png",
"activationEvents": [
"onLanguage:json",