restructure bulk edit service, https://github.com/microsoft/vscode/issues/91383
create separate files for text and file operation
This commit is contained in:
parent
f816c23b22
commit
e3b9caa935
|
@ -3,243 +3,20 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
import { dispose, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
|
||||
import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack';
|
||||
|
||||
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
|
||||
|
||||
class ModelEditTask implements IDisposable {
|
||||
|
||||
public readonly model: ITextModel;
|
||||
|
||||
protected _edits: IIdentifiedSingleEditOperation[];
|
||||
private _expectedModelVersionId: number | undefined;
|
||||
protected _newEol: EndOfLineSequence | undefined;
|
||||
|
||||
constructor(private readonly _modelReference: IReference<IResolvedTextEditorModel>) {
|
||||
this.model = this._modelReference.object.textEditorModel;
|
||||
this._edits = [];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._modelReference.dispose();
|
||||
}
|
||||
|
||||
addEdit(resourceEdit: WorkspaceTextEdit): void {
|
||||
this._expectedModelVersionId = resourceEdit.modelVersionId;
|
||||
const { edit } = resourceEdit;
|
||||
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
range = this.model.getFullModelRange();
|
||||
} else {
|
||||
range = Range.lift(edit.range);
|
||||
}
|
||||
this._edits.push(EditOperation.replaceMove(range, edit.text));
|
||||
}
|
||||
|
||||
validate(): ValidationResult {
|
||||
if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) {
|
||||
return { canApply: true };
|
||||
}
|
||||
return { canApply: false, reason: this.model.uri };
|
||||
}
|
||||
|
||||
getBeforeCursorState(): Selection[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
if (this._edits.length > 0) {
|
||||
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
|
||||
this.model.pushEditOperations(null, this._edits, () => null);
|
||||
}
|
||||
if (this._newEol !== undefined) {
|
||||
this.model.pushEOL(this._newEol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EditorEditTask extends ModelEditTask {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
|
||||
constructor(modelReference: IReference<IResolvedTextEditorModel>, editor: ICodeEditor) {
|
||||
super(modelReference);
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
getBeforeCursorState(): Selection[] | null {
|
||||
return this._editor.getSelections();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (this._editor.hasModel()) {
|
||||
this._editor.getModel().pushEOL(this._newEol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BulkEditModel implements IDisposable {
|
||||
|
||||
private _edits = new Map<string, WorkspaceTextEdit[]>();
|
||||
private _tasks: ModelEditTask[] | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _label: string | undefined,
|
||||
private readonly _editor: ICodeEditor | undefined,
|
||||
private readonly _progress: IProgress<void>,
|
||||
edits: WorkspaceTextEdit[],
|
||||
@IEditorWorkerService private readonly _editorWorker: IEditorWorkerService,
|
||||
@ITextModelService private readonly _textModelResolverService: ITextModelService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService
|
||||
) {
|
||||
edits.forEach(this._addEdit, this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._tasks) {
|
||||
dispose(this._tasks);
|
||||
}
|
||||
}
|
||||
|
||||
private _addEdit(edit: WorkspaceTextEdit): void {
|
||||
let array = this._edits.get(edit.resource.toString());
|
||||
if (!array) {
|
||||
array = [];
|
||||
this._edits.set(edit.resource.toString(), array);
|
||||
}
|
||||
array.push(edit);
|
||||
}
|
||||
|
||||
async prepare(): Promise<BulkEditModel> {
|
||||
|
||||
if (this._tasks) {
|
||||
throw new Error('illegal state - already prepared');
|
||||
}
|
||||
|
||||
this._tasks = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
for (let [key, value] of this._edits) {
|
||||
const promise = this._textModelResolverService.createModelReference(URI.parse(key)).then(async ref => {
|
||||
let task: ModelEditTask;
|
||||
let makeMinimal = false;
|
||||
if (this._editor && this._editor.hasModel() && this._editor.getModel().uri.toString() === ref.object.textEditorModel.uri.toString()) {
|
||||
task = new EditorEditTask(ref, this._editor);
|
||||
makeMinimal = true;
|
||||
} else {
|
||||
task = new ModelEditTask(ref);
|
||||
}
|
||||
|
||||
for (const edit of value) {
|
||||
if (makeMinimal) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
task.addEdit(edit);
|
||||
}
|
||||
}
|
||||
|
||||
this._tasks!.push(task);
|
||||
this._progress.report(undefined);
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
validate(): ValidationResult {
|
||||
for (const task of this._tasks!) {
|
||||
const result = task.validate();
|
||||
if (!result.canApply) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { canApply: true };
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
const tasks = this._tasks!;
|
||||
|
||||
if (tasks.length === 1) {
|
||||
// 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(
|
||||
this._label || localize('workspaceEdit', "Workspace Edit"),
|
||||
tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState()))
|
||||
);
|
||||
this._undoRedoService.pushElement(multiModelEditStackElement);
|
||||
|
||||
for (const task of tasks) {
|
||||
task.apply();
|
||||
this._progress.report(undefined);
|
||||
}
|
||||
|
||||
multiModelEditStackElement.close();
|
||||
}
|
||||
}
|
||||
import { BulkTextEdits } from 'vs/workbench/services/bulkEdit/browser/bulkTextEdits';
|
||||
import { BulkFileEdits } from 'vs/workbench/services/bulkEdit/browser/bulkFileEdits';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
|
||||
type Edit = WorkspaceFileEdit | WorkspaceTextEdit;
|
||||
|
||||
|
@ -257,10 +34,6 @@ class BulkEdit {
|
|||
edits: Edit[],
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) {
|
||||
this._label = label;
|
||||
this._editor = editor;
|
||||
|
@ -282,7 +55,7 @@ class BulkEdit {
|
|||
|
||||
async perform(): Promise<void> {
|
||||
|
||||
let seen = new Set<string>();
|
||||
let seen = new ResourceMap<true>();
|
||||
let total = 0;
|
||||
|
||||
const groups: Edit[][] = [];
|
||||
|
@ -299,8 +72,8 @@ class BulkEdit {
|
|||
|
||||
if (WorkspaceFileEdit.is(edit)) {
|
||||
total += 1;
|
||||
} else if (!seen.has(edit.resource.toString())) {
|
||||
seen.add(edit.resource.toString());
|
||||
} else if (!seen.has(edit.resource)) {
|
||||
seen.set(edit.resource, true);
|
||||
total += 2;
|
||||
}
|
||||
}
|
||||
|
@ -323,55 +96,14 @@ class BulkEdit {
|
|||
|
||||
private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress<void>) {
|
||||
this._logService.debug('_performFileEdits', JSON.stringify(edits));
|
||||
for (const edit of edits) {
|
||||
progress.report(undefined);
|
||||
|
||||
let options = edit.options || {};
|
||||
|
||||
if (edit.newUri && edit.oldUri) {
|
||||
// rename
|
||||
if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
|
||||
continue; // not overwriting, but ignoring, and the target file exists
|
||||
}
|
||||
await this._workingCopyFileService.move(edit.oldUri, edit.newUri, options.overwrite);
|
||||
|
||||
} else if (!edit.newUri && edit.oldUri) {
|
||||
// delete file
|
||||
if (await this._fileService.exists(edit.oldUri)) {
|
||||
let useTrash = this._configurationService.getValue<boolean>('files.enableTrash');
|
||||
if (useTrash && !(this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) {
|
||||
useTrash = false; // not supported by provider
|
||||
}
|
||||
await this._workingCopyFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive });
|
||||
} else if (!options.ignoreIfNotExists) {
|
||||
throw new Error(`${edit.oldUri} does not exist and can not be deleted`);
|
||||
}
|
||||
} else if (edit.newUri && !edit.oldUri) {
|
||||
// create file
|
||||
if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
|
||||
continue; // not overwriting, but ignoring, and the target file exists
|
||||
}
|
||||
await this._textFileService.create(edit.newUri, undefined, { overwrite: options.overwrite });
|
||||
}
|
||||
}
|
||||
const model = this._instaService.createInstance(BulkFileEdits, progress, edits);
|
||||
await model.apply();
|
||||
}
|
||||
|
||||
private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress<void>): Promise<void> {
|
||||
this._logService.debug('_performTextEdits', JSON.stringify(edits));
|
||||
|
||||
const model = this._instaService.createInstance(BulkEditModel, this._label, this._editor, progress, edits);
|
||||
|
||||
await model.prepare();
|
||||
|
||||
// this._throwIfConflicts(conflicts);
|
||||
const validationResult = model.validate();
|
||||
if (validationResult.canApply === false) {
|
||||
model.dispose();
|
||||
throw new Error(`${validationResult.reason.toString()} has changed in the meantime`);
|
||||
}
|
||||
|
||||
model.apply();
|
||||
model.dispose();
|
||||
const model = this._instaService.createInstance(BulkTextEdits, this._label, this._editor, progress, edits);
|
||||
await model.apply();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,7 +116,6 @@ export class BulkEditService implements IBulkEditService {
|
|||
constructor(
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
) { }
|
||||
|
||||
|
@ -413,18 +144,6 @@ export class BulkEditService implements IBulkEditService {
|
|||
|
||||
const { edits } = edit;
|
||||
let codeEditor = options?.editor;
|
||||
|
||||
// First check if loaded models were not changed in the meantime
|
||||
for (const edit of edits) {
|
||||
if (!WorkspaceFileEdit.is(edit) && typeof edit.modelVersionId === 'number') {
|
||||
let model = this._modelService.getModel(edit.resource);
|
||||
if (model && model.getVersionId() !== edit.modelVersionId) {
|
||||
// model changed in the meantime
|
||||
return Promise.reject(new Error(`${model.uri.toString()} has changed in the meantime`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to find code editor
|
||||
if (!codeEditor) {
|
||||
let candidate = this._editorService.activeTextEditorControl;
|
||||
|
|
58
src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts
Normal file
58
src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { WorkspaceFileEdit } from 'vs/editor/common/modes';
|
||||
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
import { IProgress } from 'vs/platform/progress/common/progress';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
|
||||
export class BulkFileEdits {
|
||||
|
||||
constructor(
|
||||
private readonly _progress: IProgress<void>,
|
||||
private readonly _edits: WorkspaceFileEdit[],
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) { }
|
||||
|
||||
async apply(): Promise<void> {
|
||||
for (const edit of this._edits) {
|
||||
this._progress.report(undefined);
|
||||
|
||||
const options = edit.options || {};
|
||||
|
||||
if (edit.newUri && edit.oldUri) {
|
||||
// rename
|
||||
if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
|
||||
continue; // not overwriting, but ignoring, and the target file exists
|
||||
}
|
||||
await this._workingCopyFileService.move(edit.oldUri, edit.newUri, options.overwrite);
|
||||
|
||||
} else if (!edit.newUri && edit.oldUri) {
|
||||
// delete file
|
||||
if (await this._fileService.exists(edit.oldUri)) {
|
||||
let useTrash = this._configurationService.getValue<boolean>('files.enableTrash');
|
||||
if (useTrash && !(this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) {
|
||||
useTrash = false; // not supported by provider
|
||||
}
|
||||
await this._workingCopyFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive });
|
||||
} else if (!options.ignoreIfNotExists) {
|
||||
throw new Error(`${edit.oldUri} does not exist and can not be deleted`);
|
||||
}
|
||||
} else if (edit.newUri && !edit.oldUri) {
|
||||
// create file
|
||||
if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
|
||||
continue; // not overwriting, but ignoring, and the target file exists
|
||||
}
|
||||
await this._textFileService.create(edit.newUri, undefined, { overwrite: options.overwrite });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
246
src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts
Normal file
246
src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts
Normal file
|
@ -0,0 +1,246 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
|
||||
import { WorkspaceTextEdit } from 'vs/editor/common/modes';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IProgress } from 'vs/platform/progress/common/progress';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
|
||||
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
|
||||
|
||||
class ModelEditTask implements IDisposable {
|
||||
|
||||
readonly model: ITextModel;
|
||||
|
||||
private _expectedModelVersionId: number | undefined;
|
||||
protected _edits: IIdentifiedSingleEditOperation[];
|
||||
protected _newEol: EndOfLineSequence | undefined;
|
||||
|
||||
constructor(private readonly _modelReference: IReference<IResolvedTextEditorModel>) {
|
||||
this.model = this._modelReference.object.textEditorModel;
|
||||
this._edits = [];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._modelReference.dispose();
|
||||
}
|
||||
|
||||
addEdit(resourceEdit: WorkspaceTextEdit): void {
|
||||
this._expectedModelVersionId = resourceEdit.modelVersionId;
|
||||
const { edit } = resourceEdit;
|
||||
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
range = this.model.getFullModelRange();
|
||||
} else {
|
||||
range = Range.lift(edit.range);
|
||||
}
|
||||
this._edits.push(EditOperation.replaceMove(range, edit.text));
|
||||
}
|
||||
|
||||
validate(): ValidationResult {
|
||||
if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) {
|
||||
return { canApply: true };
|
||||
}
|
||||
return { canApply: false, reason: this.model.uri };
|
||||
}
|
||||
|
||||
getBeforeCursorState(): Selection[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
if (this._edits.length > 0) {
|
||||
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
|
||||
this.model.pushEditOperations(null, this._edits, () => null);
|
||||
}
|
||||
if (this._newEol !== undefined) {
|
||||
this.model.pushEOL(this._newEol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EditorEditTask extends ModelEditTask {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
|
||||
constructor(modelReference: IReference<IResolvedTextEditorModel>, editor: ICodeEditor) {
|
||||
super(modelReference);
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
getBeforeCursorState(): Selection[] | null {
|
||||
return this._editor.getSelections();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (this._editor.hasModel()) {
|
||||
this._editor.getModel().pushEOL(this._newEol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BulkTextEdits {
|
||||
|
||||
private readonly _edits = new ResourceMap<WorkspaceTextEdit[]>();
|
||||
|
||||
constructor(
|
||||
private readonly _label: string | undefined,
|
||||
private readonly _editor: ICodeEditor | undefined,
|
||||
private readonly _progress: IProgress<void>,
|
||||
edits: WorkspaceTextEdit[],
|
||||
@IEditorWorkerService private readonly _editorWorker: IEditorWorkerService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ITextModelService private readonly _textModelResolverService: ITextModelService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService
|
||||
) {
|
||||
|
||||
for (const edit of edits) {
|
||||
let array = this._edits.get(edit.resource);
|
||||
if (!array) {
|
||||
array = [];
|
||||
this._edits.set(edit.resource, array);
|
||||
}
|
||||
array.push(edit);
|
||||
}
|
||||
}
|
||||
|
||||
private _validateBeforePrepare(): void {
|
||||
// First check if loaded models were not changed in the meantime
|
||||
for (const array of this._edits.values()) {
|
||||
for (let edit of array) {
|
||||
if (typeof edit.modelVersionId === 'number') {
|
||||
let model = this._modelService.getModel(edit.resource);
|
||||
if (model && model.getVersionId() !== edit.modelVersionId) {
|
||||
// model changed in the meantime
|
||||
throw new Error(`${model.uri.toString()} has changed in the meantime`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _createEditsTasks(): Promise<ModelEditTask[]> {
|
||||
|
||||
const tasks: ModelEditTask[] = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
for (let [key, value] of this._edits) {
|
||||
const promise = this._textModelResolverService.createModelReference(key).then(async ref => {
|
||||
let task: ModelEditTask;
|
||||
let makeMinimal = false;
|
||||
if (this._editor?.getModel()?.uri.toString() === ref.object.textEditorModel.uri.toString()) {
|
||||
task = new EditorEditTask(ref, this._editor);
|
||||
makeMinimal = true;
|
||||
} else {
|
||||
task = new ModelEditTask(ref);
|
||||
}
|
||||
|
||||
for (const edit of value) {
|
||||
if (makeMinimal) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
task.addEdit(edit);
|
||||
}
|
||||
}
|
||||
|
||||
tasks.push(task);
|
||||
this._progress.report(undefined);
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private _validateTasks(tasks: ModelEditTask[]): ValidationResult {
|
||||
for (const task of tasks) {
|
||||
const result = task.validate();
|
||||
if (!result.canApply) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { canApply: true };
|
||||
}
|
||||
|
||||
async apply(): Promise<void> {
|
||||
|
||||
this._validateBeforePrepare();
|
||||
const tasks = await this._createEditsTasks();
|
||||
|
||||
try {
|
||||
|
||||
const validation = this._validateTasks(tasks);
|
||||
if (!validation.canApply) {
|
||||
throw new Error(`${validation.reason.toString()} has changed in the meantime`);
|
||||
}
|
||||
if (tasks.length === 1) {
|
||||
// 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);
|
||||
}
|
||||
} else {
|
||||
// prepare multi model undo element
|
||||
const multiModelEditStackElement = new MultiModelEditStackElement(
|
||||
this._label || localize('workspaceEdit', "Workspace Edit"),
|
||||
tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState()))
|
||||
);
|
||||
this._undoRedoService.pushElement(multiModelEditStackElement);
|
||||
for (const task of tasks) {
|
||||
task.apply();
|
||||
this._progress.report(undefined);
|
||||
}
|
||||
multiModelEditStackElement.close();
|
||||
}
|
||||
|
||||
} finally {
|
||||
dispose(tasks);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue