Path: blob/main/src/vs/workbench/contrib/files/browser/fileActions.ts
5272 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../../../nls.js';6import { isWindows, OperatingSystem, OS } from '../../../../base/common/platform.js';7import { extname, basename, isAbsolute } from '../../../../base/common/path.js';8import * as resources from '../../../../base/common/resources.js';9import { URI } from '../../../../base/common/uri.js';10import { toErrorMessage } from '../../../../base/common/errorMessage.js';11import { Action } from '../../../../base/common/actions.js';12import { dispose, IDisposable } from '../../../../base/common/lifecycle.js';13import { VIEWLET_ID, IFilesConfiguration, VIEW_ID, UndoConfirmLevel } from '../common/files.js';14import { IFileService } from '../../../../platform/files/common/files.js';15import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js';16import { IQuickInputService, ItemActivation } from '../../../../platform/quickinput/common/quickInput.js';17import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';18import { ITextModel } from '../../../../editor/common/model.js';19import { IHostService } from '../../../services/host/browser/host.js';20import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from './fileConstants.js';21import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js';22import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';23import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';24import { ILanguageService } from '../../../../editor/common/languages/language.js';25import { IModelService } from '../../../../editor/common/services/model.js';26import { ICommandService, CommandsRegistry } from '../../../../platform/commands/common/commands.js';27import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';28import { Schemas } from '../../../../base/common/network.js';29import { IDialogService, IConfirmationResult, getFileNamesMessage } from '../../../../platform/dialogs/common/dialogs.js';30import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';31import { IEditorService } from '../../../services/editor/common/editorService.js';32import { Constants } from '../../../../base/common/uint.js';33import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from '../../../browser/parts/editor/editorCommands.js';34import { coalesce } from '../../../../base/common/arrays.js';35import { ExplorerItem, NewExplorerItem } from '../common/explorerModel.js';36import { getErrorMessage } from '../../../../base/common/errors.js';37import { triggerUpload } from '../../../../base/browser/dom.js';38import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js';39import { IWorkingCopyService } from '../../../services/workingCopy/common/workingCopyService.js';40import { IWorkingCopy } from '../../../services/workingCopy/common/workingCopy.js';41import { timeout } from '../../../../base/common/async.js';42import { IWorkingCopyFileService } from '../../../services/workingCopy/common/workingCopyFileService.js';43import { Codicon } from '../../../../base/common/codicons.js';44import { ThemeIcon } from '../../../../base/common/themables.js';45import { ViewContainerLocation } from '../../../common/views.js';46import { IViewsService } from '../../../services/views/common/viewsService.js';47import { trim, rtrim } from '../../../../base/common/strings.js';48import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';49import { ResourceFileEdit } from '../../../../editor/browser/services/bulkEditService.js';50import { IExplorerService } from './files.js';51import { BrowserFileUpload, FileDownload } from './fileImportExport.js';52import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';53import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';54import { IPathService } from '../../../services/path/common/pathService.js';55import { Action2 } from '../../../../platform/actions/common/actions.js';56import { ActiveEditorCanToggleReadonlyContext, ActiveEditorContext, EmptyWorkspaceSupportContext } from '../../../common/contextkeys.js';57import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';58import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';59import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';60import { ILocalizedString } from '../../../../platform/action/common/action.js';61import { VSBuffer } from '../../../../base/common/buffer.js';62import { getPathForFile } from '../../../../platform/dnd/browser/dnd.js';6364export const NEW_FILE_COMMAND_ID = 'explorer.newFile';65export const NEW_FILE_LABEL = nls.localize2('newFile', "New File...");66export const NEW_FOLDER_COMMAND_ID = 'explorer.newFolder';67export const NEW_FOLDER_LABEL = nls.localize2('newFolder', "New Folder...");68export const TRIGGER_RENAME_LABEL = nls.localize('rename', "Rename...");69export const MOVE_FILE_TO_TRASH_LABEL = nls.localize('delete', "Delete");70export const COPY_FILE_LABEL = nls.localize('copyFile', "Copy");71export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste");72export const FileCopiedContext = new RawContextKey<boolean>('fileCopied', false);73export const DOWNLOAD_COMMAND_ID = 'explorer.download';74export const DOWNLOAD_LABEL = nls.localize('download', "Download...");75export const UPLOAD_COMMAND_ID = 'explorer.upload';76export const UPLOAD_LABEL = nls.localize('upload', "Upload...");77const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete';78const MAX_UNDO_FILE_SIZE = 5000000; // 5mb7980async function refreshIfSeparator(value: string, explorerService: IExplorerService): Promise<void> {81if (value && ((value.indexOf('/') >= 0) || (value.indexOf('\\') >= 0))) {82// New input contains separator, multiple resources will get created workaround for #6820483await explorerService.refresh();84}85}8687async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, filesConfigurationService: IFilesConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise<void> {88let primaryButton: string;89if (useTrash) {90primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash");91} else {92primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete");93}9495// Handle dirty96const distinctElements = resources.distinctParents(elements, e => e.resource);97const dirtyWorkingCopies = new Set<IWorkingCopy>();98for (const distinctElement of distinctElements) {99for (const dirtyWorkingCopy of workingCopyFileService.getDirty(distinctElement.resource)) {100dirtyWorkingCopies.add(dirtyWorkingCopy);101}102}103104if (dirtyWorkingCopies.size) {105let message: string;106if (distinctElements.length > 1) {107message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?");108} else if (distinctElements[0].isDirectory) {109if (dirtyWorkingCopies.size === 1) {110message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder {0} with unsaved changes in 1 file. Do you want to continue?", distinctElements[0].name);111} else {112message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder {0} with unsaved changes in {1} files. Do you want to continue?", distinctElements[0].name, dirtyWorkingCopies.size);113}114} else {115message = nls.localize('dirtyMessageFileDelete', "You are deleting {0} with unsaved changes. Do you want to continue?", distinctElements[0].name);116}117118const response = await dialogService.confirm({119type: 'warning',120message,121detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),122primaryButton123});124125if (!response.confirmed) {126return;127} else {128skipConfirm = true;129}130}131132// Handle readonly133if (!skipConfirm) {134const readonlyResources = distinctElements.filter(e => filesConfigurationService.isReadonly(e.resource));135if (readonlyResources.length) {136let message: string;137if (readonlyResources.length > 1) {138message = nls.localize('readonlyMessageFilesDelete', "You are deleting files that are configured to be read-only. Do you want to continue?");139} else if (readonlyResources[0].isDirectory) {140message = nls.localize('readonlyMessageFolderOneDelete', "You are deleting a folder {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name);141} else {142message = nls.localize('readonlyMessageFolderDelete', "You are deleting a file {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name);143}144145const response = await dialogService.confirm({146type: 'warning',147message,148detail: nls.localize('continueDetail', "The read-only protection will be overridden if you continue."),149primaryButton: nls.localize('continueButtonLabel', "Continue")150});151152if (!response.confirmed) {153return;154}155}156}157158let confirmation: IConfirmationResult;159160// We do not support undo of folders, so in that case the delete action is irreversible161const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") :162distinctElements.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.");163164// Check if we need to ask for confirmation at all165if (skipConfirm || configurationService.getValue<boolean>(CONFIRM_DELETE_SETTING_KEY) === false) {166confirmation = { confirmed: true };167}168169// Confirm for moving to trash170else if (useTrash) {171let { message, detail } = getMoveToTrashMessage(distinctElements);172detail += detail ? '\n' : '';173if (isWindows) {174detail += distinctElements.length > 1 ? nls.localize('undoBinFiles', "You can restore these files from the Recycle Bin.") : nls.localize('undoBin', "You can restore this file from the Recycle Bin.");175} else {176detail += distinctElements.length > 1 ? nls.localize('undoTrashFiles', "You can restore these files from the Trash.") : nls.localize('undoTrash', "You can restore this file from the Trash.");177}178179confirmation = await dialogService.confirm({180message,181detail,182primaryButton,183checkbox: {184label: nls.localize('doNotAskAgain', "Do not ask me again")185}186});187}188189// Confirm for deleting permanently190else {191let { message, detail } = getDeleteMessage(distinctElements);192detail += detail ? '\n' : '';193detail += deleteDetail;194confirmation = await dialogService.confirm({195type: 'warning',196message,197detail,198primaryButton199});200}201202// Check for confirmation checkbox203if (confirmation.confirmed && confirmation.checkboxChecked === true) {204await configurationService.updateValue(CONFIRM_DELETE_SETTING_KEY, false);205}206207// Check for confirmation208if (!confirmation.confirmed) {209return;210}211212// Call function213try {214const resourceFileEdits = distinctElements.map(e => new ResourceFileEdit(e.resource, undefined, { recursive: true, folder: e.isDirectory, ignoreIfNotExists, skipTrashBin: !useTrash, maxSize: MAX_UNDO_FILE_SIZE }));215const options = {216undoLabel: distinctElements.length > 1 ? nls.localize({ key: 'deleteBulkEdit', comment: ['Placeholder will be replaced by the number of files deleted'] }, "Delete {0} files", distinctElements.length) : nls.localize({ key: 'deleteFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file deleted'] }, "Delete {0}", distinctElements[0].name),217progressLabel: distinctElements.length > 1 ? nls.localize({ key: 'deletingBulkEdit', comment: ['Placeholder will be replaced by the number of files deleted'] }, "Deleting {0} files", distinctElements.length) : nls.localize({ key: 'deletingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file deleted'] }, "Deleting {0}", distinctElements[0].name),218};219await explorerService.applyBulkEdit(resourceFileEdits, options);220} catch (error) {221222// Handle error to delete file(s) from a modal confirmation dialog223let errorMessage: string;224let detailMessage: string | undefined;225let primaryButton: string;226if (useTrash) {227errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?");228detailMessage = deleteDetail;229primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently");230} else {231errorMessage = toErrorMessage(error, false);232primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry");233}234235const res = await dialogService.confirm({236type: 'warning',237message: errorMessage,238detail: detailMessage,239primaryButton240});241242if (res.confirmed) {243if (useTrash) {244useTrash = false; // Delete Permanently245}246247skipConfirm = true;248ignoreIfNotExists = true;249250return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, filesConfigurationService, elements, useTrash, skipConfirm, ignoreIfNotExists);251}252}253}254255function getMoveToTrashMessage(distinctElements: ExplorerItem[]): { message: string; detail: string } {256if (containsBothDirectoryAndFile(distinctElements)) {257return {258message: nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length),259detail: getFileNamesMessage(distinctElements.map(e => e.resource))260};261}262263if (distinctElements.length > 1) {264if (distinctElements[0].isDirectory) {265return {266message: nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length),267detail: getFileNamesMessage(distinctElements.map(e => e.resource))268};269}270271return {272message: nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length),273detail: getFileNamesMessage(distinctElements.map(e => e.resource))274};275}276277if (distinctElements[0].isDirectory && !distinctElements[0].isSymbolicLink) {278return { message: nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name), detail: '' };279}280281return { message: nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name), detail: '' };282}283284function getDeleteMessage(distinctElements: ExplorerItem[]): { message: string; detail: string } {285if (containsBothDirectoryAndFile(distinctElements)) {286return {287message: nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length),288detail: getFileNamesMessage(distinctElements.map(e => e.resource))289};290}291292if (distinctElements.length > 1) {293if (distinctElements[0].isDirectory) {294return {295message: nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length),296detail: getFileNamesMessage(distinctElements.map(e => e.resource))297};298}299300return {301message: nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length),302detail: getFileNamesMessage(distinctElements.map(e => e.resource))303};304}305306if (distinctElements[0].isDirectory) {307return { message: nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name), detail: '' };308}309310return { message: nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name), detail: '' };311}312313function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean {314const directory = distinctElements.find(element => element.isDirectory);315const file = distinctElements.find(element => !element.isDirectory);316317return !!directory && !!file;318}319320321export async function findValidPasteFileTarget(322explorerService: IExplorerService,323fileService: IFileService,324dialogService: IDialogService,325targetFolder: ExplorerItem,326fileToPaste: { resource: URI | string; isDirectory?: boolean; allowOverwrite: boolean },327incrementalNaming: 'simple' | 'smart' | 'disabled'328): Promise<URI | undefined> {329330let name = typeof fileToPaste.resource === 'string' ? fileToPaste.resource : resources.basenameOrAuthority(fileToPaste.resource);331let candidate = resources.joinPath(targetFolder.resource, name);332333// In the disabled case we must ask if it's ok to overwrite the file if it exists334if (incrementalNaming === 'disabled') {335const canOverwrite = await askForOverwrite(fileService, dialogService, candidate);336if (!canOverwrite) {337return;338}339}340341while (true && !fileToPaste.allowOverwrite) {342if (!explorerService.findClosest(candidate)) {343break;344}345346if (incrementalNaming !== 'disabled') {347name = incrementFileName(name, !!fileToPaste.isDirectory, incrementalNaming);348}349candidate = resources.joinPath(targetFolder.resource, name);350}351352return candidate;353}354355export function incrementFileName(name: string, isFolder: boolean, incrementalNaming: 'simple' | 'smart'): string {356if (incrementalNaming === 'simple') {357let namePrefix = name;358let extSuffix = '';359if (!isFolder) {360extSuffix = extname(name);361namePrefix = basename(name, extSuffix);362}363364// name copy 5(.txt) => name copy 6(.txt)365// name copy(.txt) => name copy 2(.txt)366const suffixRegex = /^(.+ copy)( \d+)?$/;367if (suffixRegex.test(namePrefix)) {368return namePrefix.replace(suffixRegex, (match, g1?, g2?) => {369const number = (g2 ? parseInt(g2) : 1);370return number === 0371? `${g1}`372: (number < Constants.MAX_SAFE_SMALL_INTEGER373? `${g1} ${number + 1}`374: `${g1}${g2} copy`);375}) + extSuffix;376}377378// name(.txt) => name copy(.txt)379return `${namePrefix} copy${extSuffix}`;380}381382const separators = '[\\.\\-_]';383const maxNumber = Constants.MAX_SAFE_SMALL_INTEGER;384385// file.1.txt=>file.2.txt386const suffixFileRegex = RegExp('(.*' + separators + ')(\\d+)(\\..*)$');387if (!isFolder && name.match(suffixFileRegex)) {388return name.replace(suffixFileRegex, (match, g1?, g2?, g3?) => {389const number = parseInt(g2);390return number < maxNumber391? g1 + String(number + 1).padStart(g2.length, '0') + g3392: `${g1}${g2}.1${g3}`;393});394}395396// 1.file.txt=>2.file.txt397const prefixFileRegex = RegExp('(\\d+)(' + separators + '.*)(\\..*)$');398if (!isFolder && name.match(prefixFileRegex)) {399return name.replace(prefixFileRegex, (match, g1?, g2?, g3?) => {400const number = parseInt(g1);401return number < maxNumber402? String(number + 1).padStart(g1.length, '0') + g2 + g3403: `${g1}${g2}.1${g3}`;404});405}406407// 1.txt=>2.txt408const prefixFileNoNameRegex = RegExp('(\\d+)(\\..*)$');409if (!isFolder && name.match(prefixFileNoNameRegex)) {410return name.replace(prefixFileNoNameRegex, (match, g1?, g2?) => {411const number = parseInt(g1);412return number < maxNumber413? String(number + 1).padStart(g1.length, '0') + g2414: `${g1}.1${g2}`;415});416}417418// file.txt=>file.1.txt419const lastIndexOfDot = name.lastIndexOf('.');420if (!isFolder && lastIndexOfDot >= 0) {421return `${name.substr(0, lastIndexOfDot)}.1${name.substr(lastIndexOfDot)}`;422}423424// 123 => 124425const noNameNoExtensionRegex = RegExp('(\\d+)$');426if (!isFolder && lastIndexOfDot === -1 && name.match(noNameNoExtensionRegex)) {427return name.replace(noNameNoExtensionRegex, (match, g1?) => {428const number = parseInt(g1);429return number < maxNumber430? String(number + 1).padStart(g1.length, '0')431: `${g1}.1`;432});433}434435// file => file1436// file1 => file2437const noExtensionRegex = RegExp('(.*)(\\d*)$');438if (!isFolder && lastIndexOfDot === -1 && name.match(noExtensionRegex)) {439return name.replace(noExtensionRegex, (match, g1?, g2?) => {440let number = parseInt(g2);441if (isNaN(number)) {442number = 0;443}444return number < maxNumber445? g1 + String(number + 1).padStart(g2.length, '0')446: `${g1}${g2}.1`;447});448}449450// folder.1=>folder.2451if (isFolder && name.match(/(\d+)$/)) {452return name.replace(/(\d+)$/, (match, ...groups) => {453const number = parseInt(groups[0]);454return number < maxNumber455? String(number + 1).padStart(groups[0].length, '0')456: `${groups[0]}.1`;457});458}459460// 1.folder=>2.folder461if (isFolder && name.match(/^(\d+)/)) {462return name.replace(/^(\d+)(.*)$/, (match, ...groups) => {463const number = parseInt(groups[0]);464return number < maxNumber465? String(number + 1).padStart(groups[0].length, '0') + groups[1]466: `${groups[0]}${groups[1]}.1`;467});468}469470// file/folder=>file.1/folder.1471return `${name}.1`;472}473474/**475* Checks to see if the resource already exists, if so prompts the user if they would be ok with it being overwritten476* @param fileService The file service477* @param dialogService The dialog service478* @param targetResource The resource to be overwritten479* @return A boolean indicating if the user is ok with resource being overwritten, if the resource does not exist it returns true.480*/481async function askForOverwrite(fileService: IFileService, dialogService: IDialogService, targetResource: URI): Promise<boolean> {482const exists = await fileService.exists(targetResource);483if (!exists) {484return true;485}486// Ask for overwrite confirmation487const { confirmed } = await dialogService.confirm({488type: Severity.Warning,489message: nls.localize('confirmOverwrite', "A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", basename(targetResource.path)),490primaryButton: nls.localize('replaceButtonLabel', "&&Replace")491});492return confirmed;493}494495// Global Compare with496export class GlobalCompareResourcesAction extends Action2 {497498static readonly ID = 'workbench.files.action.compareFileWith';499static readonly LABEL = nls.localize2('globalCompareFile', "Compare Active File With...");500501constructor() {502super({503id: GlobalCompareResourcesAction.ID,504title: GlobalCompareResourcesAction.LABEL,505f1: true,506category: Categories.File,507precondition: ActiveEditorContext,508metadata: {509description: nls.localize2('compareFileWithMeta', "Opens a picker to select a file to diff with the active editor.")510}511});512}513514override async run(accessor: ServicesAccessor): Promise<void> {515const editorService = accessor.get(IEditorService);516const textModelService = accessor.get(ITextModelService);517const quickInputService = accessor.get(IQuickInputService);518519const activeInput = editorService.activeEditor;520const activeResource = EditorResourceAccessor.getOriginalUri(activeInput);521if (activeResource && textModelService.canHandleResource(activeResource)) {522const picks = await quickInputService.quickAccess.pick('', { itemActivation: ItemActivation.SECOND });523if (picks?.length === 1) {524const resource = (picks[0] as unknown as { resource: unknown }).resource;525if (URI.isUri(resource) && textModelService.canHandleResource(resource)) {526editorService.openEditor({527original: { resource: activeResource },528modified: { resource: resource },529options: { pinned: true }530});531}532}533}534}535}536537export class ToggleAutoSaveAction extends Action2 {538static readonly ID = 'workbench.action.toggleAutoSave';539540constructor() {541super({542id: ToggleAutoSaveAction.ID,543title: nls.localize2('toggleAutoSave', "Toggle Auto Save"),544f1: true,545category: Categories.File,546metadata: { description: nls.localize2('toggleAutoSaveDescription', "Toggle the ability to save files automatically after typing") }547});548}549550override run(accessor: ServicesAccessor): Promise<void> {551const filesConfigurationService = accessor.get(IFilesConfigurationService);552return filesConfigurationService.toggleAutoSave();553}554}555556abstract class BaseSaveAllAction extends Action {557private lastDirtyState: boolean;558559constructor(560id: string,561label: string,562@ICommandService protected commandService: ICommandService,563@INotificationService private notificationService: INotificationService,564@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService565) {566super(id, label);567568this.lastDirtyState = this.workingCopyService.hasDirty;569this.enabled = this.lastDirtyState;570571this.registerListeners();572}573574protected abstract doRun(context: unknown): Promise<void>;575576private registerListeners(): void {577578// update enablement based on working copy changes579this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.updateEnablement(workingCopy)));580}581582private updateEnablement(workingCopy: IWorkingCopy): void {583const hasDirty = workingCopy.isDirty() || this.workingCopyService.hasDirty;584if (this.lastDirtyState !== hasDirty) {585this.enabled = hasDirty;586this.lastDirtyState = this.enabled;587}588}589590override async run(context?: unknown): Promise<void> {591try {592await this.doRun(context);593} catch (error) {594this.notificationService.error(toErrorMessage(error, false));595}596}597}598599export class SaveAllInGroupAction extends BaseSaveAllAction {600601static readonly ID = 'workbench.files.action.saveAllInGroup';602static readonly LABEL = nls.localize('saveAllInGroup', "Save All in Group");603604override get class(): string {605return 'explorer-action ' + ThemeIcon.asClassName(Codicon.saveAll);606}607608protected doRun(context: unknown): Promise<void> {609return this.commandService.executeCommand(SAVE_ALL_IN_GROUP_COMMAND_ID, {}, context);610}611}612613export class CloseGroupAction extends Action {614615static readonly ID = 'workbench.files.action.closeGroup';616static readonly LABEL = nls.localize('closeGroup', "Close Group");617618constructor(id: string, label: string, @ICommandService private readonly commandService: ICommandService) {619super(id, label, ThemeIcon.asClassName(Codicon.closeAll));620}621622override run(context?: unknown): Promise<void> {623return this.commandService.executeCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, {}, context);624}625}626627export class FocusFilesExplorer extends Action2 {628629static readonly ID = 'workbench.files.action.focusFilesExplorer';630static readonly LABEL = nls.localize2('focusFilesExplorer', "Focus on Files Explorer");631632constructor() {633super({634id: FocusFilesExplorer.ID,635title: FocusFilesExplorer.LABEL,636f1: true,637category: Categories.File,638metadata: {639description: nls.localize2('focusFilesExplorerMetadata', "Moves focus to the file explorer view container.")640}641});642}643644override async run(accessor: ServicesAccessor): Promise<void> {645const paneCompositeService = accessor.get(IPaneCompositePartService);646await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true);647}648}649650export class ShowActiveFileInExplorer extends Action2 {651652static readonly ID = 'workbench.files.action.showActiveFileInExplorer';653static readonly LABEL = nls.localize2('showInExplorer', "Reveal Active File in Explorer View");654655constructor() {656super({657id: ShowActiveFileInExplorer.ID,658title: ShowActiveFileInExplorer.LABEL,659f1: true,660category: Categories.File,661metadata: {662description: nls.localize2('showInExplorerMetadata', "Reveals and selects the active file within the explorer view.")663}664});665}666667override async run(accessor: ServicesAccessor): Promise<void> {668const commandService = accessor.get(ICommandService);669const editorService = accessor.get(IEditorService);670const resource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });671if (resource) {672commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, resource);673}674}675}676677export class OpenActiveFileInEmptyWorkspace extends Action2 {678679static readonly ID = 'workbench.action.files.showOpenedFileInNewWindow';680static readonly LABEL = nls.localize2('openFileInEmptyWorkspace', "Open Active Editor in New Empty Workspace");681682constructor(683) {684super({685id: OpenActiveFileInEmptyWorkspace.ID,686title: OpenActiveFileInEmptyWorkspace.LABEL,687f1: true,688category: Categories.File,689precondition: EmptyWorkspaceSupportContext,690metadata: {691description: nls.localize2('openFileInEmptyWorkspaceMetadata', "Opens the active editor in a new window with no folders open.")692}693});694}695696override async run(accessor: ServicesAccessor): Promise<void> {697const editorService = accessor.get(IEditorService);698const hostService = accessor.get(IHostService);699const dialogService = accessor.get(IDialogService);700const fileService = accessor.get(IFileService);701702const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });703if (fileResource && fileService.hasProvider(fileResource)) {704hostService.openWindow([{ fileUri: fileResource }], { forceNewWindow: true });705} else {706dialogService.error(nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource."));707}708}709}710711export function validateFileName(pathService: IPathService, item: ExplorerItem, name: string, os: OperatingSystem): { content: string; severity: Severity } | null {712// Produce a well formed file name713name = getWellFormedFileName(name);714715// Name not provided716if (!name || name.length === 0 || /^\s+$/.test(name)) {717return {718content: nls.localize('emptyFileNameError', "A file or folder name must be provided."),719severity: Severity.Error720};721}722723// Relative paths only724if (name[0] === '/' || name[0] === '\\') {725return {726content: nls.localize('fileNameStartsWithSlashError', "A file or folder name cannot start with a slash."),727severity: Severity.Error728};729}730731const names = coalesce(name.split(/[\\/]/));732const parent = item.parent;733734if (name !== item.name) {735// Do not allow to overwrite existing file736const child = parent?.getChild(name);737if (child && child !== item) {738return {739content: nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name),740severity: Severity.Error741};742}743}744745// Check for invalid file name.746if (names.some(folderName => !pathService.hasValidBasename(item.resource, os, folderName))) {747// Escape * characters748const escapedName = name.replace(/\*/g, '\\*'); // CodeQL [SM02383] This only processes filenames which are enforced against having backslashes in them farther up in the stack.749return {750content: nls.localize('invalidFileNameError', "The name **{0}** is not valid as a file or folder name. Please choose a different name.", trimLongName(escapedName)),751severity: Severity.Error752};753}754755if (names.some(name => /^\s|\s$/.test(name))) {756return {757content: nls.localize('fileNameWhitespaceWarning', "Leading or trailing whitespace detected in file or folder name."),758severity: Severity.Warning759};760}761762return null;763}764765function trimLongName(name: string): string {766if (name?.length > 255) {767return `${name.substr(0, 255)}...`;768}769770return name;771}772773function getWellFormedFileName(filename: string): string {774if (!filename) {775return filename;776}777778// Trim tabs779filename = trim(filename, '\t');780781// Remove trailing slashes782filename = rtrim(filename, '/');783filename = rtrim(filename, '\\');784785return filename;786}787788export class CompareNewUntitledTextFilesAction extends Action2 {789790static readonly ID = 'workbench.files.action.compareNewUntitledTextFiles';791static readonly LABEL = nls.localize2('compareNewUntitledTextFiles', "Compare New Untitled Text Files");792793constructor() {794super({795id: CompareNewUntitledTextFilesAction.ID,796title: CompareNewUntitledTextFilesAction.LABEL,797f1: true,798category: Categories.File,799metadata: {800description: nls.localize2('compareNewUntitledTextFilesMeta', "Opens a new diff editor with two untitled files.")801}802});803}804805override async run(accessor: ServicesAccessor): Promise<void> {806const editorService = accessor.get(IEditorService);807808await editorService.openEditor({809original: { resource: undefined },810modified: { resource: undefined },811options: { pinned: true }812});813}814}815816export class CompareWithClipboardAction extends Action2 {817818static readonly ID = 'workbench.files.action.compareWithClipboard';819static readonly LABEL = nls.localize2('compareWithClipboard', "Compare Active File with Clipboard");820821private registrationDisposal: IDisposable | undefined;822private static SCHEME_COUNTER = 0;823824constructor() {825super({826id: CompareWithClipboardAction.ID,827title: CompareWithClipboardAction.LABEL,828f1: true,829category: Categories.File,830keybinding: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyC), weight: KeybindingWeight.WorkbenchContrib },831metadata: {832description: nls.localize2('compareWithClipboardMeta', "Opens a new diff editor to compare the active file with the contents of the clipboard.")833}834});835}836837override async run(accessor: ServicesAccessor): Promise<void> {838const editorService = accessor.get(IEditorService);839const instantiationService = accessor.get(IInstantiationService);840const textModelService = accessor.get(ITextModelService);841const fileService = accessor.get(IFileService);842843const resource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });844const scheme = `clipboardCompare${CompareWithClipboardAction.SCHEME_COUNTER++}`;845if (resource && (fileService.hasProvider(resource) || resource.scheme === Schemas.untitled)) {846if (!this.registrationDisposal) {847const provider = instantiationService.createInstance(ClipboardContentProvider);848this.registrationDisposal = textModelService.registerTextModelContentProvider(scheme, provider);849}850851const name = resources.basename(resource);852const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name);853854await editorService.openEditor({855original: { resource: resource.with({ scheme }) },856modified: { resource: resource },857label: editorLabel,858options: { pinned: true }859}).finally(() => {860dispose(this.registrationDisposal);861this.registrationDisposal = undefined;862});863}864}865866dispose(): void {867dispose(this.registrationDisposal);868this.registrationDisposal = undefined;869}870}871872class ClipboardContentProvider implements ITextModelContentProvider {873constructor(874@IClipboardService private readonly clipboardService: IClipboardService,875@ILanguageService private readonly languageService: ILanguageService,876@IModelService private readonly modelService: IModelService877) { }878879async provideTextContent(resource: URI): Promise<ITextModel> {880const text = await this.clipboardService.readText();881const model = this.modelService.createModel(text, this.languageService.createByFilepathOrFirstLine(resource), resource);882883return model;884}885}886887function onErrorWithRetry(notificationService: INotificationService, error: unknown, retry: () => Promise<unknown>): void {888notificationService.prompt(Severity.Error, toErrorMessage(error, false),889[{890label: nls.localize('retry', "Retry"),891run: () => retry()892}]893);894}895896async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise<void> {897const explorerService = accessor.get(IExplorerService);898const fileService = accessor.get(IFileService);899const configService = accessor.get(IConfigurationService);900const filesConfigService = accessor.get(IFilesConfigurationService);901const editorService = accessor.get(IEditorService);902const viewsService = accessor.get(IViewsService);903const notificationService = accessor.get(INotificationService);904const remoteAgentService = accessor.get(IRemoteAgentService);905const commandService = accessor.get(ICommandService);906const pathService = accessor.get(IPathService);907908const wasHidden = !viewsService.isViewVisible(VIEW_ID);909const view = await viewsService.openView(VIEW_ID, true);910if (wasHidden) {911// Give explorer some time to resolve itself #111218912await timeout(500);913}914if (!view) {915// Can happen in empty workspace case (https://github.com/microsoft/vscode/issues/100604)916917if (isFolder) {918throw new Error('Open a folder or workspace first.');919}920921return commandService.executeCommand(NEW_UNTITLED_FILE_COMMAND_ID);922}923924const stats = explorerService.getContext(false);925const stat = stats.length > 0 ? stats[0] : undefined;926let folder: ExplorerItem;927if (stat) {928folder = stat.isDirectory ? stat : (stat.parent || explorerService.roots[0]);929} else {930folder = explorerService.roots[0];931}932933if (folder.isReadonly) {934throw new Error('Parent folder is readonly.');935}936937const newStat = new NewExplorerItem(fileService, configService, filesConfigService, folder, isFolder);938folder.addChild(newStat);939940const onSuccess = async (value: string): Promise<void> => {941try {942const resourceToCreate = resources.joinPath(folder.resource, value);943if (value.endsWith('/')) {944isFolder = true;945}946await explorerService.applyBulkEdit([new ResourceFileEdit(undefined, resourceToCreate, { folder: isFolder })], {947undoLabel: nls.localize('createBulkEdit', "Create {0}", value),948progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value),949confirmBeforeUndo: true950});951await refreshIfSeparator(value, explorerService);952953if (isFolder) {954await explorerService.select(resourceToCreate, true);955} else {956await editorService.openEditor({ resource: resourceToCreate, options: { pinned: true } });957}958} catch (error) {959onErrorWithRetry(notificationService, error, () => onSuccess(value));960}961};962963const os = (await remoteAgentService.getEnvironment())?.os ?? OS;964965await explorerService.setEditable(newStat, {966validationMessage: value => validateFileName(pathService, newStat, value, os),967onFinish: async (value, success) => {968folder.removeChild(newStat);969await explorerService.setEditable(newStat, null);970if (success) {971onSuccess(value);972}973}974});975}976977CommandsRegistry.registerCommand({978id: NEW_FILE_COMMAND_ID,979handler: async (accessor) => {980await openExplorerAndCreate(accessor, false);981}982});983984CommandsRegistry.registerCommand({985id: NEW_FOLDER_COMMAND_ID,986handler: async (accessor) => {987await openExplorerAndCreate(accessor, true);988}989});990991export const renameHandler = async (accessor: ServicesAccessor) => {992const explorerService = accessor.get(IExplorerService);993const notificationService = accessor.get(INotificationService);994const remoteAgentService = accessor.get(IRemoteAgentService);995const pathService = accessor.get(IPathService);996const configurationService = accessor.get(IConfigurationService);997998const stats = explorerService.getContext(false);999const stat = stats.length > 0 ? stats[0] : undefined;1000if (!stat) {1001return;1002}10031004const os = (await remoteAgentService.getEnvironment())?.os ?? OS;10051006await explorerService.setEditable(stat, {1007validationMessage: value => validateFileName(pathService, stat, value, os),1008onFinish: async (value, success) => {1009if (success) {1010const parentResource = stat.parent!.resource;1011const targetResource = resources.joinPath(parentResource, value);1012if (stat.resource.toString() !== targetResource.toString()) {1013try {1014await explorerService.applyBulkEdit([new ResourceFileEdit(stat.resource, targetResource)], {1015confirmBeforeUndo: configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo === UndoConfirmLevel.Verbose,1016undoLabel: nls.localize('renameBulkEdit', "Rename {0} to {1}", stat.name, value),1017progressLabel: nls.localize('renamingBulkEdit', "Renaming {0} to {1}", stat.name, value),1018});1019await refreshIfSeparator(value, explorerService);1020} catch (e) {1021notificationService.error(e);1022}1023}1024}1025await explorerService.setEditable(stat, null);1026}1027});1028};10291030export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => {1031const explorerService = accessor.get(IExplorerService);1032const stats = explorerService.getContext(true).filter(s => !s.isRoot);1033if (stats.length) {1034await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, true);1035}1036};10371038export const deleteFileHandler = async (accessor: ServicesAccessor) => {1039const explorerService = accessor.get(IExplorerService);1040const stats = explorerService.getContext(true).filter(s => !s.isRoot);10411042if (stats.length) {1043await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, false);1044}1045};10461047let pasteShouldMove = false;1048export const copyFileHandler = async (accessor: ServicesAccessor) => {1049const explorerService = accessor.get(IExplorerService);1050const stats = explorerService.getContext(true);1051if (stats.length > 0) {1052await explorerService.setToCopy(stats, false);1053pasteShouldMove = false;1054}1055};10561057export const cutFileHandler = async (accessor: ServicesAccessor) => {1058const explorerService = accessor.get(IExplorerService);1059const stats = explorerService.getContext(true);1060if (stats.length > 0) {1061await explorerService.setToCopy(stats, true);1062pasteShouldMove = true;1063}1064};10651066const downloadFileHandler = async (accessor: ServicesAccessor) => {1067const explorerService = accessor.get(IExplorerService);1068const notificationService = accessor.get(INotificationService);1069const instantiationService = accessor.get(IInstantiationService);10701071const context = explorerService.getContext(true);1072const explorerItems = context.length ? context : explorerService.roots;10731074const downloadHandler = instantiationService.createInstance(FileDownload);10751076try {1077await downloadHandler.download(explorerItems);1078} catch (error) {1079notificationService.error(error);10801081throw error;1082}1083};10841085CommandsRegistry.registerCommand({1086id: DOWNLOAD_COMMAND_ID,1087handler: downloadFileHandler1088});10891090const uploadFileHandler = async (accessor: ServicesAccessor) => {1091const explorerService = accessor.get(IExplorerService);1092const notificationService = accessor.get(INotificationService);1093const instantiationService = accessor.get(IInstantiationService);10941095const context = explorerService.getContext(false);1096const element = context.length ? context[0] : explorerService.roots[0];10971098try {1099const files = await triggerUpload();1100if (files) {1101const browserUpload = instantiationService.createInstance(BrowserFileUpload);1102await browserUpload.upload(element, files);1103}1104} catch (error) {1105notificationService.error(error);11061107throw error;1108}1109};11101111CommandsRegistry.registerCommand({1112id: UPLOAD_COMMAND_ID,1113handler: uploadFileHandler1114});11151116export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: FileList) => {1117const clipboardService = accessor.get(IClipboardService);1118const explorerService = accessor.get(IExplorerService);1119const fileService = accessor.get(IFileService);1120const notificationService = accessor.get(INotificationService);1121const editorService = accessor.get(IEditorService);1122const configurationService = accessor.get(IConfigurationService);1123const uriIdentityService = accessor.get(IUriIdentityService);1124const dialogService = accessor.get(IDialogService);1125const hostService = accessor.get(IHostService);11261127const context = explorerService.getContext(false);1128const hasNativeFilesToPaste = fileList && fileList.length > 0;1129const confirmPasteNative = hasNativeFilesToPaste && configurationService.getValue<boolean>('explorer.confirmPasteNative');11301131const toPaste = await getFilesToPaste(fileList, clipboardService, hostService);11321133if (confirmPasteNative && toPaste.files.length >= 1) {1134const message = toPaste.files.length > 1 ?1135nls.localize('confirmMultiPasteNative', "Are you sure you want to paste the following {0} items?", toPaste.files.length) :1136nls.localize('confirmPasteNative', "Are you sure you want to paste '{0}'?", basename(toPaste.type === 'paths' ? toPaste.files[0].fsPath : toPaste.files[0].name));1137const detail = toPaste.files.length > 1 ? getFileNamesMessage(toPaste.files.map(item => {1138if (URI.isUri(item)) {1139return item.fsPath;1140}11411142if (toPaste.type === 'paths') {1143const path = getPathForFile(item);1144if (path) {1145return path;1146}1147}11481149return item.name;1150})) : undefined;1151const confirmation = await dialogService.confirm({1152message,1153detail,1154checkbox: {1155label: nls.localize('doNotAskAgain', "Do not ask me again")1156},1157primaryButton: nls.localize({ key: 'pasteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Paste")1158});11591160if (!confirmation.confirmed) {1161return;1162}11631164// Check for confirmation checkbox1165if (confirmation.checkboxChecked === true) {1166await configurationService.updateValue('explorer.confirmPasteNative', false);1167}1168}1169const element = context.length ? context[0] : explorerService.roots[0];1170const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;11711172const editableItem = explorerService.getEditable();1173// If it's an editable item, just do nothing1174if (editableItem) {1175return;1176}11771178try {1179let targets: URI[] = [];11801181if (toPaste.type === 'paths') { // Pasting from files on disk11821183// Check if target is ancestor of pasted folder1184const sourceTargetPairs = coalesce(await Promise.all(toPaste.files.map(async fileToPaste => {1185if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {1186throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));1187}1188const fileToPasteStat = await fileService.stat(fileToPaste);11891190// Find target1191let target: ExplorerItem;1192if (uriIdentityService.extUri.isEqual(element.resource, fileToPaste)) {1193target = element.parent!;1194} else {1195target = element.isDirectory ? element : element.parent!;1196}11971198const targetFile = await findValidPasteFileTarget(1199explorerService,1200fileService,1201dialogService,1202target,1203{ resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },1204incrementalNaming1205);12061207if (!targetFile) {1208return undefined;1209}12101211return { source: fileToPaste, target: targetFile };1212})));12131214if (sourceTargetPairs.length >= 1) {1215// Move/Copy File1216if (pasteShouldMove) {1217const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { overwrite: incrementalNaming === 'disabled' }));1218const options = {1219confirmBeforeUndo: configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo === UndoConfirmLevel.Verbose,1220progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'movingBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Moving {0} files", sourceTargetPairs.length)1221: nls.localize({ key: 'movingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)),1222undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'moveBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Move {0} files", sourceTargetPairs.length)1223: nls.localize({ key: 'moveFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target))1224};1225await explorerService.applyBulkEdit(resourceFileEdits, options);1226} else {1227const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true, overwrite: incrementalNaming === 'disabled' }));1228await applyCopyResourceEdit(sourceTargetPairs.map(pair => pair.target), resourceFileEdits);1229}1230}12311232targets = sourceTargetPairs.map(pair => pair.target);12331234} else { // Pasting from file data1235const targetAndEdits = coalesce(await Promise.all(toPaste.files.map(async file => {1236const target = element.isDirectory ? element : element.parent!;12371238const targetFile = await findValidPasteFileTarget(1239explorerService,1240fileService,1241dialogService,1242target,1243{ resource: file.name, isDirectory: false, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },1244incrementalNaming1245);1246if (!targetFile) {1247return;1248}1249return {1250target: targetFile,1251edit: new ResourceFileEdit(undefined, targetFile, {1252overwrite: incrementalNaming === 'disabled',1253contents: (async () => VSBuffer.wrap(new Uint8Array(await file.arrayBuffer())))(),1254})1255};1256})));12571258await applyCopyResourceEdit(targetAndEdits.map(pair => pair.target), targetAndEdits.map(pair => pair.edit));1259targets = targetAndEdits.map(pair => pair.target);1260}12611262if (targets.length) {1263const firstTarget = targets[0];1264await explorerService.select(firstTarget);1265if (targets.length === 1) {1266const item = explorerService.findClosest(firstTarget);1267if (item && !item.isDirectory) {1268await editorService.openEditor({ resource: item.resource, options: { pinned: true, preserveFocus: true } });1269}1270}1271}1272} catch (e) {1273notificationService.error(toErrorMessage(new Error(nls.localize('fileDeleted', "The file(s) to paste have been deleted or moved since you copied them. {0}", getErrorMessage(e))), false));1274} finally {1275if (pasteShouldMove) {1276// Cut is done. Make sure to clear cut state.1277await explorerService.setToCopy([], false);1278pasteShouldMove = false;1279}1280}12811282async function applyCopyResourceEdit(targets: readonly URI[], resourceFileEdits: ResourceFileEdit[]) {1283const undoLevel = configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo;1284const options = {1285confirmBeforeUndo: undoLevel === UndoConfirmLevel.Default || undoLevel === UndoConfirmLevel.Verbose,1286progressLabel: targets.length > 1 ? nls.localize({ key: 'copyingBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copying {0} files", targets.length)1287: nls.localize({ key: 'copyingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copying {0}", resources.basenameOrAuthority(targets[0])),1288undoLabel: targets.length > 1 ? nls.localize({ key: 'copyBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Paste {0} files", targets.length)1289: nls.localize({ key: 'copyFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Paste {0}", resources.basenameOrAuthority(targets[0]))1290};1291await explorerService.applyBulkEdit(resourceFileEdits, options);1292}1293};12941295type FilesToPaste =1296| { type: 'paths'; files: URI[] }1297| { type: 'data'; files: File[] };12981299async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService, hostService: IHostService): Promise<FilesToPaste> {1300if (fileList && fileList.length > 0) {1301// with a `fileList` we support natively pasting file from disk from clipboard1302const resources = [...fileList].map(file => getPathForFile(file)).filter(filePath => !!filePath && isAbsolute(filePath)).map((filePath) => URI.file(filePath!));1303if (resources.length) {1304return { type: 'paths', files: resources, };1305}13061307// Support pasting files that we can't read from disk1308return { type: 'data', files: [...fileList].filter(file => !getPathForFile(file)) };1309} else {1310// otherwise we fallback to reading resources from our clipboard service1311return { type: 'paths', files: resources.distinctParents(await clipboardService.readResources(), resource => resource) };1312}1313}13141315export const openFilePreserveFocusHandler = async (accessor: ServicesAccessor) => {1316const editorService = accessor.get(IEditorService);1317const explorerService = accessor.get(IExplorerService);1318const stats = explorerService.getContext(true);13191320await editorService.openEditors(stats.filter(s => !s.isDirectory).map(s => ({1321resource: s.resource,1322options: { preserveFocus: true }1323})));1324};13251326class BaseSetActiveEditorReadonlyInSession extends Action2 {13271328constructor(1329id: string,1330title: ILocalizedString,1331private readonly newReadonlyState: true | false | 'toggle' | 'reset'1332) {1333super({1334id,1335title,1336f1: true,1337category: Categories.File,1338precondition: ActiveEditorCanToggleReadonlyContext1339});1340}13411342override async run(accessor: ServicesAccessor): Promise<void> {1343const editorService = accessor.get(IEditorService);1344const filesConfigurationService = accessor.get(IFilesConfigurationService);13451346const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });1347if (!fileResource) {1348return;1349}13501351await filesConfigurationService.updateReadonly(fileResource, this.newReadonlyState);1352}1353}13541355export class SetActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {13561357static readonly ID = 'workbench.action.files.setActiveEditorReadonlyInSession';1358static readonly LABEL = nls.localize2('setActiveEditorReadonlyInSession', "Set Active Editor Read-only in Session");13591360constructor() {1361super(1362SetActiveEditorReadonlyInSession.ID,1363SetActiveEditorReadonlyInSession.LABEL,1364true1365);1366}1367}13681369export class SetActiveEditorWriteableInSession extends BaseSetActiveEditorReadonlyInSession {13701371static readonly ID = 'workbench.action.files.setActiveEditorWriteableInSession';1372static readonly LABEL = nls.localize2('setActiveEditorWriteableInSession', "Set Active Editor Writeable in Session");13731374constructor() {1375super(1376SetActiveEditorWriteableInSession.ID,1377SetActiveEditorWriteableInSession.LABEL,1378false1379);1380}1381}13821383export class ToggleActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {13841385static readonly ID = 'workbench.action.files.toggleActiveEditorReadonlyInSession';1386static readonly LABEL = nls.localize2('toggleActiveEditorReadonlyInSession', "Toggle Active Editor Read-only in Session");13871388constructor() {1389super(1390ToggleActiveEditorReadonlyInSession.ID,1391ToggleActiveEditorReadonlyInSession.LABEL,1392'toggle'1393);1394}1395}13961397export class ResetActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {13981399static readonly ID = 'workbench.action.files.resetActiveEditorReadonlyInSession';1400static readonly LABEL = nls.localize2('resetActiveEditorReadonlyInSession', "Reset Active Editor Read-only in Session");14011402constructor() {1403super(1404ResetActiveEditorReadonlyInSession.ID,1405ResetActiveEditorReadonlyInSession.LABEL,1406'reset'1407);1408}1409}141014111412