Skip to content

Commit eee4b5f

Browse files
authored
Contribute to json language server with a custom language. (microsoft#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`
1 parent 6215ccd commit eee4b5f

File tree

7 files changed

+176
-20
lines changed

7 files changed

+176
-20
lines changed

extensions/json-language-features/client/src/browser/jsonClientMain.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { ExtensionContext, Uri, l10n } from 'vscode';
7-
import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient';
8-
import { startClient, LanguageClientConstructor, SchemaRequestService } from '../jsonClient';
6+
import { Disposable, ExtensionContext, Uri, l10n } from 'vscode';
7+
import { LanguageClientOptions } from 'vscode-languageclient';
8+
import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable } from '../jsonClient';
99
import { LanguageClient } from 'vscode-languageclient/browser';
1010

1111
declare const Worker: {
@@ -14,7 +14,7 @@ declare const Worker: {
1414

1515
declare function fetch(uri: string, options: any): any;
1616

17-
let client: BaseLanguageClient | undefined;
17+
let client: AsyncDisposable | undefined;
1818

1919
// this method is called when vs code is activated
2020
export async function activate(context: ExtensionContext) {
@@ -36,7 +36,14 @@ export async function activate(context: ExtensionContext) {
3636
}
3737
};
3838

39-
client = await startClient(context, newLanguageClient, { schemaRequests });
39+
const timer = {
40+
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable {
41+
const handle = setTimeout(callback, ms, ...args);
42+
return { dispose: () => clearTimeout(handle) };
43+
}
44+
};
45+
46+
client = await startClient(context, newLanguageClient, { schemaRequests, timer });
4047

4148
} catch (e) {
4249
console.log(e);
@@ -45,7 +52,7 @@ export async function activate(context: ExtensionContext) {
4552

4653
export async function deactivate(): Promise<void> {
4754
if (client) {
48-
await client.stop();
55+
await client.dispose();
4956
client = undefined;
5057
}
5158
}

extensions/json-language-features/client/src/jsonClient.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
export type JSONLanguageStatus = { schemas: string[] };
77

88
import {
9-
workspace, window, languages, commands, ExtensionContext, extensions, Uri, ColorInformation,
9+
workspace, window, languages, commands, OutputChannel, ExtensionContext, extensions, Uri, ColorInformation,
1010
Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange,
1111
ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n
1212
} from 'vscode';
@@ -19,6 +19,7 @@ import {
1919

2020
import { hash } from './utils/hash';
2121
import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus';
22+
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
2223

2324
namespace VSCodeContentRequest {
2425
export const type: RequestType<string, string, any> = new RequestType('vscode/content');
@@ -126,6 +127,9 @@ export type LanguageClientConstructor = (name: string, description: string, clie
126127
export interface Runtime {
127128
schemaRequests: SchemaRequestService;
128129
telemetry?: TelemetryReporter;
130+
readonly timer: {
131+
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable;
132+
};
129133
}
130134

131135
export interface SchemaRequestService {
@@ -141,13 +145,51 @@ let jsoncFoldingLimit = 5000;
141145
let jsonColorDecoratorLimit = 5000;
142146
let jsoncColorDecoratorLimit = 5000;
143147

144-
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<BaseLanguageClient> {
148+
export interface AsyncDisposable {
149+
dispose(): Promise<void>;
150+
}
151+
152+
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
153+
const outputChannel = window.createOutputChannel(languageServerDescription);
154+
155+
const languageParticipants = getLanguageParticipants();
156+
context.subscriptions.push(languageParticipants);
157+
158+
let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime);
159+
160+
let restartTrigger: Disposable | undefined;
161+
languageParticipants.onDidChange(() => {
162+
if (restartTrigger) {
163+
restartTrigger.dispose();
164+
}
165+
restartTrigger = runtime.timer.setTimeout(async () => {
166+
if (client) {
167+
outputChannel.appendLine('Extensions have changed, restarting JSON server...');
168+
outputChannel.appendLine('');
169+
const oldClient = client;
170+
client = undefined;
171+
await oldClient.dispose();
172+
client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime);
173+
}
174+
}, 2000);
175+
});
176+
177+
return {
178+
dispose: async () => {
179+
restartTrigger?.dispose();
180+
await client?.dispose();
181+
outputChannel.dispose();
182+
}
183+
};
184+
}
185+
186+
async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise<AsyncDisposable> {
145187

146-
const toDispose = context.subscriptions;
188+
const toDispose: Disposable[] = [];
147189

148190
let rangeFormatting: Disposable | undefined = undefined;
149191

150-
const documentSelector = ['json', 'jsonc'];
192+
const documentSelector = languageParticipants.documentSelector;
151193

152194
const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0);
153195
schemaResolutionErrorStatusBarItem.name = l10n.t('JSON: Schema Resolution Error');
@@ -306,6 +348,7 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
306348
}
307349
};
308350

351+
clientOptions.outputChannel = outputChannel;
309352
// Create the language client and start the client.
310353
const client = newLanguageClient('json', languageServerDescription, clientOptions);
311354
client.registerProposedFeatures();
@@ -490,7 +533,13 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
490533
});
491534
}
492535

493-
return client;
536+
return {
537+
dispose: async () => {
538+
await client.stop();
539+
toDispose.forEach(d => d.dispose());
540+
rangeFormatting?.dispose();
541+
}
542+
};
494543
}
495544

496545
function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { DocumentSelector } from 'vscode-languageclient';
7+
import { Event, EventEmitter, extensions } from 'vscode';
8+
9+
/**
10+
* JSON language participant contribution.
11+
*/
12+
interface LanguageParticipantContribution {
13+
/**
14+
* The id of the language which participates with the JSON language server.
15+
*/
16+
languageId: string;
17+
/**
18+
* true if the language allows comments and false otherwise.
19+
* TODO: implement server side setting
20+
*/
21+
comments?: boolean;
22+
}
23+
24+
export interface LanguageParticipants {
25+
readonly onDidChange: Event<void>;
26+
readonly documentSelector: DocumentSelector;
27+
hasLanguage(languageId: string): boolean;
28+
useComments(languageId: string): boolean;
29+
dispose(): void;
30+
}
31+
32+
export function getLanguageParticipants(): LanguageParticipants {
33+
const onDidChangeEmmiter = new EventEmitter<void>();
34+
let languages = new Set<string>();
35+
let comments = new Set<string>();
36+
37+
function update() {
38+
const oldLanguages = languages, oldComments = comments;
39+
40+
languages = new Set();
41+
languages.add('json');
42+
languages.add('jsonc');
43+
comments = new Set();
44+
comments.add('jsonc');
45+
46+
for (const extension of extensions.allAcrossExtensionHosts) {
47+
const jsonLanguageParticipants = extension.packageJSON?.contributes?.jsonLanguageParticipants as LanguageParticipantContribution[];
48+
if (Array.isArray(jsonLanguageParticipants)) {
49+
for (const jsonLanguageParticipant of jsonLanguageParticipants) {
50+
const languageId = jsonLanguageParticipant.languageId;
51+
if (typeof languageId === 'string') {
52+
languages.add(languageId);
53+
if (jsonLanguageParticipant.comments === true) {
54+
comments.add(languageId);
55+
}
56+
}
57+
}
58+
}
59+
}
60+
return !isEqualSet(languages, oldLanguages) || !isEqualSet(comments, oldComments);
61+
}
62+
update();
63+
64+
const changeListener = extensions.onDidChange(_ => {
65+
if (update()) {
66+
onDidChangeEmmiter.fire();
67+
}
68+
});
69+
70+
return {
71+
onDidChange: onDidChangeEmmiter.event,
72+
get documentSelector() { return Array.from(languages); },
73+
hasLanguage(languageId: string) { return languages.has(languageId); },
74+
useComments(languageId: string) { return comments.has(languageId); },
75+
dispose: () => changeListener.dispose()
76+
};
77+
}
78+
79+
function isEqualSet<T>(s1: Set<T>, s2: Set<T>) {
80+
if (s1.size !== s2.size) {
81+
return false;
82+
}
83+
for (const e of s1) {
84+
if (!s2.has(e)) {
85+
return false;
86+
}
87+
}
88+
return true;
89+
}

extensions/json-language-features/client/src/languageStatus.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ThemeIcon, TextDocument, LanguageStatusSeverity, l10n
1010
} from 'vscode';
1111
import { JSONLanguageStatus, JSONSchemaSettings } from './jsonClient';
12+
import { DocumentSelector } from 'vscode-languageclient';
1213

1314
type ShowSchemasInput = {
1415
schemas: string[];
@@ -163,7 +164,7 @@ function showSchemaList(input: ShowSchemasInput) {
163164
});
164165
}
165166

166-
export function createLanguageStatusItem(documentSelector: string[], statusRequest: (uri: string) => Promise<JSONLanguageStatus>): Disposable {
167+
export function createLanguageStatusItem(documentSelector: DocumentSelector, statusRequest: (uri: string) => Promise<JSONLanguageStatus>): Disposable {
167168
const statusItem = languages.createLanguageStatusItem('json.projectStatus', documentSelector);
168169
statusItem.name = l10n.t('JSON Validation Status');
169170
statusItem.severity = LanguageStatusSeverity.Information;
@@ -268,7 +269,7 @@ export function createLimitStatusItem(newItem: (limit: number) => Disposable) {
268269
const openSettingsCommand = 'workbench.action.openSettings';
269270
const configureSettingsLabel = l10n.t('Configure');
270271

271-
export function createDocumentSymbolsLimitItem(documentSelector: string[], settingId: string, limit: number): Disposable {
272+
export function createDocumentSymbolsLimitItem(documentSelector: DocumentSelector, settingId: string, limit: number): Disposable {
272273
const statusItem = languages.createLanguageStatusItem('json.documentSymbolsStatus', documentSelector);
273274
statusItem.name = l10n.t('JSON Outline Status');
274275
statusItem.severity = LanguageStatusSeverity.Warning;

extensions/json-language-features/client/src/node/jsonClientMain.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode';
7-
import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription } from '../jsonClient';
8-
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node';
6+
import { Disposable, ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode';
7+
import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../jsonClient';
8+
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node';
99

1010
import { promises as fs } from 'fs';
1111
import * as path from 'path';
@@ -15,7 +15,7 @@ import TelemetryReporter from '@vscode/extension-telemetry';
1515
import { JSONSchemaCache } from './schemaCache';
1616

1717
let telemetry: TelemetryReporter | undefined;
18-
let client: BaseLanguageClient | undefined;
18+
let client: AsyncDisposable | undefined;
1919

2020
// this method is called when vs code is activated
2121
export async function activate(context: ExtensionContext) {
@@ -44,17 +44,24 @@ export async function activate(context: ExtensionContext) {
4444
const log = getLog(outputChannel);
4545
context.subscriptions.push(log);
4646

47+
const timer = {
48+
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable {
49+
const handle = setTimeout(callback, ms, ...args);
50+
return { dispose: () => clearTimeout(handle) };
51+
}
52+
};
53+
4754
// pass the location of the localization bundle to the server
4855
process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? '';
4956

5057
const schemaRequests = await getSchemaRequestService(context, log);
5158

52-
client = await startClient(context, newLanguageClient, { schemaRequests, telemetry });
59+
client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer });
5360
}
5461

5562
export async function deactivate(): Promise<any> {
5663
if (client) {
57-
await client.stop();
64+
await client.dispose();
5865
client = undefined;
5966
}
6067
telemetry?.dispose();

extensions/json-language-features/client/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
"src/**/*",
88
"../../../src/vscode-dts/vscode.d.ts",
99
"../../../src/vscode-dts/vscode.proposed.languageStatus.d.ts",
10+
"../../../src/vscode-dts/vscode.proposed.extensionsAny.d.ts"
1011
]
1112
}

extensions/json-language-features/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"engines": {
1010
"vscode": "^1.77.0"
1111
},
12-
"enabledApiProposals": [],
12+
"enabledApiProposals": [
13+
"extensionsAny"
14+
],
1315
"icon": "icons/json.png",
1416
"activationEvents": [
1517
"onLanguage:json",

0 commit comments

Comments
 (0)