-
Notifications
You must be signed in to change notification settings - Fork 162
Expand file tree
/
Copy pathurl-regex-provider.ts
More file actions
162 lines (141 loc) · 4.13 KB
/
Copy pathurl-regex-provider.ts
File metadata and controls
162 lines (141 loc) · 4.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* URL Regex Link Provider
*
* Detects plain text URLs using regex pattern matching.
* Supports common protocols but excludes file paths.
*
* This provider runs after OSC8LinkProvider, so explicit hyperlinks
* take precedence over regex-detected URLs.
*/
import type { IBufferRange, ILink, ILinkProvider } from '../types';
/**
* URL Regex Provider
*
* Detects plain text URLs on a single line using regex.
* Does not support multi-line URLs or file paths.
*
* Supported protocols:
* - https://, http://
* - mailto:
* - ftp://, ssh://, git://
* - tel:, magnet:
* - gemini://, gopher://, news:
*/
export class UrlRegexProvider implements ILinkProvider {
/**
* URL regex pattern
* Matches common protocols followed by valid URL characters
* Excludes file paths (no ./ or ../ or bare /)
*/
private static readonly URL_REGEX =
/(?:https?:\/\/|mailto:|ftp:\/\/|ssh:\/\/|git:\/\/|tel:|magnet:|gemini:\/\/|gopher:\/\/|news:)[\w\-.~:\/?#@!$&*+,;=%()]+/gi;
/**
* Characters to strip from end of URLs
* Common punctuation that's unlikely to be part of the URL
*/
private static readonly TRAILING_PUNCTUATION = /[.,;!?\]]+$/;
constructor(private terminal: ITerminalForUrlProvider) {}
/**
* Provide all regex-detected URLs on the given row
*/
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
const links: ILink[] = [];
const line = this.terminal.buffer.active.getLine(y);
if (!line) {
callback(undefined);
return;
}
// Convert line cells to text
const lineText = this.lineToText(line);
// Reset regex state (global flag maintains state)
UrlRegexProvider.URL_REGEX.lastIndex = 0;
// Find all URL matches in the line
let match: RegExpExecArray | null = UrlRegexProvider.URL_REGEX.exec(lineText);
while (match !== null) {
let url = match[0];
const startX = match.index;
let endX = match.index + url.length - 1; // Inclusive end
// Strip trailing punctuation
const stripped = url.replace(UrlRegexProvider.TRAILING_PUNCTUATION, '');
if (stripped.length < url.length) {
url = stripped;
endX = startX + url.length - 1;
}
// Strip unbalanced trailing parentheses
while (url.endsWith(')')) {
const open = url.split('(').length - 1;
const close = url.split(')').length - 1;
if (close > open) {
url = url.slice(0, -1);
endX--;
} else {
break;
}
}
// Skip if URL is too short (e.g., just "http://")
if (url.length > 8) {
links.push({
text: url,
range: {
start: { x: startX, y },
end: { x: endX, y },
},
activate: (event) => {
// Open link if Ctrl/Cmd is pressed
if (event.ctrlKey || event.metaKey) {
window.open(url, '_blank', 'noopener,noreferrer');
}
},
});
}
// Get next match
match = UrlRegexProvider.URL_REGEX.exec(lineText);
}
callback(links.length > 0 ? links : undefined);
}
/**
* Convert a buffer line to plain text string
*/
private lineToText(line: IBufferLineForUrlProvider): string {
const chars: string[] = [];
for (let x = 0; x < line.length; x++) {
const cell = line.getCell(x);
if (!cell) {
chars.push(' ');
continue;
}
const codepoint = cell.getCodepoint();
// Skip null characters and control characters
if (codepoint === 0 || codepoint < 32) {
chars.push(' ');
} else {
chars.push(String.fromCodePoint(codepoint));
}
}
return chars.join('');
}
dispose(): void {
// No resources to clean up
}
}
/**
* Minimal terminal interface required by UrlRegexProvider
*/
export interface ITerminalForUrlProvider {
buffer: {
active: {
getLine(y: number): IBufferLineForUrlProvider | undefined;
};
};
}
/**
* Minimal buffer line interface for URL detection
*/
interface IBufferLineForUrlProvider {
length: number;
getCell(x: number):
| {
getCodepoint(): number;
}
| undefined;
}