Skip to content

Commit 0681925

Browse files
committed
refactor bulk edit service a little so that it is fit for new edit types, #105283
1 parent 466cc0f commit 0681925

File tree

18 files changed

+297
-293
lines changed

18 files changed

+297
-293
lines changed

src/vs/editor/browser/services/bulkEditService.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,64 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
7-
import { WorkspaceEdit } from 'vs/editor/common/modes';
7+
import { TextEdit, WorkspaceEdit, WorkspaceEditMetadata, WorkspaceFileEdit, WorkspaceFileEditOptions, WorkspaceTextEdit } from 'vs/editor/common/modes';
88
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
99
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
1010
import { IDisposable } from 'vs/base/common/lifecycle';
11+
import { URI } from 'vs/base/common/uri';
12+
import { isObject } from 'vs/base/common/types';
1113

1214
export const IBulkEditService = createDecorator<IBulkEditService>('IWorkspaceEditService');
1315

16+
function isWorkspaceFileEdit(thing: any): thing is WorkspaceFileEdit {
17+
return isObject(thing) && (Boolean((<WorkspaceFileEdit>thing).newUri) || Boolean((<WorkspaceFileEdit>thing).oldUri));
18+
}
19+
20+
function isWorkspaceTextEdit(thing: any): thing is WorkspaceTextEdit {
21+
return isObject(thing) && URI.isUri((<WorkspaceTextEdit>thing).resource) && isObject((<WorkspaceTextEdit>thing).edit);
22+
}
23+
24+
export class ResourceEdit {
25+
26+
protected constructor(readonly metadata?: WorkspaceEditMetadata) { }
27+
28+
static convert(edit: WorkspaceEdit): ResourceEdit[] {
29+
30+
31+
return edit.edits.map(edit => {
32+
if (isWorkspaceTextEdit(edit)) {
33+
return new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata);
34+
}
35+
if (isWorkspaceFileEdit(edit)) {
36+
return new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata);
37+
}
38+
throw new Error('Unsupported edit');
39+
});
40+
}
41+
}
42+
43+
export class ResourceTextEdit extends ResourceEdit {
44+
constructor(
45+
readonly resource: URI,
46+
readonly textEdit: TextEdit,
47+
readonly versionId?: number,
48+
readonly metadata?: WorkspaceEditMetadata
49+
) {
50+
super(metadata);
51+
}
52+
}
53+
54+
export class ResourceFileEdit extends ResourceEdit {
55+
constructor(
56+
readonly oldResource: URI | undefined,
57+
readonly newResource: URI | undefined,
58+
readonly options?: WorkspaceFileEditOptions,
59+
readonly metadata?: WorkspaceEditMetadata
60+
) {
61+
super(metadata);
62+
}
63+
}
64+
1465
export interface IBulkEditOptions {
1566
editor?: ICodeEditor;
1667
progress?: IProgress<IProgressStep>;
@@ -23,7 +74,7 @@ export interface IBulkEditResult {
2374
ariaSummary: string;
2475
}
2576

26-
export type IBulkEditPreviewHandler = (edit: WorkspaceEdit, options?: IBulkEditOptions) => Promise<WorkspaceEdit>;
77+
export type IBulkEditPreviewHandler = (edits: ResourceEdit[], options?: IBulkEditOptions) => Promise<ResourceEdit[]>;
2778

2879
export interface IBulkEditService {
2980
readonly _serviceBrand: undefined;
@@ -32,6 +83,5 @@ export interface IBulkEditService {
3283

3384
setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable;
3485

35-
apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise<IBulkEditResult>;
86+
apply(edit: ResourceEdit[], options?: IBulkEditOptions): Promise<IBulkEditResult>;
3687
}
37-

src/vs/editor/common/modes.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Color } from 'vs/base/common/color';
88
import { Event } from 'vs/base/common/event';
99
import { IMarkdownString } from 'vs/base/common/htmlContent';
1010
import { IDisposable } from 'vs/base/common/lifecycle';
11-
import { isObject } from 'vs/base/common/types';
1211
import { URI, UriComponents } from 'vs/base/common/uri';
1312
import { Position } from 'vs/editor/common/core/position';
1413
import { IRange, Range } from 'vs/editor/common/core/range';
@@ -1337,29 +1336,6 @@ export class FoldingRangeKind {
13371336
}
13381337
}
13391338

1340-
/**
1341-
* @internal
1342-
*/
1343-
export namespace WorkspaceFileEdit {
1344-
/**
1345-
* @internal
1346-
*/
1347-
export function is(thing: any): thing is WorkspaceFileEdit {
1348-
return isObject(thing) && (Boolean((<WorkspaceFileEdit>thing).newUri) || Boolean((<WorkspaceFileEdit>thing).oldUri));
1349-
}
1350-
}
1351-
1352-
/**
1353-
* @internal
1354-
*/
1355-
export namespace WorkspaceTextEdit {
1356-
/**
1357-
* @internal
1358-
*/
1359-
export function is(thing: any): thing is WorkspaceTextEdit {
1360-
return isObject(thing) && URI.isUri((<WorkspaceTextEdit>thing).resource) && isObject((<WorkspaceTextEdit>thing).edit);
1361-
}
1362-
}
13631339

13641340
export interface WorkspaceEditMetadata {
13651341
needsConfirmation: boolean;

src/vs/editor/contrib/codeAction/codeActionCommands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
1111
import { escapeRegExpCharacters } from 'vs/base/common/strings';
1212
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1313
import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
14-
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
14+
import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
1515
import { IPosition } from 'vs/editor/common/core/position';
1616
import { IEditorContribution } from 'vs/editor/common/editorCommon';
1717
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
@@ -163,7 +163,7 @@ export async function applyCodeAction(
163163
});
164164

165165
if (action.edit) {
166-
await bulkEditService.apply(action.edit, { editor, label: action.title });
166+
await bulkEditService.apply(ResourceEdit.convert(action.edit), { editor, label: action.title });
167167
}
168168

169169
if (action.command) {

src/vs/editor/contrib/rename/rename.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { MessageController } from 'vs/editor/contrib/message/messageController';
2222
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/browser/core/editorState';
2323
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
2424
import { INotificationService } from 'vs/platform/notification/common/notification';
25-
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
25+
import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
2626
import { URI } from 'vs/base/common/uri';
2727
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
2828
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
@@ -226,7 +226,7 @@ class RenameController implements IEditorContribution {
226226
return;
227227
}
228228

229-
this._bulkEditService.apply(renameResult, {
229+
this._bulkEditService.apply(ResourceEdit.convert(renameResult), {
230230
editor: this.editor,
231231
showPreview: inputFieldResult.wantsPreview,
232232
label: nls.localize('label', "Renaming '{0}'", loc?.text),

src/vs/editor/standalone/browser/simpleServices.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ import { OS, isLinux, isMacintosh } from 'vs/base/common/platform';
1313
import Severity from 'vs/base/common/severity';
1414
import { URI } from 'vs/base/common/uri';
1515
import { ICodeEditor, IDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
16-
import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
16+
import { IBulkEditOptions, IBulkEditResult, IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
1717
import { isDiffEditorConfigurationKey, isEditorConfigurationKey } from 'vs/editor/common/config/commonEditorConfig';
1818
import { EditOperation } from 'vs/editor/common/core/editOperation';
1919
import { IPosition, Position as Pos } from 'vs/editor/common/core/position';
2020
import { Range } from 'vs/editor/common/core/range';
2121
import { IEditor } from 'vs/editor/common/editorCommon';
22-
import { ITextModel, ITextSnapshot } from 'vs/editor/common/model';
23-
import { TextEdit, WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes';
22+
import { IIdentifiedSingleEditOperation, ITextModel, ITextSnapshot } from 'vs/editor/common/model';
2423
import { IModelService } from 'vs/editor/common/services/modelService';
2524
import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
2625
import { ITextResourceConfigurationService, ITextResourcePropertiesService, ITextResourceConfigurationChangeEvent } from 'vs/editor/common/services/textResourceConfigurationService';
@@ -665,42 +664,43 @@ export class SimpleBulkEditService implements IBulkEditService {
665664
return Disposable.None;
666665
}
667666

668-
apply(workspaceEdit: WorkspaceEdit, options?: IBulkEditOptions): Promise<IBulkEditResult> {
667+
async apply(edits: ResourceEdit[], _options?: IBulkEditOptions): Promise<IBulkEditResult> {
669668

670-
let edits = new Map<ITextModel, TextEdit[]>();
669+
const textEdits = new Map<ITextModel, IIdentifiedSingleEditOperation[]>();
671670

672-
if (workspaceEdit.edits) {
673-
for (let edit of workspaceEdit.edits) {
674-
if (!WorkspaceTextEdit.is(edit)) {
675-
return Promise.reject(new Error('bad edit - only text edits are supported'));
676-
}
677-
let model = this._modelService.getModel(edit.resource);
678-
if (!model) {
679-
return Promise.reject(new Error('bad edit - model not found'));
680-
}
681-
let array = edits.get(model);
682-
if (!array) {
683-
array = [];
684-
edits.set(model, array);
685-
}
686-
array.push(edit.edit);
671+
for (let edit of edits) {
672+
if (!(edit instanceof ResourceTextEdit)) {
673+
throw new Error('bad edit - only text edits are supported');
674+
}
675+
const model = this._modelService.getModel(edit.resource);
676+
if (!model) {
677+
throw new Error('bad edit - model not found');
678+
}
679+
if (typeof edit.versionId === 'number' && model.getVersionId() !== edit.versionId) {
680+
throw new Error('bad state - model changed in the meantime');
681+
}
682+
let array = textEdits.get(model);
683+
if (!array) {
684+
array = [];
685+
textEdits.set(model, array);
687686
}
687+
array.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), edit.textEdit.text));
688688
}
689689

690+
690691
let totalEdits = 0;
691692
let totalFiles = 0;
692-
edits.forEach((edits, model) => {
693+
for (const [model, edits] of textEdits) {
693694
model.pushStackElement();
694-
model.pushEditOperations([], edits.map((e) => EditOperation.replaceMove(Range.lift(e.range), e.text)), () => []);
695+
model.pushEditOperations([], edits, () => []);
695696
model.pushStackElement();
696697
totalFiles += 1;
697698
totalEdits += edits.length;
698-
});
699+
}
699700

700-
return Promise.resolve({
701-
selection: undefined,
701+
return {
702702
ariaSummary: strings.format(SimpleServicesNLS.bulkEditServiceSummary, totalEdits, totalFiles)
703-
});
703+
};
704704
}
705705
}
706706

src/vs/workbench/api/browser/mainThreadEditors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { disposed } from 'vs/base/common/errors';
88
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
99
import { equals as objectEquals } from 'vs/base/common/objects';
1010
import { URI, UriComponents } from 'vs/base/common/uri';
11-
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
11+
import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
1212
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
1313
import { IRange } from 'vs/editor/common/core/range';
1414
import { ISelection } from 'vs/editor/common/core/selection';
@@ -223,7 +223,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape {
223223

224224
$tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise<boolean> {
225225
const { edits } = reviveWorkspaceEditDto(dto)!;
226-
return this._bulkEditService.apply({ edits }).then(() => true, _err => false);
226+
return this._bulkEditService.apply(ResourceEdit.convert({ edits })).then(() => true, _err => false);
227227
}
228228

229229
$tryInsertSnippet(id: string, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise<boolean> {

src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import { localize } from 'vs/nls';
77
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
88
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
9-
import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService';
10-
import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes';
9+
import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
1110
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
1211
import { ILogService } from 'vs/platform/log/common/log';
1312
import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress';
@@ -16,22 +15,19 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
1615
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1716
import { BulkTextEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkTextEdits';
1817
import { BulkFileEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkFileEdits';
19-
import { ResourceMap } from 'vs/base/common/map';
20-
21-
type Edit = WorkspaceFileEdit | WorkspaceTextEdit;
2218

2319
class BulkEdit {
2420

2521
private readonly _label: string | undefined;
26-
private readonly _edits: Edit[] = [];
22+
private readonly _edits: ResourceEdit[] = [];
2723
private readonly _editor: ICodeEditor | undefined;
2824
private readonly _progress: IProgress<IProgressStep>;
2925

3026
constructor(
3127
label: string | undefined,
3228
editor: ICodeEditor | undefined,
3329
progress: IProgress<IProgressStep> | undefined,
34-
edits: Edit[],
30+
edits: ResourceEdit[],
3531
@IInstantiationService private readonly _instaService: IInstantiationService,
3632
@ILogService private readonly _logService: ILogService,
3733
) {
@@ -55,52 +51,44 @@ class BulkEdit {
5551

5652
async perform(): Promise<void> {
5753

58-
let seen = new ResourceMap<true>();
59-
let total = 0;
60-
61-
const groups: Edit[][] = [];
62-
let group: Edit[] | undefined;
63-
for (const edit of this._edits) {
64-
if (!group
65-
|| (WorkspaceFileEdit.is(group[0]) && !WorkspaceFileEdit.is(edit))
66-
|| (WorkspaceTextEdit.is(group[0]) && !WorkspaceTextEdit.is(edit))
67-
) {
68-
group = [];
69-
groups.push(group);
70-
}
71-
group.push(edit);
54+
if (this._edits.length === 0) {
55+
return;
56+
}
7257

73-
if (WorkspaceFileEdit.is(edit)) {
74-
total += 1;
75-
} else if (!seen.has(edit.resource)) {
76-
seen.set(edit.resource, true);
77-
total += 2;
58+
const ranges: number[] = [1];
59+
for (let i = 1; i < this._edits.length; i++) {
60+
if (Object.getPrototypeOf(this._edits[i - 1]) === Object.getPrototypeOf(this._edits[i])) {
61+
ranges[ranges.length - 1]++;
62+
} else {
63+
ranges.push(1);
7864
}
7965
}
8066

81-
// define total work and progress callback
82-
// for child operations
83-
this._progress.report({ total });
84-
67+
this._progress.report({ total: this._edits.length });
8568
const progress: IProgress<void> = { report: _ => this._progress.report({ increment: 1 }) };
8669

87-
// do it.
88-
for (const group of groups) {
89-
if (WorkspaceFileEdit.is(group[0])) {
90-
await this._performFileEdits(<WorkspaceFileEdit[]>group, progress);
70+
71+
let index = 0;
72+
for (let range of ranges) {
73+
const group = this._edits.slice(index, index + range);
74+
if (group[0] instanceof ResourceFileEdit) {
75+
await this._performFileEdits(<ResourceFileEdit[]>group, progress);
76+
} else if (group[0] instanceof ResourceTextEdit) {
77+
await this._performTextEdits(<ResourceTextEdit[]>group, progress);
9178
} else {
92-
await this._performTextEdits(<WorkspaceTextEdit[]>group, progress);
79+
console.log('UNKNOWN EDIT');
9380
}
81+
index = index + range;
9482
}
9583
}
9684

97-
private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress<void>) {
85+
private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress<void>) {
9886
this._logService.debug('_performFileEdits', JSON.stringify(edits));
9987
const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), progress, edits);
10088
await model.apply();
10189
}
10290

103-
private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress<void>): Promise<void> {
91+
private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress<void>): Promise<void> {
10492
this._logService.debug('_performTextEdits', JSON.stringify(edits));
10593
const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, progress, edits);
10694
await model.apply();
@@ -132,17 +120,17 @@ export class BulkEditService implements IBulkEditService {
132120
return Boolean(this._previewHandler);
133121
}
134122

135-
async apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise<IBulkEditResult> {
123+
async apply(edits: ResourceEdit[], options?: IBulkEditOptions): Promise<IBulkEditResult> {
136124

137-
if (edit.edits.length === 0) {
125+
if (edits.length === 0) {
138126
return { ariaSummary: localize('nothing', "Made no edits") };
139127
}
140128

141-
if (this._previewHandler && (options?.showPreview || edit.edits.some(value => value.metadata?.needsConfirmation))) {
142-
edit = await this._previewHandler(edit, options);
129+
if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) {
130+
edits = await this._previewHandler(edits, options);
143131
}
144132

145-
const { edits } = edit;
133+
// const { edits } = edit;
146134
let codeEditor = options?.editor;
147135
// try to find code editor
148136
if (!codeEditor) {

0 commit comments

Comments
 (0)