diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 272002dd513..7fd1f9b1f14 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1042,6 +1042,7 @@ export const EventType = { UNLOAD: 'unload', PAGE_SHOW: 'pageshow', PAGE_HIDE: 'pagehide', + PASTE: 'paste', ABORT: 'abort', ERROR: 'error', RESIZE: 'resize', diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 202e523138d..9816031347d 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -116,12 +116,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const PASTE_FILE_ID = 'filesExplorer.paste'; -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: PASTE_FILE_ID, +CommandsRegistry.registerCommand(PASTE_FILE_ID, pasteFileHandler); + +KeybindingsRegistry.registerKeybindingRule({ + id: `^${PASTE_FILE_ID}`, // the `^` enables pasting files into the explorer by preventing default bubble up weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext), primary: KeyMod.CtrlCmd | KeyCode.KeyV, - handler: pasteFileHandler }); KeybindingsRegistry.registerCommandAndKeybindingRule({ diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 76c1e965ade..0f153505328 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; -import { extname, basename } from 'vs/base/common/path'; +import { extname, basename, isAbsolute } from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -1082,7 +1082,7 @@ CommandsRegistry.registerCommand({ handler: uploadFileHandler }); -export const pasteFileHandler = async (accessor: ServicesAccessor) => { +export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: FileList) => { const clipboardService = accessor.get(IClipboardService); const explorerService = accessor.get(IExplorerService); const fileService = accessor.get(IFileService); @@ -1093,7 +1093,34 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { const dialogService = accessor.get(IDialogService); const context = explorerService.getContext(true); - const toPaste = resources.distinctParents(await clipboardService.readResources(), r => r); + const hasNativeFilesToPaste = fileList && fileList.length > 0; + const confirmPasteNative = hasNativeFilesToPaste && configurationService.getValue('explorer.confirmPasteNative'); + + const toPaste = await getFilesToPaste(fileList, clipboardService); + + if (confirmPasteNative) { + const message = toPaste.length > 1 ? + nls.localize('confirmMultiPasteNative', "Are you sure you want to paste the following {0} items?", toPaste.length) : + nls.localize('confirmPasteNative', "Are you sure you want to paste '{0}'?", basename(toPaste[0].fsPath)); + const detail = toPaste.length > 1 ? getFileNamesMessage(toPaste) : undefined; + const confirmation = await dialogService.confirm({ + message, + detail, + checkbox: { + label: nls.localize('doNotAskAgain', "Do not ask me again") + }, + primaryButton: nls.localize({ key: 'pasteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Paste") + }); + + if (!confirmation.confirmed) { + return; + } + + // Check for confirmation checkbox + if (confirmation.checkboxChecked === true) { + await configurationService.updateValue('explorer.confirmPasteNative', false); + } + } const element = context.length ? context[0] : explorerService.roots[0]; const incrementalNaming = configurationService.getValue().explorer.incrementalNaming; @@ -1175,6 +1202,16 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { } }; +async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService): Promise { + if (fileList) { + // with a `fileList` we support natively pasting files from clipboard + return [...fileList].filter(file => !!file.path && isAbsolute(file.path)).map(file => URI.file(file.path)); + } else { + // otherwise we fallback to reading resources from our clipboard service + return resources.distinctParents(await clipboardService.readResources(), resource => resource); + } +} + export const openFilePreserveFocusHandler = async (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); const explorerService = accessor.get(IExplorerService); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 8423b6b046f..985ef683fce 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -444,6 +444,11 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('confirmDragAndDrop', "Controls whether the Explorer should ask for confirmation to move files and folders via drag and drop."), 'default': true }, + 'explorer.confirmPasteNative': { + 'type': 'boolean', + 'description': nls.localize('confirmPasteNative', "Controls whether the Explorer should ask for confirmation when pasting native files and folders."), + 'default': true + }, 'explorer.confirmDelete': { 'type': 'boolean', 'description': nls.localize('confirmDelete', "Controls whether the Explorer should ask for confirmation when deleting a file via the trash."), diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index ba439a3585c..fcc5cb9631e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -321,6 +321,15 @@ export class ExplorerView extends ViewPane implements IExplorerView { this.selectActiveFile(true); } })); + + // Support for paste of files into explorer + this._register(DOM.addDisposableListener(DOM.getWindow(this.container), DOM.EventType.PASTE, async event => { + if (!this.hasFocus() || this.readonlyContext.get()) { + return; + } + + await this.commandService.executeCommand('filesExplorer.paste', event.clipboardData?.files); + })); } override focus(): void {