mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-22 01:29:04 +01:00
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:
@@ -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 ? '`);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user