Path: blob/main/extensions/json-language-features/client/src/jsonClient.ts
3320 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, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange,10ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n,11RelativePattern12} 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 } from './languageStatus';23import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';2425namespace VSCodeContentRequest {26export const type: RequestType<string, string, any> = new RequestType('vscode/content');27}2829namespace SchemaContentChangeNotification {30export const type: NotificationType<string | string[]> = new NotificationType('json/schemaContent');31}3233namespace ForceValidateRequest {34export const type: RequestType<string, Diagnostic[], any> = new RequestType('json/validate');35}3637namespace LanguageStatusRequest {38export const type: RequestType<string, JSONLanguageStatus, any> = new RequestType('json/languageStatus');39}4041namespace ValidateContentRequest {42export const type: RequestType<{ schemaUri: string; content: string }, LSPDiagnostic[], any> = new RequestType('json/validateContent');43}44interface SortOptions extends LSPFormattingOptions {45}4647interface DocumentSortingParams {48/**49* The uri of the document to sort.50*/51readonly uri: string;52/**53* The sort options54*/55readonly options: SortOptions;56}5758namespace DocumentSortingRequest {59export interface ITextEdit {60range: {61start: { line: number; character: number };62end: { line: number; character: number };63};64newText: string;65}66export const type: RequestType<DocumentSortingParams, ITextEdit[], any> = new RequestType('json/sort');67}6869export interface ISchemaAssociations {70[pattern: string]: string[];71}7273export interface ISchemaAssociation {74fileMatch: string[];75uri: string;76}7778namespace SchemaAssociationNotification {79export const type: NotificationType<ISchemaAssociations | ISchemaAssociation[]> = new NotificationType('json/schemaAssociations');80}8182type Settings = {83json?: {84schemas?: JSONSchemaSettings[];85format?: { enable?: boolean };86keepLines?: { enable?: boolean };87validate?: { enable?: boolean };88resultLimit?: number;89jsonFoldingLimit?: number;90jsoncFoldingLimit?: number;91jsonColorDecoratorLimit?: number;92jsoncColorDecoratorLimit?: number;93};94http?: {95proxy?: string;96proxyStrictSSL?: boolean;97};98};99100export type JSONSchemaSettings = {101fileMatch?: string[];102url?: string;103schema?: any;104folderUri?: string;105};106107export namespace SettingIds {108export const enableFormatter = 'json.format.enable';109export const enableKeepLines = 'json.format.keepLines';110export const enableValidation = 'json.validate.enable';111export const enableSchemaDownload = 'json.schemaDownload.enable';112export const maxItemsComputed = 'json.maxItemsComputed';113export const editorFoldingMaximumRegions = 'editor.foldingMaximumRegions';114export const editorColorDecoratorsLimit = 'editor.colorDecoratorsLimit';115116export const editorSection = 'editor';117export const foldingMaximumRegions = 'foldingMaximumRegions';118export const colorDecoratorsLimit = 'colorDecoratorsLimit';119}120121export interface TelemetryReporter {122sendTelemetryEvent(eventName: string, properties?: {123[key: string]: string;124}, measurements?: {125[key: string]: number;126}): void;127}128129export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;130131export interface Runtime {132schemaRequests: SchemaRequestService;133telemetry?: TelemetryReporter;134readonly timer: {135setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable;136};137logOutputChannel: LogOutputChannel;138}139140export interface SchemaRequestService {141getContent(uri: string): Promise<string>;142clearCache?(): Promise<string[]>;143}144145export const languageServerDescription = l10n.t('JSON Language Server');146147let resultLimit = 5000;148let jsonFoldingLimit = 5000;149let jsoncFoldingLimit = 5000;150let jsonColorDecoratorLimit = 5000;151let jsoncColorDecoratorLimit = 5000;152153export interface AsyncDisposable {154dispose(): Promise<void>;155}156157export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {158const languageParticipants = getLanguageParticipants();159context.subscriptions.push(languageParticipants);160161let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);162163let restartTrigger: Disposable | undefined;164languageParticipants.onDidChange(() => {165if (restartTrigger) {166restartTrigger.dispose();167}168restartTrigger = runtime.timer.setTimeout(async () => {169if (client) {170runtime.logOutputChannel.info('Extensions have changed, restarting JSON server...');171runtime.logOutputChannel.info('');172const oldClient = client;173client = undefined;174await oldClient.dispose();175client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);176}177}, 2000);178});179180return {181dispose: async () => {182restartTrigger?.dispose();183await client?.dispose();184}185};186}187188async function startClientWithParticipants(_context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {189190const toDispose: Disposable[] = [];191192let rangeFormatting: Disposable | undefined = undefined;193194const documentSelector = languageParticipants.documentSelector;195196const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0);197schemaResolutionErrorStatusBarItem.name = l10n.t('JSON: Schema Resolution Error');198schemaResolutionErrorStatusBarItem.text = '$(alert)';199toDispose.push(schemaResolutionErrorStatusBarItem);200201const fileSchemaErrors = new Map<string, string>();202let schemaDownloadEnabled = true;203204let isClientReady = false;205206const documentSymbolsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentSymbolsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit));207toDispose.push(documentSymbolsLimitStatusbarItem);208209toDispose.push(commands.registerCommand('json.clearCache', async () => {210if (isClientReady && runtime.schemaRequests.clearCache) {211const cachedSchemas = await runtime.schemaRequests.clearCache();212await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas);213}214window.showInformationMessage(l10n.t('JSON schema cache cleared.'));215}));216217toDispose.push(commands.registerCommand('json.validate', async (schemaUri: Uri, content: string) => {218const diagnostics: LSPDiagnostic[] = await client.sendRequest(ValidateContentRequest.type, { schemaUri: schemaUri.toString(), content });219return diagnostics.map(client.protocol2CodeConverter.asDiagnostic);220}));221222toDispose.push(commands.registerCommand('json.sort', async () => {223224if (isClientReady) {225const textEditor = window.activeTextEditor;226if (textEditor) {227const documentOptions = textEditor.options;228const textEdits = await getSortTextEdits(textEditor.document, documentOptions.tabSize, documentOptions.insertSpaces);229const success = await textEditor.edit(mutator => {230for (const edit of textEdits) {231mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText);232}233});234if (!success) {235window.showErrorMessage(l10n.t('Failed to sort the JSONC document, please consider opening an issue.'));236}237}238}239}));240241function filterSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] {242const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError);243if (schemaErrorIndex !== -1) {244const schemaResolveDiagnostic = diagnostics[schemaErrorIndex];245fileSchemaErrors.set(uri.toString(), schemaResolveDiagnostic.message);246if (!schemaDownloadEnabled) {247diagnostics = diagnostics.filter(d => !isSchemaResolveError(d));248}249if (window.activeTextEditor && window.activeTextEditor.document.uri.toString() === uri.toString()) {250schemaResolutionErrorStatusBarItem.show();251}252}253return diagnostics;254}255256// Options to control the language client257const clientOptions: LanguageClientOptions = {258// Register the server for json documents259documentSelector,260initializationOptions: {261handledSchemaProtocols: ['file'], // language server only loads file-URI. Fetching schemas with other protocols ('http'...) are made on the client.262provideFormatter: false, // tell the server to not provide formatting capability and ignore the `json.format.enable` setting.263customCapabilities: { rangeFormatting: { editLimit: 10000 } }264},265synchronize: {266// Synchronize the setting section 'json' to the server267configurationSection: ['json', 'http'],268fileEvents: workspace.createFileSystemWatcher('**/*.json')269},270middleware: {271workspace: {272didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() })273},274provideDiagnostics: async (uriOrDoc, previousResolutId, token, next) => {275const diagnostics = await next(uriOrDoc, previousResolutId, token);276if (diagnostics && diagnostics.kind === DocumentDiagnosticReportKind.Full) {277const uri = uriOrDoc instanceof Uri ? uriOrDoc : uriOrDoc.uri;278diagnostics.items = filterSchemaErrorDiagnostics(uri, diagnostics.items);279}280return diagnostics;281},282handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => {283diagnostics = filterSchemaErrorDiagnostics(uri, diagnostics);284next(uri, diagnostics);285},286// testing the replace / insert mode287provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {288function update(item: CompletionItem) {289const range = item.range;290if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {291item.range = { inserting: new Range(range.start, position), replacing: range };292}293if (item.documentation instanceof MarkdownString) {294item.documentation = updateMarkdownString(item.documentation);295}296297}298function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {299if (r) {300(Array.isArray(r) ? r : r.items).forEach(update);301}302return r;303}304305const r = next(document, position, context, token);306if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {307return r.then(updateProposals);308}309return updateProposals(r);310},311provideHover(document: TextDocument, position: Position, token: CancellationToken, next: ProvideHoverSignature) {312function updateHover(r: Hover | null | undefined): Hover | null | undefined {313if (r && Array.isArray(r.contents)) {314r.contents = r.contents.map(h => h instanceof MarkdownString ? updateMarkdownString(h) : h);315}316return r;317}318const r = next(document, position, token);319if (isThenable<Hover | null | undefined>(r)) {320return r.then(updateHover);321}322return updateHover(r);323},324provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken, next: ProvideFoldingRangeSignature) {325const r = next(document, context, token);326if (isThenable<FoldingRange[] | null | undefined>(r)) {327return r;328}329return r;330},331provideDocumentColors(document: TextDocument, token: CancellationToken, next: ProvideDocumentColorsSignature) {332const r = next(document, token);333if (isThenable<ColorInformation[] | null | undefined>(r)) {334return r;335}336return r;337},338provideDocumentSymbols(document: TextDocument, token: CancellationToken, next: ProvideDocumentSymbolsSignature) {339type T = SymbolInformation[] | DocumentSymbol[];340function countDocumentSymbols(symbols: DocumentSymbol[]): number {341return symbols.reduce((previousValue, s) => previousValue + 1 + countDocumentSymbols(s.children), 0);342}343function isDocumentSymbol(r: T): r is DocumentSymbol[] {344return r[0] instanceof DocumentSymbol;345}346function checkLimit(r: T | null | undefined): T | null | undefined {347if (Array.isArray(r) && (isDocumentSymbol(r) ? countDocumentSymbols(r) : r.length) > resultLimit) {348documentSymbolsLimitStatusbarItem.update(document, resultLimit);349} else {350documentSymbolsLimitStatusbarItem.update(document, false);351}352return r;353}354const r = next(document, token);355if (isThenable<T | undefined | null>(r)) {356return r.then(checkLimit);357}358return checkLimit(r);359}360}361};362363clientOptions.outputChannel = runtime.logOutputChannel;364// Create the language client and start the client.365const client = newLanguageClient('json', languageServerDescription, clientOptions);366client.registerProposedFeatures();367368const schemaDocuments: { [uri: string]: boolean } = {};369370// handle content request371client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => {372const uri = Uri.parse(uriPath);373const uriString = uri.toString(true);374if (uri.scheme === 'untitled') {375throw new ResponseError(3, l10n.t('Unable to load {0}', uriString));376}377if (uri.scheme === 'vscode') {378try {379runtime.logOutputChannel.info('read schema from vscode: ' + uriString);380ensureFilesystemWatcherInstalled(uri);381const content = await workspace.fs.readFile(uri);382return new TextDecoder().decode(content);383} catch (e) {384throw new ResponseError(5, e.toString(), e);385}386} else if (uri.scheme !== 'http' && uri.scheme !== 'https') {387try {388const document = await workspace.openTextDocument(uri);389schemaDocuments[uriString] = true;390return document.getText();391} catch (e) {392throw new ResponseError(2, e.toString(), e);393}394} else if (schemaDownloadEnabled && workspace.isTrusted) {395if (runtime.telemetry && uri.authority === 'schema.management.azure.com') {396/* __GDPR__397"json.schema" : {398"owner": "aeschli",399"comment": "Measure the use of the Azure resource manager schemas",400"schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The azure schema URL that was requested." }401}402*/403runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriString });404}405try {406return await runtime.schemaRequests.getContent(uriString);407} catch (e) {408throw new ResponseError(4, e.toString());409}410} else {411if (!workspace.isTrusted) {412throw new ResponseError(1, l10n.t('Downloading schemas is disabled in untrusted workspaces'));413}414throw new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload));415}416});417418await client.start();419420isClientReady = true;421422const handleContentChange = (uriString: string) => {423if (schemaDocuments[uriString]) {424client.sendNotification(SchemaContentChangeNotification.type, uriString);425return true;426}427return false;428};429const handleActiveEditorChange = (activeEditor?: TextEditor) => {430if (!activeEditor) {431return;432}433434const activeDocUri = activeEditor.document.uri.toString();435436if (activeDocUri && fileSchemaErrors.has(activeDocUri)) {437schemaResolutionErrorStatusBarItem.show();438} else {439schemaResolutionErrorStatusBarItem.hide();440}441};442const handleContentClosed = (uriString: string) => {443if (handleContentChange(uriString)) {444delete schemaDocuments[uriString];445}446fileSchemaErrors.delete(uriString);447};448449const watchers: Map<string, Disposable> = new Map();450toDispose.push(new Disposable(() => {451for (const d of watchers.values()) {452d.dispose();453}454}));455456457const ensureFilesystemWatcherInstalled = (uri: Uri) => {458459const uriString = uri.toString();460if (!watchers.has(uriString)) {461try {462const watcher = workspace.createFileSystemWatcher(new RelativePattern(uri, '*'));463const handleChange = (uri: Uri) => {464runtime.logOutputChannel.info('schema change detected ' + uri.toString());465client.sendNotification(SchemaContentChangeNotification.type, uriString);466};467const createListener = watcher.onDidCreate(handleChange);468const changeListener = watcher.onDidChange(handleChange);469const deleteListener = watcher.onDidDelete(() => {470const watcher = watchers.get(uriString);471if (watcher) {472watcher.dispose();473watchers.delete(uriString);474}475});476watchers.set(uriString, Disposable.from(watcher, createListener, changeListener, deleteListener));477} catch {478runtime.logOutputChannel.info('Problem installing a file system watcher for ' + uriString);479}480}481};482483toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString())));484toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString())));485486toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange));487488const handleRetryResolveSchemaCommand = () => {489if (window.activeTextEditor) {490schemaResolutionErrorStatusBarItem.text = '$(watch)';491const activeDocUri = window.activeTextEditor.document.uri.toString();492client.sendRequest(ForceValidateRequest.type, activeDocUri).then((diagnostics) => {493const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError);494if (schemaErrorIndex !== -1) {495// Show schema resolution errors in status bar only; ref: #51032496const schemaResolveDiagnostic = diagnostics[schemaErrorIndex];497fileSchemaErrors.set(activeDocUri, schemaResolveDiagnostic.message);498} else {499schemaResolutionErrorStatusBarItem.hide();500}501schemaResolutionErrorStatusBarItem.text = '$(alert)';502});503}504};505506toDispose.push(commands.registerCommand('_json.retryResolveSchema', handleRetryResolveSchemaCommand));507508client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations());509510toDispose.push(extensions.onDidChange(async _ => {511client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations());512}));513514const associationWatcher = workspace.createFileSystemWatcher(new RelativePattern(515Uri.parse(`vscode://schemas-associations/`),516'**/schemas-associations.json')517);518toDispose.push(associationWatcher);519toDispose.push(associationWatcher.onDidChange(async _e => {520client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations());521}));522523// manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652.524updateFormatterRegistration();525toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });526527updateSchemaDownloadSetting();528529toDispose.push(workspace.onDidChangeConfiguration(e => {530if (e.affectsConfiguration(SettingIds.enableFormatter)) {531updateFormatterRegistration();532} else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) {533updateSchemaDownloadSetting();534} else if (e.affectsConfiguration(SettingIds.editorFoldingMaximumRegions) || e.affectsConfiguration(SettingIds.editorColorDecoratorsLimit)) {535client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() });536}537}));538toDispose.push(workspace.onDidGrantWorkspaceTrust(updateSchemaDownloadSetting));539540toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri)));541542function updateFormatterRegistration() {543const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter);544if (!formatEnabled && rangeFormatting) {545rangeFormatting.dispose();546rangeFormatting = undefined;547} else if (formatEnabled && !rangeFormatting) {548rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {549provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {550const filesConfig = workspace.getConfiguration('files', document);551const fileFormattingOptions = {552trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),553trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),554insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),555};556const params: DocumentRangeFormattingParams = {557textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),558range: client.code2ProtocolConverter.asRange(range),559options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)560};561562return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(563client.protocol2CodeConverter.asTextEdits,564(error) => {565client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);566return Promise.resolve([]);567}568);569}570});571}572}573574function updateSchemaDownloadSetting() {575if (!workspace.isTrusted) {576schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Unable to download schemas in untrusted workspaces.');577schemaResolutionErrorStatusBarItem.command = 'workbench.trust.manage';578return;579}580schemaDownloadEnabled = workspace.getConfiguration().get(SettingIds.enableSchemaDownload) !== false;581if (schemaDownloadEnabled) {582schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Unable to resolve schema. Click to retry.');583schemaResolutionErrorStatusBarItem.command = '_json.retryResolveSchema';584handleRetryResolveSchemaCommand();585} else {586schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Downloading schemas is disabled. Click to configure.');587schemaResolutionErrorStatusBarItem.command = { command: 'workbench.action.openSettings', arguments: [SettingIds.enableSchemaDownload], title: '' };588}589}590591async function getSortTextEdits(document: TextDocument, tabSize: string | number = 4, insertSpaces: string | boolean = true): Promise<TextEdit[]> {592const filesConfig = workspace.getConfiguration('files', document);593const options: SortOptions = {594tabSize: Number(tabSize),595insertSpaces: Boolean(insertSpaces),596trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),597trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),598insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),599};600const params: DocumentSortingParams = {601uri: document.uri.toString(),602options603};604const edits = await client.sendRequest(DocumentSortingRequest.type, params);605// Here we convert the JSON objects to real TextEdit objects606return edits.map((edit) => {607return new TextEdit(608new Range(edit.range.start.line, edit.range.start.character, edit.range.end.line, edit.range.end.character),609edit.newText610);611});612}613614return {615dispose: async () => {616await client.stop();617toDispose.forEach(d => d.dispose());618rangeFormatting?.dispose();619}620};621}622623async function getSchemaAssociations(): Promise<ISchemaAssociation[]> {624return getSchemaExtensionAssociations()625.concat(await getDynamicSchemaAssociations());626}627628function getSchemaExtensionAssociations(): ISchemaAssociation[] {629const associations: ISchemaAssociation[] = [];630extensions.allAcrossExtensionHosts.forEach(extension => {631const packageJSON = extension.packageJSON;632if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation) {633const jsonValidation = packageJSON.contributes.jsonValidation;634if (Array.isArray(jsonValidation)) {635jsonValidation.forEach(jv => {636let { fileMatch, url } = jv;637if (typeof fileMatch === 'string') {638fileMatch = [fileMatch];639}640if (Array.isArray(fileMatch) && typeof url === 'string') {641let uri: string = url;642if (uri[0] === '.' && uri[1] === '/') {643uri = Uri.joinPath(extension.extensionUri, uri).toString();644}645fileMatch = fileMatch.map(fm => {646if (fm[0] === '%') {647fm = fm.replace(/%APP_SETTINGS_HOME%/, '/User');648fm = fm.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine');649fm = fm.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces');650} else if (!fm.match(/^(\w+:\/\/|\/|!)/)) {651fm = '/' + fm;652}653return fm;654});655associations.push({ fileMatch, uri });656}657});658}659}660});661return associations;662}663664async function getDynamicSchemaAssociations(): Promise<ISchemaAssociation[]> {665const result: ISchemaAssociation[] = [];666try {667const data = await workspace.fs.readFile(Uri.parse(`vscode://schemas-associations/schemas-associations.json`));668const rawStr = new TextDecoder().decode(data);669const obj = <Record<string, string[]>>JSON.parse(rawStr);670for (const item of Object.keys(obj)) {671result.push({672fileMatch: obj[item],673uri: item674});675}676} catch {677// ignore678}679return result;680}681682function getSettings(): Settings {683const configuration = workspace.getConfiguration();684const httpSettings = workspace.getConfiguration('http');685686const normalizeLimit = (settingValue: any) => Math.trunc(Math.max(0, Number(settingValue))) || 5000;687688resultLimit = normalizeLimit(workspace.getConfiguration().get(SettingIds.maxItemsComputed));689const editorJSONSettings = workspace.getConfiguration(SettingIds.editorSection, { languageId: 'json' });690const editorJSONCSettings = workspace.getConfiguration(SettingIds.editorSection, { languageId: 'jsonc' });691692jsonFoldingLimit = normalizeLimit(editorJSONSettings.get(SettingIds.foldingMaximumRegions));693jsoncFoldingLimit = normalizeLimit(editorJSONCSettings.get(SettingIds.foldingMaximumRegions));694jsonColorDecoratorLimit = normalizeLimit(editorJSONSettings.get(SettingIds.colorDecoratorsLimit));695jsoncColorDecoratorLimit = normalizeLimit(editorJSONCSettings.get(SettingIds.colorDecoratorsLimit));696697const schemas: JSONSchemaSettings[] = [];698699const settings: Settings = {700http: {701proxy: httpSettings.get('proxy'),702proxyStrictSSL: httpSettings.get('proxyStrictSSL')703},704json: {705validate: { enable: configuration.get(SettingIds.enableValidation) },706format: { enable: configuration.get(SettingIds.enableFormatter) },707keepLines: { enable: configuration.get(SettingIds.enableKeepLines) },708schemas,709resultLimit: resultLimit + 1, // ask for one more so we can detect if the limit has been exceeded710jsonFoldingLimit: jsonFoldingLimit + 1,711jsoncFoldingLimit: jsoncFoldingLimit + 1,712jsonColorDecoratorLimit: jsonColorDecoratorLimit + 1,713jsoncColorDecoratorLimit: jsoncColorDecoratorLimit + 1714}715};716717/*718* Add schemas from the settings719* folderUri to which folder the setting is scoped to. `undefined` means global (also external files)720* settingsLocation against which path relative schema URLs are resolved721*/722const collectSchemaSettings = (schemaSettings: JSONSchemaSettings[] | undefined, folderUri: string | undefined, settingsLocation: Uri | undefined) => {723if (schemaSettings) {724for (const setting of schemaSettings) {725const url = getSchemaId(setting, settingsLocation);726if (url) {727const schemaSetting: JSONSchemaSettings = { url, fileMatch: setting.fileMatch, folderUri, schema: setting.schema };728schemas.push(schemaSetting);729}730}731}732};733734const folders = workspace.workspaceFolders ?? [];735736const schemaConfigInfo = workspace.getConfiguration('json', null).inspect<JSONSchemaSettings[]>('schemas');737if (schemaConfigInfo) {738// settings in user config739collectSchemaSettings(schemaConfigInfo.globalValue, undefined, undefined);740if (workspace.workspaceFile) {741if (schemaConfigInfo.workspaceValue) {742const settingsLocation = Uri.joinPath(workspace.workspaceFile, '..');743// settings in the workspace configuration file apply to all files (also external files)744collectSchemaSettings(schemaConfigInfo.workspaceValue, undefined, settingsLocation);745}746for (const folder of folders) {747const folderUri = folder.uri;748const folderSchemaConfigInfo = workspace.getConfiguration('json', folderUri).inspect<JSONSchemaSettings[]>('schemas');749collectSchemaSettings(folderSchemaConfigInfo?.workspaceFolderValue, folderUri.toString(false), folderUri);750}751} else {752if (schemaConfigInfo.workspaceValue && folders.length === 1) {753// single folder workspace: settings apply to all files (also external files)754collectSchemaSettings(schemaConfigInfo.workspaceValue, undefined, folders[0].uri);755}756}757}758return settings;759}760761function getSchemaId(schema: JSONSchemaSettings, settingsLocation?: Uri): string | undefined {762let url = schema.url;763if (!url) {764if (schema.schema) {765url = schema.schema.id || `vscode://schemas/custom/${encodeURIComponent(hash(schema.schema).toString(16))}`;766}767} else if (settingsLocation && (url[0] === '.' || url[0] === '/')) {768url = Uri.joinPath(settingsLocation, url).toString(false);769}770return url;771}772773function isThenable<T>(obj: ProviderResult<T>): obj is Thenable<T> {774return obj && (<any>obj)['then'];775}776777function updateMarkdownString(h: MarkdownString): MarkdownString {778const n = new MarkdownString(h.value, true);779n.isTrusted = h.isTrusted;780return n;781}782783function isSchemaResolveError(d: Diagnostic) {784return d.code === /* SchemaResolveError */ 0x300;785}786787788789790