mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 04:09:28 +00:00
Add experimental setting to define where files should be copied for markdown (#169454)
For #157043
This commit is contained in:
@@ -571,6 +571,13 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"description": "%configuration.markdown.occurrencesHighlight.enabled%",
|
"description": "%configuration.markdown.occurrencesHighlight.enabled%",
|
||||||
"scope": "resource"
|
"scope": "resource"
|
||||||
|
},
|
||||||
|
"markdown.experimental.copyFiles.destination": {
|
||||||
|
"type": "object",
|
||||||
|
"markdownDescription": "%configuration.markdown.copyFiles.destination%",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"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`.",
|
||||||
"workspaceTrust": "Required for loading styles configured in the workspace."
|
"workspaceTrust": "Required for loading styles configured in the workspace."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,11 @@
|
|||||||
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/dropIntoEditor';
|
import { createUriListSnippet, getParentDocumentUri, imageFileExtensions } from '../languageFeatures/copyFiles/dropIntoEditor';
|
||||||
import { coalesce } from '../util/arrays';
|
import { coalesce } from '../util/arrays';
|
||||||
import { Schemes } from '../util/schemes';
|
import { Schemes } from '../util/schemes';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class InsertLinkFromWorkspace implements Command {
|
export class InsertLinkFromWorkspace implements Command {
|
||||||
public readonly id = 'markdown.editor.insertLinkFromWorkspace';
|
public readonly id = 'markdown.editor.insertLinkFromWorkspace';
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import * as vscode from 'vscode';
|
|||||||
import { MdLanguageClient } from './client/client';
|
import { MdLanguageClient } from './client/client';
|
||||||
import { CommandManager } from './commandManager';
|
import { CommandManager } from './commandManager';
|
||||||
import { registerMarkdownCommands } from './commands/index';
|
import { registerMarkdownCommands } from './commands/index';
|
||||||
import { registerPasteSupport } from './languageFeatures/copyPaste';
|
import { registerPasteSupport } from './languageFeatures/copyFiles/copyPaste';
|
||||||
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
|
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
|
||||||
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
|
import { registerDropIntoEditorSupport } from './languageFeatures/copyFiles/dropIntoEditor';
|
||||||
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
|
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
|
||||||
import { registerUpdateLinksOnRename } from './languageFeatures/linkUpdater';
|
import { registerUpdateLinksOnRename } from './languageFeatures/linkUpdater';
|
||||||
import { ILogger } from './logging';
|
import { ILogger } from './logging';
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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<vscode.Uri> {
|
||||||
|
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<Record<string, string>>('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<string> {
|
||||||
|
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<string, string>([
|
||||||
|
['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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,10 +3,9 @@
|
|||||||
* 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 { Utils } from 'vscode-uri';
|
import { Schemes } from '../../util/schemes';
|
||||||
import { Schemes } from '../util/schemes';
|
import { getNewFileName } from './copyFiles';
|
||||||
import { createUriListSnippet, tryGetUriListSnippet } from './dropIntoEditor';
|
import { createUriListSnippet, tryGetUriListSnippet } from './dropIntoEditor';
|
||||||
|
|
||||||
const supportedImageMimes = new Set([
|
const supportedImageMimes = new Set([
|
||||||
@@ -59,7 +58,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uri = await this._getNewFileName(document, file);
|
const uri = await getNewFileName(document, file);
|
||||||
if (token.isCancellationRequested) {
|
if (token.isCancellationRequested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,23 +76,6 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
|
|||||||
pasteEdit.additionalEdit = workspaceEdit;
|
pasteEdit.additionalEdit = workspaceEdit;
|
||||||
return pasteEdit;
|
return pasteEdit;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getNewFileName(document: vscode.TextDocument, file: vscode.DataTransferFile): Promise<vscode.Uri> {
|
|
||||||
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,) {
|
export function registerPasteSupport(selector: vscode.DocumentSelector,) {
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as URI from 'vscode-uri';
|
import * as URI from 'vscode-uri';
|
||||||
import { Schemes } from '../util/schemes';
|
import { Schemes } from '../../util/schemes';
|
||||||
|
|
||||||
export const imageFileExtensions = new Set<string>([
|
export const imageFileExtensions = new Set<string>([
|
||||||
'bmp',
|
'bmp',
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user