Skip to content
Open
Show file tree
Hide file tree
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
127 changes: 111 additions & 16 deletions extensions/json-language-features/client/src/jsonClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {


import { hash } from './utils/hash';
import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem, createSchemaLoadIssueItem, createSchemaLoadStatusItem } from './languageStatus';
import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem, createSchemaLoadIssueItem, createSchemaLoadStatusItem, LanguageStatusItem } from './languageStatus';
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
import { matchesUrlPattern } from './utils/urlMatch';

Expand Down Expand Up @@ -76,6 +76,7 @@ export interface ISchemaAssociations {
export interface ISchemaAssociation {
fileMatch: string[];
uri: string;
source?: 'schemaStore';
}

namespace SchemaAssociationNotification {
Expand Down Expand Up @@ -107,11 +108,29 @@ export type JSONSchemaSettings = {
folderUri?: string;
};

const defaultSchemaStoreCatalogUrl = 'https://www.schemastore.org/api/json/catalog.json';

interface SchemaStoreCatalogEntry {
name: string;
description: string;
fileMatch?: string[];
url: string;
versions?: { [version: string]: string };
}

interface SchemaStoreCatalog {
$schema?: string;
version: number;
schemas: SchemaStoreCatalogEntry[];
}

export namespace SettingIds {
export const enableFormatter = 'json.format.enable';
export const enableKeepLines = 'json.format.keepLines';
export const enableValidation = 'json.validate.enable';
export const enableSchemaDownload = 'json.schemaDownload.enable';
export const enableSchemaStore = 'json.schemaStore.enable';
export const schemaStoreExclude = 'json.schemaStore.exclude';
export const trustedDomains = 'json.schemaDownload.trustedDomains';
export const maxItemsComputed = 'json.maxItemsComputed';
export const editorFoldingMaximumRegions = 'editor.foldingMaximumRegions';
Expand Down Expand Up @@ -236,6 +255,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP

const schemaLoadStatusItem = createSchemaLoadStatusItem((diagnostic: Diagnostic) => createSchemaLoadIssueItem(documentSelector, schemaDownloadEnabled, diagnostic));
toDispose.push(schemaLoadStatusItem);
let languageStatusItem: LanguageStatusItem | undefined;

toDispose.push(commands.registerCommand(CommandIds.clearCacheCommandId, async () => {
if (isClientReady && runtime.schemaRequests.clearCache) {
Expand Down Expand Up @@ -271,6 +291,9 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP

function handleSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] {
schemaLoadStatusItem.update(uri, diagnostics);
if (window.activeTextEditor?.document.uri.toString() === uri.toString()) {
languageStatusItem?.update();
}
if (!schemaDownloadEnabled) {
return diagnostics.filter(d => !isSchemaResolveError(d));
}
Expand Down Expand Up @@ -391,8 +414,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<string> {
const uri = Uri.parse(uriPath);
const uriString = uri.toString(true);
if (uri.scheme === 'untitled') {
Expand All @@ -419,7 +441,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') {
Expand All @@ -440,7 +462,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();

Expand Down Expand Up @@ -558,22 +583,31 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
updateFormatterRegistration();
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });

toDispose.push(workspace.onDidChangeConfiguration(e => {
toDispose.push(workspace.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(SettingIds.enableFormatter)) {
updateFormatterRegistration();
} else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) {
schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload);
await refreshSchemaAssociations();
triggerValidation();
} else if (e.affectsConfiguration(SettingIds.enableSchemaStore)) {
await refreshSchemaAssociations();
triggerValidation();
} else if (e.affectsConfiguration(SettingIds.schemaStoreExclude)) {
await 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<Record<string, boolean>>(SettingIds.trustedDomains, {});
await refreshSchemaAssociations();
triggerValidation();
}
}));
toDispose.push(workspace.onDidGrantWorkspaceTrust(() => triggerValidation()));

toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri)));
languageStatusItem = createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri));
toDispose.push(languageStatusItem);

function updateFormatterRegistration() {
const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter);
Expand Down Expand Up @@ -649,12 +683,50 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP

async function getSchemaAssociations(forceRefresh: boolean): Promise<ISchemaAssociation[]> {
if (!schemaAssociationsCache || forceRefresh) {
schemaAssociationsCache = computeSchemaAssociations();
schemaAssociationsCache = computeSchemaAssociations(getSchemaStoreAssociations);
}
return schemaAssociationsCache;
}

async function isTrusted(uri: Uri): Promise<boolean> {
async function refreshSchemaAssociations(): Promise<void> {
await client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true));
}

async function getSchemaStoreAssociations(): Promise<ISchemaAssociation[]> {
const configuration = workspace.getConfiguration();
if (configuration.get(SettingIds.enableSchemaStore) === false) {
return [];
}

let catalog: SchemaStoreCatalog;
try {
const content = await getSchemaContent(defaultSchemaStoreCatalogUrl, false);
catalog = JSON.parse(content);
} catch (error) {
runtime.logOutputChannel.warn(l10n.t('Unable to load SchemaStore catalog from \'{0}\': {1}.', defaultSchemaStoreCatalogUrl, getErrorMessage(error)));
return [];
}

if (!catalog || typeof catalog !== 'object' || !Array.isArray(catalog.schemas)) {
runtime.logOutputChannel.warn(l10n.t('Unable to parse SchemaStore catalog from \'{0}\': Expected a catalog object with a schemas array.', defaultSchemaStoreCatalogUrl));
return [];
}

const exclusions = configuration.get<string[]>(SettingIds.schemaStoreExclude, []).map(sanitizeSchemaStoreExclusion).filter((exclusion): exclusion is string => !!exclusion).map(exclusion => `!${exclusion}`);
const associations: ISchemaAssociation[] = [];
for (const schema of catalog.schemas) {
if (!schema || typeof schema !== 'object' || typeof schema.url !== 'string' || !Array.isArray(schema.fileMatch)) {
continue;
}
const fileMatch = schema.fileMatch.filter((pattern): pattern is string => typeof pattern === 'string');
if (fileMatch.length) {
associations.push({ uri: schema.url, fileMatch: fileMatch.concat(exclusions), source: 'schemaStore' });
}
}
return associations;
}

async function isTrusted(uri: Uri, allowKnownSchemaAssociations = true): Promise<boolean> {
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
return true;
}
Expand All @@ -665,10 +737,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);
for (const association of knownAssociations) {
if (association.uri === uriString) {
return true;
}
}
}
const settingsCache = getSettings(false);
Expand Down Expand Up @@ -765,9 +839,31 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
};
}

async function computeSchemaAssociations(): Promise<ISchemaAssociation[]> {
async function computeSchemaAssociations(getSchemaStoreAssociations: () => Promise<ISchemaAssociation[]>): Promise<ISchemaAssociation[]> {
const extensionAssociations = getSchemaExtensionAssociations();
return extensionAssociations.concat(await getDynamicSchemaAssociations());
return extensionAssociations.concat(await getDynamicSchemaAssociations(), await getSchemaStoreAssociations());
}

function sanitizeSchemaStoreExclusion(exclusion: string): string | undefined {
if (typeof exclusion !== 'string' || !exclusion || exclusion[0] === '!' || exclusion[0] === '/' || exclusion.indexOf('\\') !== -1 || exclusion.indexOf(':') !== -1 || exclusion.indexOf('..') !== -1) {
return undefined;
}
return exclusion;
}

function getErrorMessage(error: any): string {
if (error && typeof error.message === 'string') {
return error.message;
}
let errorMessage = error?.toString ? error.toString() as string : String(error);
const errorSplit = errorMessage.split('Error: ');
if (errorSplit.length > 1) {
errorMessage = errorSplit[1];
}
if (errorMessage.endsWith('.')) {
errorMessage = errorMessage.substring(0, errorMessage.length - 1);
}
return errorMessage;
}

function getSchemaExtensionAssociations(): ISchemaAssociation[] {
Expand Down Expand Up @@ -936,4 +1032,3 @@ export namespace ErrorCodes {
export function isSchemaResolveError(d: Diagnostic) {
return typeof d.code === 'number' && d.code >= ErrorCodes.SchemaResolveError;
}

13 changes: 10 additions & 3 deletions extensions/json-language-features/client/src/languageStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,11 @@ function showSchemaList(input: ShowSchemasInput) {
});
}

export function createLanguageStatusItem(documentSelector: DocumentSelector, statusRequest: (uri: string) => Promise<JSONLanguageStatus>): Disposable {
export interface LanguageStatusItem extends Disposable {
update(): void;
}

export function createLanguageStatusItem(documentSelector: DocumentSelector, statusRequest: (uri: string) => Promise<JSONLanguageStatus>): LanguageStatusItem {
const statusItem = languages.createLanguageStatusItem('json.projectStatus', documentSelector);
statusItem.name = l10n.t('JSON Validation Status');
statusItem.severity = LanguageStatusSeverity.Information;
Expand Down Expand Up @@ -213,7 +217,11 @@ export function createLanguageStatusItem(documentSelector: DocumentSelector, sta

updateLanguageStatus();

return Disposable.from(statusItem, activeEditorListener, showSchemasCommand);
const disposable = Disposable.from(statusItem, activeEditorListener, showSchemasCommand);
return {
update: updateLanguageStatus,
dispose: () => disposable.dispose()
};
}

export function createLimitStatusItem(newItem: (limit: number) => Disposable) {
Expand Down Expand Up @@ -361,4 +369,3 @@ export function createSchemaLoadIssueItem(documentSelector: DocumentSelector, sc
return Disposable.from(statusItem);
}


21 changes: 21 additions & 0 deletions extensions/json-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@
"usesOnlineServices"
]
},
"json.schemaStore.enable": {
"type": "boolean",
"default": true,
"description": "%json.enableSchemaStore.desc%",
"tags": [
"usesOnlineServices"
]
},
"json.schemaStore.exclude": {
"type": "array",
"default": [],
"description": "%json.schemaStore.exclude.desc%",
"items": {
"type": "string",
"default": "**/openapi.json",
"description": "%json.schemaStore.exclude.item.desc%"
},
"tags": [
"usesOnlineServices"
]
},
"json.schemaDownload.trustedDomains": {
"type": "object",
"default": {
Expand Down
3 changes: 3 additions & 0 deletions extensions/json-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"json.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).",
"json.maxItemsExceededInformation.desc": "Show notification when exceeding the maximum number of outline symbols and folding regions.",
"json.enableSchemaDownload.desc": "When enabled, JSON schemas can be fetched from http and https locations.",
"json.enableSchemaStore.desc": "When enabled, JSON schema associations are loaded from the SchemaStore catalog.",
"json.schemaStore.exclude.desc": "Relative path selectors for files that should not receive JSON schema associations from the SchemaStore catalog. Other schema association mechanisms are unaffected. Absolute paths and paths containing `..` are ignored.",
"json.schemaStore.exclude.item.desc": "A relative path selector, such as `foo/bar/openapi.json` or `**/openapi.json`. Absolute paths and paths containing `..` are ignored.",
"json.command.clearCache": "Clear Schema Cache",
"json.command.sort": "Sort Document",
"json.workspaceTrust": "The extension requires workspace trust to load schemas from http and https.",
Expand Down
19 changes: 8 additions & 11 deletions extensions/json-language-features/server/src/jsonServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

import { runSafe, runSafeAsync } from './utils/runner.js';
import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation.js';
import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Range, Position, SortOptions, SeverityLevel } from 'vscode-json-languageservice';
import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Range, Position, SortOptions, SeverityLevel, LanguageSettings } from 'vscode-json-languageservice';
import { getLanguageModelCache } from './languageModelCache.js';
import { Utils, URI } from 'vscode-uri';
import * as l10n from '@vscode/l10n';
Expand Down Expand Up @@ -358,20 +358,21 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
});

function updateConfiguration(extraSchemas?: SchemaConfiguration[]) {
const languageSettings = {
const schemas = new Array<SchemaConfiguration>();
const languageSettings: LanguageSettings = {
validate: validateEnabled,
allowComments: true,
schemas: new Array<SchemaConfiguration>()
schemas
};
if (schemaAssociations) {
if (Array.isArray(schemaAssociations)) {
Array.prototype.push.apply(languageSettings.schemas, schemaAssociations);
Array.prototype.push.apply(schemas, schemaAssociations);
} else {
for (const pattern in schemaAssociations) {
const association = schemaAssociations[pattern];
if (Array.isArray(association)) {
association.forEach(uri => {
languageSettings.schemas.push({ uri, fileMatch: [pattern] });
schemas.push({ uri, fileMatch: [pattern] });
});
}
}
Expand All @@ -384,12 +385,12 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
uri = schema.schema.id || `vscode://schemas/custom/${index}`;
}
if (uri) {
languageSettings.schemas.push({ uri, fileMatch: schema.fileMatch, schema: schema.schema, folderUri: schema.folderUri });
schemas.push({ uri, fileMatch: schema.fileMatch, schema: schema.schema, folderUri: schema.folderUri });
}
});
}
if (extraSchemas) {
languageSettings.schemas.push(...extraSchemas);
schemas.push(...extraSchemas);
}

languageService.configure(languageSettings);
Expand Down Expand Up @@ -576,7 +577,3 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
function getFullRange(document: TextDocument): Range {
return Range.create(Position.create(0, 0), document.positionAt(document.getText().length));
}




Loading