Skip to content

Commit 4d3db48

Browse files
authored
Enable explorer pasting of files without paths (microsoft#204052)
Fixes microsoft#204051 This enables pasting clipboard files that don't have a `.path`. This enables: - Pasting image data into the explorer - Pasting files into the explorer on web
1 parent 12904c6 commit 4d3db48

File tree

1 file changed

+115
-65
lines changed

1 file changed

+115
-65
lines changed

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

Lines changed: 115 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis
5959
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
6060
import { Categories } from 'vs/platform/action/common/actionCommonCategories';
6161
import { ILocalizedString } from 'vs/platform/action/common/action';
62+
import { VSBuffer } from 'vs/base/common/buffer';
6263

6364
export const NEW_FILE_COMMAND_ID = 'explorer.newFile';
6465
export const NEW_FILE_LABEL = nls.localize2('newFile', "New File...");
@@ -308,11 +309,11 @@ export async function findValidPasteFileTarget(
308309
fileService: IFileService,
309310
dialogService: IDialogService,
310311
targetFolder: ExplorerItem,
311-
fileToPaste: { resource: URI; isDirectory?: boolean; allowOverwrite: boolean },
312+
fileToPaste: { resource: URI | string; isDirectory?: boolean; allowOverwrite: boolean },
312313
incrementalNaming: 'simple' | 'smart' | 'disabled'
313314
): Promise<URI | undefined> {
314315

315-
let name = resources.basenameOrAuthority(fileToPaste.resource);
316+
let name = typeof fileToPaste.resource === 'string' ? fileToPaste.resource : resources.basenameOrAuthority(fileToPaste.resource);
316317
let candidate = resources.joinPath(targetFolder.resource, name);
317318

318319
// In the disabled case we must ask if it's ok to overwrite the file if it exists
@@ -1098,11 +1099,11 @@ export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: Fi
10981099

10991100
const toPaste = await getFilesToPaste(fileList, clipboardService);
11001101

1101-
if (confirmPasteNative && toPaste?.length >= 1) {
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;
1102+
if (confirmPasteNative && toPaste.files.length >= 1) {
1103+
const message = toPaste.files.length > 1 ?
1104+
nls.localize('confirmMultiPasteNative', "Are you sure you want to paste the following {0} items?", toPaste.files.length) :
1105+
nls.localize('confirmPasteNative', "Are you sure you want to paste '{0}'?", basename(toPaste.type === 'paths' ? toPaste.files[0].fsPath : toPaste.files[0].name));
1106+
const detail = toPaste.files.length > 1 ? getFileNamesMessage(toPaste.files.map(item => toPaste.type === 'paths' ? item.path : (item as File).name)) : undefined;
11061107
const confirmation = await dialogService.confirm({
11071108
message,
11081109
detail,
@@ -1131,67 +1132,94 @@ export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: Fi
11311132
}
11321133

11331134
try {
1134-
// Check if target is ancestor of pasted folder
1135-
const sourceTargetPairs = coalesce(await Promise.all(toPaste.map(async fileToPaste => {
1135+
let targets: URI[] = [];
11361136

1137-
if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
1138-
throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
1139-
}
1140-
const fileToPasteStat = await fileService.stat(fileToPaste);
1137+
if (toPaste.type === 'paths') { // Pasting from files on disk
11411138

1142-
// Find target
1143-
let target: ExplorerItem;
1144-
if (uriIdentityService.extUri.isEqual(element.resource, fileToPaste)) {
1145-
target = element.parent!;
1146-
} else {
1147-
target = element.isDirectory ? element : element.parent!;
1148-
}
1139+
// Check if target is ancestor of pasted folder
1140+
const sourceTargetPairs = coalesce(await Promise.all(toPaste.files.map(async fileToPaste => {
1141+
if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
1142+
throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
1143+
}
1144+
const fileToPasteStat = await fileService.stat(fileToPaste);
1145+
1146+
// Find target
1147+
let target: ExplorerItem;
1148+
if (uriIdentityService.extUri.isEqual(element.resource, fileToPaste)) {
1149+
target = element.parent!;
1150+
} else {
1151+
target = element.isDirectory ? element : element.parent!;
1152+
}
11491153

1150-
const targetFile = await findValidPasteFileTarget(
1151-
explorerService,
1152-
fileService,
1153-
dialogService,
1154-
target,
1155-
{ resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },
1156-
incrementalNaming
1157-
);
1158-
1159-
if (!targetFile) {
1160-
return undefined;
1154+
const targetFile = await findValidPasteFileTarget(
1155+
explorerService,
1156+
fileService,
1157+
dialogService,
1158+
target,
1159+
{ resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },
1160+
incrementalNaming
1161+
);
1162+
1163+
if (!targetFile) {
1164+
return undefined;
1165+
}
1166+
1167+
return { source: fileToPaste, target: targetFile };
1168+
})));
1169+
1170+
if (sourceTargetPairs.length >= 1) {
1171+
// Move/Copy File
1172+
if (pasteShouldMove) {
1173+
const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { overwrite: incrementalNaming === 'disabled' }));
1174+
const options = {
1175+
confirmBeforeUndo: configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo === UndoConfirmLevel.Verbose,
1176+
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)
1177+
: nls.localize({ key: 'movingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)),
1178+
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)
1179+
: nls.localize({ key: 'moveFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target))
1180+
};
1181+
await explorerService.applyBulkEdit(resourceFileEdits, options);
1182+
} else {
1183+
const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true, overwrite: incrementalNaming === 'disabled' }));
1184+
await applyCopyResourceEdit(sourceTargetPairs.map(pair => pair.target), resourceFileEdits);
1185+
}
11611186
}
11621187

1163-
return { source: fileToPaste, target: targetFile };
1164-
})));
1165-
1166-
if (sourceTargetPairs.length >= 1) {
1167-
// Move/Copy File
1168-
if (pasteShouldMove) {
1169-
const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { overwrite: incrementalNaming === 'disabled' }));
1170-
const options = {
1171-
confirmBeforeUndo: configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo === UndoConfirmLevel.Verbose,
1172-
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)
1173-
: nls.localize({ key: 'movingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)),
1174-
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)
1175-
: nls.localize({ key: 'moveFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target))
1176-
};
1177-
await explorerService.applyBulkEdit(resourceFileEdits, options);
1178-
} else {
1179-
const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true, overwrite: incrementalNaming === 'disabled' }));
1180-
const undoLevel = configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo;
1181-
const options = {
1182-
confirmBeforeUndo: undoLevel === UndoConfirmLevel.Default || undoLevel === UndoConfirmLevel.Verbose,
1183-
progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'copyingBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copying {0} files", sourceTargetPairs.length)
1184-
: nls.localize({ key: 'copyingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)),
1185-
undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'copyBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Paste {0} files", sourceTargetPairs.length)
1186-
: nls.localize({ key: 'copyFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Paste {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target))
1188+
targets = sourceTargetPairs.map(pair => pair.target);
1189+
1190+
} else { // Pasting from file data
1191+
const targetAndEdits = coalesce(await Promise.all(toPaste.files.map(async file => {
1192+
const target = element.isDirectory ? element : element.parent!;
1193+
1194+
const targetFile = await findValidPasteFileTarget(
1195+
explorerService,
1196+
fileService,
1197+
dialogService,
1198+
target,
1199+
{ resource: file.name, isDirectory: false, allowOverwrite: pasteShouldMove || incrementalNaming === 'disabled' },
1200+
incrementalNaming
1201+
);
1202+
if (!targetFile) {
1203+
return;
1204+
}
1205+
return {
1206+
target: targetFile,
1207+
edit: new ResourceFileEdit(undefined, targetFile, {
1208+
overwrite: incrementalNaming === 'disabled',
1209+
contents: (async () => VSBuffer.wrap(new Uint8Array(await file.arrayBuffer())))(),
1210+
})
11871211
};
1188-
await explorerService.applyBulkEdit(resourceFileEdits, options);
1189-
}
1212+
})));
1213+
1214+
await applyCopyResourceEdit(targetAndEdits.map(pair => pair.target), targetAndEdits.map(pair => pair.edit));
1215+
targets = targetAndEdits.map(pair => pair.target);
1216+
}
11901217

1191-
const pair = sourceTargetPairs[0];
1192-
await explorerService.select(pair.target);
1193-
if (sourceTargetPairs.length === 1) {
1194-
const item = explorerService.findClosest(pair.target);
1218+
if (targets.length) {
1219+
const firstTarget = targets[0];
1220+
await explorerService.select(firstTarget);
1221+
if (targets.length === 1) {
1222+
const item = explorerService.findClosest(firstTarget);
11951223
if (item && !item.isDirectory) {
11961224
await editorService.openEditor({ resource: item.resource, options: { pinned: true, preserveFocus: true } });
11971225
}
@@ -1206,15 +1234,37 @@ export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: Fi
12061234
pasteShouldMove = false;
12071235
}
12081236
}
1237+
1238+
async function applyCopyResourceEdit(targets: readonly URI[], resourceFileEdits: ResourceFileEdit[]) {
1239+
const undoLevel = configurationService.getValue<IFilesConfiguration>().explorer.confirmUndo;
1240+
const options = {
1241+
confirmBeforeUndo: undoLevel === UndoConfirmLevel.Default || undoLevel === UndoConfirmLevel.Verbose,
1242+
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)
1243+
: nls.localize({ key: 'copyingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copying {0}", resources.basenameOrAuthority(targets[0])),
1244+
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)
1245+
: nls.localize({ key: 'copyFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Paste {0}", resources.basenameOrAuthority(targets[0]))
1246+
};
1247+
await explorerService.applyBulkEdit(resourceFileEdits, options);
1248+
}
12091249
};
12101250

1211-
async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService): Promise<readonly URI[]> {
1251+
type FilesToPaste =
1252+
| { type: 'paths'; files: URI[] }
1253+
| { type: 'data'; files: File[] };
1254+
1255+
async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService): Promise<FilesToPaste> {
12121256
if (fileList && fileList.length > 0) {
1213-
// with a `fileList` we support natively pasting files from clipboard
1214-
return [...fileList].filter(file => !!file.path && isAbsolute(file.path)).map(file => URI.file(file.path));
1257+
// with a `fileList` we support natively pasting file from disk from clipboard
1258+
const resources = [...fileList].filter(file => !!file.path && isAbsolute(file.path)).map(file => URI.file(file.path));
1259+
if (resources.length) {
1260+
return { type: 'paths', files: resources, };
1261+
}
1262+
1263+
// Support pasting files that we can't read from disk
1264+
return { type: 'data', files: [...fileList].filter(file => !file.path) };
12151265
} else {
12161266
// otherwise we fallback to reading resources from our clipboard service
1217-
return resources.distinctParents(await clipboardService.readResources(), resource => resource);
1267+
return { type: 'paths', files: resources.distinctParents(await clipboardService.readResources(), resource => resource) };
12181268
}
12191269
}
12201270

0 commit comments

Comments
 (0)