Skip to content

Commit abbb5a8

Browse files
committed
Add multi-line link tests
1 parent 6afd37f commit abbb5a8

File tree

3 files changed

+204
-5
lines changed

3 files changed

+204
-5
lines changed

src/vs/workbench/contrib/terminalContrib/links/browser/terminalMultiLineLinkDetector.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ export class TerminalMultiLineLinkDetector implements ITerminalLinkDetector {
6262
return [];
6363
}
6464

65-
6665
this._logService.trace('terminalMultiLineLinkDetector#detect text', text);
6766

6867
// Match against the fallback matchers which are mainly designed to catch paths with spaces
@@ -85,12 +84,12 @@ export class TerminalMultiLineLinkDetector implements ITerminalLinkDetector {
8584
continue;
8685
}
8786

88-
this._logService.trace('terminalMultiLineLinkDetector#detect candidates', link);
87+
this._logService.trace('terminalMultiLineLinkDetector#detect candidate', link);
8988

9089
// Scan up looking for the first line that could be a path
9190
let possiblePath: string | undefined;
92-
for (let index = startLine - 1; index > 0; index--) {
93-
// TODO: Skip wrapped lines
91+
for (let index = startLine - 1; index >= 0; index--) {
92+
// TODO: This does not currently skip wrapped lines
9493
const text = getXtermLineContent(this.xterm.buffer.active, index, index, this.xterm.cols);
9594
if (!text.match(/^\s*\d/)) {
9695
possiblePath = text;

src/vs/workbench/contrib/terminalContrib/links/test/browser/linkTestUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export async function assertLinkHelper(
2525
lines.push(detector.xterm.buffer.active.getLine(i)!);
2626
}
2727

28-
const actualLinks = (await detector.detect(lines, 0, detector.xterm.buffer.active.cursorY)).map(e => {
28+
// Detect links always on the last line with content
29+
const actualLinks = (await detector.detect(lines, detector.xterm.buffer.active.cursorY, detector.xterm.buffer.active.cursorY)).map(e => {
2930
return {
3031
link: e.uri?.toString() ?? e.text,
3132
type: expectedType,
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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 { isWindows, OperatingSystem } from 'vs/base/common/platform';
7+
import { format } from 'vs/base/common/strings';
8+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
9+
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
10+
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
11+
import { TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminalContrib/links/browser/links';
12+
import { assertLinkHelper } from 'vs/workbench/contrib/terminalContrib/links/test/browser/linkTestUtils';
13+
import { Terminal } from 'xterm';
14+
import { timeout } from 'vs/base/common/async';
15+
import { strictEqual } from 'assert';
16+
import { TerminalLinkResolver } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkResolver';
17+
import { IFileService } from 'vs/platform/files/common/files';
18+
import { createFileStat } from 'vs/workbench/test/common/workbenchTestServices';
19+
import { URI } from 'vs/base/common/uri';
20+
import { NullLogService } from 'vs/platform/log/common/log';
21+
import { ITerminalLogService } from 'vs/platform/terminal/common/terminal';
22+
import { TerminalMultiLineLinkDetector } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalMultiLineLinkDetector';
23+
24+
const unixLinks: (string | { link: string; resource: URI })[] = [
25+
// Absolute
26+
'/foo',
27+
'/foo/bar',
28+
'/foo/[bar]',
29+
'/foo/[bar].baz',
30+
'/foo/[bar]/baz',
31+
'/foo/bar+more',
32+
// User home
33+
{ link: '~/foo', resource: URI.file('/home/foo') },
34+
// Relative
35+
{ link: './foo', resource: URI.file('/parent/cwd/foo') },
36+
{ link: './$foo', resource: URI.file('/parent/cwd/$foo') },
37+
{ link: '../foo', resource: URI.file('/parent/foo') },
38+
{ link: 'foo/bar', resource: URI.file('/parent/cwd/foo/bar') },
39+
{ link: 'foo/bar+more', resource: URI.file('/parent/cwd/foo/bar+more') },
40+
];
41+
42+
const windowsLinks: (string | { link: string; resource: URI })[] = [
43+
// Absolute
44+
'c:\\foo',
45+
{ link: '\\\\?\\C:\\foo', resource: URI.file('C:\\foo') },
46+
'c:/foo',
47+
'c:/foo/bar',
48+
'c:\\foo\\bar',
49+
'c:\\foo\\bar+more',
50+
'c:\\foo/bar\\baz',
51+
// User home
52+
{ link: '~\\foo', resource: URI.file('C:\\Home\\foo') },
53+
{ link: '~/foo', resource: URI.file('C:\\Home\\foo') },
54+
// Relative
55+
{ link: '.\\foo', resource: URI.file('C:\\Parent\\Cwd\\foo') },
56+
{ link: './foo', resource: URI.file('C:\\Parent\\Cwd\\foo') },
57+
{ link: './$foo', resource: URI.file('C:\\Parent\\Cwd\\$foo') },
58+
{ link: '..\\foo', resource: URI.file('C:\\Parent\\foo') },
59+
{ link: 'foo/bar', resource: URI.file('C:\\Parent\\Cwd\\foo\\bar') },
60+
{ link: 'foo/bar', resource: URI.file('C:\\Parent\\Cwd\\foo\\bar') },
61+
{ link: 'foo/[bar]', resource: URI.file('C:\\Parent\\Cwd\\foo\\[bar]') },
62+
{ link: 'foo/[bar].baz', resource: URI.file('C:\\Parent\\Cwd\\foo\\[bar].baz') },
63+
{ link: 'foo/[bar]/baz', resource: URI.file('C:\\Parent\\Cwd\\foo\\[bar]/baz') },
64+
{ link: 'foo\\bar', resource: URI.file('C:\\Parent\\Cwd\\foo\\bar') },
65+
{ link: 'foo\\[bar].baz', resource: URI.file('C:\\Parent\\Cwd\\foo\\[bar].baz') },
66+
{ link: 'foo\\[bar]\\baz', resource: URI.file('C:\\Parent\\Cwd\\foo\\[bar]\\baz') },
67+
{ link: 'foo\\bar+more', resource: URI.file('C:\\Parent\\Cwd\\foo\\bar+more') },
68+
];
69+
70+
interface LinkFormatInfo {
71+
urlFormat: string;
72+
/**
73+
* The start offset to the buffer range that is not in the actual link (but is in the matched
74+
* area.
75+
*/
76+
linkCellStartOffset?: number;
77+
/**
78+
* The end offset to the buffer range that is not in the actual link (but is in the matched
79+
* area.
80+
*/
81+
linkCellEndOffset?: number;
82+
line?: string;
83+
column?: string;
84+
}
85+
86+
const supportedLinkFormats: LinkFormatInfo[] = [
87+
{ urlFormat: '{0}\r\n{1}:foo', line: '5' },
88+
{ urlFormat: '{0}\r\n{1}: foo', line: '5' },
89+
{ urlFormat: '{0}\r\n{1}:{2} foo', line: '5', column: '3' },
90+
{ urlFormat: '{0}\r\n {1}:{2} foo', line: '5', column: '3' },
91+
];
92+
93+
suite('Workbench - TerminalMultiLineLinkDetector', () => {
94+
let instantiationService: TestInstantiationService;
95+
let configurationService: TestConfigurationService;
96+
let detector: TerminalMultiLineLinkDetector;
97+
let resolver: TerminalLinkResolver;
98+
let xterm: Terminal;
99+
let validResources: URI[];
100+
101+
async function assertLinks(
102+
type: TerminalBuiltinLinkType,
103+
text: string,
104+
expected: ({ uri: URI; range: [number, number][] })[]
105+
) {
106+
const race = await Promise.race([
107+
assertLinkHelper(text, expected, detector, type).then(() => 'success'),
108+
timeout(2).then(() => 'timeout')
109+
]);
110+
strictEqual(race, 'success', `Awaiting link assertion for "${text}" timed out`);
111+
}
112+
113+
async function assertLinksMain(link: string, resource?: URI) {
114+
const uri = resource ?? URI.file(link);
115+
const lines = link.split('\r\n');
116+
const lastLine = lines.at(-1)!;
117+
await assertLinks(TerminalBuiltinLinkType.LocalFile, link, [{ uri, range: [[1, lines.length], [lastLine.length, lines.length]] }]);
118+
}
119+
120+
setup(() => {
121+
instantiationService = new TestInstantiationService();
122+
configurationService = new TestConfigurationService();
123+
instantiationService.stub(IConfigurationService, configurationService);
124+
instantiationService.stub(IFileService, {
125+
async stat(resource) {
126+
if (!validResources.map(e => e.path).includes(resource.path)) {
127+
throw new Error('Doesn\'t exist');
128+
}
129+
return createFileStat(resource);
130+
}
131+
});
132+
instantiationService.stub(ITerminalLogService, new NullLogService());
133+
resolver = instantiationService.createInstance(TerminalLinkResolver);
134+
validResources = [];
135+
136+
xterm = new Terminal({ allowProposedApi: true, cols: 80, rows: 30 });
137+
});
138+
139+
suite('macOS/Linux', () => {
140+
setup(() => {
141+
detector = instantiationService.createInstance(TerminalMultiLineLinkDetector, xterm, {
142+
initialCwd: '/parent/cwd',
143+
os: OperatingSystem.Linux,
144+
remoteAuthority: undefined,
145+
userHome: '/home',
146+
backend: undefined
147+
}, resolver);
148+
});
149+
150+
for (const l of unixLinks) {
151+
const baseLink = typeof l === 'string' ? l : l.link;
152+
const resource = typeof l === 'string' ? URI.file(l) : l.resource;
153+
suite(`Link: ${baseLink}`, () => {
154+
for (let i = 0; i < supportedLinkFormats.length; i++) {
155+
const linkFormat = supportedLinkFormats[i];
156+
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
157+
test(`should detect in "${escapeMultilineTestName(formattedLink)}"`, async () => {
158+
validResources = [resource];
159+
await assertLinksMain(formattedLink, resource);
160+
});
161+
}
162+
});
163+
}
164+
});
165+
166+
// Only test these when on Windows because there is special behavior around replacing separators
167+
// in URI that cannot be changed
168+
if (isWindows) {
169+
suite('Windows', () => {
170+
setup(() => {
171+
detector = instantiationService.createInstance(TerminalMultiLineLinkDetector, xterm, {
172+
initialCwd: 'C:\\Parent\\Cwd',
173+
os: OperatingSystem.Windows,
174+
remoteAuthority: undefined,
175+
userHome: 'C:\\Home',
176+
}, resolver);
177+
});
178+
179+
for (const l of windowsLinks) {
180+
const baseLink = typeof l === 'string' ? l : l.link;
181+
const resource = typeof l === 'string' ? URI.file(l) : l.resource;
182+
suite(`Link "${baseLink}"`, () => {
183+
for (let i = 0; i < supportedLinkFormats.length; i++) {
184+
const linkFormat = supportedLinkFormats[i];
185+
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
186+
test(`should detect in "${escapeMultilineTestName(formattedLink)}"`, async () => {
187+
validResources = [resource];
188+
await assertLinksMain(formattedLink, resource);
189+
});
190+
}
191+
});
192+
}
193+
});
194+
}
195+
});
196+
197+
function escapeMultilineTestName(text: string): string {
198+
return text.replaceAll('\r\n', '\\r\\n');
199+
}

0 commit comments

Comments
 (0)