1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
|
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { hasWorkspaceFileExtension, isSavedWorkspace, isUntitledWorkspace, IWorkspaceContextService, IWorkspaceIdentifier, WorkbenchState, WORKSPACE_EXTENSION, WORKSPACE_FILTER } from 'vs/platform/workspace/common/workspace';
import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing';
import { IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, IEnterWorkspaceResult, IStoredWorkspace } from 'vs/platform/workspaces/common/workspaces';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { distinct, firstOrDefault } from 'vs/base/common/arrays';
import { basename, isEqual, isEqualAuthority, joinPath, removeTrailingPathSeparator } from 'vs/base/common/resources';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { Schemas } from 'vs/base/common/network';
import { SaveReason } from 'vs/workbench/common/editor';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditingService {
declare readonly _serviceBrand: undefined;
constructor(
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
@IWorkspaceContextService protected readonly contextService: WorkspaceService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@INotificationService private readonly notificationService: INotificationService,
@ICommandService private readonly commandService: ICommandService,
@IFileService private readonly fileService: IFileService,
@ITextFileService private readonly textFileService: ITextFileService,
@IWorkspacesService protected readonly workspacesService: IWorkspacesService,
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IDialogService protected readonly dialogService: IDialogService,
@IHostService protected readonly hostService: IHostService,
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService
) { }
async pickNewWorkspacePath(): Promise<URI | undefined> {
const availableFileSystems = [Schemas.file];
if (this.environmentService.remoteAuthority) {
availableFileSystems.unshift(Schemas.vscodeRemote);
}
let workspacePath = await this.fileDialogService.showSaveDialog({
saveLabel: mnemonicButtonLabel(localize('save', "Save")),
title: localize('saveWorkspace', "Save Workspace"),
filters: WORKSPACE_FILTER,
defaultUri: joinPath(await this.fileDialogService.defaultWorkspacePath(), this.getNewWorkspaceName()),
availableFileSystems
});
if (!workspacePath) {
return; // canceled
}
if (!hasWorkspaceFileExtension(workspacePath)) {
// Always ensure we have workspace file extension
// (see https://github.com/microsoft/vscode/issues/84818)
workspacePath = workspacePath.with({ path: `${workspacePath.path}.${WORKSPACE_EXTENSION}` });
}
return workspacePath;
}
private getNewWorkspaceName(): string {
switch (this.contextService.getWorkbenchState()) {
case WorkbenchState.FOLDER: {
const folder = firstOrDefault(this.contextService.getWorkspace().folders);
if (folder) {
return `${basename(folder.uri)}.${WORKSPACE_EXTENSION}`;
}
break;
}
case WorkbenchState.WORKSPACE: {
const configPathURI = this.getCurrentWorkspaceIdentifier()?.configPath;
if (configPathURI && isSavedWorkspace(configPathURI, this.environmentService)) {
return basename(configPathURI);
}
break;
}
}
return `workspace.${WORKSPACE_EXTENSION}`;
}
async updateFolders(index: number, deleteCount?: number, foldersToAddCandidates?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise<void> {
const folders = this.contextService.getWorkspace().folders;
let foldersToDelete: URI[] = [];
if (typeof deleteCount === 'number') {
foldersToDelete = folders.slice(index, index + deleteCount).map(folder => folder.uri);
}
let foldersToAdd: IWorkspaceFolderCreationData[] = [];
if (Array.isArray(foldersToAddCandidates)) {
foldersToAdd = foldersToAddCandidates.map(folderToAdd => ({ uri: removeTrailingPathSeparator(folderToAdd.uri), name: folderToAdd.name })); // Normalize
}
const wantsToDelete = foldersToDelete.length > 0;
const wantsToAdd = foldersToAdd.length > 0;
if (!wantsToAdd && !wantsToDelete) {
return; // return early if there is nothing to do
}
// Add Folders
if (wantsToAdd && !wantsToDelete) {
return this.doAddFolders(foldersToAdd, index, donotNotifyError);
}
// Delete Folders
if (wantsToDelete && !wantsToAdd) {
return this.removeFolders(foldersToDelete);
}
// Add & Delete Folders
else {
// if we are in single-folder state and the folder is replaced with
// other folders, we handle this specially and just enter workspace
// mode with the folders that are being added.
if (this.includesSingleFolderWorkspace(foldersToDelete)) {
return this.createAndEnterWorkspace(foldersToAdd);
}
// if we are not in workspace-state, we just add the folders
if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
return this.doAddFolders(foldersToAdd, index, donotNotifyError);
}
// finally, update folders within the workspace
return this.doUpdateFolders(foldersToAdd, foldersToDelete, index, donotNotifyError);
}
}
private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToDelete: URI[], index?: number, donotNotifyError: boolean = false): Promise<void> {
try {
await this.contextService.updateFolders(foldersToAdd, foldersToDelete, index);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
addFolders(foldersToAddCandidates: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): Promise<void> {
// Normalize
const foldersToAdd = foldersToAddCandidates.map(folderToAdd => ({ uri: removeTrailingPathSeparator(folderToAdd.uri), name: folderToAdd.name }));
return this.doAddFolders(foldersToAdd, undefined, donotNotifyError);
}
private async doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): Promise<void> {
const state = this.contextService.getWorkbenchState();
const remoteAuthority = this.environmentService.remoteAuthority;
if (remoteAuthority) {
// https://github.com/microsoft/vscode/issues/94191
foldersToAdd = foldersToAdd.filter(folder => folder.uri.scheme !== Schemas.file && (folder.uri.scheme !== Schemas.vscodeRemote || isEqualAuthority(folder.uri.authority, remoteAuthority)));
}
// If we are in no-workspace or single-folder workspace, adding folders has to
// enter a workspace.
if (state !== WorkbenchState.WORKSPACE) {
let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri }));
newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd);
newWorkspaceFolders = distinct(newWorkspaceFolders, folder => this.uriIdentityService.extUri.getComparisonKey(folder.uri));
if (state === WorkbenchState.EMPTY && newWorkspaceFolders.length === 0 || state === WorkbenchState.FOLDER && newWorkspaceFolders.length === 1) {
return; // return if the operation is a no-op for the current state
}
return this.createAndEnterWorkspace(newWorkspaceFolders);
}
// Delegate addition of folders to workspace service otherwise
try {
await this.contextService.addFolders(foldersToAdd, index);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
async removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): Promise<void> {
// If we are in single-folder state and the opened folder is to be removed,
// we create an empty workspace and enter it.
if (this.includesSingleFolderWorkspace(foldersToRemove)) {
return this.createAndEnterWorkspace([]);
}
// Delegate removal of folders to workspace service otherwise
try {
await this.contextService.removeFolders(foldersToRemove);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
private includesSingleFolderWorkspace(folders: URI[]): boolean {
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
const workspaceFolder = this.contextService.getWorkspace().folders[0];
return (folders.some(folder => this.uriIdentityService.extUri.isEqual(folder, workspaceFolder.uri)));
}
return false;
}
async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise<void> {
if (path && !await this.isValidTargetWorkspacePath(path)) {
return;
}
const remoteAuthority = this.environmentService.remoteAuthority;
const untitledWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority);
if (path) {
try {
await this.saveWorkspaceAs(untitledWorkspace, path);
} finally {
await this.workspacesService.deleteUntitledWorkspace(untitledWorkspace); // https://github.com/microsoft/vscode/issues/100276
}
} else {
path = untitledWorkspace.configPath;
}
return this.enterWorkspace(path);
}
async saveAndEnterWorkspace(workspaceUri: URI): Promise<void> {
const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();
if (!workspaceIdentifier) {
return;
}
// Allow to save the workspace of the current window
// if we have an identical match on the path
if (isEqual(workspaceIdentifier.configPath, workspaceUri)) {
return this.saveWorkspace(workspaceIdentifier);
}
// From this moment on we require a valid target that is not opened already
if (!await this.isValidTargetWorkspacePath(workspaceUri)) {
return;
}
await this.saveWorkspaceAs(workspaceIdentifier, workspaceUri);
return this.enterWorkspace(workspaceUri);
}
async isValidTargetWorkspacePath(workspaceUri: URI): Promise<boolean> {
return true; // OK
}
protected async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise<void> {
const configPathURI = workspace.configPath;
// Return early if target is same as source
if (this.uriIdentityService.extUri.isEqual(configPathURI, targetConfigPathURI)) {
return;
}
const isFromUntitledWorkspace = isUntitledWorkspace(configPathURI, this.environmentService);
// Read the contents of the workspace file, update it to new location and save it.
const raw = await this.fileService.readFile(configPathURI);
const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value.toString(), configPathURI, isFromUntitledWorkspace, targetConfigPathURI, this.uriIdentityService.extUri);
await this.textFileService.create([{ resource: targetConfigPathURI, value: newRawWorkspaceContents, options: { overwrite: true } }]);
// Set trust for the workspace file
await this.trustWorkspaceConfiguration(targetConfigPathURI);
}
protected async saveWorkspace(workspace: IWorkspaceIdentifier): Promise<void> {
const configPathURI = workspace.configPath;
// First: try to save any existing model as it could be dirty
const existingModel = this.textFileService.files.get(configPathURI);
if (existingModel) {
await existingModel.save({ force: true, reason: SaveReason.EXPLICIT });
return;
}
// Second: if the file exists on disk, simply return
const workspaceFileExists = await this.fileService.exists(configPathURI);
if (workspaceFileExists) {
return;
}
// Finally, we need to re-create the file as it was deleted
const newWorkspace: IStoredWorkspace = { folders: [] };
const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(JSON.stringify(newWorkspace, null, '\t'), configPathURI, false, configPathURI, this.uriIdentityService.extUri);
await this.textFileService.create([{ resource: configPathURI, value: newRawWorkspaceContents }]);
}
private handleWorkspaceConfigurationEditingError(error: JSONEditingError): void {
switch (error.code) {
case JSONEditingErrorCode.ERROR_INVALID_FILE:
this.onInvalidWorkspaceConfigurationFileError();
break;
case JSONEditingErrorCode.ERROR_FILE_DIRTY:
this.onWorkspaceConfigurationFileDirtyError();
break;
default:
this.notificationService.error(error.message);
}
}
private onInvalidWorkspaceConfigurationFileError(): void {
const message = localize('errorInvalidTaskConfiguration', "Unable to write into workspace configuration file. Please open the file to correct errors/warnings in it and try again.");
this.askToOpenWorkspaceConfigurationFile(message);
}
private onWorkspaceConfigurationFileDirtyError(): void {
const message = localize('errorWorkspaceConfigurationFileDirty', "Unable to write into workspace configuration file because the file has unsaved changes. Please save it and try again.");
this.askToOpenWorkspaceConfigurationFile(message);
}
private askToOpenWorkspaceConfigurationFile(message: string): void {
this.notificationService.prompt(Severity.Error, message,
[{
label: localize('openWorkspaceConfigurationFile', "Open Workspace Configuration"),
run: () => this.commandService.executeCommand('workbench.action.openWorkspaceConfigFile')
}]
);
}
abstract enterWorkspace(workspaceUri: URI): Promise<void>;
protected async doEnterWorkspace(workspaceUri: URI): Promise<IEnterWorkspaceResult | undefined> {
if (!!this.environmentService.extensionTestsLocationURI) {
throw new Error('Entering a new workspace is not possible in tests.');
}
const workspace = await this.workspacesService.getWorkspaceIdentifier(workspaceUri);
// Settings migration (only if we come from a folder workspace)
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
await this.migrateWorkspaceSettings(workspace);
}
const workspaceImpl = this.contextService as WorkspaceService;
await workspaceImpl.initialize(workspace);
return this.workspacesService.enterWorkspace(workspaceUri);
}
private migrateWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise<void> {
return this.doCopyWorkspaceSettings(toWorkspace, setting => setting.scope === ConfigurationScope.WINDOW);
}
copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise<void> {
return this.doCopyWorkspaceSettings(toWorkspace);
}
private doCopyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier, filter?: (config: IConfigurationPropertySchema) => boolean): Promise<void> {
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
const targetWorkspaceConfiguration: any = {};
for (const key of this.configurationService.keys().workspace) {
if (configurationProperties[key]) {
if (filter && !filter(configurationProperties[key])) {
continue;
}
targetWorkspaceConfiguration[key] = this.configurationService.inspect(key).workspaceValue;
}
}
return this.jsonEditingService.write(toWorkspace.configPath, [{ path: ['settings'], value: targetWorkspaceConfiguration }], true);
}
private async trustWorkspaceConfiguration(configPathURI: URI): Promise<void> {
if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.workspaceTrustManagementService.isWorkspaceTrusted()) {
await this.workspaceTrustManagementService.setUrisTrust([configPathURI], true);
}
}
protected getCurrentWorkspaceIdentifier(): IWorkspaceIdentifier | undefined {
const workspace = this.contextService.getWorkspace();
if (workspace?.configuration) {
return { id: workspace.id, configPath: workspace.configuration };
}
return undefined;
}
}
|