Skip to content

Commit f296715

Browse files
authored
Merge pull request microsoft#146320 from microsoft/joh/bulkEditSave
2 parents a616d41 + b5299e3 commit f296715

File tree

8 files changed

+158
-32
lines changed

8 files changed

+158
-32
lines changed

src/vs/base/common/map.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,67 @@ export class ResourceMap<T> implements Map<URI, T> {
845845
}
846846
}
847847

848+
export class ResourceSet implements Set<URI> {
849+
850+
readonly [Symbol.toStringTag]: string = 'ResourceSet';
851+
852+
private readonly _map: ResourceMap<URI>;
853+
854+
constructor(toKey?: ResourceMapKeyFn);
855+
constructor(entries: readonly URI[], toKey?: ResourceMapKeyFn);
856+
constructor(entriesOrKey?: readonly URI[] | ResourceMapKeyFn, toKey?: ResourceMapKeyFn) {
857+
if (!entriesOrKey || typeof entriesOrKey === 'function') {
858+
this._map = new ResourceMap(entriesOrKey);
859+
} else {
860+
this._map = new ResourceMap(toKey);
861+
entriesOrKey.forEach(this.add, this);
862+
}
863+
}
864+
865+
866+
get size(): number {
867+
return this._map.size;
868+
}
869+
870+
add(value: URI): this {
871+
this._map.set(value, value);
872+
return this;
873+
}
874+
875+
clear(): void {
876+
this._map.clear();
877+
}
878+
879+
delete(value: URI): boolean {
880+
return this._map.delete(value);
881+
}
882+
883+
forEach(callbackfn: (value: URI, value2: URI, set: Set<URI>) => void, thisArg?: any): void {
884+
this._map.forEach((_value, key) => callbackfn.call(thisArg, key, key, this));
885+
}
886+
887+
has(value: URI): boolean {
888+
return this._map.has(value);
889+
}
890+
891+
entries(): IterableIterator<[URI, URI]> {
892+
return this._map.entries();
893+
}
894+
895+
keys(): IterableIterator<URI> {
896+
return this._map.keys();
897+
}
898+
899+
values(): IterableIterator<URI> {
900+
return this._map.keys();
901+
}
902+
903+
[Symbol.iterator](): IterableIterator<URI> {
904+
return this.keys();
905+
}
906+
}
907+
908+
848909
interface Item<K, V> {
849910
previous: Item<K, V> | undefined;
850911
next: Item<K, V> | undefined;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export interface IBulkEditOptions {
7575
undoRedoSource?: UndoRedoSource;
7676
undoRedoGroupId?: number;
7777
confirmBeforeUndo?: boolean;
78+
respectAutoSaveConfig?: boolean;
7879
}
7980

8081
export interface IBulkEditResult {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,13 @@ export async function applyCodeAction(
168168
await item.resolve(CancellationToken.None);
169169

170170
if (item.action.edit) {
171-
await bulkEditService.apply(ResourceEdit.convert(item.action.edit), { editor, label: item.action.title, code: 'undoredo.codeAction' });
171+
await bulkEditService.apply(ResourceEdit.convert(item.action.edit), {
172+
editor,
173+
label: item.action.title,
174+
quotableLabel: item.action.title,
175+
code: 'undoredo.codeAction',
176+
respectAutoSaveConfig: true
177+
});
172178
}
173179

174180
if (item.action.command) {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,10 @@ class RenameController implements IEditorContribution {
236236
this._bulkEditService.apply(ResourceEdit.convert(renameResult), {
237237
editor: this.editor,
238238
showPreview: inputFieldResult.wantsPreview,
239-
label: nls.localize('label', "Renaming '{0}'", loc?.text),
239+
label: nls.localize('label', "Renaming '{0}' to '{1}'", loc?.text, inputFieldResult.newName),
240240
code: 'undoredo.rename',
241-
quotableLabel: nls.localize('quotableLabel', "Renaming {0}", loc?.text),
241+
quotableLabel: nls.localize('quotableLabel', "Renaming {0} to {1}", loc?.text, inputFieldResult.newName),
242+
respectAutoSaveConfig: true
242243
}).then(result => {
243244
if (result.ariaSummary) {
244245
alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc!.text, inputFieldResult.newName, result.ariaSummary));

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ export class BulkCellEdits {
3737
@INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService,
3838
) { }
3939

40-
async apply(): Promise<void> {
41-
40+
async apply(): Promise<readonly URI[]> {
41+
const resources: URI[] = [];
4242
const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString()));
4343

4444
for (let group of editsByNotebook) {
@@ -60,6 +60,10 @@ export class BulkCellEdits {
6060
ref.dispose();
6161

6262
this._progress.report(undefined);
63+
64+
resources.push(first.resource);
6365
}
66+
67+
return resources;
6468
}
6569
}

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

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import { LinkedList } from 'vs/base/common/linkedList';
2121
import { CancellationToken } from 'vs/base/common/cancellation';
2222
import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle';
2323
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
24-
import { ResourceMap } from 'vs/base/common/map';
24+
import { ResourceMap, ResourceSet } from 'vs/base/common/map';
25+
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
26+
import { URI } from 'vs/base/common/uri';
27+
import { Registry } from 'vs/platform/registry/common/platform';
28+
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
29+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2530

2631
class BulkEdit {
2732

@@ -67,10 +72,10 @@ class BulkEdit {
6772
}
6873
}
6974

70-
async perform(): Promise<void> {
75+
async perform(): Promise<readonly URI[]> {
7176

7277
if (this._edits.length === 0) {
73-
return;
78+
return [];
7479
}
7580

7681
const ranges: number[] = [1];
@@ -88,41 +93,44 @@ class BulkEdit {
8893
// Increment by percentage points since progress API expects that
8994
const progress: IProgress<void> = { report: _ => this._progress.report({ increment: 100 / this._edits.length }) };
9095

96+
const resources: (readonly URI[])[] = [];
9197
let index = 0;
9298
for (let range of ranges) {
9399
if (this._token.isCancellationRequested) {
94100
break;
95101
}
96102
const group = this._edits.slice(index, index + range);
97103
if (group[0] instanceof ResourceFileEdit) {
98-
await this._performFileEdits(<ResourceFileEdit[]>group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress);
104+
resources.push(await this._performFileEdits(<ResourceFileEdit[]>group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress));
99105
} else if (group[0] instanceof ResourceTextEdit) {
100-
await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress);
106+
resources.push(await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress));
101107
} else if (group[0] instanceof ResourceNotebookCellEdit) {
102-
await this._performCellEdits(<ResourceNotebookCellEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress);
108+
resources.push(await this._performCellEdits(<ResourceNotebookCellEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress));
103109
} else {
104110
console.log('UNKNOWN EDIT');
105111
}
106112
index = index + range;
107113
}
114+
115+
return resources.flat();
108116
}
109117

110-
private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress<void>) {
118+
private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress<void>): Promise<readonly URI[]> {
111119
this._logService.debug('_performFileEdits', JSON.stringify(edits));
112120
const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits);
113-
await model.apply();
121+
return await model.apply();
114122
}
115123

116-
private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<void> {
124+
private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> {
117125
this._logService.debug('_performTextEdits', JSON.stringify(edits));
118126
const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', this._editor, undoRedoGroup, undoRedoSource, progress, this._token, edits);
119-
await model.apply();
127+
return await model.apply();
120128
}
121129

122-
private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<void> {
130+
private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> {
123131
this._logService.debug('_performCellEdits', JSON.stringify(edits));
124132
const model = this._instaService.createInstance(BulkCellEdits, undoRedoGroup, undoRedoSource, progress, this._token, edits);
125-
await model.apply();
133+
return await model.apply();
126134
}
127135
}
128136

@@ -138,7 +146,9 @@ export class BulkEditService implements IBulkEditService {
138146
@ILogService private readonly _logService: ILogService,
139147
@IEditorService private readonly _editorService: IEditorService,
140148
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
141-
@IDialogService private readonly _dialogService: IDialogService
149+
@IDialogService private readonly _dialogService: IDialogService,
150+
@IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService,
151+
@IConfigurationService private readonly _configService: IConfigurationService,
142152
) { }
143153

144154
setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable {
@@ -212,8 +222,15 @@ export class BulkEditService implements IBulkEditService {
212222

213223
let listener: IDisposable | undefined;
214224
try {
215-
listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label, e.reason), 'veto.blukEditService'));
216-
await bulkEdit.perform();
225+
listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this._shouldVeto(label, e.reason), 'veto.blukEditService'));
226+
const resources = await bulkEdit.perform();
227+
228+
// when enabled (option AND setting) loop over all dirty working copies and trigger save
229+
// for those that were involved in this bulk edit operation.
230+
if (options?.respectAutoSaveConfig && this._configService.getValue(autoSaveSetting) === true && resources.length > 1) {
231+
await this._saveAll(resources);
232+
}
233+
217234
return { ariaSummary: bulkEdit.ariaMessage() };
218235
} catch (err) {
219236
// console.log('apply FAILED');
@@ -226,7 +243,23 @@ export class BulkEditService implements IBulkEditService {
226243
}
227244
}
228245

229-
private async shouldVeto(label: string | undefined, reason: ShutdownReason): Promise<boolean> {
246+
private async _saveAll(resources: readonly URI[]) {
247+
const set = new ResourceSet(resources);
248+
const saves = this._workingCopyService.dirtyWorkingCopies.map(async (copy) => {
249+
if (set.has(copy.resource)) {
250+
await copy.save();
251+
}
252+
});
253+
254+
const result = await Promise.allSettled(saves);
255+
for (const item of result) {
256+
if (item.status === 'rejected') {
257+
this._logService.warn(item.reason);
258+
}
259+
}
260+
}
261+
262+
private async _shouldVeto(label: string | undefined, reason: ShutdownReason): Promise<boolean> {
230263
label = label || localize('fileOperation', "File operation");
231264
const reasonLabel = reason === ShutdownReason.CLOSE ? localize('closeTheWindow', "Close Window") : reason === ShutdownReason.LOAD ? localize('changeWorkspace', "Change Workspace") :
232265
reason === ShutdownReason.RELOAD ? localize('reloadTheWindow', "Reload Window") : localize('quit', "Quit");
@@ -240,3 +273,16 @@ export class BulkEditService implements IBulkEditService {
240273
}
241274

242275
registerSingleton(IBulkEditService, BulkEditService, true);
276+
277+
const autoSaveSetting = 'files.refactoring.autoSave';
278+
279+
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
280+
id: 'files',
281+
properties: {
282+
[autoSaveSetting]: {
283+
description: localize('refactoring.autoSave', "Controls if files that were part of a refactoring are saved automatically"),
284+
default: true,
285+
type: 'boolean'
286+
}
287+
}
288+
});

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log';
1616
import { VSBuffer } from 'vs/base/common/buffer';
1717
import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService';
1818
import { CancellationToken } from 'vs/base/common/cancellation';
19-
import { flatten, tail } from 'vs/base/common/arrays';
19+
import { tail } from 'vs/base/common/arrays';
2020
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
2121

2222
interface IFileOperation {
@@ -51,7 +51,7 @@ class RenameOperation implements IFileOperation {
5151
) { }
5252

5353
get uris() {
54-
return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri]));
54+
return this._edits.map(edit => [edit.newUri, edit.oldUri]).flat();
5555
}
5656

5757
async perform(token: CancellationToken): Promise<IFileOperation> {
@@ -105,7 +105,7 @@ class CopyOperation implements IFileOperation {
105105
) { }
106106

107107
get uris() {
108-
return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri]));
108+
return this._edits.map(edit => [edit.newUri, edit.oldUri]).flat();
109109
}
110110

111111
async perform(token: CancellationToken): Promise<IFileOperation> {
@@ -293,7 +293,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement {
293293
readonly operations: IFileOperation[],
294294
readonly confirmBeforeUndo: boolean
295295
) {
296-
this.resources = (<URI[]>[]).concat(...operations.map(op => op.uris));
296+
this.resources = operations.map(op => op.uris).flat();
297297
}
298298

299299
async undo(): Promise<void> {
@@ -332,7 +332,7 @@ export class BulkFileEdits {
332332
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
333333
) { }
334334

335-
async apply(): Promise<void> {
335+
async apply(): Promise<readonly URI[]> {
336336
const undoOperations: IFileOperation[] = [];
337337
const undoRedoInfo = { undoRedoGroupId: this._undoRedoGroup.id };
338338

@@ -350,7 +350,7 @@ export class BulkFileEdits {
350350
}
351351

352352
if (edits.length === 0) {
353-
return;
353+
return [];
354354
}
355355

356356
const groups: Array<RenameEdit | CopyEdit | DeleteEdit | CreateEdit>[] = [];
@@ -395,6 +395,8 @@ export class BulkFileEdits {
395395
this._progress.report(undefined);
396396
}
397397

398-
this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource);
398+
const undoRedoElement = new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo);
399+
this._undoRedoService.pushElement(undoRedoElement, this._undoRedoGroup, this._undoRedoSource);
400+
return undoRedoElement.resources;
399401
}
400402
}

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,16 +232,17 @@ export class BulkTextEdits {
232232
return { canApply: true };
233233
}
234234

235-
async apply(): Promise<void> {
235+
async apply(): Promise<readonly URI[]> {
236236

237237
this._validateBeforePrepare();
238238
const tasks = await this._createEditsTasks();
239239

240-
if (this._token.isCancellationRequested) {
241-
return;
242-
}
243240
try {
241+
if (this._token.isCancellationRequested) {
242+
return [];
243+
}
244244

245+
const resources: URI[] = [];
245246
const validation = this._validateTasks(tasks);
246247
if (!validation.canApply) {
247248
throw new Error(`${validation.reason.toString()} has changed in the meantime`);
@@ -254,6 +255,7 @@ export class BulkTextEdits {
254255
this._undoRedoService.pushElement(singleModelEditStackElement, this._undoRedoGroup, this._undoRedoSource);
255256
task.apply();
256257
singleModelEditStackElement.close();
258+
resources.push(task.model.uri);
257259
}
258260
this._progress.report(undefined);
259261
} else {
@@ -267,10 +269,13 @@ export class BulkTextEdits {
267269
for (const task of tasks) {
268270
task.apply();
269271
this._progress.report(undefined);
272+
resources.push(task.model.uri);
270273
}
271274
multiModelEditStackElement.close();
272275
}
273276

277+
return resources;
278+
274279
} finally {
275280
dispose(tasks);
276281
}

0 commit comments

Comments
 (0)