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

feat: autocomplete choices for options #2018

Merged
merged 2 commits into from Sep 5, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/argsert.ts
Expand Up @@ -73,7 +73,7 @@ export function argsert(
position += 1;
});
} catch (err) {
console.warn(err.stack);
console.warn((err as Error).stack);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋 hi all, here my 50cent, sorry!

The scope of usage unknown instead of any is to prevent fatal errors when catch receive something what is not an Error.

"errorString".stack > fatal error
to avoid this i suggest to add a check if err is instance of Error like suggested by documentation instead of cast err as Error

https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/#use-unknown-catch-variables

}
}

Expand Down
51 changes: 49 additions & 2 deletions lib/completion.ts
Expand Up @@ -68,6 +68,7 @@ export class Completion implements CompletionInstance {

this.commandCompletions(completions, args, current);
this.optionCompletions(completions, args, argv, current);
this.choicesCompletions(completions, args, argv, current);
done(null, completions);
}

Expand All @@ -82,7 +83,8 @@ export class Completion implements CompletionInstance {
.getContext().commands;
if (
!current.match(/^-/) &&
parentCommands[parentCommands.length - 1] !== current
parentCommands[parentCommands.length - 1] !== current &&
!this.previousArgHasChoices(args)
) {
this.usage.getCommands().forEach(usageCommand => {
const commandName = parseCommand(usageCommand[0]).cmd;
Expand All @@ -105,7 +107,10 @@ export class Completion implements CompletionInstance {
argv: Arguments,
current: string
) {
if (current.match(/^-/) || (current === '' && completions.length === 0)) {
if (
(current.match(/^-/) || (current === '' && completions.length === 0)) &&
!this.previousArgHasChoices(args)
) {
const options = this.yargs.getOptions();
const positionalKeys =
this.yargs.getGroups()[this.usage.getPositionalGroupName()] || [];
Expand All @@ -129,6 +134,48 @@ export class Completion implements CompletionInstance {
}
}

private choicesCompletions(
completions: string[],
args: string[],
argv: Arguments,
current: string
) {
if (this.previousArgHasChoices(args)) {
const choices = this.getPreviousArgChoices(args);
if (choices && choices.length > 0) {
completions.push(...choices);
}
}
}

private getPreviousArgChoices(args: string[]): string[] | void {
if (args.length < 1) return; // no args
let previousArg = args[args.length - 1];
let filter = '';
// use second to last argument if the last one is not an option starting with --
if (!previousArg.startsWith('--') && args.length > 1) {
filter = previousArg; // use last arg as filter for choices
previousArg = args[args.length - 2];
}
if (!previousArg.startsWith('--')) return; // still no valid arg, abort
const previousArgKey = previousArg.replace(/-/g, '');

const options = this.yargs.getOptions();
if (
Object.keys(options.key).some(key => key === previousArgKey) &&
Array.isArray(options.choices[previousArgKey])
) {
return options.choices[previousArgKey].filter(
choice => !filter || choice.startsWith(filter)
);
}
}

private previousArgHasChoices(args: string[]): boolean {
const choices = this.getPreviousArgChoices(args);
return choices !== undefined && choices.length > 0;
}

private argsContainKey(
args: string[],
argv: Arguments,
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/maybe-async-result.ts
Expand Up @@ -19,7 +19,7 @@ export function maybeAsyncResult<T>(
? result.then((result: T) => resultHandler(result))
: resultHandler(result);
} catch (err) {
return errorHandler(err);
return errorHandler(err as Error);
}
}

Expand Down
77 changes: 77 additions & 0 deletions test/completion.cjs
Expand Up @@ -296,6 +296,83 @@ describe('Completion', () => {
r.logs.should.include('--foo');
r.logs.should.not.include('bar');
});

it('completes choices if previous option requires a choice', () => {
process.env.SHELL = '/bin/bash';
const r = checkUsage(() => {
return yargs([
'./completion',
'--get-yargs-completions',
'./completion',
'--fruit',
])
.options({
fruit: {
describe: 'fruit option',
choices: ['apple', 'banana', 'pear'],
},
amount: {describe: 'amount', type: 'number'},
})
.completion('completion', false).argv;
});

r.logs.should.have.length(3);
r.logs.should.include('apple');
r.logs.should.include('banana');
r.logs.should.include('pear');
});

it('completes choices if previous option requires a choice and space has been entered', () => {
process.env.SHELL = '/bin/bash';
const r = checkUsage(() => {
return yargs([
'./completion',
'--get-yargs-completions',
'./completion',
'--fruit',
'',
])
.options({
fruit: {
describe: 'fruit option',
choices: ['apple', 'banana', 'pear'],
},
amount: {describe: 'amount', type: 'number'},
})
.completion('completion', false).argv;
});

r.logs.should.have.length(3);
r.logs.should.include('apple');
r.logs.should.include('banana');
r.logs.should.include('pear');
});

it('completes choices if previous option requires a choice and a partial choice has been entered', () => {
process.env.SHELL = '/bin/bash';
const r = checkUsage(() => {
return yargs([
'./completion',
'--get-yargs-completions',
'./completion',
'--fruit',
'ap',
])
.options({
fruit: {
describe: 'fruit option',
choices: ['apple', 'banana', 'pear'],
},
amount: {describe: 'amount', type: 'number'},
})
.completion('completion', false).argv;
});

r.logs.should.have.length(1);
r.logs.should.include('apple');
r.logs.should.not.include('banana');
r.logs.should.not.include('pear');
});
});

describe('generateCompletionScript()', () => {
Expand Down