Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/files/browser/fileActions.ts
5272 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as nls from '../../../../nls.js';
7
import { isWindows, OperatingSystem, OS } from '../../../../base/common/platform.js';
8
import { extname, basename, isAbsolute } from '../../../../base/common/path.js';
9
import * as resources from '../../../../base/common/resources.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
12
import { Action } from '../../../../base/common/actions.js';
13
import { dispose, IDisposable } from '../../../../base/common/lifecycle.js';
14
import { VIEWLET_ID, IFilesConfiguration, VIEW_ID, UndoConfirmLevel } from '../common/files.js';
15
import { IFileService } from '../../../../platform/files/common/files.js';
16
import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js';
17
import { IQuickInputService, ItemActivation } from '../../../../platform/quickinput/common/quickInput.js';
18
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
19
import { ITextModel } from '../../../../editor/common/model.js';
20
import { IHostService } from '../../../services/host/browser/host.js';
21
import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from './fileConstants.js';
22
import { ITextModelService, ITextModelContentProvider } from '../../../../editor/common/services/resolverService.js';
23
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
24
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
25
import { ILanguageService } from '../../../../editor/common/languages/language.js';
26
import { IModelService } from '../../../../editor/common/services/model.js';
27
import { ICommandService, CommandsRegistry } from '../../../../platform/commands/common/commands.js';
28
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
29
import { Schemas } from '../../../../base/common/network.js';
30
import { IDialogService, IConfirmationResult, getFileNamesMessage } from '../../../../platform/dialogs/common/dialogs.js';
31
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
32
import { IEditorService } from '../../../services/editor/common/editorService.js';
33
import { Constants } from '../../../../base/common/uint.js';
34
import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from '../../../browser/parts/editor/editorCommands.js';
35
import { coalesce } from '../../../../base/common/arrays.js';
36
import { ExplorerItem, NewExplorerItem } from '../common/explorerModel.js';
37
import { getErrorMessage } from '../../../../base/common/errors.js';
38
import { triggerUpload } from '../../../../base/browser/dom.js';
39
import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js';
40
import { IWorkingCopyService } from '../../../services/workingCopy/common/workingCopyService.js';
41
import { IWorkingCopy } from '../../../services/workingCopy/common/workingCopy.js';
42
import { timeout } from '../../../../base/common/async.js';
43
import { IWorkingCopyFileService } from '../../../services/workingCopy/common/workingCopyFileService.js';
44
import { Codicon } from '../../../../base/common/codicons.js';
45
import { ThemeIcon } from '../../../../base/common/themables.js';
46
import { ViewContainerLocation } from '../../../common/views.js';
47
import { IViewsService } from '../../../services/views/common/viewsService.js';
48
import { trim, rtrim } from '../../../../base/common/strings.js';
49
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
50
import { ResourceFileEdit } from '../../../../editor/browser/services/bulkEditService.js';
51
import { IExplorerService } from './files.js';
52
import { BrowserFileUpload, FileDownload } from './fileImportExport.js';
53
import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';
54
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
55
import { IPathService } from '../../../services/path/common/pathService.js';
56
import { Action2 } from '../../../../platform/actions/common/actions.js';
57
import { ActiveEditorCanToggleReadonlyContext, ActiveEditorContext, EmptyWorkspaceSupportContext } from '../../../common/contextkeys.js';
58
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
59
import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
60
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
61
import { ILocalizedString } from '../../../../platform/action/common/action.js';
62
import { VSBuffer } from '../../../../base/common/buffer.js';
63
import { getPathForFile } from '../../../../platform/dnd/browser/dnd.js';
64
65
export const NEW_FILE_COMMAND_ID = 'explorer.newFile';
66
export const NEW_FILE_LABEL = nls.localize2('newFile', "New File...");
67
export const NEW_FOLDER_COMMAND_ID = 'explorer.newFolder';
68
export const NEW_FOLDER_LABEL = nls.localize2('newFolder', "New Folder...");
69
export const TRIGGER_RENAME_LABEL = nls.localize('rename', "Rename...");
70
export const MOVE_FILE_TO_TRASH_LABEL = nls.localize('delete', "Delete");
71
export const COPY_FILE_LABEL = nls.localize('copyFile', "Copy");
72
export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste");
73
export const FileCopiedContext = new RawContextKey<boolean>('fileCopied', false);
74
export const DOWNLOAD_COMMAND_ID = 'explorer.download';
75
export const DOWNLOAD_LABEL = nls.localize('download', "Download...");
76
export const UPLOAD_COMMAND_ID = 'explorer.upload';
77
export const UPLOAD_LABEL = nls.localize('upload', "Upload...");
78
const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete';
79
const MAX_UNDO_FILE_SIZE = 5000000; // 5mb
80
81
async function refreshIfSeparator(value: string, explorerService: IExplorerService): Promise<void> {
82
if (value && ((value.indexOf('/') >= 0) || (value.indexOf('\\') >= 0))) {
83
// New input contains separator, multiple resources will get created workaround for #68204
84
await explorerService.refresh();
85
}
86
}
87
88
async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, filesConfigurationService: IFilesConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise<void> {
89
let primaryButton: string;
90
if (useTrash) {
91
primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash");
92
} else {
93
primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete");
94
}
95
96
// Handle dirty
97
const distinctElements = resources.distinctParents(elements, e => e.resource);
98
const dirtyWorkingCopies = new Set<IWorkingCopy>();
99
for (const distinctElement of distinctElements) {
100
for (const dirtyWorkingCopy of workingCopyFileService.getDirty(distinctElement.resource)) {
101
dirtyWorkingCopies.add(dirtyWorkingCopy);
102
}
103
}
104
105
if (dirtyWorkingCopies.size) {
106
let message: string;
107
if (distinctElements.length > 1) {
108
message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?");
109
} else if (distinctElements[0].isDirectory) {
110
if (dirtyWorkingCopies.size === 1) {
111
message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder {0} with unsaved changes in 1 file. Do you want to continue?", distinctElements[0].name);
112
} else {
113
message = 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);
114
}
115
} else {
116
message = nls.localize('dirtyMessageFileDelete', "You are deleting {0} with unsaved changes. Do you want to continue?", distinctElements[0].name);
117
}
118
119
const response = await dialogService.confirm({
120
type: 'warning',
121
message,
122
detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),
123
primaryButton
124
});
125
126
if (!response.confirmed) {
127
return;
128
} else {
129
skipConfirm = true;
130
}
131
}
132
133
// Handle readonly
134
if (!skipConfirm) {
135
const readonlyResources = distinctElements.filter(e => filesConfigurationService.isReadonly(e.resource));
136
if (readonlyResources.length) {
137
let message: string;
138
if (readonlyResources.length > 1) {
139
message = nls.localize('readonlyMessageFilesDelete', "You are deleting files that are configured to be read-only. Do you want to continue?");
140
} else if (readonlyResources[0].isDirectory) {
141
message = nls.localize('readonlyMessageFolderOneDelete', "You are deleting a folder {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name);
142
} else {
143
message = nls.localize('readonlyMessageFolderDelete', "You are deleting a file {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name);
144
}
145
146
const response = await dialogService.confirm({
147
type: 'warning',
148
message,
149
detail: nls.localize('continueDetail', "The read-only protection will be overridden if you continue."),
150
primaryButton: nls.localize('continueButtonLabel', "Continue")
151
});
152
153
if (!response.confirmed) {
154
return;
155
}
156
}
157
}
158
159
let confirmation: IConfirmationResult;
160
161
// We do not support undo of folders, so in that case the delete action is irreversible
162
const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") :
163
distinctElements.length > 1 ? nls.localize('restorePlural', "You can restore these files using the Undo command.") : nls.localize('restore', "You can restore this file using the Undo command.");
164
165
// Check if we need to ask for confirmation at all
166
if (skipConfirm || configurationService.getValue<boolean>(CONFIRM_DELETE_SETTING_KEY) === false) {
167
confirmation = { confirmed: true };
168
}
169
170
// Confirm for moving to trash
171
else if (useTrash) {
172
let { message, detail } = getMoveToTrashMessage(distinctElements);
173
detail += detail ? '\n' : '';
174
if (isWindows) {
175
detail += 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.");
176
} else {
177
detail += 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.");
178
}
179
180
confirmation = await dialogService.confirm({
181
message,
182
detail,
183
primaryButton,
184
checkbox: {
185
label: nls.localize('doNotAskAgain', "Do not ask me again")
186
}
187
});
188
}
189
190
// Confirm for deleting permanently
191
else {
192
let { message, detail } = getDeleteMessage(distinctElements);
193
detail += detail ? '\n' : '';
194
detail += deleteDetail;
195
confirmation = await dialogService.confirm({
196
type: 'warning',
197
message,
198
detail,
199
primaryButton
200
});
201
}
202
203
// Check for confirmation checkbox
204
if (confirmation.confirmed && confirmation.checkboxChecked === true) {
205
await configurationService.updateValue(CONFIRM_DELETE_SETTING_KEY, false);
206
}
207
208
// Check for confirmation
209
if (!confirmation.confirmed) {
210
return;
211
}
212
213
// Call function
214
try {
215
const resourceFileEdits = distinctElements.map(e => new ResourceFileEdit(e.resource, undefined, { recursive: true, folder: e.isDirectory, ignoreIfNotExists, skipTrashBin: !useTrash, maxSize: MAX_UNDO_FILE_SIZE }));
216
const options = {
217
undoLabel: 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),
218
progressLabel: 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),
219
};
220
await explorerService.applyBulkEdit(resourceFileEdits, options);
221
} catch (error) {
222
223
// Handle error to delete file(s) from a modal confirmation dialog
224
let errorMessage: string;
225
let detailMessage: string | undefined;
226
let primaryButton: string;
227
if (useTrash) {
228
errorMessage = 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?");
229
detailMessage = deleteDetail;
230
primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently");
231
} else {
232
errorMessage = toErrorMessage(error, false);
233
primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry");
234
}
235
236
const res = await dialogService.confirm({
237
type: 'warning',
238
message: errorMessage,
239
detail: detailMessage,
240
primaryButton
241
});
242
243
if (res.confirmed) {
244
if (useTrash) {
245
useTrash = false; // Delete Permanently
246
}
247
248
skipConfirm = true;
249
ignoreIfNotExists = true;
250
251
return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, filesConfigurationService, elements, useTrash, skipConfirm, ignoreIfNotExists);
252
}
253
}
254
}
255
256
function getMoveToTrashMessage(distinctElements: ExplorerItem[]): { message: string; detail: string } {
257
if (containsBothDirectoryAndFile(distinctElements)) {
258
return {
259
message: nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length),
260
detail: getFileNamesMessage(distinctElements.map(e => e.resource))
261
};
262
}
263
264
if (distinctElements.length > 1) {
265
if (distinctElements[0].isDirectory) {
266
return {
267
message: nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length),
268
detail: getFileNamesMessage(distinctElements.map(e => e.resource))
269
};
270
}
271
272
return {
273
message: nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length),
274
detail: getFileNamesMessage(distinctElements.map(e => e.resource))
275
};
276
}
277
278
if (distinctElements[0].isDirectory && !distinctElements[0].isSymbolicLink) {
279
return { message: nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name), detail: '' };
280
}
281
282
return { message: nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name), detail: '' };
283
}
284
285
function getDeleteMessage(distinctElements: ExplorerItem[]): { message: string; detail: string } {
286
if (containsBothDirectoryAndFile(distinctElements)) {
287
return {
288
message: nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length),
289
detail: getFileNamesMessage(distinctElements.map(e => e.resource))
290
};
291
}
292
293
if (distinctElements.length > 1) {
294
if (distinctElements[0].isDirectory) {
295
return {
296
message: nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length),
297
detail: getFileNamesMessage(distinctElements.map(e => e.resource))
298
};
299
}
300
301
return {
302
message: nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length),
303
detail: getFileNamesMessage(distinctElements.map(e => e.resource))
304
};
305
}
306
307
if (distinctElements[0].isDirectory) {
308
return { message: nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name), detail: '' };
309
}
310
311
return { message: nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name), detail: '' };
312
}
313
314
function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean {
315
const directory = distinctElements.find(element => element.isDirectory);
316
const file = distinctElements.find(element => !element.isDirectory);
317
318
return !!directory && !!file;
319
}
320
321
322
export async function findValidPasteFileTarget(
323
explorerService: IExplorerService,
324
fileService: IFileService,
325
dialogService: IDialogService,
326
targetFolder: ExplorerItem,
327
fileToPaste: { resource: URI | string; isDirectory?: boolean; allowOverwrite: boolean },
328
incrementalNaming: 'simple' | 'smart' | 'disabled'
329
): Promise<URI | undefined> {
330
331
let name = typeof fileToPaste.resource === 'string' ? fileToPaste.resource : resources.basenameOrAuthority(fileToPaste.resource);
332
let candidate = resources.joinPath(targetFolder.resource, name);
333
334
// In the disabled case we must ask if it's ok to overwrite the file if it exists
335
if (incrementalNaming === 'disabled') {
336
const canOverwrite = await askForOverwrite(fileService, dialogService, candidate);
337
if (!canOverwrite) {
338
return;
339
}
340
}
341
342
while (true && !fileToPaste.allowOverwrite) {
343
if (!explorerService.findClosest(candidate)) {
344
break;
345
}
346
347
if (incrementalNaming !== 'disabled') {
348
name = incrementFileName(name, !!fileToPaste.isDirectory, incrementalNaming);
349
}
350
candidate = resources.joinPath(targetFolder.resource, name);
351
}
352
353
return candidate;
354
}
355
356
export function incrementFileName(name: string, isFolder: boolean, incrementalNaming: 'simple' | 'smart'): string {
357
if (incrementalNaming === 'simple') {
358
let namePrefix = name;
359
let extSuffix = '';
360
if (!isFolder) {
361
extSuffix = extname(name);
362
namePrefix = basename(name, extSuffix);
363
}
364
365
// name copy 5(.txt) => name copy 6(.txt)
366
// name copy(.txt) => name copy 2(.txt)
367
const suffixRegex = /^(.+ copy)( \d+)?$/;
368
if (suffixRegex.test(namePrefix)) {
369
return namePrefix.replace(suffixRegex, (match, g1?, g2?) => {
370
const number = (g2 ? parseInt(g2) : 1);
371
return number === 0
372
? `${g1}`
373
: (number < Constants.MAX_SAFE_SMALL_INTEGER
374
? `${g1} ${number + 1}`
375
: `${g1}${g2} copy`);
376
}) + extSuffix;
377
}
378
379
// name(.txt) => name copy(.txt)
380
return `${namePrefix} copy${extSuffix}`;
381
}
382
383
const separators = '[\\.\\-_]';
384
const maxNumber = Constants.MAX_SAFE_SMALL_INTEGER;
385
386
// file.1.txt=>file.2.txt
387
const suffixFileRegex = RegExp('(.*' + separators + ')(\\d+)(\\..*)$');
388
if (!isFolder && name.match(suffixFileRegex)) {
389
return name.replace(suffixFileRegex, (match, g1?, g2?, g3?) => {
390
const number = parseInt(g2);
391
return number < maxNumber
392
? g1 + String(number + 1).padStart(g2.length, '0') + g3
393
: `${g1}${g2}.1${g3}`;
394
});
395
}
396
397
// 1.file.txt=>2.file.txt
398
const prefixFileRegex = RegExp('(\\d+)(' + separators + '.*)(\\..*)$');
399
if (!isFolder && name.match(prefixFileRegex)) {
400
return name.replace(prefixFileRegex, (match, g1?, g2?, g3?) => {
401
const number = parseInt(g1);
402
return number < maxNumber
403
? String(number + 1).padStart(g1.length, '0') + g2 + g3
404
: `${g1}${g2}.1${g3}`;
405
});
406
}
407
408
// 1.txt=>2.txt
409
const prefixFileNoNameRegex = RegExp('(\\d+)(\\..*)$');
410
if (!isFolder && name.match(prefixFileNoNameRegex)) {
411
return name.replace(prefixFileNoNameRegex, (match, g1?, g2?) => {
412
const number = parseInt(g1);
413
return number < maxNumber
414
? String(number + 1).padStart(g1.length, '0') + g2
415
: `${g1}.1${g2}`;
416
});
417
}
418
419
// file.txt=>file.1.txt
420
const lastIndexOfDot = name.lastIndexOf('.');
421
if (!isFolder && lastIndexOfDot >= 0) {
422
return `${name.substr(0, lastIndexOfDot)}.1${name.substr(lastIndexOfDot)}`;
423
}
424
425
// 123 => 124
426
const noNameNoExtensionRegex = RegExp('(\\d+)$');
427
if (!isFolder && lastIndexOfDot === -1 && name.match(noNameNoExtensionRegex)) {
428
return name.replace(noNameNoExtensionRegex, (match, g1?) => {
429
const number = parseInt(g1);
430
return number < maxNumber
431
? String(number + 1).padStart(g1.length, '0')
432
: `${g1}.1`;
433
});
434
}
435
436
// file => file1
437
// file1 => file2
438
const noExtensionRegex = RegExp('(.*)(\\d*)$');
439
if (!isFolder && lastIndexOfDot === -1 && name.match(noExtensionRegex)) {
440
return name.replace(noExtensionRegex, (match, g1?, g2?) => {
441
let number = parseInt(g2);
442
if (isNaN(number)) {
443
number = 0;
444
}
445
return number < maxNumber
446
? g1 + String(number + 1).padStart(g2.length, '0')
447
: `${g1}${g2}.1`;
448
});
449
}
450
451
// folder.1=>folder.2
452
if (isFolder && name.match(/(\d+)$/)) {
453
return name.replace(/(\d+)$/, (match, ...groups) => {
454
const number = parseInt(groups[0]);
455
return number < maxNumber
456
? String(number + 1).padStart(groups[0].length, '0')
457
: `${groups[0]}.1`;
458
});
459
}
460
461
// 1.folder=>2.folder
462
if (isFolder && name.match(/^(\d+)/)) {
463
return name.replace(/^(\d+)(.*)$/, (match, ...groups) => {
464
const number = parseInt(groups[0]);
465
return number < maxNumber
466
? String(number + 1).padStart(groups[0].length, '0') + groups[1]
467
: `${groups[0]}${groups[1]}.1`;
468
});
469
}
470
471
// file/folder=>file.1/folder.1
472
return `${name}.1`;
473
}
474
475
/**
476
* Checks to see if the resource already exists, if so prompts the user if they would be ok with it being overwritten
477
* @param fileService The file service
478
* @param dialogService The dialog service
479
* @param targetResource The resource to be overwritten
480
* @return A boolean indicating if the user is ok with resource being overwritten, if the resource does not exist it returns true.
481
*/
482
async function askForOverwrite(fileService: IFileService, dialogService: IDialogService, targetResource: URI): Promise<boolean> {
483
const exists = await fileService.exists(targetResource);
484
if (!exists) {
485
return true;
486
}
487
// Ask for overwrite confirmation
488
const { confirmed } = await dialogService.confirm({
489
type: Severity.Warning,
490
message: 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)),
491
primaryButton: nls.localize('replaceButtonLabel', "&&Replace")
492
});
493
return confirmed;
494
}
495
496
// Global Compare with
497
export class GlobalCompareResourcesAction extends Action2 {
498
499
static readonly ID = 'workbench.files.action.compareFileWith';
500
static readonly LABEL = nls.localize2('globalCompareFile', "Compare Active File With...");
501
502
constructor() {
503
super({
504
id: GlobalCompareResourcesAction.ID,
505
title: GlobalCompareResourcesAction.LABEL,
506
f1: true,
507
category: Categories.File,
508
precondition: ActiveEditorContext,
509
metadata: {
510
description: nls.localize2('compareFileWithMeta', "Opens a picker to select a file to diff with the active editor.")
511
}
512
});
513
}
514
515
override async run(accessor: ServicesAccessor): Promise<void> {
516
const editorService = accessor.get(IEditorService);
517
const textModelService = accessor.get(ITextModelService);
518
const quickInputService = accessor.get(IQuickInputService);
519
520
const activeInput = editorService.activeEditor;
521
const activeResource = EditorResourceAccessor.getOriginalUri(activeInput);
522
if (activeResource && textModelService.canHandleResource(activeResource)) {
523
const picks = await quickInputService.quickAccess.pick('', { itemActivation: ItemActivation.SECOND });
524
if (picks?.length === 1) {
525
const resource = (picks[0] as unknown as { resource: unknown }).resource;
526
if (URI.isUri(resource) && textModelService.canHandleResource(resource)) {
527
editorService.openEditor({
528
original: { resource: activeResource },
529
modified: { resource: resource },
530
options: { pinned: true }
531
});
532
}
533
}
534
}
535
}
536
}
537
538
export class ToggleAutoSaveAction extends Action2 {
539
static readonly ID = 'workbench.action.toggleAutoSave';
540
541
constructor() {
542
super({
543
id: ToggleAutoSaveAction.ID,
544
title: nls.localize2('toggleAutoSave', "Toggle Auto Save"),
545
f1: true,
546
category: Categories.File,
547
metadata: { description: nls.localize2('toggleAutoSaveDescription', "Toggle the ability to save files automatically after typing") }
548
});
549
}
550
551
override run(accessor: ServicesAccessor): Promise<void> {
552
const filesConfigurationService = accessor.get(IFilesConfigurationService);
553
return filesConfigurationService.toggleAutoSave();
554
}
555
}
556
557
abstract class BaseSaveAllAction extends Action {
558
private lastDirtyState: boolean;
559
560
constructor(
561
id: string,
562
label: string,
563
@ICommandService protected commandService: ICommandService,
564
@INotificationService private notificationService: INotificationService,
565
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
566
) {
567
super(id, label);
568
569
this.lastDirtyState = this.workingCopyService.hasDirty;
570
this.enabled = this.lastDirtyState;
571
572
this.registerListeners();
573
}
574
575
protected abstract doRun(context: unknown): Promise<void>;
576
577
private registerListeners(): void {
578
579
// update enablement based on working copy changes
580
this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.updateEnablement(workingCopy)));
581
}
582
583
private updateEnablement(workingCopy: IWorkingCopy): void {
584
const hasDirty = workingCopy.isDirty() || this.workingCopyService.hasDirty;
585
if (this.lastDirtyState !== hasDirty) {
586
this.enabled = hasDirty;
587
this.lastDirtyState = this.enabled;
588
}
589
}
590
591
override async run(context?: unknown): Promise<void> {
592
try {
593
await this.doRun(context);
594
} catch (error) {
595
this.notificationService.error(toErrorMessage(error, false));
596
}
597
}
598
}
599
600
export class SaveAllInGroupAction extends BaseSaveAllAction {
601
602
static readonly ID = 'workbench.files.action.saveAllInGroup';
603
static readonly LABEL = nls.localize('saveAllInGroup', "Save All in Group");
604
605
override get class(): string {
606
return 'explorer-action ' + ThemeIcon.asClassName(Codicon.saveAll);
607
}
608
609
protected doRun(context: unknown): Promise<void> {
610
return this.commandService.executeCommand(SAVE_ALL_IN_GROUP_COMMAND_ID, {}, context);
611
}
612
}
613
614
export class CloseGroupAction extends Action {
615
616
static readonly ID = 'workbench.files.action.closeGroup';
617
static readonly LABEL = nls.localize('closeGroup', "Close Group");
618
619
constructor(id: string, label: string, @ICommandService private readonly commandService: ICommandService) {
620
super(id, label, ThemeIcon.asClassName(Codicon.closeAll));
621
}
622
623
override run(context?: unknown): Promise<void> {
624
return this.commandService.executeCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, {}, context);
625
}
626
}
627
628
export class FocusFilesExplorer extends Action2 {
629
630
static readonly ID = 'workbench.files.action.focusFilesExplorer';
631
static readonly LABEL = nls.localize2('focusFilesExplorer', "Focus on Files Explorer");
632
633
constructor() {
634
super({
635
id: FocusFilesExplorer.ID,
636
title: FocusFilesExplorer.LABEL,
637
f1: true,
638
category: Categories.File,
639
metadata: {
640
description: nls.localize2('focusFilesExplorerMetadata', "Moves focus to the file explorer view container.")
641
}
642
});
643
}
644
645
override async run(accessor: ServicesAccessor): Promise<void> {
646
const paneCompositeService = accessor.get(IPaneCompositePartService);
647
await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true);
648
}
649
}
650
651
export class ShowActiveFileInExplorer extends Action2 {
652
653
static readonly ID = 'workbench.files.action.showActiveFileInExplorer';
654
static readonly LABEL = nls.localize2('showInExplorer', "Reveal Active File in Explorer View");
655
656
constructor() {
657
super({
658
id: ShowActiveFileInExplorer.ID,
659
title: ShowActiveFileInExplorer.LABEL,
660
f1: true,
661
category: Categories.File,
662
metadata: {
663
description: nls.localize2('showInExplorerMetadata', "Reveals and selects the active file within the explorer view.")
664
}
665
});
666
}
667
668
override async run(accessor: ServicesAccessor): Promise<void> {
669
const commandService = accessor.get(ICommandService);
670
const editorService = accessor.get(IEditorService);
671
const resource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
672
if (resource) {
673
commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, resource);
674
}
675
}
676
}
677
678
export class OpenActiveFileInEmptyWorkspace extends Action2 {
679
680
static readonly ID = 'workbench.action.files.showOpenedFileInNewWindow';
681
static readonly LABEL = nls.localize2('openFileInEmptyWorkspace', "Open Active Editor in New Empty Workspace");
682
683
constructor(
684
) {
685
super({
686
id: OpenActiveFileInEmptyWorkspace.ID,
687
title: OpenActiveFileInEmptyWorkspace.LABEL,
688
f1: true,
689
category: Categories.File,
690
precondition: EmptyWorkspaceSupportContext,
691
metadata: {
692
description: nls.localize2('openFileInEmptyWorkspaceMetadata', "Opens the active editor in a new window with no folders open.")
693
}
694
});
695
}
696
697
override async run(accessor: ServicesAccessor): Promise<void> {
698
const editorService = accessor.get(IEditorService);
699
const hostService = accessor.get(IHostService);
700
const dialogService = accessor.get(IDialogService);
701
const fileService = accessor.get(IFileService);
702
703
const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
704
if (fileResource && fileService.hasProvider(fileResource)) {
705
hostService.openWindow([{ fileUri: fileResource }], { forceNewWindow: true });
706
} else {
707
dialogService.error(nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource."));
708
}
709
}
710
}
711
712
export function validateFileName(pathService: IPathService, item: ExplorerItem, name: string, os: OperatingSystem): { content: string; severity: Severity } | null {
713
// Produce a well formed file name
714
name = getWellFormedFileName(name);
715
716
// Name not provided
717
if (!name || name.length === 0 || /^\s+$/.test(name)) {
718
return {
719
content: nls.localize('emptyFileNameError', "A file or folder name must be provided."),
720
severity: Severity.Error
721
};
722
}
723
724
// Relative paths only
725
if (name[0] === '/' || name[0] === '\\') {
726
return {
727
content: nls.localize('fileNameStartsWithSlashError', "A file or folder name cannot start with a slash."),
728
severity: Severity.Error
729
};
730
}
731
732
const names = coalesce(name.split(/[\\/]/));
733
const parent = item.parent;
734
735
if (name !== item.name) {
736
// Do not allow to overwrite existing file
737
const child = parent?.getChild(name);
738
if (child && child !== item) {
739
return {
740
content: nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name),
741
severity: Severity.Error
742
};
743
}
744
}
745
746
// Check for invalid file name.
747
if (names.some(folderName => !pathService.hasValidBasename(item.resource, os, folderName))) {
748
// Escape * characters
749
const escapedName = name.replace(/\*/g, '\\*'); // CodeQL [SM02383] This only processes filenames which are enforced against having backslashes in them farther up in the stack.
750
return {
751
content: nls.localize('invalidFileNameError', "The name **{0}** is not valid as a file or folder name. Please choose a different name.", trimLongName(escapedName)),
752
severity: Severity.Error
753
};
754
}
755
756
if (names.some(name => /^\s|\s$/.test(name))) {
757
return {
758
content: nls.localize('fileNameWhitespaceWarning', "Leading or trailing whitespace detected in file or folder name."),
759
severity: Severity.Warning
760
};
761
}
762
763
return null;
764
}
765
766
function trimLongName(name: string): string {
767
if (name?.length > 255) {
768
return `${name.substr(0, 255)}...`;
769
}
770
771
return name;
772
}
773
774
function getWellFormedFileName(filename: string): string {
775
if (!filename) {
776
return filename;
777
}
778
779
// Trim tabs
780
filename = trim(filename, '\t');
781
782
// Remove trailing slashes
783
filename = rtrim(filename, '/');
784
filename = rtrim(filename, '\\');
785
786
return filename;
787
}
788
789
export class CompareNewUntitledTextFilesAction extends Action2 {
790
791
static readonly ID = 'workbench.files.action.compareNewUntitledTextFiles';
792
static readonly LABEL = nls.localize2('compareNewUntitledTextFiles', "Compare New Untitled Text Files");
793
794
constructor() {
795
super({
796
id: CompareNewUntitledTextFilesAction.ID,
797
title: CompareNewUntitledTextFilesAction.LABEL,
798
f1: true,
799
category: Categories.File,
800
metadata: {
801
description: nls.localize2('compareNewUntitledTextFilesMeta', "Opens a new diff editor with two untitled files.")
802
}
803
});
804
}
805
806
override async run(accessor: ServicesAccessor): Promise<void> {
807
const editorService = accessor.get(IEditorService);
808
809
await editorService.openEditor({
810
original: { resource: undefined },
811
modified: { resource: undefined },
812
options: { pinned: true }
813
});
814
}
815
}
816
817
export class CompareWithClipboardAction extends Action2 {
818
819
static readonly ID = 'workbench.files.action.compareWithClipboard';
820
static readonly LABEL = nls.localize2('compareWithClipboard', "Compare Active File with Clipboard");
821
822
private registrationDisposal: IDisposable | undefined;
823
private static SCHEME_COUNTER = 0;
824
825
constructor() {
826
super({
827
id: CompareWithClipboardAction.ID,
828
title: CompareWithClipboardAction.LABEL,
829
f1: true,
830
category: Categories.File,
831
keybinding: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyC), weight: KeybindingWeight.WorkbenchContrib },
832
metadata: {
833
description: nls.localize2('compareWithClipboardMeta', "Opens a new diff editor to compare the active file with the contents of the clipboard.")
834
}
835
});
836
}
837
838
override async run(accessor: ServicesAccessor): Promise<void> {
839
const editorService = accessor.get(IEditorService);
840
const instantiationService = accessor.get(IInstantiationService);
841
const textModelService = accessor.get(ITextModelService);
842
const fileService = accessor.get(IFileService);
843
844
const resource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
845
const scheme = `clipboardCompare${CompareWithClipboardAction.SCHEME_COUNTER++}`;
846
if (resource && (fileService.hasProvider(resource) || resource.scheme === Schemas.untitled)) {
847
if (!this.registrationDisposal) {
848
const provider = instantiationService.createInstance(ClipboardContentProvider);
849
this.registrationDisposal = textModelService.registerTextModelContentProvider(scheme, provider);
850
}
851
852
const name = resources.basename(resource);
853
const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name);
854
855
await editorService.openEditor({
856
original: { resource: resource.with({ scheme }) },
857
modified: { resource: resource },
858
label: editorLabel,
859
options: { pinned: true }
860
}).finally(() => {
861
dispose(this.registrationDisposal);
862
this.registrationDisposal = undefined;
863
});
864
}
865
}
866
867
dispose(): void {
868
dispose(this.registrationDisposal);
869
this.registrationDisposal = undefined;
870
}
871
}
872
873
class ClipboardContentProvider implements ITextModelContentProvider {
874
constructor(
875
@IClipboardService private readonly clipboardService: IClipboardService,
876
@ILanguageService private readonly languageService: ILanguageService,
877
@IModelService private readonly modelService: IModelService
878
) { }
879
880
async provideTextContent(resource: URI): Promise<ITextModel> {
881
const text = await this.clipboardService.readText();
882
const model = this.modelService.createModel(text, this.languageService.createByFilepathOrFirstLine(resource), resource);
883
884
return model;
885
}
886
}
887
888
function onErrorWithRetry(notificationService: INotificationService, error: unknown, retry: () => Promise<unknown>): void {
889
notificationService.prompt(Severity.Error, toErrorMessage(error, false),
890
[{
891
label: nls.localize('retry', "Retry"),
892
run: () => retry()
893
}]
894
);
895
}
896
897
async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise<void> {
898
const explorerService = accessor.get(IExplorerService);
899
const fileService = accessor.get(IFileService);
900
const configService = accessor.get(IConfigurationService);
901
const filesConfigService = accessor.get(IFilesConfigurationService);
902
const editorService = accessor.get(IEditorService);
903
const viewsService = accessor.get(IViewsService);
904
const notificationService = accessor.get(INotificationService);
905
const remoteAgentService = accessor.get(IRemoteAgentService);
906
const commandService = accessor.get(ICommandService);
907
const pathService = accessor.get(IPathService);
908
909
const wasHidden = !viewsService.isViewVisible(VIEW_ID);
910
const view = await viewsService.openView(VIEW_ID, true);
911
if (wasHidden) {
912
// Give explorer some time to resolve itself #111218
913
await timeout(500);
914
}
915
if (!view) {
916
// Can happen in empty workspace case (https://github.com/microsoft/vscode/issues/100604)
917
918
if (isFolder) {
919
throw new Error('Open a folder or workspace first.');
920
}
921
922
return commandService.executeCommand(NEW_UNTITLED_FILE_COMMAND_ID);
923
}
924
925
const stats = explorerService.getContext(false);
926
const stat = stats.length > 0 ? stats[0] : undefined;
927
let folder: ExplorerItem;
928
if (stat) {
929
folder = stat.isDirectory ? stat : (stat.parent || explorerService.roots[0]);
930
} else {
931
folder = explorerService.roots[0];
932
}
933
934
if (folder.isReadonly) {
935
throw new Error('Parent folder is readonly.');
936
}
937
938
const newStat = new NewExplorerItem(fileService, configService, filesConfigService, folder, isFolder);
939
folder.addChild(newStat);
940
941
const onSuccess = async (value: string): Promise<void> => {
942
try {
943
const resourceToCreate = resources.joinPath(folder.resource, value);
944
if (value.endsWith('/')) {
945
isFolder = true;
946
}
947
await explorerService.applyBulkEdit([new ResourceFileEdit(undefined, resourceToCreate, { folder: isFolder })], {
948
undoLabel: nls.localize('createBulkEdit', "Create {0}", value),
949
progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value),
950
confirmBeforeUndo: true
951
});
952
await refreshIfSeparator(value, explorerService);
953
954
if (isFolder) {
955
await explorerService.select(resourceToCreate, true);
956
} else {
957
await editorService.openEditor({ resource: resourceToCreate, options: { pinned: true } });
958
}
959
} catch (error) {
960
onErrorWithRetry(notificationService, error, () => onSuccess(value));
961
}
962
};
963
964
const os = (await remoteAgentService.getEnvironment())?.os ?? OS;
965
966
await explorerService.setEditable(newStat, {
967
validationMessage: value => validateFileName(pathService, newStat, value, os),
968
onFinish: async (value, success) => {
969
folder.removeChild(newStat);
970
await explorerService.setEditable(newStat, null);
971
if (success) {
972
onSuccess(value);
973
}
974
}
975
});
976
}
977
978
CommandsRegistry.registerCommand({
979
id: NEW_FILE_COMMAND_ID,
980
handler: async (accessor) => {
981
await openExplorerAndCreate(accessor, false);
982
}
983
});
984
985
CommandsRegistry.registerCommand({
986
id: NEW_FOLDER_COMMAND_ID,
987
handler: async (accessor) => {
988
await openExplorerAndCreate(accessor, true);
989
}
990
});
991
992
export const renameHandler = async (accessor: ServicesAccessor) => {
993
const explorerService = accessor.get(IExplorerService);
994
const notificationService = accessor.get(INotificationService);
995
const remoteAgentService = accessor.get(IRemoteAgentService);
996
const pathService = accessor.get(IPathService);
997
const configurationService = accessor.get(IConfigurationService);
998
999
const stats = explorerService.getContext(false);
1000
const stat = stats.length > 0 ? stats[0] : undefined;
1001
if (!stat) {
1002
return;
1003
}
1004
1005
const os = (await remoteAgentService.getEnvironment())?.os ?? OS;
1006
1007
await explorerService.setEditable(stat, {
1008
validationMessage: value => validateFileName(pathService, stat, value, os),
1009
onFinish: async (value, success) => {
1010
if (success) {
1011
const parentResource = stat.parent!.resource;
1012
const targetResource = resources.joinPath(parentResource, value);
1013
if (stat.resource.toString() !== targetResource.toString()) {
1014
try {
1015
await explorerService.applyBulkEdit([new ResourceFileEdit(stat.resource, targetResource)], {
1016
confirmBeforeUndo: configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo === UndoConfirmLevel.Verbose,
1017
undoLabel: nls.localize('renameBulkEdit', "Rename {0} to {1}", stat.name, value),
1018
progressLabel: nls.localize('renamingBulkEdit', "Renaming {0} to {1}", stat.name, value),
1019
});
1020
await refreshIfSeparator(value, explorerService);
1021
} catch (e) {
1022
notificationService.error(e);
1023
}
1024
}
1025
}
1026
await explorerService.setEditable(stat, null);
1027
}
1028
});
1029
};
1030
1031
export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => {
1032
const explorerService = accessor.get(IExplorerService);
1033
const stats = explorerService.getContext(true).filter(s => !s.isRoot);
1034
if (stats.length) {
1035
await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, true);
1036
}
1037
};
1038
1039
export const deleteFileHandler = async (accessor: ServicesAccessor) => {
1040
const explorerService = accessor.get(IExplorerService);
1041
const stats = explorerService.getContext(true).filter(s => !s.isRoot);
1042
1043
if (stats.length) {
1044
await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, false);
1045
}
1046
};
1047
1048
let pasteShouldMove = false;
1049
export const copyFileHandler = async (accessor: ServicesAccessor) => {
1050
const explorerService = accessor.get(IExplorerService);
1051
const stats = explorerService.getContext(true);
1052
if (stats.length > 0) {
1053
await explorerService.setToCopy(stats, false);
1054
pasteShouldMove = false;
1055
}
1056
};
1057
1058
export const cutFileHandler = async (accessor: ServicesAccessor) => {
1059
const explorerService = accessor.get(IExplorerService);
1060
const stats = explorerService.getContext(true);
1061
if (stats.length > 0) {
1062
await explorerService.setToCopy(stats, true);
1063
pasteShouldMove = true;
1064
}
1065
};
1066
1067
const downloadFileHandler = async (accessor: ServicesAccessor) => {
1068
const explorerService = accessor.get(IExplorerService);
1069
const notificationService = accessor.get(INotificationService);
1070
const instantiationService = accessor.get(IInstantiationService);
1071
1072
const context = explorerService.getContext(true);
1073
const explorerItems = context.length ? context : explorerService.roots;
1074
1075
const downloadHandler = instantiationService.createInstance(FileDownload);
1076
1077
try {
1078
await downloadHandler.download(explorerItems);
1079
} catch (error) {
1080
notificationService.error(error);
1081
1082
throw error;
1083
}
1084
};
1085
1086
CommandsRegistry.registerCommand({
1087
id: DOWNLOAD_COMMAND_ID,
1088
handler: downloadFileHandler
1089
});
1090
1091
const uploadFileHandler = async (accessor: ServicesAccessor) => {
1092
const explorerService = accessor.get(IExplorerService);
1093
const notificationService = accessor.get(INotificationService);
1094
const instantiationService = accessor.get(IInstantiationService);
1095
1096
const context = explorerService.getContext(false);
1097
const element = context.length ? context[0] : explorerService.roots[0];
1098
1099
try {
1100
const files = await triggerUpload();
1101
if (files) {
1102
const browserUpload = instantiationService.createInstance(BrowserFileUpload);
1103
await browserUpload.upload(element, files);
1104
}
1105
} catch (error) {
1106
notificationService.error(error);
1107
1108
throw error;
1109
}
1110
};
1111
1112
CommandsRegistry.registerCommand({
1113
id: UPLOAD_COMMAND_ID,
1114
handler: uploadFileHandler
1115
});
1116
1117
export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: FileList) => {
1118
const clipboardService = accessor.get(IClipboardService);
1119
const explorerService = accessor.get(IExplorerService);
1120
const fileService = accessor.get(IFileService);
1121
const notificationService = accessor.get(INotificationService);
1122
const editorService = accessor.get(IEditorService);
1123
const configurationService = accessor.get(IConfigurationService);
1124
const uriIdentityService = accessor.get(IUriIdentityService);
1125
const dialogService = accessor.get(IDialogService);
1126
const hostService = accessor.get(IHostService);
1127
1128
const context = explorerService.getContext(false);
1129
const hasNativeFilesToPaste = fileList && fileList.length > 0;
1130
const confirmPasteNative = hasNativeFilesToPaste && configurationService.getValue<boolean>('explorer.confirmPasteNative');
1131
1132
const toPaste = await getFilesToPaste(fileList, clipboardService, hostService);
1133
1134
if (confirmPasteNative && toPaste.files.length >= 1) {
1135
const message = toPaste.files.length > 1 ?
1136
nls.localize('confirmMultiPasteNative', "Are you sure you want to paste the following {0} items?", toPaste.files.length) :
1137
nls.localize('confirmPasteNative', "Are you sure you want to paste '{0}'?", basename(toPaste.type === 'paths' ? toPaste.files[0].fsPath : toPaste.files[0].name));
1138
const detail = toPaste.files.length > 1 ? getFileNamesMessage(toPaste.files.map(item => {
1139
if (URI.isUri(item)) {
1140
return item.fsPath;
1141
}
1142
1143
if (toPaste.type === 'paths') {
1144
const path = getPathForFile(item);
1145
if (path) {
1146
return path;
1147
}
1148
}
1149
1150
return item.name;
1151
})) : undefined;
1152
const confirmation = await dialogService.confirm({
1153
message,
1154
detail,
1155
checkbox: {
1156
label: nls.localize('doNotAskAgain', "Do not ask me again")
1157
},
1158
primaryButton: nls.localize({ key: 'pasteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Paste")
1159
});
1160
1161
if (!confirmation.confirmed) {
1162
return;
1163
}
1164
1165
// Check for confirmation checkbox
1166
if (confirmation.checkboxChecked === true) {
1167
await configurationService.updateValue('explorer.confirmPasteNative', false);
1168
}
1169
}
1170
const element = context.length ? context[0] : explorerService.roots[0];
1171
const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
1172
1173
const editableItem = explorerService.getEditable();
1174
// If it's an editable item, just do nothing
1175
if (editableItem) {
1176
return;
1177
}
1178
1179
try {
1180
let targets: URI[] = [];
1181
1182
if (toPaste.type === 'paths') { // Pasting from files on disk
1183
1184
// Check if target is ancestor of pasted folder
1185
const sourceTargetPairs = coalesce(await Promise.all(toPaste.files.map(async fileToPaste => {
1186
if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
1187
throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
1188
}
1189
const fileToPasteStat = await fileService.stat(fileToPaste);
1190
1191
// Find target
1192
let target: ExplorerItem;
1193
if (uriIdentityService.extUri.isEqual(element.resource, fileToPaste)) {
1194
target = element.parent!;
1195
} else {
1196
target = element.isDirectory ? element : element.parent!;
1197
}
1198
1199
const targetFile = await findValidPasteFileTarget(
1200
explorerService,
1201
fileService,
1202
dialogService,
1203
target,
1204
{ resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },
1205
incrementalNaming
1206
);
1207
1208
if (!targetFile) {
1209
return undefined;
1210
}
1211
1212
return { source: fileToPaste, target: targetFile };
1213
})));
1214
1215
if (sourceTargetPairs.length >= 1) {
1216
// Move/Copy File
1217
if (pasteShouldMove) {
1218
const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { overwrite: incrementalNaming === 'disabled' }));
1219
const options = {
1220
confirmBeforeUndo: configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo === UndoConfirmLevel.Verbose,
1221
progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'movingBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Moving {0} files", sourceTargetPairs.length)
1222
: nls.localize({ key: 'movingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)),
1223
undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'moveBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Move {0} files", sourceTargetPairs.length)
1224
: nls.localize({ key: 'moveFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target))
1225
};
1226
await explorerService.applyBulkEdit(resourceFileEdits, options);
1227
} else {
1228
const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true, overwrite: incrementalNaming === 'disabled' }));
1229
await applyCopyResourceEdit(sourceTargetPairs.map(pair => pair.target), resourceFileEdits);
1230
}
1231
}
1232
1233
targets = sourceTargetPairs.map(pair => pair.target);
1234
1235
} else { // Pasting from file data
1236
const targetAndEdits = coalesce(await Promise.all(toPaste.files.map(async file => {
1237
const target = element.isDirectory ? element : element.parent!;
1238
1239
const targetFile = await findValidPasteFileTarget(
1240
explorerService,
1241
fileService,
1242
dialogService,
1243
target,
1244
{ resource: file.name, isDirectory: false, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },
1245
incrementalNaming
1246
);
1247
if (!targetFile) {
1248
return;
1249
}
1250
return {
1251
target: targetFile,
1252
edit: new ResourceFileEdit(undefined, targetFile, {
1253
overwrite: incrementalNaming === 'disabled',
1254
contents: (async () => VSBuffer.wrap(new Uint8Array(await file.arrayBuffer())))(),
1255
})
1256
};
1257
})));
1258
1259
await applyCopyResourceEdit(targetAndEdits.map(pair => pair.target), targetAndEdits.map(pair => pair.edit));
1260
targets = targetAndEdits.map(pair => pair.target);
1261
}
1262
1263
if (targets.length) {
1264
const firstTarget = targets[0];
1265
await explorerService.select(firstTarget);
1266
if (targets.length === 1) {
1267
const item = explorerService.findClosest(firstTarget);
1268
if (item && !item.isDirectory) {
1269
await editorService.openEditor({ resource: item.resource, options: { pinned: true, preserveFocus: true } });
1270
}
1271
}
1272
}
1273
} catch (e) {
1274
notificationService.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));
1275
} finally {
1276
if (pasteShouldMove) {
1277
// Cut is done. Make sure to clear cut state.
1278
await explorerService.setToCopy([], false);
1279
pasteShouldMove = false;
1280
}
1281
}
1282
1283
async function applyCopyResourceEdit(targets: readonly URI[], resourceFileEdits: ResourceFileEdit[]) {
1284
const undoLevel = configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo;
1285
const options = {
1286
confirmBeforeUndo: undoLevel === UndoConfirmLevel.Default || undoLevel === UndoConfirmLevel.Verbose,
1287
progressLabel: targets.length > 1 ? nls.localize({ key: 'copyingBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copying {0} files", targets.length)
1288
: nls.localize({ key: 'copyingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copying {0}", resources.basenameOrAuthority(targets[0])),
1289
undoLabel: targets.length > 1 ? nls.localize({ key: 'copyBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Paste {0} files", targets.length)
1290
: nls.localize({ key: 'copyFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Paste {0}", resources.basenameOrAuthority(targets[0]))
1291
};
1292
await explorerService.applyBulkEdit(resourceFileEdits, options);
1293
}
1294
};
1295
1296
type FilesToPaste =
1297
| { type: 'paths'; files: URI[] }
1298
| { type: 'data'; files: File[] };
1299
1300
async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService, hostService: IHostService): Promise<FilesToPaste> {
1301
if (fileList && fileList.length > 0) {
1302
// with a `fileList` we support natively pasting file from disk from clipboard
1303
const resources = [...fileList].map(file => getPathForFile(file)).filter(filePath => !!filePath && isAbsolute(filePath)).map((filePath) => URI.file(filePath!));
1304
if (resources.length) {
1305
return { type: 'paths', files: resources, };
1306
}
1307
1308
// Support pasting files that we can't read from disk
1309
return { type: 'data', files: [...fileList].filter(file => !getPathForFile(file)) };
1310
} else {
1311
// otherwise we fallback to reading resources from our clipboard service
1312
return { type: 'paths', files: resources.distinctParents(await clipboardService.readResources(), resource => resource) };
1313
}
1314
}
1315
1316
export const openFilePreserveFocusHandler = async (accessor: ServicesAccessor) => {
1317
const editorService = accessor.get(IEditorService);
1318
const explorerService = accessor.get(IExplorerService);
1319
const stats = explorerService.getContext(true);
1320
1321
await editorService.openEditors(stats.filter(s => !s.isDirectory).map(s => ({
1322
resource: s.resource,
1323
options: { preserveFocus: true }
1324
})));
1325
};
1326
1327
class BaseSetActiveEditorReadonlyInSession extends Action2 {
1328
1329
constructor(
1330
id: string,
1331
title: ILocalizedString,
1332
private readonly newReadonlyState: true | false | 'toggle' | 'reset'
1333
) {
1334
super({
1335
id,
1336
title,
1337
f1: true,
1338
category: Categories.File,
1339
precondition: ActiveEditorCanToggleReadonlyContext
1340
});
1341
}
1342
1343
override async run(accessor: ServicesAccessor): Promise<void> {
1344
const editorService = accessor.get(IEditorService);
1345
const filesConfigurationService = accessor.get(IFilesConfigurationService);
1346
1347
const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
1348
if (!fileResource) {
1349
return;
1350
}
1351
1352
await filesConfigurationService.updateReadonly(fileResource, this.newReadonlyState);
1353
}
1354
}
1355
1356
export class SetActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {
1357
1358
static readonly ID = 'workbench.action.files.setActiveEditorReadonlyInSession';
1359
static readonly LABEL = nls.localize2('setActiveEditorReadonlyInSession', "Set Active Editor Read-only in Session");
1360
1361
constructor() {
1362
super(
1363
SetActiveEditorReadonlyInSession.ID,
1364
SetActiveEditorReadonlyInSession.LABEL,
1365
true
1366
);
1367
}
1368
}
1369
1370
export class SetActiveEditorWriteableInSession extends BaseSetActiveEditorReadonlyInSession {
1371
1372
static readonly ID = 'workbench.action.files.setActiveEditorWriteableInSession';
1373
static readonly LABEL = nls.localize2('setActiveEditorWriteableInSession', "Set Active Editor Writeable in Session");
1374
1375
constructor() {
1376
super(
1377
SetActiveEditorWriteableInSession.ID,
1378
SetActiveEditorWriteableInSession.LABEL,
1379
false
1380
);
1381
}
1382
}
1383
1384
export class ToggleActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {
1385
1386
static readonly ID = 'workbench.action.files.toggleActiveEditorReadonlyInSession';
1387
static readonly LABEL = nls.localize2('toggleActiveEditorReadonlyInSession', "Toggle Active Editor Read-only in Session");
1388
1389
constructor() {
1390
super(
1391
ToggleActiveEditorReadonlyInSession.ID,
1392
ToggleActiveEditorReadonlyInSession.LABEL,
1393
'toggle'
1394
);
1395
}
1396
}
1397
1398
export class ResetActiveEditorReadonlyInSession extends BaseSetActiveEditorReadonlyInSession {
1399
1400
static readonly ID = 'workbench.action.files.resetActiveEditorReadonlyInSession';
1401
static readonly LABEL = nls.localize2('resetActiveEditorReadonlyInSession', "Reset Active Editor Read-only in Session");
1402
1403
constructor() {
1404
super(
1405
ResetActiveEditorReadonlyInSession.ID,
1406
ResetActiveEditorReadonlyInSession.LABEL,
1407
'reset'
1408
);
1409
}
1410
}
1411
1412