Allow deleting files via the explorer when a folder is marked as readonly (fix #195701) (#237342)

pull/174286/head^2
Benjamin Pasero 2025-01-06 17:20:28 +01:00 committed by GitHub
parent e3bea63a7a
commit 85925efe02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 51 additions and 32 deletions

View File

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

View File

@ -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<void> {
async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, filesConfigurationService: IFilesConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise<void> {
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);
}
};

View File

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

View File

@ -40,7 +40,7 @@ export const ExplorerViewletVisibleContext = new RawContextKey<boolean>('explore
export const FoldersViewVisibleContext = new RawContextKey<boolean>('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<boolean>('explorerResourceIsFolder', false, { type: 'boolean', description: localize('explorerResourceIsFolder', "True when the focused item in the EXPLORER is a folder.") });
export const ExplorerResourceReadonlyContext = new RawContextKey<boolean>('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<boolean>('explorerResourceParentReadonly', false, { type: 'boolean', description: localize('explorerResourceParentReadonly', "True when the focused item in the EXPLORER's parent is read-only.") });
/**