Allow external copying files into the workspace on markdown drop / paste (#182572)

Allow copying files in the workspace on markdown drop / paste

Fixes #157043

Also:

- Renames the markdown paste settings and makes them no longer experimental
- Makes the copyFiles setting no longer experimental
- Adds a `markdown.copyFiles.overwriteBehavior` which lets you control if/how existing files are overwritten
This commit is contained in:
Matt Bierner
2023-05-15 20:17:52 -07:00
committed by GitHub
parent 97f8af3e08
commit 7a7d45793b
8 changed files with 435 additions and 252 deletions

View File

@@ -0,0 +1,234 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as URI from 'vscode-uri';
import { Schemes } from '../../util/schemes';
import { NewFilePathGenerator } from './copyFiles';
import { coalesce } from '../../util/arrays';
import { getDocumentDir } from '../../util/document';
enum MediaKind {
Image,
Video,
}
export const mediaFileExtensions = new Map<string, MediaKind>([
// Images
['bmp', MediaKind.Image],
['gif', MediaKind.Image],
['ico', MediaKind.Image],
['jpe', MediaKind.Image],
['jpeg', MediaKind.Image],
['jpg', MediaKind.Image],
['png', MediaKind.Image],
['psd', MediaKind.Image],
['svg', MediaKind.Image],
['tga', MediaKind.Image],
['tif', MediaKind.Image],
['tiff', MediaKind.Image],
['webp', MediaKind.Image],
// Videos
['ogg', MediaKind.Video],
['mp4', MediaKind.Video],
]);
export const mediaMimes = new Set([
'image/bmp',
'image/gif',
'image/jpeg',
'image/png',
'image/webp',
'video/mp4',
'video/ogg',
]);
export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> {
const urlList = await dataTransfer.get('text/uri-list')?.asString();
if (!urlList || token.isCancellationRequested) {
return undefined;
}
const uris: vscode.Uri[] = [];
for (const resource of urlList.split(/\r?\n/g)) {
try {
uris.push(vscode.Uri.parse(resource));
} catch {
// noop
}
}
return createUriListSnippet(document, uris);
}
interface UriListSnippetOptions {
readonly placeholderText?: string;
readonly placeholderStartIndex?: number;
/**
* Should the snippet be for an image link or video?
*
* If `undefined`, tries to infer this from the uri.
*/
readonly insertAsMedia?: boolean;
readonly separator?: string;
}
export function createUriListSnippet(
document: vscode.TextDocument,
uris: readonly vscode.Uri[],
options?: UriListSnippetOptions
): { snippet: vscode.SnippetString; label: string } | undefined {
if (!uris.length) {
return;
}
const dir = getDocumentDir(document);
const snippet = new vscode.SnippetString();
let insertedLinkCount = 0;
let insertedImageCount = 0;
uris.forEach((uri, i) => {
const mdPath = getMdPath(dir, uri);
const ext = URI.Utils.extname(uri).toLowerCase().replace('.', '');
const insertAsMedia = typeof options?.insertAsMedia === 'undefined' ? mediaFileExtensions.has(ext) : !!options.insertAsMedia;
const insertAsVideo = mediaFileExtensions.get(ext) === MediaKind.Video;
if (insertAsVideo) {
insertedImageCount++;
snippet.appendText(`<video src="${mdPath}" controls title="`);
snippet.appendPlaceholder('Title');
snippet.appendText('"></video>');
} else {
if (insertAsMedia) {
insertedImageCount++;
} else {
insertedLinkCount++;
}
snippet.appendText(insertAsMedia ? '![' : '[');
const placeholderText = options?.placeholderText ?? (insertAsMedia ? 'Alt text' : 'label');
const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : undefined;
snippet.appendPlaceholder(placeholderText, placeholderIndex);
snippet.appendText(`](${mdPath})`);
}
if (i < uris.length - 1 && uris.length > 1) {
snippet.appendText(options?.separator ?? ' ');
}
});
let label: string;
if (insertedImageCount > 0 && insertedLinkCount > 0) {
label = vscode.l10n.t('Insert Markdown Images and Links');
} else if (insertedImageCount > 0) {
label = insertedImageCount > 1
? vscode.l10n.t('Insert Markdown Images')
: vscode.l10n.t('Insert Markdown Image');
} else {
label = insertedLinkCount > 1
? vscode.l10n.t('Insert Markdown Links')
: vscode.l10n.t('Insert Markdown Link');
}
return { snippet, label };
}
/**
* Create a new edit from the image files in a data transfer.
*
* This tries copying files outside of the workspace into the workspace.
*/
export async function createEditForMediaFiles(
document: vscode.TextDocument,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken
): Promise<{ snippet: vscode.SnippetString; label: string; additionalEdits: vscode.WorkspaceEdit } | undefined> {
if (document.uri.scheme === Schemes.untitled) {
return;
}
interface FileEntry {
readonly uri: vscode.Uri;
readonly newFile?: { readonly contents: vscode.DataTransferFile; readonly overwrite: boolean };
}
const pathGenerator = new NewFilePathGenerator();
const fileEntries = coalesce(await Promise.all(Array.from(dataTransfer, async ([mime, item]): Promise<FileEntry | undefined> => {
if (!mediaMimes.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 newFile = await pathGenerator.getNewFilePath(document, file, token);
if (!newFile) {
return;
}
return { uri: newFile.uri, newFile: { contents: file, overwrite: newFile.overwrite } };
})));
if (!fileEntries.length) {
return;
}
const workspaceEdit = new vscode.WorkspaceEdit();
for (const entry of fileEntries) {
if (entry.newFile) {
workspaceEdit.createFile(entry.uri, {
contents: entry.newFile.contents,
overwrite: entry.newFile.overwrite,
});
}
}
const snippet = createUriListSnippet(document, fileEntries.map(entry => entry.uri));
if (!snippet) {
return;
}
return {
snippet: snippet.snippet,
label: snippet.label,
additionalEdits: workspaceEdit,
};
}
function getMdPath(dir: vscode.Uri | undefined, file: vscode.Uri) {
if (dir && dir.scheme === file.scheme && dir.authority === file.authority) {
if (file.scheme === Schemes.file) {
// On windows, we must use the native `path.relative` to generate the relative path
// so that drive-letters are resolved cast insensitively. However we then want to
// convert back to a posix path to insert in to the document.
const relativePath = path.relative(dir.fsPath, file.fsPath);
return encodeURI(path.posix.normalize(relativePath.split(path.sep).join(path.posix.sep)));
}
return encodeURI(path.posix.relative(dir.path, file.path));
}
return file.toString(false);
}