Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/json-language-features/client/src/jsonClient.ts
5222 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
export type JSONLanguageStatus = { schemas: string[] };
7
8
import {
9
workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation,
10
Diagnostic, StatusBarAlignment, TextDocument, FormattingOptions, CancellationToken, FoldingRange,
11
ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n,
12
RelativePattern, CodeAction, CodeActionKind, CodeActionContext
13
} from 'vscode';
14
import {
15
LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind,
16
Diagnostic as LSPDiagnostic,
17
DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams,
18
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, BaseLanguageClient, ProvideFoldingRangeSignature, ProvideDocumentSymbolsSignature, ProvideDocumentColorsSignature
19
} from 'vscode-languageclient';
20
21
22
import { hash } from './utils/hash';
23
import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem, createSchemaLoadIssueItem, createSchemaLoadStatusItem } from './languageStatus';
24
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
25
import { matchesUrlPattern } from './utils/urlMatch';
26
27
namespace VSCodeContentRequest {
28
export const type: RequestType<string, string, any> = new RequestType('vscode/content');
29
}
30
31
namespace SchemaContentChangeNotification {
32
export const type: NotificationType<string | string[]> = new NotificationType('json/schemaContent');
33
}
34
35
namespace ForceValidateRequest {
36
export const type: RequestType<string, Diagnostic[], any> = new RequestType('json/validate');
37
}
38
39
namespace LanguageStatusRequest {
40
export const type: RequestType<string, JSONLanguageStatus, any> = new RequestType('json/languageStatus');
41
}
42
43
namespace ValidateContentRequest {
44
export const type: RequestType<{ schemaUri: string; content: string }, LSPDiagnostic[], any> = new RequestType('json/validateContent');
45
}
46
47
interface SortOptions extends LSPFormattingOptions {
48
}
49
50
interface DocumentSortingParams {
51
/**
52
* The uri of the document to sort.
53
*/
54
readonly uri: string;
55
/**
56
* The sort options
57
*/
58
readonly options: SortOptions;
59
}
60
61
namespace DocumentSortingRequest {
62
export interface ITextEdit {
63
range: {
64
start: { line: number; character: number };
65
end: { line: number; character: number };
66
};
67
newText: string;
68
}
69
export const type: RequestType<DocumentSortingParams, ITextEdit[], any> = new RequestType('json/sort');
70
}
71
72
export interface ISchemaAssociations {
73
[pattern: string]: string[];
74
}
75
76
export interface ISchemaAssociation {
77
fileMatch: string[];
78
uri: string;
79
}
80
81
namespace SchemaAssociationNotification {
82
export const type: NotificationType<ISchemaAssociations | ISchemaAssociation[]> = new NotificationType('json/schemaAssociations');
83
}
84
85
type Settings = {
86
json?: {
87
schemas?: JSONSchemaSettings[];
88
format?: { enable?: boolean };
89
keepLines?: { enable?: boolean };
90
validate?: { enable?: boolean };
91
resultLimit?: number;
92
jsonFoldingLimit?: number;
93
jsoncFoldingLimit?: number;
94
jsonColorDecoratorLimit?: number;
95
jsoncColorDecoratorLimit?: number;
96
};
97
http?: {
98
proxy?: string;
99
proxyStrictSSL?: boolean;
100
};
101
};
102
103
export type JSONSchemaSettings = {
104
fileMatch?: string[];
105
url?: string;
106
schema?: any;
107
folderUri?: string;
108
};
109
110
export namespace SettingIds {
111
export const enableFormatter = 'json.format.enable';
112
export const enableKeepLines = 'json.format.keepLines';
113
export const enableValidation = 'json.validate.enable';
114
export const enableSchemaDownload = 'json.schemaDownload.enable';
115
export const trustedDomains = 'json.schemaDownload.trustedDomains';
116
export const maxItemsComputed = 'json.maxItemsComputed';
117
export const editorFoldingMaximumRegions = 'editor.foldingMaximumRegions';
118
export const editorColorDecoratorsLimit = 'editor.colorDecoratorsLimit';
119
120
export const editorSection = 'editor';
121
export const foldingMaximumRegions = 'foldingMaximumRegions';
122
export const colorDecoratorsLimit = 'colorDecoratorsLimit';
123
}
124
125
export namespace CommandIds {
126
export const workbenchActionOpenSettings = 'workbench.action.openSettings';
127
export const workbenchTrustManage = 'workbench.trust.manage';
128
export const retryResolveSchemaCommandId = '_json.retryResolveSchema';
129
export const configureTrustedDomainsCommandId = '_json.configureTrustedDomains';
130
export const showAssociatedSchemaList = '_json.showAssociatedSchemaList';
131
export const clearCacheCommandId = 'json.clearCache';
132
export const validateCommandId = 'json.validate';
133
export const sortCommandId = 'json.sort';
134
}
135
136
export interface TelemetryReporter {
137
sendTelemetryEvent(eventName: string, properties?: {
138
[key: string]: string;
139
}, measurements?: {
140
[key: string]: number;
141
}): void;
142
}
143
144
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
145
146
export interface Runtime {
147
schemaRequests: SchemaRequestService;
148
telemetry?: TelemetryReporter;
149
readonly timer: {
150
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable;
151
};
152
logOutputChannel: LogOutputChannel;
153
}
154
155
export interface SchemaRequestService {
156
getContent(uri: string): Promise<string>;
157
clearCache?(): Promise<string[]>;
158
}
159
160
export enum SchemaRequestServiceErrors {
161
UntrustedWorkspaceError = 1,
162
UntrustedSchemaError = 2,
163
OpenTextDocumentAccessError = 3,
164
HTTPDisabledError = 4,
165
HTTPError = 5,
166
VSCodeAccessError = 6,
167
UntitledAccessError = 7,
168
}
169
170
export const languageServerDescription = l10n.t('JSON Language Server');
171
172
let resultLimit = 5000;
173
let jsonFoldingLimit = 5000;
174
let jsoncFoldingLimit = 5000;
175
let jsonColorDecoratorLimit = 5000;
176
let jsoncColorDecoratorLimit = 5000;
177
178
export interface AsyncDisposable {
179
dispose(): Promise<void>;
180
}
181
182
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
183
const languageParticipants = getLanguageParticipants();
184
context.subscriptions.push(languageParticipants);
185
186
let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);
187
188
let restartTrigger: Disposable | undefined;
189
languageParticipants.onDidChange(() => {
190
if (restartTrigger) {
191
restartTrigger.dispose();
192
}
193
restartTrigger = runtime.timer.setTimeout(async () => {
194
if (client) {
195
runtime.logOutputChannel.info('Extensions have changed, restarting JSON server...');
196
runtime.logOutputChannel.info('');
197
const oldClient = client;
198
client = undefined;
199
await oldClient.dispose();
200
client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime);
201
}
202
}, 2000);
203
});
204
205
return {
206
dispose: async () => {
207
restartTrigger?.dispose();
208
await client?.dispose();
209
}
210
};
211
}
212
213
async function startClientWithParticipants(_context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
214
215
const toDispose: Disposable[] = [];
216
217
let rangeFormatting: Disposable | undefined = undefined;
218
let settingsCache: Settings | undefined = undefined;
219
let schemaAssociationsCache: Promise<ISchemaAssociation[]> | undefined = undefined;
220
221
const documentSelector = languageParticipants.documentSelector;
222
223
const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0);
224
schemaResolutionErrorStatusBarItem.name = l10n.t('JSON: Schema Resolution Error');
225
schemaResolutionErrorStatusBarItem.text = '$(alert)';
226
toDispose.push(schemaResolutionErrorStatusBarItem);
227
228
const fileSchemaErrors = new Map<string, string>();
229
let schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload);
230
let trustedDomains = workspace.getConfiguration().get<Record<string, boolean>>(SettingIds.trustedDomains, {});
231
232
let isClientReady = false;
233
234
const documentSymbolsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentSymbolsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit));
235
toDispose.push(documentSymbolsLimitStatusbarItem);
236
237
const schemaLoadStatusItem = createSchemaLoadStatusItem((diagnostic: Diagnostic) => createSchemaLoadIssueItem(documentSelector, schemaDownloadEnabled, diagnostic));
238
toDispose.push(schemaLoadStatusItem);
239
240
toDispose.push(commands.registerCommand(CommandIds.clearCacheCommandId, async () => {
241
if (isClientReady && runtime.schemaRequests.clearCache) {
242
const cachedSchemas = await runtime.schemaRequests.clearCache();
243
await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas);
244
}
245
window.showInformationMessage(l10n.t('JSON schema cache cleared.'));
246
}));
247
248
toDispose.push(commands.registerCommand(CommandIds.validateCommandId, async (schemaUri: Uri, content: string) => {
249
const diagnostics: LSPDiagnostic[] = await client.sendRequest(ValidateContentRequest.type, { schemaUri: schemaUri.toString(), content });
250
return diagnostics.map(client.protocol2CodeConverter.asDiagnostic);
251
}));
252
253
toDispose.push(commands.registerCommand(CommandIds.sortCommandId, async () => {
254
255
if (isClientReady) {
256
const textEditor = window.activeTextEditor;
257
if (textEditor) {
258
const documentOptions = textEditor.options;
259
const textEdits = await getSortTextEdits(textEditor.document, documentOptions.tabSize, documentOptions.insertSpaces);
260
const success = await textEditor.edit(mutator => {
261
for (const edit of textEdits) {
262
mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText);
263
}
264
});
265
if (!success) {
266
window.showErrorMessage(l10n.t('Failed to sort the JSONC document, please consider opening an issue.'));
267
}
268
}
269
}
270
}));
271
272
function handleSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] {
273
schemaLoadStatusItem.update(uri, diagnostics);
274
if (!schemaDownloadEnabled) {
275
return diagnostics.filter(d => !isSchemaResolveError(d));
276
}
277
return diagnostics;
278
}
279
280
// Options to control the language client
281
const clientOptions: LanguageClientOptions = {
282
// Register the server for json documents
283
documentSelector,
284
initializationOptions: {
285
handledSchemaProtocols: ['file'], // language server only loads file-URI. Fetching schemas with other protocols ('http'...) are made on the client.
286
provideFormatter: false, // tell the server to not provide formatting capability and ignore the `json.format.enable` setting.
287
customCapabilities: { rangeFormatting: { editLimit: 10000 } }
288
},
289
synchronize: {
290
// Synchronize the setting section 'json' to the server
291
configurationSection: ['json', 'http'],
292
fileEvents: workspace.createFileSystemWatcher('**/*.json')
293
},
294
middleware: {
295
workspace: {
296
didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) })
297
},
298
provideDiagnostics: async (uriOrDoc, previousResolutId, token, next) => {
299
const diagnostics = await next(uriOrDoc, previousResolutId, token);
300
if (diagnostics && diagnostics.kind === DocumentDiagnosticReportKind.Full) {
301
const uri = uriOrDoc instanceof Uri ? uriOrDoc : uriOrDoc.uri;
302
diagnostics.items = handleSchemaErrorDiagnostics(uri, diagnostics.items);
303
}
304
return diagnostics;
305
},
306
handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => {
307
diagnostics = handleSchemaErrorDiagnostics(uri, diagnostics);
308
next(uri, diagnostics);
309
},
310
// testing the replace / insert mode
311
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
312
function update(item: CompletionItem) {
313
const range = item.range;
314
if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {
315
item.range = { inserting: new Range(range.start, position), replacing: range };
316
}
317
if (item.documentation instanceof MarkdownString) {
318
item.documentation = updateMarkdownString(item.documentation);
319
}
320
321
}
322
function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {
323
if (r) {
324
(Array.isArray(r) ? r : r.items).forEach(update);
325
}
326
return r;
327
}
328
329
const r = next(document, position, context, token);
330
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
331
return r.then(updateProposals);
332
}
333
return updateProposals(r);
334
},
335
provideHover(document: TextDocument, position: Position, token: CancellationToken, next: ProvideHoverSignature) {
336
function updateHover(r: Hover | null | undefined): Hover | null | undefined {
337
if (r && Array.isArray(r.contents)) {
338
r.contents = r.contents.map(h => h instanceof MarkdownString ? updateMarkdownString(h) : h);
339
}
340
return r;
341
}
342
const r = next(document, position, token);
343
if (isThenable<Hover | null | undefined>(r)) {
344
return r.then(updateHover);
345
}
346
return updateHover(r);
347
},
348
provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken, next: ProvideFoldingRangeSignature) {
349
const r = next(document, context, token);
350
if (isThenable<FoldingRange[] | null | undefined>(r)) {
351
return r;
352
}
353
return r;
354
},
355
provideDocumentColors(document: TextDocument, token: CancellationToken, next: ProvideDocumentColorsSignature) {
356
const r = next(document, token);
357
if (isThenable<ColorInformation[] | null | undefined>(r)) {
358
return r;
359
}
360
return r;
361
},
362
provideDocumentSymbols(document: TextDocument, token: CancellationToken, next: ProvideDocumentSymbolsSignature) {
363
type T = SymbolInformation[] | DocumentSymbol[];
364
function countDocumentSymbols(symbols: DocumentSymbol[]): number {
365
return symbols.reduce((previousValue, s) => previousValue + 1 + countDocumentSymbols(s.children), 0);
366
}
367
function isDocumentSymbol(r: T): r is DocumentSymbol[] {
368
return r[0] instanceof DocumentSymbol;
369
}
370
function checkLimit(r: T | null | undefined): T | null | undefined {
371
if (Array.isArray(r) && (isDocumentSymbol(r) ? countDocumentSymbols(r) : r.length) > resultLimit) {
372
documentSymbolsLimitStatusbarItem.update(document, resultLimit);
373
} else {
374
documentSymbolsLimitStatusbarItem.update(document, false);
375
}
376
return r;
377
}
378
const r = next(document, token);
379
if (isThenable<T | undefined | null>(r)) {
380
return r.then(checkLimit);
381
}
382
return checkLimit(r);
383
}
384
}
385
};
386
387
clientOptions.outputChannel = runtime.logOutputChannel;
388
// Create the language client and start the client.
389
const client = newLanguageClient('json', languageServerDescription, clientOptions);
390
client.registerProposedFeatures();
391
392
const schemaDocuments: { [uri: string]: boolean } = {};
393
394
// handle content request
395
client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => {
396
const uri = Uri.parse(uriPath);
397
const uriString = uri.toString(true);
398
if (uri.scheme === 'untitled') {
399
throw new ResponseError(SchemaRequestServiceErrors.UntitledAccessError, l10n.t('Unable to load {0}', uriString));
400
}
401
if (uri.scheme === 'vscode') {
402
try {
403
runtime.logOutputChannel.info('read schema from vscode: ' + uriString);
404
ensureFilesystemWatcherInstalled(uri);
405
const content = await workspace.fs.readFile(uri);
406
return new TextDecoder().decode(content);
407
} catch (e) {
408
throw new ResponseError(SchemaRequestServiceErrors.VSCodeAccessError, e.toString(), e);
409
}
410
} else if (uri.scheme !== 'http' && uri.scheme !== 'https') {
411
try {
412
const document = await workspace.openTextDocument(uri);
413
schemaDocuments[uriString] = true;
414
return document.getText();
415
} catch (e) {
416
throw new ResponseError(SchemaRequestServiceErrors.OpenTextDocumentAccessError, e.toString(), e);
417
}
418
} else if (schemaDownloadEnabled) {
419
if (!workspace.isTrusted) {
420
throw new ResponseError(SchemaRequestServiceErrors.UntrustedWorkspaceError, l10n.t('Downloading schemas is disabled in untrusted workspaces'));
421
}
422
if (!await isTrusted(uri)) {
423
throw new ResponseError(SchemaRequestServiceErrors.UntrustedSchemaError, l10n.t('Location {0} is untrusted', uriString));
424
}
425
if (runtime.telemetry && uri.authority === 'schema.management.azure.com') {
426
/* __GDPR__
427
"json.schema" : {
428
"owner": "aeschli",
429
"comment": "Measure the use of the Azure resource manager schemas",
430
"schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The azure schema URL that was requested." }
431
}
432
*/
433
runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriString });
434
}
435
try {
436
return await runtime.schemaRequests.getContent(uriString);
437
} catch (e) {
438
throw new ResponseError(SchemaRequestServiceErrors.HTTPError, e.toString(), e);
439
}
440
} else {
441
throw new ResponseError(SchemaRequestServiceErrors.HTTPDisabledError, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload));
442
}
443
});
444
445
await client.start();
446
447
isClientReady = true;
448
449
const handleContentChange = (uriString: string) => {
450
if (schemaDocuments[uriString]) {
451
client.sendNotification(SchemaContentChangeNotification.type, uriString);
452
return true;
453
}
454
return false;
455
};
456
const handleContentClosed = (uriString: string) => {
457
if (handleContentChange(uriString)) {
458
delete schemaDocuments[uriString];
459
}
460
fileSchemaErrors.delete(uriString);
461
};
462
463
const watchers: Map<string, Disposable> = new Map();
464
toDispose.push(new Disposable(() => {
465
for (const d of watchers.values()) {
466
d.dispose();
467
}
468
}));
469
470
471
const ensureFilesystemWatcherInstalled = (uri: Uri) => {
472
473
const uriString = uri.toString();
474
if (!watchers.has(uriString)) {
475
try {
476
const watcher = workspace.createFileSystemWatcher(new RelativePattern(uri, '*'));
477
const handleChange = (uri: Uri) => {
478
runtime.logOutputChannel.info('schema change detected ' + uri.toString());
479
client.sendNotification(SchemaContentChangeNotification.type, uriString);
480
};
481
const createListener = watcher.onDidCreate(handleChange);
482
const changeListener = watcher.onDidChange(handleChange);
483
const deleteListener = watcher.onDidDelete(() => {
484
const watcher = watchers.get(uriString);
485
if (watcher) {
486
watcher.dispose();
487
watchers.delete(uriString);
488
}
489
});
490
watchers.set(uriString, Disposable.from(watcher, createListener, changeListener, deleteListener));
491
} catch {
492
runtime.logOutputChannel.info('Problem installing a file system watcher for ' + uriString);
493
}
494
}
495
};
496
497
toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString())));
498
toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString())));
499
500
toDispose.push(commands.registerCommand(CommandIds.retryResolveSchemaCommandId, triggerValidation));
501
502
toDispose.push(commands.registerCommand(CommandIds.configureTrustedDomainsCommandId, configureTrustedDomains));
503
504
toDispose.push(languages.registerCodeActionsProvider(documentSelector, {
505
provideCodeActions(_document: TextDocument, _range: Range, context: CodeActionContext): CodeAction[] {
506
const codeActions: CodeAction[] = [];
507
508
for (const diagnostic of context.diagnostics) {
509
if (typeof diagnostic.code !== 'number') {
510
continue;
511
}
512
switch (diagnostic.code) {
513
case ErrorCodes.UntrustedSchemaError: {
514
const title = l10n.t('Configure Trusted Domains...');
515
const action = new CodeAction(title, CodeActionKind.QuickFix);
516
const schemaUri = diagnostic.relatedInformation?.[0]?.location.uri;
517
if (schemaUri) {
518
action.command = { command: CommandIds.configureTrustedDomainsCommandId, arguments: [schemaUri.toString()], title };
519
} else {
520
action.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.trustedDomains], title };
521
}
522
action.diagnostics = [diagnostic];
523
action.isPreferred = true;
524
codeActions.push(action);
525
}
526
break;
527
case ErrorCodes.HTTPDisabledError: {
528
const title = l10n.t('Enable Schema Downloading...');
529
const action = new CodeAction(title, CodeActionKind.QuickFix);
530
action.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.enableSchemaDownload], title };
531
action.diagnostics = [diagnostic];
532
action.isPreferred = true;
533
codeActions.push(action);
534
}
535
break;
536
}
537
}
538
539
return codeActions;
540
}
541
}, {
542
providedCodeActionKinds: [CodeActionKind.QuickFix]
543
}));
544
545
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(false));
546
547
toDispose.push(extensions.onDidChange(async _ => {
548
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true));
549
}));
550
551
const associationWatcher = workspace.createFileSystemWatcher(new RelativePattern(Uri.parse(`vscode://schemas-associations/`), '**/schemas-associations.json'));
552
toDispose.push(associationWatcher);
553
toDispose.push(associationWatcher.onDidChange(async _e => {
554
client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true));
555
}));
556
557
// manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652.
558
updateFormatterRegistration();
559
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
560
561
toDispose.push(workspace.onDidChangeConfiguration(e => {
562
if (e.affectsConfiguration(SettingIds.enableFormatter)) {
563
updateFormatterRegistration();
564
} else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) {
565
schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload);
566
triggerValidation();
567
} else if (e.affectsConfiguration(SettingIds.editorFoldingMaximumRegions) || e.affectsConfiguration(SettingIds.editorColorDecoratorsLimit)) {
568
client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) });
569
} else if (e.affectsConfiguration(SettingIds.trustedDomains)) {
570
trustedDomains = workspace.getConfiguration().get<Record<string, boolean>>(SettingIds.trustedDomains, {});
571
triggerValidation();
572
}
573
}));
574
toDispose.push(workspace.onDidGrantWorkspaceTrust(() => triggerValidation()));
575
576
toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri)));
577
578
function updateFormatterRegistration() {
579
const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter);
580
if (!formatEnabled && rangeFormatting) {
581
rangeFormatting.dispose();
582
rangeFormatting = undefined;
583
} else if (formatEnabled && !rangeFormatting) {
584
rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {
585
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
586
const filesConfig = workspace.getConfiguration('files', document);
587
const fileFormattingOptions = {
588
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
589
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
590
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
591
};
592
const params: DocumentRangeFormattingParams = {
593
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
594
range: client.code2ProtocolConverter.asRange(range),
595
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
596
};
597
598
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
599
client.protocol2CodeConverter.asTextEdits,
600
(error) => {
601
client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);
602
return Promise.resolve([]);
603
}
604
);
605
}
606
});
607
}
608
}
609
610
async function triggerValidation() {
611
const activeTextEditor = window.activeTextEditor;
612
if (activeTextEditor && languageParticipants.hasLanguage(activeTextEditor.document.languageId)) {
613
schemaResolutionErrorStatusBarItem.text = '$(watch)';
614
schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Validating...');
615
const activeDocUri = activeTextEditor.document.uri.toString();
616
await client.sendRequest(ForceValidateRequest.type, activeDocUri);
617
}
618
}
619
620
async function getSortTextEdits(document: TextDocument, tabSize: string | number = 4, insertSpaces: string | boolean = true): Promise<TextEdit[]> {
621
const filesConfig = workspace.getConfiguration('files', document);
622
const options: SortOptions = {
623
tabSize: Number(tabSize),
624
insertSpaces: Boolean(insertSpaces),
625
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
626
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
627
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
628
};
629
const params: DocumentSortingParams = {
630
uri: document.uri.toString(),
631
options
632
};
633
const edits = await client.sendRequest(DocumentSortingRequest.type, params);
634
// Here we convert the JSON objects to real TextEdit objects
635
return edits.map((edit) => {
636
return new TextEdit(
637
new Range(edit.range.start.line, edit.range.start.character, edit.range.end.line, edit.range.end.character),
638
edit.newText
639
);
640
});
641
}
642
643
function getSettings(forceRefresh: boolean): Settings {
644
if (!settingsCache || forceRefresh) {
645
settingsCache = computeSettings();
646
}
647
return settingsCache;
648
}
649
650
async function getSchemaAssociations(forceRefresh: boolean): Promise<ISchemaAssociation[]> {
651
if (!schemaAssociationsCache || forceRefresh) {
652
schemaAssociationsCache = computeSchemaAssociations();
653
}
654
return schemaAssociationsCache;
655
}
656
657
async function isTrusted(uri: Uri): Promise<boolean> {
658
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
659
return true;
660
}
661
const uriString = uri.toString(true);
662
663
// Check against trustedDomains setting
664
if (matchesUrlPattern(uri, trustedDomains)) {
665
return true;
666
}
667
668
const knownAssociations = await getSchemaAssociations(false);
669
for (const association of knownAssociations) {
670
if (association.uri === uriString) {
671
return true;
672
}
673
}
674
const settingsCache = getSettings(false);
675
if (settingsCache.json && settingsCache.json.schemas) {
676
for (const schemaSetting of settingsCache.json.schemas) {
677
const schemaUri = schemaSetting.url;
678
if (schemaUri === uriString) {
679
return true;
680
}
681
}
682
}
683
return false;
684
}
685
686
async function configureTrustedDomains(schemaUri: string): Promise<void> {
687
interface QuickPickItemWithAction {
688
label: string;
689
description?: string;
690
execute: () => Promise<void>;
691
}
692
693
const items: QuickPickItemWithAction[] = [];
694
695
try {
696
const uri = Uri.parse(schemaUri);
697
const domain = `${uri.scheme}://${uri.authority}`;
698
699
// Add "Trust domain" option
700
items.push({
701
label: l10n.t('Trust Domain: {0}', domain),
702
description: l10n.t('Allow all schemas from this domain'),
703
execute: async () => {
704
const config = workspace.getConfiguration();
705
const currentDomains = config.get<Record<string, boolean>>(SettingIds.trustedDomains, {});
706
currentDomains[domain] = true;
707
await config.update(SettingIds.trustedDomains, currentDomains, true);
708
await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);
709
}
710
});
711
712
// Add "Trust URI" option
713
items.push({
714
label: l10n.t('Trust URI: {0}', schemaUri),
715
description: l10n.t('Allow only this specific schema'),
716
execute: async () => {
717
const config = workspace.getConfiguration();
718
const currentDomains = config.get<Record<string, boolean>>(SettingIds.trustedDomains, {});
719
currentDomains[schemaUri] = true;
720
await config.update(SettingIds.trustedDomains, currentDomains, true);
721
await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);
722
}
723
});
724
} catch (e) {
725
runtime.logOutputChannel.error(`Failed to parse schema URI: ${schemaUri}`);
726
}
727
728
729
// Always add "Configure setting" option
730
items.push({
731
label: l10n.t('Configure Setting'),
732
description: l10n.t('Open settings editor'),
733
execute: async () => {
734
await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains);
735
}
736
});
737
738
const selected = await window.showQuickPick(items, {
739
placeHolder: l10n.t('Select how to configure trusted schema domains')
740
});
741
742
if (selected) {
743
await selected.execute();
744
}
745
}
746
747
748
return {
749
dispose: async () => {
750
await client.stop();
751
toDispose.forEach(d => d.dispose());
752
rangeFormatting?.dispose();
753
}
754
};
755
}
756
757
async function computeSchemaAssociations(): Promise<ISchemaAssociation[]> {
758
const extensionAssociations = getSchemaExtensionAssociations();
759
return extensionAssociations.concat(await getDynamicSchemaAssociations());
760
}
761
762
function getSchemaExtensionAssociations(): ISchemaAssociation[] {
763
const associations: ISchemaAssociation[] = [];
764
extensions.allAcrossExtensionHosts.forEach(extension => {
765
const packageJSON = extension.packageJSON;
766
if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation) {
767
const jsonValidation = packageJSON.contributes.jsonValidation;
768
if (Array.isArray(jsonValidation)) {
769
jsonValidation.forEach(jv => {
770
let { fileMatch, url } = jv;
771
if (typeof fileMatch === 'string') {
772
fileMatch = [fileMatch];
773
}
774
if (Array.isArray(fileMatch) && typeof url === 'string') {
775
let uri: string = url;
776
if (uri[0] === '.' && uri[1] === '/') {
777
uri = Uri.joinPath(extension.extensionUri, uri).toString();
778
}
779
fileMatch = fileMatch.map(fm => {
780
if (fm[0] === '%') {
781
fm = fm.replace(/%APP_SETTINGS_HOME%/, '/User');
782
fm = fm.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine');
783
fm = fm.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces');
784
} else if (!fm.match(/^(\w+:\/\/|\/|!)/)) {
785
fm = '/' + fm;
786
}
787
return fm;
788
});
789
associations.push({ fileMatch, uri });
790
}
791
});
792
}
793
}
794
});
795
return associations;
796
}
797
798
async function getDynamicSchemaAssociations(): Promise<ISchemaAssociation[]> {
799
const result: ISchemaAssociation[] = [];
800
try {
801
const data = await workspace.fs.readFile(Uri.parse(`vscode://schemas-associations/schemas-associations.json`));
802
const rawStr = new TextDecoder().decode(data);
803
const obj = <Record<string, string[]>>JSON.parse(rawStr);
804
for (const item of Object.keys(obj)) {
805
result.push({
806
fileMatch: obj[item],
807
uri: item
808
});
809
}
810
} catch {
811
// ignore
812
}
813
return result;
814
}
815
816
817
818
function computeSettings(): Settings {
819
const configuration = workspace.getConfiguration();
820
const httpSettings = workspace.getConfiguration('http');
821
822
const normalizeLimit = (settingValue: any) => Math.trunc(Math.max(0, Number(settingValue))) || 5000;
823
824
resultLimit = normalizeLimit(workspace.getConfiguration().get(SettingIds.maxItemsComputed));
825
const editorJSONSettings = workspace.getConfiguration(SettingIds.editorSection, { languageId: 'json' });
826
const editorJSONCSettings = workspace.getConfiguration(SettingIds.editorSection, { languageId: 'jsonc' });
827
828
jsonFoldingLimit = normalizeLimit(editorJSONSettings.get(SettingIds.foldingMaximumRegions));
829
jsoncFoldingLimit = normalizeLimit(editorJSONCSettings.get(SettingIds.foldingMaximumRegions));
830
jsonColorDecoratorLimit = normalizeLimit(editorJSONSettings.get(SettingIds.colorDecoratorsLimit));
831
jsoncColorDecoratorLimit = normalizeLimit(editorJSONCSettings.get(SettingIds.colorDecoratorsLimit));
832
833
const schemas: JSONSchemaSettings[] = [];
834
835
const settings: Settings = {
836
http: {
837
proxy: httpSettings.get('proxy'),
838
proxyStrictSSL: httpSettings.get('proxyStrictSSL')
839
},
840
json: {
841
validate: { enable: configuration.get(SettingIds.enableValidation) },
842
format: { enable: configuration.get(SettingIds.enableFormatter) },
843
keepLines: { enable: configuration.get(SettingIds.enableKeepLines) },
844
schemas,
845
resultLimit: resultLimit + 1, // ask for one more so we can detect if the limit has been exceeded
846
jsonFoldingLimit: jsonFoldingLimit + 1,
847
jsoncFoldingLimit: jsoncFoldingLimit + 1,
848
jsonColorDecoratorLimit: jsonColorDecoratorLimit + 1,
849
jsoncColorDecoratorLimit: jsoncColorDecoratorLimit + 1
850
}
851
};
852
853
/*
854
* Add schemas from the settings
855
* folderUri to which folder the setting is scoped to. `undefined` means global (also external files)
856
* settingsLocation against which path relative schema URLs are resolved
857
*/
858
const collectSchemaSettings = (schemaSettings: JSONSchemaSettings[] | undefined, folderUri: string | undefined, settingsLocation: Uri | undefined) => {
859
if (schemaSettings) {
860
for (const setting of schemaSettings) {
861
const url = getSchemaId(setting, settingsLocation);
862
if (url) {
863
const schemaSetting: JSONSchemaSettings = { url, fileMatch: setting.fileMatch, folderUri, schema: setting.schema };
864
schemas.push(schemaSetting);
865
}
866
}
867
}
868
};
869
870
const folders = workspace.workspaceFolders ?? [];
871
872
const schemaConfigInfo = workspace.getConfiguration('json', null).inspect<JSONSchemaSettings[]>('schemas');
873
if (schemaConfigInfo) {
874
// settings in user config
875
collectSchemaSettings(schemaConfigInfo.globalValue, undefined, undefined);
876
if (workspace.workspaceFile) {
877
if (schemaConfigInfo.workspaceValue) {
878
const settingsLocation = Uri.joinPath(workspace.workspaceFile, '..');
879
// settings in the workspace configuration file apply to all files (also external files)
880
collectSchemaSettings(schemaConfigInfo.workspaceValue, undefined, settingsLocation);
881
}
882
for (const folder of folders) {
883
const folderUri = folder.uri;
884
const folderSchemaConfigInfo = workspace.getConfiguration('json', folderUri).inspect<JSONSchemaSettings[]>('schemas');
885
collectSchemaSettings(folderSchemaConfigInfo?.workspaceFolderValue, folderUri.toString(false), folderUri);
886
}
887
} else {
888
if (schemaConfigInfo.workspaceValue && folders.length === 1) {
889
// single folder workspace: settings apply to all files (also external files)
890
collectSchemaSettings(schemaConfigInfo.workspaceValue, undefined, folders[0].uri);
891
}
892
}
893
}
894
return settings;
895
}
896
897
function getSchemaId(schema: JSONSchemaSettings, settingsLocation?: Uri): string | undefined {
898
let url = schema.url;
899
if (!url) {
900
if (schema.schema) {
901
url = schema.schema.id || `vscode://schemas/custom/${encodeURIComponent(hash(schema.schema).toString(16))}`;
902
}
903
} else if (settingsLocation && (url[0] === '.' || url[0] === '/')) {
904
url = Uri.joinPath(settingsLocation, url).toString(false);
905
}
906
return url;
907
}
908
909
function isThenable<T>(obj: unknown): obj is Thenable<T> {
910
return !!obj && typeof (obj as unknown as Thenable<T>).then === 'function';
911
}
912
913
function updateMarkdownString(h: MarkdownString): MarkdownString {
914
const n = new MarkdownString(h.value, true);
915
n.isTrusted = h.isTrusted;
916
return n;
917
}
918
919
export namespace ErrorCodes {
920
export const SchemaResolveError = 0x10000;
921
export const UntrustedSchemaError = SchemaResolveError + SchemaRequestServiceErrors.UntrustedSchemaError;
922
export const HTTPDisabledError = SchemaResolveError + SchemaRequestServiceErrors.HTTPDisabledError;
923
}
924
925
export function isSchemaResolveError(d: Diagnostic) {
926
return typeof d.code === 'number' && d.code >= ErrorCodes.SchemaResolveError;
927
}
928
929
930
931