Path: blob/main/extensions/json-language-features/client/src/jsonClient.ts
5222 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45export type JSONLanguageStatus = { schemas: string[] };67import {8workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation,9Diagnostic, StatusBarAlignment, TextDocument, FormattingOptions, CancellationToken, FoldingRange,10ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n,11RelativePattern, CodeAction, CodeActionKind, CodeActionContext12} from 'vscode';13import {14LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind,15Diagnostic as LSPDiagnostic,16DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams,17DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, BaseLanguageClient, ProvideFoldingRangeSignature, ProvideDocumentSymbolsSignature, ProvideDocumentColorsSignature18} from 'vscode-languageclient';192021import { hash } from './utils/hash';22import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem, createSchemaLoadIssueItem, createSchemaLoadStatusItem } from './languageStatus';23import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';24import { matchesUrlPattern } from './utils/urlMatch';2526namespace VSCodeContentRequest {27export const type: RequestType<string, string, any> = new RequestType('vscode/content');28}2930namespace SchemaContentChangeNotification {31export const type: NotificationType<string | string[]> = new NotificationType('json/schemaContent');32}3334namespace ForceValidateRequest {35export const type: RequestType<string, Diagnostic[], any> = new RequestType('json/validate');36}3738namespace LanguageStatusRequest {39export const type: RequestType<string, JSONLanguageStatus, any> = new RequestType('json/languageStatus');40}4142namespace ValidateContentRequest {43export const type: RequestType<{ schemaUri: string; content: string }, LSPDiagnostic[], any> = new RequestType('json/validateContent');44}4546interface SortOptions extends LSPFormattingOptions {47}4849interface DocumentSortingParams {50/**51* The uri of the document to sort.52*/53readonly uri: string;54/**55* The sort options56*/57readonly options: SortOptions;58}5960namespace DocumentSortingRequest {61export interface ITextEdit {62range: {63start: { line: number; character: number };64end: { line: number; character: number };65};66newText: string;67}68export const type: RequestType<DocumentSortingParams, ITextEdit[], any> = new RequestType('json/sort');69}7071export interface ISchemaAssociations {72[pattern: string]: string[];73}7475export interface ISchemaAssociation {76fileMatch: string[];77uri: string;78}7980namespace SchemaAssociationNotification {81export const type: NotificationType<ISchemaAssociations | ISchemaAssociation[]> = new NotificationType('json/schemaAssociations');82}8384type Settings = {85json?: {86schemas?: JSONSchemaSettings[];87format?: { enable?: boolean };88keepLines?: { enable?: boolean };89validate?: { enable?: boolean };90resultLimit?: number;91jsonFoldingLimit?: number;92jsoncFoldingLimit?: number;93jsonColorDecoratorLimit?: number;94jsoncColorDecoratorLimit?: number;95};96http?: {97proxy?: string;98proxyStrictSSL?: boolean;99};100};101102export type JSONSchemaSettings = {103fileMatch?: string[];104url?: string;105schema?: any;106folderUri?: string;107};108109export namespace SettingIds {110export const enableFormatter = 'json.format.enable';111export const enableKeepLines = 'json.format.keepLines';112export const enableValidation = 'json.validate.enable';113export const enableSchemaDownload = 'json.schemaDownload.enable';114export const trustedDomains = 'json.schemaDownload.trustedDomains';115export const maxItemsComputed = 'json.maxItemsComputed';116export const editorFoldingMaximumRegions = 'editor.foldingMaximumRegions';117export const editorColorDecoratorsLimit = 'editor.colorDecoratorsLimit';118119export const editorSection = 'editor';120export const foldingMaximumRegions = 'foldingMaximumRegions';121export const colorDecoratorsLimit = 'colorDecoratorsLimit';122}123124export namespace CommandIds {125export const workbenchActionOpenSettings = 'workbench.action.openSettings';126export const workbenchTrustManage = 'workbench.trust.manage';127export const retryResolveSchemaCommandId = '_json.retryResolveSchema';128export const configureTrustedDomainsCommandId = '_json.configureTrustedDomains';129export const showAssociatedSchemaList = '_json.showAssociatedSchemaList';130export const clearCacheCommandId = 'json.clearCache';131export const validateCommandId = 'json.validate';132export const sortCommandId = 'json.sort';133}134135export interface TelemetryReporter {136sendTelemetryEvent(eventName: string, properties?: {137[key: string]: string;138}, measurements?: {139[key: string]: number;140}): void;141}142143export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;144145export interface Runtime {146schemaRequests: SchemaRequestService;147telemetry?: TelemetryReporter;148readonly timer: {149setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable;150};151logOutputChannel: LogOutputChannel;152}153154export interface SchemaRequestService {155getContent(uri: string): Promise<string>;156clearCache?(): Promise<string[]>;157}158159export enum SchemaRequestServiceErrors {160UntrustedWorkspaceError = 1,161UntrustedSchemaError = 2,162OpenTextDocumentAccessError = 3,163HTTPDisabledError = 4,164HTTPError = 5,165VSCodeAccessError = 6,166UntitledAccessError = 7,167}168169export const languageServerDescription = l10n.t('JSON Language Server');170171let resultLimit = 5000;172let jsonFoldingLimit = 5000;173let jsoncFoldingLimit = 5000;174let jsonColorDecoratorLimit = 5000;175let jsoncColorDecoratorLimit = 5000;176177export interface AsyncDisposable {178dispose(): Promise<void>;179}180181export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {182const languageParticipants = getLanguageParticipants();183context.subscriptions.push(languageParticipants);184185let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);186187let restartTrigger: Disposable | undefined;188languageParticipants.onDidChange(() => {189if (restartTrigger) {190restartTrigger.dispose();191}192restartTrigger = runtime.timer.setTimeout(async () => {193if (client) {194runtime.logOutputChannel.info('Extensions have changed, restarting JSON server...');195runtime.logOutputChannel.info('');196const oldClient = client;197client = undefined;198await oldClient.dispose();199client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);200}201}, 2000);202});203204return {205dispose: async () => {206restartTrigger?.dispose();207await client?.dispose();208}209};210}211212async function startClientWithParticipants(_context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {213214const toDispose: Disposable[] = [];215216let rangeFormatting: Disposable | undefined = undefined;217let settingsCache: Settings | undefined = undefined;218let schemaAssociationsCache: Promise<ISchemaAssociation[]> | undefined = undefined;219220const documentSelector = languageParticipants.documentSelector;221222const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0);223schemaResolutionErrorStatusBarItem.name = l10n.t('JSON: Schema Resolution Error');224schemaResolutionErrorStatusBarItem.text = '$(alert)';225toDispose.push(schemaResolutionErrorStatusBarItem);226227const fileSchemaErrors = new Map<string, string>();228let schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload);229let trustedDomains = workspace.getConfiguration().get<Record<string, boolean>>(SettingIds.trustedDomains, {});230231let isClientReady = false;232233const documentSymbolsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentSymbolsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit));234toDispose.push(documentSymbolsLimitStatusbarItem);235236const schemaLoadStatusItem = createSchemaLoadStatusItem((diagnostic: Diagnostic) => createSchemaLoadIssueItem(documentSelector, schemaDownloadEnabled, diagnostic));237toDispose.push(schemaLoadStatusItem);238239toDispose.push(commands.registerCommand(CommandIds.clearCacheCommandId, async () => {240if (isClientReady && runtime.schemaRequests.clearCache) {241const cachedSchemas = await runtime.schemaRequests.clearCache();242await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas);243}244window.showInformationMessage(l10n.t('JSON schema cache cleared.'));245}));246247toDispose.push(commands.registerCommand(CommandIds.validateCommandId, async (schemaUri: Uri, content: string) => {248const diagnostics: LSPDiagnostic[] = await client.sendRequest(ValidateContentRequest.type, { schemaUri: schemaUri.toString(), content });249return diagnostics.map(client.protocol2CodeConverter.asDiagnostic);250}));251252toDispose.push(commands.registerCommand(CommandIds.sortCommandId, async () => {253254if (isClientReady) {255const textEditor = window.activeTextEditor;256if (textEditor) {257const documentOptions = textEditor.options;258const textEdits = await getSortTextEdits(textEditor.document, documentOptions.tabSize, documentOptions.insertSpaces);259const success = await textEditor.edit(mutator => {260for (const edit of textEdits) {261mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText);262}263});264if (!success) {265window.showErrorMessage(l10n.t('Failed to sort the JSONC document, please consider opening an issue.'));266}267}268}269}));270271function handleSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] {272schemaLoadStatusItem.update(uri, diagnostics);273if (!schemaDownloadEnabled) {274return diagnostics.filter(d => !isSchemaResolveError(d));275}276return diagnostics;277}278279// Options to control the language client280const clientOptions: LanguageClientOptions = {281// Register the server for json documents282documentSelector,283initializationOptions: {284handledSchemaProtocols: ['file'], // language server only loads file-URI. Fetching schemas with other protocols ('http'...) are made on the client.285provideFormatter: false, // tell the server to not provide formatting capability and ignore the `json.format.enable` setting.286customCapabilities: { rangeFormatting: { editLimit: 10000 } }287},288synchronize: {289// Synchronize the setting section 'json' to the server290configurationSection: ['json', 'http'],291fileEvents: workspace.createFileSystemWatcher('**/*.json')292},293middleware: {294workspace: {295didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) })296},297provideDiagnostics: async (uriOrDoc, previousResolutId, token, next) => {298const diagnostics = await next(uriOrDoc, previousResolutId, token);299if (diagnostics && diagnostics.kind === DocumentDiagnosticReportKind.Full) {300const uri = uriOrDoc instanceof Uri ? uriOrDoc : uriOrDoc.uri;301diagnostics.items = handleSchemaErrorDiagnostics(uri, diagnostics.items);302}303return diagnostics;304},305handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => {306diagnostics = handleSchemaErrorDiagnostics(uri, diagnostics);307next(uri, diagnostics);308},309// testing the replace / insert mode310provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {311function update(item: CompletionItem) {312const range = item.range;313if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {314item.range = { inserting: new Range(range.start, position), replacing: range };315}316if (item.documentation instanceof MarkdownString) {317item.documentation = updateMarkdownString(item.documentation);318}319320}321function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {322if (r) {323(Array.isArray(r) ? r : r.items).forEach(update);324}325return r;326}327328const r = next(document, position, context, token);329if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {330return r.then(updateProposals);331}332return updateProposals(r);333},334provideHover(document: TextDocument, position: Position, token: CancellationToken, next: ProvideHoverSignature) {335function updateHover(r: Hover | null | undefined): Hover | null | undefined {336if (r && Array.isArray(r.contents)) {337r.contents = r.contents.map(h => h instanceof MarkdownString ? updateMarkdownString(h) : h);338}339return r;340}341const r = next(document, position, token);342if (isThenable<Hover | null | undefined>(r)) {343return r.then(updateHover);344}345return updateHover(r);346},347provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken, next: ProvideFoldingRangeSignature) {348const r = next(document, context, token);349if (isThenable<FoldingRange[] | null | undefined>(r)) {350return r;351}352return r;353},354provideDocumentColors(document: TextDocument, token: CancellationToken, next: ProvideDocumentColorsSignature) {355const r = next(document, token);356if (isThenable<ColorInformation[] | null | undefined>(r)) {357return r;358}359return r;360},361provideDocumentSymbols(document: TextDocument, token: CancellationToken, next: ProvideDocumentSymbolsSignature) {362type T = SymbolInformation[] | DocumentSymbol[];363function countDocumentSymbols(symbols: DocumentSymbol[]): number {364return symbols.reduce((previousValue, s) => previousValue + 1 + countDocumentSymbols(s.children), 0);365}366function isDocumentSymbol(r: T): r is DocumentSymbol[] {367return r[0] instanceof DocumentSymbol;368}369function checkLimit(r: T | null | undefined): T | null | undefined {370if (Array.isArray(r) && (isDocumentSymbol(r) ? countDocumentSymbols(r) : r.length) > resultLimit) {371documentSymbolsLimitStatusbarItem.update(document, resultLimit);372} else {373documentSymbolsLimitStatusbarItem.update(document, false);374}375return r;376}377const r = next(document, token);378if (isThenable<T | undefined | null>(r)) {379return r.then(checkLimit);380}381return checkLimit(r);382}383}384};385386clientOptions.outputChannel = runtime.logOutputChannel;387// Create the language client and start the client.388const client = newLanguageClient('json', languageServerDescription, clientOptions);389client.registerProposedFeatures();390391const schemaDocuments: { [uri: string]: boolean } = {};392393// handle content request394client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => {395const uri = Uri.parse(uriPath);396const uriString = uri.toString(true);397if (uri.scheme === 'untitled') {398throw new ResponseError(SchemaRequestServiceErrors.UntitledAccessError, l10n.t('Unable to load {0}', uriString));399}400if (uri.scheme === 'vscode') {401try {402runtime.logOutputChannel.info('read schema from vscode: ' + uriString);403ensureFilesystemWatcherInstalled(uri);404const content = await workspace.fs.readFile(uri);405return new TextDecoder().decode(content);406} catch (e) {407throw new ResponseError(SchemaRequestServiceErrors.VSCodeAccessError, e.toString(), e);408}409} else if (uri.scheme !== 'http' && uri.scheme !== 'https') {410try {411const document = await workspace.openTextDocument(uri);412schemaDocuments[uriString] = true;413return document.getText();414} catch (e) {415throw new ResponseError(SchemaRequestServiceErrors.OpenTextDocumentAccessError, e.toString(), e);416}417} else if (schemaDownloadEnabled) {418if (!workspace.isTrusted) {419throw new ResponseError(SchemaRequestServiceErrors.UntrustedWorkspaceError, l10n.t('Downloading schemas is disabled in untrusted workspaces'));420}421if (!await isTrusted(uri)) {422throw new ResponseError(SchemaRequestServiceErrors.UntrustedSchemaError, l10n.t('Location {0} is untrusted', uriString));423}424if (runtime.telemetry && uri.authority === 'schema.management.azure.com') {425/* __GDPR__426"json.schema" : {427"owner": "aeschli",428"comment": "Measure the use of the Azure resource manager schemas",429"schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The azure schema URL that was requested." }430}431*/432runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriString });433}434try {435return await runtime.schemaRequests.getContent(uriString);436} catch (e) {437throw new ResponseError(SchemaRequestServiceErrors.HTTPError, e.toString(), e);438}439} else {440throw new ResponseError(SchemaRequestServiceErrors.HTTPDisabledError, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload));441}442});443444await client.start();445446isClientReady = true;447448const handleContentChange = (uriString: string) => {449if (schemaDocuments[uriString]) {450client.sendNotification(SchemaContentChangeNotification.type, uriString);451return true;452}453return false;454};455const handleContentClosed = (uriString: string) => {456if (handleContentChange(uriString)) {457delete schemaDocuments[uriString];458}459fileSchemaErrors.delete(uriString);460};461462const watchers: Map<string, Disposable> = new Map();463toDispose.push(new Disposable(() => {464for (const d of watchers.values()) {465d.dispose();466}467}));468469470const ensureFilesystemWatcherInstalled = (uri: Uri) => {471472const uriString = uri.toString();473if (!watchers.has(uriString)) {474try {475const watcher = workspace.createFileSystemWatcher(new RelativePattern(uri, '*'));476const handleChange = (uri: Uri) => {477runtime.logOutputChannel.info('schema change detected ' + uri.toString());478client.sendNotification(SchemaContentChangeNotification.type, uriString);479};480const createListener = watcher.onDidCreate(handleChange);481const changeListener = watcher.onDidChange(handleChange);482const deleteListener = watcher.onDidDelete(() => {483const watcher = watchers.get(uriString);484if (watcher) {485watcher.dispose();486watchers.delete(uriString);487}488});489watchers.set(uriString, Disposable.from(watcher, createListener, changeListener, deleteListener));490} catch {491runtime.logOutputChannel.info('Problem installing a file system watcher for ' + uriString);492}493}494};495496toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString())));497toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString())));498499toDispose.push(commands.registerCommand(CommandIds.retryResolveSchemaCommandId, triggerValidation));500501toDispose.push(commands.registerCommand(CommandIds.configureTrustedDomainsCommandId, configureTrustedDomains));502503toDispose.push(languages.registerCodeActionsProvider(documentSelector, {504provideCodeActions(_document: TextDocument, _range: Range, context: CodeActionContext): CodeAction[] {505const codeActions: CodeAction[] = [];506507for (const diagnostic of context.diagnostics) {508if (typeof diagnostic.code !== 'number') {509continue;510}511switch (diagnostic.code) {512case ErrorCodes.UntrustedSchemaError: {513const title = l10n.t('Configure Trusted Domains...');514const action = new CodeAction(title, CodeActionKind.QuickFix);515const schemaUri = diagnostic.relatedInformation?.[0]?.location.uri;516if (schemaUri) {517action.command = { command: CommandIds.configureTrustedDomainsCommandId, arguments: [schemaUri.toString()], title };518} else {519action.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.trustedDomains], title };520}521action.diagnostics = [diagnostic];522action.isPreferred = true;523codeActions.push(action);524}525break;526case ErrorCodes.HTTPDisabledError: {527const title = l10n.t('Enable Schema Downloading...');528const action = new CodeAction(title, CodeActionKind.QuickFix);529action.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.enableSchemaDownload], title };530action.diagnostics = [diagnostic];531action.isPreferred = true;532codeActions.push(action);533}534break;535}536}537538return codeActions;539}540}, {541providedCodeActionKinds: [CodeActionKind.QuickFix]542}));543544client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(false));545546toDispose.push(extensions.onDidChange(async _ => {547client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true));548}));549550const associationWatcher = workspace.createFileSystemWatcher(new RelativePattern(Uri.parse(`vscode://schemas-associations/`), '**/schemas-associations.json'));551toDispose.push(associationWatcher);552toDispose.push(associationWatcher.onDidChange(async _e => {553client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true));554}));555556// manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652.557updateFormatterRegistration();558toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });559560toDispose.push(workspace.onDidChangeConfiguration(e => {561if (e.affectsConfiguration(SettingIds.enableFormatter)) {562updateFormatterRegistration();563} else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) {564schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload);565triggerValidation();566} else if (e.affectsConfiguration(SettingIds.editorFoldingMaximumRegions) || e.affectsConfiguration(SettingIds.editorColorDecoratorsLimit)) {567client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) });568} else if (e.affectsConfiguration(SettingIds.trustedDomains)) {569trustedDomains = workspace.getConfiguration().get<Record<string, boolean>>(SettingIds.trustedDomains, {});570triggerValidation();571}572}));573toDispose.push(workspace.onDidGrantWorkspaceTrust(() => triggerValidation()));574575toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri)));576577function updateFormatterRegistration() {578const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter);579if (!formatEnabled && rangeFormatting) {580rangeFormatting.dispose();581rangeFormatting = undefined;582} else if (formatEnabled && !rangeFormatting) {583rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {584provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {585const filesConfig = workspace.getConfiguration('files', document);586const fileFormattingOptions = {587trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),588trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),589insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),590};591const params: DocumentRangeFormattingParams = {592textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),593range: client.code2ProtocolConverter.asRange(range),594options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)595};596597return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(598client.protocol2CodeConverter.asTextEdits,599(error) => {600client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);601return Promise.resolve([]);602}603);604}605});606}607}608609async function triggerValidation() {610const activeTextEditor = window.activeTextEditor;611if (activeTextEditor && languageParticipants.hasLanguage(activeTextEditor.document.languageId)) {612schemaResolutionErrorStatusBarItem.text = '$(watch)';613schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Validating...');614const activeDocUri = activeTextEditor.document.uri.toString();615await client.sendRequest(ForceValidateRequest.type, activeDocUri);616}617}618619async function getSortTextEdits(document: TextDocument, tabSize: string | number = 4, insertSpaces: string | boolean = true): Promise<TextEdit[]> {620const filesConfig = workspace.getConfiguration('files', document);621const options: SortOptions = {622tabSize: Number(tabSize),623insertSpaces: Boolean(insertSpaces),624trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),625trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),626insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),627};628const params: DocumentSortingParams = {629uri: document.uri.toString(),630options631};632const edits = await client.sendRequest(DocumentSortingRequest.type, params);633// Here we convert the JSON objects to real TextEdit objects634return edits.map((edit) => {635return new TextEdit(636new Range(edit.range.start.line, edit.range.start.character, edit.range.end.line, edit.range.end.character),637edit.newText638);639});640}641642function getSettings(forceRefresh: boolean): Settings {643if (!settingsCache || forceRefresh) {644settingsCache = computeSettings();645}646return settingsCache;647}648649async function getSchemaAssociations(forceRefresh: boolean): Promise<ISchemaAssociation[]> {650if (!schemaAssociationsCache || forceRefresh) {651schemaAssociationsCache = computeSchemaAssociations();652}653return schemaAssociationsCache;654}655656async function isTrusted(uri: Uri): Promise<boolean> {657if (uri.scheme !== 'http' && uri.scheme !== 'https') {658return true;659}660const uriString = uri.toString(true);661662// Check against trustedDomains setting663if (matchesUrlPattern(uri, trustedDomains)) {664return true;665}666667const knownAssociations = await getSchemaAssociations(false);668for (const association of knownAssociations) {669if (association.uri === uriString) {670return true;671}672}673const settingsCache = getSettings(false);674if (settingsCache.json && settingsCache.json.schemas) {675for (const schemaSetting of settingsCache.json.schemas) {676const schemaUri = schemaSetting.url;677if (schemaUri === uriString) {678return true;679}680}681}682return false;683}684685async function configureTrustedDomains(schemaUri: string): Promise<void> {686interface QuickPickItemWithAction {687label: string;688description?: string;689execute: () => Promise<void>;690}691692const items: QuickPickItemWithAction[] = [];693694try {695const uri = Uri.parse(schemaUri);696const domain = `${uri.scheme}://${uri.authority}`;697698// Add "Trust domain" option699items.push({700label: l10n.t('Trust Domain: {0}', domain),701description: l10n.t('Allow all schemas from this domain'),702execute: async () => {703const config = workspace.getConfiguration();704const currentDomains = config.get<Record<string, boolean>>(SettingIds.trustedDomains, {});705currentDomains[domain] = true;706await config.update(SettingIds.trustedDomains, currentDomains, true);707await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);708}709});710711// Add "Trust URI" option712items.push({713label: l10n.t('Trust URI: {0}', schemaUri),714description: l10n.t('Allow only this specific schema'),715execute: async () => {716const config = workspace.getConfiguration();717const currentDomains = config.get<Record<string, boolean>>(SettingIds.trustedDomains, {});718currentDomains[schemaUri] = true;719await config.update(SettingIds.trustedDomains, currentDomains, true);720await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);721}722});723} catch (e) {724runtime.logOutputChannel.error(`Failed to parse schema URI: ${schemaUri}`);725}726727728// Always add "Configure setting" option729items.push({730label: l10n.t('Configure Setting'),731description: l10n.t('Open settings editor'),732execute: async () => {733await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);734}735});736737const selected = await window.showQuickPick(items, {738placeHolder: l10n.t('Select how to configure trusted schema domains')739});740741if (selected) {742await selected.execute();743}744}745746747return {748dispose: async () => {749await client.stop();750toDispose.forEach(d => d.dispose());751rangeFormatting?.dispose();752}753};754}755756async function computeSchemaAssociations(): Promise<ISchemaAssociation[]> {757const extensionAssociations = getSchemaExtensionAssociations();758return extensionAssociations.concat(await getDynamicSchemaAssociations());759}760761function getSchemaExtensionAssociations(): ISchemaAssociation[] {762const associations: ISchemaAssociation[] = [];763extensions.allAcrossExtensionHosts.forEach(extension => {764const packageJSON = extension.packageJSON;765if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation) {766const jsonValidation = packageJSON.contributes.jsonValidation;767if (Array.isArray(jsonValidation)) {768jsonValidation.forEach(jv => {769let { fileMatch, url } = jv;770if (typeof fileMatch === 'string') {771fileMatch = [fileMatch];772}773if (Array.isArray(fileMatch) && typeof url === 'string') {774let uri: string = url;775if (uri[0] === '.' && uri[1] === '/') {776uri = Uri.joinPath(extension.extensionUri, uri).toString();777}778fileMatch = fileMatch.map(fm => {779if (fm[0] === '%') {780fm = fm.replace(/%APP_SETTINGS_HOME%/, '/User');781fm = fm.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine');782fm = fm.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces');783} else if (!fm.match(/^(\w+:\/\/|\/|!)/)) {784fm = '/' + fm;785}786return fm;787});788associations.push({ fileMatch, uri });789}790});791}792}793});794return associations;795}796797async function getDynamicSchemaAssociations(): Promise<ISchemaAssociation[]> {798const result: ISchemaAssociation[] = [];799try {800const data = await workspace.fs.readFile(Uri.parse(`vscode://schemas-associations/schemas-associations.json`));801const rawStr = new TextDecoder().decode(data);802const obj = <Record<string, string[]>>JSON.parse(rawStr);803for (const item of Object.keys(obj)) {804result.push({805fileMatch: obj[item],806uri: item807});808}809} catch {810// ignore811}812return result;813}814815816817function computeSettings(): Settings {818const configuration = workspace.getConfiguration();819const httpSettings = workspace.getConfiguration('http');820821const normalizeLimit = (settingValue: any) => Math.trunc(Math.max(0, Number(settingValue))) || 5000;822823resultLimit = normalizeLimit(workspace.getConfiguration().get(SettingIds.maxItemsComputed));824const editorJSONSettings = workspace.getConfiguration(SettingIds.editorSection, { languageId: 'json' });825const editorJSONCSettings = workspace.getConfiguration(SettingIds.editorSection, { languageId: 'jsonc' });826827jsonFoldingLimit = normalizeLimit(editorJSONSettings.get(SettingIds.foldingMaximumRegions));828jsoncFoldingLimit = normalizeLimit(editorJSONCSettings.get(SettingIds.foldingMaximumRegions));829jsonColorDecoratorLimit = normalizeLimit(editorJSONSettings.get(SettingIds.colorDecoratorsLimit));830jsoncColorDecoratorLimit = normalizeLimit(editorJSONCSettings.get(SettingIds.colorDecoratorsLimit));831832const schemas: JSONSchemaSettings[] = [];833834const settings: Settings = {835http: {836proxy: httpSettings.get('proxy'),837proxyStrictSSL: httpSettings.get('proxyStrictSSL')838},839json: {840validate: { enable: configuration.get(SettingIds.enableValidation) },841format: { enable: configuration.get(SettingIds.enableFormatter) },842keepLines: { enable: configuration.get(SettingIds.enableKeepLines) },843schemas,844resultLimit: resultLimit + 1, // ask for one more so we can detect if the limit has been exceeded845jsonFoldingLimit: jsonFoldingLimit + 1,846jsoncFoldingLimit: jsoncFoldingLimit + 1,847jsonColorDecoratorLimit: jsonColorDecoratorLimit + 1,848jsoncColorDecoratorLimit: jsoncColorDecoratorLimit + 1849}850};851852/*853* Add schemas from the settings854* folderUri to which folder the setting is scoped to. `undefined` means global (also external files)855* settingsLocation against which path relative schema URLs are resolved856*/857const collectSchemaSettings = (schemaSettings: JSONSchemaSettings[] | undefined, folderUri: string | undefined, settingsLocation: Uri | undefined) => {858if (schemaSettings) {859for (const setting of schemaSettings) {860const url = getSchemaId(setting, settingsLocation);861if (url) {862const schemaSetting: JSONSchemaSettings = { url, fileMatch: setting.fileMatch, folderUri, schema: setting.schema };863schemas.push(schemaSetting);864}865}866}867};868869const folders = workspace.workspaceFolders ?? [];870871const schemaConfigInfo = workspace.getConfiguration('json', null).inspect<JSONSchemaSettings[]>('schemas');872if (schemaConfigInfo) {873// settings in user config874collectSchemaSettings(schemaConfigInfo.globalValue, undefined, undefined);875if (workspace.workspaceFile) {876if (schemaConfigInfo.workspaceValue) {877const settingsLocation = Uri.joinPath(workspace.workspaceFile, '..');878// settings in the workspace configuration file apply to all files (also external files)879collectSchemaSettings(schemaConfigInfo.workspaceValue, undefined, settingsLocation);880}881for (const folder of folders) {882const folderUri = folder.uri;883const folderSchemaConfigInfo = workspace.getConfiguration('json', folderUri).inspect<JSONSchemaSettings[]>('schemas');884collectSchemaSettings(folderSchemaConfigInfo?.workspaceFolderValue, folderUri.toString(false), folderUri);885}886} else {887if (schemaConfigInfo.workspaceValue && folders.length === 1) {888// single folder workspace: settings apply to all files (also external files)889collectSchemaSettings(schemaConfigInfo.workspaceValue, undefined, folders[0].uri);890}891}892}893return settings;894}895896function getSchemaId(schema: JSONSchemaSettings, settingsLocation?: Uri): string | undefined {897let url = schema.url;898if (!url) {899if (schema.schema) {900url = schema.schema.id || `vscode://schemas/custom/${encodeURIComponent(hash(schema.schema).toString(16))}`;901}902} else if (settingsLocation && (url[0] === '.' || url[0] === '/')) {903url = Uri.joinPath(settingsLocation, url).toString(false);904}905return url;906}907908function isThenable<T>(obj: unknown): obj is Thenable<T> {909return !!obj && typeof (obj as unknown as Thenable<T>).then === 'function';910}911912function updateMarkdownString(h: MarkdownString): MarkdownString {913const n = new MarkdownString(h.value, true);914n.isTrusted = h.isTrusted;915return n;916}917918export namespace ErrorCodes {919export const SchemaResolveError = 0x10000;920export const UntrustedSchemaError = SchemaResolveError + SchemaRequestServiceErrors.UntrustedSchemaError;921export const HTTPDisabledError = SchemaResolveError + SchemaRequestServiceErrors.HTTPDisabledError;922}923924export function isSchemaResolveError(d: Diagnostic) {925return typeof d.code === 'number' && d.code >= ErrorCodes.SchemaResolveError;926}927928929930931