Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/configuration/common/configurationEditing.ts
3296 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
import * as nls from '../../../../nls.js';
7
import { URI } from '../../../../base/common/uri.js';
8
import * as json from '../../../../base/common/json.js';
9
import { setProperty } from '../../../../base/common/jsonEdit.js';
10
import { Queue } from '../../../../base/common/async.js';
11
import { Edit, FormattingOptions } from '../../../../base/common/jsonFormatter.js';
12
import { Registry } from '../../../../platform/registry/common/platform.js';
13
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
14
import { ITextFileService } from '../../textfile/common/textfiles.js';
15
import { IConfigurationUpdateOptions, IConfigurationUpdateOverrides } from '../../../../platform/configuration/common/configuration.js';
16
import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS, TASKS_DEFAULT, FOLDER_SCOPES, IWorkbenchConfigurationService, APPLICATION_SCOPES, MCP_CONFIGURATION_KEY } from './configuration.js';
17
import { FileOperationError, FileOperationResult, IFileService } from '../../../../platform/files/common/files.js';
18
import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';
19
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, keyFromOverrideIdentifiers, OVERRIDE_PROPERTY_REGEX } from '../../../../platform/configuration/common/configurationRegistry.js';
20
import { IEditorService } from '../../editor/common/editorService.js';
21
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
22
import { IOpenSettingsOptions, IPreferencesService } from '../../preferences/common/preferences.js';
23
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
24
import { ITextModel } from '../../../../editor/common/model.js';
25
import { IDisposable, IReference } from '../../../../base/common/lifecycle.js';
26
import { Range } from '../../../../editor/common/core/range.js';
27
import { EditOperation } from '../../../../editor/common/core/editOperation.js';
28
import { Selection } from '../../../../editor/common/core/selection.js';
29
import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';
30
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
31
import { ErrorNoTelemetry } from '../../../../base/common/errors.js';
32
import { IFilesConfigurationService } from '../../filesConfiguration/common/filesConfigurationService.js';
33
34
export const enum ConfigurationEditingErrorCode {
35
36
/**
37
* Error when trying to write a configuration key that is not registered.
38
*/
39
ERROR_UNKNOWN_KEY,
40
41
/**
42
* Error when trying to write an application setting into workspace settings.
43
*/
44
ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION,
45
46
/**
47
* Error when trying to write a machne setting into workspace settings.
48
*/
49
ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE,
50
51
/**
52
* Error when trying to write an invalid folder configuration key to folder settings.
53
*/
54
ERROR_INVALID_FOLDER_CONFIGURATION,
55
56
/**
57
* Error when trying to write to user target but not supported for provided key.
58
*/
59
ERROR_INVALID_USER_TARGET,
60
61
/**
62
* Error when trying to write to user target but not supported for provided key.
63
*/
64
ERROR_INVALID_WORKSPACE_TARGET,
65
66
/**
67
* Error when trying to write a configuration key to folder target
68
*/
69
ERROR_INVALID_FOLDER_TARGET,
70
71
/**
72
* Error when trying to write to language specific setting but not supported for preovided key
73
*/
74
ERROR_INVALID_RESOURCE_LANGUAGE_CONFIGURATION,
75
76
/**
77
* Error when trying to write to the workspace configuration without having a workspace opened.
78
*/
79
ERROR_NO_WORKSPACE_OPENED,
80
81
/**
82
* Error when trying to write and save to the configuration file while it is dirty in the editor.
83
*/
84
ERROR_CONFIGURATION_FILE_DIRTY,
85
86
/**
87
* Error when trying to write and save to the configuration file while it is not the latest in the disk.
88
*/
89
ERROR_CONFIGURATION_FILE_MODIFIED_SINCE,
90
91
/**
92
* Error when trying to write to a configuration file that contains JSON errors.
93
*/
94
ERROR_INVALID_CONFIGURATION,
95
96
/**
97
* Error when trying to write a policy configuration
98
*/
99
ERROR_POLICY_CONFIGURATION,
100
101
/**
102
* Internal Error.
103
*/
104
ERROR_INTERNAL
105
}
106
107
export class ConfigurationEditingError extends ErrorNoTelemetry {
108
constructor(message: string, public code: ConfigurationEditingErrorCode) {
109
super(message);
110
}
111
}
112
113
export interface IConfigurationValue {
114
key: string;
115
value: unknown;
116
}
117
118
export interface IConfigurationEditingOptions extends IConfigurationUpdateOptions {
119
/**
120
* Scope of configuration to be written into.
121
*/
122
scopes?: IConfigurationUpdateOverrides;
123
}
124
125
export const enum EditableConfigurationTarget {
126
USER_LOCAL = 1,
127
USER_REMOTE,
128
WORKSPACE,
129
WORKSPACE_FOLDER
130
}
131
132
interface IConfigurationEditOperation extends IConfigurationValue {
133
target: EditableConfigurationTarget;
134
jsonPath: json.JSONPath;
135
resource?: URI;
136
workspaceStandAloneConfigurationKey?: string;
137
}
138
139
export class ConfigurationEditing {
140
141
public _serviceBrand: undefined;
142
143
private queue: Queue<void>;
144
145
constructor(
146
private readonly remoteSettingsResource: URI | null,
147
@IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService,
148
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
149
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
150
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
151
@IFileService private readonly fileService: IFileService,
152
@ITextModelService private readonly textModelResolverService: ITextModelService,
153
@ITextFileService private readonly textFileService: ITextFileService,
154
@INotificationService private readonly notificationService: INotificationService,
155
@IPreferencesService private readonly preferencesService: IPreferencesService,
156
@IEditorService private readonly editorService: IEditorService,
157
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
158
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
159
) {
160
this.queue = new Queue<void>();
161
}
162
163
async writeConfiguration(target: EditableConfigurationTarget, value: IConfigurationValue, options: IConfigurationEditingOptions = {}): Promise<void> {
164
const operation = this.getConfigurationEditOperation(target, value, options.scopes || {});
165
// queue up writes to prevent race conditions
166
return this.queue.queue(async () => {
167
try {
168
await this.doWriteConfiguration(operation, options);
169
} catch (error) {
170
if (options.donotNotifyError) {
171
throw error;
172
}
173
await this.onError(error, operation, options.scopes);
174
}
175
});
176
}
177
178
private async doWriteConfiguration(operation: IConfigurationEditOperation, options: IConfigurationEditingOptions): Promise<void> {
179
await this.validate(operation.target, operation, !options.handleDirtyFile, options.scopes || {});
180
const resource: URI = operation.resource!;
181
const reference = await this.resolveModelReference(resource);
182
try {
183
const formattingOptions = this.getFormattingOptions(reference.object.textEditorModel);
184
await this.updateConfiguration(operation, reference.object.textEditorModel, formattingOptions, options);
185
} finally {
186
reference.dispose();
187
}
188
}
189
190
private async updateConfiguration(operation: IConfigurationEditOperation, model: ITextModel, formattingOptions: FormattingOptions, options: IConfigurationEditingOptions): Promise<void> {
191
if (this.hasParseErrors(model.getValue(), operation)) {
192
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION, operation.target, operation);
193
}
194
195
if (this.textFileService.isDirty(model.uri) && options.handleDirtyFile) {
196
switch (options.handleDirtyFile) {
197
case 'save': await this.save(model, operation); break;
198
case 'revert': await this.textFileService.revert(model.uri); break;
199
}
200
}
201
202
const edit = this.getEdits(operation, model.getValue(), formattingOptions)[0];
203
if (edit) {
204
let disposable: IDisposable | undefined;
205
try {
206
// Optimization: we apply edits to a text model and save it
207
// right after. Use the files config service to signal this
208
// to the workbench to optimise the UI during this operation.
209
// For example, avoids to briefly show dirty indicators.
210
disposable = this.filesConfigurationService.enableAutoSaveAfterShortDelay(model.uri);
211
if (this.applyEditsToBuffer(edit, model)) {
212
await this.save(model, operation);
213
}
214
} finally {
215
disposable?.dispose();
216
}
217
}
218
}
219
220
private async save(model: ITextModel, operation: IConfigurationEditOperation): Promise<void> {
221
try {
222
await this.textFileService.save(model.uri, { ignoreErrorHandler: true });
223
} catch (error) {
224
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
225
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_MODIFIED_SINCE, operation.target, operation);
226
}
227
throw new ConfigurationEditingError(nls.localize('fsError', "Error while writing to {0}. {1}", this.stringifyTarget(operation.target), error.message), ConfigurationEditingErrorCode.ERROR_INTERNAL);
228
}
229
}
230
231
private applyEditsToBuffer(edit: Edit, model: ITextModel): boolean {
232
const startPosition = model.getPositionAt(edit.offset);
233
const endPosition = model.getPositionAt(edit.offset + edit.length);
234
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
235
const currentText = model.getValueInRange(range);
236
if (edit.content !== currentText) {
237
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
238
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
239
return true;
240
}
241
return false;
242
}
243
244
private getEdits({ value, jsonPath }: IConfigurationEditOperation, modelContent: string, formattingOptions: FormattingOptions): Edit[] {
245
if (jsonPath.length) {
246
return setProperty(modelContent, jsonPath, value, formattingOptions);
247
}
248
249
// Without jsonPath, the entire configuration file is being replaced, so we just use JSON.stringify
250
const content = JSON.stringify(value, null, formattingOptions.insertSpaces && formattingOptions.tabSize ? ' '.repeat(formattingOptions.tabSize) : '\t');
251
return [{
252
content,
253
length: modelContent.length,
254
offset: 0
255
}];
256
}
257
258
private getFormattingOptions(model: ITextModel): FormattingOptions {
259
const { insertSpaces, tabSize } = model.getOptions();
260
const eol = model.getEOL();
261
return { insertSpaces, tabSize, eol };
262
}
263
264
private async onError(error: ConfigurationEditingError, operation: IConfigurationEditOperation, scopes: IConfigurationUpdateOverrides | undefined): Promise<void> {
265
switch (error.code) {
266
case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION:
267
this.onInvalidConfigurationError(error, operation);
268
break;
269
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY:
270
this.onConfigurationFileDirtyError(error, operation, scopes);
271
break;
272
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_MODIFIED_SINCE:
273
return this.doWriteConfiguration(operation, { scopes, handleDirtyFile: 'revert' });
274
default:
275
this.notificationService.error(error.message);
276
}
277
}
278
279
private onInvalidConfigurationError(error: ConfigurationEditingError, operation: IConfigurationEditOperation,): void {
280
const openStandAloneConfigurationActionLabel = operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY ? nls.localize('openTasksConfiguration', "Open Tasks Configuration")
281
: operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration")
282
: operation.workspaceStandAloneConfigurationKey === MCP_CONFIGURATION_KEY ? nls.localize('openMcpConfiguration', "Open MCP Configuration")
283
: null;
284
if (openStandAloneConfigurationActionLabel) {
285
this.notificationService.prompt(Severity.Error, error.message,
286
[{
287
label: openStandAloneConfigurationActionLabel,
288
run: () => this.openFile(operation.resource!)
289
}]
290
);
291
} else {
292
this.notificationService.prompt(Severity.Error, error.message,
293
[{
294
label: nls.localize('open', "Open Settings"),
295
run: () => this.openSettings(operation)
296
}]
297
);
298
}
299
}
300
301
private onConfigurationFileDirtyError(error: ConfigurationEditingError, operation: IConfigurationEditOperation, scopes: IConfigurationUpdateOverrides | undefined): void {
302
const openStandAloneConfigurationActionLabel = operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY ? nls.localize('openTasksConfiguration', "Open Tasks Configuration")
303
: operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration")
304
: null;
305
if (openStandAloneConfigurationActionLabel) {
306
this.notificationService.prompt(Severity.Error, error.message,
307
[{
308
label: nls.localize('saveAndRetry', "Save and Retry"),
309
run: () => {
310
const key = operation.key ? `${operation.workspaceStandAloneConfigurationKey}.${operation.key}` : operation.workspaceStandAloneConfigurationKey!;
311
this.writeConfiguration(operation.target, { key, value: operation.value }, { handleDirtyFile: 'save', scopes });
312
}
313
},
314
{
315
label: openStandAloneConfigurationActionLabel,
316
run: () => this.openFile(operation.resource!)
317
}]
318
);
319
} else {
320
this.notificationService.prompt(Severity.Error, error.message,
321
[{
322
label: nls.localize('saveAndRetry', "Save and Retry"),
323
run: () => this.writeConfiguration(operation.target, { key: operation.key, value: operation.value }, { handleDirtyFile: 'save', scopes })
324
},
325
{
326
label: nls.localize('open', "Open Settings"),
327
run: () => this.openSettings(operation)
328
}]
329
);
330
}
331
}
332
333
private openSettings(operation: IConfigurationEditOperation): void {
334
const options: IOpenSettingsOptions = { jsonEditor: true };
335
switch (operation.target) {
336
case EditableConfigurationTarget.USER_LOCAL:
337
this.preferencesService.openUserSettings(options);
338
break;
339
case EditableConfigurationTarget.USER_REMOTE:
340
this.preferencesService.openRemoteSettings(options);
341
break;
342
case EditableConfigurationTarget.WORKSPACE:
343
this.preferencesService.openWorkspaceSettings(options);
344
break;
345
case EditableConfigurationTarget.WORKSPACE_FOLDER:
346
if (operation.resource) {
347
const workspaceFolder = this.contextService.getWorkspaceFolder(operation.resource);
348
if (workspaceFolder) {
349
this.preferencesService.openFolderSettings({ folderUri: workspaceFolder.uri, jsonEditor: true });
350
}
351
}
352
break;
353
}
354
}
355
356
private openFile(resource: URI): void {
357
this.editorService.openEditor({ resource, options: { pinned: true } });
358
}
359
360
private toConfigurationEditingError(code: ConfigurationEditingErrorCode, target: EditableConfigurationTarget, operation: IConfigurationEditOperation): ConfigurationEditingError {
361
const message = this.toErrorMessage(code, target, operation);
362
return new ConfigurationEditingError(message, code);
363
}
364
365
private toErrorMessage(error: ConfigurationEditingErrorCode, target: EditableConfigurationTarget, operation: IConfigurationEditOperation): string {
366
switch (error) {
367
368
// API constraints
369
case ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION: return nls.localize('errorPolicyConfiguration', "Unable to write {0} because it is configured in system policy.", operation.key);
370
case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", this.stringifyTarget(target), operation.key);
371
case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION: return nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key);
372
case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE: return nls.localize('errorInvalidWorkspaceConfigurationMachine', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key);
373
case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION: return nls.localize('errorInvalidFolderConfiguration', "Unable to write to Folder Settings because {0} does not support the folder resource scope.", operation.key);
374
case ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET: return nls.localize('errorInvalidUserTarget', "Unable to write to User Settings because {0} does not support for global scope.", operation.key);
375
case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET: return nls.localize('errorInvalidWorkspaceTarget', "Unable to write to Workspace Settings because {0} does not support for workspace scope in a multi folder workspace.", operation.key);
376
case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET: return nls.localize('errorInvalidFolderTarget', "Unable to write to Folder Settings because no resource is provided.");
377
case ConfigurationEditingErrorCode.ERROR_INVALID_RESOURCE_LANGUAGE_CONFIGURATION: return nls.localize('errorInvalidResourceLanguageConfiguration', "Unable to write to Language Settings because {0} is not a resource language setting.", operation.key);
378
case ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED: return nls.localize('errorNoWorkspaceOpened', "Unable to write to {0} because no workspace is opened. Please open a workspace first and try again.", this.stringifyTarget(target));
379
380
// User issues
381
case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION: {
382
if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) {
383
return nls.localize('errorInvalidTaskConfiguration', "Unable to write into the tasks configuration file. Please open it to correct errors/warnings in it and try again.");
384
}
385
if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) {
386
return nls.localize('errorInvalidLaunchConfiguration', "Unable to write into the launch configuration file. Please open it to correct errors/warnings in it and try again.");
387
}
388
if (operation.workspaceStandAloneConfigurationKey === MCP_CONFIGURATION_KEY) {
389
return nls.localize('errorInvalidMCPConfiguration', "Unable to write into the MCP configuration file. Please open it to correct errors/warnings in it and try again.");
390
}
391
switch (target) {
392
case EditableConfigurationTarget.USER_LOCAL:
393
return nls.localize('errorInvalidConfiguration', "Unable to write into user settings. Please open the user settings to correct errors/warnings in it and try again.");
394
case EditableConfigurationTarget.USER_REMOTE:
395
return nls.localize('errorInvalidRemoteConfiguration', "Unable to write into remote user settings. Please open the remote user settings to correct errors/warnings in it and try again.");
396
case EditableConfigurationTarget.WORKSPACE:
397
return nls.localize('errorInvalidConfigurationWorkspace', "Unable to write into workspace settings. Please open the workspace settings to correct errors/warnings in the file and try again.");
398
case EditableConfigurationTarget.WORKSPACE_FOLDER: {
399
let workspaceFolderName: string = '<<unknown>>';
400
if (operation.resource) {
401
const folder = this.contextService.getWorkspaceFolder(operation.resource);
402
if (folder) {
403
workspaceFolderName = folder.name;
404
}
405
}
406
return nls.localize('errorInvalidConfigurationFolder', "Unable to write into folder settings. Please open the '{0}' folder settings to correct errors/warnings in it and try again.", workspaceFolderName);
407
}
408
default:
409
return '';
410
}
411
}
412
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: {
413
if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) {
414
return nls.localize('errorTasksConfigurationFileDirty', "Unable to write into tasks configuration file because the file has unsaved changes. Please save it first and then try again.");
415
}
416
if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) {
417
return nls.localize('errorLaunchConfigurationFileDirty', "Unable to write into launch configuration file because the file has unsaved changes. Please save it first and then try again.");
418
}
419
if (operation.workspaceStandAloneConfigurationKey === MCP_CONFIGURATION_KEY) {
420
return nls.localize('errorMCPConfigurationFileDirty', "Unable to write into MCP configuration file because the file has unsaved changes. Please save it first and then try again.");
421
}
422
switch (target) {
423
case EditableConfigurationTarget.USER_LOCAL:
424
return nls.localize('errorConfigurationFileDirty', "Unable to write into user settings because the file has unsaved changes. Please save the user settings file first and then try again.");
425
case EditableConfigurationTarget.USER_REMOTE:
426
return nls.localize('errorRemoteConfigurationFileDirty', "Unable to write into remote user settings because the file has unsaved changes. Please save the remote user settings file first and then try again.");
427
case EditableConfigurationTarget.WORKSPACE:
428
return nls.localize('errorConfigurationFileDirtyWorkspace', "Unable to write into workspace settings because the file has unsaved changes. Please save the workspace settings file first and then try again.");
429
case EditableConfigurationTarget.WORKSPACE_FOLDER: {
430
let workspaceFolderName: string = '<<unknown>>';
431
if (operation.resource) {
432
const folder = this.contextService.getWorkspaceFolder(operation.resource);
433
if (folder) {
434
workspaceFolderName = folder.name;
435
}
436
}
437
return nls.localize('errorConfigurationFileDirtyFolder', "Unable to write into folder settings because the file has unsaved changes. Please save the '{0}' folder settings file first and then try again.", workspaceFolderName);
438
}
439
default:
440
return '';
441
}
442
}
443
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_MODIFIED_SINCE:
444
if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) {
445
return nls.localize('errorTasksConfigurationFileModifiedSince', "Unable to write into tasks configuration file because the content of the file is newer.");
446
}
447
if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) {
448
return nls.localize('errorLaunchConfigurationFileModifiedSince', "Unable to write into launch configuration file because the content of the file is newer.");
449
}
450
if (operation.workspaceStandAloneConfigurationKey === MCP_CONFIGURATION_KEY) {
451
return nls.localize('errorMCPConfigurationFileModifiedSince', "Unable to write into MCP configuration file because the content of the file is newer.");
452
}
453
switch (target) {
454
case EditableConfigurationTarget.USER_LOCAL:
455
return nls.localize('errorConfigurationFileModifiedSince', "Unable to write into user settings because the content of the file is newer.");
456
case EditableConfigurationTarget.USER_REMOTE:
457
return nls.localize('errorRemoteConfigurationFileModifiedSince', "Unable to write into remote user settings because the content of the file is newer.");
458
case EditableConfigurationTarget.WORKSPACE:
459
return nls.localize('errorConfigurationFileModifiedSinceWorkspace', "Unable to write into workspace settings because the content of the file is newer.");
460
case EditableConfigurationTarget.WORKSPACE_FOLDER:
461
return nls.localize('errorConfigurationFileModifiedSinceFolder', "Unable to write into folder settings because the content of the file is newer.");
462
}
463
case ConfigurationEditingErrorCode.ERROR_INTERNAL: return nls.localize('errorUnknown', "Unable to write to {0} because of an internal error.", this.stringifyTarget(target));
464
}
465
}
466
467
private stringifyTarget(target: EditableConfigurationTarget): string {
468
switch (target) {
469
case EditableConfigurationTarget.USER_LOCAL:
470
return nls.localize('userTarget', "User Settings");
471
case EditableConfigurationTarget.USER_REMOTE:
472
return nls.localize('remoteUserTarget', "Remote User Settings");
473
case EditableConfigurationTarget.WORKSPACE:
474
return nls.localize('workspaceTarget', "Workspace Settings");
475
case EditableConfigurationTarget.WORKSPACE_FOLDER:
476
return nls.localize('folderTarget', "Folder Settings");
477
default:
478
return '';
479
}
480
}
481
482
private defaultResourceValue(resource: URI): string {
483
const basename: string = this.uriIdentityService.extUri.basename(resource);
484
const configurationValue: string = basename.substr(0, basename.length - this.uriIdentityService.extUri.extname(resource).length);
485
switch (configurationValue) {
486
case TASKS_CONFIGURATION_KEY: return TASKS_DEFAULT;
487
default: return '{}';
488
}
489
}
490
491
private async resolveModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
492
const exists = await this.fileService.exists(resource);
493
if (!exists) {
494
await this.textFileService.write(resource, this.defaultResourceValue(resource), { encoding: 'utf8' });
495
}
496
return this.textModelResolverService.createModelReference(resource);
497
}
498
499
private hasParseErrors(content: string, operation: IConfigurationEditOperation): boolean {
500
// If we write to a workspace standalone file and replace the entire contents (no key provided)
501
// we can return here because any parse errors can safely be ignored since all contents are replaced
502
if (operation.workspaceStandAloneConfigurationKey && !operation.key) {
503
return false;
504
}
505
const parseErrors: json.ParseError[] = [];
506
json.parse(content, parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
507
return parseErrors.length > 0;
508
}
509
510
private async validate(target: EditableConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationUpdateOverrides): Promise<void> {
511
512
if (this.configurationService.inspect(operation.key).policyValue !== undefined) {
513
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION, target, operation);
514
}
515
516
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
517
const configurationScope = configurationProperties[operation.key]?.scope;
518
519
/**
520
* Key to update must be a known setting from the registry unless
521
* - the key is standalone configuration (eg: tasks, debug)
522
* - the key is an override identifier
523
* - the operation is to delete the key
524
*/
525
if (!operation.workspaceStandAloneConfigurationKey) {
526
const validKeys = this.configurationService.keys().default;
527
if (validKeys.indexOf(operation.key) < 0 && !OVERRIDE_PROPERTY_REGEX.test(operation.key) && operation.value !== undefined) {
528
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY, target, operation);
529
}
530
}
531
532
if (operation.workspaceStandAloneConfigurationKey) {
533
// Global launches are not supported
534
if ((operation.workspaceStandAloneConfigurationKey !== TASKS_CONFIGURATION_KEY) && (operation.workspaceStandAloneConfigurationKey !== MCP_CONFIGURATION_KEY) && (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE)) {
535
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, target, operation);
536
}
537
}
538
539
// Target cannot be workspace or folder if no workspace opened
540
if ((target === EditableConfigurationTarget.WORKSPACE || target === EditableConfigurationTarget.WORKSPACE_FOLDER) && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
541
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED, target, operation);
542
}
543
544
if (target === EditableConfigurationTarget.WORKSPACE) {
545
if (!operation.workspaceStandAloneConfigurationKey && !OVERRIDE_PROPERTY_REGEX.test(operation.key)) {
546
if (configurationScope && APPLICATION_SCOPES.includes(configurationScope)) {
547
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, target, operation);
548
}
549
if (configurationScope === ConfigurationScope.MACHINE) {
550
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE, target, operation);
551
}
552
}
553
}
554
555
if (target === EditableConfigurationTarget.WORKSPACE_FOLDER) {
556
if (!operation.resource) {
557
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation);
558
}
559
560
if (!operation.workspaceStandAloneConfigurationKey && !OVERRIDE_PROPERTY_REGEX.test(operation.key)) {
561
if (configurationScope !== undefined && !FOLDER_SCOPES.includes(configurationScope)) {
562
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, target, operation);
563
}
564
}
565
}
566
567
if (overrides.overrideIdentifiers?.length) {
568
if (configurationScope !== ConfigurationScope.LANGUAGE_OVERRIDABLE) {
569
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_RESOURCE_LANGUAGE_CONFIGURATION, target, operation);
570
}
571
}
572
573
if (!operation.resource) {
574
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation);
575
}
576
577
if (checkDirty && this.textFileService.isDirty(operation.resource)) {
578
throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, target, operation);
579
}
580
581
}
582
583
private getConfigurationEditOperation(target: EditableConfigurationTarget, config: IConfigurationValue, overrides: IConfigurationUpdateOverrides): IConfigurationEditOperation {
584
585
// Check for standalone workspace configurations
586
if (config.key) {
587
const standaloneConfigurationMap = target === EditableConfigurationTarget.USER_LOCAL ? USER_STANDALONE_CONFIGURATIONS : WORKSPACE_STANDALONE_CONFIGURATIONS;
588
const standaloneConfigurationKeys = Object.keys(standaloneConfigurationMap);
589
for (const key of standaloneConfigurationKeys) {
590
const resource = this.getConfigurationFileResource(target, key, standaloneConfigurationMap[key], overrides.resource, undefined);
591
592
// Check for prefix
593
if (config.key === key) {
594
const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key] : [];
595
return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource: resource ?? undefined, workspaceStandAloneConfigurationKey: key, target };
596
}
597
598
// Check for prefix.<setting>
599
const keyPrefix = `${key}.`;
600
if (config.key.indexOf(keyPrefix) === 0) {
601
const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key, config.key.substring(keyPrefix.length)] : [config.key.substring(keyPrefix.length)];
602
return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource: resource ?? undefined, workspaceStandAloneConfigurationKey: key, target };
603
}
604
}
605
}
606
607
const key = config.key;
608
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
609
const configurationScope = configurationProperties[key]?.scope;
610
let jsonPath = overrides.overrideIdentifiers?.length ? [keyFromOverrideIdentifiers(overrides.overrideIdentifiers), key] : [key];
611
if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) {
612
return { key, jsonPath, value: config.value, resource: this.getConfigurationFileResource(target, key, '', null, configurationScope) ?? undefined, target };
613
}
614
615
const resource = this.getConfigurationFileResource(target, key, FOLDER_SETTINGS_PATH, overrides.resource, configurationScope);
616
if (this.isWorkspaceConfigurationResource(resource)) {
617
jsonPath = ['settings', ...jsonPath];
618
}
619
return { key, jsonPath, value: config.value, resource: resource ?? undefined, target };
620
}
621
622
private isWorkspaceConfigurationResource(resource: URI | null): boolean {
623
const workspace = this.contextService.getWorkspace();
624
return !!(workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath);
625
}
626
627
private getConfigurationFileResource(target: EditableConfigurationTarget, key: string, relativePath: string, resource: URI | null | undefined, scope: ConfigurationScope | undefined): URI | null {
628
if (target === EditableConfigurationTarget.USER_LOCAL) {
629
if (key === TASKS_CONFIGURATION_KEY) {
630
return this.userDataProfileService.currentProfile.tasksResource;
631
} if (key === MCP_CONFIGURATION_KEY) {
632
return this.userDataProfileService.currentProfile.mcpResource;
633
} else {
634
if (!this.userDataProfileService.currentProfile.isDefault && this.configurationService.isSettingAppliedForAllProfiles(key)) {
635
return this.userDataProfilesService.defaultProfile.settingsResource;
636
}
637
return this.userDataProfileService.currentProfile.settingsResource;
638
}
639
}
640
if (target === EditableConfigurationTarget.USER_REMOTE) {
641
return this.remoteSettingsResource;
642
}
643
const workbenchState = this.contextService.getWorkbenchState();
644
if (workbenchState !== WorkbenchState.EMPTY) {
645
646
const workspace = this.contextService.getWorkspace();
647
648
if (target === EditableConfigurationTarget.WORKSPACE) {
649
if (workbenchState === WorkbenchState.WORKSPACE) {
650
return workspace.configuration ?? null;
651
}
652
if (workbenchState === WorkbenchState.FOLDER) {
653
return workspace.folders[0].toResource(relativePath);
654
}
655
}
656
657
if (target === EditableConfigurationTarget.WORKSPACE_FOLDER) {
658
if (resource) {
659
const folder = this.contextService.getWorkspaceFolder(resource);
660
if (folder) {
661
return folder.toResource(relativePath);
662
}
663
}
664
}
665
}
666
return null;
667
}
668
}
669
670