Skip to content

Commit 7a7d457

Browse files
authored
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
1 parent 97f8af3 commit 7a7d457

File tree

8 files changed

+421
-238
lines changed

8 files changed

+421
-238
lines changed

extensions/markdown-language-features/package.json

+42-6
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,36 @@
456456
"markdownDescription": "%configuration.markdown.editor.drop.enabled%",
457457
"scope": "resource"
458458
},
459-
"markdown.experimental.editor.pasteLinks.enabled": {
459+
"markdown.editor.drop.copyIntoWorkspace": {
460+
"type": "string",
461+
"markdownDescription": "%configuration.markdown.editor.drop.copyIntoWorkspace%",
462+
"default": "mediaFiles",
463+
"enum": [
464+
"mediaFiles",
465+
"never"
466+
],
467+
"markdownEnumDescriptions": [
468+
"%configuration.copyIntoWorkspace.mediaFiles%",
469+
"%configuration.copyIntoWorkspace.never%"
470+
]
471+
},
472+
"markdown.editor.filePaste.enabled": {
460473
"type": "boolean",
461474
"scope": "resource",
462-
"markdownDescription": "%configuration.markdown.editor.pasteLinks.enabled%",
463-
"default": true,
464-
"tags": [
465-
"experimental"
475+
"markdownDescription": "%configuration.markdown.editor.filePaste.enabled%",
476+
"default": true
477+
},
478+
"markdown.editor.filePaste.copyIntoWorkspace": {
479+
"type": "string",
480+
"markdownDescription": "%configuration.markdown.editor.filePaste.copyIntoWorkspace%",
481+
"default": "mediaFiles",
482+
"enum": [
483+
"mediaFiles",
484+
"never"
485+
],
486+
"markdownEnumDescriptions": [
487+
"%configuration.copyIntoWorkspace.mediaFiles%",
488+
"%configuration.copyIntoWorkspace.never%"
466489
]
467490
},
468491
"markdown.validate.enabled": {
@@ -588,13 +611,26 @@
588611
"description": "%configuration.markdown.occurrencesHighlight.enabled%",
589612
"scope": "resource"
590613
},
591-
"markdown.experimental.copyFiles.destination": {
614+
"markdown.copyFiles.destination": {
592615
"type": "object",
593616
"markdownDescription": "%configuration.markdown.copyFiles.destination%",
594617
"additionalProperties": {
595618
"type": "string"
596619
}
597620
},
621+
"markdown.copyFiles.overwriteBehavior": {
622+
"type": "string",
623+
"markdownDescription": "%configuration.markdown.copyFiles.overwriteBehavior%",
624+
"default": "nameIncrementally",
625+
"enum": [
626+
"nameIncrementally",
627+
"overwrite"
628+
],
629+
"markdownEnumDescriptions": [
630+
"%configuration.markdown.copyFiles.overwriteBehavior.nameIncrementally%",
631+
"%configuration.markdown.copyFiles.overwriteBehavior.overwrite%"
632+
]
633+
},
598634
"markdown.preferredMdPathExtensionStyle": {
599635
"type": "string",
600636
"default": "auto",

extensions/markdown-language-features/package.nls.json

+15-8
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@
3232
"configuration.markdown.links.openLocation.currentGroup": "Open links in the active editor group.",
3333
"configuration.markdown.links.openLocation.beside": "Open links beside the active editor.",
3434
"configuration.markdown.suggest.paths.enabled.description": "Enable path suggestions while writing links in Markdown files.",
35-
"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)`.",
35+
"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)`.",
3636
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.never": "Disable workspace header suggestions.",
37-
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example `[link text](##`.",
38-
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example `[link text](#` or `[link text](##`.",
37+
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example: `[link text](##`.",
38+
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example: `[link text](#` or `[link text](##`.",
3939
"configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.",
40-
"configuration.markdown.editor.pasteLinks.enabled": "Enable pasting files into a Markdown editor inserts Markdown links. Requires enabling `#editor.pasteAs.enabled#`.",
40+
"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",
41+
"configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.",
42+
"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.",
43+
"configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.",
44+
"configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.",
4145
"configuration.markdown.validate.enabled.description": "Enable all error reporting in Markdown files.",
42-
"configuration.markdown.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, for example `[link][ref]`. Requires enabling `#markdown.validate.enabled#`.",
43-
"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#`.",
46+
"configuration.markdown.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, for example: `[link][ref]`. Requires enabling `#markdown.validate.enabled#`.",
47+
"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#`.",
4448
"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#`.",
45-
"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.",
49+
"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.",
4650
"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.",
4751
"configuration.markdown.validate.unusedLinkDefinitions.description": "Validate link definitions that are unused in the current file.",
4852
"configuration.markdown.validate.duplicateLinkDefinitions.description": "Validate duplicated definitions in the current file.",
@@ -54,7 +58,10 @@
5458
"configuration.markdown.updateLinksOnFileMove.include.property": "The glob pattern to match file paths against. Set to true to enable the pattern.",
5559
"configuration.markdown.updateLinksOnFileMove.enableForDirectories": "Enable updating links when a directory is moved or renamed in the workspace.",
5660
"configuration.markdown.occurrencesHighlight.enabled": "Enable highlighting link occurrences in the current document.",
57-
"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`.",
61+
"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`.",
62+
"configuration.markdown.copyFiles.overwriteBehavior": "Controls if files created by drop or paste should overwrite existing files.",
63+
"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`.",
64+
"configuration.markdown.copyFiles.overwriteBehavior.overwrite": "If a file with the same name already exists, overwrite it.",
5865
"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.",
5966
"configuration.markdown.preferredMdPathExtensionStyle.auto": "For existing paths, try to maintain the file extension style. For new paths, add file extensions.",
6067
"configuration.markdown.preferredMdPathExtensionStyle.includeExtension": "Prefer including the file extension. For example, path completions to a file named `file.md` will insert `file.md`.",

extensions/markdown-language-features/src/commands/insertResource.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import * as vscode from 'vscode';
77
import { Utils } from 'vscode-uri';
88
import { Command } from '../commandManager';
9-
import { createUriListSnippet, getParentDocumentUri, imageFileExtensions } from '../languageFeatures/copyFiles/dropIntoEditor';
9+
import { createUriListSnippet, mediaFileExtensions } from '../languageFeatures/copyFiles/shared';
1010
import { coalesce } from '../util/arrays';
11+
import { getParentDocumentUri } from '../util/document';
1112
import { Schemes } from '../util/schemes';
1213

1314

@@ -47,7 +48,7 @@ export class InsertImageFromWorkspace implements Command {
4748
canSelectFolders: false,
4849
canSelectMany: true,
4950
filters: {
50-
[vscode.l10n.t("Images")]: Array.from(imageFileExtensions)
51+
[vscode.l10n.t("Media")]: Array.from(mediaFileExtensions.keys())
5152
},
5253
openLabel: vscode.l10n.t("Insert image"),
5354
title: vscode.l10n.t("Insert image"),
@@ -75,14 +76,14 @@ async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode
7576
await vscode.workspace.applyEdit(edit);
7677
}
7778

78-
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsImage: boolean) {
79+
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean) {
7980
const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => {
8081
const selectionText = activeEditor.document.getText(selection);
8182
const snippet = createUriListSnippet(activeEditor.document, selectedFiles, {
82-
insertAsImage: insertAsImage,
83+
insertAsMedia,
8384
placeholderText: selectionText,
8485
placeholderStartIndex: (i + 1) * selectedFiles.length,
85-
separator: insertAsImage ? '\n' : ' ',
86+
separator: insertAsMedia ? '\n' : ' ',
8687
});
8788

8889
return snippet ? new vscode.SnippetTextEdit(selection, snippet.snippet) : undefined;

extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts

+40-7
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,41 @@ import * as path from 'path';
66
import * as picomatch from 'picomatch';
77
import * as vscode from 'vscode';
88
import { Utils } from 'vscode-uri';
9-
import { getParentDocumentUri } from './dropIntoEditor';
9+
import { getParentDocumentUri } from '../../util/document';
10+
11+
type OverwriteBehavior = 'overwrite' | 'nameIncrementally';
12+
13+
interface CopyFileConfiguration {
14+
readonly destination: Record<string, string>;
15+
readonly overwriteBehavior: OverwriteBehavior;
16+
}
17+
18+
function getCopyFileConfiguration(document: vscode.TextDocument): CopyFileConfiguration {
19+
const config = vscode.workspace.getConfiguration('markdown', document);
20+
return {
21+
destination: config.get<Record<string, string>>('copyFiles.destination') ?? {},
22+
overwriteBehavior: readOverwriteBehavior(config),
23+
};
24+
}
25+
26+
function readOverwriteBehavior(config: vscode.WorkspaceConfiguration): OverwriteBehavior {
27+
switch (config.get('copyFiles.overwriteBehavior')) {
28+
case 'overwrite': return 'overwrite';
29+
default: return 'nameIncrementally';
30+
}
31+
}
1032

1133
export class NewFilePathGenerator {
1234

1335
private readonly _usedPaths = new Set<string>();
1436

15-
async getNewFilePath(document: vscode.TextDocument, file: vscode.DataTransferFile, token: vscode.CancellationToken): Promise<vscode.Uri | undefined> {
16-
const desiredPath = getDesiredNewFilePath(document, file);
37+
async getNewFilePath(
38+
document: vscode.TextDocument,
39+
file: vscode.DataTransferFile,
40+
token: vscode.CancellationToken,
41+
): Promise<{ readonly uri: vscode.Uri; readonly overwrite: boolean } | undefined> {
42+
const config = getCopyFileConfiguration(document);
43+
const desiredPath = getDesiredNewFilePath(config, document, file);
1744

1845
const root = Utils.dirname(desiredPath);
1946
const ext = path.extname(file.name);
@@ -29,13 +56,20 @@ export class NewFilePathGenerator {
2956
continue;
3057
}
3158

59+
// Try overwriting if it already exists
60+
if (config.overwriteBehavior === 'overwrite') {
61+
this._usedPaths.add(uri.toString());
62+
return { uri, overwrite: true };
63+
}
64+
65+
// Otherwise we need to check the fs to see if it exists
3266
try {
3367
await vscode.workspace.fs.stat(uri);
3468
} catch {
3569
if (!this._wasPathAlreadyUsed(uri)) {
3670
// Does not exist
3771
this._usedPaths.add(uri.toString());
38-
return uri;
72+
return { uri, overwrite: false };
3973
}
4074
}
4175
}
@@ -46,10 +80,9 @@ export class NewFilePathGenerator {
4680
}
4781
}
4882

49-
function getDesiredNewFilePath(document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri {
83+
function getDesiredNewFilePath(config: CopyFileConfiguration, document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri {
5084
const docUri = getParentDocumentUri(document);
51-
const config = vscode.workspace.getConfiguration('markdown').get<Record<string, string>>('experimental.copyFiles.destination') ?? {};
52-
for (const [rawGlob, rawDest] of Object.entries(config)) {
85+
for (const [rawGlob, rawDest] of Object.entries(config.destination)) {
5386
for (const glob of parseGlob(rawGlob)) {
5487
if (picomatch.isMatch(docUri.path, glob)) {
5588
return resolveCopyDestination(docUri, file.name, rawDest, uri => vscode.workspace.getWorkspaceFolder(uri)?.uri);

0 commit comments

Comments
 (0)