diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 97370fd6f2c..c7d1b418241 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -845,6 +845,67 @@ export class ResourceMap implements Map { } } +export class ResourceSet implements Set { + + readonly [Symbol.toStringTag]: string = 'ResourceSet'; + + private readonly _map: ResourceMap; + + constructor(toKey?: ResourceMapKeyFn); + constructor(entries: readonly URI[], toKey?: ResourceMapKeyFn); + constructor(entriesOrKey?: readonly URI[] | ResourceMapKeyFn, toKey?: ResourceMapKeyFn) { + if (!entriesOrKey || typeof entriesOrKey === 'function') { + this._map = new ResourceMap(entriesOrKey); + } else { + this._map = new ResourceMap(toKey); + entriesOrKey.forEach(this.add, this); + } + } + + + get size(): number { + return this._map.size; + } + + add(value: URI): this { + this._map.set(value, value); + return this; + } + + clear(): void { + this._map.clear(); + } + + delete(value: URI): boolean { + return this._map.delete(value); + } + + forEach(callbackfn: (value: URI, value2: URI, set: Set) => void, thisArg?: any): void { + this._map.forEach((_value, key) => callbackfn.call(thisArg, key, key, this)); + } + + has(value: URI): boolean { + return this._map.has(value); + } + + entries(): IterableIterator<[URI, URI]> { + return this._map.entries(); + } + + keys(): IterableIterator { + return this._map.keys(); + } + + values(): IterableIterator { + return this._map.keys(); + } + + [Symbol.iterator](): IterableIterator { + return this.keys(); + } +} + + interface Item { previous: Item | undefined; next: Item | undefined; diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 251a958e602..558376fae33 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -75,6 +75,7 @@ export interface IBulkEditOptions { undoRedoSource?: UndoRedoSource; undoRedoGroupId?: number; confirmBeforeUndo?: boolean; + respectAutoSaveConfig?: boolean; } export interface IBulkEditResult { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index b3f036f7331..3921bbb6b91 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts @@ -168,7 +168,13 @@ export async function applyCodeAction( await item.resolve(CancellationToken.None); if (item.action.edit) { - await bulkEditService.apply(ResourceEdit.convert(item.action.edit), { editor, label: item.action.title, code: 'undoredo.codeAction' }); + await bulkEditService.apply(ResourceEdit.convert(item.action.edit), { + editor, + label: item.action.title, + quotableLabel: item.action.title, + code: 'undoredo.codeAction', + respectAutoSaveConfig: true + }); } if (item.action.command) { diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index bbc1dd38b21..13291e31fb6 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -236,9 +236,10 @@ class RenameController implements IEditorContribution { this._bulkEditService.apply(ResourceEdit.convert(renameResult), { editor: this.editor, showPreview: inputFieldResult.wantsPreview, - label: nls.localize('label', "Renaming '{0}'", loc?.text), + label: nls.localize('label', "Renaming '{0}' to '{1}'", loc?.text, inputFieldResult.newName), code: 'undoredo.rename', - quotableLabel: nls.localize('quotableLabel', "Renaming {0}", loc?.text), + quotableLabel: nls.localize('quotableLabel', "Renaming {0} to {1}", loc?.text, inputFieldResult.newName), + respectAutoSaveConfig: true }).then(result => { if (result.ariaSummary) { alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc!.text, inputFieldResult.newName, result.ariaSummary)); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts index 6b0bb5fdf18..2bd51ab3be7 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -37,8 +37,8 @@ export class BulkCellEdits { @INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService, ) { } - async apply(): Promise { - + async apply(): Promise { + const resources: URI[] = []; const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); for (let group of editsByNotebook) { @@ -60,6 +60,10 @@ export class BulkCellEdits { ref.dispose(); this._progress.report(undefined); + + resources.push(first.resource); } + + return resources; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index a259de1b6d5..aaab23b4da9 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -21,7 +21,12 @@ import { LinkedList } from 'vs/base/common/linkedList'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ResourceMap } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } from 'vs/base/common/map'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { URI } from 'vs/base/common/uri'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; class BulkEdit { @@ -67,10 +72,10 @@ class BulkEdit { } } - async perform(): Promise { + async perform(): Promise { if (this._edits.length === 0) { - return; + return []; } const ranges: number[] = [1]; @@ -88,6 +93,7 @@ class BulkEdit { // Increment by percentage points since progress API expects that const progress: IProgress = { report: _ => this._progress.report({ increment: 100 / this._edits.length }) }; + const resources: (readonly URI[])[] = []; let index = 0; for (let range of ranges) { if (this._token.isCancellationRequested) { @@ -95,34 +101,36 @@ class BulkEdit { } const group = this._edits.slice(index, index + range); if (group[0] instanceof ResourceFileEdit) { - await this._performFileEdits(group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress); + resources.push(await this._performFileEdits(group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress)); } else if (group[0] instanceof ResourceTextEdit) { - await this._performTextEdits(group, this._undoRedoGroup, this._undoRedoSource, progress); + resources.push(await this._performTextEdits(group, this._undoRedoGroup, this._undoRedoSource, progress)); } else if (group[0] instanceof ResourceNotebookCellEdit) { - await this._performCellEdits(group, this._undoRedoGroup, this._undoRedoSource, progress); + resources.push(await this._performCellEdits(group, this._undoRedoGroup, this._undoRedoSource, progress)); } else { console.log('UNKNOWN EDIT'); } index = index + range; } + + return resources.flat(); } - private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress) { + private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress): Promise { this._logService.debug('_performFileEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits); - await model.apply(); + return await model.apply(); } - private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress): Promise { + private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', this._editor, undoRedoGroup, undoRedoSource, progress, this._token, edits); - await model.apply(); + return await model.apply(); } - private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress): Promise { + private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress): Promise { this._logService.debug('_performCellEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkCellEdits, undoRedoGroup, undoRedoSource, progress, this._token, edits); - await model.apply(); + return await model.apply(); } } @@ -138,7 +146,9 @@ export class BulkEditService implements IBulkEditService { @ILogService private readonly _logService: ILogService, @IEditorService private readonly _editorService: IEditorService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, - @IDialogService private readonly _dialogService: IDialogService + @IDialogService private readonly _dialogService: IDialogService, + @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, + @IConfigurationService private readonly _configService: IConfigurationService, ) { } setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable { @@ -212,8 +222,15 @@ export class BulkEditService implements IBulkEditService { let listener: IDisposable | undefined; try { - listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label, e.reason), 'veto.blukEditService')); - await bulkEdit.perform(); + listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this._shouldVeto(label, e.reason), 'veto.blukEditService')); + const resources = await bulkEdit.perform(); + + // when enabled (option AND setting) loop over all dirty working copies and trigger save + // for those that were involved in this bulk edit operation. + if (options?.respectAutoSaveConfig && this._configService.getValue(autoSaveSetting) === true && resources.length > 1) { + await this._saveAll(resources); + } + return { ariaSummary: bulkEdit.ariaMessage() }; } catch (err) { // console.log('apply FAILED'); @@ -226,7 +243,23 @@ export class BulkEditService implements IBulkEditService { } } - private async shouldVeto(label: string | undefined, reason: ShutdownReason): Promise { + private async _saveAll(resources: readonly URI[]) { + const set = new ResourceSet(resources); + const saves = this._workingCopyService.dirtyWorkingCopies.map(async (copy) => { + if (set.has(copy.resource)) { + await copy.save(); + } + }); + + const result = await Promise.allSettled(saves); + for (const item of result) { + if (item.status === 'rejected') { + this._logService.warn(item.reason); + } + } + } + + private async _shouldVeto(label: string | undefined, reason: ShutdownReason): Promise { label = label || localize('fileOperation', "File operation"); const reasonLabel = reason === ShutdownReason.CLOSE ? localize('closeTheWindow', "Close Window") : reason === ShutdownReason.LOAD ? localize('changeWorkspace', "Change Workspace") : reason === ShutdownReason.RELOAD ? localize('reloadTheWindow', "Reload Window") : localize('quit', "Quit"); @@ -240,3 +273,16 @@ export class BulkEditService implements IBulkEditService { } registerSingleton(IBulkEditService, BulkEditService, true); + +const autoSaveSetting = 'files.refactoring.autoSave'; + +Registry.as(Extensions.Configuration).registerConfiguration({ + id: 'files', + properties: { + [autoSaveSetting]: { + description: localize('refactoring.autoSave', "Controls if files that were part of a refactoring are saved automatically"), + default: true, + type: 'boolean' + } + } +}); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index b3247bba89d..86de76556d2 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -16,7 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { flatten, tail } from 'vs/base/common/arrays'; +import { tail } from 'vs/base/common/arrays'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IFileOperation { @@ -51,7 +51,7 @@ class RenameOperation implements IFileOperation { ) { } get uris() { - return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); + return this._edits.map(edit => [edit.newUri, edit.oldUri]).flat(); } async perform(token: CancellationToken): Promise { @@ -105,7 +105,7 @@ class CopyOperation implements IFileOperation { ) { } get uris() { - return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); + return this._edits.map(edit => [edit.newUri, edit.oldUri]).flat(); } async perform(token: CancellationToken): Promise { @@ -293,7 +293,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { readonly operations: IFileOperation[], readonly confirmBeforeUndo: boolean ) { - this.resources = ([]).concat(...operations.map(op => op.uris)); + this.resources = operations.map(op => op.uris).flat(); } async undo(): Promise { @@ -332,7 +332,7 @@ export class BulkFileEdits { @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, ) { } - async apply(): Promise { + async apply(): Promise { const undoOperations: IFileOperation[] = []; const undoRedoInfo = { undoRedoGroupId: this._undoRedoGroup.id }; @@ -350,7 +350,7 @@ export class BulkFileEdits { } if (edits.length === 0) { - return; + return []; } const groups: Array[] = []; @@ -395,6 +395,8 @@ export class BulkFileEdits { this._progress.report(undefined); } - this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource); + const undoRedoElement = new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo); + this._undoRedoService.pushElement(undoRedoElement, this._undoRedoGroup, this._undoRedoSource); + return undoRedoElement.resources; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index b0f3ba726bb..b1ff888c894 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -232,16 +232,17 @@ export class BulkTextEdits { return { canApply: true }; } - async apply(): Promise { + async apply(): Promise { this._validateBeforePrepare(); const tasks = await this._createEditsTasks(); - if (this._token.isCancellationRequested) { - return; - } try { + if (this._token.isCancellationRequested) { + return []; + } + const resources: URI[] = []; const validation = this._validateTasks(tasks); if (!validation.canApply) { throw new Error(`${validation.reason.toString()} has changed in the meantime`); @@ -254,6 +255,7 @@ export class BulkTextEdits { this._undoRedoService.pushElement(singleModelEditStackElement, this._undoRedoGroup, this._undoRedoSource); task.apply(); singleModelEditStackElement.close(); + resources.push(task.model.uri); } this._progress.report(undefined); } else { @@ -267,10 +269,13 @@ export class BulkTextEdits { for (const task of tasks) { task.apply(); this._progress.report(undefined); + resources.push(task.model.uri); } multiModelEditStackElement.close(); } + return resources; + } finally { dispose(tasks); }