fileActions.ts 42.1 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import * as nls from 'vs/nls';
7
import { isWindows, isWeb } from 'vs/base/common/platform';
B
Benjamin Pasero 已提交
8
import * as extpath from 'vs/base/common/extpath';
9
import { extname, basename } from 'vs/base/common/path';
10
import * as resources from 'vs/base/common/resources';
11
import { URI } from 'vs/base/common/uri';
J
Johannes Rieken 已提交
12
import { toErrorMessage } from 'vs/base/common/errorMessage';
13
import * as strings from 'vs/base/common/strings';
14
import { Action } from 'vs/base/common/actions';
J
Johannes Rieken 已提交
15
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
16
import { VIEWLET_ID, IExplorerService, IFilesConfiguration } from 'vs/workbench/contrib/files/common/files';
I
isidor 已提交
17
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
18
import { IFileService } from 'vs/platform/files/common/files';
R
rebornix 已提交
19
import { toResource, SideBySideEditor, IEditorInput } from 'vs/workbench/common/editor';
S
SteVen Batten 已提交
20
import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet';
21
import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput';
B
Benjamin Pasero 已提交
22
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
I
isidor 已提交
23
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
A
Alex Dima 已提交
24
import { ITextModel } from 'vs/editor/common/model';
25
import { IHostService } from 'vs/workbench/services/host/browser/host';
26
import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands';
M
Max Furman 已提交
27
import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
28
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
M
Max Furman 已提交
29 30 31
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
I
isidor 已提交
32
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
I
isidor 已提交
33
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
34
import { Schemas } from 'vs/base/common/network';
35
import { IDialogService, IConfirmationResult, getFileNamesMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
36
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
37
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
38
import { Constants } from 'vs/base/common/uint';
I
isidor 已提交
39
import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
M
Matt Bierner 已提交
40
import { coalesce } from 'vs/base/common/arrays';
J
Joao Moreno 已提交
41
import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
I
isidor 已提交
42
import { getErrorMessage } from 'vs/base/common/errors';
43
import { triggerDownload, asDomUri } from 'vs/base/browser/dom';
44
import { mnemonicButtonLabel } from 'vs/base/common/labels';
45
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
46
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
B
Benjamin Pasero 已提交
47
import { sequence, timeout } from 'vs/base/common/async';
48
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
49
import { once } from 'vs/base/common/functional';
R
rebornix 已提交
50 51
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
52
import { Codicon } from 'vs/base/common/codicons';
53
import { openEditorWith } from 'vs/workbench/contrib/files/common/openWith';
E
Erich Gamma 已提交
54

I
isidor 已提交
55
export const NEW_FILE_COMMAND_ID = 'explorer.newFile';
I
isidor 已提交
56 57
export const NEW_FILE_LABEL = nls.localize('newFile', "New File");

I
isidor 已提交
58
export const NEW_FOLDER_COMMAND_ID = 'explorer.newFolder';
I
isidor 已提交
59 60
export const NEW_FOLDER_LABEL = nls.localize('newFolder', "New Folder");

I
isidor 已提交
61 62
export const TRIGGER_RENAME_LABEL = nls.localize('rename', "Rename");

I
isidor 已提交
63 64
export const MOVE_FILE_TO_TRASH_LABEL = nls.localize('delete', "Delete");

I
isidor 已提交
65 66 67 68 69 70
export const COPY_FILE_LABEL = nls.localize('copyFile', "Copy");

export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste");

export const FileCopiedContext = new RawContextKey<boolean>('fileCopied', false);

71 72
export const DOWNLOAD_LABEL = nls.localize('download', "Download");

73 74
const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete';

I
isidor 已提交
75 76 77
function onError(notificationService: INotificationService, error: any): void {
	if (error.message === 'string') {
		error = error.message;
E
Erich Gamma 已提交
78 79
	}

I
isidor 已提交
80
	notificationService.error(toErrorMessage(error, false));
81 82
}

I
isidor 已提交
83 84 85 86 87 88
function refreshIfSeparator(value: string, explorerService: IExplorerService): void {
	if (value && ((value.indexOf('/') >= 0) || (value.indexOf('\\') >= 0))) {
		// New input contains separator, multiple resources will get created workaround for #68204
		explorerService.refresh();
	}
}
E
Erich Gamma 已提交
89

90
/* New File */
I
isidor 已提交
91
export class NewFileAction extends Action {
92 93
	static readonly ID = 'workbench.files.action.createFileFromExplorer';
	static readonly LABEL = nls.localize('createNewFile', "New File");
E
Erich Gamma 已提交
94 95

	constructor(
I
isidor 已提交
96 97
		@IExplorerService explorerService: IExplorerService,
		@ICommandService private commandService: ICommandService
E
Erich Gamma 已提交
98
	) {
I
isidor 已提交
99
		super('explorer.newFile', NEW_FILE_LABEL);
100
		this.class = 'explorer-action ' + Codicon.newFile.classNames;
M
Matt Bierner 已提交
101
		this._register(explorerService.onDidChangeEditable(e => {
I
isidor 已提交
102
			const elementIsBeingEdited = explorerService.isEditable(e);
103 104
			this.enabled = !elementIsBeingEdited;
		}));
105 106
	}

107
	run(): Promise<void> {
I
isidor 已提交
108
		return this.commandService.executeCommand(NEW_FILE_COMMAND_ID);
E
Erich Gamma 已提交
109 110 111
	}
}

112
/* New Folder */
I
isidor 已提交
113
export class NewFolderAction extends Action {
114 115
	static readonly ID = 'workbench.files.action.createFolderFromExplorer';
	static readonly LABEL = nls.localize('createNewFolder', "New Folder");
E
Erich Gamma 已提交
116 117

	constructor(
I
isidor 已提交
118 119
		@IExplorerService explorerService: IExplorerService,
		@ICommandService private commandService: ICommandService
E
Erich Gamma 已提交
120
	) {
I
isidor 已提交
121
		super('explorer.newFolder', NEW_FOLDER_LABEL);
122
		this.class = 'explorer-action ' + Codicon.newFolder.classNames;
M
Matt Bierner 已提交
123
		this._register(explorerService.onDidChangeEditable(e => {
I
isidor 已提交
124
			const elementIsBeingEdited = explorerService.isEditable(e);
125 126
			this.enabled = !elementIsBeingEdited;
		}));
E
Erich Gamma 已提交
127 128
	}

129
	run(): Promise<void> {
I
isidor 已提交
130
		return this.commandService.executeCommand(NEW_FOLDER_COMMAND_ID);
E
Erich Gamma 已提交
131 132 133 134
	}
}

/* Create new file from anywhere: Open untitled */
135
export class GlobalNewUntitledFileAction extends Action {
B
Benjamin Pasero 已提交
136 137
	static readonly ID = 'workbench.action.files.newUntitledFile';
	static readonly LABEL = nls.localize('newUntitledFile', "New Untitled File");
E
Erich Gamma 已提交
138 139 140 141

	constructor(
		id: string,
		label: string,
142
		@IEditorService private readonly editorService: IEditorService
E
Erich Gamma 已提交
143 144 145 146
	) {
		super(id, label);
	}

147 148
	async run(): Promise<void> {
		await this.editorService.openEditor({ options: { pinned: true } }); // untitled are always pinned
E
Erich Gamma 已提交
149 150 151
	}
}

152
async function deleteFiles(workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise<void> {
153 154 155 156 157 158 159 160
	let primaryButton: string;
	if (useTrash) {
		primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash");
	} else {
		primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete");
	}

	// Handle dirty
161 162 163 164 165 166 167
	const distinctElements = resources.distinctParents(elements, e => e.resource);
	const dirtyWorkingCopies = new Set<IWorkingCopy>();
	for (const distinctElement of distinctElements) {
		for (const dirtyWorkingCopy of workingCopyFileService.getDirty(distinctElement.resource)) {
			dirtyWorkingCopies.add(dirtyWorkingCopy);
		}
	}
I
isidor 已提交
168
	let confirmed = true;
169
	if (dirtyWorkingCopies.size) {
170 171 172 173
		let message: string;
		if (distinctElements.length > 1) {
			message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?");
		} else if (distinctElements[0].isDirectory) {
174
			if (dirtyWorkingCopies.size === 1) {
I
isidor 已提交
175
				message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder {0} with unsaved changes in 1 file. Do you want to continue?", distinctElements[0].name);
176
			} else {
177
				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);
178 179
			}
		} else {
I
isidor 已提交
180
			message = nls.localize('dirtyMessageFileDelete', "You are deleting {0} with unsaved changes. Do you want to continue?", distinctElements[0].name);
181
		}
E
Erich Gamma 已提交
182

I
isidor 已提交
183
		const response = await dialogService.confirm({
184 185 186 187 188
			message,
			type: 'warning',
			detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."),
			primaryButton
		});
189

I
isidor 已提交
190 191 192 193
		if (!response.confirmed) {
			confirmed = false;
		} else {
			skipConfirm = true;
194
		}
I
isidor 已提交
195
	}
E
Erich Gamma 已提交
196

I
isidor 已提交
197 198 199 200
	// Check if file is dirty in editor and save it to avoid data loss
	if (!confirmed) {
		return;
	}
201

202
	let confirmation: IConfirmationResult;
203

I
isidor 已提交
204 205
	// Check if we need to ask for confirmation at all
	if (skipConfirm || (useTrash && configurationService.getValue<boolean>(CONFIRM_DELETE_SETTING_KEY) === false)) {
206
		confirmation = { confirmed: true };
I
isidor 已提交
207
	}
E
Erich Gamma 已提交
208

I
isidor 已提交
209 210
	// Confirm for moving to trash
	else if (useTrash) {
211 212 213
		let { message, detail } = getMoveToTrashMessage(distinctElements);
		detail += detail ? '\n' : '';
		if (isWindows) {
I
isidor 已提交
214
			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.");
215
		} else {
I
isidor 已提交
216
			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.");
217
		}
B
Benjamin Pasero 已提交
218

219
		confirmation = await dialogService.confirm({
I
isidor 已提交
220
			message,
221
			detail,
I
isidor 已提交
222 223 224 225 226 227 228
			primaryButton,
			checkbox: {
				label: nls.localize('doNotAskAgain', "Do not ask me again")
			},
			type: 'question'
		});
	}
E
Erich Gamma 已提交
229

I
isidor 已提交
230 231
	// Confirm for deleting permanently
	else {
232 233 234
		let { message, detail } = getDeleteMessage(distinctElements);
		detail += detail ? '\n' : '';
		detail += nls.localize('irreversible', "This action is irreversible!");
235
		confirmation = await dialogService.confirm({
I
isidor 已提交
236
			message,
237
			detail,
I
isidor 已提交
238 239 240 241
			primaryButton,
			type: 'warning'
		});
	}
242

243 244 245 246
	// Check for confirmation checkbox
	if (confirmation.confirmed && confirmation.checkboxChecked === true) {
		await configurationService.updateValue(CONFIRM_DELETE_SETTING_KEY, false, ConfigurationTarget.USER);
	}
247

248 249 250 251
	// Check for confirmation
	if (!confirmation.confirmed) {
		return;
	}
I
isidor 已提交
252

253 254
	// Call function
	try {
255
		await Promise.all(distinctElements.map(e => workingCopyFileService.delete(e.resource, { useTrash: useTrash, recursive: true })));
256
	} catch (error) {
I
isidor 已提交
257

258 259 260 261 262 263 264 265 266 267 268 269
		// Handle error to delete file(s) from a modal confirmation dialog
		let errorMessage: string;
		let detailMessage: string | undefined;
		let primaryButton: string;
		if (useTrash) {
			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?");
			detailMessage = nls.localize('irreversible', "This action is irreversible!");
			primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently");
		} else {
			errorMessage = toErrorMessage(error, false);
			primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry");
		}
270

271 272 273 274 275 276
		const res = await dialogService.confirm({
			message: errorMessage,
			detail: detailMessage,
			type: 'warning',
			primaryButton
		});
277

278 279 280 281
		if (res.confirmed) {
			if (useTrash) {
				useTrash = false; // Delete Permanently
			}
282

283
			skipConfirm = true;
284

285
			return deleteFiles(workingCopyFileService, dialogService, configurationService, elements, useTrash, skipConfirm);
286 287
		}
	}
288
}
B
Benjamin Pasero 已提交
289

290
function getMoveToTrashMessage(distinctElements: ExplorerItem[]): { message: string, detail: string } {
291
	if (containsBothDirectoryAndFile(distinctElements)) {
292 293 294 295
		return {
			message: nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length),
			detail: getFileNamesMessage(distinctElements.map(e => e.resource))
		};
296
	}
B
Benjamin Pasero 已提交
297

298
	if (distinctElements.length > 1) {
B
Benjamin Pasero 已提交
299
		if (distinctElements[0].isDirectory) {
300 301 302 303
			return {
				message: nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length),
				detail: getFileNamesMessage(distinctElements.map(e => e.resource))
			};
304
		}
B
Benjamin Pasero 已提交
305

306 307 308 309
		return {
			message: nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length),
			detail: getFileNamesMessage(distinctElements.map(e => e.resource))
		};
310 311
	}

312
	if (distinctElements[0].isDirectory) {
313
		return { message: nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name), detail: '' };
314
	}
315

316
	return { message: nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name), detail: '' };
317
}
B
Benjamin Pasero 已提交
318

319
function getDeleteMessage(distinctElements: ExplorerItem[]): { message: string, detail: string } {
320
	if (containsBothDirectoryAndFile(distinctElements)) {
321 322 323 324
		return {
			message: nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length),
			detail: getFileNamesMessage(distinctElements.map(e => e.resource))
		};
325
	}
B
Benjamin Pasero 已提交
326

327
	if (distinctElements.length > 1) {
B
Benjamin Pasero 已提交
328
		if (distinctElements[0].isDirectory) {
329 330 331 332
			return {
				message: nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length),
				detail: getFileNamesMessage(distinctElements.map(e => e.resource))
			};
B
Benjamin Pasero 已提交
333 334
		}

335 336 337 338
		return {
			message: nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length),
			detail: getFileNamesMessage(distinctElements.map(e => e.resource))
		};
339 340
	}

341
	if (distinctElements[0].isDirectory) {
342
		return { message: nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name), detail: '' };
343
	}
344

345
	return { message: nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name), detail: '' };
346 347 348 349 350 351 352
}

function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean {
	const directories = distinctElements.filter(element => element.isDirectory);
	const files = distinctElements.filter(element => !element.isDirectory);

	return directories.length > 0 && files.length > 0;
E
Erich Gamma 已提交
353 354 355
}


356
export function findValidPasteFileTarget(explorerService: IExplorerService, targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean, allowOverwrite: boolean }, incrementalNaming: 'simple' | 'smart'): URI {
357
	let name = resources.basenameOrAuthority(fileToPaste.resource);
E
Erich Gamma 已提交
358

359
	let candidate = resources.joinPath(targetFolder.resource, name);
H
Howard Hung 已提交
360
	while (true && !fileToPaste.allowOverwrite) {
361
		if (!explorerService.findClosest(candidate)) {
362
			break;
E
Erich Gamma 已提交
363 364
		}

I
isidor 已提交
365
		name = incrementFileName(name, !!fileToPaste.isDirectory, incrementalNaming);
366
		candidate = resources.joinPath(targetFolder.resource, name);
E
Erich Gamma 已提交
367 368
	}

369 370
	return candidate;
}
E
Erich Gamma 已提交
371

I
isidor 已提交
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
export function incrementFileName(name: string, isFolder: boolean, incrementalNaming: 'simple' | 'smart'): string {
	if (incrementalNaming === 'simple') {
		let namePrefix = name;
		let extSuffix = '';
		if (!isFolder) {
			extSuffix = extname(name);
			namePrefix = basename(name, extSuffix);
		}

		// name copy 5(.txt) => name copy 6(.txt)
		// name copy(.txt) => name copy 2(.txt)
		const suffixRegex = /^(.+ copy)( \d+)?$/;
		if (suffixRegex.test(namePrefix)) {
			return namePrefix.replace(suffixRegex, (match, g1?, g2?) => {
				let number = (g2 ? parseInt(g2) : 1);
				return number === 0
					? `${g1}`
					: (number < Constants.MAX_SAFE_SMALL_INTEGER
						? `${g1} ${number + 1}`
						: `${g1}${g2} copy`);
			}) + extSuffix;
		}

		// name(.txt) => name copy(.txt)
		return `${namePrefix} copy${extSuffix}`;
	}

	const separators = '[\\.\\-_]';
	const maxNumber = Constants.MAX_SAFE_SMALL_INTEGER;

	// file.1.txt=>file.2.txt
	let suffixFileRegex = RegExp('(.*' + separators + ')(\\d+)(\\..*)$');
	if (!isFolder && name.match(suffixFileRegex)) {
		return name.replace(suffixFileRegex, (match, g1?, g2?, g3?) => {
			let number = parseInt(g2);
			return number < maxNumber
				? g1 + strings.pad(number + 1, g2.length) + g3
				: strings.format('{0}{1}.1{2}', g1, g2, g3);
		});
	}

	// 1.file.txt=>2.file.txt
	let prefixFileRegex = RegExp('(\\d+)(' + separators + '.*)(\\..*)$');
	if (!isFolder && name.match(prefixFileRegex)) {
		return name.replace(prefixFileRegex, (match, g1?, g2?, g3?) => {
			let number = parseInt(g1);
			return number < maxNumber
				? strings.pad(number + 1, g1.length) + g2 + g3
				: strings.format('{0}{1}.1{2}', g1, g2, g3);
		});
	}

	// 1.txt=>2.txt
	let prefixFileNoNameRegex = RegExp('(\\d+)(\\..*)$');
	if (!isFolder && name.match(prefixFileNoNameRegex)) {
		return name.replace(prefixFileNoNameRegex, (match, g1?, g2?) => {
			let number = parseInt(g1);
			return number < maxNumber
				? strings.pad(number + 1, g1.length) + g2
				: strings.format('{0}.1{1}', g1, g2);
		});
	}

	// file.txt=>file.1.txt
	const lastIndexOfDot = name.lastIndexOf('.');
	if (!isFolder && lastIndexOfDot >= 0) {
		return strings.format('{0}.1{1}', name.substr(0, lastIndexOfDot), name.substr(lastIndexOfDot));
	}

	// folder.1=>folder.2
	if (isFolder && name.match(/(\d+)$/)) {
443
		return name.replace(/(\d+)$/, (match, ...groups) => {
I
isidor 已提交
444 445 446 447 448 449 450 451 452
			let number = parseInt(groups[0]);
			return number < maxNumber
				? strings.pad(number + 1, groups[0].length)
				: strings.format('{0}.1', groups[0]);
		});
	}

	// 1.folder=>2.folder
	if (isFolder && name.match(/^(\d+)/)) {
453
		return name.replace(/^(\d+)(.*)$/, (match, ...groups) => {
I
isidor 已提交
454 455 456 457 458 459 460 461 462
			let number = parseInt(groups[0]);
			return number < maxNumber
				? strings.pad(number + 1, groups[0].length) + groups[1]
				: strings.format('{0}{1}.1', groups[0], groups[1]);
		});
	}

	// file/folder=>file.1/folder.1
	return strings.format('{0}.1', name);
E
Erich Gamma 已提交
463 464 465 466 467
}

// Global Compare with
export class GlobalCompareResourcesAction extends Action {

B
Benjamin Pasero 已提交
468 469
	static readonly ID = 'workbench.files.action.compareFileWith';
	static readonly LABEL = nls.localize('globalCompareFile', "Compare Active File With...");
E
Erich Gamma 已提交
470 471 472 473

	constructor(
		id: string,
		label: string,
474
		@IQuickInputService private readonly quickInputService: IQuickInputService,
475 476
		@IEditorService private readonly editorService: IEditorService,
		@INotificationService private readonly notificationService: INotificationService,
E
Erich Gamma 已提交
477 478 479 480
	) {
		super(id, label);
	}

481
	async run(): Promise<void> {
482
		const activeInput = this.editorService.activeEditor;
483
		const activeResource = activeInput ? activeInput.resource : undefined;
484
		if (activeResource) {
E
Erich Gamma 已提交
485

B
Benjamin Pasero 已提交
486
			// Compare with next editor that opens
R
rebornix 已提交
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
			const toDispose = this.editorService.overrideOpenEditor({
				getEditorOverrides: (editor: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => {
					return [];
				},
				open: editor => {
					// Only once!
					toDispose.dispose();

					// Open editor as diff
					const resource = editor.resource;
					if (resource) {
						return {
							override: this.editorService.openEditor({
								leftResource: activeResource,
								rightResource: resource
							})
						};
					}

					return undefined;
507
				}
B
Benjamin Pasero 已提交
508
			});
509

B
Benjamin Pasero 已提交
510 511 512 513
			once(this.quickInputService.onHide)((async () => {
				await timeout(0); // prevent race condition with editor
				toDispose.dispose();
			}));
514

515
			// Bring up quick access
516
			this.quickInputService.quickAccess.show('', { itemActivation: ItemActivation.SECOND });
E
Erich Gamma 已提交
517
		} else {
518
			this.notificationService.info(nls.localize('openFileToCompare', "Open a file first to compare it with another file."));
E
Erich Gamma 已提交
519 520 521 522
		}
	}
}

R
rebornix 已提交
523 524 525 526 527 528 529 530 531 532
export class ReopenResourcesAction extends Action {

	static readonly ID = 'workbench.files.action.reopenWithEditor';
	static readonly LABEL = nls.localize('workbench.files.action.reopenWithEditor', "Reopen With...");

	constructor(
		id: string,
		label: string,
		@IQuickInputService private readonly quickInputService: IQuickInputService,
		@IEditorService private readonly editorService: IEditorService,
533
		@IConfigurationService private readonly configurationService: IConfigurationService
R
rebornix 已提交
534 535 536 537 538 539
	) {
		super(id, label);
	}

	async run(): Promise<void> {
		const activeInput = this.editorService.activeEditor;
540 541 542 543
		if (!activeInput) {
			return;
		}

R
rebornix 已提交
544 545 546 547 548 549 550
		const activeEditorPane = this.editorService.activeEditorPane;
		if (!activeEditorPane) {
			return;
		}

		const options = activeEditorPane.options;
		const group = activeEditorPane.group;
551
		return openEditorWith(activeInput, undefined, options, group, this.editorService, this.configurationService, this.quickInputService);
R
rebornix 已提交
552 553 554
	}
}

555
export class ToggleAutoSaveAction extends Action {
B
Benjamin Pasero 已提交
556 557
	static readonly ID = 'workbench.action.toggleAutoSave';
	static readonly LABEL = nls.localize('toggleAutoSave', "Toggle Auto Save");
558 559 560 561

	constructor(
		id: string,
		label: string,
562
		@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
563 564 565 566
	) {
		super(id, label);
	}

567
	run(): Promise<void> {
568
		return this.filesConfigurationService.toggleAutoSave();
569 570 571
	}
}

I
isidor 已提交
572
export abstract class BaseSaveAllAction extends Action {
573
	private lastDirtyState: boolean;
E
Erich Gamma 已提交
574 575 576 577

	constructor(
		id: string,
		label: string,
I
isidor 已提交
578
		@ICommandService protected commandService: ICommandService,
I
isidor 已提交
579
		@INotificationService private notificationService: INotificationService,
580
		@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
E
Erich Gamma 已提交
581
	) {
I
isidor 已提交
582
		super(id, label);
E
Erich Gamma 已提交
583

584 585
		this.lastDirtyState = this.workingCopyService.hasDirty;
		this.enabled = this.lastDirtyState;
E
Erich Gamma 已提交
586 587 588 589

		this.registerListeners();
	}

590
	protected abstract doRun(context: unknown): Promise<void>;
E
Erich Gamma 已提交
591 592 593

	private registerListeners(): void {

594
		// update enablement based on working copy changes
595
		this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.updateEnablement(workingCopy)));
E
Erich Gamma 已提交
596 597
	}

598 599
	private updateEnablement(workingCopy: IWorkingCopy): void {
		const hasDirty = workingCopy.isDirty() || this.workingCopyService.hasDirty;
600
		if (this.lastDirtyState !== hasDirty) {
601
			this.enabled = hasDirty;
602
			this.lastDirtyState = this.enabled;
E
Erich Gamma 已提交
603 604 605
		}
	}

606
	async run(context?: unknown): Promise<void> {
I
isidor 已提交
607 608 609
		try {
			await this.doRun(context);
		} catch (error) {
I
isidor 已提交
610
			onError(this.notificationService, error);
I
isidor 已提交
611
		}
612
	}
E
Erich Gamma 已提交
613 614 615 616
}

export class SaveAllAction extends BaseSaveAllAction {

B
Benjamin Pasero 已提交
617 618
	static readonly ID = 'workbench.action.files.saveAll';
	static readonly LABEL = SAVE_ALL_LABEL;
E
Erich Gamma 已提交
619

B
Benjamin Pasero 已提交
620
	get class(): string {
621
		return 'explorer-action ' + Codicon.saveAll.classNames;
E
Erich Gamma 已提交
622 623
	}

624
	protected doRun(): Promise<void> {
I
isidor 已提交
625
		return this.commandService.executeCommand(SAVE_ALL_COMMAND_ID);
626 627 628 629 630
	}
}

export class SaveAllInGroupAction extends BaseSaveAllAction {

B
Benjamin Pasero 已提交
631 632
	static readonly ID = 'workbench.files.action.saveAllInGroup';
	static readonly LABEL = nls.localize('saveAllInGroup', "Save All in Group");
633

B
Benjamin Pasero 已提交
634
	get class(): string {
635
		return 'explorer-action ' + Codicon.saveAll.classNames;
636 637
	}

638
	protected doRun(context: unknown): Promise<void> {
I
isidor 已提交
639
		return this.commandService.executeCommand(SAVE_ALL_IN_GROUP_COMMAND_ID, {}, context);
640
	}
E
Erich Gamma 已提交
641 642
}

I
isidor 已提交
643 644
export class CloseGroupAction extends Action {

B
Benjamin Pasero 已提交
645 646
	static readonly ID = 'workbench.files.action.closeGroup';
	static readonly LABEL = nls.localize('closeGroup', "Close Group");
I
isidor 已提交
647

648
	constructor(id: string, label: string, @ICommandService private readonly commandService: ICommandService) {
649
		super(id, label, Codicon.closeAll.classNames);
I
isidor 已提交
650 651
	}

652
	run(context?: unknown): Promise<void> {
I
isidor 已提交
653 654 655 656
		return this.commandService.executeCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, {}, context);
	}
}

657 658
export class FocusFilesExplorer extends Action {

B
Benjamin Pasero 已提交
659 660
	static readonly ID = 'workbench.files.action.focusFilesExplorer';
	static readonly LABEL = nls.localize('focusFilesExplorer', "Focus on Files Explorer");
661 662 663 664

	constructor(
		id: string,
		label: string,
665
		@IViewletService private readonly viewletService: IViewletService
666 667 668 669
	) {
		super(id, label);
	}

670 671
	async run(): Promise<void> {
		await this.viewletService.openViewlet(VIEWLET_ID, true);
672 673 674
	}
}

675 676
export class ShowActiveFileInExplorer extends Action {

B
Benjamin Pasero 已提交
677 678
	static readonly ID = 'workbench.files.action.showActiveFileInExplorer';
	static readonly LABEL = nls.localize('showInExplorer', "Reveal Active File in Side Bar");
679 680 681 682

	constructor(
		id: string,
		label: string,
683 684 685
		@IEditorService private readonly editorService: IEditorService,
		@INotificationService private readonly notificationService: INotificationService,
		@ICommandService private readonly commandService: ICommandService
686 687 688 689
	) {
		super(id, label);
	}

690
	async run(): Promise<void> {
B
Benjamin Pasero 已提交
691
		const resource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER });
692
		if (resource) {
I
isidor 已提交
693
			this.commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, resource);
694
		} else {
695
			this.notificationService.info(nls.localize('openFileToShow', "Open a file first to show it in the explorer"));
696 697 698 699
		}
	}
}

700 701
export class CollapseExplorerView extends Action {

B
Benjamin Pasero 已提交
702 703
	static readonly ID = 'workbench.files.action.collapseExplorerFolders';
	static readonly LABEL = nls.localize('collapseExplorerFolders', "Collapse Folders in Explorer");
704

M
Matt Bierner 已提交
705
	constructor(id: string,
706
		label: string,
707 708
		@IViewletService private readonly viewletService: IViewletService,
		@IExplorerService readonly explorerService: IExplorerService
709
	) {
710
		super(id, label, 'explorer-action ' + Codicon.collapseAll.classNames);
M
Matt Bierner 已提交
711
		this._register(explorerService.onDidChangeEditable(e => {
712 713 714
			const elementIsBeingEdited = explorerService.isEditable(e);
			this.enabled = !elementIsBeingEdited;
		}));
715 716
	}

717
	async run(): Promise<void> {
S
SteVen Batten 已提交
718
		const explorerViewlet = (await this.viewletService.openViewlet(VIEWLET_ID))?.getViewPaneContainer() as ExplorerViewPaneContainer;
I
isidor 已提交
719 720 721 722
		const explorerView = explorerViewlet.getExplorerView();
		if (explorerView) {
			explorerView.collapseAll();
		}
723 724 725 726 727
	}
}

export class RefreshExplorerView extends Action {

B
Benjamin Pasero 已提交
728 729
	static readonly ID = 'workbench.files.action.refreshFilesExplorer';
	static readonly LABEL = nls.localize('refreshExplorer', "Refresh Explorer");
730

I
isidor 已提交
731

732
	constructor(
M
Matt Bierner 已提交
733
		id: string, label: string,
I
isidor 已提交
734 735
		@IViewletService private readonly viewletService: IViewletService,
		@IExplorerService private readonly explorerService: IExplorerService
736
	) {
737
		super(id, label, 'explorer-action ' + Codicon.refresh.classNames);
M
Matt Bierner 已提交
738
		this._register(explorerService.onDidChangeEditable(e => {
I
isidor 已提交
739 740 741
			const elementIsBeingEdited = explorerService.isEditable(e);
			this.enabled = !elementIsBeingEdited;
		}));
742 743
	}

744
	async run(): Promise<void> {
I
isidor 已提交
745 746
		await this.viewletService.openViewlet(VIEWLET_ID);
		this.explorerService.refresh();
747 748 749
	}
}

750 751
export class ShowOpenedFileInNewWindow extends Action {

B
Benjamin Pasero 已提交
752 753
	static readonly ID = 'workbench.action.files.showOpenedFileInNewWindow';
	static readonly LABEL = nls.localize('openFileInNewWindow', "Open Active File in New Window");
754 755 756 757

	constructor(
		id: string,
		label: string,
758
		@IEditorService private readonly editorService: IEditorService,
759
		@IHostService private readonly hostService: IHostService,
760 761
		@INotificationService private readonly notificationService: INotificationService,
		@IFileService private readonly fileService: IFileService
762 763 764 765
	) {
		super(id, label);
	}

766
	async run(): Promise<void> {
B
Benjamin Pasero 已提交
767
		const fileResource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER });
768 769
		if (fileResource) {
			if (this.fileService.canHandleResource(fileResource)) {
770
				this.hostService.openWindow([{ fileUri: fileResource }], { forceNewWindow: true });
771 772 773
			} else {
				this.notificationService.info(nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource."));
			}
774
		} else {
775
			this.notificationService.info(nls.localize('openFileToShowInNewWindow.nofile', "Open a file first to open in new window"));
776 777 778 779
		}
	}
}

780
export function validateFileName(item: ExplorerItem, name: string): { content: string, severity: Severity } | null {
E
Erich Gamma 已提交
781 782 783 784
	// Produce a well formed file name
	name = getWellFormedFileName(name);

	// Name not provided
785
	if (!name || name.length === 0 || /^\s+$/.test(name)) {
786 787 788 789
		return {
			content: nls.localize('emptyFileNameError', "A file or folder name must be provided."),
			severity: Severity.Error
		};
E
Erich Gamma 已提交
790 791
	}

792 793
	// Relative paths only
	if (name[0] === '/' || name[0] === '\\') {
794 795 796 797
		return {
			content: nls.localize('fileNameStartsWithSlashError', "A file or folder name cannot start with a slash."),
			severity: Severity.Error
		};
798 799
	}

M
Matt Bierner 已提交
800
	const names = coalesce(name.split(/[\\/]/));
801
	const parent = item.parent;
802

803 804
	if (name !== item.name) {
		// Do not allow to overwrite existing file
B
Benjamin Pasero 已提交
805
		const child = parent?.getChild(name);
I
isidor 已提交
806
		if (child && child !== item) {
807 808 809 810
			return {
				content: nls.localize('fileNameExistsError', "A file or folder **{0}** already exists at this location. Please choose a different name.", name),
				severity: Severity.Error
			};
811
		}
E
Erich Gamma 已提交
812 813
	}

814
	// Invalid File name
I
isidor 已提交
815 816
	const windowsBasenameValidity = item.resource.scheme === Schemas.file && isWindows;
	if (names.some((folderName) => !extpath.isValidBasename(folderName, windowsBasenameValidity))) {
817 818 819 820
		return {
			content: nls.localize('invalidFileNameError', "The name **{0}** is not valid as a file or folder name. Please choose a different name.", trimLongName(name)),
			severity: Severity.Error
		};
E
Erich Gamma 已提交
821 822
	}

823
	if (names.some(name => /^\s|\s$/.test(name))) {
J
jeanp413 已提交
824
		return {
825
			content: nls.localize('fileNameWhitespaceWarning', "Leading or trailing whitespace detected in file or folder name."),
J
jeanp413 已提交
826 827 828 829 830 831 832
			severity: Severity.Warning
		};
	}

	return null;
}

833
function trimLongName(name: string): string {
B
Benjamin Pasero 已提交
834
	if (name?.length > 255) {
835 836 837 838 839 840
		return `${name.substr(0, 255)}...`;
	}

	return name;
}

E
Erich Gamma 已提交
841 842 843 844 845
export function getWellFormedFileName(filename: string): string {
	if (!filename) {
		return filename;
	}

846 847
	// Trim tabs
	filename = strings.trim(filename, '\t');
E
Erich Gamma 已提交
848

849
	// Remove trailing dots and slashes
E
Erich Gamma 已提交
850
	filename = strings.rtrim(filename, '.');
T
Till Salinger 已提交
851 852 853
	filename = strings.rtrim(filename, '/');
	filename = strings.rtrim(filename, '\\');

E
Erich Gamma 已提交
854 855 856
	return filename;
}

M
Max Furman 已提交
857 858
export class CompareWithClipboardAction extends Action {

B
Benjamin Pasero 已提交
859 860
	static readonly ID = 'workbench.files.action.compareWithClipboard';
	static readonly LABEL = nls.localize('compareWithClipboard', "Compare Active File with Clipboard");
M
Max Furman 已提交
861

B
Benjamin Pasero 已提交
862
	private registrationDisposal: IDisposable | undefined;
I
isidor 已提交
863
	private static SCHEME_COUNTER = 0;
M
Max Furman 已提交
864 865 866 867

	constructor(
		id: string,
		label: string,
868 869 870 871
		@IEditorService private readonly editorService: IEditorService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@ITextModelService private readonly textModelService: ITextModelService,
		@IFileService private readonly fileService: IFileService
M
Max Furman 已提交
872 873 874 875 876 877
	) {
		super(id, label);

		this.enabled = true;
	}

878
	async run(): Promise<void> {
B
Benjamin Pasero 已提交
879
		const resource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER });
I
isidor 已提交
880
		const scheme = `clipboardCompare${CompareWithClipboardAction.SCHEME_COUNTER++}`;
881
		if (resource && (this.fileService.canHandleResource(resource) || resource.scheme === Schemas.untitled)) {
B
Benjamin Pasero 已提交
882
			if (!this.registrationDisposal) {
883
				const provider = this.instantiationService.createInstance(ClipboardContentProvider);
I
isidor 已提交
884
				this.registrationDisposal = this.textModelService.registerTextModelContentProvider(scheme, provider);
B
Benjamin Pasero 已提交
885 886
			}

887
			const name = resources.basename(resource);
M
Max Furman 已提交
888 889
			const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name);

I
isidor 已提交
890
			await this.editorService.openEditor({ leftResource: resource.with({ scheme }), rightResource: resource, label: editorLabel }).finally(() => {
B
Benjamin Pasero 已提交
891 892
				dispose(this.registrationDisposal);
				this.registrationDisposal = undefined;
I
isidor 已提交
893
			});
M
Max Furman 已提交
894 895 896
		}
	}

B
Benjamin Pasero 已提交
897
	dispose(): void {
M
Max Furman 已提交
898 899
		super.dispose();

B
Benjamin Pasero 已提交
900 901
		dispose(this.registrationDisposal);
		this.registrationDisposal = undefined;
M
Max Furman 已提交
902 903 904 905 906
	}
}

class ClipboardContentProvider implements ITextModelContentProvider {
	constructor(
907 908 909
		@IClipboardService private readonly clipboardService: IClipboardService,
		@IModeService private readonly modeService: IModeService,
		@IModelService private readonly modelService: IModelService
M
Max Furman 已提交
910 911
	) { }

912
	async provideTextContent(resource: URI): Promise<ITextModel> {
I
isidor 已提交
913 914
		const text = await this.clipboardService.readText();
		const model = this.modelService.createModel(text, this.modeService.createByFilepathOrFirstLine(resource), resource);
B
Benjamin Pasero 已提交
915

916
		return model;
M
Max Furman 已提交
917 918 919
	}
}

920
function onErrorWithRetry(notificationService: INotificationService, error: unknown, retry: () => Promise<unknown>): void {
I
isidor 已提交
921 922 923 924 925 926 927 928 929 930 931
	notificationService.prompt(Severity.Error, toErrorMessage(error, false),
		[{
			label: nls.localize('retry', "Retry"),
			run: () => retry()
		}]
	);
}

async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise<void> {
	const explorerService = accessor.get(IExplorerService);
	const fileService = accessor.get(IFileService);
932
	const textFileService = accessor.get(ITextFileService);
I
isidor 已提交
933
	const editorService = accessor.get(IEditorService);
934
	const viewletService = accessor.get(IViewletService);
B
Benjamin Pasero 已提交
935
	const notificationService = accessor.get(INotificationService);
936 937

	await viewletService.openViewlet(VIEWLET_ID, true);
938

939 940 941 942 943 944 945 946
	const stats = explorerService.getContext(false);
	const stat = stats.length > 0 ? stats[0] : undefined;
	let folder: ExplorerItem;
	if (stat) {
		folder = stat.isDirectory ? stat : (stat.parent || explorerService.roots[0]);
	} else {
		folder = explorerService.roots[0];
	}
947

948 949 950
	if (folder.isReadonly) {
		throw new Error('Parent folder is readonly.');
	}
951

952 953 954
	const newStat = new NewExplorerItem(fileService, folder, isFolder);
	const sortOrder = explorerService.sortOrder;
	await folder.fetchChildren(sortOrder);
I
isidor 已提交
955

956
	folder.addChild(newStat);
I
isidor 已提交
957

958 959 960
	const onSuccess = async (value: string): Promise<void> => {
		try {
			const created = isFolder ? await fileService.createFolder(resources.joinPath(folder.resource, value)) : await textFileService.create(resources.joinPath(folder.resource, value));
961
			refreshIfSeparator(value, explorerService);
962 963 964 965 966

			isFolder ?
				await explorerService.select(created.resource, true) :
				await editorService.openEditor({ resource: created.resource, options: { pinned: true } });
		} catch (error) {
967
			onErrorWithRetry(notificationService, error, () => onSuccess(value));
968
		}
969 970 971 972 973 974 975 976 977 978 979 980
	};

	explorerService.setEditable(newStat, {
		validationMessage: value => validateFileName(newStat, value),
		onFinish: (value, success) => {
			folder.removeChild(newStat);
			explorerService.setEditable(newStat, null);
			if (success) {
				onSuccess(value);
			}
		}
	});
981 982
}

I
isidor 已提交
983 984
CommandsRegistry.registerCommand({
	id: NEW_FILE_COMMAND_ID,
I
isidor 已提交
985 986
	handler: async (accessor) => {
		await openExplorerAndCreate(accessor, false);
I
isidor 已提交
987 988
	}
});
I
isidor 已提交
989 990 991

CommandsRegistry.registerCommand({
	id: NEW_FOLDER_COMMAND_ID,
I
isidor 已提交
992 993
	handler: async (accessor) => {
		await openExplorerAndCreate(accessor, true);
I
isidor 已提交
994 995
	}
});
I
isidor 已提交
996

I
isidor 已提交
997
export const renameHandler = (accessor: ServicesAccessor) => {
998
	const explorerService = accessor.get(IExplorerService);
999
	const workingCopyFileService = accessor.get(IWorkingCopyFileService);
I
isidor 已提交
1000
	const notificationService = accessor.get(INotificationService);
M
Matt Bierner 已提交
1001

1002 1003
	const stats = explorerService.getContext(false);
	const stat = stats.length > 0 ? stats[0] : undefined;
I
isidor 已提交
1004 1005 1006
	if (!stat) {
		return;
	}
1007 1008 1009

	explorerService.setEditable(stat, {
		validationMessage: value => validateFileName(stat, value),
I
isidor 已提交
1010
		onFinish: async (value, success) => {
1011
			if (success) {
I
isidor 已提交
1012
				const parentResource = stat.parent!.resource;
1013
				const targetResource = resources.joinPath(parentResource, value);
I
isidor 已提交
1014
				if (stat.resource.toString() !== targetResource.toString()) {
I
isidor 已提交
1015
					try {
1016
						await workingCopyFileService.move(stat.resource, targetResource);
I
isidor 已提交
1017 1018 1019 1020
						refreshIfSeparator(value, explorerService);
					} catch (e) {
						notificationService.error(e);
					}
I
isidor 已提交
1021
				}
1022 1023 1024 1025
			}
			explorerService.setEditable(stat, null);
		}
	});
I
isidor 已提交
1026
};
I
isidor 已提交
1027

1028
export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => {
1029
	const explorerService = accessor.get(IExplorerService);
1030 1031
	const stats = explorerService.getContext(true).filter(s => !s.isRoot);
	if (stats.length) {
1032
		await deleteFiles(accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true);
1033
	}
I
isidor 已提交
1034
};
I
isidor 已提交
1035

1036
export const deleteFileHandler = async (accessor: ServicesAccessor) => {
1037
	const explorerService = accessor.get(IExplorerService);
1038
	const stats = explorerService.getContext(true).filter(s => !s.isRoot);
I
isidor 已提交
1039

1040
	if (stats.length) {
1041
		await deleteFiles(accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false);
1042
	}
I
isidor 已提交
1043
};
I
isidor 已提交
1044

1045
let pasteShouldMove = false;
1046
export const copyFileHandler = (accessor: ServicesAccessor) => {
1047
	const explorerService = accessor.get(IExplorerService);
1048 1049
	const stats = explorerService.getContext(true);
	if (stats.length > 0) {
I
isidor 已提交
1050 1051 1052
		explorerService.setToCopy(stats, false);
		pasteShouldMove = false;
	}
I
isidor 已提交
1053 1054
};

I
isidor 已提交
1055
export const cutFileHandler = (accessor: ServicesAccessor) => {
I
isidor 已提交
1056
	const explorerService = accessor.get(IExplorerService);
1057 1058
	const stats = explorerService.getContext(true);
	if (stats.length > 0) {
I
isidor 已提交
1059 1060 1061
		explorerService.setToCopy(stats, true);
		pasteShouldMove = true;
	}
I
isidor 已提交
1062 1063
};

I
isidor 已提交
1064 1065
export const DOWNLOAD_COMMAND_ID = 'explorer.download';
const downloadFileHandler = (accessor: ServicesAccessor) => {
1066
	const fileService = accessor.get(IFileService);
1067
	const workingCopyFileService = accessor.get(IWorkingCopyFileService);
I
isidor 已提交
1068
	const fileDialogService = accessor.get(IFileDialogService);
1069 1070
	const explorerService = accessor.get(IExplorerService);
	const stats = explorerService.getContext(true);
I
isidor 已提交
1071

1072
	let canceled = false;
I
isidor 已提交
1073
	sequence(stats.map(s => async () => {
1074 1075 1076 1077
		if (canceled) {
			return;
		}

1078 1079
		if (isWeb) {
			if (!s.isDirectory) {
1080 1081 1082 1083 1084 1085 1086 1087
				let bufferOrUri: Uint8Array | URI;
				try {
					bufferOrUri = (await fileService.readFile(s.resource, { limits: { size: 1024 * 1024  /* set a limit to reduce memory pressure */ } })).value.buffer;
				} catch (error) {
					bufferOrUri = asDomUri(s.resource);
				}

				triggerDownload(bufferOrUri, s.name);
1088 1089
			}
		} else {
I
isidor 已提交
1090
			let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath(Schemas.file) : fileDialogService.defaultFilePath(Schemas.file);
1091
			if (defaultUri) {
1092 1093
				defaultUri = resources.joinPath(defaultUri, s.name);
			}
1094

1095 1096 1097 1098 1099 1100 1101
			const destination = await fileDialogService.showSaveDialog({
				availableFileSystems: [Schemas.file],
				saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")),
				title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"),
				defaultUri
			});
			if (destination) {
1102
				await workingCopyFileService.copy(s.resource, destination, true);
1103 1104 1105
			} else {
				// User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100
				canceled = true;
I
isidor 已提交
1106
			}
1107
		}
I
isidor 已提交
1108
	}));
I
isidor 已提交
1109
};
1110

I
isidor 已提交
1111 1112 1113 1114 1115
CommandsRegistry.registerCommand({
	id: DOWNLOAD_COMMAND_ID,
	handler: downloadFileHandler
});

I
isidor 已提交
1116
export const pasteFileHandler = async (accessor: ServicesAccessor) => {
1117
	const clipboardService = accessor.get(IClipboardService);
1118 1119
	const explorerService = accessor.get(IExplorerService);
	const fileService = accessor.get(IFileService);
1120
	const workingCopyFileService = accessor.get(IWorkingCopyFileService);
1121 1122
	const notificationService = accessor.get(INotificationService);
	const editorService = accessor.get(IEditorService);
I
isidor 已提交
1123
	const configurationService = accessor.get(IConfigurationService);
1124

1125 1126 1127
	const context = explorerService.getContext(true);
	const toPaste = resources.distinctParents(clipboardService.readResources(), r => r);
	const element = context.length ? context[0] : explorerService.roots[0];
1128

1129 1130
	// Check if target is ancestor of pasted folder
	const stats = await Promise.all(toPaste.map(async fileToPaste => {
I
isidor 已提交
1131

1132 1133 1134
		if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
			throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
		}
1135

1136 1137
		try {
			const fileToPasteStat = await fileService.resolve(fileToPaste);
1138

1139 1140 1141 1142 1143 1144 1145
			// Find target
			let target: ExplorerItem;
			if (element.resource.toString() === fileToPaste.toString()) {
				target = element.parent!;
			} else {
				target = element.isDirectory ? element : element.parent!;
			}
1146

1147
			const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
1148
			const targetFile = findValidPasteFileTarget(explorerService, target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming);
1149

1150 1151
			// Move/Copy File
			if (pasteShouldMove) {
1152
				return await workingCopyFileService.move(fileToPaste, targetFile);
1153
			} else {
1154
				return await workingCopyFileService.copy(fileToPaste, targetFile);
1155
			}
1156
		} catch (e) {
I
isidor 已提交
1157
			onError(notificationService, new Error(nls.localize('fileDeleted', "The file to paste has been deleted or moved since you copied it. {0}", getErrorMessage(e))));
1158 1159 1160
			return undefined;
		}
	}));
I
isidor 已提交
1161

1162 1163 1164
	if (pasteShouldMove) {
		// Cut is done. Make sure to clear cut state.
		explorerService.setToCopy([], false);
I
isidor 已提交
1165
		pasteShouldMove = false;
1166 1167 1168 1169 1170
	}
	if (stats.length >= 1) {
		const stat = stats[0];
		if (stat && !stat.isDirectory && stats.length === 1) {
			await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } });
I
isidor 已提交
1171
		}
1172 1173
		if (stat) {
			await explorerService.select(stat.resource);
I
isidor 已提交
1174
		}
1175
	}
I
isidor 已提交
1176
};
I
isidor 已提交
1177 1178 1179

export const openFilePreserveFocusHandler = async (accessor: ServicesAccessor) => {
	const editorService = accessor.get(IEditorService);
1180 1181
	const explorerService = accessor.get(IExplorerService);
	const stats = explorerService.getContext(true);
I
isidor 已提交
1182

1183 1184 1185 1186
	await editorService.openEditors(stats.filter(s => !s.isDirectory).map(s => ({
		resource: s.resource,
		options: { preserveFocus: true }
	})));
I
isidor 已提交
1187
};