add support for file/folder terminal completions (#234289)
This commit is contained in:
Родитель
ee21e638be
Коммит
d79858c114
|
@ -20,11 +20,13 @@ function getBuiltinCommands(shell: string): string[] | undefined {
|
|||
if (cachedCommands) {
|
||||
return cachedCommands;
|
||||
}
|
||||
// fixes a bug with file/folder completions brought about by the '.' command
|
||||
const filter = (cmd: string) => cmd && cmd !== '.';
|
||||
const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell };
|
||||
switch (shellType) {
|
||||
case 'bash': {
|
||||
const bashOutput = execSync('compgen -b', options);
|
||||
const bashResult = bashOutput.split('\n').filter(cmd => cmd);
|
||||
const bashResult = bashOutput.split('\n').filter(filter);
|
||||
if (bashResult.length) {
|
||||
cachedBuiltinCommands?.set(shellType, bashResult);
|
||||
return bashResult;
|
||||
|
@ -33,7 +35,7 @@ function getBuiltinCommands(shell: string): string[] | undefined {
|
|||
}
|
||||
case 'zsh': {
|
||||
const zshOutput = execSync('printf "%s\\n" ${(k)builtins}', options);
|
||||
const zshResult = zshOutput.split('\n').filter(cmd => cmd);
|
||||
const zshResult = zshOutput.split('\n').filter(filter);
|
||||
if (zshResult.length) {
|
||||
cachedBuiltinCommands?.set(shellType, zshResult);
|
||||
return zshResult;
|
||||
|
@ -43,7 +45,7 @@ function getBuiltinCommands(shell: string): string[] | undefined {
|
|||
// TODO: ghost text in the command line prevents
|
||||
// completions from working ATM for fish
|
||||
const fishOutput = execSync('functions -n', options);
|
||||
const fishResult = fishOutput.split(', ').filter(cmd => cmd);
|
||||
const fishResult = fishOutput.split(', ').filter(filter);
|
||||
if (fishResult.length) {
|
||||
cachedBuiltinCommands?.set(shellType, fishResult);
|
||||
return fishResult;
|
||||
|
@ -64,122 +66,81 @@ function getBuiltinCommands(shell: string): string[] | undefined {
|
|||
export async function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(vscode.window.registerTerminalCompletionProvider({
|
||||
id: 'terminal-suggest',
|
||||
async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: { commandLine: string; cursorPosition: number }, token: vscode.CancellationToken): Promise<vscode.TerminalCompletionItem[] | undefined> {
|
||||
async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: { commandLine: string; cursorPosition: number }, token: vscode.CancellationToken): Promise<vscode.TerminalCompletionItem[] | vscode.TerminalCompletionList | undefined> {
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableCommands = await getCommandsInPath();
|
||||
if (!availableCommands) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Leverage shellType when available https://github.com/microsoft/vscode/issues/230165
|
||||
const shellPath = 'shellPath' in terminal.creationOptions ? terminal.creationOptions.shellPath : vscode.env.shell;
|
||||
if (!shellPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commandsInPath = await getCommandsInPath();
|
||||
const builtinCommands = getBuiltinCommands(shellPath);
|
||||
builtinCommands?.forEach(command => availableCommands.add(command));
|
||||
|
||||
const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition);
|
||||
let result: vscode.TerminalCompletionItem[] = [];
|
||||
const specs = [codeCompletionSpec, codeInsidersCompletionSpec];
|
||||
for (const spec of specs) {
|
||||
const specName = getLabel(spec);
|
||||
if (!specName || !availableCommands.has(specName)) {
|
||||
continue;
|
||||
}
|
||||
if (terminalContext.commandLine.startsWith(specName)) {
|
||||
if ('options' in codeInsidersCompletionSpec && codeInsidersCompletionSpec.options) {
|
||||
for (const option of codeInsidersCompletionSpec.options) {
|
||||
const optionLabel = getLabel(option);
|
||||
if (!optionLabel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (optionLabel.startsWith(prefix) || (prefix.length > specName.length && prefix.trim() === specName)) {
|
||||
result.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag));
|
||||
}
|
||||
if (option.args !== undefined) {
|
||||
const args = Array.isArray(option.args) ? option.args : [option.args];
|
||||
for (const arg of args) {
|
||||
if (!arg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.template) {
|
||||
// TODO: return file/folder completion items
|
||||
if (arg.template === 'filepaths') {
|
||||
// if (label.startsWith(prefix+\s*)) {
|
||||
// result.push(FilePathCompletionItem)
|
||||
// }
|
||||
} else if (arg.template === 'folders') {
|
||||
// if (label.startsWith(prefix+\s*)) {
|
||||
// result.push(FolderPathCompletionItem)
|
||||
// }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition);
|
||||
const expectedText = `${optionLabel} `;
|
||||
if (arg.suggestions?.length && precedingText.includes(expectedText)) {
|
||||
// there are specific suggestions to show
|
||||
result = [];
|
||||
const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText);
|
||||
const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length);
|
||||
for (const suggestion of arg.suggestions) {
|
||||
const suggestionLabel = getLabel(suggestion);
|
||||
if (suggestionLabel && suggestionLabel.startsWith(currentPrefix)) {
|
||||
const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' ';
|
||||
// prefix will be '' if there is a space before the cursor
|
||||
result.push(createCompletionItem(terminalContext.cursorPosition, precedingText, suggestionLabel, arg.name, hasSpaceBeforeCursor, vscode.TerminalCompletionItemKind.Argument));
|
||||
}
|
||||
}
|
||||
if (result.length) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!commandsInPath || !builtinCommands) {
|
||||
return;
|
||||
}
|
||||
const commands = [...commandsInPath, ...builtinCommands];
|
||||
|
||||
for (const command of availableCommands) {
|
||||
if (command.startsWith(prefix)) {
|
||||
result.push(createCompletionItem(terminalContext.cursorPosition, prefix, command));
|
||||
const items: vscode.TerminalCompletionItem[] = [];
|
||||
const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition);
|
||||
|
||||
const specs = [codeCompletionSpec, codeInsidersCompletionSpec];
|
||||
const specCompletions = await getCompletionItemsFromSpecs(specs, terminalContext, new Set(commands), prefix, token);
|
||||
|
||||
let filesRequested = specCompletions.filesRequested;
|
||||
let foldersRequested = specCompletions.foldersRequested;
|
||||
items.push(...specCompletions.items);
|
||||
|
||||
if (!specCompletions.specificSuggestionsProvided) {
|
||||
for (const command of commands) {
|
||||
if (command.startsWith(prefix)) {
|
||||
items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const uniqueResults = new Map<string, vscode.TerminalCompletionItem>();
|
||||
for (const item of result) {
|
||||
for (const item of items) {
|
||||
if (!uniqueResults.has(item.label)) {
|
||||
uniqueResults.set(item.label, item);
|
||||
}
|
||||
}
|
||||
return uniqueResults.size ? Array.from(uniqueResults.values()) : undefined;
|
||||
const resultItems = uniqueResults.size ? Array.from(uniqueResults.values()) : undefined;
|
||||
|
||||
// If no completions are found, the prefix is a path, and neither files nor folders
|
||||
// are going to be requested (for a specific spec's argument), show file/folder completions
|
||||
const shouldShowResourceCompletions = !resultItems?.length && prefix.match(/^[./\\ ]/) && !filesRequested && !foldersRequested;
|
||||
if (shouldShowResourceCompletions) {
|
||||
filesRequested = true;
|
||||
foldersRequested = true;
|
||||
}
|
||||
|
||||
if (filesRequested || foldersRequested) {
|
||||
return new vscode.TerminalCompletionList(resultItems, { filesRequested, foldersRequested, cwd: terminal.shellIntegration?.cwd, pathSeparator: shellPath.includes('/') ? '/' : '\\' });
|
||||
}
|
||||
return resultItems;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string | undefined {
|
||||
function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string[] | undefined {
|
||||
if (typeof spec === 'string') {
|
||||
return spec;
|
||||
return [spec];
|
||||
}
|
||||
if (typeof spec.name === 'string') {
|
||||
return spec.name;
|
||||
return [spec.name];
|
||||
}
|
||||
if (!Array.isArray(spec.name) || spec.name.length === 0) {
|
||||
return;
|
||||
}
|
||||
return spec.name[0];
|
||||
return spec.name;
|
||||
}
|
||||
|
||||
function createCompletionItem(cursorPosition: number, prefix: string, label: string, description?: string, hasSpaceBeforeCursor?: boolean, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem {
|
||||
|
@ -245,3 +206,89 @@ function getPrefix(commandLine: string, cursorPosition: number): string {
|
|||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
export function asArray<T>(x: T | T[]): T[];
|
||||
export function asArray<T>(x: T | readonly T[]): readonly T[];
|
||||
export function asArray<T>(x: T | T[]): T[] {
|
||||
return Array.isArray(x) ? x : [x];
|
||||
}
|
||||
|
||||
function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: Set<string>, prefix: string, token: vscode.CancellationToken): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; specificSuggestionsProvided: boolean } {
|
||||
let items: vscode.TerminalCompletionItem[] = [];
|
||||
let filesRequested = false;
|
||||
let foldersRequested = false;
|
||||
for (const spec of specs) {
|
||||
const specLabels = getLabel(spec);
|
||||
if (!specLabels) {
|
||||
continue;
|
||||
}
|
||||
for (const specLabel of specLabels) {
|
||||
if (!availableCommands.has(specLabel) || token.isCancellationRequested) {
|
||||
continue;
|
||||
}
|
||||
if (terminalContext.commandLine.startsWith(specLabel)) {
|
||||
if ('options' in spec && spec.options) {
|
||||
for (const option of spec.options) {
|
||||
const optionLabels = getLabel(option);
|
||||
if (!optionLabels) {
|
||||
continue;
|
||||
}
|
||||
for (const optionLabel of optionLabels) {
|
||||
if (optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) {
|
||||
items.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag));
|
||||
}
|
||||
if (!option.args) {
|
||||
continue;
|
||||
}
|
||||
const args = asArray(option.args);
|
||||
for (const arg of args) {
|
||||
if (!arg) {
|
||||
continue;
|
||||
}
|
||||
const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1);
|
||||
const expectedText = `${specLabel} ${optionLabel} `;
|
||||
if (!precedingText.includes(expectedText)) {
|
||||
continue;
|
||||
}
|
||||
if (arg.template) {
|
||||
if (arg.template === 'filepaths') {
|
||||
if (precedingText.includes(expectedText)) {
|
||||
filesRequested = true;
|
||||
}
|
||||
} else if (arg.template === 'folders') {
|
||||
if (precedingText.includes(expectedText)) {
|
||||
foldersRequested = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (arg.suggestions?.length) {
|
||||
// there are specific suggestions to show
|
||||
items = [];
|
||||
const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText);
|
||||
const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length);
|
||||
for (const suggestion of arg.suggestions) {
|
||||
const suggestionLabels = getLabel(suggestion);
|
||||
if (!suggestionLabels) {
|
||||
continue;
|
||||
}
|
||||
for (const suggestionLabel of suggestionLabels) {
|
||||
if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) {
|
||||
const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' ';
|
||||
// prefix will be '' if there is a space before the cursor
|
||||
items.push(createCompletionItem(terminalContext.cursorPosition, precedingText, suggestionLabel, arg.name, hasSpaceBeforeCursor, vscode.TerminalCompletionItemKind.Argument));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (items.length) {
|
||||
return { items, filesRequested, foldersRequested, specificSuggestionsProvided: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { items, filesRequested, foldersRequested, specificSuggestionsProvided: false };
|
||||
}
|
||||
|
||||
|
|
|
@ -1667,6 +1667,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
TerminalShellExecutionCommandLineConfidence: extHostTypes.TerminalShellExecutionCommandLineConfidence,
|
||||
TerminalCompletionItem: extHostTypes.TerminalCompletionItem,
|
||||
TerminalCompletionItemKind: extHostTypes.TerminalCompletionItemKind,
|
||||
TerminalCompletionList: extHostTypes.TerminalCompletionList,
|
||||
TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason,
|
||||
TextEdit: extHostTypes.TextEdit,
|
||||
SnippetTextEdit: extHostTypes.SnippetTextEdit,
|
||||
|
|
|
@ -84,7 +84,7 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../servic
|
|||
import * as search from '../../services/search/common/search.js';
|
||||
import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js';
|
||||
import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js';
|
||||
import { TerminalCompletionItem, TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js';
|
||||
import { TerminalCompletionItem, TerminalCompletionList, TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js';
|
||||
import * as tasks from './shared/tasks.js';
|
||||
|
||||
export interface IWorkspaceData extends IStaticWorkspaceData {
|
||||
|
@ -2430,7 +2430,7 @@ export interface ExtHostTerminalServiceShape {
|
|||
$acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void;
|
||||
$createContributedProfileTerminal(id: string, options: ICreateContributedTerminalProfileOptions): Promise<void>;
|
||||
$provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto, token: CancellationToken): Promise<SingleOrMany<TerminalQuickFix> | undefined>;
|
||||
$provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto, token: CancellationToken): Promise<TerminalCompletionItem[] | undefined>;
|
||||
$provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto, token: CancellationToken): Promise<TerminalCompletionItem[] | TerminalCompletionList | undefined>;
|
||||
}
|
||||
|
||||
export interface ExtHostTerminalShellIntegrationShape {
|
||||
|
|
|
@ -56,7 +56,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID
|
|||
getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection;
|
||||
getTerminalById(id: number): ExtHostTerminal | null;
|
||||
getTerminalIdByApiObject(apiTerminal: vscode.Terminal): number | null;
|
||||
registerTerminalCompletionProvider<T extends vscode.TerminalCompletionItem[]>(extension: IExtensionDescription, provider: vscode.TerminalCompletionProvider<vscode.TerminalCompletionItem>, ...triggerCharacters: string[]): vscode.Disposable;
|
||||
registerTerminalCompletionProvider(extension: IExtensionDescription, provider: vscode.TerminalCompletionProvider<vscode.TerminalCompletionItem>, ...triggerCharacters: string[]): vscode.Disposable;
|
||||
}
|
||||
|
||||
interface IEnvironmentVariableCollection extends vscode.EnvironmentVariableCollection {
|
||||
|
@ -746,7 +746,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
|
|||
});
|
||||
}
|
||||
|
||||
public async $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto): Promise<vscode.TerminalCompletionItem[] | undefined> {
|
||||
public async $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto): Promise<vscode.TerminalCompletionItem[] | vscode.TerminalCompletionList | undefined> {
|
||||
const token = new CancellationTokenSource().token;
|
||||
if (token.isCancellationRequested || !this.activeTerminal) {
|
||||
return undefined;
|
||||
|
|
|
@ -2145,6 +2145,41 @@ export class TerminalCompletionItem implements vscode.TerminalCompletionItem {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a collection of {@link CompletionItem completion items} to be presented
|
||||
* in the editor.
|
||||
*/
|
||||
export class TerminalCompletionList<T extends TerminalCompletionItem = TerminalCompletionItem> {
|
||||
|
||||
/**
|
||||
* Resources should be shown in the completions list
|
||||
*/
|
||||
resourceRequestConfig?: TerminalResourceRequestConfig;
|
||||
|
||||
/**
|
||||
* The completion items.
|
||||
*/
|
||||
items: T[];
|
||||
|
||||
/**
|
||||
* Creates a new completion list.
|
||||
*
|
||||
* @param items The completion items.
|
||||
* @param isIncomplete The list is not complete.
|
||||
*/
|
||||
constructor(items?: T[], resourceRequestConfig?: TerminalResourceRequestConfig) {
|
||||
this.items = items ?? [];
|
||||
this.resourceRequestConfig = resourceRequestConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TerminalResourceRequestConfig {
|
||||
filesRequested?: boolean;
|
||||
foldersRequested?: boolean;
|
||||
cwd?: vscode.Uri;
|
||||
pathSeparator: string;
|
||||
}
|
||||
|
||||
export enum TaskRevealKind {
|
||||
Always = 1,
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
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 { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
|
||||
import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js';
|
||||
|
@ -24,10 +26,47 @@ export interface ITerminalCompletion extends ISimpleCompletion {
|
|||
kind?: TerminalCompletionItemKind;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a collection of {@link CompletionItem completion items} to be presented
|
||||
* in the editor.
|
||||
*/
|
||||
export class TerminalCompletionList<ITerminalCompletion> {
|
||||
|
||||
/**
|
||||
* Resources should be shown in the completions list
|
||||
*/
|
||||
resourceRequestConfig?: TerminalResourceRequestConfig;
|
||||
|
||||
/**
|
||||
* The completion items.
|
||||
*/
|
||||
items?: ITerminalCompletion[];
|
||||
|
||||
/**
|
||||
* Creates a new completion list.
|
||||
*
|
||||
* @param items The completion items.
|
||||
* @param isIncomplete The list is not complete.
|
||||
*/
|
||||
constructor(items?: ITerminalCompletion[], resourceRequestConfig?: TerminalResourceRequestConfig) {
|
||||
this.items = items;
|
||||
this.resourceRequestConfig = resourceRequestConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TerminalResourceRequestConfig {
|
||||
filesRequested?: boolean;
|
||||
foldersRequested?: boolean;
|
||||
cwd?: URI;
|
||||
pathSeparator: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ITerminalCompletionProvider {
|
||||
id: string;
|
||||
shellTypes?: TerminalShellType[];
|
||||
provideCompletions(value: string, cursorPosition: number, token: CancellationToken): Promise<ISimpleCompletion[] | undefined>;
|
||||
provideCompletions(value: string, cursorPosition: number, token: CancellationToken): Promise<ITerminalCompletion[] | TerminalCompletionList<ITerminalCompletion> | undefined>;
|
||||
triggerCharacters?: string[];
|
||||
isBuiltin?: boolean;
|
||||
}
|
||||
|
@ -55,7 +94,9 @@ 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();
|
||||
}
|
||||
|
||||
|
@ -79,9 +120,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
|||
});
|
||||
}
|
||||
|
||||
async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean): Promise<ISimpleCompletion[] | undefined> {
|
||||
const completionItems: ISimpleCompletion[] = [];
|
||||
|
||||
async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean): Promise<ITerminalCompletion[] | undefined> {
|
||||
if (!this._providers || !this._providers.values) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -110,31 +149,93 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
|||
providers = providers.filter(p => p.isBuiltin);
|
||||
}
|
||||
|
||||
await this._collectCompletions(providers, shellType, promptValue, cursorPosition, completionItems, token);
|
||||
return completionItems.length > 0 ? completionItems : undefined;
|
||||
return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token);
|
||||
}
|
||||
|
||||
private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, completionItems: ISimpleCompletion[], token: CancellationToken) {
|
||||
private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, token: CancellationToken): Promise<ITerminalCompletion[] | undefined> {
|
||||
const completionPromises = providers.map(async provider => {
|
||||
if (provider.shellTypes && !provider.shellTypes.includes(shellType)) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
const completions: ITerminalCompletion[] | TerminalCompletionList<ITerminalCompletion> | undefined = await provider.provideCompletions(promptValue, cursorPosition, token);
|
||||
if (!completions) {
|
||||
return undefined;
|
||||
}
|
||||
const completions = await provider.provideCompletions(promptValue, cursorPosition, token);
|
||||
const devModeEnabled = this._configurationService.getValue(TerminalSettingId.DevMode);
|
||||
if (completions) {
|
||||
return completions.map(completion => {
|
||||
if (devModeEnabled && !completion.detail?.includes(provider.id)) {
|
||||
completion.detail = `(${provider.id}) ${completion.detail ?? ''}`;
|
||||
}
|
||||
return completion;
|
||||
});
|
||||
const completionItems = Array.isArray(completions) ? completions : completions.items ?? [];
|
||||
|
||||
const itemsWithModifiedLabels = completionItems.map(completion => {
|
||||
if (devModeEnabled && !completion.detail?.includes(provider.id)) {
|
||||
completion.detail = `(${provider.id}) ${completion.detail ?? ''}`;
|
||||
}
|
||||
return completion;
|
||||
});
|
||||
|
||||
if (Array.isArray(completions)) {
|
||||
return itemsWithModifiedLabels;
|
||||
}
|
||||
return [];
|
||||
if (completions.resourceRequestConfig) {
|
||||
const resourceCompletions = await this._resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition);
|
||||
if (resourceCompletions) {
|
||||
itemsWithModifiedLabels.push(...resourceCompletions);
|
||||
}
|
||||
return itemsWithModifiedLabels;
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
||||
const results = await Promise.all(completionPromises);
|
||||
results.forEach(completions => completionItems.push(...completions));
|
||||
return results.filter(result => !!result).flat();
|
||||
}
|
||||
|
||||
private async _resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number): Promise<ITerminalCompletion[] | undefined> {
|
||||
const cwd = URI.revive(resourceRequestConfig.cwd);
|
||||
const foldersRequested = resourceRequestConfig.foldersRequested ?? false;
|
||||
const filesRequested = resourceRequestConfig.filesRequested ?? false;
|
||||
if (!cwd || (!foldersRequested && !filesRequested)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceCompletions: ITerminalCompletion[] = [];
|
||||
const fileStat = await this._fileService.resolve(cwd, { resolveSingleChildDescendants: true });
|
||||
|
||||
if (!fileStat || !fileStat?.children) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const stat of fileStat.children) {
|
||||
let kind: TerminalCompletionItemKind | undefined;
|
||||
if (foldersRequested && stat.isDirectory) {
|
||||
kind = TerminalCompletionItemKind.Folder;
|
||||
}
|
||||
if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === 'file')) {
|
||||
kind = TerminalCompletionItemKind.File;
|
||||
}
|
||||
if (kind === undefined) {
|
||||
continue;
|
||||
}
|
||||
const lastWord = promptValue.substring(0, cursorPosition).split(' ').pop();
|
||||
const lastIndexOfDot = lastWord?.lastIndexOf('.') ?? -1;
|
||||
const lastIndexOfSlash = lastWord?.lastIndexOf(resourceRequestConfig.pathSeparator) ?? -1;
|
||||
let label;
|
||||
if (lastIndexOfSlash > -1) {
|
||||
label = stat.resource.fsPath.replace(cwd.fsPath, '').substring(1);
|
||||
} else if (lastIndexOfDot === -1) {
|
||||
label = '.' + stat.resource.fsPath.replace(cwd.fsPath, '');
|
||||
} else {
|
||||
label = stat.resource.fsPath.replace(cwd.fsPath, '');
|
||||
}
|
||||
|
||||
resourceCompletions.push({
|
||||
label,
|
||||
kind,
|
||||
isDirectory: kind === TerminalCompletionItemKind.Folder,
|
||||
isFile: kind === TerminalCompletionItemKind.File,
|
||||
replacementIndex: cursorPosition,
|
||||
replacementLength: label.length
|
||||
});
|
||||
}
|
||||
return resourceCompletions.length ? resourceCompletions : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -202,7 +202,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|||
normalizedLeadingLineContent = normalizePathSeparator(normalizedLeadingLineContent, this._pathSeparator);
|
||||
}
|
||||
for (const completion of completions) {
|
||||
if (!completion.icon && completion.kind) {
|
||||
if (!completion.icon && completion.kind !== undefined) {
|
||||
completion.icon = this._kindToIconMap.get(completion.kind);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,9 @@ declare module 'vscode' {
|
|||
* @param token A cancellation token.
|
||||
* @return A list of completions.
|
||||
*/
|
||||
provideTerminalCompletions(terminal: Terminal, context: TerminalCompletionContext, token: CancellationToken): ProviderResult<T[]>;
|
||||
provideTerminalCompletions(terminal: Terminal, context: TerminalCompletionContext, token: CancellationToken): ProviderResult<T[] | TerminalCompletionList<T>>;
|
||||
}
|
||||
|
||||
|
||||
export interface TerminalCompletionItem {
|
||||
/**
|
||||
* The label of the completion.
|
||||
|
@ -80,4 +79,48 @@ declare module 'vscode' {
|
|||
*/
|
||||
export function registerTerminalCompletionProvider<T extends TerminalCompletionItem>(provider: TerminalCompletionProvider<T>, ...triggerCharacters: string[]): Disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a collection of {@link TerminalCompletionItem completion items} to be presented
|
||||
* in the terminal.
|
||||
*/
|
||||
export class TerminalCompletionList<T extends TerminalCompletionItem = TerminalCompletionItem> {
|
||||
|
||||
/**
|
||||
* Resources that should be shown in the completions list for the cwd of the terminal.
|
||||
*/
|
||||
resourceRequestConfig?: TerminalResourceRequestConfig;
|
||||
|
||||
/**
|
||||
* The completion items.
|
||||
*/
|
||||
items: T[];
|
||||
|
||||
/**
|
||||
* Creates a new completion list.
|
||||
*
|
||||
* @param items The completion items.
|
||||
* @param resourceRequestConfig Indicates which resources should be shown as completions for the cwd of the terminal.
|
||||
*/
|
||||
constructor(items?: T[], resourceRequestConfig?: TerminalResourceRequestConfig);
|
||||
}
|
||||
|
||||
export interface TerminalResourceRequestConfig {
|
||||
/**
|
||||
* Show files as completion items.
|
||||
*/
|
||||
filesRequested?: boolean;
|
||||
/**
|
||||
* Show folders as completion items.
|
||||
*/
|
||||
foldersRequested?: boolean;
|
||||
/**
|
||||
* If no cwd is provided, no resources will be shown as completions.
|
||||
*/
|
||||
cwd?: Uri;
|
||||
/**
|
||||
* The path separator to use when constructing paths.
|
||||
*/
|
||||
pathSeparator: string;
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче