Repository: microsoft/vscode (TypeScript)
Date: 2022-07-06 12:51:32+00:00
Task type: Bug Fix

Issue:
File links without folders when command detection should prefer searching over matching a file in the initial cwd
With file structure on Windows[^1]:

- package.json
- src/
  - package.json

When shell integration is disabled, clicking a `package.json` link currently open `./package.json` directly.

The expectation here because cwd detection isn't possible on Windows unless shell integration is enabled it to prefer a search since we cannot tell what the active directory is. Consider the following problem case:

1. Open terminal
2. `cd src`
3. `ls`
4. Click `package.json`, 🐛 `./package.json` will open but a search should as the cwd is ambiguous

[^1]: The cwd can be detected on Linux/macOS even if shell integration is disabled


Issue discussion:

Solution patch:
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts
@@ -218,6 +218,16 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener {
 
 	private async _tryOpenExactLink(text: string, link: ITerminalSimpleLink): Promise<boolean> {
 		const sanitizedLink = text.replace(/:\d+(:\d+)?$/, '');
+		// For links made up of only a file name (no folder), disallow exact link matching. For
+		// example searching for `foo.txt` when there is no cwd information available (ie. only the
+		// initial cwd) should NOT search  as it's ambiguous if there are multiple matches.
+		//
+		// However, for a link like `src/foo.txt`, if there's an exact match for `src/foo.txt` in
+		// any folder we want to take it, even if there are partial matches like `src2/foo.txt`
+		// available.
+		if (!sanitizedLink.match(/[\\/]/)) {
+			return false;
+		}
 		try {
 			const result = await this._getExactMatch(sanitizedLink);
 			if (result) {


Test patch:
diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts
--- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts
+++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts
@@ -97,10 +97,21 @@ suite('Workbench - TerminalLinkOpeners', () => {
 			capabilities.add(TerminalCapability.CommandDetection, commandDetection);
 		});
 
-		test('should open single exact match against cwd when searching if it exists', async () => {
+		test('should open single exact match against cwd when searching if it exists when command detection cwd is available', async () => {
 			localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux);
 			const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener);
 			opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, Promise.resolve('/initial/cwd'), localFileOpener, localFolderOpener, OperatingSystem.Linux);
+			// Set a fake detected command starting as line 0 to establish the cwd
+			commandDetection.setCommands([{
+				command: '',
+				cwd: '/initial/cwd',
+				timestamp: 0,
+				getOutput() { return undefined; },
+				marker: {
+					line: 0
+				} as Partial<IXtermMarker> as any,
+				hasOutput: true
+			}]);
 			fileService.setFiles([
 				URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo/bar.txt' }),
 				URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo2/bar.txt' })
@@ -116,6 +127,44 @@ suite('Workbench - TerminalLinkOpeners', () => {
 			});
 		});
 
+		test('should open single exact match against cwd for paths containing a separator when searching if it exists, even when command detection isn\'t available', async () => {
+			localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux);
+			const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener);
+			opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, Promise.resolve('/initial/cwd'), localFileOpener, localFolderOpener, OperatingSystem.Linux);
+			fileService.setFiles([
+				URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo/bar.txt' }),
+				URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo2/bar.txt' })
+			]);
+			await opener.open({
+				text: 'foo/bar.txt',
+				bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } },
+				type: TerminalBuiltinLinkType.Search
+			});
+			deepStrictEqual(activationResult, {
+				link: 'file:///initial/cwd/foo/bar.txt',
+				source: 'editor'
+			});
+		});
+
+		test('should not open single exact match for paths not containing a when command detection isn\'t available', async () => {
+			localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux);
+			const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener);
+			opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, Promise.resolve('/initial/cwd'), localFileOpener, localFolderOpener, OperatingSystem.Linux);
+			fileService.setFiles([
+				URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo/bar.txt' }),
+				URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo2/bar.txt' })
+			]);
+			await opener.open({
+				text: 'bar.txt',
+				bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } },
+				type: TerminalBuiltinLinkType.Search
+			});
+			deepStrictEqual(activationResult, {
+				link: 'bar.txt',
+				source: 'search'
+			});
+		});
+
 		suite('macOS/Linux', () => {
 			setup(() => {
 				localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux);