Path: blob/main/extensions/markdown-language-features/src/languageFeatures/copyFiles/smartDropOrPaste.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*--------------------------------------------------------------------------------------------*/45import * as vscode from 'vscode';6import { IMdParser } from '../../markdownEngine';7import { ITextDocument } from '../../types/textDocument';8import { Schemes } from '../../util/schemes';910const smartPasteLineRegexes = [11{ regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link12{ regex: /\$\$[\s\S]*?\$\$/gm }, // In a fenced math block13{ regex: /`[^`]*`/g }, // In inline code14{ regex: /\$[^$]*\$/g }, // In inline math15{ regex: /<[^<>\s]*>/g }, // Autolink16{ regex: /^[ ]{0,3}\[\w+\]:\s.*$/g, isWholeLine: true }, // Block link definition (needed as tokens are not generated for these)17];1819export async function shouldInsertMarkdownLinkByDefault(20parser: IMdParser,21document: ITextDocument,22pasteUrlSetting: InsertMarkdownLink,23ranges: readonly vscode.Range[],24token: vscode.CancellationToken25): Promise<boolean> {26switch (pasteUrlSetting) {27case InsertMarkdownLink.Always: {28return true;29}30case InsertMarkdownLink.Smart: {31return checkSmart();32}33case InsertMarkdownLink.SmartWithSelection: {34// At least one range must not be empty35if (!ranges.some(range => document.getText(range).trim().length > 0)) {36return false;37}38// And all ranges must be smart39return checkSmart();40}41default: {42return false;43}44}4546async function checkSmart(): Promise<boolean> {47return (await Promise.all(ranges.map(range => shouldSmartPasteForSelection(parser, document, range, token)))).every(x => x);48}49}5051const textTokenTypes = new Set([52'paragraph_open',53'inline',54'heading_open',55'ordered_list_open',56'bullet_list_open',57'list_item_open',58'blockquote_open',59]);6061async function shouldSmartPasteForSelection(62parser: IMdParser,63document: ITextDocument,64selectedRange: vscode.Range,65token: vscode.CancellationToken66): Promise<boolean> {67// Disable for multi-line selections68if (selectedRange.start.line !== selectedRange.end.line) {69return false;70}7172const rangeText = document.getText(selectedRange);73// Disable when the selection is already a link74if (findValidUriInText(rangeText)) {75return false;76}7778if (/\[.*\]\(.*\)/.test(rangeText) || /!\[.*\]\(.*\)/.test(rangeText)) {79return false;80}8182// Check if selection is inside a special block level element using markdown engine83const tokens = await parser.tokenize(document);84if (token.isCancellationRequested) {85return false;86}8788for (let i = 0; i < tokens.length; i++) {89const token = tokens[i];90if (!token.map) {91continue;92}93if (token.map[0] <= selectedRange.start.line && token.map[1] > selectedRange.start.line) {94if (!textTokenTypes.has(token.type)) {95return false;96}97}9899// Special case for html such as:100//101// <b>102// |103// </b>104//105// In this case pasting will cause the html block to be created even though the cursor is not currently inside a block106if (token.type === 'html_block' && token.map[1] === selectedRange.start.line) {107const nextToken = tokens.at(i + 1);108// The next token does not need to be a html_block, but it must be on the next line109if (nextToken?.map?.[0] === selectedRange.end.line + 1) {110return false;111}112}113}114115// Run additional regex checks on the current line to check if we are inside an inline element116const line = document.getText(new vscode.Range(selectedRange.start.line, 0, selectedRange.start.line, Number.MAX_SAFE_INTEGER));117for (const regex of smartPasteLineRegexes) {118for (const match of line.matchAll(regex.regex)) {119if (match.index === undefined) {120continue;121}122123if (regex.isWholeLine) {124return false;125}126127if (selectedRange.start.character > match.index && selectedRange.start.character < match.index + match[0].length) {128return false;129}130}131}132133return true;134}135136const externalUriSchemes: ReadonlySet<string> = new Set([137Schemes.http,138Schemes.https,139Schemes.mailto,140Schemes.file,141]);142143export function findValidUriInText(text: string): string | undefined {144const trimmedUrlList = text.trim();145146if (!/^\S+$/.test(trimmedUrlList) // Uri must consist of a single sequence of characters without spaces147|| !trimmedUrlList.includes(':') // And it must have colon somewhere for the scheme. We will verify the schema again later148) {149return;150}151152let uri: vscode.Uri;153try {154uri = vscode.Uri.parse(trimmedUrlList);155} catch {156// Could not parse157return;158}159160// `Uri.parse` is lenient and will return a `file:` uri even for non-uri text such as `abc`161// Make sure that the resolved scheme starts the original text162if (!trimmedUrlList.toLowerCase().startsWith(uri.scheme.toLowerCase() + ':')) {163return;164}165166// Only enable for an allow list of schemes. Otherwise this can be accidentally activated for non-uri text167// such as `c:\abc` or `value:foo`168if (!externalUriSchemes.has(uri.scheme.toLowerCase())) {169return;170}171172// Some part of the uri must not be empty173// This disables the feature for text such as `http:`174if (!uri.authority && uri.path.length < 2 && !uri.query && !uri.fragment) {175return;176}177178return trimmedUrlList;179}180181export enum InsertMarkdownLink {182Always = 'always',183SmartWithSelection = 'smartWithSelection',184Smart = 'smart',185Never = 'never'186}187188189190