Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework markdown paste resource #201838

Merged
merged 1 commit into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 11 additions & 1 deletion extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -503,11 +503,21 @@
"%configuration.copyIntoWorkspace.never%"
]
},
"markdown.editor.filePaste.videoSnippet": {
"type": "string",
"markdownDescription": "%configuration.markdown.editor.filePaste.videoSnippet%",
"default": "<video controls src=\"${src}\" title=\"${title}\"></video>"
},
"markdown.editor.filePaste.audioSnippet": {
"type": "string",
"markdownDescription": "%configuration.markdown.editor.filePaste.audioSnippet%",
"default": "<audio controls src=\"${src}\" title=\"${title}\"></audio>"
},
"markdown.editor.pasteUrlAsFormattedLink.enabled": {
"type": "string",
"scope": "resource",
"markdownDescription": "%configuration.markdown.editor.pasteUrlAsFormattedLink.enabled%",
"default": "never",
"default": "smart",
"enum": [
"always",
"smart",
Expand Down
10 changes: 6 additions & 4 deletions extensions/markdown-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
"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.editor.pasteUrlAsFormattedLink.enabled": "Controls how a Markdown link is created when a URL is pasted into the Markdown editor. Requires enabling `#editor.pasteAs.enabled#`.",
"configuration.pasteUrlAsFormattedLink.always": "Always creates a Markdown link when a URL is pasted into the Markdown editor.",
"configuration.pasteUrlAsFormattedLink.smart": "Smartly avoids creating a Markdown link in specific cases, such as within code brackets or inside an existing Markdown link.",
"configuration.pasteUrlAsFormattedLink.never": "Never creates a Markdown link when a URL is pasted into the Markdown editor.",
"configuration.markdown.editor.pasteUrlAsFormattedLink.enabled": "Controls if Markdown links are created when URLs are pasted into a Markdown editor. Requires enabling `#editor.pasteAs.enabled#`.",
"configuration.pasteUrlAsFormattedLink.always": "Always insert Markdown links.",
"configuration.pasteUrlAsFormattedLink.smart": "Smartly create Markdown links by default when you have selected text and are not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.",
"configuration.pasteUrlAsFormattedLink.never": "Never create Markdown links.",
"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.fragmentLinks.enabled.description": "Validate fragment links to headers in the current Markdown file, for example: `[link](#header)`. Requires enabling `#markdown.validate.enabled#`.",
Expand All @@ -71,5 +71,7 @@
"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.removeExtension": "Prefer removing the file extension. For example, path completions to a file named `file.md` will insert `file` without the `.md`.",
"configuration.markdown.editor.filePaste.videoSnippet": "Snippet used when adding videos to Markdown. This snippet can use the following variables:\n- `${src}` — The resolved path of the video file.\n- `${title}` — The title used for the video. A snippet placeholder will automatically be created for this variable.",
"configuration.markdown.editor.filePaste.audioSnippet": "Snippet used when adding audio to Markdown. This snippet can use the following variables:\n- `${src}` — The resolved path of the audio file.\n- `${title}` — The title used for the audio. A snippet placeholder will automatically be created for this variable.",
"workspaceTrust": "Required for loading styles configured in the workspace."
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ export class InsertLinkFromWorkspace implements Command {
title: vscode.l10n.t("Insert link"),
defaultUri: getDefaultUri(activeEditor.document),
});
if (!resources) {
return;
}

return insertLink(activeEditor, resources ?? [], false);
return insertLink(activeEditor, resources, false);
}
}

Expand All @@ -54,8 +57,11 @@ export class InsertImageFromWorkspace implements Command {
title: vscode.l10n.t("Insert image"),
defaultUri: getDefaultUri(activeEditor.document),
});
if (!resources) {
return;
}

return insertLink(activeEditor, resources ?? [], true);
return insertLink(activeEditor, resources, true);
}
}

Expand All @@ -67,27 +73,28 @@ function getDefaultUri(document: vscode.TextDocument) {
return Utils.dirname(docUri);
}

async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsImage: boolean): Promise<void> {
if (!selectedFiles.length) {
return;
async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: readonly vscode.Uri[], insertAsMedia: boolean): Promise<void> {
const edit = createInsertLinkEdit(activeEditor, selectedFiles, insertAsMedia);
if (edit) {
await vscode.workspace.applyEdit(edit);
}

const edit = createInsertLinkEdit(activeEditor, selectedFiles, insertAsImage);
await vscode.workspace.applyEdit(edit);
}

function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean, title = '', placeholderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false) {
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: readonly vscode.Uri[], insertAsMedia: boolean) {
const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => {
const selectionText = activeEditor.document.getText(selection);
const snippet = createUriListSnippet(activeEditor.document, selectedFiles.map(uri => ({ uri })), title, placeholderValue, pasteAsMarkdownLink, isExternalLink, {
insertAsMedia,
const snippet = createUriListSnippet(activeEditor.document.uri, selectedFiles.map(uri => ({ uri })), {
insertAsMedia: insertAsMedia,
placeholderText: selectionText,
placeholderStartIndex: (i + 1) * selectedFiles.length,
separator: insertAsMedia ? '\n' : ' ',
});

return snippet ? new vscode.SnippetTextEdit(selection, snippet.snippet) : undefined;
}));
if (!snippetEdits.length) {
return;
}

const edit = new vscode.WorkspaceEdit();
edit.set(activeEditor.document.uri, snippetEdits);
Expand Down
6 changes: 2 additions & 4 deletions extensions/markdown-language-features/src/extension.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +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/pasteResourceProvider';
import { registerLinkPasteSupport } from './languageFeatures/copyFiles/pasteUrlProvider';
import { registerResourceDropOrPasteSupport } from './languageFeatures/copyFiles/dropOrPasteResource';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { registerDropIntoEditorSupport } from './languageFeatures/copyFiles/dropResourceProvider';
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { registerUpdateLinksOnRename } from './languageFeatures/linkUpdater';
import { ILogger } from './logging';
Expand Down Expand Up @@ -57,9 +56,8 @@ function registerMarkdownLanguageFeatures(
return vscode.Disposable.from(
// Language features
registerDiagnosticSupport(selector, commandManager),
registerDropIntoEditorSupport(selector),
registerFindFileReferenceSupport(commandManager, client),
registerPasteSupport(selector),
registerResourceDropOrPasteSupport(selector),
registerLinkPasteSupport(selector),
registerUpdateLinksOnRename(client),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@
* 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 '../../util/document';

type OverwriteBehavior = 'overwrite' | 'nameIncrementally';

interface CopyFileConfiguration {
export interface CopyFileConfiguration {
readonly destination: Record<string, string>;
readonly overwriteBehavior: OverwriteBehavior;
}

function getCopyFileConfiguration(document: vscode.TextDocument): CopyFileConfiguration {
export function getCopyFileConfiguration(document: vscode.TextDocument): CopyFileConfiguration {
const config = vscode.workspace.getConfiguration('markdown', document);
return {
destination: config.get<Record<string, string>>('copyFiles.destination') ?? {},
Expand All @@ -30,72 +28,7 @@ function readOverwriteBehavior(config: vscode.WorkspaceConfiguration): Overwrite
}
}

export class NewFilePathGenerator {

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

async getNewFilePath(
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 ext = Utils.extname(desiredPath);
let baseName = Utils.basename(desiredPath);
baseName = baseName.slice(0, baseName.length - ext.length);
for (let i = 0; ; ++i) {
if (token.isCancellationRequested) {
return undefined;
}

const name = i === 0 ? baseName : `${baseName}-${i}`;
const uri = vscode.Uri.joinPath(root, name + ext);
if (this._wasPathAlreadyUsed(uri)) {
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 {
await vscode.workspace.fs.stat(uri);
} catch {
if (!this._wasPathAlreadyUsed(uri)) {
// Does not exist
this._usedPaths.add(uri.toString());
return { uri, overwrite: false };
}
}
}
}

private _wasPathAlreadyUsed(uri: vscode.Uri) {
return this._usedPaths.has(uri.toString());
}
}

function getDesiredNewFilePath(config: CopyFileConfiguration, document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri {
const docUri = getParentDocumentUri(document.uri);
for (const [rawGlob, rawDest] of Object.entries(config.destination)) {
for (const glob of parseGlob(rawGlob)) {
if (picomatch.isMatch(docUri.path, glob, { dot: true })) {
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> {
export 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);
Expand Down Expand Up @@ -165,7 +98,7 @@ function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string
['fileExtName', path.extname(fileName).replace('.', '')], // File extension (without dot): png
]);

return outDest.replaceAll(/(?<escape>\\\$)|(?<!\\)\$\{(?<name>\w+)(?:\/(?<pattern>(?:\\\/|[^\}])+?)\/(?<replacement>(?:\\\/|[^\}])+?)\/)?\}/g, (match, _escape, name, pattern, replacement, _offset, _str, groups) => {
return outDest.replaceAll(/(?<escape>\\\$)|(?<!\\)\$\{(?<name>\w+)(?:\/(?<pattern>(?:\\\/|[^\}\/])+)\/(?<replacement>(?:\\\/|[^\}\/])*)\/)?\}/g, (match, _escape, name, pattern, replacement, _offset, _str, groups) => {
if (groups?.['escape']) {
return '$';
}
Expand All @@ -176,7 +109,11 @@ function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string
}

if (pattern && replacement) {
return entry.replace(new RegExp(replaceTransformEscapes(pattern)), replaceTransformEscapes(replacement));
try {
return entry.replace(new RegExp(replaceTransformEscapes(pattern)), replaceTransformEscapes(replacement));
} catch (e) {
console.log(`Error applying 'resolveCopyDestinationSetting' transform: ${pattern} -> ${replacement}`);
}
}

return entry;
Expand All @@ -186,4 +123,3 @@ function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string
function replaceTransformEscapes(str: string): string {
return str.replaceAll(/\\\//g, '/');
}