mirror of
https://github.com/Microsoft/vscode
synced 2024-10-02 17:32:41 +00:00
Merge pull request #146320 from microsoft/joh/bulkEditSave
This commit is contained in:
commit
f29671557b
|
@ -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;
|
||||
|
|
|
@ -75,6 +75,7 @@ export interface IBulkEditOptions {
|
|||
undoRedoSource?: UndoRedoSource;
|
||||
undoRedoGroupId?: number;
|
||||
confirmBeforeUndo?: boolean;
|
||||
respectAutoSaveConfig?: boolean;
|
||||
}
|
||||
|
||||
export interface IBulkEditResult {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue