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

import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
8
import { flatten } from 'vs/base/common/arrays';
9
import { IStringDictionary, forEach, values, groupBy, size } from 'vs/base/common/collections';
J
Joao Moreno 已提交
10
import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
11
import URI from 'vs/base/common/uri';
J
Johannes Rieken 已提交
12
import { TPromise } from 'vs/base/common/winjs.base';
J
Joao Moreno 已提交
13
import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
14
import { IFileService, IFileChange } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
15
import { EditOperation } from 'vs/editor/common/core/editOperation';
A
Alex Dima 已提交
16
import { Range, IRange } from 'vs/editor/common/core/range';
A
Alex Dima 已提交
17 18
import { Selection, ISelection } from 'vs/editor/common/core/selection';
import { IIdentifiedSingleEditOperation, IModel, EndOfLineSequence, ICommonCodeEditor } from 'vs/editor/common/editorCommon';
J
Johannes Rieken 已提交
19
import { IProgressRunner } from 'vs/platform/progress/common/progress';
E
Erich Gamma 已提交
20

21
export interface IResourceEdit {
E
Erich Gamma 已提交
22 23 24
	resource: URI;
	range?: IRange;
	newText: string;
25
	newEol?: EndOfLineSequence;
26 27
}

E
Erich Gamma 已提交
28 29 30 31 32 33 34 35
interface IRecording {
	stop(): void;
	hasChanged(resource: URI): boolean;
	allChanges(): IFileChange[];
}

class ChangeRecorder {

36
	private _fileService: IFileService;
E
Erich Gamma 已提交
37

38
	constructor(fileService?: IFileService) {
39
		this._fileService = fileService;
E
Erich Gamma 已提交
40 41 42 43
	}

	public start(): IRecording {

44
		const changes: IStringDictionary<IFileChange[]> = Object.create(null);
E
Erich Gamma 已提交
45

46 47 48 49
		let stop: IDisposable;
		if (this._fileService) {
			stop = this._fileService.onFileChanges((event) => {
				event.changes.forEach(change => {
E
Erich Gamma 已提交
50

51 52
					const key = String(change.resource);
					let array = changes[key];
E
Erich Gamma 已提交
53

54 55 56
					if (!array) {
						changes[key] = array = [];
					}
E
Erich Gamma 已提交
57

58 59
					array.push(change);
				});
E
Erich Gamma 已提交
60
			});
61
		}
E
Erich Gamma 已提交
62 63

		return {
64
			stop: () => { return stop && stop.dispose(); },
E
Erich Gamma 已提交
65
			hasChanged: (resource: URI) => !!changes[resource.toString()],
J
Johannes Rieken 已提交
66
			allChanges: () => flatten(values(changes))
E
Erich Gamma 已提交
67 68 69 70
		};
	}
}

71
class EditTask implements IDisposable {
E
Erich Gamma 已提交
72

73 74
	private _initialSelections: Selection[];
	private _endCursorSelection: Selection;
J
Joao Moreno 已提交
75 76
	private get _model(): IModel { return this._modelReference.object.textEditorModel; }
	private _modelReference: IReference<ITextEditorModel>;
E
Erich Gamma 已提交
77
	private _edits: IIdentifiedSingleEditOperation[];
78
	private _newEol: EndOfLineSequence;
E
Erich Gamma 已提交
79

J
Joao Moreno 已提交
80
	constructor(modelReference: IReference<ITextEditorModel>) {
E
Erich Gamma 已提交
81
		this._endCursorSelection = null;
J
Joao Moreno 已提交
82
		this._modelReference = modelReference;
E
Erich Gamma 已提交
83 84 85 86
		this._edits = [];
	}

	public addEdit(edit: IResourceEdit): void {
87

88
		if (typeof edit.newEol === 'number') {
J
Johannes Rieken 已提交
89
			// honor eol-change
90
			this._newEol = edit.newEol;
E
Erich Gamma 已提交
91
		}
J
Johannes Rieken 已提交
92 93 94

		if (edit.range || edit.newText) {
			// create edit operation
A
Alex Dima 已提交
95
			let range: Range;
J
Johannes Rieken 已提交
96 97 98
			if (!edit.range) {
				range = this._model.getFullModelRange();
			} else {
A
Alex Dima 已提交
99
				range = Range.lift(edit.range);
J
Johannes Rieken 已提交
100
			}
A
Alex Dima 已提交
101
			this._edits.push(EditOperation.replaceMove(range, edit.newText));
J
Johannes Rieken 已提交
102
		}
E
Erich Gamma 已提交
103 104 105
	}

	public apply(): void {
106
		if (this._edits.length > 0) {
J
Johannes Rieken 已提交
107 108 109 110 111 112 113 114 115

			this._edits = this._edits.map((value, index) => ({ value, index })).sort((a, b) => {
				let ret = Range.compareRangesUsingStarts(a.value.range, b.value.range);
				if (ret === 0) {
					ret = a.index - b.index;
				}
				return ret;
			}).map(element => element.value);

116 117 118 119 120
			this._initialSelections = this._getInitialSelections();
			this._model.pushEditOperations(this._initialSelections, this._edits, (edits) => this._getEndCursorSelections(edits));
		}
		if (this._newEol !== undefined) {
			this._model.setEOL(this._newEol);
E
Erich Gamma 已提交
121 122 123
		}
	}

124
	protected _getInitialSelections(): Selection[] {
125 126
		const firstRange = this._edits[0].range;
		const initialSelection = new Selection(
E
Erich Gamma 已提交
127 128 129 130 131 132 133 134
			firstRange.startLineNumber,
			firstRange.startColumn,
			firstRange.endLineNumber,
			firstRange.endColumn
		);
		return [initialSelection];
	}

J
Johannes Rieken 已提交
135
	private _getEndCursorSelections(inverseEditOperations: IIdentifiedSingleEditOperation[]): Selection[] {
136 137 138 139 140
		let relevantEditIndex = 0;
		for (let i = 0; i < inverseEditOperations.length; i++) {
			const editRange = inverseEditOperations[i].range;
			for (let j = 0; j < this._initialSelections.length; j++) {
				const selectionRange = this._initialSelections[j];
E
Erich Gamma 已提交
141 142 143 144 145 146 147
				if (Range.areIntersectingOrTouching(editRange, selectionRange)) {
					relevantEditIndex = i;
					break;
				}
			}
		}

148
		const srcRange = inverseEditOperations[relevantEditIndex].range;
A
Alex Dima 已提交
149
		this._endCursorSelection = new Selection(
E
Erich Gamma 已提交
150 151 152 153 154 155 156 157
			srcRange.endLineNumber,
			srcRange.endColumn,
			srcRange.endLineNumber,
			srcRange.endColumn
		);
		return [this._endCursorSelection];
	}

158
	public getEndCursorSelection(): Selection {
E
Erich Gamma 已提交
159 160 161
		return this._endCursorSelection;
	}

162
	dispose() {
J
Joao Moreno 已提交
163 164 165 166
		if (this._model) {
			this._modelReference.dispose();
			this._modelReference = null;
		}
167
	}
E
Erich Gamma 已提交
168 169 170 171
}

class SourceModelEditTask extends EditTask {

J
Johannes Rieken 已提交
172
	private _knownInitialSelections: Selection[];
E
Erich Gamma 已提交
173

J
Joao Moreno 已提交
174 175
	constructor(modelReference: IReference<ITextEditorModel>, initialSelections: Selection[]) {
		super(modelReference);
E
Erich Gamma 已提交
176 177 178
		this._knownInitialSelections = initialSelections;
	}

179
	protected _getInitialSelections(): Selection[] {
E
Erich Gamma 已提交
180 181 182 183
		return this._knownInitialSelections;
	}
}

184
class BulkEditModel implements IDisposable {
E
Erich Gamma 已提交
185

186
	private _textModelResolverService: ITextModelResolverService;
E
Erich Gamma 已提交
187 188 189 190 191
	private _numberOfResourcesToModify: number = 0;
	private _numberOfChanges: number = 0;
	private _edits: IStringDictionary<IResourceEdit[]> = Object.create(null);
	private _tasks: EditTask[];
	private _sourceModel: URI;
192
	private _sourceSelections: Selection[];
E
Erich Gamma 已提交
193 194
	private _sourceModelTask: SourceModelEditTask;

195 196
	constructor(textModelResolverService: ITextModelResolverService, sourceModel: URI, sourceSelections: Selection[], edits: IResourceEdit[], private progress: IProgressRunner = null) {
		this._textModelResolverService = textModelResolverService;
E
Erich Gamma 已提交
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
		this._sourceModel = sourceModel;
		this._sourceSelections = sourceSelections;
		this._sourceModelTask = null;

		for (let edit of edits) {
			this._addEdit(edit);
		}
	}

	public resourcesCount(): number {
		return this._numberOfResourcesToModify;
	}

	public changeCount(): number {
		return this._numberOfChanges;
	}

	private _addEdit(edit: IResourceEdit): void {
215
		let array = this._edits[edit.resource.toString()];
E
Erich Gamma 已提交
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
		if (!array) {
			this._edits[edit.resource.toString()] = array = [];
			this._numberOfResourcesToModify += 1;
		}
		this._numberOfChanges += 1;
		array.push(edit);
	}

	public prepare(): TPromise<BulkEditModel> {

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

		this._tasks = [];
231
		const promises: TPromise<any>[] = [];
E
Erich Gamma 已提交
232

233 234 235 236
		if (this.progress) {
			this.progress.total(this._numberOfResourcesToModify * 2);
		}

E
Erich Gamma 已提交
237
		forEach(this._edits, entry => {
J
Joao Moreno 已提交
238
			const promise = this._textModelResolverService.createModelReference(URI.parse(entry.key)).then(ref => {
J
Joao Moreno 已提交
239
				const model = ref.object;
240

E
Erich Gamma 已提交
241 242 243 244
				if (!model || !model.textEditorModel) {
					throw new Error(`Cannot load file ${entry.key}`);
				}

245 246
				const textEditorModel = model.textEditorModel;
				let task: EditTask;
E
Erich Gamma 已提交
247

J
Johannes Rieken 已提交
248
				if (this._sourceModel && textEditorModel.uri.toString() === this._sourceModel.toString()) {
J
Joao Moreno 已提交
249
					this._sourceModelTask = new SourceModelEditTask(ref, this._sourceSelections);
E
Erich Gamma 已提交
250 251
					task = this._sourceModelTask;
				} else {
J
Joao Moreno 已提交
252
					task = new EditTask(ref);
E
Erich Gamma 已提交
253 254 255 256
				}

				entry.value.forEach(edit => task.addEdit(edit));
				this._tasks.push(task);
257 258 259
				if (this.progress) {
					this.progress.worked(1);
				}
E
Erich Gamma 已提交
260 261 262 263
			});
			promises.push(promise);
		});

264

E
Erich Gamma 已提交
265 266 267
		return TPromise.join(promises).then(_ => this);
	}

268
	public apply(): Selection {
269
		this._tasks.forEach(task => this.applyTask(task));
270
		let r: Selection = null;
E
Erich Gamma 已提交
271 272 273 274 275
		if (this._sourceModelTask) {
			r = this._sourceModelTask.getEndCursorSelection();
		}
		return r;
	}
276

277
	private applyTask(task: EditTask): void {
278 279 280 281 282
		task.apply();
		if (this.progress) {
			this.progress.worked(1);
		}
	}
283 284 285 286

	dispose(): void {
		this._tasks = dispose(this._tasks);
	}
E
Erich Gamma 已提交
287 288 289
}

export interface BulkEdit {
290
	progress(progress: IProgressRunner): void;
E
Erich Gamma 已提交
291 292
	add(edit: IResourceEdit[]): void;
	finish(): TPromise<ISelection>;
293
	ariaMessage(): string;
E
Erich Gamma 已提交
294 295
}

296 297
export function bulkEdit(textModelResolverService: ITextModelResolverService, editor: ICommonCodeEditor, edits: IResourceEdit[], fileService?: IFileService, progress: IProgressRunner = null): TPromise<any> {
	let bulk = createBulkEdit(textModelResolverService, editor, fileService);
E
Erich Gamma 已提交
298
	bulk.add(edits);
299
	bulk.progress(progress);
E
Erich Gamma 已提交
300 301 302
	return bulk.finish();
}

303
export function createBulkEdit(textModelResolverService: ITextModelResolverService, editor?: ICommonCodeEditor, fileService?: IFileService): BulkEdit {
E
Erich Gamma 已提交
304 305

	let all: IResourceEdit[] = [];
306
	let recording = new ChangeRecorder(fileService).start();
307 308 309
	let progressRunner: IProgressRunner;

	function progress(progress: IProgressRunner) {
J
Johannes Rieken 已提交
310
		progressRunner = progress;
311
	}
E
Erich Gamma 已提交
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329

	function add(edits: IResourceEdit[]): void {
		all.push(...edits);
	}

	function getConcurrentEdits() {
		let names: string[];
		for (let edit of all) {
			if (recording.hasChanged(edit.resource)) {
				if (!names) {
					names = [];
				}
				names.push(edit.resource.fsPath);
			}
		}
		if (names) {
			return nls.localize('conflict', "These files have changed in the meantime: {0}", names.join(', '));
		}
M
Matt Bierner 已提交
330
		return undefined;
E
Erich Gamma 已提交
331 332 333 334 335
	}

	function finish(): TPromise<ISelection> {

		if (all.length === 0) {
336
			return TPromise.as(undefined);
E
Erich Gamma 已提交
337 338 339 340
		}

		let concurrentEdits = getConcurrentEdits();
		if (concurrentEdits) {
341
			return TPromise.wrapError<ISelection>(concurrentEdits);
E
Erich Gamma 已提交
342 343 344
		}

		let uri: URI;
345
		let selections: Selection[];
E
Erich Gamma 已提交
346

347
		if (editor && editor.getModel()) {
348
			uri = editor.getModel().uri;
E
Erich Gamma 已提交
349 350 351
			selections = editor.getSelections();
		}

352
		const model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner);
E
Erich Gamma 已提交
353 354 355 356 357 358 359 360 361

		return model.prepare().then(_ => {

			let concurrentEdits = getConcurrentEdits();
			if (concurrentEdits) {
				throw new Error(concurrentEdits);
			}

			recording.stop();
362 363 364 365

			const result = model.apply();
			model.dispose();
			return result;
E
Erich Gamma 已提交
366 367 368
		});
	}

369 370 371 372 373 374 375 376 377 378 379 380
	function ariaMessage(): string {
		let editCount = all.length;
		let resourceCount = size(groupBy(all, edit => edit.resource.toString()));
		if (editCount === 0) {
			return nls.localize('summary.0', "Made no edits");
		} else if (editCount > 1 && resourceCount > 1) {
			return nls.localize('summary.nm', "Made {0} text edits in {1} files", editCount, resourceCount);
		} else {
			return nls.localize('summary.n0', "Made {0} text edits in one file", editCount, resourceCount);
		}
	}

E
Erich Gamma 已提交
381
	return {
382
		progress,
E
Erich Gamma 已提交
383
		add,
384 385
		finish,
		ariaMessage
A
tslint  
Alex Dima 已提交
386
	};
E
Erich Gamma 已提交
387
}