diff --git a/.changeset/quiet-files-wait.md b/.changeset/quiet-files-wait.md new file mode 100644 index 0000000..b137058 --- /dev/null +++ b/.changeset/quiet-files-wait.md @@ -0,0 +1,5 @@ +--- +'@bomb.sh/tab': patch +--- + +fix: keep the shell directive set by an option or argument handler, so handlers can enable file and directory completion diff --git a/README.md b/README.md index c019b23..3c6da7a 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,19 @@ my-cli complete -- install --port="" :4 ``` +The last line is the shell directive. `:4` (`ShellCompDirectiveNoFileComp`) is the default and stops the shell from falling back to file completion. A handler can set another directive for its completion request, for example to let the shell complete file or directory paths: + +```typescript +import t, { ShellCompDirective } from '@bomb.sh/tab'; + +devCmd.option('config', 'Config file', () => { + t.directive = ShellCompDirective.ShellCompDirectiveDefault; // files +}); +devCmd.argument('dir', () => { + t.directive = ShellCompDirective.ShellCompDirectiveFilterDirs; // directories +}); +``` + ## Documentation See [bombshell docs](https://bomb.sh/docs/tab/). diff --git a/src/t.ts b/src/t.ts index b22cd9c..f211323 100644 --- a/src/t.ts +++ b/src/t.ts @@ -387,8 +387,6 @@ export class RootCommand extends Command { } private complete(toComplete: string) { - this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - const seen = new Set(); this.completions .filter((comp) => { @@ -412,6 +410,7 @@ export class RootCommand extends Command { parse(args: string[]) { this.completions = []; + this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp; const endsWithSpace = args[args.length - 1] === ''; diff --git a/tests/directive.test.ts b/tests/directive.test.ts new file mode 100644 index 0000000..f0840d1 --- /dev/null +++ b/tests/directive.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { RootCommand, ShellCompDirective } from '../src/t'; + +function parse(root: RootCommand, args: string[]): string[] { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + root.parse(args); + return log.mock.calls.map(([line]) => String(line)); +} + +describe('completion directive', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('disables file completion by default', () => { + const root = new RootCommand(); + root.command('build', 'Build').option('out', 'Output file', () => {}); + + expect(parse(root, ['build', '--out', ''])).toEqual([ + `:${ShellCompDirective.ShellCompDirectiveNoFileComp}`, + ]); + }); + + it('keeps a directive set by an option handler', () => { + const root = new RootCommand(); + root.command('build', 'Build').option('out', 'Output file', () => { + root.directive = ShellCompDirective.ShellCompDirectiveDefault; + }); + + expect(parse(root, ['build', '--out', ''])).toEqual([ + `:${ShellCompDirective.ShellCompDirectiveDefault}`, + ]); + }); + + it('keeps a directive set by an argument handler', () => { + const root = new RootCommand(); + root.command('serve', 'Serve').argument('dir', () => { + root.directive = ShellCompDirective.ShellCompDirectiveFilterDirs; + }); + + expect(parse(root, ['serve', ''])).toEqual([ + `:${ShellCompDirective.ShellCompDirectiveFilterDirs}`, + ]); + }); + + it('resets the directive for the next completion request', () => { + const root = new RootCommand(); + const build = root.command('build', 'Build'); + build.option('out', 'Output file', () => { + root.directive = ShellCompDirective.ShellCompDirectiveDefault; + }); + build.option('mode', 'Build mode', (complete) => { + complete('production', ''); + }); + + parse(root, ['build', '--out', '']); + + expect(parse(root, ['build', '--mode', ''])).toEqual([ + 'production\t', + `:${ShellCompDirective.ShellCompDirectiveNoFileComp}`, + ]); + }); +});