Skip to content

Commit 1ee09e9

Browse files
authored
support showing symlimk details in terminal suggest (#251296)
1 parent e2a90e9 commit 1ee09e9

File tree

4 files changed

+87
-6
lines changed

4 files changed

+87
-6
lines changed

extensions/terminal-suggest/src/env/pathExecutableCache.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class PathExecutableCache implements vscode.Disposable {
7676
const promises: Promise<Set<ICompletionResource> | undefined>[] = [];
7777
const labels: Set<string> = new Set<string>();
7878
for (const path of paths) {
79-
promises.push(this._getFilesInPath(path, pathSeparator, labels));
79+
promises.push(this._getExecutablesInPath(path, pathSeparator, labels));
8080
}
8181

8282
// Merge all results
@@ -96,7 +96,7 @@ export class PathExecutableCache implements vscode.Disposable {
9696
return this._cachedExes;
9797
}
9898

99-
private async _getFilesInPath(path: string, pathSeparator: string, labels: Set<string>): Promise<Set<ICompletionResource> | undefined> {
99+
private async _getExecutablesInPath(path: string, pathSeparator: string, labels: Set<string>): Promise<Set<ICompletionResource> | undefined> {
100100
try {
101101
const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false);
102102
if (!dirExists) {
@@ -106,11 +106,53 @@ export class PathExecutableCache implements vscode.Disposable {
106106
const fileResource = vscode.Uri.file(path);
107107
const files = await vscode.workspace.fs.readDirectory(fileResource);
108108
for (const [file, fileType] of files) {
109-
const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator);
110-
if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, this._cachedWindowsExeExtensions)) {
111-
result.add({ label: file, documentation: formattedPath, kind: vscode.TerminalCompletionItemKind.Method });
112-
labels.add(file);
109+
let kind: vscode.TerminalCompletionItemKind | undefined;
110+
let formattedPath: string | undefined;
111+
const resource = vscode.Uri.joinPath(fileResource, file);
112+
113+
// Skip unknown or directory file types early
114+
if (fileType === vscode.FileType.Unknown || fileType === vscode.FileType.Directory) {
115+
continue;
116+
}
117+
118+
try {
119+
const lstat = await fs.lstat(resource.fsPath);
120+
if (lstat.isSymbolicLink()) {
121+
try {
122+
const symlinkRealPath = await fs.realpath(resource.fsPath);
123+
const isExec = await isExecutable(symlinkRealPath, this._cachedWindowsExeExtensions);
124+
if (!isExec) {
125+
continue;
126+
}
127+
kind = vscode.TerminalCompletionItemKind.Method;
128+
formattedPath = `${resource.fsPath} -> ${symlinkRealPath}`;
129+
} catch {
130+
continue;
131+
}
132+
}
133+
} catch {
134+
// Ignore errors for unreadable files
135+
continue;
136+
}
137+
138+
formattedPath = formattedPath ?? getFriendlyResourcePath(resource, pathSeparator);
139+
140+
// Check if already added or not executable
141+
if (labels.has(file)) {
142+
continue;
113143
}
144+
145+
const isExec = kind === vscode.TerminalCompletionItemKind.Method || await isExecutable(formattedPath, this._cachedWindowsExeExtensions);
146+
if (!isExec) {
147+
continue;
148+
}
149+
150+
result.add({
151+
label: file,
152+
documentation: formattedPath,
153+
kind: kind ?? vscode.TerminalCompletionItemKind.Method
154+
});
155+
labels.add(file);
114156
}
115157
return result;
116158
} catch (e) {

extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import 'mocha';
77
import { strictEqual } from 'node:assert';
8+
import type { MarkdownString } from 'vscode';
89
import { PathExecutableCache } from '../../env/pathExecutableCache';
910

1011
suite('PathExecutableCache', () => {
@@ -31,4 +32,39 @@ suite('PathExecutableCache', () => {
3132
const result2 = await cache.getExecutablesInPath(env);
3233
strictEqual(result !== result2, true);
3334
});
35+
36+
if (process.platform !== 'win32') {
37+
test('cache should include executables found via symbolic links', async () => {
38+
const path = require('path');
39+
// Always use the source fixture directory to ensure symlinks are present
40+
const fixtureDir = path.resolve(__dirname.replace(/out[\/].*$/, 'src/test/env'), '../fixtures/symlink-test');
41+
const env = { PATH: fixtureDir };
42+
const cache = new PathExecutableCache();
43+
const result = await cache.getExecutablesInPath(env);
44+
cache.refresh();
45+
const labels = Array.from(result!.labels!);
46+
47+
strictEqual(labels.includes('real-executable.sh'), true);
48+
strictEqual(labels.includes('symlink-executable.sh'), true);
49+
strictEqual(result?.completionResources?.size, 2);
50+
51+
const completionResources = result!.completionResources!;
52+
let realDocRaw: string | MarkdownString | undefined = undefined;
53+
let symlinkDocRaw: string | MarkdownString | undefined = undefined;
54+
for (const resource of completionResources) {
55+
if (resource.label === 'real-executable.sh') {
56+
realDocRaw = resource.documentation;
57+
} else if (resource.label === 'symlink-executable.sh') {
58+
symlinkDocRaw = resource.documentation;
59+
}
60+
}
61+
const realDoc = typeof realDocRaw === 'string' ? realDocRaw : (realDocRaw && 'value' in realDocRaw ? realDocRaw.value : undefined);
62+
const symlinkDoc = typeof symlinkDocRaw === 'string' ? symlinkDocRaw : (symlinkDocRaw && 'value' in symlinkDocRaw ? symlinkDocRaw.value : undefined);
63+
64+
const realPath = path.join(fixtureDir, 'real-executable.sh');
65+
const symlinkPath = path.join(fixtureDir, 'symlink-executable.sh');
66+
strictEqual(realDoc, realPath);
67+
strictEqual(symlinkDoc, `${symlinkPath} -> ${realPath}`);
68+
});
69+
}
3470
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
echo "real executable"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
real-executable.sh

0 commit comments

Comments
 (0)