Path: blob/main/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as vscode from 'vscode';6import { IMdParser } from '../../markdownEngine';7import { coalesce } from '../../util/arrays';8import { getParentDocumentUri } from '../../util/document';9import { getMediaKindForMime, MediaKind, Mime, rootMediaMimesTypes } from '../../util/mimes';10import { Schemes } from '../../util/schemes';11import { UriList } from '../../util/uriList';12import { NewFilePathGenerator } from './newFilePathGenerator';13import { audioEditKind, baseLinkEditKind, createInsertUriListEdit, createUriListSnippet, DropOrPasteEdit, getSnippetLabelAndKind, imageEditKind, linkEditKind, videoEditKind } from './shared';14import { InsertMarkdownLink, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste';1516enum CopyFilesSettings {17Never = 'never',18MediaFiles = 'mediaFiles',19}2021/**22* Provides support for pasting or dropping resources into markdown documents.23*24* This includes:25*26* - `text/uri-list` data in the data transfer.27* - File object in the data transfer.28* - Media data in the data transfer, such as `image/png`.29*/30class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider {3132public static readonly mimeTypes = [33Mime.textUriList,34'files',35...Object.values(rootMediaMimesTypes).map(type => `${type}/*`),36];3738private readonly _yieldTo = [39vscode.DocumentDropOrPasteEditKind.Text,40vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link', 'image', 'attachment'), // Prefer notebook attachments41];4243constructor(44private readonly _parser: IMdParser,45) { }4647public async provideDocumentDropEdits(48document: vscode.TextDocument,49position: vscode.Position,50dataTransfer: vscode.DataTransfer,51token: vscode.CancellationToken,52): Promise<vscode.DocumentDropEdit | undefined> {53const edit = await this._createEdit(document, [new vscode.Range(position, position)], dataTransfer, {54insert: this._getEnabled(document, 'editor.drop.enabled'),55copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get<CopyFilesSettings>('editor.drop.copyIntoWorkspace', CopyFilesSettings.MediaFiles)56}, undefined, token);5758if (!edit || token.isCancellationRequested) {59return;60}6162const dropEdit = new vscode.DocumentDropEdit(edit.snippet);63dropEdit.title = edit.label;64dropEdit.kind = edit.kind;65dropEdit.additionalEdit = edit.additionalEdits;66dropEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];67return dropEdit;68}6970public async provideDocumentPasteEdits(71document: vscode.TextDocument,72ranges: readonly vscode.Range[],73dataTransfer: vscode.DataTransfer,74context: vscode.DocumentPasteEditContext,75token: vscode.CancellationToken,76): Promise<vscode.DocumentPasteEdit[] | undefined> {77const edit = await this._createEdit(document, ranges, dataTransfer, {78insert: this._getEnabled(document, 'editor.paste.enabled'),79copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get<CopyFilesSettings>('editor.paste.copyIntoWorkspace', CopyFilesSettings.MediaFiles)80}, context, token);8182if (!edit || token.isCancellationRequested) {83return;84}8586const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, edit.kind);87pasteEdit.additionalEdit = edit.additionalEdits;88pasteEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];89return [pasteEdit];90}9192private _getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink {93const setting = vscode.workspace.getConfiguration('markdown', document).get<boolean | InsertMarkdownLink>(settingName, true);94// Convert old boolean values to new enum setting95if (setting === false) {96return InsertMarkdownLink.Never;97} else if (setting === true) {98return InsertMarkdownLink.Smart;99} else {100return setting;101}102}103104private async _createEdit(105document: vscode.TextDocument,106ranges: readonly vscode.Range[],107dataTransfer: vscode.DataTransfer,108settings: Readonly<{109insert: InsertMarkdownLink;110copyIntoWorkspace: CopyFilesSettings;111}>,112context: vscode.DocumentPasteEditContext | undefined,113token: vscode.CancellationToken,114): Promise<DropOrPasteEdit | undefined> {115if (settings.insert === InsertMarkdownLink.Never) {116return;117}118119let edit = await this._createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token);120if (token.isCancellationRequested) {121return;122}123124if (!edit) {125edit = await this._createEditFromUriListData(document, ranges, dataTransfer, context, token);126}127128if (!edit || token.isCancellationRequested) {129return;130}131132if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, settings.insert, ranges, token))) {133edit.yieldTo.push(vscode.DocumentDropOrPasteEditKind.Empty.append('uri'));134}135136return edit;137}138139private async _createEditFromUriListData(140document: vscode.TextDocument,141ranges: readonly vscode.Range[],142dataTransfer: vscode.DataTransfer,143context: vscode.DocumentPasteEditContext | undefined,144token: vscode.CancellationToken,145): Promise<DropOrPasteEdit | undefined> {146const uriListData = await dataTransfer.get(Mime.textUriList)?.asString();147if (!uriListData || token.isCancellationRequested) {148return;149}150151const uriList = UriList.from(uriListData);152if (!uriList.entries.length) {153return;154}155156// In some browsers, copying from the address bar sets both text/uri-list and text/plain.157// Disable ourselves if there's also a text entry with the same http(s) uri as our list,158// unless we are explicitly requested.159if (160uriList.entries.length === 1161&& (uriList.entries[0].uri.scheme === Schemes.http || uriList.entries[0].uri.scheme === Schemes.https)162&& !context?.only?.contains(baseLinkEditKind)163) {164const text = await dataTransfer.get(Mime.textPlain)?.asString();165if (token.isCancellationRequested) {166return;167}168169if (text && textMatchesUriList(text, uriList)) {170return;171}172}173174const edit = createInsertUriListEdit(document, ranges, uriList, { linkKindHint: context?.only });175if (!edit) {176return;177}178179const additionalEdits = new vscode.WorkspaceEdit();180additionalEdits.set(document.uri, edit.edits);181182return {183label: edit.label,184kind: edit.kind,185snippet: new vscode.SnippetString(''),186additionalEdits,187yieldTo: []188};189}190191/**192* Create a new edit for media files in a data transfer.193*194* This tries copying files outside of the workspace into the workspace.195*/196private async _createEditForMediaFiles(197document: vscode.TextDocument,198dataTransfer: vscode.DataTransfer,199copyIntoWorkspace: CopyFilesSettings,200token: vscode.CancellationToken,201): Promise<DropOrPasteEdit | undefined> {202if (copyIntoWorkspace !== CopyFilesSettings.MediaFiles || getParentDocumentUri(document.uri).scheme === Schemes.untitled) {203return;204}205206interface FileEntry {207readonly uri: vscode.Uri;208readonly kind: MediaKind;209readonly newFile?: { readonly contents: vscode.DataTransferFile; readonly overwrite: boolean };210}211212const pathGenerator = new NewFilePathGenerator();213const fileEntries = coalesce(await Promise.all(Array.from(dataTransfer, async ([mime, item]): Promise<FileEntry | undefined> => {214const mediaKind = getMediaKindForMime(mime);215if (!mediaKind) {216return;217}218219const file = item?.asFile();220if (!file) {221return;222}223224if (file.uri) {225// If the file is already in a workspace, we don't want to create a copy of it226const workspaceFolder = vscode.workspace.getWorkspaceFolder(file.uri);227if (workspaceFolder) {228return { uri: file.uri, kind: mediaKind };229}230}231232const newFile = await pathGenerator.getNewFilePath(document, file, token);233if (!newFile) {234return;235}236return { uri: newFile.uri, kind: mediaKind, newFile: { contents: file, overwrite: newFile.overwrite } };237})));238if (!fileEntries.length) {239return;240}241242const snippet = createUriListSnippet(document.uri, fileEntries);243if (!snippet) {244return;245}246247const additionalEdits = new vscode.WorkspaceEdit();248for (const entry of fileEntries) {249if (entry.newFile) {250additionalEdits.createFile(entry.uri, {251contents: entry.newFile.contents,252overwrite: entry.newFile.overwrite,253});254}255}256257const { label, kind } = getSnippetLabelAndKind(snippet);258return {259snippet: snippet.snippet,260label,261kind,262additionalEdits,263yieldTo: [],264};265}266}267268function textMatchesUriList(text: string, uriList: UriList): boolean {269if (text === uriList.entries[0].str) {270return true;271}272273try {274const uri = vscode.Uri.parse(text);275return uriList.entries.some(entry => entry.uri.toString() === uri.toString());276} catch {277return false;278}279}280281export function registerResourceDropOrPasteSupport(selector: vscode.DocumentSelector, parser: IMdParser): vscode.Disposable {282const providedEditKinds = [283baseLinkEditKind,284linkEditKind,285imageEditKind,286audioEditKind,287videoEditKind,288];289290return vscode.Disposable.from(291vscode.languages.registerDocumentPasteEditProvider(selector, new ResourcePasteOrDropProvider(parser), {292providedPasteEditKinds: providedEditKinds,293pasteMimeTypes: ResourcePasteOrDropProvider.mimeTypes,294}),295vscode.languages.registerDocumentDropEditProvider(selector, new ResourcePasteOrDropProvider(parser), {296providedDropEditKinds: providedEditKinds,297dropMimeTypes: ResourcePasteOrDropProvider.mimeTypes,298}),299);300}301302303