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

Add API to json language features #239701

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -40,10 +40,13 @@ export async function activate(context: ExtensionContext) {
const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true });
context.subscriptions.push(logOutputChannel);

client = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel });
const [_client, api] = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel });
client = _client;

return api;
} catch (e) {
console.log(e);
return;
}
}

86 changes: 86 additions & 0 deletions extensions/json-language-features/client/src/extension-api.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Uri, TextDocument, Disposable } from 'vscode';

/**
* This interface describes the shape for the json language features extension
* API. It includes functions to register schema association providers and
* providers for custom uri schemas whose are used to request json schemas. To
* acquire this API use the default mechanics, e.g:
*
* ```ts
* // get json language features extension API
* const api = await vscode.extensions.getExtension<JsonApi>('vscode.json-language-features').activate();
* ```
*/
export interface JsonApi {
/**
* Register a provider to associate a json schema with a requested
* `TextDocument`.
*
* @param provider The schema association provider.
* @return A disposable that unregisters the provider.
*/
registerSchemaAssociationProvider(provider: SchemaAssociationProvider): Disposable;

/**
* Notify the language server that the schema association has changed for a
* given uri, or multiple uris.
*
* @param uri The uri of the document whose schema association has changed.
* @return `true` if any of the provided document uris were registered.
*/
schemaAssociationChanged(uri: Uri | Uri[]): boolean;

/**
* Register a provider to handle json schema requests for a specific uri
* schema.
*
* @param schema The uri schema to register for.
* @param provider The provider to handle the schema request.
* @return A disposable that unregisters the provider.
*/
registerUriSchemaProvider(schema: string, provider: UriSchemaProvider): Disposable;

/**
* Notify the language server that the content of a specific schema, or
* multiple schemas has changed.
*
* @param uri The uri of the schema whose content has changed.
* @return `true` if any of the provided schema uris were registered.
*/
schemaContentChanged(uri: Uri | Uri[]): boolean;
}

export interface SchemaAssociationProvider {
/**
* Provide a `Uri` to a schema that should be used for the given `TextDocument`.
*
* @param document The document for which the schema association is requested.
*/
provideSchemaAssociation(document: TextDocument): Uri | null | undefined;

/**
* Optional dispose function which is invoked when the json language
* features are deactivated.
*/
dispose?(): any;
}

export interface UriSchemaProvider {
/**
* Provide the content of the schema for the given uri.
*
* @param uri The uri of the schema to provide.
*/
provideSchemaContent(uri: Uri): string;

/**
* Optional dispose function which is invoked when the json language
* features are deactivated.
*/
dispose?(): any
}
147 changes: 130 additions & 17 deletions extensions/json-language-features/client/src/jsonClient.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import {
import { hash } from './utils/hash';
import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus';
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
import { JsonApi, SchemaAssociationProvider, UriSchemaProvider } from './extension-api';

namespace VSCodeContentRequest {
export const type: RequestType<string, string, any> = new RequestType('vscode/content');
@@ -151,11 +152,11 @@ export interface AsyncDisposable {
dispose(): Promise<void>;
}

export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<[AsyncDisposable, JsonApi]> {
const languageParticipants = getLanguageParticipants();
context.subscriptions.push(languageParticipants);

let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);
let [client, api]: [Disposable | undefined, JsonApi] = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);

let restartTrigger: Disposable | undefined;
languageParticipants.onDidChange(() => {
@@ -169,20 +170,23 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
const oldClient = client;
client = undefined;
await oldClient.dispose();
client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);
[client, api] = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);
}
}, 2000);
});

return {
dispose: async () => {
restartTrigger?.dispose();
await client?.dispose();
}
};
return [
{
dispose: async () => {
restartTrigger?.dispose();
await client?.dispose();
}
},
api
];
}

async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<[AsyncDisposable, JsonApi]> {

const toDispose: Disposable[] = [];

@@ -231,6 +235,9 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa
}
}));

const schemaAssociationProvider: SchemaAssociationProvider[] = [];
const uriSchemaProvider: { [schema: string]: UriSchemaProvider } = {};

function filterSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] {
const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError);
if (schemaErrorIndex !== -1) {
@@ -377,6 +384,16 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa
throw new ResponseError(5, e.toString(), e);
}
} else if (uri.scheme !== 'http' && uri.scheme !== 'https') {
const provider = uriSchemaProvider[uriString];
if (provider) {
try {
const schema = provider.provideSchemaContent(uri);
schemaDocuments[uriString] = true;
return schema;
} catch (e) {
throw new ResponseError(6, e.toString(), e);
}
}
try {
const document = await workspace.openTextDocument(uri);
schemaDocuments[uriString] = true;
@@ -495,12 +512,42 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa

toDispose.push(commands.registerCommand('_json.retryResolveSchema', handleRetryResolveSchemaCommand));

client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context));
let schemaAssociations = getSchemaAssociations(context);
const dynamicSchemaAssociations: typeof schemaAssociations = [];

function sendSchemaAssociations() {
client.sendNotification(SchemaAssociationNotification.type, schemaAssociations.concat(dynamicSchemaAssociations));
}

sendSchemaAssociations();

function provideSchemaAssociation(doc: TextDocument) {
for (const provider of schemaAssociationProvider) {
try {
const schema = provider.provideSchemaAssociation(doc);
if (schema) {
const stringUri = schema.toString();
const idx = dynamicSchemaAssociations.findIndex(s => s.uri === stringUri);
if (idx === -1) {
dynamicSchemaAssociations.push({ uri: stringUri, fileMatch: [doc.uri.toString()] });
} else {
dynamicSchemaAssociations[idx].fileMatch.push(doc.uri.toString());
}
sendSchemaAssociations();
}
} catch (e) {
console.error(e);
}
}
}

toDispose.push(extensions.onDidChange(_ => {
client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context));
schemaAssociations = getSchemaAssociations(context);
sendSchemaAssociations();
}));

toDispose.push(workspace.onDidOpenTextDocument(provideSchemaAssociation));

// manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652.
updateFormatterRegistration();
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
@@ -586,13 +633,79 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa
});
}

return {
dispose: async () => {
await client.stop();
toDispose.forEach(d => d.dispose());
rangeFormatting?.dispose();
const api = {
registerSchemaAssociationProvider: function (provider: SchemaAssociationProvider): Disposable {
schemaAssociationProvider.push(provider);
return {
dispose: () => {
const idx = schemaAssociationProvider.indexOf(provider);
if (idx !== -1) {
schemaAssociationProvider.splice(idx, 1);
}
}
};
},
schemaAssociationChanged: function (uri: Uri | Uri[]): boolean {
const uris = Array.isArray(uri) ? uri : [uri];

const stringUris = uris.map(uri => uri.toString());

let hasChanged = false;
for (const uri of stringUris) {
for (const association of dynamicSchemaAssociations) {
const idx = association.fileMatch.findIndex(m => m === uri);
if (idx !== -1) {
hasChanged = true;
association.fileMatch.splice(idx, 1);

if (association.fileMatch.length === 0) {
const idx = dynamicSchemaAssociations.indexOf(association);
dynamicSchemaAssociations.splice(idx, 1);
}

const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri);
if (doc) {
provideSchemaAssociation(doc);
}
break;
}
}
}
return hasChanged;
},
registerUriSchemaProvider: function (schema: string, provider: UriSchemaProvider): Disposable {
uriSchemaProvider[schema] = provider;
return {
dispose: () => {
delete uriSchemaProvider[schema];
}
};
},
schemaContentChanged: function (uri: Uri | Uri[]): boolean {
const uris = Array.isArray(uri) ? uri : [uri];

const stringUris = uris.map(uri => uri.toString()).filter(uri => schemaDocuments[uri]);

const hasChanged = stringUris.length > 0;
if (hasChanged) {
client.sendNotification(SchemaContentChangeNotification.type, stringUris);
}
return hasChanged;
}
};

return [
{
dispose: async () => {
await client.stop();
Object.values(uriSchemaProvider).forEach(d => d.dispose?.());
schemaAssociationProvider.forEach(d => d.dispose?.());
toDispose.forEach(d => d.dispose());
rangeFormatting?.dispose();
}
},
api
];
}

function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] {
Original file line number Diff line number Diff line change
@@ -54,7 +54,10 @@ export async function activate(context: ExtensionContext) {

const schemaRequests = await getSchemaRequestService(context, logOutputChannel);

client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer, logOutputChannel });
const [_client, api] = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer, logOutputChannel });
client = _client;

return api;
}

export async function deactivate(): Promise<any> {