Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support CDPATH in terminal suggest #239406

Merged
merged 6 commits into from
Feb 3, 2025
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 @@ -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';
Expand Down Expand Up @@ -82,7 +83,7 @@ export interface ITerminalCompletionService {
_serviceBrand: undefined;
readonly providers: IterableIterator<ITerminalCompletionProvider>;
registerTerminalCompletionProvider(extensionIdentifier: string, id: string, provider: ITerminalCompletionProvider, ...triggerCharacters: string[]): IDisposable;
provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise<ITerminalCompletion[] | undefined>;
provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise<ITerminalCompletion[] | undefined>;
}

export class TerminalCompletionService extends Disposable implements ITerminalCompletionService {
Expand All @@ -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();
Expand All @@ -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<ITerminalCompletion[] | undefined> {
async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, capabilities: ITerminalCapabilityStore, token: CancellationToken, triggerCharacter?: boolean, skipExtensionCompletions?: boolean): Promise<ITerminalCompletion[] | undefined> {
if (!this._providers || !this._providers.values || cursorPosition < 0) {
return undefined;
}
Expand All @@ -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);
Expand All @@ -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<ITerminalCompletion[] | undefined> {
private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, capabilities: ITerminalCapabilityStore, token: CancellationToken): Promise<ITerminalCompletion[] | undefined> {
const completionPromises = providers.map(async provider => {
if (provider.shellTypes && !provider.shellTypes.includes(shellType)) {
return undefined;
Expand All @@ -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);
}
Expand All @@ -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<ITerminalCompletion[] | undefined> {
async resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number, provider: string, capabilities: ITerminalCapabilityStore): Promise<ITerminalCompletion[] | undefined> {
if (resourceRequestConfig.shouldNormalizePrefix) {
// for tests, make sure the right path separator is used
promptValue = promptValue.replaceAll(/[\\/]/g, resourceRequestConfig.pathSeparator);
Expand Down Expand Up @@ -349,6 +351,42 @@ 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) {
try {
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`;
resourceCompletions.push({
label,
provider,
kind: TerminalCompletionItemKind.Folder,
isDirectory: child.isDirectory,
isFile: child.isFile,
detail,
replacementIndex: cursorPosition - lastWord.length,
replacementLength: lastWord.length
});
}
}
} catch { /* ignore */ }
}
}
}
}

// Add parent directory to the bottom of the list because it's not as useful as other suggestions
//
// For example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -134,6 +135,18 @@ export const terminalSuggestConfiguration: IStringDictionary<IConfigurationPrope
default: true,
tags: ['experimental'],
},
[TerminalSuggestSettingId.CdPath]: {
restricted: true,
markdownDescription: localize('suggest.cdPath', "Controls whether to enable $CDPATH support which exposes children of the folders in the $CDPATH variable regardless of the current working directory. $CDPATH is expected to be semi colon-separated on Windows and colon-separated on other platforms."),
enum: ['off', 'relative', 'absolute'],
markdownEnumDescriptions: [
localize('suggest.cdPath.off', "Disable the feature."),
localize('suggest.cdPath.relative', "Enable the feature and use relative paths."),
localize('suggest.cdPath.absolute', "Enable the feature and use absolute paths. This is useful when the shell doesn't natively support `$CDPATH`."),
],
default: 'absolute',
tags: ['experimental'],
},
};


Loading
Loading