From 7fe56a59b5ebcc231bdc3cce4efa60a9ccf85bf3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 07:41:41 -0800 Subject: [PATCH 1/5] Support CDPATH in terminal suggest Fixes #239401 --- .../browser/terminalCompletionService.ts | 47 +++++++++++++++---- .../suggest/browser/terminalSuggestAddon.ts | 2 +- .../common/terminalSuggestConfiguration.ts | 13 +++++ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 36d4de75fcd4b..18ecd71882b76 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -12,6 +12,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { TerminalCapability, type ITerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { GeneralShellType, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js'; import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js'; @@ -82,7 +83,7 @@ export interface ITerminalCompletionService { _serviceBrand: undefined; readonly providers: IterableIterator; registerTerminalCompletionProvider(extensionIdentifier: string, id: string, provider: ITerminalCompletionProvider, ...triggerCharacters: string[]): IDisposable; - provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise; + provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise; } export class TerminalCompletionService extends Disposable implements ITerminalCompletionService { @@ -101,7 +102,8 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } } - constructor(@IConfigurationService private readonly _configurationService: IConfigurationService, + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, @IFileService private readonly _fileService: IFileService ) { super(); @@ -127,7 +129,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo }); } - async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise { + async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise { if (!this._providers || !this._providers.values || cursorPosition < 0) { return undefined; } @@ -153,7 +155,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (skipExtensionCompletions) { providers = providers.filter(p => p.isBuiltin); - return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); + return this._collectCompletions(providers, shellType, promptValue, cursorPosition, capabilities, token); } const providerConfig: { [key: string]: boolean } = this._configurationService.getValue(TerminalSuggestSettingId.Providers); @@ -166,10 +168,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return; } - return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); + return this._collectCompletions(providers, shellType, promptValue, cursorPosition, capabilities, token); } - private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, token: CancellationToken): Promise { + private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, capabilities: ITerminalCapabilityStore, token: CancellationToken): Promise { const completionPromises = providers.map(async provider => { if (provider.shellTypes && !provider.shellTypes.includes(shellType)) { return undefined; @@ -193,7 +195,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return completionItems; } if (completions.resourceRequestConfig) { - const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition, provider.id); + const resourceCompletions = await this.resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition, provider.id, capabilities); if (resourceCompletions) { completionItems.push(...resourceCompletions); } @@ -206,7 +208,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo return results.filter(result => !!result).flat(); } - async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, provider: string): Promise { + async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, provider: string, capabilities: ITerminalCapabilityStore): Promise { if (resourceRequestConfig.shouldNormalizePrefix) { // for tests, make sure the right path separator is used promptValue = promptValue.replaceAll(/[\\/]/g, resourceRequestConfig.pathSeparator); @@ -349,6 +351,35 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } } + // Support $CDPATH specially for the `cd` command only + if (promptValue.startsWith('cd ')) { + const config = this._configurationService.getValue(TerminalSuggestSettingId.CdPath); + if (config === 'absolute' || config === 'relative') { + const cdPath = capabilities.get(TerminalCapability.ShellEnvDetection)?.env?.get('CDPATH'); + if (cdPath) { + const cdPathEntries = cdPath.split(useForwardSlash ? ';' : ':'); + for (const cdPathEntry of cdPathEntries) { + const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); + if (fileStat?.children) { + for (const child of fileStat.children) { + const label = config === 'relative' ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); + resourceCompletions.push({ + label, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: child.isDirectory, + isFile: child.isFile, + detail: `CDPATH`, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } + } + } + } + } + } + // Add parent directory to the bottom of the list because it's not as useful as other suggestions // // For example: diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 1af621de8cf1b..599199ad9e877 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -169,7 +169,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest }; this._requestedCompletionsIndex = this._currentPromptInputState.cursorIndex; - const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.prefix, this._currentPromptInputState.cursorIndex, this.shellType, token, doNotRequestExtensionCompletions); + const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.prefix, this._currentPromptInputState.cursorIndex, this.shellType, this._capabilities, token, doNotRequestExtensionCompletions); if (!providedCompletions?.length || token.isCancellationRequested) { return; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index e11d8d0587c2b..664892c681f1d 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -17,6 +17,7 @@ export const enum TerminalSuggestSettingId { WindowsExecutableExtensions = 'terminal.integrated.suggest.windowsExecutableExtensions', Providers = 'terminal.integrated.suggest.providers', ShowStatusBar = 'terminal.integrated.suggest.showStatusBar', + CdPath = 'terminal.integrated.suggest.cdPath', } export const windowsDefaultExecutableExtensions: string[] = [ @@ -134,6 +135,18 @@ export const terminalSuggestConfiguration: IStringDictionary Date: Sat, 1 Feb 2025 07:46:25 -0800 Subject: [PATCH 2/5] Handle errors and improve detail when config is absolute --- .../browser/terminalCompletionService.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 18ecd71882b76..a3bc6a1e3bc93 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -359,22 +359,26 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (cdPath) { const cdPathEntries = cdPath.split(useForwardSlash ? ';' : ':'); for (const cdPathEntry of cdPathEntries) { - const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); - if (fileStat?.children) { - for (const child of fileStat.children) { - const label = config === 'relative' ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); - resourceCompletions.push({ - label, - provider, - kind: TerminalCompletionItemKind.Folder, - isDirectory: child.isDirectory, - isFile: child.isFile, - detail: `CDPATH`, - replacementIndex: cursorPosition - lastWord.length, - replacementLength: lastWord.length - }); + try { + const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); + if (fileStat?.children) { + for (const child of fileStat.children) { + const useRelative = config === 'relative'; + const label = useRelative ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); + const detail = useRelative ? `CDPATH ${getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator)}` : `CDPATH`; + resourceCompletions.push({ + label, + provider, + kind: TerminalCompletionItemKind.Folder, + isDirectory: child.isDirectory, + isFile: child.isFile, + detail, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: lastWord.length + }); + } } - } + } catch { /* ignore */ } } } } From 732c306210defed1e0ff2489d260821a84a9546d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 07:49:57 -0800 Subject: [PATCH 3/5] Fix compile in completion tests --- .../browser/terminalCompletionService.test.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) 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 7cdac7c3d6745..bdc89e39c17c5 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 @@ -13,6 +13,7 @@ import { TestInstantiationService } from '../../../../../../platform/instantiati import { createFileStat } from '../../../../../test/common/workbenchTestServices.js'; import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; const pathSeparator = isWindows ? '\\' : '/'; @@ -77,7 +78,7 @@ suite('TerminalCompletionService', () => { suite('resolveResources should return undefined', () => { test('if cwd is not provided', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, new TerminalCapabilityStore()); assert(!result); }); @@ -87,7 +88,7 @@ suite('TerminalCompletionService', () => { pathSeparator }; validResources = [URI.parse('file:///test')]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, new TerminalCapabilityStore()); assert(!result); }); }); @@ -107,7 +108,7 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '.', detail: '/test/' }, @@ -123,7 +124,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -139,7 +140,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -154,7 +155,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -170,7 +171,7 @@ suite('TerminalCompletionService', () => { shouldNormalizePrefix: true, env: { HOME: '/test/' } }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '~/', detail: '/test/' }, @@ -197,7 +198,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -217,7 +218,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -245,7 +246,7 @@ suite('TerminalCompletionService', () => { }; validResources = [URI.parse('file:///usr')]; childResources = []; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '/usr/', detail: '/' }, @@ -267,7 +268,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///C:/test/anotherFolder/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '.\\', detail: 'C:\\test\\' }, @@ -290,7 +291,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/foldera/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -313,7 +314,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, { resource: URI.parse('file:///test/folder2/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: '.', detail: '/test/' }, @@ -335,7 +336,7 @@ suite('TerminalCompletionService', () => { resource: URI.parse(`file:///test/folder${i}/`), isDirectory: true })); - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, new TerminalCapabilityStore()); assert(result); // includes the 1000 folders + ./ and ./../ @@ -356,7 +357,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, { resource: URI.parse('file:///test/folder2/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider, new TerminalCapabilityStore()); assertCompletions(result, [ { label: './', detail: '/test/' }, From 74974c9bb3091d3aca8c20999c82e1ba2ccc754f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 08:07:40 -0800 Subject: [PATCH 4/5] Add tests for CDPATH feature --- .../browser/terminalCompletionService.ts | 3 + .../browser/terminalCompletionService.test.ts | 89 +++++++++++++++---- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index a3bc6a1e3bc93..ea42bf6b6f30d 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -363,6 +363,9 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo const fileStat = await this._fileService.resolve(URI.file(cdPathEntry), { resolveSingleChildDescendants: true }); if (fileStat?.children) { for (const child of fileStat.children) { + if (!child.isDirectory) { + continue; + } const useRelative = config === 'relative'; const label = useRelative ? basename(child.resource.fsPath) : getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator); const detail = useRelative ? `CDPATH ${getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator)}` : `CDPATH`; 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 bdc89e39c17c5..92d723eb8d9a5 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 @@ -14,6 +14,8 @@ import { createFileStat } from '../../../../../test/common/workbenchTestServices import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; +import { ShellEnvDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/shellEnvDetectionCapability.js'; +import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; const pathSeparator = isWindows ? '\\' : '/'; @@ -50,6 +52,7 @@ suite('TerminalCompletionService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; + let capabilities: TerminalCapabilityStore; let validResources: URI[]; let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean }[]; let terminalCompletionService: TerminalCompletionService; @@ -67,18 +70,20 @@ suite('TerminalCompletionService', () => { return createFileStat(resource); }, async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise { - return createFileStat(resource, undefined, undefined, undefined, childResources); + const children = childResources.filter(e => e.resource.fsPath.startsWith(resource.fsPath)); + return createFileStat(resource, undefined, undefined, undefined, children); }, }); terminalCompletionService = store.add(instantiationService.createInstance(TerminalCompletionService)); validResources = []; childResources = []; + capabilities = store.add(new TerminalCapabilityStore()); }); suite('resolveResources should return undefined', () => { test('if cwd is not provided', async () => { const resourceRequestConfig: TerminalResourceRequestConfig = { pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); assert(!result); }); @@ -88,7 +93,7 @@ suite('TerminalCompletionService', () => { pathSeparator }; validResources = [URI.parse('file:///test')]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); assert(!result); }); }); @@ -108,7 +113,7 @@ suite('TerminalCompletionService', () => { foldersRequested: true, pathSeparator }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 1, provider, capabilities); assertCompletions(result, [ { label: '.', detail: '/test/' }, @@ -124,7 +129,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 3, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -140,7 +145,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./', 5, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -155,7 +160,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ./f', 6, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -171,7 +176,7 @@ suite('TerminalCompletionService', () => { shouldNormalizePrefix: true, env: { HOME: '/test/' } }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ~/', 5, provider, capabilities); assertCompletions(result, [ { label: '~/', detail: '/test/' }, @@ -198,7 +203,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -218,7 +223,7 @@ suite('TerminalCompletionService', () => { pathSeparator, shouldNormalizePrefix: true }; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './h', 3, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -246,7 +251,7 @@ suite('TerminalCompletionService', () => { }; validResources = [URI.parse('file:///usr')]; childResources = []; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '/usr/', 5, provider, capabilities); assertCompletions(result, [ { label: '/usr/', detail: '/' }, @@ -268,7 +273,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///C:/test/anotherFolder/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '.\\folder', 8, provider, capabilities); assertCompletions(result, [ { label: '.\\', detail: 'C:\\test\\' }, @@ -291,7 +296,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/foldera/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder', 8, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -314,7 +319,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, { resource: URI.parse('file:///test/folder2/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, '', 0, provider, capabilities); assertCompletions(result, [ { label: '.', detail: '/test/' }, @@ -336,7 +341,7 @@ suite('TerminalCompletionService', () => { resource: URI.parse(`file:///test/folder${i}/`), isDirectory: true })); - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './', 2, provider, capabilities); assert(result); // includes the 1000 folders + ./ and ./../ @@ -357,7 +362,7 @@ suite('TerminalCompletionService', () => { { resource: URI.parse('file:///test/folder1/'), isDirectory: true }, { resource: URI.parse('file:///test/folder2/'), isDirectory: true } ]; - const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider, new TerminalCapabilityStore()); + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, './folder1', 10, provider, capabilities); assertCompletions(result, [ { label: './', detail: '/test/' }, @@ -367,4 +372,56 @@ suite('TerminalCompletionService', () => { ], { replacementIndex: 1, replacementLength: 9 }); }); }); + + suite('cdpath', () => { + let shellEnvDetection: ShellEnvDetectionCapability; + + setup(() => { + validResources = [URI.parse('file:///test')]; + childResources = [ + { resource: URI.parse('file:///cdpath_value/folder1/'), isDirectory: true }, + { resource: URI.parse('file:///cdpath_value/file1.txt'), isFile: true }, + ]; + + shellEnvDetection = store.add(new ShellEnvDetectionCapability()); + shellEnvDetection.setEnvironment({ CDPATH: '/cdpath_value' }, true); + capabilities.add(TerminalCapability.ShellEnvDetection, shellEnvDetection); + }); + + test('cd | should show paths from $CDPATH (relative)', async () => { + configurationService.setUserConfiguration('terminal.integrated.suggest.cdPath', 'relative'); + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + filesRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); + + assertCompletions(result, [ + { label: '.', detail: '/test/' }, + { label: 'folder1', detail: 'CDPATH /cdpath_value/folder1/' }, + { label: '../', detail: '/' }, + ], { replacementIndex: 3, replacementLength: 0 }); + }); + + test('cd | should show paths from $CDPATH (absolute)', async () => { + configurationService.setUserConfiguration('terminal.integrated.suggest.cdPath', 'absolute'); + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse('file:///test'), + foldersRequested: true, + filesRequested: true, + pathSeparator, + shouldNormalizePrefix: true + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); + + assertCompletions(result, [ + { label: '.', detail: '/test/' }, + { label: '/cdpath_value/folder1/', detail: 'CDPATH' }, + { label: '../', detail: '/' }, + ], { replacementIndex: 3, replacementLength: 0 }); + }); + }); }); From beb0595efe5aba2b3a35ef3930dd62c5355ddd84 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 3 Feb 2025 02:44:17 -0800 Subject: [PATCH 5/5] Add test for parsing out multiple CD PATHs and files --- .../browser/terminalCompletionService.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) 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 92d723eb8d9a5..0c2ed66de936a 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 @@ -423,5 +423,49 @@ suite('TerminalCompletionService', () => { { label: '../', detail: '/' }, ], { replacementIndex: 3, replacementLength: 0 }); }); + + test('cd | should support pulling from multiple paths in $CDPATH', async () => { + configurationService.setUserConfiguration('terminal.integrated.suggest.cdPath', 'relative'); + const pathPrefix = isWindows ? 'c:\\' : '/'; + const delimeter = isWindows ? ';' : ':'; + const separator = isWindows ? '\\' : '/'; + shellEnvDetection.setEnvironment({ CDPATH: `${pathPrefix}cdpath1_value${delimeter}${pathPrefix}cdpath2_value${separator}inner_dir` }, true); + + const uriPathPrefix = isWindows ? 'file:///c:/' : 'file:///'; + validResources = [ + URI.parse(`${uriPathPrefix}test`), + URI.parse(`${uriPathPrefix}cdpath1_value`), + URI.parse(`${uriPathPrefix}cdpath2_value`), + URI.parse(`${uriPathPrefix}cdpath2_value/inner_dir`) + ]; + childResources = [ + { resource: URI.parse(`${uriPathPrefix}cdpath1_value/folder1/`), isDirectory: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath1_value/folder2/`), isDirectory: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath1_value/file1.txt`), isFile: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath2_value/inner_dir/folder1/`), isDirectory: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath2_value/inner_dir/folder2/`), isDirectory: true }, + { resource: URI.parse(`${uriPathPrefix}cdpath2_value/inner_dir/file1.txt`), isFile: true }, + ]; + + const resourceRequestConfig: TerminalResourceRequestConfig = { + cwd: URI.parse(`${uriPathPrefix}test`), + foldersRequested: true, + filesRequested: true, + pathSeparator, + // TODO: This is a hack to make the test pass, clean up when https://github.com/microsoft/vscode/issues/239411 is done + shouldNormalizePrefix: !isWindows + }; + const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'cd ', 3, provider, capabilities); + + const finalPrefix = isWindows ? 'C:\\' : '/'; + assertCompletions(result, [ + { label: '.', detail: `${finalPrefix}test/` }, + { label: 'folder1', detail: `CDPATH ${finalPrefix}cdpath1_value/folder1/` }, + { label: 'folder2', detail: `CDPATH ${finalPrefix}cdpath1_value/folder2/` }, + { label: 'folder1', detail: `CDPATH ${finalPrefix}cdpath2_value/inner_dir/folder1/` }, + { label: 'folder2', detail: `CDPATH ${finalPrefix}cdpath2_value/inner_dir/folder2/` }, + { label: '../', detail: finalPrefix }, + ], { replacementIndex: 3, replacementLength: 0 }); + }); }); });