Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,15 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
return undefined;
}
}
} else {
// `./` by itself means the current directory, use cwd directly to avoid
// trailing slash issues with URI.joinPath on some remote file systems.
try {
await this._fileService.stat(cwd);
lastWordFolderResource = cwd;
} catch {
return undefined;
}
}
}

Expand Down Expand Up @@ -368,7 +377,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
case 'tilde': {
const home = this._getHomeDir(useWindowsStylePath, capabilities);
if (home) {
lastWordFolderResource = URI.joinPath(URI.file(home), lastWordFolder.slice(1).replaceAll('\\ ', ' '));
lastWordFolderResource = URI.joinPath(createUriFromLocalPath(cwd, home), lastWordFolder.slice(1).replaceAll('\\ ', ' '));
}
if (!lastWordFolderResource) {
// Use less strong wording here as it's not as strong of a concept on Windows
Expand All @@ -381,9 +390,9 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
}
case 'absolute': {
if (shellType === WindowsShellType.GitBash) {
lastWordFolderResource = URI.file(gitBashToWindowsPath(lastWordFolder, this._processEnv.SystemDrive));
lastWordFolderResource = createUriFromLocalPath(cwd, gitBashToWindowsPath(lastWordFolder, this._processEnv.SystemDrive));
} else {
lastWordFolderResource = URI.file(lastWordFolder.replaceAll('\\ ', ' '));
lastWordFolderResource = createUriFromLocalPath(cwd, lastWordFolder.replaceAll('\\ ', ' '));
}
break;
}
Expand Down Expand Up @@ -549,7 +558,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
const cdPathEntries = cdPath.split(useWindowsStylePath ? ';' : ':');
for (const cdPathEntry of cdPathEntries) {
try {
const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true });
const fileStat = await this._fileService.resolve(createUriFromLocalPath(cwd, cdPathEntry), { resolveSingleChildDescendants: true });
if (fileStat?.children) {
for (const child of fileStat.children) {
if (!child.isDirectory) {
Expand Down Expand Up @@ -610,7 +619,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
let homeResource: URI | string | undefined;
const home = this._getHomeDir(useWindowsStylePath, capabilities);
if (home) {
homeResource = URI.joinPath(URI.file(home), lastWordFolder.slice(1).replaceAll('\\ ', ' '));
homeResource = createUriFromLocalPath(cwd, home);
}
if (!homeResource) {
// Use less strong wording here as it's not as strong of a concept on Windows
Expand Down Expand Up @@ -685,3 +694,15 @@ function getIsAbsolutePath(shellType: TerminalShellType | undefined, pathSeparat
}
return useWindowsStylePath ? /^[a-zA-Z]:[\\\/]/.test(lastWord) : lastWord.startsWith(pathSeparator);
}

/**
* Creates a URI from an absolute path, preserving the scheme and authority from the cwd.
* For local file:// URIs, uses URI.file() which handles Windows path normalization.
* For remote URIs (e.g., vscode-remote://wsl+Ubuntu), preserves the remote context.
*/
function createUriFromLocalPath(cwd: URI, absolutePath: string): URI {
if (cwd.scheme === 'file') {
return URI.file(absolutePath);
}
return cwd.with({ path: absolutePath });
}
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,87 @@ suite('TerminalCompletionService', () => {
});
});
}
if (!isWindows) {
suite('remote file completion (e.g. WSL)', () => {
const remoteAuthority = 'wsl+Ubuntu';
const remoteTestEnv: IProcessEnvironment = {
HOME: '/home/remoteuser',
USERPROFILE: '/home/remoteuser'
};

test('/absolute/path should preserve remote authority', async () => {
terminalCompletionService.processEnv = remoteTestEnv;
const resourceOptions: TerminalCompletionResourceOptions = {
cwd: URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser' }),
showDirectories: true,
pathSeparator: '/'
};
validResources = [
URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home' }),
URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser' }),
];
childResources = [
{ resource: URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser' }), isDirectory: true },
];
const result = await terminalCompletionService.resolveResources(resourceOptions, '/home/', 6, provider, capabilities);

// Check that results exist and have the correct scheme/authority
assert.ok(result && result.length > 0, 'Should return completions for remote absolute path');
// Verify completions contain paths resolved via the remote file service (not local file://)
const absoluteCompletion = result?.find(c => c.label === '/home/');
assert.ok(absoluteCompletion, 'Should have absolute path completion');
assert.ok(absoluteCompletion.detail?.includes('/home/'), 'Detail should show remote path');
});

test('~/ should preserve remote authority for tilde expansion', async () => {
terminalCompletionService.processEnv = remoteTestEnv;
const resourceOptions: TerminalCompletionResourceOptions = {
cwd: URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser/project' }),
showDirectories: true,
pathSeparator: '/'
};
validResources = [
URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser' }),
URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser/project' }),
];
childResources = [
{ resource: URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser/Documents' }), isDirectory: true },
{ resource: URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser/project' }), isDirectory: true },
];
const result = await terminalCompletionService.resolveResources(resourceOptions, '~/', 2, provider, capabilities);

// Check that results exist for remote tilde path
assert.ok(result && result.length > 0, 'Should return completions for remote tilde path');
// Verify the tilde path was resolved using the remote home directory
const documentsCompletion = result?.find(c => c.detail?.includes('Documents'));
assert.ok(documentsCompletion, 'Should find Documents folder from remote home');
});

test('./relative should preserve remote authority for relative paths', async () => {
terminalCompletionService.processEnv = remoteTestEnv;
const resourceOptions: TerminalCompletionResourceOptions = {
cwd: URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser/project' }),
showDirectories: true,
pathSeparator: '/'
};
validResources = [
URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser/project' }),
];
childResources = [
{ resource: URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser/project/src' }), isDirectory: true },
{ resource: URI.from({ scheme: 'vscode-remote', authority: remoteAuthority, path: '/home/remoteuser/project/docs' }), isDirectory: true },
];
const result = await terminalCompletionService.resolveResources(resourceOptions, './', 2, provider, capabilities);

// Check that results exist for remote relative path
assert.ok(result && result.length > 0, 'Should return completions for remote relative path');
// Verify completions are from the remote filesystem
const srcCompletion = result?.find(c => c.detail?.includes('/home/remoteuser/project/src'));
assert.ok(srcCompletion, 'Should find src folder completion with remote path in detail');
});
});
}

suite('completion label escaping', () => {
test('| should escape special characters in file/folder names for POSIX shells', async () => {
const resourceOptions: TerminalCompletionResourceOptions = {
Expand Down
Loading