diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 95654ffe418..599f4b93849 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -413,6 +413,13 @@ export async function resolveCwdFromCurrentCommandString(currentCommandString: s } const relativeFolder = lastSlashIndex === -1 ? '' : prefix.slice(0, lastSlashIndex); + // Don't pre-resolve paths with .. segments - let the completion service handle those + // to avoid double-navigation (e.g., typing ../ would resolve cwd to parent here, + // then completion service would navigate up again from the already-parent cwd) + if (relativeFolder.includes('..')) { + return undefined; + } + // Use vscode.Uri.joinPath for path resolution const resolvedUri = vscode.Uri.joinPath(currentCwd, relativeFolder); diff --git a/extensions/terminal-suggest/src/test/completions/cd.test.ts b/extensions/terminal-suggest/src/test/completions/cd.test.ts index a40d5ee2103..cee2f62e55a 100644 --- a/extensions/terminal-suggest/src/test/completions/cd.test.ts +++ b/extensions/terminal-suggest/src/test/completions/cd.test.ts @@ -37,7 +37,8 @@ export const cdTestSuiteSpec: ISuiteSpec = { // Relative directories (changes cwd due to /) { input: 'cd child/|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdChild } }, - { input: 'cd ../|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } }, - { input: 'cd ../sibling|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } }, + // Paths with .. are handled by the completion service to avoid double-navigation (no cwd resolution) + { input: 'cd ../|', expectedCompletions, expectedResourceRequests: { type: 'folders' } }, + { input: 'cd ../sibling|', expectedCompletions, expectedResourceRequests: { type: 'folders' } }, ] }; diff --git a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts index e08b755e60a..1b06db30546 100644 --- a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts +++ b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts @@ -84,8 +84,9 @@ export const lsTestSuiteSpec: ISuiteSpec = { // Relative directories (changes cwd due to /) { input: 'ls child/|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdChild } }, - { input: 'ls ../|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdParent } }, - { input: 'ls ../sibling|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdParent } }, + // Paths with .. are handled by the completion service to avoid double-navigation (no cwd resolution) + { input: 'ls ../|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both' } }, + { input: 'ls ../sibling|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both' } }, ] }; diff --git a/extensions/terminal-suggest/src/test/helpers.ts b/extensions/terminal-suggest/src/test/helpers.ts index a4101a49194..b5080535fcf 100644 --- a/extensions/terminal-suggest/src/test/helpers.ts +++ b/extensions/terminal-suggest/src/test/helpers.ts @@ -21,7 +21,7 @@ export interface ITestSpec { input: string; expectedResourceRequests?: { type: 'files' | 'folders' | 'both'; - cwd: Uri; + cwd?: Uri; }; expectedCompletions?: (string | ICompletionResource)[]; } diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 793cc9a634b..57749d2df68 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -85,7 +85,7 @@ suite('Terminal Suggest', () => { let expectedString = testSpec.expectedCompletions ? `[${testSpec.expectedCompletions.map(e => `'${e}'`).join(', ')}]` : '[]'; if (testSpec.expectedResourceRequests) { expectedString += ` + ${testSpec.expectedResourceRequests.type}`; - if (testSpec.expectedResourceRequests.cwd.fsPath !== testPaths.cwd.fsPath) { + if (testSpec.expectedResourceRequests.cwd && testSpec.expectedResourceRequests.cwd.fsPath !== testPaths.cwd.fsPath) { expectedString += ` @ ${basename(testSpec.expectedResourceRequests.cwd.fsPath)}/`; } } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index bf819a3c6d7..c8bbbaa6d2d 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -591,7 +591,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (lastWordFolder.length > 0) { label = addPathRelativePrefix(lastWordFolder + label, resourceOptions, lastWordFolderHasDotPrefix); } - const parentDir = URI.joinPath(cwd, '..' + resourceOptions.pathSeparator); + const parentDir = URI.joinPath(lastWordFolderResource, '..' + resourceOptions.pathSeparator); resourceCompletions.push({ label, provider, 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 f6ba7c0cc09..00367e498ad 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 @@ -198,6 +198,32 @@ suite('TerminalCompletionService', () => { ], { replacementRange: [1, 3] }); }); + test('../| should return parent folder completions', async () => { + // Scenario: cwd is /parent/folder1, sibling is /parent/folder2 + // When typing ../, should see contents of /parent/ (folder1 and folder2) + validResources = [ + URI.parse('file:///parent/folder1'), + URI.parse('file:///parent'), + ]; + childResources = [ + { resource: URI.parse('file:///parent/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///parent/folder2/'), isDirectory: true }, + ]; + const resourceOptions: TerminalCompletionResourceOptions = { + cwd: URI.parse('file:///parent/folder1'), + showDirectories: true, + pathSeparator + }; + const result = await terminalCompletionService.resolveResources(resourceOptions, '../', 3, provider, capabilities); + + assertCompletions(result, [ + { label: '../', detail: '/parent/' }, + { label: '../folder1/', detail: '/parent/folder1/' }, + { label: '../folder2/', detail: '/parent/folder2/' }, + { label: '../../', detail: '/' }, + ], { replacementRange: [0, 3] }); + }); + test('cd ./| should return folder completions', async () => { const resourceOptions: TerminalCompletionResourceOptions = { cwd: URI.parse('file:///test'), @@ -564,7 +590,8 @@ suite('TerminalCompletionService', () => { assertCompletions(result, [ { label: './test/', detail: '/test/test/' }, { label: './test/inner/', detail: '/test/test/inner/' }, - { label: './test/../', detail: '/' } + // ../` from the viewed folder (/test/test/) goes to /test/, not / + { label: './test/../', detail: '/test/' } ], { replacementRange: [0, 5] }); }); test('test/| should normalize current and parent folders', async () => {