diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index f78d2158ac8..09351856a62 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -571,13 +571,6 @@ "default": false, "description": "%configuration.markdown.occurrencesHighlight.enabled%", "scope": "resource" - }, - "markdown.experimental.copyFiles.destination": { - "type": "object", - "markdownDescription": "%configuration.markdown.copyFiles.destination%", - "additionalProperties": { - "type": "string" - } } } }, diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 42c030abb10..676d875d3b9 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -49,6 +49,5 @@ "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.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`.", "workspaceTrust": "Required for loading styles configured in the workspace." } diff --git a/extensions/markdown-language-features/src/commands/insertResource.ts b/extensions/markdown-language-features/src/commands/insertResource.ts index 7cdd861cb5b..66041df75d3 100644 --- a/extensions/markdown-language-features/src/commands/insertResource.ts +++ b/extensions/markdown-language-features/src/commands/insertResource.ts @@ -6,11 +6,12 @@ import * as vscode from 'vscode'; import { Utils } from 'vscode-uri'; import { Command } from '../commandManager'; -import { createUriListSnippet, getParentDocumentUri, imageFileExtensions } from '../languageFeatures/copyFiles/dropIntoEditor'; +import { createUriListSnippet, getParentDocumentUri, imageFileExtensions } from '../languageFeatures/dropIntoEditor'; import { coalesce } from '../util/arrays'; import { Schemes } from '../util/schemes'; + export class InsertLinkFromWorkspace implements Command { public readonly id = 'markdown.editor.insertLinkFromWorkspace'; diff --git a/extensions/markdown-language-features/src/extension.shared.ts b/extensions/markdown-language-features/src/extension.shared.ts index ce709783d0b..dbfafc2bdb5 100644 --- a/extensions/markdown-language-features/src/extension.shared.ts +++ b/extensions/markdown-language-features/src/extension.shared.ts @@ -7,9 +7,9 @@ import * as vscode from 'vscode'; import { MdLanguageClient } from './client/client'; import { CommandManager } from './commandManager'; import { registerMarkdownCommands } from './commands/index'; -import { registerPasteSupport } from './languageFeatures/copyFiles/copyPaste'; +import { registerPasteSupport } from './languageFeatures/copyPaste'; import { registerDiagnosticSupport } from './languageFeatures/diagnostics'; -import { registerDropIntoEditorSupport } from './languageFeatures/copyFiles/dropIntoEditor'; +import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor'; import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences'; import { registerUpdateLinksOnRename } from './languageFeatures/linkUpdater'; import { ILogger } from './logging'; diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts deleted file mode 100644 index fe435e0795b..00000000000 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts +++ /dev/null @@ -1,119 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 picomatch from 'picomatch'; -import * as vscode from 'vscode'; -import { Utils } from 'vscode-uri'; -import { getParentDocumentUri } from './dropIntoEditor'; - - -export async function getNewFileName(document: vscode.TextDocument, file: vscode.DataTransferFile): Promise { - const desiredPath = getDesiredNewFilePath(document, file); - - const root = Utils.dirname(desiredPath); - const ext = path.extname(file.name); - const baseName = path.basename(file.name, ext); - for (let i = 0; ; ++i) { - const name = i === 0 ? baseName : `${baseName}-${i}`; - const uri = vscode.Uri.joinPath(root, `${name}${ext}`); - try { - await vscode.workspace.fs.stat(uri); - } catch { - // Does not exist - return uri; - } - } -} - -function getDesiredNewFilePath(document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri { - const docUri = getParentDocumentUri(document); - const config = vscode.workspace.getConfiguration('markdown').get>('experimental.copyFiles.destination') ?? {}; - for (const [rawGlob, rawDest] of Object.entries(config)) { - for (const glob of parseGlob(rawGlob)) { - if (picomatch.isMatch(docUri.path, glob)) { - return resolveCopyDestination(docUri, file.name, rawDest, uri => vscode.workspace.getWorkspaceFolder(uri)?.uri); - } - } - } - - // Default to next to current file - return vscode.Uri.joinPath(Utils.dirname(docUri), file.name); -} - -function parseGlob(rawGlob: string): Iterable { - if (rawGlob.startsWith('/')) { - // Anchor to workspace folders - return (vscode.workspace.workspaceFolders ?? []).map(folder => vscode.Uri.joinPath(folder.uri, rawGlob).path); - } - - // Relative path, so implicitly track on ** to match everything - if (!rawGlob.startsWith('**')) { - return ['**/' + rawGlob]; - } - - return [rawGlob]; -} - -type GetWorkspaceFolder = (documentUri: vscode.Uri) => vscode.Uri | undefined; - -export function resolveCopyDestination(documentUri: vscode.Uri, fileName: string, dest: string, getWorkspaceFolder: GetWorkspaceFolder): vscode.Uri { - const resolvedDest = resolveCopyDestinationSetting(documentUri, fileName, dest, getWorkspaceFolder); - - if (resolvedDest.startsWith('/')) { - // Absolute path - return Utils.resolvePath(documentUri, resolvedDest); - } - - // Relative to document - const dirName = Utils.dirname(documentUri); - return Utils.resolvePath(dirName, resolvedDest); -} - - -function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string, dest: string, getWorkspaceFolder: GetWorkspaceFolder): string { - let outDest = dest; - - // Destination that start with `/` implicitly means go to workspace root - if (outDest.startsWith('/')) { - outDest = '${documentWorkspaceFolder}/' + outDest.slice(1); - } - - // Destination that ends with `/` implicitly needs a fileName - if (outDest.endsWith('/')) { - outDest += '${fileName}'; - } - - const documentDirName = Utils.dirname(documentUri); - const documentBaseName = Utils.basename(documentUri); - const documentExtName = Utils.extname(documentUri); - - const workspaceFolder = getWorkspaceFolder(documentUri); - - const vars = new Map([ - ['documentDirName', documentDirName.fsPath], // Parent directory path - ['documentFileName', documentBaseName], // Full filename: file.md - ['documentBaseName', documentBaseName.slice(0, documentBaseName.length - documentExtName.length)], // Just the name: file - ['documentExtName', documentExtName.replace('.', '')], // Just the file ext: md - - // Workspace - ['documentWorkspaceFolder', (workspaceFolder ?? documentDirName).fsPath], - - // File - ['fileName', fileName],// Full file name - ]); - - return outDest.replaceAll(/\$\{(\w+)(?:\/([^\}]+?)\/([^\}]+?)\/)?\}/g, (_, name, pattern, replacement) => { - const entry = vars.get(name); - if (!entry) { - return ''; - } - - if (pattern && replacement) { - return entry.replace(new RegExp(pattern), replacement); - } - - return entry; - }); -} diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts b/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts similarity index 80% rename from extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts rename to extensions/markdown-language-features/src/languageFeatures/copyPaste.ts index 1babe16107d..0a39f9fabb4 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts @@ -3,9 +3,10 @@ * 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 { Schemes } from '../../util/schemes'; -import { getNewFileName } from './copyFiles'; +import { Utils } from 'vscode-uri'; +import { Schemes } from '../util/schemes'; import { createUriListSnippet, tryGetUriListSnippet } from './dropIntoEditor'; const supportedImageMimes = new Set([ @@ -58,7 +59,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider { } } - const uri = await getNewFileName(document, file); + const uri = await this._getNewFileName(document, file); if (token.isCancellationRequested) { return; } @@ -76,6 +77,23 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider { pasteEdit.additionalEdit = workspaceEdit; return pasteEdit; } + + private async _getNewFileName(document: vscode.TextDocument, file: vscode.DataTransferFile): Promise { + const root = Utils.dirname(document.uri); + + const ext = path.extname(file.name); + const baseName = path.basename(file.name, ext); + for (let i = 0; ; ++i) { + const name = i === 0 ? baseName : `${baseName}-${i}`; + const uri = vscode.Uri.joinPath(root, `${name}${ext}`); + try { + await vscode.workspace.fs.stat(uri); + } catch { + // Does not exist + return uri; + } + } + } } export function registerPasteSupport(selector: vscode.DocumentSelector,) { diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts similarity index 98% rename from extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts rename to extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts index e91d9d11008..4f8c6593ee2 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts +++ b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as URI from 'vscode-uri'; -import { Schemes } from '../../util/schemes'; +import { Schemes } from '../util/schemes'; export const imageFileExtensions = new Set([ 'bmp', diff --git a/extensions/markdown-language-features/src/test/copyFile.test.ts b/extensions/markdown-language-features/src/test/copyFile.test.ts deleted file mode 100644 index e557d29a04e..00000000000 --- a/extensions/markdown-language-features/src/test/copyFile.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import 'mocha'; -import * as vscode from 'vscode'; -import { resolveCopyDestination } from '../languageFeatures/copyFiles/copyFiles'; - - -suite.only('resolveCopyDestination', () => { - - test('Relative destinations should resolve next to document', async () => { - const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md'); - - { - const dest = resolveCopyDestination(documentUri, 'img.png', '${fileName}', () => vscode.Uri.parse('test://projects/project/')); - assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.png'); - } - { - const dest = resolveCopyDestination(documentUri, 'img.png', './${fileName}', () => vscode.Uri.parse('test://projects/project/')); - assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.png'); - } - { - const dest = resolveCopyDestination(documentUri, 'img.png', '../${fileName}', () => vscode.Uri.parse('test://projects/project/')); - assert.strictEqual(dest.toString(), 'test://projects/project/img.png'); - } - }); - - test('Destination starting with / should go to workspace root', async () => { - const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md'); - const dest = resolveCopyDestination(documentUri, 'img.png', '/${fileName}', () => vscode.Uri.parse('test://projects/project/')); - - assert.strictEqual(dest.toString(), 'test://projects/project/img.png'); - }); - - test('If there is no workspace root, / should resolve to document dir', async () => { - const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md'); - const dest = resolveCopyDestination(documentUri, 'img.png', '/${fileName}', () => undefined); - - assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.png'); - }); - - test('If path ends in /, we should automatically add the fileName', async () => { - { - const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md'); - const dest = resolveCopyDestination(documentUri, 'img.png', 'images/', () => vscode.Uri.parse('test://projects/project/')); - assert.strictEqual(dest.toString(), 'test://projects/project/sub/images/img.png'); - } - { - const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md'); - const dest = resolveCopyDestination(documentUri, 'img.png', './', () => vscode.Uri.parse('test://projects/project/')); - assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.png'); - } - { - const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md'); - const dest = resolveCopyDestination(documentUri, 'img.png', '/', () => vscode.Uri.parse('test://projects/project/')); - - assert.strictEqual(dest.toString(), 'test://projects/project/img.png'); - } - }); - - test('Basic transform', async () => { - const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md'); - const dest = resolveCopyDestination(documentUri, 'img.png', '${fileName/.png/.gif/}', () => undefined); - - assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.gif'); - }); - - test('transforms should support capture groups', async () => { - const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md'); - const dest = resolveCopyDestination(documentUri, 'img.png', '${fileName/(.+)\\.(.+)/$2.$1/}', () => undefined); - - assert.strictEqual(dest.toString(), 'test://projects/project/sub/png.img'); - }); -});