bulkEditService.ts 14.7 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { mergeSort } from 'vs/base/common/arrays';
J
Johannes Rieken 已提交
7
import { dispose, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle';
8
import { URI } from 'vs/base/common/uri';
9
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
J
Johannes Rieken 已提交
10
import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService';
J
Johannes Rieken 已提交
11
import { EditOperation } from 'vs/editor/common/core/editOperation';
12
import { Range } from 'vs/editor/common/core/range';
13
import { Selection } from 'vs/editor/common/core/selection';
14
import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
15
import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes';
16
import { IModelService } from 'vs/editor/common/services/modelService';
17
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
18
import { localize } from 'vs/nls';
19
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
20 21
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
22
import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress';
23
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
B
Benjamin Pasero 已提交
24
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
B
Benjamin Pasero 已提交
25
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
A
renames  
Alex Dima 已提交
26
import { EditorOption } from 'vs/editor/common/config/editorOptions';
27
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
J
Johannes Rieken 已提交
28
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
29
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
30
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
31
import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack';
E
Erich Gamma 已提交
32

33 34
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };

J
Johannes Rieken 已提交
35
class ModelEditTask implements IDisposable {
E
Erich Gamma 已提交
36

37
	public readonly model: ITextModel;
E
Erich Gamma 已提交
38

J
Johannes Rieken 已提交
39
	protected _edits: IIdentifiedSingleEditOperation[];
40
	private _expectedModelVersionId: number | undefined;
41
	protected _newEol: EndOfLineSequence | undefined;
J
Johannes Rieken 已提交
42

43
	constructor(private readonly _modelReference: IReference<IResolvedTextEditorModel>) {
44
		this.model = this._modelReference.object.textEditorModel;
E
Erich Gamma 已提交
45 46 47
		this._edits = [];
	}

48
	dispose() {
49
		this._modelReference.dispose();
50
	}
J
Johannes Rieken 已提交
51

52
	addEdit(resourceEdit: WorkspaceTextEdit): void {
53
		this._expectedModelVersionId = resourceEdit.modelVersionId;
54
		const { edit } = resourceEdit;
55

56 57 58 59 60 61 62
		if (typeof edit.eol === 'number') {
			// honor eol-change
			this._newEol = edit.eol;
		}
		if (!edit.range && !edit.text) {
			// lacks both a range and the text
			return;
J
Johannes Rieken 已提交
63
		}
64 65 66 67 68 69 70 71
		if (Range.isEmpty(edit.range) && !edit.text) {
			// no-op edit (replace empty range with empty text)
			return;
		}

		// create edit operation
		let range: Range;
		if (!edit.range) {
72
			range = this.model.getFullModelRange();
73 74 75 76
		} else {
			range = Range.lift(edit.range);
		}
		this._edits.push(EditOperation.replaceMove(range, edit.text));
E
Erich Gamma 已提交
77 78
	}

79
	validate(): ValidationResult {
80
		if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) {
81 82
			return { canApply: true };
		}
83 84 85 86 87
		return { canApply: false, reason: this.model.uri };
	}

	getBeforeCursorState(): Selection[] | null {
		return null;
88 89
	}

90
	apply(): void {
91
		if (this._edits.length > 0) {
J
Johannes Rieken 已提交
92
			this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
93
			this.model.pushEditOperations(null, this._edits, () => null);
94 95
		}
		if (this._newEol !== undefined) {
96
			this.model.pushEOL(this._newEol);
E
Erich Gamma 已提交
97 98 99 100
		}
	}
}

J
Johannes Rieken 已提交
101
class EditorEditTask extends ModelEditTask {
E
Erich Gamma 已提交
102

J
Johannes Rieken 已提交
103
	private _editor: ICodeEditor;
E
Erich Gamma 已提交
104

105
	constructor(modelReference: IReference<IResolvedTextEditorModel>, editor: ICodeEditor) {
J
Joao Moreno 已提交
106
		super(modelReference);
J
Johannes Rieken 已提交
107
		this._editor = editor;
E
Erich Gamma 已提交
108 109
	}

110 111 112 113
	getBeforeCursorState(): Selection[] | null {
		return this._editor.getSelections();
	}

J
Johannes Rieken 已提交
114 115 116 117 118 119
	apply(): void {
		if (this._edits.length > 0) {
			this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
			this._editor.executeEdits('', this._edits);
		}
		if (this._newEol !== undefined) {
M
Matt Bierner 已提交
120 121 122
			if (this._editor.hasModel()) {
				this._editor.getModel().pushEOL(this._newEol);
			}
J
Johannes Rieken 已提交
123
		}
E
Erich Gamma 已提交
124 125 126
	}
}

127
class BulkEditModel implements IDisposable {
E
Erich Gamma 已提交
128

129
	private _edits = new Map<string, WorkspaceTextEdit[]>();
130
	private _tasks: ModelEditTask[] | undefined;
E
Erich Gamma 已提交
131

132
	constructor(
133
		private readonly _label: string | undefined,
134 135
		private readonly _editor: ICodeEditor | undefined,
		private readonly _progress: IProgress<void>,
136
		edits: WorkspaceTextEdit[],
137 138
		@IEditorWorkerService private readonly _editorWorker: IEditorWorkerService,
		@ITextModelService private readonly _textModelResolverService: ITextModelService,
A
Alex Dima 已提交
139
		@IUndoRedoService private readonly _undoRedoService: IUndoRedoService
140
	) {
141
		edits.forEach(this._addEdit, this);
142
	}
143

144
	dispose(): void {
145 146 147
		if (this._tasks) {
			dispose(this._tasks);
		}
E
Erich Gamma 已提交
148 149
	}

150
	private _addEdit(edit: WorkspaceTextEdit): void {
151
		let array = this._edits.get(edit.resource.toString());
E
Erich Gamma 已提交
152
		if (!array) {
153 154
			array = [];
			this._edits.set(edit.resource.toString(), array);
E
Erich Gamma 已提交
155 156 157 158
		}
		array.push(edit);
	}

159
	async prepare(): Promise<BulkEditModel> {
E
Erich Gamma 已提交
160 161 162 163 164 165

		if (this._tasks) {
			throw new Error('illegal state - already prepared');
		}

		this._tasks = [];
J
Johannes Rieken 已提交
166
		const promises: Promise<any>[] = [];
E
Erich Gamma 已提交
167

168
		for (let [key, value] of this._edits) {
169
			const promise = this._textModelResolverService.createModelReference(URI.parse(key)).then(async ref => {
J
Johannes Rieken 已提交
170
				let task: ModelEditTask;
171
				let makeMinimal = false;
172
				if (this._editor && this._editor.hasModel() && this._editor.getModel().uri.toString() === ref.object.textEditorModel.uri.toString()) {
J
Johannes Rieken 已提交
173
					task = new EditorEditTask(ref, this._editor);
174
					makeMinimal = true;
E
Erich Gamma 已提交
175
				} else {
J
Johannes Rieken 已提交
176
					task = new ModelEditTask(ref);
E
Erich Gamma 已提交
177 178
				}

179 180
				for (const edit of value) {
					if (makeMinimal) {
181 182 183 184 185 186 187 188
						const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.edit]);
						if (!newEdits) {
							task.addEdit(edit);
						} else {
							for (let moreMinialEdit of newEdits) {
								task.addEdit({ ...edit, edit: moreMinialEdit });
							}
						}
189 190
					} else {
						task.addEdit(edit);
191 192 193
					}
				}

194
				this._tasks!.push(task);
195
				this._progress.report(undefined);
E
Erich Gamma 已提交
196 197
			});
			promises.push(promise);
198
		}
E
Erich Gamma 已提交
199

200
		await Promise.all(promises);
201

202
		return this;
E
Erich Gamma 已提交
203 204
	}

205
	validate(): ValidationResult {
206
		for (const task of this._tasks!) {
207 208 209 210 211 212 213 214
			const result = task.validate();
			if (!result.canApply) {
				return result;
			}
		}
		return { canApply: true };
	}

J
Johannes Rieken 已提交
215
	apply(): void {
216 217
		const tasks = this._tasks!;

A
Alex Dima 已提交
218
		if (tasks.length === 1) {
219 220 221 222 223 224 225 226 227 228 229
			// This edit touches a single model => keep things simple
			for (const task of tasks) {
				task.model.pushStackElement();
				task.apply();
				task.model.pushStackElement();
				this._progress.report(undefined);
			}
			return;
		}

		const multiModelEditStackElement = new MultiModelEditStackElement(
230
			this._label || localize('workspaceEdit', "Workspace Edit"),
231
			tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState()))
232 233 234 235
		);
		this._undoRedoService.pushElement(multiModelEditStackElement);

		for (const task of tasks) {
236 237
			task.apply();
			this._progress.report(undefined);
238
		}
239 240

		multiModelEditStackElement.close();
241
	}
E
Erich Gamma 已提交
242 243
}

244
type Edit = WorkspaceFileEdit | WorkspaceTextEdit;
E
Erich Gamma 已提交
245

J
Johannes Rieken 已提交
246
class BulkEdit {
E
Erich Gamma 已提交
247

248
	private readonly _label: string | undefined;
J
Johannes Rieken 已提交
249 250 251
	private readonly _edits: Edit[] = [];
	private readonly _editor: ICodeEditor | undefined;
	private readonly _progress: IProgress<IProgressStep>;
252

253
	constructor(
254
		label: string | undefined,
M
Matt Bierner 已提交
255
		editor: ICodeEditor | undefined,
B
Benjamin Pasero 已提交
256
		progress: IProgress<IProgressStep> | undefined,
J
Johannes Rieken 已提交
257
		edits: Edit[],
J
Johannes Rieken 已提交
258
		@IInstantiationService private readonly _instaService: IInstantiationService,
259
		@ILogService private readonly _logService: ILogService,
260
		@IFileService private readonly _fileService: IFileService,
B
Benjamin Pasero 已提交
261
		@ITextFileService private readonly _textFileService: ITextFileService,
262
		@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
B
Benjamin Pasero 已提交
263
		@IConfigurationService private readonly _configurationService: IConfigurationService
264
	) {
265
		this._label = label;
266
		this._editor = editor;
267
		this._progress = progress || Progress.None;
J
Johannes Rieken 已提交
268
		this._edits = edits;
E
Erich Gamma 已提交
269 270
	}

271
	ariaMessage(): string {
272
		const editCount = this._edits.length;
273 274
		const resourceCount = this._edits.length;
		if (editCount === 0) {
275
			return localize('summary.0', "Made no edits");
276
		} else if (editCount > 1 && resourceCount > 1) {
277
			return localize('summary.nm', "Made {0} text edits in {1} files", editCount, resourceCount);
278
		} else {
279
			return localize('summary.n0', "Made {0} text edits in one file", editCount, resourceCount);
280
		}
281 282
	}

J
Johannes Rieken 已提交
283
	async perform(): Promise<void> {
284

285 286
		let seen = new Set<string>();
		let total = 0;
287

288
		const groups: Edit[][] = [];
M
Matt Bierner 已提交
289
		let group: Edit[] | undefined;
290
		for (const edit of this._edits) {
J
Johannes Rieken 已提交
291
			if (!group
292 293
				|| (WorkspaceFileEdit.is(group[0]) && !WorkspaceFileEdit.is(edit))
				|| (WorkspaceTextEdit.is(group[0]) && !WorkspaceTextEdit.is(edit))
J
Johannes Rieken 已提交
294
			) {
295 296 297 298 299
				group = [];
				groups.push(group);
			}
			group.push(edit);

300
			if (WorkspaceFileEdit.is(edit)) {
301 302 303 304
				total += 1;
			} else if (!seen.has(edit.resource.toString())) {
				seen.add(edit.resource.toString());
				total += 2;
E
Erich Gamma 已提交
305 306
			}
		}
307 308 309

		// define total work and progress callback
		// for child operations
B
Benjamin Pasero 已提交
310 311
		this._progress.report({ total });

J
Johannes Rieken 已提交
312
		const progress: IProgress<void> = { report: _ => this._progress.report({ increment: 1 }) };
313

J
Johannes Rieken 已提交
314
		// do it.
315
		for (const group of groups) {
316 317
			if (WorkspaceFileEdit.is(group[0])) {
				await this._performFileEdits(<WorkspaceFileEdit[]>group, progress);
318
			} else {
J
Johannes Rieken 已提交
319
				await this._performTextEdits(<WorkspaceTextEdit[]>group, progress);
320
			}
E
Erich Gamma 已提交
321 322 323
		}
	}

324
	private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress<void>) {
325
		this._logService.debug('_performFileEdits', JSON.stringify(edits));
326 327
		for (const edit of edits) {
			progress.report(undefined);
E
Erich Gamma 已提交
328

J
Johannes Rieken 已提交
329 330
			let options = edit.options || {};

331
			if (edit.newUri && edit.oldUri) {
J
Johannes Rieken 已提交
332
				// rename
B
Benjamin Pasero 已提交
333
				if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
J
Johannes Rieken 已提交
334 335
					continue; // not overwriting, but ignoring, and the target file exists
				}
336
				await this._workingCopyFileService.move(edit.oldUri, edit.newUri, options.overwrite);
J
Johannes Rieken 已提交
337

338
			} else if (!edit.newUri && edit.oldUri) {
J
Johannes Rieken 已提交
339
				// delete file
B
Benjamin Pasero 已提交
340
				if (await this._fileService.exists(edit.oldUri)) {
341
					let useTrash = this._configurationService.getValue<boolean>('files.enableTrash');
342
					if (useTrash && !(this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) {
343 344
						useTrash = false; // not supported by provider
					}
345
					await this._workingCopyFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive });
346 347
				} else if (!options.ignoreIfNotExists) {
					throw new Error(`${edit.oldUri} does not exist and can not be deleted`);
J
Johannes Rieken 已提交
348
				}
349
			} else if (edit.newUri && !edit.oldUri) {
J
Johannes Rieken 已提交
350
				// create file
B
Benjamin Pasero 已提交
351
				if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
J
Johannes Rieken 已提交
352
					continue; // not overwriting, but ignoring, and the target file exists
353
				}
J
Johannes Rieken 已提交
354
				await this._textFileService.create(edit.newUri, undefined, { overwrite: options.overwrite });
355
			}
E
Erich Gamma 已提交
356
		}
357
	}
E
Erich Gamma 已提交
358

J
Johannes Rieken 已提交
359
	private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress<void>): Promise<void> {
360
		this._logService.debug('_performTextEdits', JSON.stringify(edits));
E
Erich Gamma 已提交
361

362
		const model = this._instaService.createInstance(BulkEditModel, this._label, this._editor, progress, edits);
E
Erich Gamma 已提交
363

364
		await model.prepare();
E
Erich Gamma 已提交
365

J
Johannes Rieken 已提交
366
		// this._throwIfConflicts(conflicts);
367 368
		const validationResult = model.validate();
		if (validationResult.canApply === false) {
369
			model.dispose();
370 371 372
			throw new Error(`${validationResult.reason.toString()} has changed in the meantime`);
		}

373
		model.apply();
374 375
		model.dispose();
	}
E
Erich Gamma 已提交
376
}
377 378 379

export class BulkEditService implements IBulkEditService {

380
	declare readonly _serviceBrand: undefined;
381

J
Johannes Rieken 已提交
382 383
	private _previewHandler?: IBulkEditPreviewHandler;

384
	constructor(
J
Johannes Rieken 已提交
385
		@IInstantiationService private readonly _instaService: IInstantiationService,
386
		@ILogService private readonly _logService: ILogService,
387
		@IModelService private readonly _modelService: IModelService,
388
		@IEditorService private readonly _editorService: IEditorService,
389
	) { }
390

J
Johannes Rieken 已提交
391 392 393 394 395 396 397 398 399
	setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable {
		this._previewHandler = handler;
		return toDisposable(() => {
			if (this._previewHandler === handler) {
				this._previewHandler = undefined;
			}
		});
	}

J
Johannes Rieken 已提交
400 401 402 403
	hasPreviewHandler(): boolean {
		return Boolean(this._previewHandler);
	}

J
Johannes Rieken 已提交
404
	async apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise<IBulkEditResult> {
405

406 407 408 409
		if (edit.edits.length === 0) {
			return { ariaSummary: localize('nothing', "Made no edits") };
		}

410
		if (this._previewHandler && (options?.showPreview || edit.edits.some(value => value.metadata?.needsConfirmation))) {
J
Johannes Rieken 已提交
411 412
			edit = await this._previewHandler(edit, options);
		}
413

J
Johannes Rieken 已提交
414 415
		const { edits } = edit;
		let codeEditor = options?.editor;
416 417

		// First check if loaded models were not changed in the meantime
418
		for (const edit of edits) {
419
			if (!WorkspaceFileEdit.is(edit) && typeof edit.modelVersionId === 'number') {
420 421 422
				let model = this._modelService.getModel(edit.resource);
				if (model && model.getVersionId() !== edit.modelVersionId) {
					// model changed in the meantime
423
					return Promise.reject(new Error(`${model.uri.toString()} has changed in the meantime`));
424 425 426 427 428 429
				}
			}
		}

		// try to find code editor
		if (!codeEditor) {
430
			let candidate = this._editorService.activeTextEditorControl;
B
Benjamin Pasero 已提交
431 432
			if (isCodeEditor(candidate)) {
				codeEditor = candidate;
433 434 435
			}
		}

A
renames  
Alex Dima 已提交
436
		if (codeEditor && codeEditor.getOption(EditorOption.readOnly)) {
437 438 439
			// If the code editor is readonly still allow bulk edits to be applied #68549
			codeEditor = undefined;
		}
440
		const bulkEdit = this._instaService.createInstance(BulkEdit, options?.quotableLabel || options?.label, codeEditor, options?.progress, edits);
441
		return bulkEdit.perform().then(() => {
J
Johannes Rieken 已提交
442
			return { ariaSummary: bulkEdit.ariaMessage() };
443
		}).catch(err => {
444 445
			// console.log('apply FAILED');
			// console.log(err);
446
			this._logService.error(err);
447
			throw err;
448
		});
449 450 451
	}
}

452
registerSingleton(IBulkEditService, BulkEditService, true);