Merge pull request #146320 from microsoft/joh/bulkEditSave

This commit is contained in:
Johannes Rieken 2022-03-31 16:57:04 +02:00 committed by GitHub
commit f29671557b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 158 additions and 32 deletions

View file

@ -845,6 +845,67 @@ export class ResourceMap<T> implements Map<URI, T> {
}
}
export class ResourceSet implements Set<URI> {
readonly [Symbol.toStringTag]: string = 'ResourceSet';
private readonly _map: ResourceMap<URI>;
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<URI>) => 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<URI> {
return this._map.keys();
}
values(): IterableIterator<URI> {
return this._map.keys();
}
[Symbol.iterator](): IterableIterator<URI> {
return this.keys();
}
}
interface Item<K, V> {
previous: Item<K, V> | undefined;
next: Item<K, V> | undefined;

View file

@ -75,6 +75,7 @@ export interface IBulkEditOptions {
undoRedoSource?: UndoRedoSource;
undoRedoGroupId?: number;
confirmBeforeUndo?: boolean;
respectAutoSaveConfig?: boolean;
}
export interface IBulkEditResult {

View file

@ -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) {

View file

@ -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));

View file

@ -37,8 +37,8 @@ export class BulkCellEdits {
@INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService,
) { }
async apply(): Promise<void> {
async apply(): Promise<readonly URI[]> {
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;
}
}

View file

@ -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<void> {
async perform(): Promise<readonly URI[]> {
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<void> = { 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(<ResourceFileEdit[]>group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress);
resources.push(await this._performFileEdits(<ResourceFileEdit[]>group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress));
} else if (group[0] instanceof ResourceTextEdit) {
await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress);
resources.push(await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress));
} else if (group[0] instanceof ResourceNotebookCellEdit) {
await this._performCellEdits(<ResourceNotebookCellEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress);
resources.push(await this._performCellEdits(<ResourceNotebookCellEdit[]>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<void>) {
private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress<void>): Promise<readonly URI[]> {
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<void>): Promise<void> {
private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> {
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<void>): Promise<void> {
private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> {
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<boolean> {
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<boolean> {
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<IConfigurationRegistry>(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'
}
}
});

View file

@ -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<IFileOperation> {
@ -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<IFileOperation> {
@ -293,7 +293,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement {
readonly operations: IFileOperation[],
readonly confirmBeforeUndo: boolean
) {
this.resources = (<URI[]>[]).concat(...operations.map(op => op.uris));
this.resources = operations.map(op => op.uris).flat();
}
async undo(): Promise<void> {
@ -332,7 +332,7 @@ export class BulkFileEdits {
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
) { }
async apply(): Promise<void> {
async apply(): Promise<readonly URI[]> {
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<RenameEdit | CopyEdit | DeleteEdit | CreateEdit>[] = [];
@ -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;
}
}

View file

@ -232,16 +232,17 @@ export class BulkTextEdits {
return { canApply: true };
}
async apply(): Promise<void> {
async apply(): Promise<readonly URI[]> {
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);
}