diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index be7dddf26ff..6cd04336cfb 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -14,7 +14,7 @@ import { COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMA import { CommandsRegistry, ICommandHandler } from '../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerResourceAvailableEditorIdsContext, FoldersViewVisibleContext } from '../common/files.js'; +import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceWritableContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerResourceAvailableEditorIdsContext, FoldersViewVisibleContext } from '../common/files.js'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from '../../../browser/actions/workspaceCommands.js'; import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, REOPEN_WITH_COMMAND_ID } from '../../../browser/parts/editor/editorCommands.js'; import { AutoSaveAfterShortDelayContext } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; @@ -52,7 +52,7 @@ const RENAME_ID = 'renameFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: RENAME_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceWritableContext), primary: KeyCode.F2, mac: { primary: KeyCode.Enter @@ -64,7 +64,7 @@ const MOVE_FILE_TO_TRASH_ID = 'moveFileToTrash'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: MOVE_FILE_TO_TRASH_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceMoveableToTrash), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace, @@ -77,7 +77,7 @@ const DELETE_FILE_ID = 'deleteFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext), + when: FilesExplorerFocusCondition, primary: KeyMod.Shift | KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace @@ -88,7 +88,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash.toNegated()), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceMoveableToTrash.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace @@ -100,7 +100,7 @@ const CUT_FILE_ID = 'filesExplorer.cut'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CUT_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceWritableContext), primary: KeyMod.CtrlCmd | KeyCode.KeyX, handler: cutFileHandler, }); @@ -121,7 +121,7 @@ CommandsRegistry.registerCommand(PASTE_FILE_ID, pasteFileHandler); KeybindingsRegistry.registerKeybindingRule({ id: `^${PASTE_FILE_ID}`, // the `^` enables pasting files into the explorer by preventing default bubble up weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceWritableContext), primary: KeyMod.CtrlCmd | KeyCode.KeyV, }); @@ -479,7 +479,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: NEW_FILE_COMMAND_ID, title: NEW_FILE_LABEL, - precondition: ExplorerResourceNotReadonlyContext + precondition: ExplorerResourceWritableContext }, when: ExplorerFolderContext }); @@ -490,7 +490,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: NEW_FOLDER_COMMAND_ID, title: NEW_FOLDER_LABEL, - precondition: ExplorerResourceNotReadonlyContext + precondition: ExplorerResourceWritableContext }, when: ExplorerFolderContext }); @@ -540,7 +540,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { id: CUT_FILE_ID, title: nls.localize('cut', "Cut"), }, - when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext) + when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceWritableContext) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -559,7 +559,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: PASTE_FILE_ID, title: PASTE_FILE_LABEL, - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext, FileCopiedContext) + precondition: ContextKeyExpr.and(ExplorerResourceWritableContext, FileCopiedContext) }, when: ExplorerFolderContext }); @@ -593,8 +593,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ IsWebContext, // only on folders ExplorerFolderContext, - // only on editable folders - ExplorerResourceNotReadonlyContext + // only on writable folders + ExplorerResourceWritableContext ) })); @@ -638,7 +638,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: RENAME_ID, title: TRIGGER_RENAME_LABEL, - precondition: ExplorerResourceNotReadonlyContext, + precondition: ExplorerResourceWritableContext, }, when: ExplorerRootContext.toNegated() }); @@ -648,13 +648,11 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 20, command: { id: MOVE_FILE_TO_TRASH_ID, - title: MOVE_FILE_TO_TRASH_LABEL, - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext), + title: MOVE_FILE_TO_TRASH_LABEL }, alt: { id: DELETE_FILE_ID, - title: nls.localize('deleteFile', "Delete Permanently"), - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext), + title: nls.localize('deleteFile', "Delete Permanently") }, when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash) }); @@ -664,8 +662,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 20, command: { id: DELETE_FILE_ID, - title: nls.localize('deleteFile', "Delete Permanently"), - precondition: ExplorerResourceNotReadonlyContext, + title: nls.localize('deleteFile', "Delete Permanently") }, when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash.toNegated()) }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 635765f09b7..50f786ea2f7 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -93,7 +93,7 @@ async function refreshIfSeparator(value: string, explorerService: IExplorerServi } } -async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise { +async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, filesConfigurationService: IFilesConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise { let primaryButton: string; if (useTrash) { primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); @@ -109,7 +109,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer dirtyWorkingCopies.add(dirtyWorkingCopy); } } - let confirmed = true; + if (dirtyWorkingCopies.size) { let message: string; if (distinctElements.length > 1) { @@ -132,18 +132,40 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer }); if (!response.confirmed) { - confirmed = false; + return; } else { skipConfirm = true; } } - // Check if file is dirty in editor and save it to avoid data loss - if (!confirmed) { - return; + // Handle readonly + if (!skipConfirm) { + const readonlyResources = distinctElements.filter(e => filesConfigurationService.isReadonly(e.resource)); + if (readonlyResources.length) { + let message: string; + if (readonlyResources.length > 1) { + message = nls.localize('readonlyMessageFilesDelete', "You are deleting files that are configured to be read-only. Do you want to continue?"); + } else if (readonlyResources[0].isDirectory) { + message = nls.localize('readonlyMessageFolderOneDelete', "You are deleting a folder {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name); + } else { + message = nls.localize('readonlyMessageFolderDelete', "You are deleting a file {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name); + } + + const response = await dialogService.confirm({ + type: 'warning', + message, + detail: nls.localize('continueDetail', "The read-only protection will be overridden if you continue."), + primaryButton: nls.localize('continueButtonLabel', "Continue") + }); + + if (!response.confirmed) { + return; + } + } } let confirmation: IConfirmationResult; + // We do not support undo of folders, so in that case the delete action is irreversible const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") : distinctElements.length > 1 ? nls.localize('restorePlural', "You can restore these files using the Undo command.") : nls.localize('restore', "You can restore this file using the Undo command."); @@ -234,7 +256,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer skipConfirm = true; ignoreIfNotExists = true; - return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, elements, useTrash, skipConfirm, ignoreIfNotExists); + return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, filesConfigurationService, elements, useTrash, skipConfirm, ignoreIfNotExists); } } } @@ -1020,7 +1042,7 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); + await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, true); } }; @@ -1029,7 +1051,7 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); + await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, false); } }; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 1c718837501..f91029d58d5 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,7 +8,7 @@ import { URI } from '../../../../../base/common/uri.js'; import * as perf from '../../../../../base/common/performance.js'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../../base/common/actions.js'; import { memoize } from '../../../../../base/common/decorators.js'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, ExplorerResourceNotReadonlyContext, ViewHasSomeCollapsibleRootItemContext, FoldersViewVisibleContext, ExplorerResourceParentReadOnlyContext, ExplorerFindProviderActive } from '../../common/files.js'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, ExplorerResourceWritableContext, ViewHasSomeCollapsibleRootItemContext, FoldersViewVisibleContext, ExplorerResourceParentReadOnlyContext, ExplorerFindProviderActive } from '../../common/files.js'; import { FileCopiedContext, NEW_FILE_COMMAND_ID, NEW_FOLDER_COMMAND_ID } from '../fileActions.js'; import * as DOM from '../../../../../base/browser/dom.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; @@ -988,7 +988,7 @@ export function createFileIconThemableTreeContainerScope(container: HTMLElement, const CanCreateContext = ContextKeyExpr.or( // Folder: can create unless readonly - ContextKeyExpr.and(ExplorerFolderContext, ExplorerResourceNotReadonlyContext), + ContextKeyExpr.and(ExplorerFolderContext, ExplorerResourceWritableContext), // File: can create unless parent is readonly ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ExplorerResourceParentReadOnlyContext.toNegated()) ); diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 56715560f15..0eb091699a5 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -40,7 +40,7 @@ export const ExplorerViewletVisibleContext = new RawContextKey('explore export const FoldersViewVisibleContext = new RawContextKey('foldersViewVisible', true, { type: 'boolean', description: localize('foldersViewVisible', "True when the FOLDERS view (the file tree within the explorer view container) is visible.") }); export const ExplorerFolderContext = new RawContextKey('explorerResourceIsFolder', false, { type: 'boolean', description: localize('explorerResourceIsFolder', "True when the focused item in the EXPLORER is a folder.") }); export const ExplorerResourceReadonlyContext = new RawContextKey('explorerResourceReadonly', false, { type: 'boolean', description: localize('explorerResourceReadonly', "True when the focused item in the EXPLORER is read-only.") }); -export const ExplorerResourceNotReadonlyContext = ExplorerResourceReadonlyContext.toNegated(); +export const ExplorerResourceWritableContext = ExplorerResourceReadonlyContext.toNegated(); export const ExplorerResourceParentReadOnlyContext = new RawContextKey('explorerResourceParentReadonly', false, { type: 'boolean', description: localize('explorerResourceParentReadonly', "True when the focused item in the EXPLORER's parent is read-only.") }); /**