Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.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
7
import { WorkspaceFileEditOptions } from '../../../../editor/common/languages.js';
8
import { IFileService, FileSystemProviderCapabilities, IFileContent, IFileStatWithMetadata } from '../../../../platform/files/common/files.js';
9
import { IProgress } from '../../../../platform/progress/common/progress.js';
10
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
11
import { IWorkingCopyFileService, IFileOperationUndoRedoInfo, IMoveOperation, ICopyOperation, IDeleteOperation, ICreateOperation, ICreateFileOperation } from '../../../services/workingCopy/common/workingCopyFileService.js';
12
import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService, UndoRedoGroup, UndoRedoSource } from '../../../../platform/undoRedo/common/undoRedo.js';
13
import { URI } from '../../../../base/common/uri.js';
14
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
15
import { ILogService } from '../../../../platform/log/common/log.js';
16
import { VSBuffer } from '../../../../base/common/buffer.js';
17
import { ResourceFileEdit } from '../../../../editor/browser/services/bulkEditService.js';
18
import { CancellationToken } from '../../../../base/common/cancellation.js';
19
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
20
import { Schemas } from '../../../../base/common/network.js';
21
22
interface IFileOperation {
23
uris: URI[];
24
perform(token: CancellationToken): Promise<IFileOperation>;
25
}
26
27
class Noop implements IFileOperation {
28
readonly uris = [];
29
async perform() { return this; }
30
toString(): string {
31
return '(noop)';
32
}
33
}
34
35
class RenameEdit {
36
readonly type = 'rename';
37
constructor(
38
readonly newUri: URI,
39
readonly oldUri: URI,
40
readonly options: WorkspaceFileEditOptions
41
) { }
42
}
43
44
class RenameOperation implements IFileOperation {
45
46
constructor(
47
private readonly _edits: RenameEdit[],
48
private readonly _undoRedoInfo: IFileOperationUndoRedoInfo,
49
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
50
@IFileService private readonly _fileService: IFileService,
51
) { }
52
53
get uris() {
54
return this._edits.flatMap(edit => [edit.newUri, edit.oldUri]);
55
}
56
57
async perform(token: CancellationToken): Promise<IFileOperation> {
58
59
const moves: IMoveOperation[] = [];
60
const undoes: RenameEdit[] = [];
61
for (const edit of this._edits) {
62
// check: not overwriting, but ignoring, and the target file exists
63
const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri);
64
if (!skip) {
65
moves.push({
66
file: { source: edit.oldUri, target: edit.newUri },
67
overwrite: edit.options.overwrite
68
});
69
70
// reverse edit
71
undoes.push(new RenameEdit(edit.oldUri, edit.newUri, edit.options));
72
}
73
}
74
75
if (moves.length === 0) {
76
return new Noop();
77
}
78
79
await this._workingCopyFileService.move(moves, token, this._undoRedoInfo);
80
return new RenameOperation(undoes, { isUndoing: true }, this._workingCopyFileService, this._fileService);
81
}
82
83
toString(): string {
84
return `(rename ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`;
85
}
86
}
87
88
class CopyEdit {
89
readonly type = 'copy';
90
constructor(
91
readonly newUri: URI,
92
readonly oldUri: URI,
93
readonly options: WorkspaceFileEditOptions
94
) { }
95
}
96
97
class CopyOperation implements IFileOperation {
98
99
constructor(
100
private readonly _edits: CopyEdit[],
101
private readonly _undoRedoInfo: IFileOperationUndoRedoInfo,
102
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
103
@IFileService private readonly _fileService: IFileService,
104
@IInstantiationService private readonly _instaService: IInstantiationService
105
) { }
106
107
get uris() {
108
return this._edits.flatMap(edit => [edit.newUri, edit.oldUri]);
109
}
110
111
async perform(token: CancellationToken): Promise<IFileOperation> {
112
113
// (1) create copy operations, remove noops
114
const copies: ICopyOperation[] = [];
115
for (const edit of this._edits) {
116
//check: not overwriting, but ignoring, and the target file exists
117
const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri);
118
if (!skip) {
119
copies.push({ file: { source: edit.oldUri, target: edit.newUri }, overwrite: edit.options.overwrite });
120
}
121
}
122
123
if (copies.length === 0) {
124
return new Noop();
125
}
126
127
// (2) perform the actual copy and use the return stats to build undo edits
128
const stats = await this._workingCopyFileService.copy(copies, token, this._undoRedoInfo);
129
const undoes: DeleteEdit[] = [];
130
131
for (let i = 0; i < stats.length; i++) {
132
const stat = stats[i];
133
const edit = this._edits[i];
134
undoes.push(new DeleteEdit(stat.resource, { recursive: true, folder: this._edits[i].options.folder || stat.isDirectory, ...edit.options }, false));
135
}
136
137
return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true });
138
}
139
140
toString(): string {
141
return `(copy ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`;
142
}
143
}
144
145
class CreateEdit {
146
readonly type = 'create';
147
constructor(
148
readonly newUri: URI,
149
readonly options: WorkspaceFileEditOptions,
150
readonly contents: VSBuffer | undefined,
151
) { }
152
}
153
154
class CreateOperation implements IFileOperation {
155
156
constructor(
157
private readonly _edits: CreateEdit[],
158
private readonly _undoRedoInfo: IFileOperationUndoRedoInfo,
159
@IFileService private readonly _fileService: IFileService,
160
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
161
@IInstantiationService private readonly _instaService: IInstantiationService,
162
@ITextFileService private readonly _textFileService: ITextFileService
163
) { }
164
165
get uris() {
166
return this._edits.map(edit => edit.newUri);
167
}
168
169
async perform(token: CancellationToken): Promise<IFileOperation> {
170
171
const folderCreates: ICreateOperation[] = [];
172
const fileCreates: ICreateFileOperation[] = [];
173
const undoes: DeleteEdit[] = [];
174
175
for (const edit of this._edits) {
176
if (edit.newUri.scheme === Schemas.untitled) {
177
continue; // ignore, will be handled by a later edit
178
}
179
if (edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
180
continue; // not overwriting, but ignoring, and the target file exists
181
}
182
if (edit.options.folder) {
183
folderCreates.push({ resource: edit.newUri });
184
} else {
185
// If the contents are part of the edit they include the encoding, thus use them. Otherwise get the encoding for a new empty file.
186
const encodedReadable = typeof edit.contents !== 'undefined' ? edit.contents : await this._textFileService.getEncodedReadable(edit.newUri);
187
fileCreates.push({ resource: edit.newUri, contents: encodedReadable, overwrite: edit.options.overwrite });
188
}
189
undoes.push(new DeleteEdit(edit.newUri, edit.options, !edit.options.folder && !edit.contents));
190
}
191
192
if (folderCreates.length === 0 && fileCreates.length === 0) {
193
return new Noop();
194
}
195
196
await this._workingCopyFileService.createFolder(folderCreates, token, this._undoRedoInfo);
197
await this._workingCopyFileService.create(fileCreates, token, this._undoRedoInfo);
198
199
return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true });
200
}
201
202
toString(): string {
203
return `(create ${this._edits.map(edit => edit.options.folder ? `folder ${edit.newUri}` : `file ${edit.newUri} with ${edit.contents?.byteLength || 0} bytes`).join(', ')})`;
204
}
205
}
206
207
class DeleteEdit {
208
readonly type = 'delete';
209
constructor(
210
readonly oldUri: URI,
211
readonly options: WorkspaceFileEditOptions,
212
readonly undoesCreate: boolean,
213
) { }
214
}
215
216
class DeleteOperation implements IFileOperation {
217
218
constructor(
219
private _edits: DeleteEdit[],
220
private readonly _undoRedoInfo: IFileOperationUndoRedoInfo,
221
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
222
@IFileService private readonly _fileService: IFileService,
223
@IConfigurationService private readonly _configurationService: IConfigurationService,
224
@IInstantiationService private readonly _instaService: IInstantiationService,
225
@ILogService private readonly _logService: ILogService
226
) { }
227
228
get uris() {
229
return this._edits.map(edit => edit.oldUri);
230
}
231
232
async perform(token: CancellationToken): Promise<IFileOperation> {
233
// delete file
234
235
const deletes: IDeleteOperation[] = [];
236
const undoes: CreateEdit[] = [];
237
238
for (const edit of this._edits) {
239
let fileStat: IFileStatWithMetadata | undefined;
240
try {
241
fileStat = await this._fileService.resolve(edit.oldUri, { resolveMetadata: true });
242
} catch (err) {
243
if (!edit.options.ignoreIfNotExists) {
244
throw new Error(`${edit.oldUri} does not exist and can not be deleted`);
245
}
246
continue;
247
}
248
249
deletes.push({
250
resource: edit.oldUri,
251
recursive: edit.options.recursive,
252
useTrash: !edit.options.skipTrashBin && this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue<boolean>('files.enableTrash')
253
});
254
255
256
// read file contents for undo operation. when a file is too large it won't be restored
257
let fileContent: IFileContent | undefined;
258
let fileContentExceedsMaxSize = false;
259
if (!edit.undoesCreate && !edit.options.folder) {
260
fileContentExceedsMaxSize = typeof edit.options.maxSize === 'number' && fileStat.size > edit.options.maxSize;
261
if (!fileContentExceedsMaxSize) {
262
try {
263
fileContent = await this._fileService.readFile(edit.oldUri);
264
} catch (err) {
265
this._logService.error(err);
266
}
267
}
268
}
269
if (!fileContentExceedsMaxSize) {
270
undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent?.value));
271
}
272
}
273
274
if (deletes.length === 0) {
275
return new Noop();
276
}
277
278
await this._workingCopyFileService.delete(deletes, token, this._undoRedoInfo);
279
280
if (undoes.length === 0) {
281
return new Noop();
282
}
283
return this._instaService.createInstance(CreateOperation, undoes, { isUndoing: true });
284
}
285
286
toString(): string {
287
return `(delete ${this._edits.map(edit => edit.oldUri).join(', ')})`;
288
}
289
}
290
291
class FileUndoRedoElement implements IWorkspaceUndoRedoElement {
292
293
readonly type = UndoRedoElementType.Workspace;
294
295
readonly resources: readonly URI[];
296
297
constructor(
298
readonly label: string,
299
readonly code: string,
300
readonly operations: IFileOperation[],
301
readonly confirmBeforeUndo: boolean
302
) {
303
this.resources = operations.flatMap(op => op.uris);
304
}
305
306
async undo(): Promise<void> {
307
await this._reverse();
308
}
309
310
async redo(): Promise<void> {
311
await this._reverse();
312
}
313
314
private async _reverse() {
315
for (let i = 0; i < this.operations.length; i++) {
316
const op = this.operations[i];
317
const undo = await op.perform(CancellationToken.None);
318
this.operations[i] = undo;
319
}
320
}
321
322
toString(): string {
323
return this.operations.map(op => String(op)).join(', ');
324
}
325
}
326
327
export class BulkFileEdits {
328
329
constructor(
330
private readonly _label: string,
331
private readonly _code: string,
332
private readonly _undoRedoGroup: UndoRedoGroup,
333
private readonly _undoRedoSource: UndoRedoSource | undefined,
334
private readonly _confirmBeforeUndo: boolean,
335
private readonly _progress: IProgress<void>,
336
private readonly _token: CancellationToken,
337
private readonly _edits: ResourceFileEdit[],
338
@IInstantiationService private readonly _instaService: IInstantiationService,
339
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
340
) { }
341
342
async apply(): Promise<readonly URI[]> {
343
const undoOperations: IFileOperation[] = [];
344
const undoRedoInfo = { undoRedoGroupId: this._undoRedoGroup.id };
345
346
const edits: Array<RenameEdit | CopyEdit | DeleteEdit | CreateEdit> = [];
347
for (const edit of this._edits) {
348
if (edit.newResource && edit.oldResource && !edit.options?.copy) {
349
edits.push(new RenameEdit(edit.newResource, edit.oldResource, edit.options ?? {}));
350
} else if (edit.newResource && edit.oldResource && edit.options?.copy) {
351
edits.push(new CopyEdit(edit.newResource, edit.oldResource, edit.options ?? {}));
352
} else if (!edit.newResource && edit.oldResource) {
353
edits.push(new DeleteEdit(edit.oldResource, edit.options ?? {}, false));
354
} else if (edit.newResource && !edit.oldResource) {
355
edits.push(new CreateEdit(edit.newResource, edit.options ?? {}, await edit.options.contents));
356
}
357
}
358
359
if (edits.length === 0) {
360
return [];
361
}
362
363
const groups: Array<RenameEdit | CopyEdit | DeleteEdit | CreateEdit>[] = [];
364
groups[0] = [edits[0]];
365
366
for (let i = 1; i < edits.length; i++) {
367
const edit = edits[i];
368
const lastGroup = groups.at(-1);
369
if (lastGroup?.[0].type === edit.type) {
370
lastGroup.push(edit);
371
} else {
372
groups.push([edit]);
373
}
374
}
375
376
for (const group of groups) {
377
378
if (this._token.isCancellationRequested) {
379
break;
380
}
381
382
let op: IFileOperation | undefined;
383
switch (group[0].type) {
384
case 'rename':
385
op = this._instaService.createInstance(RenameOperation, <RenameEdit[]>group, undoRedoInfo);
386
break;
387
case 'copy':
388
op = this._instaService.createInstance(CopyOperation, <CopyEdit[]>group, undoRedoInfo);
389
break;
390
case 'delete':
391
op = this._instaService.createInstance(DeleteOperation, <DeleteEdit[]>group, undoRedoInfo);
392
break;
393
case 'create':
394
op = this._instaService.createInstance(CreateOperation, <CreateEdit[]>group, undoRedoInfo);
395
break;
396
}
397
398
if (op) {
399
const undoOp = await op.perform(this._token);
400
undoOperations.push(undoOp);
401
}
402
this._progress.report(undefined);
403
}
404
405
const undoRedoElement = new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo);
406
this._undoRedoService.pushElement(undoRedoElement, this._undoRedoGroup, this._undoRedoSource);
407
return undoRedoElement.resources;
408
}
409
}
410
411