diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index bf819a3c6d753..eb8761b215225 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -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; + } } } @@ -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 @@ -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; } @@ -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) { @@ -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 @@ -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 }); +} diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts index f6ba7c0cc097a..23b8845644ce1 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalCompletionService.test.ts @@ -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 = {