Path: blob/main/src/vs/workbench/contrib/files/browser/fileActions.ts
3296 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; // 5mb7980function onError(notificationService: INotificationService, error: any): void {81if (error.message === 'string') {82error = error.message;83}8485notificationService.error(toErrorMessage(error, false));86}8788async function refreshIfSeparator(value: string, explorerService: IExplorerService): Promise<void> {89if (value && ((value.indexOf('/') >= 0) || (value.indexOf('\\') >= 0))) {90// New input contains separator, multiple resources will get created workaround for #6820491await explorerService.refresh();92}93}9495async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, filesConfigurationService: IFilesConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise<void> {96let primaryButton: string;97if (useTrash) {98primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash");99} else {100primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete");101}102103// Handle dirty104const distinctElements = resources.distinctParents(elements, e => e.resource);105const dirtyWorkingCopies = new Set<IWorkingCopy>();106for (const distinctElement of distinctElements) {107for (const dirtyWorkingCopy of workingCopyFileService.getDirty(distinctElement.resource)) {108dirtyWorkingCopies.add(dirtyWorkingCopy);109}110}111112if (dirtyWorkingCopies.size) {113let message: string;114if (distinctElements.length > 1) {115message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?");116} else if (distinctElements[0].isDirectory) {117if (dirtyWorkingCopies.size === 1) {118message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder {0} with unsaved changes in 1 file. Do you want to continue?", distinctElements[0].name);119} else {120message = 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);121}122} else {123message = nls.localize('dirtyMessageFileDelete', "You are deleting {0} with unsaved changes. Do you want to continue?", distinctElements[0].name);124}125126const response = await dialogService.confirm({127type: 'warning',128message,129detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),130primaryButton131});132133if (!response.confirmed) {134return;135} else {136skipConfirm = true;137}138}139140// Handle readonly141if (!skipConfirm) {142const readonlyResources = distinctElements.filter(e => filesConfigurationService.isReadonly(e.resource));143if (readonlyResources.length) {144let message: string;145if (readonlyResources.length > 1) {146message = nls.localize('readonlyMessageFilesDelete', "You are deleting files that are configured to be read-only. Do you want to continue?");147} else if (readonlyResources[0].isDirectory) {148message = nls.localize('readonlyMessageFolderOneDelete', "You are deleting a folder {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name);149} else {150message = nls.localize('readonlyMessageFolderDelete', "You are deleting a file {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name);151}152153const response = await dialogService.confirm({154type: 'warning',155message,156detail: nls.localize('continueDetail', "The read-only protection will be overridden if you continue."),157primaryButton: nls.localize('continueButtonLabel', "Continue")158});159160if (!response.confirmed) {161return;162}163}164}165166let confirmation: IConfirmationResult;167168// We do not support undo of folders, so in that case the delete action is irreversible169const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") :170distinctElements.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.");171172// Check if we need to ask for confirmation at all173if (skipConfirm || (useTrash && configurationService.getValue<boolean>(CONFIRM_DELETE_SETTING_KEY) === false)) {174confirmation = { confirmed: true };175}176177// Confirm for moving to trash178else if (useTrash) {179let { message, detail } = getMoveToTrashMessage(distinctElements);180detail += detail ? '\n' : '';181if (isWindows) {182detail += 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.");183} else {184detail += 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.");185}186187confirmation = await dialogService.confirm({188message,189detail,190primaryButton,191checkbox: {192label: nls.localize('doNotAskAgain', "Do not ask me again")193}194});195}196197// Confirm for deleting permanently198else {199let { message, detail } = getDeleteMessage(distinctElements);200detail += detail ? '\n' : '';201detail += deleteDetail;202confirmation = await dialogService.confirm({203type: 'warning',204message,205detail,206primaryButton207});208}209210// Check for confirmation checkbox211if (confirmation.confirmed && confirmation.checkboxChecked === true) {212await configurationService.updateValue(CONFIRM_DELETE_SETTING_KEY, false);213}214215// Check for confirmation216if (!confirmation.confirmed) {217return;218}219220// Call function221try {222const resourceFileEdits = distinctElements.map(e => new ResourceFileEdit(e.resource, undefined, { recursive: true, folder: e.isDirectory, ignoreIfNotExists, skipTrashBin: !useTrash, maxSize: MAX_UNDO_FILE_SIZE }));223const options = {224undoLabel: 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),225progressLabel: 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),226};227await explorerService.applyBulkEdit(resourceFileEdits, options);228} catch (error) {229230// Handle error to delete file(s) from a modal confirmation dialog231let errorMessage: string;232let detailMessage: string | undefined;233let primaryButton: string;234if (useTrash) {235errorMessage = 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?");236detailMessage = deleteDetail;237primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently");238} else {239errorMessage = toErrorMessage(error, false);240primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry");241}242243const res = await dialogService.confirm({244type: 'warning',245message: errorMessage,246detail: detailMessage,247primaryButton248});249250if (res.confirmed) {251if (useTrash) {252useTrash = false; // Delete Permanently253}254255skipConfirm = true;256ignoreIfNotExists = true;257258return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, filesConfigurationService, elements, useTrash, skipConfirm, ignoreIfNotExists);259}260}261}262263function getMoveToTrashMessage(distinctElements: ExplorerItem[]): { message: string; detail: string } {264if (containsBothDirectoryAndFile(distinctElements)) {265return {266message: nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length),267detail: getFileNamesMessage(distinctElements.map(e => e.resource))268};269}270271if (distinctElements.length > 1) {272if (distinctElements[0].isDirectory) {273return {274message: nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length),275detail: getFileNamesMessage(distinctElements.map(e => e.resource))276};277}278279return {280message: nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length),281detail: getFileNamesMessage(distinctElements.map(e => e.resource))282};283}284285if (distinctElements[0].isDirectory && !distinctElements[0].isSymbolicLink) {286return { message: nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name), detail: '' };287}288289return { message: nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name), detail: '' };290}291292function getDeleteMessage(distinctElements: ExplorerItem[]): { message: string; detail: string } {293if (containsBothDirectoryAndFile(distinctElements)) {294return {295message: nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length),296detail: getFileNamesMessage(distinctElements.map(e => e.resource))297};298}299300if (distinctElements.length > 1) {301if (distinctElements[0].isDirectory) {302return {303message: nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length),304detail: getFileNamesMessage(distinctElements.map(e => e.resource))305};306}307308return {309message: nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length),310detail: getFileNamesMessage(distinctElements.map(e => e.resource))311};312}313314if (distinctElements[0].isDirectory) {315return { message: nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name), detail: '' };316}317318return { message: nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name), detail: '' };319}320321function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean {322const directory = distinctElements.find(element => element.isDirectory);323const file = distinctElements.find(element => !element.isDirectory);324325return !!directory && !!file;326}327328329export async function findValidPasteFileTarget(330explorerService: IExplorerService,331fileService: IFileService,332dialogService: IDialogService,333targetFolder: ExplorerItem,334fileToPaste: { resource: URI | string; isDirectory?: boolean; allowOverwrite: boolean },335incrementalNaming: 'simple' | 'smart' | 'disabled'336): Promise<URI | undefined> {337338let name = typeof fileToPaste.resource === 'string' ? fileToPaste.resource : resources.basenameOrAuthority(fileToPaste.resource);339let candidate = resources.joinPath(targetFolder.resource, name);340341// In the disabled case we must ask if it's ok to overwrite the file if it exists342if (incrementalNaming === 'disabled') {343const canOverwrite = await askForOverwrite(fileService, dialogService, candidate);344if (!canOverwrite) {345return;346}347}348349while (true && !fileToPaste.allowOverwrite) {350if (!explorerService.findClosest(candidate)) {351break;352}353354if (incrementalNaming !== 'disabled') {355name = incrementFileName(name, !!fileToPaste.isDirectory, incrementalNaming);356}357candidate = resources.joinPath(targetFolder.resource, name);358}359360return candidate;361}362363export function incrementFileName(name: string, isFolder: boolean, incrementalNaming: 'simple' | 'smart'): string {364if (incrementalNaming === 'simple') {365let namePrefix = name;366let extSuffix = '';367if (!isFolder) {368extSuffix = extname(name);369namePrefix = basename(name, extSuffix);370}371372// name copy 5(.txt) => name copy 6(.txt)373// name copy(.txt) => name copy 2(.txt)374const suffixRegex = /^(.+ copy)( \d+)?$/;375if (suffixRegex.test(namePrefix)) {376return namePrefix.replace(suffixRegex, (match, g1?, g2?) => {377const number = (g2 ? parseInt(g2) : 1);378return number === 0379? `${g1}`380: (number < Constants.MAX_SAFE_SMALL_INTEGER381? `${g1} ${number + 1}`382: `${g1}${g2} copy`);383}) + extSuffix;384}385386// name(.txt) => name copy(.txt)387return `${namePrefix} copy${extSuffix}`;388}389390const separators = '[\\.\\-_]';391const maxNumber = Constants.MAX_SAFE_SMALL_INTEGER;392393// file.1.txt=>file.2.txt394const suffixFileRegex = RegExp('(.*' + separators + ')(\\d+)(\\..*)$');395if (!isFolder && name.match(suffixFileRegex)) {396return name.replace(suffixFileRegex, (match, g1?, g2?, g3?) => {397const number = parseInt(g2);398return number < maxNumber399? g1 + String(number + 1).padStart(g2.length, '0') + g3400: `${g1}${g2}.1${g3}`;401});402}403404// 1.file.txt=>2.file.txt405const prefixFileRegex = RegExp('(\\d+)(' + separators + '.*)(\\..*)$');406if (!isFolder && name.match(prefixFileRegex)) {407return name.replace(prefixFileRegex, (match, g1?, g2?, g3?) => {408const number = parseInt(g1);409return number < maxNumber410? String(number + 1).padStart(g1.length, '0') + g2 + g3411: `${g1}${g2}.1${g3}`;412});413}414415// 1.txt=>2.txt416const prefixFileNoNameRegex = RegExp('(\\d+)(\\..*)$');417if (!isFolder && name.match(prefixFileNoNameRegex)) {418return name.replace(prefixFileNoNameRegex, (match, g1?, g2?) => {419const number = parseInt(g1);420return number < maxNumber421? String(number + 1).padStart(g1.length, '0') + g2422: `${g1}.1${g2}`;423});424}425426// file.txt=>file.1.txt427const lastIndexOfDot = name.lastIndexOf('.');428if (!isFolder && lastIndexOfDot >= 0) {429return `${name.substr(0, lastIndexOfDot)}.1${name.substr(lastIndexOfDot)}`;430}431432// 123 => 124433const noNameNoExtensionRegex = RegExp('(\\d+)$');434if (!isFolder && lastIndexOfDot === -1 && name.match(noNameNoExtensionRegex)) {435return name.replace(noNameNoExtensionRegex, (match, g1?) => {436const number = parseInt(g1);437return number < maxNumber438? String(number + 1).padStart(g1.length, '0')439: `${g1}.1`;440});441}442443// file => file1444// file1 => file2445const noExtensionRegex = RegExp('(.*)(\\d*)$');446if (!isFolder && lastIndexOfDot === -1 && name.match(noExtensionRegex)) {447return name.replace(noExtensionRegex, (match, g1?, g2?) => {448let number = parseInt(g2);449if (isNaN(number)) {450number = 0;451}452return number < maxNumber453? g1 + String(number + 1).padStart(g2.length, '0')454: `${g1}${g2}.1`;455});456}457458// folder.1=>folder.2459if (isFolder && name.match(/(\d+)$/)) {460return name.replace(/(\d+)$/, (match, ...groups) => {461const number = parseInt(groups[0]);462return number < maxNumber463? String(number + 1).padStart(groups[0].length, '0')464: `${groups[0]}.1`;465});466}467468// 1.folder=>2.folder469if (isFolder && name.match(/^(\d+)/)) {470return name.replace(/^(\d+)(.*)$/, (match, ...groups) => {471const number = parseInt(groups[0]);472return number < maxNumber473? String(number + 1).padStart(groups[0].length, '0') + groups[1]474: `${groups[0]}${groups[1]}.1`;475});476}477478// file/folder=>file.1/folder.1479return `${name}.1`;480}481482/**483* Checks to see if the resource already exists, if so prompts the user if they would be ok with it being overwritten484* @param fileService The file service485* @param dialogService The dialog service486* @param targetResource The resource to be overwritten487* @return A boolean indicating if the user is ok with resource being overwritten, if the resource does not exist it returns true.488*/489async function askForOverwrite(fileService: IFileService, dialogService: IDialogService, targetResource: URI): Promise<boolean> {490const exists = await fileService.exists(targetResource);491if (!exists) {492return true;493}494// Ask for overwrite confirmation495const { confirmed } = await dialogService.confirm({496type: Severity.Warning,497message: 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)),498primaryButton: nls.localize('replaceButtonLabel', "&&Replace")499});500return confirmed;501}502503// Global Compare with504export class GlobalCompareResourcesAction extends Action2 {505506static readonly ID = 'workbench.files.action.compareFileWith';507static readonly LABEL = nls.localize2('globalCompareFile', "Compare Active File With...");508509constructor() {510super({511id: GlobalCompareResourcesAction.ID,512title: GlobalCompareResourcesAction.LABEL,513f1: true,514category: Categories.File,515precondition: ActiveEditorContext,516metadata: {517description: nls.localize2('compareFileWithMeta', "Opens a picker to select a file to diff with the active editor.")518}519});520}521522override async run(accessor: ServicesAccessor): Promise<void> {523const editorService = accessor.get(IEditorService);524const textModelService = accessor.get(ITextModelService);525const quickInputService = accessor.get(IQuickInputService);526527const activeInput = editorService.activeEditor;528const activeResource = EditorResourceAccessor.getOriginalUri(activeInput);529if (activeResource && textModelService.canHandleResource(activeResource)) {530const picks = await quickInputService.quickAccess.pick('', { itemActivation: ItemActivation.SECOND });531if (picks?.length === 1) {532const resource = (picks[0] as unknown as { resource: unknown }).resource;533if (URI.isUri(resource) && textModelService.canHandleResource(resource)) {534editorService.openEditor({535original: { resource: activeResource },536modified: { resource: resource },537options: { pinned: true }538});539}540}541}542}543}544545export class ToggleAutoSaveAction extends Action2 {546static readonly ID = 'workbench.action.toggleAutoSave';547548constructor() {549super({550id: ToggleAutoSaveAction.ID,551title: nls.localize2('toggleAutoSave', "Toggle Auto Save"),552f1: true,553category: Categories.File,554metadata: { description: nls.localize2('toggleAutoSaveDescription', "Toggle the ability to save files automatically after typing") }555});556}557558override run(accessor: ServicesAccessor): Promise<void> {559const filesConfigurationService = accessor.get(IFilesConfigurationService);560return filesConfigurationService.toggleAutoSave();561}562}563564abstract class BaseSaveAllAction extends Action {565private lastDirtyState: boolean;566567constructor(568id: string,569label: string,570@ICommandService protected commandService: ICommandService,571@INotificationService private notificationService: INotificationService,572@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService573) {574super(id, label);575576this.lastDirtyState = this.workingCopyService.hasDirty;577this.enabled = this.lastDirtyState;578579this.registerListeners();580}581582protected abstract doRun(context: unknown): Promise<void>;583584private registerListeners(): void {585586// update enablement based on working copy changes587this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.updateEnablement(workingCopy)));588}589590private updateEnablement(workingCopy: IWorkingCopy): void {591const hasDirty = workingCopy.isDirty() || this.workingCopyService.hasDirty;592if (this.lastDirtyState !== hasDirty) {593this.enabled = hasDirty;594this.lastDirtyState = this.enabled;595}596}597598override async run(context?: unknown): Promise<void> {599try {600await this.doRun(context);601} catch (error) {602onError(this.notificationService, error);603}604}605}606607export class SaveAllInGroupAction extends BaseSaveAllAction {608609static readonly ID = 'workbench.files.action.saveAllInGroup';610static readonly LABEL = nls.localize('saveAllInGroup', "Save All in Group");611612override get class(): string {613return 'explorer-action ' + ThemeIcon.asClassName(Codicon.saveAll);614}615616protected doRun(context: unknown): Promise<void> {617return this.commandService.executeCommand(SAVE_ALL_IN_GROUP_COMMAND_ID, {}, context);618}619}620621export class CloseGroupAction extends Action {622623static readonly ID = 'workbench.files.action.closeGroup';624static readonly LABEL = nls.localize('closeGroup', "Close Group");625626constructor(id: string, label: string, @ICommandService private readonly commandService: ICommandService) {627super(id, label, ThemeIcon.asClassName(Codicon.closeAll));628}629630override run(context?: unknown): Promise<void> {631return this.commandService.executeCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, {}, context);632}633}634635export class FocusFilesExplorer extends Action2 {636637static readonly ID = 'workbench.files.action.focusFilesExplorer';638static readonly LABEL = nls.localize2('focusFilesExplorer', "Focus on Files Explorer");639640constructor() {641super({642id: FocusFilesExplorer.ID,643title: FocusFilesExplorer.LABEL,644f1: true,645category: Categories.File,646metadata: {647description: nls.localize2('focusFilesExplorerMetadata', "Moves focus to the file explorer view container.")648}649});650}651652override async run(accessor: ServicesAccessor): Promise<void> {653const paneCompositeService = accessor.get(IPaneCompositePartService);654await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true);655}656}657658export class ShowActiveFileInExplorer extends Action2 {659660static readonly ID = 'workbench.files.action.showActiveFileInExplorer';661static readonly LABEL = nls.localize2('showInExplorer', "Reveal Active File in Explorer View");662663constructor() {664super({665id: ShowActiveFileInExplorer.ID,666title: ShowActiveFileInExplorer.LABEL,667f1: true,668category: Categories.File,669metadata: {670description: nls.localize2('showInExplorerMetadata', "Reveals and selects the active file within the explorer view.")671}672});673}674675override async run(accessor: ServicesAccessor): Promise<void> {676const commandService = accessor.get(ICommandService);677const editorService = accessor.get(IEditorService);678const resource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });679if (resource) {680commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, resource);681}682}683}684685export class OpenActiveFileInEmptyWorkspace extends Action2 {686687static readonly ID = 'workbench.action.files.showOpenedFileInNewWindow';688static readonly LABEL = nls.localize2('openFileInEmptyWorkspace', "Open Active File in New Empty Workspace");689690constructor(691) {692super({693id: OpenActiveFileInEmptyWorkspace.ID,694title: OpenActiveFileInEmptyWorkspace.LABEL,695f1: true,696category: Categories.File,697precondition: EmptyWorkspaceSupportContext,698metadata: {699description: nls.localize2('openFileInEmptyWorkspaceMetadata', "Opens the active file in a new window with no folders open.")700}701});702}703704override async run(accessor: ServicesAccessor): Promise<void> {705const editorService = accessor.get(IEditorService);706const hostService = accessor.get(IHostService);707const dialogService = accessor.get(IDialogService);708const fileService = accessor.get(IFileService);709710const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });711if (fileResource) {712if (fileService.hasProvider(fileResource)) {713hostService.openWindow([{ fileUri: fileResource }], { forceNewWindow: true });714} else {715dialogService.error(nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource."));716}717}718}719}720721export function validateFileName(pathService: IPathService, item: ExplorerItem, name: string, os: OperatingSystem): { content: string; severity: Severity } | null {722// Produce a well formed file name723name = getWellFormedFileName(name);724725// Name not provided726if (!name || name.length === 0 || /^\s+$/.test(name)) {727return {728content: nls.localize('emptyFileNameError', "A file or folder name must be provided."),729severity: Severity.Error730};731}732733// Relative paths only734if (name[0] === '/' || name[0] === '\\') {735return {736content: nls.localize('fileNameStartsWithSlashError', "A file or folder name cannot start with a slash."),737severity: Severity.Error738};739}740741const names = coalesce(name.split(/[\\/]/));742const parent = item.parent;743744if (name !== item.name) {745// Do not allow to overwrite existing file746const child = parent?.getChild(name);747if (child && child !== item) {748return {749content: nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name),750severity: Severity.Error751};752}753}754755// Check for invalid file name.756if (names.some(folderName => !pathService.hasValidBasename(item.resource, os, folderName))) {757// Escape * characters758const escapedName = name.replace(/\*/g, '\\*'); // CodeQL [SM02383] This only processes filenames which are enforced against having backslashes in them farther up in the stack.759return {760content: nls.localize('invalidFileNameError', "The name **{0}** is not valid as a file or folder name. Please choose a different name.", trimLongName(escapedName)),761severity: Severity.Error762};763}764765if (names.some(name => /^\s|\s$/.test(name))) {766return {767content: nls.localize('fileNameWhitespaceWarning', "Leading or trailing whitespace detected in file or folder name."),768severity: Severity.Warning769};770}771772return null;773}774775function trimLongName(name: string): string {776if (name?.length > 255) {777return `${name.substr(0, 255)}...`;778}779780return name;781}782783function getWellFormedFileName(filename: string): string {784if (!filename) {785return filename;786}787788// Trim tabs789filename = trim(filename, '\t');790791// Remove trailing slashes792filename = rtrim(filename, '/');793filename = rtrim(filename, '\\');794795return filename;796}797798export class CompareNewUntitledTextFilesAction extends Action2 {799800static readonly ID = 'workbench.files.action.compareNewUntitledTextFiles';801static readonly LABEL = nls.localize2('compareNewUntitledTextFiles', "Compare New Untitled Text Files");802803constructor() {804super({805id: CompareNewUntitledTextFilesAction.ID,806title: CompareNewUntitledTextFilesAction.LABEL,807f1: true,808category: Categories.File,809metadata: {810description: nls.localize2('compareNewUntitledTextFilesMeta', "Opens a new diff editor with two untitled files.")811}812});813}814815override async run(accessor: ServicesAccessor): Promise<void> {816const editorService = accessor.get(IEditorService);817818await editorService.openEditor({819original: { resource: undefined },820modified: { resource: undefined },821options: { pinned: true }822});823}824}825826export class CompareWithClipboardAction extends Action2 {827828static readonly ID = 'workbench.files.action.compareWithClipboard';829static readonly LABEL = nls.localize2('compareWithClipboard', "Compare Active File with Clipboard");830831private registrationDisposal: IDisposable | undefined;832private static SCHEME_COUNTER = 0;833834constructor() {835super({836id: CompareWithClipboardAction.ID,837title: CompareWithClipboardAction.LABEL,838f1: true,839category: Categories.File,840keybinding: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyC), weight: KeybindingWeight.WorkbenchContrib },841metadata: {842description: nls.localize2('compareWithClipboardMeta', "Opens a new diff editor to compare the active file with the contents of the clipboard.")843}844});845}846847override async run(accessor: ServicesAccessor): Promise<void> {848const editorService = accessor.get(IEditorService);849const instantiationService = accessor.get(IInstantiationService);850const textModelService = accessor.get(ITextModelService);851const fileService = accessor.get(IFileService);852853const resource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });854const scheme = `clipboardCompare${CompareWithClipboardAction.SCHEME_COUNTER++}`;855if (resource && (fileService.hasProvider(resource) || resource.scheme === Schemas.untitled)) {856if (!this.registrationDisposal) {857const provider = instantiationService.createInstance(ClipboardContentProvider);858this.registrationDisposal = textModelService.registerTextModelContentProvider(scheme, provider);859}860861const name = resources.basename(resource);862const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name);863864await editorService.openEditor({865original: { resource: resource.with({ scheme }) },866modified: { resource: resource },867label: editorLabel,868options: { pinned: true }869}).finally(() => {870dispose(this.registrationDisposal);871this.registrationDisposal = undefined;872});873}874}875876dispose(): void {877dispose(this.registrationDisposal);878this.registrationDisposal = undefined;879}880}881882class ClipboardContentProvider implements ITextModelContentProvider {883constructor(884@IClipboardService private readonly clipboardService: IClipboardService,885@ILanguageService private readonly languageService: ILanguageService,886@IModelService private readonly modelService: IModelService887) { }888889async provideTextContent(resource: URI): Promise<ITextModel> {890const text = await this.clipboardService.readText();891const model = this.modelService.createModel(text, this.languageService.createByFilepathOrFirstLine(resource), resource);892893return model;894}895}896897function onErrorWithRetry(notificationService: INotificationService, error: unknown, retry: () => Promise<unknown>): void {898notificationService.prompt(Severity.Error, toErrorMessage(error, false),899[{900label: nls.localize('retry', "Retry"),901run: () => retry()902}]903);904}905906async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise<void> {907const explorerService = accessor.get(IExplorerService);908const fileService = accessor.get(IFileService);909const configService = accessor.get(IConfigurationService);910const filesConfigService = accessor.get(IFilesConfigurationService);911const editorService = accessor.get(IEditorService);912const viewsService = accessor.get(IViewsService);913const notificationService = accessor.get(INotificationService);914const remoteAgentService = accessor.get(IRemoteAgentService);915const commandService = accessor.get(ICommandService);916const pathService = accessor.get(IPathService);917918const wasHidden = !viewsService.isViewVisible(VIEW_ID);919const view = await viewsService.openView(VIEW_ID, true);920if (wasHidden) {921// Give explorer some time to resolve itself #111218922await timeout(500);923}924if (!view) {925// Can happen in empty workspace case (https://github.com/microsoft/vscode/issues/100604)926927if (isFolder) {928throw new Error('Open a folder or workspace first.');929}930931return commandService.executeCommand(NEW_UNTITLED_FILE_COMMAND_ID);932}933934const stats = explorerService.getContext(false);935const stat = stats.length > 0 ? stats[0] : undefined;936let folder: ExplorerItem;937if (stat) {938folder = stat.isDirectory ? stat : (stat.parent || explorerService.roots[0]);939} else {940folder = explorerService.roots[0];941}942943if (folder.isReadonly) {944throw new Error('Parent folder is readonly.');945}946947const newStat = new NewExplorerItem(fileService, configService, filesConfigService, folder, isFolder);948folder.addChild(newStat);949950const onSuccess = async (value: string): Promise<void> => {951try {952const resourceToCreate = resources.joinPath(folder.resource, value);953if (value.endsWith('/')) {954isFolder = true;955}956await explorerService.applyBulkEdit([new ResourceFileEdit(undefined, resourceToCreate, { folder: isFolder })], {957undoLabel: nls.localize('createBulkEdit', "Create {0}", value),958progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value),959confirmBeforeUndo: true960});961await refreshIfSeparator(value, explorerService);962963if (isFolder) {964await explorerService.select(resourceToCreate, true);965} else {966await editorService.openEditor({ resource: resourceToCreate, options: { pinned: true } });967}968} catch (error) {969onErrorWithRetry(notificationService, error, () => onSuccess(value));970}971};972973const os = (await remoteAgentService.getEnvironment())?.os ?? OS;974975await explorerService.setEditable(newStat, {976validationMessage: value => validateFileName(pathService, newStat, value, os),977onFinish: async (value, success) => {978folder.removeChild(newStat);979await explorerService.setEditable(newStat, null);980if (success) {981onSuccess(value);982}983}984});985}986987CommandsRegistry.registerCommand({988id: NEW_FILE_COMMAND_ID,989handler: async (accessor) => {990await openExplorerAndCreate(accessor, false);991}992});993994CommandsRegistry.registerCommand({995id: NEW_FOLDER_COMMAND_ID,996handler: async (accessor) => {997await openExplorerAndCreate(accessor, true);998}999});10001001export const renameHandler = async (accessor: ServicesAccessor) => {1002const explorerService = accessor.get(IExplorerService);1003const notificationService = accessor.get(INotificationService);1004const remoteAgentService = accessor.get(IRemoteAgentService);1005const pathService = accessor.get(IPathService);1006const configurationService = accessor.get(IConfigurationService);10071008const stats = explorerService.getContext(false);1009const stat = stats.length > 0 ? stats[0] : undefined;1010if (!stat) {1011return;1012}10131014const os = (await remoteAgentService.getEnvironment())?.os ?? OS;10151016await explorerService.setEditable(stat, {1017validationMessage: value => validateFileName(pathService, stat, value, os),1018onFinish: async (value, success) => {1019if (success) {1020const parentResource = stat.parent!.resource;1021const targetResource = resources.joinPath(parentResource, value);1022if (stat.resource.toString() !== targetResource.toString()) {1023try {1024await explorerService.applyBulkEdit([new ResourceFileEdit(stat.resource, targetResource)], {1025confirmBeforeUndo: configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo === UndoConfirmLevel.Verbose,1026undoLabel: nls.localize('renameBulkEdit', "Rename {0} to {1}", stat.name, value),1027progressLabel: nls.localize('renamingBulkEdit', "Renaming {0} to {1}", stat.name, value),1028});1029await refreshIfSeparator(value, explorerService);1030} catch (e) {1031notificationService.error(e);1032}1033}1034}1035await explorerService.setEditable(stat, null);1036}1037});1038};10391040export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => {1041const explorerService = accessor.get(IExplorerService);1042const stats = explorerService.getContext(true).filter(s => !s.isRoot);1043if (stats.length) {1044await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, true);1045}1046};10471048export const deleteFileHandler = async (accessor: ServicesAccessor) => {1049const explorerService = accessor.get(IExplorerService);1050const stats = explorerService.getContext(true).filter(s => !s.isRoot);10511052if (stats.length) {1053await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, false);1054}1055};10561057let pasteShouldMove = false;1058export const copyFileHandler = async (accessor: ServicesAccessor) => {1059const explorerService = accessor.get(IExplorerService);1060const stats = explorerService.getContext(true);1061if (stats.length > 0) {1062await explorerService.setToCopy(stats, false);1063pasteShouldMove = false;1064}1065};10661067export const cutFileHandler = async (accessor: ServicesAccessor) => {1068const explorerService = accessor.get(IExplorerService);1069const stats = explorerService.getContext(true);1070if (stats.length > 0) {1071await explorerService.setToCopy(stats, true);1072pasteShouldMove = true;1073}1074};10751076const downloadFileHandler = async (accessor: ServicesAccessor) => {1077const explorerService = accessor.get(IExplorerService);1078const notificationService = accessor.get(INotificationService);1079const instantiationService = accessor.get(IInstantiationService);10801081const context = explorerService.getContext(true);1082const explorerItems = context.length ? context : explorerService.roots;10831084const downloadHandler = instantiationService.createInstance(FileDownload);10851086try {1087await downloadHandler.download(explorerItems);1088} catch (error) {1089notificationService.error(error);10901091throw error;1092}1093};10941095CommandsRegistry.registerCommand({1096id: DOWNLOAD_COMMAND_ID,1097handler: downloadFileHandler1098});10991100const uploadFileHandler = async (accessor: ServicesAccessor) => {1101const explorerService = accessor.get(IExplorerService);1102const notificationService = accessor.get(INotificationService);1103const instantiationService = accessor.get(IInstantiationService);11041105const context = explorerService.getContext(false);1106const element = context.length ? context[0] : explorerService.roots[0];11071108try {1109const files = await triggerUpload();1110if (files) {1111const browserUpload = instantiationService.createInstance(BrowserFileUpload);1112await browserUpload.upload(element, files);1113}1114} catch (error) {1115notificationService.error(error);11161117throw error;1118}1119};11201121CommandsRegistry.registerCommand({1122id: UPLOAD_COMMAND_ID,1123handler: uploadFileHandler1124});11251126export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: FileList) => {1127const clipboardService = accessor.get(IClipboardService);1128const explorerService = accessor.get(IExplorerService);1129const fileService = accessor.get(IFileService);1130const notificationService = accessor.get(INotificationService);1131const editorService = accessor.get(IEditorService);1132const configurationService = accessor.get(IConfigurationService);1133const uriIdentityService = accessor.get(IUriIdentityService);1134const dialogService = accessor.get(IDialogService);1135const hostService = accessor.get(IHostService);11361137const context = explorerService.getContext(false);1138const hasNativeFilesToPaste = fileList && fileList.length > 0;1139const confirmPasteNative = hasNativeFilesToPaste && configurationService.getValue<boolean>('explorer.confirmPasteNative');11401141const toPaste = await getFilesToPaste(fileList, clipboardService, hostService);11421143if (confirmPasteNative && toPaste.files.length >= 1) {1144const message = toPaste.files.length > 1 ?1145nls.localize('confirmMultiPasteNative', "Are you sure you want to paste the following {0} items?", toPaste.files.length) :1146nls.localize('confirmPasteNative', "Are you sure you want to paste '{0}'?", basename(toPaste.type === 'paths' ? toPaste.files[0].fsPath : toPaste.files[0].name));1147const detail = toPaste.files.length > 1 ? getFileNamesMessage(toPaste.files.map(item => {1148if (URI.isUri(item)) {1149return item.fsPath;1150}11511152if (toPaste.type === 'paths') {1153const path = getPathForFile(item);1154if (path) {1155return path;1156}1157}11581159return item.name;1160})) : undefined;1161const confirmation = await dialogService.confirm({1162message,1163detail,1164checkbox: {1165label: nls.localize('doNotAskAgain', "Do not ask me again")1166},1167primaryButton: nls.localize({ key: 'pasteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Paste")1168});11691170if (!confirmation.confirmed) {1171return;1172}11731174// Check for confirmation checkbox1175if (confirmation.checkboxChecked === true) {1176await configurationService.updateValue('explorer.confirmPasteNative', false);1177}1178}1179const element = context.length ? context[0] : explorerService.roots[0];1180const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;11811182const editableItem = explorerService.getEditable();1183// If it's an editable item, just do nothing1184if (editableItem) {1185return;1186}11871188try {1189let targets: URI[] = [];11901191if (toPaste.type === 'paths') { // Pasting from files on disk11921193// Check if target is ancestor of pasted folder1194const sourceTargetPairs = coalesce(await Promise.all(toPaste.files.map(async fileToPaste => {1195if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {1196throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));1197}1198const fileToPasteStat = await fileService.stat(fileToPaste);11991200// Find target1201let target: ExplorerItem;1202if (uriIdentityService.extUri.isEqual(element.resource, fileToPaste)) {1203target = element.parent!;1204} else {1205target = element.isDirectory ? element : element.parent!;1206}12071208const targetFile = await findValidPasteFileTarget(1209explorerService,1210fileService,1211dialogService,1212target,1213{ resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },1214incrementalNaming1215);12161217if (!targetFile) {1218return undefined;1219}12201221return { source: fileToPaste, target: targetFile };1222})));12231224if (sourceTargetPairs.length >= 1) {1225// Move/Copy File1226if (pasteShouldMove) {1227const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { overwrite: incrementalNaming === 'disabled' }));1228const options = {1229confirmBeforeUndo: configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo === UndoConfirmLevel.Verbose,1230progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'movingBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Moving {0} files", sourceTargetPairs.length)1231: nls.localize({ key: 'movingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)),1232undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'moveBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Move {0} files", sourceTargetPairs.length)1233: nls.localize({ key: 'moveFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target))1234};1235await explorerService.applyBulkEdit(resourceFileEdits, options);1236} else {1237const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true, overwrite: incrementalNaming === 'disabled' }));1238await applyCopyResourceEdit(sourceTargetPairs.map(pair => pair.target), resourceFileEdits);1239}1240}12411242targets = sourceTargetPairs.map(pair => pair.target);12431244} else { // Pasting from file data1245const targetAndEdits = coalesce(await Promise.all(toPaste.files.map(async file => {1246const target = element.isDirectory ? element : element.parent!;12471248const targetFile = await findValidPasteFileTarget(1249explorerService,1250fileService,1251dialogService,1252target,1253{ resource: file.name, isDirectory: false, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },1254incrementalNaming1255);1256if (!targetFile) {1257return;1258}1259return {1260target: targetFile,1261edit: new ResourceFileEdit(undefined, targetFile, {1262overwrite: incrementalNaming === 'disabled',1263contents: (async () => VSBuffer.wrap(new Uint8Array(await file.arrayBuffer())))(),1264})1265};1266})));12671268await applyCopyResourceEdit(targetAndEdits.map(pair => pair.target), targetAndEdits.map(pair => pair.edit));1269targets = targetAndEdits.map(pair => pair.target);1270}12711272if (targets.length) {1273const firstTarget = targets[0];1274await explorerService.select(firstTarget);1275if (targets.length === 1) {1276const item = explorerService.findClosest(firstTarget);1277if (item && !item.isDirectory) {1278await editorService.openEditor({ resource: item.resource, options: { pinned: true, preserveFocus: true } });1279}1280}1281}1282} catch (e) {1283onError(notificationService, new Error(nls.localize('fileDeleted', "The file(s) to paste have been deleted or moved since you copied them. {0}", getErrorMessage(e))));1284} finally {1285if (pasteShouldMove) {1286// Cut is done. Make sure to clear cut state.1287await explorerService.setToCopy([], false);1288pasteShouldMove = false;1289}1290}12911292async function applyCopyResourceEdit(targets: readonly URI[], resourceFileEdits: ResourceFileEdit[]) {1293const undoLevel = configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo;1294const options = {1295confirmBeforeUndo: undoLevel === UndoConfirmLevel.Default || undoLevel === UndoConfirmLevel.Verbose,1296progressLabel: targets.length > 1 ? nls.localize({ key: 'copyingBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copying {0} files", targets.length)1297: nls.localize({ key: 'copyingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copying {0}", resources.basenameOrAuthority(targets[0])),1298undoLabel: targets.length > 1 ? nls.localize({ key: 'copyBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Paste {0} files", targets.length)1299: nls.localize({ key: 'copyFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Paste {0}", resources.basenameOrAuthority(targets[0]))1300};1301await explorerService.applyBulkEdit(resourceFileEdits, options);1302}1303};13041305type FilesToPaste =1306| { type: 'paths'; files: URI[] }1307| { type: 'data'; files: File[] };13081309async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService, hostService: IHostService): Promise<FilesToPaste> {1310if (fileList && fileList.length > 0) {1311// with a `fileList` we support natively pasting file from disk from clipboard1312const resources = [...fileList].map(file => getPathForFile(file)).filter(filePath => !!filePath && isAbsolute(filePath)).map((filePath) => URI.file(filePath!));1313if (resources.length) {1314return { type: 'paths', files: resources, };1315}13161317// Support pasting files that we can't read from disk1318return { type: 'data', files: [...fileList].filter(file => !getPathForFile(file)) };1319} else {1320// otherwise we fallback to reading resources from our clipboard service1321return { type: 'paths', files: resources.distinctParents(await clipboardService.readResources(), resource => resource) };1322}1323}13241325export const openFilePreserveFocusHandler = async (accessor: ServicesAccessor) => {1326const editorService = accessor.get(IEditorService);1327const explorerService = accessor.get(IExplorerService);1328const stats = explorerService.getContext(true);13291330await editorService.openEditors(stats.filter(s => !s.isDirectory).map(s => ({1331resource: s.resource,1332options: { preserveFocus: true }1333})));1334};13351336class BaseSetActiveEditorReadonlyInSession extends Action2 {13371338constructor(1339id: string,1340title: ILocalizedString,1341private readonly newReadonlyState: true | false | 'toggle' | 'reset'1342) {1343super({1344id,1345title,1346f1: true,1347category: Categories.File,1348precondition: ActiveEditorCanToggleReadonlyContext1349});1350}13511352override async run(accessor: ServicesAccessor): Promise<void> {1353const editorService = accessor.get(IEditorService);1354const filesConfigurationService = accessor.get(IFilesConfigurationService);13551356const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });1357if (!fileResource) {1358return;1359}13601361await filesConfigurationService.updateReadonly(fileResource, this.newReadonlyState);1362}1363}13641365export class SetActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {13661367static readonly ID = 'workbench.action.files.setActiveEditorReadonlyInSession';1368static readonly LABEL = nls.localize2('setActiveEditorReadonlyInSession', "Set Active Editor Read-only in Session");13691370constructor() {1371super(1372SetActiveEditorReadonlyInSession.ID,1373SetActiveEditorReadonlyInSession.LABEL,1374true1375);1376}1377}13781379export class SetActiveEditorWriteableInSession extends BaseSetActiveEditorReadonlyInSession {13801381static readonly ID = 'workbench.action.files.setActiveEditorWriteableInSession';1382static readonly LABEL = nls.localize2('setActiveEditorWriteableInSession', "Set Active Editor Writeable in Session");13831384constructor() {1385super(1386SetActiveEditorWriteableInSession.ID,1387SetActiveEditorWriteableInSession.LABEL,1388false1389);1390}1391}13921393export class ToggleActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {13941395static readonly ID = 'workbench.action.files.toggleActiveEditorReadonlyInSession';1396static readonly LABEL = nls.localize2('toggleActiveEditorReadonlyInSession', "Toggle Active Editor Read-only in Session");13971398constructor() {1399super(1400ToggleActiveEditorReadonlyInSession.ID,1401ToggleActiveEditorReadonlyInSession.LABEL,1402'toggle'1403);1404}1405}14061407export class ResetActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {14081409static readonly ID = 'workbench.action.files.resetActiveEditorReadonlyInSession';1410static readonly LABEL = nls.localize2('resetActiveEditorReadonlyInSession', "Reset Active Editor Read-only in Session");14111412constructor() {1413super(1414ResetActiveEditorReadonlyInSession.ID,1415ResetActiveEditorReadonlyInSession.LABEL,1416'reset'1417);1418}1419}142014211422