Path: blob/main/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.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*--------------------------------------------------------------------------------------------*/4import * as path from 'path';5import * as vscode from 'vscode';6import { Utils } from 'vscode-uri';78type OverwriteBehavior = 'overwrite' | 'nameIncrementally';910export interface CopyFileConfiguration {11readonly destination: Record<string, string>;12readonly overwriteBehavior: OverwriteBehavior;13}1415export function getCopyFileConfiguration(document: vscode.TextDocument): CopyFileConfiguration {16const config = vscode.workspace.getConfiguration('markdown', document);17return {18destination: config.get<Record<string, string>>('copyFiles.destination') ?? {},19overwriteBehavior: readOverwriteBehavior(config),20};21}2223function readOverwriteBehavior(config: vscode.WorkspaceConfiguration): OverwriteBehavior {24switch (config.get('copyFiles.overwriteBehavior')) {25case 'overwrite': return 'overwrite';26default: return 'nameIncrementally';27}28}2930export function parseGlob(rawGlob: string): Iterable<string> {31if (rawGlob.startsWith('/')) {32// Anchor to workspace folders33return (vscode.workspace.workspaceFolders ?? []).map(folder => vscode.Uri.joinPath(folder.uri, rawGlob).path);34}3536// Relative path, so implicitly track on ** to match everything37if (!rawGlob.startsWith('**')) {38return ['**/' + rawGlob];39}4041return [rawGlob];42}4344type GetWorkspaceFolder = (documentUri: vscode.Uri) => vscode.Uri | undefined;4546export function resolveCopyDestination(documentUri: vscode.Uri, fileName: string, dest: string, getWorkspaceFolder: GetWorkspaceFolder): vscode.Uri {47const resolvedDest = resolveCopyDestinationSetting(documentUri, fileName, dest, getWorkspaceFolder);4849if (resolvedDest.startsWith('/')) {50// Absolute path51return Utils.resolvePath(documentUri, resolvedDest);52}5354// Relative to document55const dirName = Utils.dirname(documentUri);56return Utils.resolvePath(dirName, resolvedDest);57}585960function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string, dest: string, getWorkspaceFolder: GetWorkspaceFolder): string {61let outDest = dest.trim();62if (!outDest) {63outDest = '${fileName}';64}6566// Destination that start with `/` implicitly means go to workspace root67if (outDest.startsWith('/')) {68outDest = '${documentWorkspaceFolder}/' + outDest.slice(1);69}7071// Destination that ends with `/` implicitly needs a fileName72if (outDest.endsWith('/')) {73outDest += '${fileName}';74}7576const documentDirName = Utils.dirname(documentUri);77const documentBaseName = Utils.basename(documentUri);78const documentExtName = Utils.extname(documentUri);7980const workspaceFolder = getWorkspaceFolder(documentUri);8182const vars = new Map<string, string>([83// Document84['documentDirName', documentDirName.path], // Absolute parent directory path of the Markdown document, e.g. `/Users/me/myProject/docs`.85['documentRelativeDirName', workspaceFolder ? path.posix.relative(workspaceFolder.path, documentDirName.path) : documentDirName.path], // Relative parent directory path of the Markdown document, e.g. `docs`. This is the same as `${documentDirName}` if the file is not part of a workspace.86['documentFileName', documentBaseName], // The full filename of the Markdown document, e.g. `README.md`.87['documentBaseName', documentBaseName.slice(0, documentBaseName.length - documentExtName.length)], // The basename of the Markdown document, e.g. `README`.88['documentExtName', documentExtName.replace('.', '')], // The extension of the Markdown document, e.g. `md`.89['documentFilePath', documentUri.path], // Absolute path of the Markdown document, e.g. `/Users/me/myProject/docs/README.md`.90['documentRelativeFilePath', workspaceFolder ? path.posix.relative(workspaceFolder.path, documentUri.path) : documentUri.path], // Relative path of the Markdown document, e.g. `docs/README.md`. This is the same as `${documentFilePath}` if the file is not part of a workspace.9192// Workspace93['documentWorkspaceFolder', ((workspaceFolder ?? documentDirName).path)], // The workspace folder for the Markdown document, e.g. `/Users/me/myProject`. This is the same as `${documentDirName}` if the file is not part of a workspace.9495// File96['fileName', fileName], // The file name of the dropped file, e.g. `image.png`.97['fileExtName', path.extname(fileName).replace('.', '')], // The extension of the dropped file, e.g. `png`.98['unixTime', Date.now().toString()], // The current Unix timestamp in milliseconds.99['isoTime', new Date().toISOString()], // The current time in ISO 8601 format, e.g. '2025-06-06T08:40:32.123Z'.100]);101102return outDest.replaceAll(/(?<escape>\\\$)|(?<!\\)\$\{(?<name>\w+)(?:\/(?<pattern>(?:\\\/|[^\}\/])+)\/(?<replacement>(?:\\\/|[^\}\/])*)\/)?\}/g, (match, _escape, name, pattern, replacement, _offset, _str, groups) => {103if (groups?.['escape']) {104return '$';105}106107const entry = vars.get(name);108if (typeof entry !== 'string') {109return match;110}111112if (pattern && replacement) {113try {114return entry.replace(new RegExp(replaceTransformEscapes(pattern)), replaceTransformEscapes(replacement));115} catch (e) {116console.log(`Error applying 'resolveCopyDestinationSetting' transform: ${pattern} -> ${replacement}`);117}118}119120return entry;121});122}123124function replaceTransformEscapes(str: string): string {125return str.replaceAll(/\\\//g, '/');126}127128129