Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow creating multiple files or attachments on paste #181975

Merged
merged 1 commit into from May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 23 additions & 8 deletions extensions/ipynb/src/notebookImagePaste.ts
Expand Up @@ -18,6 +18,16 @@ enum MimeType {
uriList = 'text/uri-list',
}

const imageMimeTypes: ReadonlySet<string> = new Set<string>([
MimeType.bmp,
MimeType.gif,
MimeType.ico,
MimeType.jpeg,
MimeType.png,
MimeType.tiff,
MimeType.webp,
]);

const imageExtToMime: ReadonlyMap<string, string> = new Map<string, string>([
['.bmp', MimeType.bmp],
['.gif', MimeType.gif],
Expand Down Expand Up @@ -126,16 +136,21 @@ async function getDroppedImageData(
): Promise<readonly ImageAttachmentData[]> {

// Prefer using image data in the clipboard
// TODO: dataTransfer.get() limits to one image pasted. Should we support multiple?
const pngDataItem = dataTransfer.get(MimeType.png);
if (pngDataItem) {
const fileItem = pngDataItem.asFile();
if (!fileItem) {
return [];
const files = coalesce(await Promise.all(Array.from(dataTransfer, async ([mimeType, item]): Promise<ImageAttachmentData | undefined> => {
if (!imageMimeTypes.has(mimeType)) {
return;
}

const file = item.asFile();
if (!file) {
return;
}

const data = await fileItem.data();
return [{ fileName: fileItem.name, mimeType: MimeType.png, data }];
const data = await file.data();
return { fileName: file.name, mimeType, data };
})));
if (files.length) {
return files;
}

// Then fallback to image files in the uri-list
Expand Down
Expand Up @@ -8,23 +8,42 @@ import * as vscode from 'vscode';
import { Utils } from 'vscode-uri';
import { getParentDocumentUri } from './dropIntoEditor';

export class NewFilePathGenerator {

export async function getNewFileName(document: vscode.TextDocument, file: vscode.DataTransferFile): Promise<vscode.Uri> {
const desiredPath = getDesiredNewFilePath(document, file);

const root = Utils.dirname(desiredPath);
const ext = path.extname(file.name);
const baseName = path.basename(file.name, ext);
for (let i = 0; ; ++i) {
const name = i === 0 ? baseName : `${baseName}-${i}`;
const uri = vscode.Uri.joinPath(root, `${name}${ext}`);
try {
await vscode.workspace.fs.stat(uri);
} catch {
// Does not exist
return uri;
private readonly _usedPaths = new Set<string>();

async getNewFilePath(document: vscode.TextDocument, file: vscode.DataTransferFile, token: vscode.CancellationToken): Promise<vscode.Uri | undefined> {
const desiredPath = getDesiredNewFilePath(document, file);

const root = Utils.dirname(desiredPath);
const ext = path.extname(file.name);
const baseName = path.basename(file.name, ext);
for (let i = 0; ; ++i) {
if (token.isCancellationRequested) {
return undefined;
}

const name = i === 0 ? baseName : `${baseName}-${i}`;
const uri = vscode.Uri.joinPath(root, name + ext);
if (this._wasPathAlreadyUsed(uri)) {
continue;
}

try {
await vscode.workspace.fs.stat(uri);
} catch {
if (!this._wasPathAlreadyUsed(uri)) {
// Does not exist
this._usedPaths.add(uri.toString());
return uri;
}
}
}
}

private _wasPathAlreadyUsed(uri: vscode.Uri) {
return this._usedPaths.has(uri.toString());
}
}

function getDesiredNewFilePath(document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri {
Expand Down
Expand Up @@ -4,13 +4,17 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { coalesce } from '../../util/arrays';
import { Schemes } from '../../util/schemes';
import { getNewFileName } from './copyFiles';
import { NewFilePathGenerator } from './copyFiles';
import { createUriListSnippet, tryGetUriListSnippet } from './dropIntoEditor';

const supportedImageMimes = new Set([
'image/bmp',
'image/gif',
'image/jpeg',
'image/png',
'image/jpg',
'image/webp',
]);

class PasteEditProvider implements vscode.DocumentPasteEditProvider {
Expand All @@ -26,53 +30,63 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
return;
}

for (const imageMime of supportedImageMimes) {
const item = dataTransfer.get(imageMime);
const file = item?.asFile();
if (item && file) {
const edit = await this._makeCreateImagePasteEdit(document, file, token);
if (token.isCancellationRequested) {
return;
}

if (edit) {
return edit;
}
}
const edit = await this._makeCreateImagePasteEdit(document, dataTransfer, token);
if (edit) {
return edit;
}

const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet, snippet.label) : undefined;
}

private async _makeCreateImagePasteEdit(document: vscode.TextDocument, file: vscode.DataTransferFile, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
private async _makeCreateImagePasteEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
if (document.uri.scheme === Schemes.untitled) {
return undefined;
return;
}

if (file.uri) {
// If file is already in workspace, we don't want to create a copy of it
const workspaceFolder = vscode.workspace.getWorkspaceFolder(file.uri);
if (workspaceFolder) {
const snippet = createUriListSnippet(document, [file.uri]);
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet, snippet.label) : undefined;
}
interface FileEntry {
readonly uri: vscode.Uri;
readonly newFileContents?: vscode.DataTransferFile;
}

const uri = await getNewFileName(document, file);
if (token.isCancellationRequested) {
const pathGenerator = new NewFilePathGenerator();
const fileEntries = coalesce(await Promise.all(Array.from(dataTransfer, async ([mime, item]): Promise<FileEntry | undefined> => {
if (!supportedImageMimes.has(mime)) {
return;
}

const file = item?.asFile();
if (!file) {
return;
}

if (file.uri) {
// If the file is already in a workspace, we don't want to create a copy of it
const workspaceFolder = vscode.workspace.getWorkspaceFolder(file.uri);
if (workspaceFolder) {
return { uri: file.uri };
}
}

const uri = await pathGenerator.getNewFilePath(document, file, token);
return uri ? { uri, newFileContents: file } : undefined;
})));
if (!fileEntries.length) {
return;
}

const snippet = createUriListSnippet(document, [uri]);
const workspaceEdit = new vscode.WorkspaceEdit();
for (const entry of fileEntries) {
if (entry.newFileContents) {
workspaceEdit.createFile(entry.uri, { contents: entry.newFileContents });
}
}

const snippet = createUriListSnippet(document, fileEntries.map(entry => entry.uri));
if (!snippet) {
return;
}

// Note that there is currently no way to undo the file creation :/
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.createFile(uri, { contents: file });

const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, snippet.label);
pasteEdit.additionalEdit = workspaceEdit;
return pasteEdit;
Expand Down