Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/markdown-language-features/src/languageFeatures/copyFiles/smartDropOrPaste.ts
3294 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as vscode from 'vscode';
7
import { IMdParser } from '../../markdownEngine';
8
import { ITextDocument } from '../../types/textDocument';
9
import { Schemes } from '../../util/schemes';
10
11
const smartPasteLineRegexes = [
12
{ regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link
13
{ regex: /\$\$[\s\S]*?\$\$/gm }, // In a fenced math block
14
{ regex: /`[^`]*`/g }, // In inline code
15
{ regex: /\$[^$]*\$/g }, // In inline math
16
{ regex: /<[^<>\s]*>/g }, // Autolink
17
{ regex: /^[ ]{0,3}\[\w+\]:\s.*$/g, isWholeLine: true }, // Block link definition (needed as tokens are not generated for these)
18
];
19
20
export async function shouldInsertMarkdownLinkByDefault(
21
parser: IMdParser,
22
document: ITextDocument,
23
pasteUrlSetting: InsertMarkdownLink,
24
ranges: readonly vscode.Range[],
25
token: vscode.CancellationToken
26
): Promise<boolean> {
27
switch (pasteUrlSetting) {
28
case InsertMarkdownLink.Always: {
29
return true;
30
}
31
case InsertMarkdownLink.Smart: {
32
return checkSmart();
33
}
34
case InsertMarkdownLink.SmartWithSelection: {
35
// At least one range must not be empty
36
if (!ranges.some(range => document.getText(range).trim().length > 0)) {
37
return false;
38
}
39
// And all ranges must be smart
40
return checkSmart();
41
}
42
default: {
43
return false;
44
}
45
}
46
47
async function checkSmart(): Promise<boolean> {
48
return (await Promise.all(ranges.map(range => shouldSmartPasteForSelection(parser, document, range, token)))).every(x => x);
49
}
50
}
51
52
const textTokenTypes = new Set([
53
'paragraph_open',
54
'inline',
55
'heading_open',
56
'ordered_list_open',
57
'bullet_list_open',
58
'list_item_open',
59
'blockquote_open',
60
]);
61
62
async function shouldSmartPasteForSelection(
63
parser: IMdParser,
64
document: ITextDocument,
65
selectedRange: vscode.Range,
66
token: vscode.CancellationToken
67
): Promise<boolean> {
68
// Disable for multi-line selections
69
if (selectedRange.start.line !== selectedRange.end.line) {
70
return false;
71
}
72
73
const rangeText = document.getText(selectedRange);
74
// Disable when the selection is already a link
75
if (findValidUriInText(rangeText)) {
76
return false;
77
}
78
79
if (/\[.*\]\(.*\)/.test(rangeText) || /!\[.*\]\(.*\)/.test(rangeText)) {
80
return false;
81
}
82
83
// Check if selection is inside a special block level element using markdown engine
84
const tokens = await parser.tokenize(document);
85
if (token.isCancellationRequested) {
86
return false;
87
}
88
89
for (let i = 0; i < tokens.length; i++) {
90
const token = tokens[i];
91
if (!token.map) {
92
continue;
93
}
94
if (token.map[0] <= selectedRange.start.line && token.map[1] > selectedRange.start.line) {
95
if (!textTokenTypes.has(token.type)) {
96
return false;
97
}
98
}
99
100
// Special case for html such as:
101
//
102
// <b>
103
// |
104
// </b>
105
//
106
// In this case pasting will cause the html block to be created even though the cursor is not currently inside a block
107
if (token.type === 'html_block' && token.map[1] === selectedRange.start.line) {
108
const nextToken = tokens.at(i + 1);
109
// The next token does not need to be a html_block, but it must be on the next line
110
if (nextToken?.map?.[0] === selectedRange.end.line + 1) {
111
return false;
112
}
113
}
114
}
115
116
// Run additional regex checks on the current line to check if we are inside an inline element
117
const line = document.getText(new vscode.Range(selectedRange.start.line, 0, selectedRange.start.line, Number.MAX_SAFE_INTEGER));
118
for (const regex of smartPasteLineRegexes) {
119
for (const match of line.matchAll(regex.regex)) {
120
if (match.index === undefined) {
121
continue;
122
}
123
124
if (regex.isWholeLine) {
125
return false;
126
}
127
128
if (selectedRange.start.character > match.index && selectedRange.start.character < match.index + match[0].length) {
129
return false;
130
}
131
}
132
}
133
134
return true;
135
}
136
137
const externalUriSchemes: ReadonlySet<string> = new Set([
138
Schemes.http,
139
Schemes.https,
140
Schemes.mailto,
141
Schemes.file,
142
]);
143
144
export function findValidUriInText(text: string): string | undefined {
145
const trimmedUrlList = text.trim();
146
147
if (!/^\S+$/.test(trimmedUrlList) // Uri must consist of a single sequence of characters without spaces
148
|| !trimmedUrlList.includes(':') // And it must have colon somewhere for the scheme. We will verify the schema again later
149
) {
150
return;
151
}
152
153
let uri: vscode.Uri;
154
try {
155
uri = vscode.Uri.parse(trimmedUrlList);
156
} catch {
157
// Could not parse
158
return;
159
}
160
161
// `Uri.parse` is lenient and will return a `file:` uri even for non-uri text such as `abc`
162
// Make sure that the resolved scheme starts the original text
163
if (!trimmedUrlList.toLowerCase().startsWith(uri.scheme.toLowerCase() + ':')) {
164
return;
165
}
166
167
// Only enable for an allow list of schemes. Otherwise this can be accidentally activated for non-uri text
168
// such as `c:\abc` or `value:foo`
169
if (!externalUriSchemes.has(uri.scheme.toLowerCase())) {
170
return;
171
}
172
173
// Some part of the uri must not be empty
174
// This disables the feature for text such as `http:`
175
if (!uri.authority && uri.path.length < 2 && !uri.query && !uri.fragment) {
176
return;
177
}
178
179
return trimmedUrlList;
180
}
181
182
export enum InsertMarkdownLink {
183
Always = 'always',
184
SmartWithSelection = 'smartWithSelection',
185
Smart = 'smart',
186
Never = 'never'
187
}
188
189
190