mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 13:29:44 +00: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:
parent
97f8af3e08
commit
7a7d45793b
|
@ -456,13 +456,36 @@
|
||||||
"markdownDescription": "%configuration.markdown.editor.drop.enabled%",
|
"markdownDescription": "%configuration.markdown.editor.drop.enabled%",
|
||||||
"scope": "resource"
|
"scope": "resource"
|
||||||
},
|
},
|
||||||
"markdown.experimental.editor.pasteLinks.enabled": {
|
"markdown.editor.drop.copyIntoWorkspace": {
|
||||||
|
"type": "string",
|
||||||
|
"markdownDescription": "%configuration.markdown.editor.drop.copyIntoWorkspace%",
|
||||||
|
"default": "mediaFiles",
|
||||||
|
"enum": [
|
||||||
|
"mediaFiles",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"markdownEnumDescriptions": [
|
||||||
|
"%configuration.copyIntoWorkspace.mediaFiles%",
|
||||||
|
"%configuration.copyIntoWorkspace.never%"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"markdown.editor.filePaste.enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"scope": "resource",
|
"scope": "resource",
|
||||||
"markdownDescription": "%configuration.markdown.editor.pasteLinks.enabled%",
|
"markdownDescription": "%configuration.markdown.editor.filePaste.enabled%",
|
||||||
"default": true,
|
"default": true
|
||||||
"tags": [
|
},
|
||||||
"experimental"
|
"markdown.editor.filePaste.copyIntoWorkspace": {
|
||||||
|
"type": "string",
|
||||||
|
"markdownDescription": "%configuration.markdown.editor.filePaste.copyIntoWorkspace%",
|
||||||
|
"default": "mediaFiles",
|
||||||
|
"enum": [
|
||||||
|
"mediaFiles",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"markdownEnumDescriptions": [
|
||||||
|
"%configuration.copyIntoWorkspace.mediaFiles%",
|
||||||
|
"%configuration.copyIntoWorkspace.never%"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"markdown.validate.enabled": {
|
"markdown.validate.enabled": {
|
||||||
|
@ -588,13 +611,26 @@
|
||||||
"description": "%configuration.markdown.occurrencesHighlight.enabled%",
|
"description": "%configuration.markdown.occurrencesHighlight.enabled%",
|
||||||
"scope": "resource"
|
"scope": "resource"
|
||||||
},
|
},
|
||||||
"markdown.experimental.copyFiles.destination": {
|
"markdown.copyFiles.destination": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"markdownDescription": "%configuration.markdown.copyFiles.destination%",
|
"markdownDescription": "%configuration.markdown.copyFiles.destination%",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"markdown.copyFiles.overwriteBehavior": {
|
||||||
|
"type": "string",
|
||||||
|
"markdownDescription": "%configuration.markdown.copyFiles.overwriteBehavior%",
|
||||||
|
"default": "nameIncrementally",
|
||||||
|
"enum": [
|
||||||
|
"nameIncrementally",
|
||||||
|
"overwrite"
|
||||||
|
],
|
||||||
|
"markdownEnumDescriptions": [
|
||||||
|
"%configuration.markdown.copyFiles.overwriteBehavior.nameIncrementally%",
|
||||||
|
"%configuration.markdown.copyFiles.overwriteBehavior.overwrite%"
|
||||||
|
]
|
||||||
|
},
|
||||||
"markdown.preferredMdPathExtensionStyle": {
|
"markdown.preferredMdPathExtensionStyle": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "auto",
|
"default": "auto",
|
||||||
|
|
|
@ -32,17 +32,21 @@
|
||||||
"configuration.markdown.links.openLocation.currentGroup": "Open links in the active editor group.",
|
"configuration.markdown.links.openLocation.currentGroup": "Open links in the active editor group.",
|
||||||
"configuration.markdown.links.openLocation.beside": "Open links beside the active editor.",
|
"configuration.markdown.links.openLocation.beside": "Open links beside the active editor.",
|
||||||
"configuration.markdown.suggest.paths.enabled.description": "Enable path suggestions while writing links in Markdown files.",
|
"configuration.markdown.suggest.paths.enabled.description": "Enable path suggestions while writing links in Markdown files.",
|
||||||
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions": "Enable suggestions for headers in other Markdown files in the current workspace. Accepting one of these suggestions inserts the full path to header in that file, for example `[link text](/path/to/file.md#header)`.",
|
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions": "Enable suggestions for headers in other Markdown files in the current workspace. Accepting one of these suggestions inserts the full path to header in that file, for example: `[link text](/path/to/file.md#header)`.",
|
||||||
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.never": "Disable workspace header suggestions.",
|
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.never": "Disable workspace header suggestions.",
|
||||||
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example `[link text](##`.",
|
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example: `[link text](##`.",
|
||||||
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example `[link text](#` or `[link text](##`.",
|
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example: `[link text](#` or `[link text](##`.",
|
||||||
"configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.",
|
"configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.",
|
||||||
"configuration.markdown.editor.pasteLinks.enabled": "Enable pasting files into a Markdown editor inserts Markdown links. Requires enabling `#editor.pasteAs.enabled#`.",
|
"configuration.markdown.editor.drop.copyIntoWorkspace": "Controls if files outside of the workspace that are dropped into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied dropped files should be created",
|
||||||
|
"configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.",
|
||||||
|
"configuration.markdown.editor.filePaste.copyIntoWorkspace": "Controls if files outside of the workspace that are pasted into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied files should be created.",
|
||||||
|
"configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.",
|
||||||
|
"configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.",
|
||||||
"configuration.markdown.validate.enabled.description": "Enable all error reporting in Markdown files.",
|
"configuration.markdown.validate.enabled.description": "Enable all error reporting in Markdown files.",
|
||||||
"configuration.markdown.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, for example `[link][ref]`. Requires enabling `#markdown.validate.enabled#`.",
|
"configuration.markdown.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, for example: `[link][ref]`. Requires enabling `#markdown.validate.enabled#`.",
|
||||||
"configuration.markdown.validate.fragmentLinks.enabled.description": "Validate fragment links to headers in the current Markdown file, for example `[link](#header)`. Requires enabling `#markdown.validate.enabled#`.",
|
"configuration.markdown.validate.fragmentLinks.enabled.description": "Validate fragment links to headers in the current Markdown file, for example: `[link](#header)`. Requires enabling `#markdown.validate.enabled#`.",
|
||||||
"configuration.markdown.validate.fileLinks.enabled.description": "Validate links to other files in Markdown files, for example `[link](/path/to/file.md)`. This checks that the target files exists. Requires enabling `#markdown.validate.enabled#`.",
|
"configuration.markdown.validate.fileLinks.enabled.description": "Validate links to other files in Markdown files, for example `[link](/path/to/file.md)`. This checks that the target files exists. Requires enabling `#markdown.validate.enabled#`.",
|
||||||
"configuration.markdown.validate.fileLinks.markdownFragmentLinks.description": "Validate the fragment part of links to headers in other files in Markdown files, for example `[link](/path/to/file.md#header)`. Inherits the setting value from `#markdown.validate.fragmentLinks.enabled#` by default.",
|
"configuration.markdown.validate.fileLinks.markdownFragmentLinks.description": "Validate the fragment part of links to headers in other files in Markdown files, for example: `[link](/path/to/file.md#header)`. Inherits the setting value from `#markdown.validate.fragmentLinks.enabled#` by default.",
|
||||||
"configuration.markdown.validate.ignoredLinks.description": "Configure links that should not be validated. For example adding `/about` would not validate the link `[about](/about)`, while the glob `/assets/**/*.svg` would let you skip validation for any link to `.svg` files under the `assets` directory.",
|
"configuration.markdown.validate.ignoredLinks.description": "Configure links that should not be validated. For example adding `/about` would not validate the link `[about](/about)`, while the glob `/assets/**/*.svg` would let you skip validation for any link to `.svg` files under the `assets` directory.",
|
||||||
"configuration.markdown.validate.unusedLinkDefinitions.description": "Validate link definitions that are unused in the current file.",
|
"configuration.markdown.validate.unusedLinkDefinitions.description": "Validate link definitions that are unused in the current file.",
|
||||||
"configuration.markdown.validate.duplicateLinkDefinitions.description": "Validate duplicated definitions in the current file.",
|
"configuration.markdown.validate.duplicateLinkDefinitions.description": "Validate duplicated definitions in the current file.",
|
||||||
|
@ -54,7 +58,10 @@
|
||||||
"configuration.markdown.updateLinksOnFileMove.include.property": "The glob pattern to match file paths against. Set to true to enable the pattern.",
|
"configuration.markdown.updateLinksOnFileMove.include.property": "The glob pattern to match file paths against. Set to true to enable the pattern.",
|
||||||
"configuration.markdown.updateLinksOnFileMove.enableForDirectories": "Enable updating links when a directory is moved or renamed in the workspace.",
|
"configuration.markdown.updateLinksOnFileMove.enableForDirectories": "Enable updating links when a directory is moved or renamed in the workspace.",
|
||||||
"configuration.markdown.occurrencesHighlight.enabled": "Enable highlighting link occurrences in the current document.",
|
"configuration.markdown.occurrencesHighlight.enabled": "Enable highlighting link occurrences in the current document.",
|
||||||
"configuration.markdown.copyFiles.destination": "Defines where files copied into a Markdown document should be created. This is a map from globs that match on the Markdown document to destinations.\n\nThe destinations may use the following variables:\n\n- `${documentFileName}` — The full filename of the Markdown document, for example `readme.md`.\n- `${documentBaseName}` — The basename of Markdown document, for example `readme`.\n- `${documentExtName}` — The extension of the Markdown document, for example `md`.\n- `${documentDirName}` — The name of the Markdown document's parent directory.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, for examples, `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of in a workspace.\n- `${fileName}` — The file name of the dropped file, for example `image.png`.",
|
"configuration.markdown.copyFiles.destination": "Defines where files copied created by drop or paste should be created. This is a map from globs that match on the Markdown document to destinations.\n\nThe destinations may use the following variables:\n\n- `${documentFileName}` — The full filename of the Markdown document, for example: `readme.md`.\n- `${documentBaseName}` — The basename of Markdown document, for example: `readme`.\n- `${documentExtName}` — The extension of the Markdown document, for example: `md`.\n- `${documentDirName}` — The name of the Markdown document's parent directory.\n- `${documentWorkspaceFolder}` — The workspace folder for the Markdown document, for example: `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of in a workspace.\n- `${fileName}` — The file name of the dropped file, for example: `image.png`.",
|
||||||
|
"configuration.markdown.copyFiles.overwriteBehavior": "Controls if files created by drop or paste should overwrite existing files.",
|
||||||
|
"configuration.markdown.copyFiles.overwriteBehavior.nameIncrementally": "If a file with the same name already exists, append a number to the file name, for example: `image.png` becomes `image-1.png`.",
|
||||||
|
"configuration.markdown.copyFiles.overwriteBehavior.overwrite": "If a file with the same name already exists, overwrite it.",
|
||||||
"configuration.markdown.preferredMdPathExtensionStyle": "Controls if file extensions (e.g. `.md`) are added or not for links to Markdown files. This setting is used when file paths are added by tooling such as path completions or file renames.",
|
"configuration.markdown.preferredMdPathExtensionStyle": "Controls if file extensions (e.g. `.md`) are added or not for links to Markdown files. This setting is used when file paths are added by tooling such as path completions or file renames.",
|
||||||
"configuration.markdown.preferredMdPathExtensionStyle.auto": "For existing paths, try to maintain the file extension style. For new paths, add file extensions.",
|
"configuration.markdown.preferredMdPathExtensionStyle.auto": "For existing paths, try to maintain the file extension style. For new paths, add file extensions.",
|
||||||
"configuration.markdown.preferredMdPathExtensionStyle.includeExtension": "Prefer including the file extension. For example, path completions to a file named `file.md` will insert `file.md`.",
|
"configuration.markdown.preferredMdPathExtensionStyle.includeExtension": "Prefer including the file extension. For example, path completions to a file named `file.md` will insert `file.md`.",
|
||||||
|
|
|
@ -6,8 +6,9 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { Utils } from 'vscode-uri';
|
import { Utils } from 'vscode-uri';
|
||||||
import { Command } from '../commandManager';
|
import { Command } from '../commandManager';
|
||||||
import { createUriListSnippet, getParentDocumentUri, imageFileExtensions } from '../languageFeatures/copyFiles/dropIntoEditor';
|
import { createUriListSnippet, mediaFileExtensions } from '../languageFeatures/copyFiles/shared';
|
||||||
import { coalesce } from '../util/arrays';
|
import { coalesce } from '../util/arrays';
|
||||||
|
import { getParentDocumentUri } from '../util/document';
|
||||||
import { Schemes } from '../util/schemes';
|
import { Schemes } from '../util/schemes';
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ export class InsertImageFromWorkspace implements Command {
|
||||||
canSelectFolders: false,
|
canSelectFolders: false,
|
||||||
canSelectMany: true,
|
canSelectMany: true,
|
||||||
filters: {
|
filters: {
|
||||||
[vscode.l10n.t("Images")]: Array.from(imageFileExtensions)
|
[vscode.l10n.t("Media")]: Array.from(mediaFileExtensions.keys())
|
||||||
},
|
},
|
||||||
openLabel: vscode.l10n.t("Insert image"),
|
openLabel: vscode.l10n.t("Insert image"),
|
||||||
title: vscode.l10n.t("Insert image"),
|
title: vscode.l10n.t("Insert image"),
|
||||||
|
@ -75,14 +76,14 @@ async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode
|
||||||
await vscode.workspace.applyEdit(edit);
|
await vscode.workspace.applyEdit(edit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsImage: boolean) {
|
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean) {
|
||||||
const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => {
|
const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => {
|
||||||
const selectionText = activeEditor.document.getText(selection);
|
const selectionText = activeEditor.document.getText(selection);
|
||||||
const snippet = createUriListSnippet(activeEditor.document, selectedFiles, {
|
const snippet = createUriListSnippet(activeEditor.document, selectedFiles, {
|
||||||
insertAsImage: insertAsImage,
|
insertAsMedia,
|
||||||
placeholderText: selectionText,
|
placeholderText: selectionText,
|
||||||
placeholderStartIndex: (i + 1) * selectedFiles.length,
|
placeholderStartIndex: (i + 1) * selectedFiles.length,
|
||||||
separator: insertAsImage ? '\n' : ' ',
|
separator: insertAsMedia ? '\n' : ' ',
|
||||||
});
|
});
|
||||||
|
|
||||||
return snippet ? new vscode.SnippetTextEdit(selection, snippet.snippet) : undefined;
|
return snippet ? new vscode.SnippetTextEdit(selection, snippet.snippet) : undefined;
|
||||||
|
|
|
@ -6,14 +6,41 @@ import * as path from 'path';
|
||||||
import * as picomatch from 'picomatch';
|
import * as picomatch from 'picomatch';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { Utils } from 'vscode-uri';
|
import { Utils } from 'vscode-uri';
|
||||||
import { getParentDocumentUri } from './dropIntoEditor';
|
import { getParentDocumentUri } from '../../util/document';
|
||||||
|
|
||||||
|
type OverwriteBehavior = 'overwrite' | 'nameIncrementally';
|
||||||
|
|
||||||
|
interface CopyFileConfiguration {
|
||||||
|
readonly destination: Record<string, string>;
|
||||||
|
readonly overwriteBehavior: OverwriteBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCopyFileConfiguration(document: vscode.TextDocument): CopyFileConfiguration {
|
||||||
|
const config = vscode.workspace.getConfiguration('markdown', document);
|
||||||
|
return {
|
||||||
|
destination: config.get<Record<string, string>>('copyFiles.destination') ?? {},
|
||||||
|
overwriteBehavior: readOverwriteBehavior(config),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOverwriteBehavior(config: vscode.WorkspaceConfiguration): OverwriteBehavior {
|
||||||
|
switch (config.get('copyFiles.overwriteBehavior')) {
|
||||||
|
case 'overwrite': return 'overwrite';
|
||||||
|
default: return 'nameIncrementally';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class NewFilePathGenerator {
|
export class NewFilePathGenerator {
|
||||||
|
|
||||||
private readonly _usedPaths = new Set<string>();
|
private readonly _usedPaths = new Set<string>();
|
||||||
|
|
||||||
async getNewFilePath(document: vscode.TextDocument, file: vscode.DataTransferFile, token: vscode.CancellationToken): Promise<vscode.Uri | undefined> {
|
async getNewFilePath(
|
||||||
const desiredPath = getDesiredNewFilePath(document, file);
|
document: vscode.TextDocument,
|
||||||
|
file: vscode.DataTransferFile,
|
||||||
|
token: vscode.CancellationToken,
|
||||||
|
): Promise<{ readonly uri: vscode.Uri; readonly overwrite: boolean } | undefined> {
|
||||||
|
const config = getCopyFileConfiguration(document);
|
||||||
|
const desiredPath = getDesiredNewFilePath(config, document, file);
|
||||||
|
|
||||||
const root = Utils.dirname(desiredPath);
|
const root = Utils.dirname(desiredPath);
|
||||||
const ext = path.extname(file.name);
|
const ext = path.extname(file.name);
|
||||||
|
@ -29,13 +56,20 @@ export class NewFilePathGenerator {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try overwriting if it already exists
|
||||||
|
if (config.overwriteBehavior === 'overwrite') {
|
||||||
|
this._usedPaths.add(uri.toString());
|
||||||
|
return { uri, overwrite: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we need to check the fs to see if it exists
|
||||||
try {
|
try {
|
||||||
await vscode.workspace.fs.stat(uri);
|
await vscode.workspace.fs.stat(uri);
|
||||||
} catch {
|
} catch {
|
||||||
if (!this._wasPathAlreadyUsed(uri)) {
|
if (!this._wasPathAlreadyUsed(uri)) {
|
||||||
// Does not exist
|
// Does not exist
|
||||||
this._usedPaths.add(uri.toString());
|
this._usedPaths.add(uri.toString());
|
||||||
return uri;
|
return { uri, overwrite: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,10 +80,9 @@ export class NewFilePathGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDesiredNewFilePath(document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri {
|
function getDesiredNewFilePath(config: CopyFileConfiguration, document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri {
|
||||||
const docUri = getParentDocumentUri(document);
|
const docUri = getParentDocumentUri(document);
|
||||||
const config = vscode.workspace.getConfiguration('markdown').get<Record<string, string>>('experimental.copyFiles.destination') ?? {};
|
for (const [rawGlob, rawDest] of Object.entries(config.destination)) {
|
||||||
for (const [rawGlob, rawDest] of Object.entries(config)) {
|
|
||||||
for (const glob of parseGlob(rawGlob)) {
|
for (const glob of parseGlob(rawGlob)) {
|
||||||
if (picomatch.isMatch(docUri.path, glob)) {
|
if (picomatch.isMatch(docUri.path, glob)) {
|
||||||
return resolveCopyDestination(docUri, file.name, rawDest, uri => vscode.workspace.getWorkspaceFolder(uri)?.uri);
|
return resolveCopyDestination(docUri, file.name, rawDest, uri => vscode.workspace.getWorkspaceFolder(uri)?.uri);
|
||||||
|
|
|
@ -4,18 +4,8 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { coalesce } from '../../util/arrays';
|
|
||||||
import { Schemes } from '../../util/schemes';
|
import { Schemes } from '../../util/schemes';
|
||||||
import { NewFilePathGenerator } from './copyFiles';
|
import { createEditForMediaFiles, mediaMimes, tryGetUriListSnippet } from './shared';
|
||||||
import { createUriListSnippet, tryGetUriListSnippet } from './dropIntoEditor';
|
|
||||||
|
|
||||||
const supportedImageMimes = new Set([
|
|
||||||
'image/bmp',
|
|
||||||
'image/gif',
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/webp',
|
|
||||||
]);
|
|
||||||
|
|
||||||
class PasteEditProvider implements vscode.DocumentPasteEditProvider {
|
class PasteEditProvider implements vscode.DocumentPasteEditProvider {
|
||||||
|
|
||||||
|
@ -27,12 +17,12 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
|
||||||
dataTransfer: vscode.DataTransfer,
|
dataTransfer: vscode.DataTransfer,
|
||||||
token: vscode.CancellationToken,
|
token: vscode.CancellationToken,
|
||||||
): Promise<vscode.DocumentPasteEdit | undefined> {
|
): Promise<vscode.DocumentPasteEdit | undefined> {
|
||||||
const enabled = vscode.workspace.getConfiguration('markdown', document).get('experimental.editor.pasteLinks.enabled', true);
|
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.filePaste.enabled', true);
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createEdit = await this._makeCreateImagePasteEdit(document, dataTransfer, token);
|
const createEdit = await this._getMediaFilesEdit(document, dataTransfer, token);
|
||||||
if (createEdit) {
|
if (createEdit) {
|
||||||
return createEdit;
|
return createEdit;
|
||||||
}
|
}
|
||||||
|
@ -47,56 +37,23 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
|
||||||
return uriEdit;
|
return uriEdit;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _makeCreateImagePasteEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
|
private async _getMediaFilesEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
|
||||||
if (document.uri.scheme === Schemes.untitled) {
|
if (document.uri.scheme === Schemes.untitled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileEntry {
|
const copyFilesIntoWorkspace = vscode.workspace.getConfiguration('markdown', document).get<'mediaFiles' | 'never'>('editor.filePaste.copyIntoWorkspace', 'mediaFiles');
|
||||||
readonly uri: vscode.Uri;
|
if (copyFilesIntoWorkspace === 'never') {
|
||||||
readonly newFileContents?: vscode.DataTransferFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceEdit = new vscode.WorkspaceEdit();
|
const edit = await createEditForMediaFiles(document, dataTransfer, token);
|
||||||
for (const entry of fileEntries) {
|
if (!edit) {
|
||||||
if (entry.newFileContents) {
|
|
||||||
workspaceEdit.createFile(entry.uri, { contents: entry.newFileContents });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const snippet = createUriListSnippet(document, fileEntries.map(entry => entry.uri));
|
|
||||||
if (!snippet) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pasteEdit = new vscode.DocumentPasteEdit(snippet.snippet, this._id, snippet.label);
|
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, this._id, edit.label);
|
||||||
pasteEdit.additionalEdit = workspaceEdit;
|
pasteEdit.additionalEdit = edit.additionalEdits;
|
||||||
pasteEdit.priority = this._getPriority(dataTransfer);
|
pasteEdit.priority = this._getPriority(dataTransfer);
|
||||||
return pasteEdit;
|
return pasteEdit;
|
||||||
}
|
}
|
||||||
|
@ -114,7 +71,7 @@ export function registerPasteSupport(selector: vscode.DocumentSelector,) {
|
||||||
return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteEditProvider(), {
|
return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteEditProvider(), {
|
||||||
pasteMimeTypes: [
|
pasteMimeTypes: [
|
||||||
'text/uri-list',
|
'text/uri-list',
|
||||||
...supportedImageMimes,
|
...mediaMimes,
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,186 +3,71 @@
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* 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 vscode from 'vscode';
|
||||||
import * as URI from 'vscode-uri';
|
import { createEditForMediaFiles as createEditForMediaFiles, tryGetUriListSnippet } from './shared';
|
||||||
import { Schemes } from '../../util/schemes';
|
import { Schemes } from '../../util/schemes';
|
||||||
|
|
||||||
export const imageFileExtensions = new Set<string>([
|
|
||||||
'bmp',
|
|
||||||
'gif',
|
|
||||||
'ico',
|
|
||||||
'jpe',
|
|
||||||
'jpeg',
|
|
||||||
'jpg',
|
|
||||||
'png',
|
|
||||||
'psd',
|
|
||||||
'svg',
|
|
||||||
'tga',
|
|
||||||
'tif',
|
|
||||||
'tiff',
|
|
||||||
'webp',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const videoFileExtensions = new Set<string>([
|
class MarkdownImageDropProvider implements vscode.DocumentDropEditProvider {
|
||||||
'ogg',
|
private readonly _id = 'insertLink';
|
||||||
'mp4'
|
|
||||||
]);
|
async provideDocumentDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
|
||||||
|
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesEdit = await this._getMediaFilesEdit(document, dataTransfer, token);
|
||||||
|
if (filesEdit) {
|
||||||
|
return filesEdit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.isCancellationRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._getUriListEdit(document, dataTransfer, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getUriListEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
|
||||||
|
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
|
||||||
|
if (!snippet) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = new vscode.DocumentDropEdit(snippet.snippet);
|
||||||
|
edit.id = this._id;
|
||||||
|
edit.label = snippet.label;
|
||||||
|
return edit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getMediaFilesEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
|
||||||
|
if (document.uri.scheme === Schemes.untitled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyIntoWorkspace = vscode.workspace.getConfiguration('markdown', document).get<'mediaFiles' | 'never'>('editor.drop.copyIntoWorkspace', 'mediaFiles');
|
||||||
|
if (copyIntoWorkspace !== 'mediaFiles') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesEdit = await createEditForMediaFiles(document, dataTransfer, token);
|
||||||
|
if (!filesEdit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = new vscode.DocumentDropEdit(filesEdit.snippet);
|
||||||
|
edit.id = this._id;
|
||||||
|
edit.label = filesEdit.label;
|
||||||
|
edit.additionalEdit = filesEdit.additionalEdits;
|
||||||
|
return edit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
|
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
|
||||||
return vscode.languages.registerDocumentDropEditProvider(selector, new class implements vscode.DocumentDropEditProvider {
|
return vscode.languages.registerDocumentDropEditProvider(selector, new MarkdownImageDropProvider(), {
|
||||||
async provideDocumentDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
|
|
||||||
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);
|
|
||||||
if (!enabled) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
|
|
||||||
if (!snippet) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const edit = new vscode.DocumentDropEdit(snippet.snippet);
|
|
||||||
edit.id = 'insertLink';
|
|
||||||
edit.label = snippet.label;
|
|
||||||
return edit;
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
dropMimeTypes: [
|
dropMimeTypes: [
|
||||||
'text/uri-list'
|
'text/uri-list'
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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?
|
|
||||||
*
|
|
||||||
* If `undefined`, tries to infer this from the uri.
|
|
||||||
*/
|
|
||||||
readonly insertAsImage?: 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 undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 insertAsImage = typeof options?.insertAsImage === 'undefined' ? imageFileExtensions.has(ext) : !!options.insertAsImage;
|
|
||||||
const insertAsVideo = videoFileExtensions.has(ext);
|
|
||||||
|
|
||||||
if (insertAsVideo) {
|
|
||||||
insertedImageCount++;
|
|
||||||
snippet.appendText(`<video src="${mdPath}" controls title="`);
|
|
||||||
snippet.appendPlaceholder('Title');
|
|
||||||
snippet.appendText('"></video>');
|
|
||||||
} else {
|
|
||||||
if (insertAsImage) {
|
|
||||||
insertedImageCount++;
|
|
||||||
} else {
|
|
||||||
insertedLinkCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
snippet.appendText(insertAsImage ? '![' : '[');
|
|
||||||
|
|
||||||
const placeholderText = options?.placeholderText ?? (insertAsImage ? '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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDocumentDir(document: vscode.TextDocument): vscode.Uri | undefined {
|
|
||||||
const docUri = getParentDocumentUri(document);
|
|
||||||
if (docUri.scheme === Schemes.untitled) {
|
|
||||||
return vscode.workspace.workspaceFolders?.[0]?.uri;
|
|
||||||
}
|
|
||||||
return URI.Utils.dirname(docUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getParentDocumentUri(document: vscode.TextDocument): vscode.Uri {
|
|
||||||
if (document.uri.scheme === Schemes.notebookCell) {
|
|
||||||
for (const notebook of vscode.workspace.notebookDocuments) {
|
|
||||||
for (const cell of notebook.getCells()) {
|
|
||||||
if (cell.document === document) {
|
|
||||||
return notebook.uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return document.uri;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
30
extensions/markdown-language-features/src/util/document.ts
Normal file
30
extensions/markdown-language-features/src/util/document.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { Schemes } from './schemes';
|
||||||
|
import { Utils } from 'vscode-uri';
|
||||||
|
|
||||||
|
export function getDocumentDir(document: vscode.TextDocument): vscode.Uri | undefined {
|
||||||
|
const docUri = getParentDocumentUri(document);
|
||||||
|
if (docUri.scheme === Schemes.untitled) {
|
||||||
|
return vscode.workspace.workspaceFolders?.[0]?.uri;
|
||||||
|
}
|
||||||
|
return Utils.dirname(docUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getParentDocumentUri(document: vscode.TextDocument): vscode.Uri {
|
||||||
|
if (document.uri.scheme === Schemes.notebookCell) {
|
||||||
|
for (const notebook of vscode.workspace.notebookDocuments) {
|
||||||
|
for (const cell of notebook.getCells()) {
|
||||||
|
if (cell.document === document) {
|
||||||
|
return notebook.uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.uri;
|
||||||
|
}
|
Loading…
Reference in a new issue