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