Repository: microsoft/vscode (TypeScript)
Date: 2022-02-03 21:35:21+00:00
Task type: Bug Fix

Issue:
Handle directories in search link opener as well
Clicking a `fixtures` link in the terminal when the folder exists in the cwd results in a search:

![image](https://user-images.githubusercontent.com/2193314/152005046-5caf4a12-70b9-443b-91b9-a975162ff633.png)

We do resolve this path and find that `${cwd}/fixtures` is a directory, so we could defer to the local folder handlers in this case which should be more useful and consistent with how files are treated.



Issue discussion:
to verify, with log level set to debug, make sure that you do not see `Opening link` with `type`:3 which represents a low confidence search link for this case https://github.com/microsoft/vscode/blob/8c20462d2a842a3838a5ba37b2b6557256fbdbf7/src/vs/workbench/contrib/terminal/browser/links/links.ts#L78-L82.


If you `echo src` in the vscode or xterm.js and click that link, it should focus that folder in the explorer instead of opening the search.

For dirs outside the workspace, eg:

```
cd ..
echo vscode-docs
```

Clicking vscode-docs should open a new window.

We handle local files specially here:

https://github.com/microsoft/vscode/blob/1981fd8ece9ac68c681f60b354e8167fec087a84/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts#L146-L154

See how in `_getExactMatch` we only return based on `fileStat.isFile`, it should also handle directories both in and out of the workspace.

Solution patch:
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts
@@ -78,10 +78,11 @@ export class TerminalLinkManager extends DisposableStore {
 
 		// Setup link openers
 		const localFileOpener = this._instantiationService.createInstance(TerminalLocalFileLinkOpener, this._processManager.os || OS);
+		const localFolderInWorkspaceOpener = this._instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener);
 		this._openers.set(TerminalBuiltinLinkType.LocalFile, localFileOpener);
-		this._openers.set(TerminalBuiltinLinkType.LocalFolderInWorkspace, this._instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener));
+		this._openers.set(TerminalBuiltinLinkType.LocalFolderInWorkspace, localFolderInWorkspaceOpener);
 		this._openers.set(TerminalBuiltinLinkType.LocalFolderOutsideWorkspace, this._instantiationService.createInstance(TerminalLocalFolderOutsideWorkspaceLinkOpener));
-		this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, this._processManager.os || OS));
+		this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, localFolderInWorkspaceOpener, this._processManager.os || OS));
 		this._openers.set(TerminalBuiltinLinkType.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processManager.remoteAuthority));
 
 		this._registerStandardLinkProviders();
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
@@ -111,6 +111,7 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener {
 	constructor(
 		private readonly _capabilities: ITerminalCapabilityStore,
 		private readonly _localFileOpener: TerminalLocalFileLinkOpener,
+		private readonly _localFolderInWorkspaceOpener: TerminalLocalFolderInWorkspaceLinkOpener,
 		private readonly _os: OperatingSystem,
 		@IFileService private readonly _fileService: IFileService,
 		@IInstantiationService private readonly _instantiationService: IInstantiationService,
@@ -143,14 +144,18 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener {
 		}
 		const sanitizedLink = matchLink.replace(/:\d+(:\d+)?$/, '');
 		try {
-			const uri = await this._getExactMatch(sanitizedLink);
-			if (uri) {
-				return this._localFileOpener.open({
+			const result = await this._getExactMatch(sanitizedLink);
+			if (result) {
+				const { uri, isDirectory } = result;
+				const linkToOpen = {
 					text: matchLink,
 					uri,
 					bufferRange: link.bufferRange,
 					type: link.type
-				});
+				};
+				if (uri) {
+					return isDirectory ? this._localFolderInWorkspaceOpener.open(linkToOpen) : this._localFileOpener.open(linkToOpen);
+				}
 			}
 		} catch {
 			// Fallback to searching quick access
@@ -187,21 +192,19 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener {
 		return text;
 	}
 
-	private async _getExactMatch(sanitizedLink: string): Promise<URI | undefined> {
-		let exactResource: URI | undefined;
+	private async _getExactMatch(sanitizedLink: string): Promise<IResourceMatch | undefined> {
+		let resourceMatch: IResourceMatch | undefined;
 		if (osPathModule(this._os).isAbsolute(sanitizedLink)) {
 			const scheme = this._workbenchEnvironmentService.remoteAuthority ? Schemas.vscodeRemote : Schemas.file;
-			const resource = URI.from({ scheme, path: sanitizedLink });
+			const uri = URI.from({ scheme, path: sanitizedLink });
 			try {
-				const fileStat = await this._fileService.resolve(resource);
-				if (fileStat.isFile) {
-					exactResource = resource;
-				}
+				const fileStat = await this._fileService.resolve(uri);
+				resourceMatch = { uri, isDirectory: fileStat.isDirectory };
 			} catch {
-				// File doesn't exist, continue on
+				// File or dir doesn't exist, continue on
 			}
 		}
-		if (!exactResource) {
+		if (!resourceMatch) {
 			const results = await this._searchService.fileSearch(
 				this._fileQueryBuilder.file(this._workspaceContextService.getWorkspace().folders, {
 					// Remove optional :row:col from the link as openEditor supports it
@@ -210,13 +213,18 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener {
 				})
 			);
 			if (results.results.length === 1) {
-				exactResource = results.results[0].resource;
+				resourceMatch = { uri: results.results[0].resource };
 			}
 		}
-		return exactResource;
+		return resourceMatch;
 	}
 }
 
+interface IResourceMatch {
+	uri: URI | undefined;
+	isDirectory?: boolean;
+}
+
 export class TerminalUrlLinkOpener implements ITerminalLinkOpener {
 	constructor(
 		private readonly _isRemote: boolean,


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
@@ -16,7 +16,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
 import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
 import { CommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability';
 import { TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
-import { TerminalLocalFileLinkOpener, TerminalSearchLinkOpener } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners';
+import { TerminalLocalFileLinkOpener, TerminalLocalFolderInWorkspaceLinkOpener, TerminalSearchLinkOpener } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners';
 import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
 import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore';
 import { ITerminalCommand, IXtermMarker } from 'vs/workbench/contrib/terminal/common/terminal';
@@ -103,7 +103,8 @@ suite('Workbench - TerminalLinkOpeners', () => {
 		suite('macOS/Linux', () => {
 			setup(() => {
 				localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux);
-				opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, OperatingSystem.Linux);
+				const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener);
+				opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, localFolderOpener, OperatingSystem.Linux);
 			});
 
 			test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => {
@@ -150,7 +151,8 @@ suite('Workbench - TerminalLinkOpeners', () => {
 		suite('Windows', () => {
 			setup(() => {
 				localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Windows);
-				opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, OperatingSystem.Windows);
+				const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener);
+				opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, localFolderOpener, OperatingSystem.Windows);
 			});
 
 			test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => {