diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index fa1c6d10209921..72e92d7b86c11a 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -391,8 +391,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP const schemaDocuments: { [uri: string]: boolean } = {}; - // handle content request - client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => { + async function getSchemaContent(uriPath: string, allowKnownSchemaAssociations = true): Promise { const uri = Uri.parse(uriPath); const uriString = uri.toString(true); if (uri.scheme === 'untitled') { @@ -419,7 +418,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP if (!workspace.isTrusted) { throw new ResponseError(SchemaRequestServiceErrors.UntrustedWorkspaceError, l10n.t('Downloading schemas is disabled in untrusted workspaces')); } - if (!await isTrusted(uri)) { + if (!await isTrusted(uri, allowKnownSchemaAssociations)) { throw new ResponseError(SchemaRequestServiceErrors.UntrustedSchemaError, l10n.t('Location {0} is untrusted', uriString)); } if (runtime.telemetry && uri.authority === 'schema.management.azure.com') { @@ -440,7 +439,10 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP } else { throw new ResponseError(SchemaRequestServiceErrors.HTTPDisabledError, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); } - }); + } + + // handle content request + client.onRequest(VSCodeContentRequest.type, uriPath => getSchemaContent(uriPath)); await client.start(); @@ -542,16 +544,22 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP providedCodeActionKinds: [CodeActionKind.QuickFix] })); - client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(false)); - let schemaAssociationRefreshGeneration = 0; let schemaAssociationRefreshTrigger: Disposable | undefined; + let schemaAssociationRefreshIncludesRemoteRegistries = false; + const getRemoteSchemaRegistryContent = (uri: string): Promise => { + let timeout: Disposable | undefined; + return new Promise((resolve, reject) => { + timeout = runtime.timer.setTimeout(() => reject(new Error(`Timed out while loading schema registry ${uri}`)), 5000); + getSchemaContent(uri, false).then(resolve, reject); + }).finally(() => timeout?.dispose()); + }; const refreshSchemaAssociations = () => { const generation = ++schemaAssociationRefreshGeneration; schemaAssociationRefreshTrigger?.dispose(); schemaAssociationRefreshTrigger = runtime.timer.setTimeout(async () => { schemaAssociationRefreshTrigger = undefined; - const associations = await getSchemaAssociations(true); + const associations = await getSchemaAssociations(true, schemaAssociationRefreshIncludesRemoteRegistries); if (generation === schemaAssociationRefreshGeneration) { client.sendNotification(SchemaAssociationNotification.type, associations); } @@ -562,9 +570,15 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP schemaAssociationRefreshTrigger?.dispose(); })); + client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(false, false)); + getSchemaAssociations(true, true).then(associations => { + schemaAssociationRefreshIncludesRemoteRegistries = true; + client.sendNotification(SchemaAssociationNotification.type, associations); + }); + const registryWatchers = new Map(); const updateRegistryWatchers = () => { - const registryUris = new Set(getSchemaRegistryUris().map(uri => uri.toString())); + const registryUris = new Set(getSchemaRegistryUris().filter(isWatchableRegistryUri).map(uri => uri.toString())); for (const [uri, watcher] of registryWatchers) { if (!registryUris.has(uri)) { watcher.dispose(); @@ -598,15 +612,20 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP updateFormatterRegistration(); } else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) { schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload); + refreshSchemaAssociations(); triggerValidation(); } else if (e.affectsConfiguration(SettingIds.editorFoldingMaximumRegions) || e.affectsConfiguration(SettingIds.editorColorDecoratorsLimit)) { client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) }); } else if (e.affectsConfiguration(SettingIds.trustedDomains)) { trustedDomains = workspace.getConfiguration().get>(SettingIds.trustedDomains, {}); + refreshSchemaAssociations(); triggerValidation(); } })); - toDispose.push(workspace.onDidGrantWorkspaceTrust(() => triggerValidation())); + toDispose.push(workspace.onDidGrantWorkspaceTrust(() => { + refreshSchemaAssociations(); + triggerValidation(); + })); toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri))); @@ -682,14 +701,14 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP return settingsCache; } - async function getSchemaAssociations(forceRefresh: boolean): Promise { + async function getSchemaAssociations(forceRefresh: boolean, includeRemoteRegistries: boolean): Promise { if (!schemaAssociationsCache || forceRefresh) { - schemaAssociationsCache = computeSchemaAssociations(); + schemaAssociationsCache = computeSchemaAssociations(getRemoteSchemaRegistryContent, includeRemoteRegistries); } return schemaAssociationsCache; } - async function isTrusted(uri: Uri): Promise { + async function isTrusted(uri: Uri, allowKnownSchemaAssociations = true): Promise { if (uri.scheme !== 'http' && uri.scheme !== 'https') { return true; } @@ -700,10 +719,12 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP return true; } - const knownAssociations = await getSchemaAssociations(false); - for (const association of knownAssociations) { - if (association.uri === uriString) { - return true; + if (allowKnownSchemaAssociations) { + const knownAssociations = await getSchemaAssociations(false, schemaAssociationRefreshIncludesRemoteRegistries); + for (const association of knownAssociations) { + if (association.uri === uriString) { + return true; + } } } const settingsCache = getSettings(false); @@ -800,9 +821,9 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP }; } -async function computeSchemaAssociations(): Promise { +async function computeSchemaAssociations(getRegistryContent: (uri: string) => Promise, includeRemoteRegistries: boolean): Promise { const extensionAssociations = getSchemaExtensionAssociations(); - return extensionAssociations.concat(await getSchemaRegistryAssociations()); + return extensionAssociations.concat(await getSchemaRegistryAssociations(getRegistryContent, includeRemoteRegistries)); } function resolveExtensionResource(extensionUri: Uri, resource: string): Uri { @@ -860,14 +881,20 @@ function getSchemaRegistryUris(): Uri[] { return result; } -async function getSchemaRegistryAssociations(): Promise { - const result: ISchemaAssociation[] = []; - for (const registryUri of getSchemaRegistryUris()) { +function isWatchableRegistryUri(uri: Uri): boolean { + return uri.scheme !== 'http' && uri.scheme !== 'https'; +} + +async function getSchemaRegistryAssociations(getRegistryContent: (uri: string) => Promise, includeRemoteRegistries: boolean): Promise { + const registryUris = getSchemaRegistryUris().filter(uri => includeRemoteRegistries || isWatchableRegistryUri(uri)); + const registryAssociations = await Promise.all(registryUris.map(async registryUri => { try { - const data = await workspace.fs.readFile(registryUri); - const rawStr = new TextDecoder().decode(data); + const rawStr = isWatchableRegistryUri(registryUri) + ? new TextDecoder().decode(await workspace.fs.readFile(registryUri)) + : await getRegistryContent(registryUri.toString(true)); const registry = <{ schemas?: { url?: string; fileMatch?: string[] }[] }>JSON.parse(rawStr); if (Array.isArray(registry.schemas)) { + const result: ISchemaAssociation[] = []; for (const schema of registry.schemas) { if (typeof schema.url === 'string' && Array.isArray(schema.fileMatch) && schema.fileMatch.every(fileMatch => typeof fileMatch === 'string')) { result.push({ @@ -876,12 +903,14 @@ async function getSchemaRegistryAssociations(): Promise { }); } } + return result; } } catch { // Ignore unavailable or invalid registry. } - } - return result; + return []; + })); + return registryAssociations.flat(); } @@ -996,4 +1025,3 @@ export namespace ErrorCodes { export function isSchemaResolveError(d: Diagnostic) { return typeof d.code === 'number' && d.code >= ErrorCodes.SchemaResolveError; } - diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index cf3e0d5ecc691e..4fa10ef714794d 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -177,6 +177,9 @@ "jsonValidationRegistry": [ { "url": "vscode://schemas-associations/schemas-associations.json" + }, + { + "url": "https://www.schemastore.org/api/json/catalog.json" } ], "commands": [