Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/cold-eggs-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bomb.sh/tab': patch
---

feat(commander): support completions for positional arguments
41 changes: 41 additions & 0 deletions examples/demo.commander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ program
console.log('Linting files...');
});

// Command with multiple required positional arguments
program
.command('copy <source> <destination>')
.description('Copy files')
.action((source, destination) => {
console.log(`Copying ${source} to ${destination}...`);
});

// Initialize tab completion
const completion = tab(program);

Expand Down Expand Up @@ -129,5 +137,38 @@ if (devCommandInstance) {
}
}

// Positional arguments on lint command
const lintCommandInstance = completion.commands.get('lint');
if (lintCommandInstance) {
const filesArg = lintCommandInstance.arguments.get('files');
if (filesArg) {
filesArg.handler = (complete) => {
complete('main.ts', 'Main file');
complete('index.ts', 'Index file');
};
}
}

// Positional arguments on copy command
const copyCommandInstance = completion.commands.get('copy');
if (copyCommandInstance) {
const sourceArg = copyCommandInstance.arguments.get('source');
if (sourceArg) {
sourceArg.handler = (complete) => {
complete('src/', 'Source directory');
complete('dist/', 'Distribution directory');
complete('public/', 'Public assets');
};
}
const destinationArg = copyCommandInstance.arguments.get('destination');
if (destinationArg) {
destinationArg.handler = (complete) => {
complete('build/', 'Build output');
complete('release/', 'Release directory');
complete('backup/', 'Backup location');
};
}
}

// Parse command line arguments
program.parse();
23 changes: 22 additions & 1 deletion src/commander.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Command as CommanderCommand } from 'commander';
import t, { type RootCommand } from './t';
import t, { Command as TabCommand, type RootCommand } from './t';

Check warning on line 2 in src/commander.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Imports "TabCommand" are only used as type

// rawArgs is available on (just) the Commander root command, but is not included in the TypeScript types.
interface CommandWithRawArgs extends CommanderCommand {
Expand Down Expand Up @@ -165,6 +165,25 @@
registerOption(t, flags, longFlag, option.description || '', shortFlag);
}
}

processArguments(t, command);
}

function processArguments(tabCommand: TabCommand, cmd: CommanderCommand): void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The processArguments integration is very straight-forward and makes good use of Commander. Nice!

for (const arg of cmd.registeredArguments) {
const choices = arg.argChoices;
if (choices?.length) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ho ho! Support for completing choices, nice.

tabCommand.argument(
arg.name(),
(complete) => {
for (const choice of choices) complete(choice, '');
},
arg.variadic
);
} else {
tabCommand.argument(arg.name(), undefined, arg.variadic);
}
}
}

function processSubcommands(rootCommand: CommanderCommand): void {
Expand Down Expand Up @@ -198,6 +217,8 @@
);
}
}

processArguments(command, cmd);
}
}

Expand Down
108 changes: 108 additions & 0 deletions tests/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,35 @@ exports[`cli completion tests for commander > cli option value handling > should
"
`;

exports[`cli completion tests for commander > copy command argument handlers > should complete destination argument with build suggestions 1`] = `
"build/ Build output
release/ Release directory
backup/ Backup location
:4
"
`;

exports[`cli completion tests for commander > copy command argument handlers > should complete source argument with directory suggestions 1`] = `
"src/ Source directory
dist/ Distribution directory
public/ Public assets
:4
"
`;

exports[`cli completion tests for commander > copy command argument handlers > should filter destination suggestions when typing partial input 1`] = `
"build/ Build output
backup/ Backup location
:4
"
`;

exports[`cli completion tests for commander > copy command argument handlers > should filter source suggestions when typing partial input 1`] = `
"src/ Source directory
:4
"
`;

exports[`cli completion tests for commander > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = `
"--port Specify port
:4
Expand All @@ -737,6 +766,84 @@ exports[`cli completion tests for commander > edge case completions for end with
"
`;

exports[`cli completion tests for commander > lint command argument handlers > should complete files argument with file suggestions 1`] = `
"main.ts Main file
index.ts Index file
:4
"
`;

exports[`cli completion tests for commander > lint command argument handlers > should continue completing variadic files argument after first file 1`] = `
"main.ts Main file
index.ts Index file
:4
"
`;

exports[`cli completion tests for commander > lint command argument handlers > should continue completing variadic suggestions after first file 1`] = `
"index.ts Index file
:4
"
`;

exports[`cli completion tests for commander > lint command argument handlers > should filter file suggestions when typing partial input 1`] = `
"main.ts Main file
:4
"
`;

exports[`cli completion tests for commander > positional argument completions > should complete multiple positional arguments when ending with part of the value 1`] = `
"index.ts Index file
:4
"
`;

exports[`cli completion tests for commander > positional argument completions > should complete multiple positional arguments when ending with space 1`] = `
"main.ts Main file
index.ts Index file
:4
"
`;

exports[`cli completion tests for commander > positional argument completions > should complete single positional argument when ending with space 1`] = `
"main.ts Main file
index.ts Index file
:4
"
`;

exports[`cli completion tests for commander > root command argument tests > should complete root command project argument 1`] = `
"dev Start dev server
serve Start the server
build Build the project
deploy Deploy the application
lint Lint source files
copy Copy files
:4
"
`;

exports[`cli completion tests for commander > root command argument tests > should complete root command project argument after options 1`] = `
"dev Start dev server
serve Start the server
build Build the project
deploy Deploy the application
lint Lint source files
copy Copy files
:4
"
`;

exports[`cli completion tests for commander > root command argument tests > should complete root command project argument with options and partial input 1`] = `
":4
"
`;

exports[`cli completion tests for commander > root command argument tests > should complete root command project argument with partial input 1`] = `
":4
"
`;

exports[`cli completion tests for commander > root command option tests > should complete root command --logLevel option values 1`] = `
"info Info level
warn Warn level
Expand Down Expand Up @@ -829,6 +936,7 @@ serve Start the server
build Build the project
deploy Deploy the application
lint Lint source files
copy Copy files
:4
"
`;
Expand Down
11 changes: 4 additions & 7 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ function runCommand(command: string): Promise<string> {
const cliTools = ['t', 'citty', 'cac', 'commander'];

describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
// Commander does not have custom completions for arguments yet.
const shouldSkipTest = cliTool === 'commander';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Goodbye test suppression. Woop!


const commandPrefix = `pnpm tsx examples/demo.${cliTool}.ts complete --`;

it('should complete cli options', async () => {
Expand Down Expand Up @@ -230,7 +227,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
});
});

describe.runIf(!shouldSkipTest)('root command argument tests', () => {
describe('root command argument tests', () => {
it('should complete root command project argument', async () => {
const command = `${commandPrefix} ""`;
const output = await runCommand(command);
Expand Down Expand Up @@ -352,7 +349,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
});
});

describe.runIf(!shouldSkipTest)('positional argument completions', () => {
describe('positional argument completions', () => {
it('should complete multiple positional arguments when ending with space', async () => {
const command = `${commandPrefix} lint ""`;
const output = await runCommand(command);
Expand All @@ -372,7 +369,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
});
});

describe.runIf(!shouldSkipTest)('copy command argument handlers', () => {
describe('copy command argument handlers', () => {
it('should complete source argument with directory suggestions', async () => {
const command = `${commandPrefix} copy ""`;
const output = await runCommand(command);
Expand All @@ -398,7 +395,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
});
});

describe.runIf(!shouldSkipTest)('lint command argument handlers', () => {
describe('lint command argument handlers', () => {
it('should complete files argument with file suggestions', async () => {
const command = `${commandPrefix} lint ""`;
const output = await runCommand(command);
Expand Down
Loading