Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 9 commits into from
Jan 30, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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;
}
}
59 changes: 54 additions & 5 deletions extensions/json-language-features/client/src/jsonClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand Down Expand Up @@ -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 {
Expand All @@ -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>;
}

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 = 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');
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.all) {
RedCMD marked this conversation as resolved.
Show resolved Hide resolved
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);
RedCMD marked this conversation as resolved.
Show resolved Hide resolved
}
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion extensions/json-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"icon": "icons/json.png",
"activationEvents": [
"onLanguage:json",
"onLanguage:jsonc"
"onLanguage:jsonc",
"onLanguage:snippets"
RedCMD marked this conversation as resolved.
Show resolved Hide resolved
RedCMD marked this conversation as resolved.
Show resolved Hide resolved
],
"main": "./client/out/node/jsonClientMain",
"browser": "./client/dist/browser/jsonClientMain",
Expand Down