|
| 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