Deleting or renaming a file or its parent with dirty within should prompt user (fixes #9313)

This commit is contained in:
Benjamin Pasero 2016-08-10 09:11:40 +02:00
parent dcf34da773
commit 8575dcd1bf
4 changed files with 142 additions and 92 deletions

View file

@ -17,6 +17,7 @@ export interface IMessageWithAction {
export interface IConfirmation {
title?: string;
type?: 'none' | 'info' | 'error' | 'question' | 'warning';
message: string;
detail?: string;
primaryButton?: string;

View file

@ -16,7 +16,6 @@ import URI from 'vs/base/common/uri';
import errors = require('vs/base/common/errors');
import strings = require('vs/base/common/strings');
import {Event, EventType as CommonEventType} from 'vs/base/common/events';
import {getPathLabel} from 'vs/base/common/labels';
import severity from 'vs/base/common/severity';
import diagnostics = require('vs/base/common/diagnostics');
import {BaseTextEditor} from 'vs/workbench/browser/parts/editor/textEditor';
@ -28,7 +27,7 @@ import {EventType as WorkbenchEventType} from 'vs/workbench/common/events';
import {LocalFileChangeEvent, VIEWLET_ID, ITextFileService, TextFileChangeEvent, EventType as FileEventType} from 'vs/workbench/parts/files/common/files';
import {IFileService, IFileStat, IImportResult} from 'vs/platform/files/common/files';
import {DiffEditorInput, toDiffLabel} from 'vs/workbench/common/editor/diffEditorInput';
import {asFileEditorInput, getUntitledOrFileResource, UntitledEditorInput, ConfirmResult, IEditorIdentifier} from 'vs/workbench/common/editor';
import {asFileEditorInput, getUntitledOrFileResource, UntitledEditorInput, IEditorIdentifier} from 'vs/workbench/common/editor';
import {FileEditorInput} from 'vs/workbench/parts/files/common/editors/fileEditorInput';
import {FileStat, NewStatPlaceholder} from 'vs/workbench/parts/files/common/explorerViewModel';
import {ExplorerView} from 'vs/workbench/parts/files/browser/views/explorerView';
@ -145,23 +144,6 @@ export class BaseFileAction extends Action {
this._messageService.show(Severity.Error, errorWithRetry);
}
protected handleDirty(): TPromise<boolean /* cancel */> {
if (this.textFileService.isDirty(this._element.resource)) {
let res = this.textFileService.confirmSave([this._element.resource]);
if (res === ConfirmResult.SAVE) {
return this.textFileService.save(this._element.resource).then(() => false);
}
if (res === ConfirmResult.DONT_SAVE) {
return this.textFileService.revert(this._element.resource).then(() => false);
}
return TPromise.as(true);
}
return TPromise.as(false);
}
}
export class TriggerRenameFileAction extends BaseFileAction {
@ -337,19 +319,36 @@ export class RenameFileAction extends BaseRenameAction {
public runAction(newName: string): TPromise<any> {
// Check if file is dirty in editor and save it to avoid data loss
return this.handleDirty().then((cancel: boolean) => {
if (cancel) {
// Handle dirty
let revertPromise: TPromise<any> = TPromise.as(null);
const dirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, this.element.resource.fsPath));
if (dirty.length) {
let message: string;
if (this.element.isDirectory) {
if (dirty.length === 1) {
message = nls.localize('dirtyMessageFolderOne', "You are renaming a folder with unsaved changes in 1 file. Do you want to continue?");
} else {
message = nls.localize('dirtyMessageFolder', "You are renaming a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length);
}
} else {
message = nls.localize('dirtyMessageFile', "You are renaming a file with unsaved changes. Do you want to continue?");
}
const res = this.messageService.confirm({
message,
type: 'warning',
detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),
primaryButton: nls.localize({ key: 'renameLabel', comment: ['&& denotes a mnemonic'] }, "&&Rename")
});
if (!res) {
return TPromise.as(null);
}
// If the file is still dirty, do not touch it because a save is pending to disk and we can not abort it
if (this.textFileService.isDirty(this.element.resource)) {
this.onWarning(nls.localize('warningFileDirty', "File '{0}' is currently being saved, please try again later.", getPathLabel(this.element.resource)));
return TPromise.as(null);
}
revertPromise = this.textFileService.revertAll(dirty);
}
return revertPromise.then(() => {
return this.fileService.rename(this.element.resource, newName).then(null, (error: Error) => {
this.onErrorWithRetry(error, () => this.runAction(newName));
});
@ -690,56 +689,97 @@ export class BaseDeleteFileAction extends BaseFileAction {
this.tree.clearHighlight();
}
// Ask for Confirm
if (!this.skipConfirm) {
let confirm: IConfirmation;
if (this.useTrash) {
confirm = {
message: this.element.isDirectory ? nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", this.element.name) : nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", this.element.name),
detail: isWindows ? nls.localize('undoBin', "You can restore from the recycle bin.") : nls.localize('undoTrash', "You can restore from the trash."),
primaryButton: isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash")
};
} else {
confirm = {
message: this.element.isDirectory ? nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", this.element.name) : nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", this.element.name),
detail: nls.localize('irreversible', "This action is irreversible!"),
primaryButton: nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete")
};
}
if (!this.messageService.confirm(confirm)) {
return TPromise.as(null);
}
let primaryButton: string;
if (this.useTrash) {
primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash");
} else {
primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete");
}
// Since a delete operation can take a while we want to emit the event proactively to avoid issues
// with stale entries in the explorer tree.
this.eventService.emit('files.internal:fileChanged', new LocalFileChangeEvent(this.element.clone(), null));
// Call function
let servicePromise = this.fileService.del(this.element.resource, this.useTrash).then(() => {
if (this.element.parent) {
this.tree.setFocus(this.element.parent); // move focus to parent
}
}, (error: any) => {
// Allow to retry
let extraAction: Action;
if (this.useTrash) {
extraAction = new Action('permanentDelete', nls.localize('permDelete', "Delete Permanently"), null, true, () => { this.useTrash = false; this.skipConfirm = true; return this.run(); });
// Handle dirty
let revertPromise: TPromise<any> = TPromise.as(null);
const dirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, this.element.resource.fsPath));
if (dirty.length) {
let message: string;
if (this.element.isDirectory) {
if (dirty.length === 1) {
message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder with unsaved changes in 1 file. Do you want to continue?");
} else {
message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length);
}
} else {
message = nls.localize('dirtyMessageFileDelete', "You are deleting a file with unsaved changes. Do you want to continue?");
}
this.onErrorWithRetry(error, () => this.run(), extraAction);
const res = this.messageService.confirm({
message,
type: 'warning',
detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),
primaryButton
});
// Since the delete failed, best we can do is to refresh the explorer from the root to show the current state of files.
let event = new LocalFileChangeEvent(new FileStat(this.contextService.getWorkspace().resource, true, true), new FileStat(this.contextService.getWorkspace().resource, true, true));
this.eventService.emit('files.internal:fileChanged', event);
if (!res) {
return TPromise.as(null);
}
// Focus back to tree
this.tree.DOMFocus();
this.skipConfirm = true; // since we already asked for confirmation
revertPromise = this.textFileService.revertAll(dirty);
}
// Check if file is dirty in editor and save it to avoid data loss
return revertPromise.then(() => {
// Ask for Confirm
if (!this.skipConfirm) {
let confirm: IConfirmation;
if (this.useTrash) {
confirm = {
message: this.element.isDirectory ? nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", this.element.name) : nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", this.element.name),
detail: isWindows ? nls.localize('undoBin', "You can restore from the recycle bin.") : nls.localize('undoTrash', "You can restore from the trash."),
primaryButton
};
} else {
confirm = {
message: this.element.isDirectory ? nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", this.element.name) : nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", this.element.name),
detail: nls.localize('irreversible', "This action is irreversible!"),
primaryButton
};
}
if (!this.messageService.confirm(confirm)) {
return TPromise.as(null);
}
}
// Since a delete operation can take a while we want to emit the event proactively to avoid issues
// with stale entries in the explorer tree.
this.eventService.emit('files.internal:fileChanged', new LocalFileChangeEvent(this.element.clone(), null));
// Call function
let servicePromise = this.fileService.del(this.element.resource, this.useTrash).then(() => {
if (this.element.parent) {
this.tree.setFocus(this.element.parent); // move focus to parent
}
}, (error: any) => {
// Allow to retry
let extraAction: Action;
if (this.useTrash) {
extraAction = new Action('permanentDelete', nls.localize('permDelete', "Delete Permanently"), null, true, () => { this.useTrash = false; this.skipConfirm = true; return this.run(); });
}
this.onErrorWithRetry(error, () => this.run(), extraAction);
// Since the delete failed, best we can do is to refresh the explorer from the root to show the current state of files.
let event = new LocalFileChangeEvent(new FileStat(this.contextService.getWorkspace().resource, true, true), new FileStat(this.contextService.getWorkspace().resource, true, true));
this.eventService.emit('files.internal:fileChanged', event);
// Focus back to tree
this.tree.DOMFocus();
});
return servicePromise;
});
return servicePromise;
}
}

View file

@ -27,9 +27,7 @@ import {LocalFileChangeEvent, IFilesConfiguration, ITextFileService} from 'vs/wo
import {IFileOperationResult, FileOperationResult, IFileStat, IFileService} from 'vs/platform/files/common/files';
import {FileEditorInput} from 'vs/workbench/parts/files/common/editors/fileEditorInput';
import {DuplicateFileAction, ImportFileAction, PasteFileAction, keybindingForAction, IEditableData, IFileViewletState} from 'vs/workbench/parts/files/browser/fileActions';
import {ConfirmResult} from 'vs/workbench/common/editor';
import {IDataSource, ITree, IElementCallback, IAccessibilityProvider, IRenderer, ContextMenuEvent, ISorter, IFilter, IDragAndDrop, IDragAndDropData, IDragOverReaction, DRAG_OVER_ACCEPT_BUBBLE_DOWN, DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY, DRAG_OVER_ACCEPT_BUBBLE_UP, DRAG_OVER_ACCEPT_BUBBLE_UP_COPY, DRAG_OVER_REJECT} from 'vs/base/parts/tree/browser/tree';
import labels = require('vs/base/common/labels');
import {DesktopDragAndDropData, ExternalElementsDragAndDropData} from 'vs/base/parts/tree/browser/treeDnd';
import {ClickBehavior, DefaultController} from 'vs/base/parts/tree/browser/treeDefaults';
import {ActionsRenderer} from 'vs/base/parts/tree/browser/actionsRenderer';
@ -846,29 +844,36 @@ export class FileDragAndDrop implements IDragAndDrop {
return copyAction.run();
}
// Handle dirty
let saveOrRevertPromise: TPromise<boolean> = TPromise.as(null);
if (this.textFileService.isDirty(source.resource)) {
let res = this.textFileService.confirmSave([source.resource]);
if (res === ConfirmResult.SAVE) {
saveOrRevertPromise = this.textFileService.save(source.resource);
} else if (res === ConfirmResult.DONT_SAVE) {
saveOrRevertPromise = this.textFileService.revert(source.resource);
} else if (res === ConfirmResult.CANCEL) {
// Handle dirty (in file or inside the folder if any)
let revertPromise: TPromise<any> = TPromise.as(null);
const dirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, source.resource.fsPath));
if (dirty.length) {
let message:string;
if (source.isDirectory) {
if (dirty.length === 1) {
message = nls.localize('dirtyMessageFolderOne', "You are moving a folder with unsaved changes in 1 file. Do you want to continue?");
} else {
message = nls.localize('dirtyMessageFolder', "You are moving a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length);
}
} else {
message = nls.localize('dirtyMessageFile', "You are moving a file with unsaved changes. Do you want to continue?");
}
const res = this.messageService.confirm({
message,
type: 'warning',
detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),
primaryButton: nls.localize({ key: 'moveLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
});
if (!res) {
return TPromise.as(null);
}
revertPromise = this.textFileService.revertAll(dirty);
}
// For move, first check if file is dirty and save
return saveOrRevertPromise.then(() => {
// If the file is still dirty, do not touch it because a save is pending to the disk and we can not abort it
if (this.textFileService.isDirty(source.resource)) {
this.messageService.show(Severity.Warning, nls.localize('warningFileDirty', "File '{0}' is currently being saved, please try again later.", labels.getPathLabel(source.resource)));
return TPromise.as(null);
}
return revertPromise.then(() => {
let targetResource = URI.file(paths.join(target.resource.fsPath, source.name));
let didHandleConflict = false;

View file

@ -53,6 +53,10 @@ export class MessageService extends WorkbenchMessageService {
opts.detail = confirmation.detail;
}
if (confirmation.type) {
opts.type = confirmation.type;
}
let result = this.windowService.getWindow().showMessageBox(opts);
if (isLinux) {