Skip to content

Commit 96b1781

Browse files
SimonSiefkebpasero
andauthored
feature: allow to paste files from the clipboard (#195730)
* feature: add basic file pasting (native) * feature: allow native file pasting to coexist with explorer file pasting * refactor: remove console log * undo-import-changes * fix: lint * refactor: move paste file handler to explorerView * simplify code * remove unused import * fix: handle files without path when running in web * 💄 * feature: add paste confirmation dialog * improve confirmation message * feature: improve confirmation message when items are folders and not files * simplify code * simplify code * remove unused import --------- Co-authored-by: Benjamin Pasero <[email protected]> Co-authored-by: Benjamin Pasero <[email protected]>
1 parent c0dd927 commit 96b1781

5 files changed

Lines changed: 59 additions & 6 deletions

File tree

src/vs/base/browser/dom.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,7 @@ export const EventType = {
10421042
UNLOAD: 'unload',
10431043
PAGE_SHOW: 'pageshow',
10441044
PAGE_HIDE: 'pagehide',
1045+
PASTE: 'paste',
10451046
ABORT: 'abort',
10461047
ERROR: 'error',
10471048
RESIZE: 'resize',

src/vs/workbench/contrib/files/browser/fileActions.contribution.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
116116

117117
const PASTE_FILE_ID = 'filesExplorer.paste';
118118

119-
KeybindingsRegistry.registerCommandAndKeybindingRule({
120-
id: PASTE_FILE_ID,
119+
CommandsRegistry.registerCommand(PASTE_FILE_ID, pasteFileHandler);
120+
121+
KeybindingsRegistry.registerKeybindingRule({
122+
id: `^${PASTE_FILE_ID}`, // the `^` enables pasting files into the explorer by preventing default bubble up
121123
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
122124
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext),
123125
primary: KeyMod.CtrlCmd | KeyCode.KeyV,
124-
handler: pasteFileHandler
125126
});
126127

127128
KeybindingsRegistry.registerCommandAndKeybindingRule({

src/vs/workbench/contrib/files/browser/fileActions.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as nls from 'vs/nls';
77
import { isWindows, OperatingSystem, OS } from 'vs/base/common/platform';
8-
import { extname, basename } from 'vs/base/common/path';
8+
import { extname, basename, isAbsolute } from 'vs/base/common/path';
99
import * as resources from 'vs/base/common/resources';
1010
import { URI } from 'vs/base/common/uri';
1111
import { toErrorMessage } from 'vs/base/common/errorMessage';
@@ -1082,7 +1082,7 @@ CommandsRegistry.registerCommand({
10821082
handler: uploadFileHandler
10831083
});
10841084

1085-
export const pasteFileHandler = async (accessor: ServicesAccessor) => {
1085+
export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: FileList) => {
10861086
const clipboardService = accessor.get(IClipboardService);
10871087
const explorerService = accessor.get(IExplorerService);
10881088
const fileService = accessor.get(IFileService);
@@ -1093,7 +1093,34 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
10931093
const dialogService = accessor.get(IDialogService);
10941094

10951095
const context = explorerService.getContext(true);
1096-
const toPaste = resources.distinctParents(await clipboardService.readResources(), r => r);
1096+
const hasNativeFilesToPaste = fileList && fileList.length > 0;
1097+
const confirmPasteNative = hasNativeFilesToPaste && configurationService.getValue<boolean>('explorer.confirmPasteNative');
1098+
1099+
const toPaste = await getFilesToPaste(fileList, clipboardService);
1100+
1101+
if (confirmPasteNative) {
1102+
const message = toPaste.length > 1 ?
1103+
nls.localize('confirmMultiPasteNative', "Are you sure you want to paste the following {0} items?", toPaste.length) :
1104+
nls.localize('confirmPasteNative', "Are you sure you want to paste '{0}'?", basename(toPaste[0].fsPath));
1105+
const detail = toPaste.length > 1 ? getFileNamesMessage(toPaste) : undefined;
1106+
const confirmation = await dialogService.confirm({
1107+
message,
1108+
detail,
1109+
checkbox: {
1110+
label: nls.localize('doNotAskAgain', "Do not ask me again")
1111+
},
1112+
primaryButton: nls.localize({ key: 'pasteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Paste")
1113+
});
1114+
1115+
if (!confirmation.confirmed) {
1116+
return;
1117+
}
1118+
1119+
// Check for confirmation checkbox
1120+
if (confirmation.checkboxChecked === true) {
1121+
await configurationService.updateValue('explorer.confirmPasteNative', false);
1122+
}
1123+
}
10971124
const element = context.length ? context[0] : explorerService.roots[0];
10981125
const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
10991126

@@ -1175,6 +1202,16 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
11751202
}
11761203
};
11771204

1205+
async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService): Promise<readonly URI[]> {
1206+
if (fileList) {
1207+
// with a `fileList` we support natively pasting files from clipboard
1208+
return [...fileList].filter(file => !!file.path && isAbsolute(file.path)).map(file => URI.file(file.path));
1209+
} else {
1210+
// otherwise we fallback to reading resources from our clipboard service
1211+
return resources.distinctParents(await clipboardService.readResources(), resource => resource);
1212+
}
1213+
}
1214+
11781215
export const openFilePreserveFocusHandler = async (accessor: ServicesAccessor) => {
11791216
const editorService = accessor.get(IEditorService);
11801217
const explorerService = accessor.get(IExplorerService);

src/vs/workbench/contrib/files/browser/files.contribution.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,11 @@ configurationRegistry.registerConfiguration({
444444
'description': nls.localize('confirmDragAndDrop', "Controls whether the Explorer should ask for confirmation to move files and folders via drag and drop."),
445445
'default': true
446446
},
447+
'explorer.confirmPasteNative': {
448+
'type': 'boolean',
449+
'description': nls.localize('confirmPasteNative', "Controls whether the Explorer should ask for confirmation when pasting native files and folders."),
450+
'default': true
451+
},
447452
'explorer.confirmDelete': {
448453
'type': 'boolean',
449454
'description': nls.localize('confirmDelete', "Controls whether the Explorer should ask for confirmation when deleting a file via the trash."),

src/vs/workbench/contrib/files/browser/views/explorerView.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,15 @@ export class ExplorerView extends ViewPane implements IExplorerView {
321321
this.selectActiveFile(true);
322322
}
323323
}));
324+
325+
// Support for paste of files into explorer
326+
this._register(DOM.addDisposableListener(DOM.getWindow(this.container), DOM.EventType.PASTE, async event => {
327+
if (!this.hasFocus() || this.readonlyContext.get()) {
328+
return;
329+
}
330+
331+
await this.commandService.executeCommand('filesExplorer.paste', event.clipboardData?.files);
332+
}));
324333
}
325334

326335
override focus(): void {

0 commit comments

Comments
 (0)