Skip to content

Commit 6803935

Browse files
committed
bulk: have UX and model checked states, propagate check states to child edits
1 parent 09b83d3 commit 6803935

4 files changed

Lines changed: 235 additions & 123 deletions

File tree

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export class BulkEditPane extends ViewPane {
183183
this._setTreeInput(input);
184184

185185
// refresh when check state changes
186-
this._sessionDisposables.add(input.onDidChangeCheckedState(() => {
186+
this._sessionDisposables.add(input.checked.onDidChange(() => {
187187
this._tree.updateChildren();
188188
}));
189189
});
@@ -238,10 +238,8 @@ export class BulkEditPane extends ViewPane {
238238

239239
toggleChecked() {
240240
const [first] = this._tree.getFocus();
241-
if (first instanceof FileElement) {
242-
first.edit.updateChecked(!first.edit.isChecked());
243-
} else if (first instanceof TextEditElement && first.parent.edit.isChecked()) {
244-
first.edit.updateChecked(!first.edit.isChecked());
241+
if ((first instanceof FileElement || first instanceof TextEditElement) && !first.isDisabled()) {
242+
first.setChecked(!first.isChecked());
245243
}
246244
}
247245

@@ -277,7 +275,7 @@ export class BulkEditPane extends ViewPane {
277275

278276
private _done(accept: boolean): void {
279277
if (this._currentResolve) {
280-
this._currentResolve(accept ? this._currentInput?.asWorkspaceEdit() : undefined);
278+
this._currentResolve(accept ? this._currentInput?.getWorkspaceEdit() : undefined);
281279
this._currentInput = undefined;
282280
}
283281
this._setState(State.Message);

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

Lines changed: 98 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
88
import { IModeService } from 'vs/editor/common/services/modeService';
99
import { IModelService } from 'vs/editor/common/services/modelService';
1010
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
11-
import { WorkspaceEdit, TextEdit, WorkspaceTextEdit, WorkspaceFileEdit, WorkspaceEditMetadata } from 'vs/editor/common/modes';
11+
import { WorkspaceEdit, WorkspaceTextEdit, WorkspaceFileEdit, WorkspaceEditMetadata } from 'vs/editor/common/modes';
1212
import { DisposableStore } from 'vs/base/common/lifecycle';
1313
import { mergeSort, coalesceInPlace } from 'vs/base/common/arrays';
1414
import { Range } from 'vs/editor/common/core/range';
@@ -21,34 +21,35 @@ import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflic
2121
import { values } from 'vs/base/common/map';
2222
import { localize } from 'vs/nls';
2323

24-
export class CheckedObject {
24+
export class CheckedStates<T extends object> {
2525

26-
private _checked: boolean = true;
26+
private readonly _states = new WeakMap<T, boolean>();
2727

28-
constructor(protected _emitter: Emitter<any>) { }
28+
private readonly _onDidChange = new Emitter<T>();
29+
readonly onDidChange: Event<T> = this._onDidChange.event;
2930

30-
updateChecked(checked: boolean) {
31-
if (this._checked !== checked) {
32-
this._checked = checked;
33-
this._emitter.fire(this);
34-
}
31+
dispose(): void {
32+
this._onDidChange.dispose();
33+
}
34+
35+
isChecked(obj: T): boolean {
36+
return this._states.get(obj) ?? false;
3537
}
3638

37-
isChecked(): boolean {
38-
return this._checked;
39+
updateChecked(obj: T, value: boolean): void {
40+
if (this._states.get(obj) !== value) {
41+
this._states.set(obj, value);
42+
this._onDidChange.fire(obj);
43+
}
3944
}
4045
}
4146

42-
export class BulkTextEdit extends CheckedObject {
47+
export class BulkTextEdit {
4348

4449
constructor(
4550
readonly parent: BulkFileOperation,
46-
readonly textEdit: WorkspaceTextEdit,
47-
emitter: Emitter<BulkFileOperation | BulkTextEdit>
48-
) {
49-
super(emitter);
50-
this.updateChecked(!textEdit.metadata?.needsConfirmation);
51-
}
51+
readonly textEdit: WorkspaceTextEdit
52+
) { }
5253
}
5354

5455
export const enum BulkFileOperationType {
@@ -58,7 +59,7 @@ export const enum BulkFileOperationType {
5859
Rename = 8,
5960
}
6061

61-
export class BulkFileOperation extends CheckedObject {
62+
export class BulkFileOperation {
6263

6364
type: BulkFileOperationType = 0;
6465
textEdits: BulkTextEdit[] = [];
@@ -68,24 +69,16 @@ export class BulkFileOperation extends CheckedObject {
6869
constructor(
6970
readonly uri: URI,
7071
readonly parent: BulkFileOperations
71-
) {
72-
super(parent._onDidChangeCheckedState);
73-
}
72+
) { }
7473

7574
addEdit(index: number, type: BulkFileOperationType, edit: WorkspaceTextEdit | WorkspaceFileEdit, ) {
7675
this.type |= type;
7776
this.originalEdits.set(index, edit);
7877
if (WorkspaceTextEdit.is(edit)) {
79-
this.textEdits.push(new BulkTextEdit(this, edit, this._emitter));
78+
this.textEdits.push(new BulkTextEdit(this, edit));
8079

81-
} else {
82-
if (type === BulkFileOperationType.Rename) {
83-
this.newUri = edit.newUri;
84-
}
85-
// one needsConfirmation is enough to uncheck this item
86-
if (this.isChecked() && edit.metadata?.needsConfirmation) {
87-
this.updateChecked(false);
88-
}
80+
} else if (type === BulkFileOperationType.Rename) {
81+
this.newUri = edit.newUri;
8982
}
9083
}
9184
}
@@ -118,8 +111,7 @@ export class BulkFileOperations {
118111
return await result._init();
119112
}
120113

121-
readonly _onDidChangeCheckedState = new Emitter<BulkFileOperation | BulkTextEdit>();
122-
readonly onDidChangeCheckedState: Event<BulkFileOperation | BulkTextEdit> = this._onDidChangeCheckedState.event;
114+
readonly checked = new CheckedStates<WorkspaceFileEdit | WorkspaceTextEdit>();
123115

124116
readonly fileOperations: BulkFileOperation[] = [];
125117
readonly categories: BulkCategory[] = [];
@@ -131,23 +123,10 @@ export class BulkFileOperations {
131123
@IInstantiationService instaService: IInstantiationService,
132124
) {
133125
this.conflicts = instaService.createInstance(ConflictDetector, _bulkEdit);
134-
135-
// reflect checked-state for files in all categories the file occurs in
136-
this._onDidChangeCheckedState.event(e => {
137-
if (e instanceof BulkFileOperation && e.parent) {
138-
for (let item of this.categories) {
139-
for (let file of item.fileOperations) {
140-
if (file.uri.toString() === e.uri.toString()) {
141-
file.updateChecked(e.isChecked());
142-
}
143-
}
144-
}
145-
}
146-
});
147126
}
148127

149128
dispose(): void {
150-
this._onDidChangeCheckedState.dispose();
129+
this.checked.dispose();
151130
this.conflicts.dispose();
152131
}
153132

@@ -163,6 +142,9 @@ export class BulkFileOperations {
163142
let uri: URI;
164143
let type: BulkFileOperationType;
165144

145+
// store inital checked state
146+
this.checked.updateChecked(edit, !edit.metadata?.needsConfirmation);
147+
166148
if (WorkspaceTextEdit.is(edit)) {
167149
type = BulkFileOperationType.TextEdit;
168150
uri = edit.resource;
@@ -234,43 +216,79 @@ export class BulkFileOperations {
234216
return this;
235217
}
236218

237-
asWorkspaceEdit(): WorkspaceEdit {
219+
getWorkspaceEdit(): WorkspaceEdit {
238220
const result: WorkspaceEdit = { edits: [] };
239221
let allAccepted = true;
240-
for (let file of this.fileOperations) {
241222

242-
if (!file.isChecked()) {
243-
allAccepted = false;
223+
for (let i = 0; i < this._bulkEdit.edits.length; i++) {
224+
const edit = this._bulkEdit.edits[i];
225+
if (this.checked.isChecked(edit)) {
226+
result.edits[i] = edit;
244227
continue;
245228
}
229+
allAccepted = false;
230+
}
246231

247-
const keyOfEdit = (edit: TextEdit) => JSON.stringify(edit);
248-
const checkedEdits = new Set<string>();
232+
if (allAccepted) {
233+
return this._bulkEdit;
234+
}
249235

250-
for (let edit of file.textEdits) {
251-
if (edit.isChecked()) {
252-
checkedEdits.add(keyOfEdit(edit.textEdit.edit));
253-
}
254-
}
236+
// not all edits have been accepted
237+
coalesceInPlace(result.edits);
238+
return result;
239+
}
255240

256-
file.originalEdits.forEach((value, idx) => {
241+
getFileEdits(uri: URI): IIdentifiedSingleEditOperation[] {
257242

258-
if (WorkspaceTextEdit.is(value) && !checkedEdits.has(keyOfEdit(value.edit))) {
259-
allAccepted = false;
260-
return;
261-
}
243+
for (let file of this.fileOperations) {
244+
if (file.uri.toString() === uri.toString()) {
262245

263-
result.edits[idx] = value;
246+
const result: IIdentifiedSingleEditOperation[] = [];
247+
let ignoreAll = false;
264248

265-
});
249+
file.originalEdits.forEach(edit => {
250+
251+
if (WorkspaceTextEdit.is(edit)) {
252+
if (this.checked.isChecked(edit)) {
253+
result.push(EditOperation.replaceMove(Range.lift(edit.edit.range), edit.edit.text));
254+
}
255+
256+
} else if (!this.checked.isChecked(edit)) {
257+
// UNCHECKED WorkspaceFileEdit disables all text edits
258+
ignoreAll = true;
259+
}
260+
});
261+
262+
if (ignoreAll) {
263+
return [];
264+
}
265+
266+
return mergeSort(
267+
result,
268+
(a, b) => Range.compareRangesUsingStarts(a.range, b.range)
269+
);
270+
}
266271
}
267-
if (!allAccepted) {
268-
// only return a new edit when something has changed
269-
coalesceInPlace(result.edits);
270-
return result;
272+
return [];
273+
}
274+
275+
getUriOfEdit(edit: WorkspaceFileEdit | WorkspaceTextEdit): URI {
276+
if (WorkspaceTextEdit.is(edit)) {
277+
return edit.resource;
271278
}
272-
return this._bulkEdit;
273279

280+
for (let file of this.fileOperations) {
281+
let found = false;
282+
file.originalEdits.forEach(value => {
283+
if (!found && value === edit) {
284+
found = true;
285+
}
286+
});
287+
if (found) {
288+
return file.uri;
289+
}
290+
}
291+
throw new Error('invalid edit');
274292
}
275293
}
276294

@@ -308,30 +326,26 @@ export class BulkEditPreviewProvider implements ITextModelContentProvider {
308326

309327
private async _init() {
310328
for (let operation of this._operations.fileOperations) {
311-
await this._applyTextEditsToPreviewModel(operation);
329+
await this._applyTextEditsToPreviewModel(operation.uri);
312330
}
313-
this._disposables.add(this._operations.onDidChangeCheckedState(element => {
314-
let operation = element instanceof BulkFileOperation ? element : element.parent;
315-
this._applyTextEditsToPreviewModel(operation);
331+
this._disposables.add(this._operations.checked.onDidChange(e => {
332+
const uri = this._operations.getUriOfEdit(e);
333+
this._applyTextEditsToPreviewModel(uri);
316334
}));
317335
}
318336

319-
private async _applyTextEditsToPreviewModel(operation: BulkFileOperation) {
320-
const model = await this._getOrCreatePreviewModel(operation.uri);
337+
private async _applyTextEditsToPreviewModel(uri: URI) {
338+
const model = await this._getOrCreatePreviewModel(uri);
321339

322340
// undo edits that have been done before
323341
let undoEdits = this._modelPreviewEdits.get(model.id);
324342
if (undoEdits) {
325343
model.applyEdits(undoEdits);
326344
}
327-
// compute new edits
328-
const newEdits = mergeSort(
329-
operation.textEdits.filter(edit => edit.isChecked() && edit.parent.isChecked()).map(edit => EditOperation.replaceMove(Range.lift(edit.textEdit.edit.range), edit.textEdit.edit.text)),
330-
(a, b) => Range.compareRangesUsingStarts(a.range, b.range)
331-
);
332-
// apply edits and keep undo edits
333-
undoEdits = model.applyEdits(newEdits);
334-
this._modelPreviewEdits.set(model.id, undoEdits);
345+
// apply new edits and keep (future) undo edits
346+
const newEdits = this._operations.getFileEdits(uri);
347+
const newUndoEdits = model.applyEdits(newEdits);
348+
this._modelPreviewEdits.set(model.id, newUndoEdits);
335349
}
336350

337351
private async _getOrCreatePreviewModel(uri: URI) {

0 commit comments

Comments
 (0)