-
Notifications
You must be signed in to change notification settings - Fork 665
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
300 additions
and
0 deletions.
There are no files selected for viewing
170 changes: 170 additions & 0 deletions
170
packages/@ionic/cli-framework/src/lib/__tests__/completion.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import { Command, CommandMap, CommandMapDefault, Namespace, NamespaceMap } from '../command'; | ||
import { CommandMetadata, NamespaceMetadata } from '../../definitions'; | ||
|
||
import { getCompletionWords } from '../completion'; | ||
|
||
class MyNamespace extends Namespace { | ||
async getMetadata(): Promise<CommandMetadata> { | ||
return { | ||
name: 'my', | ||
summary: '', | ||
}; | ||
} | ||
|
||
async getNamespaces(): Promise<NamespaceMap> { | ||
return new NamespaceMap([ | ||
['foo', async () => new FooNamespace(this)], | ||
['defns', async () => new NamespaceWithDefault(this)], | ||
['f', 'foo'], | ||
]); | ||
} | ||
} | ||
|
||
class NamespaceWithDefault extends Namespace { | ||
async getMetadata(): Promise<NamespaceMetadata> { | ||
return { | ||
name: 'defns', | ||
summary: '', | ||
}; | ||
} | ||
|
||
async getCommands(): Promise<CommandMap> { | ||
return new CommandMap([ | ||
[CommandMapDefault, async () => new DefaultCommand(this)], | ||
]); | ||
} | ||
} | ||
|
||
class FooNamespace extends Namespace { | ||
async getMetadata(): Promise<NamespaceMetadata> { | ||
return { | ||
name: 'foo', | ||
summary: '', | ||
}; | ||
} | ||
|
||
async getCommands(): Promise<CommandMap> { | ||
return new CommandMap([ | ||
['bar', async () => new BarCommand(this)], | ||
['baz', async () => new BazCommand(this)], | ||
['b', 'bar'], | ||
]); | ||
} | ||
} | ||
|
||
class EmptyNamespace extends Namespace { | ||
async getMetadata(): Promise<NamespaceMetadata> { | ||
return { | ||
name: 'empty', | ||
summary: '' | ||
}; | ||
} | ||
} | ||
|
||
class DefaultCommand extends Command { | ||
async getMetadata(): Promise<CommandMetadata> { | ||
return { | ||
name: 'def', | ||
summary: '', | ||
options: [ | ||
{ | ||
name: 'str-opt', | ||
summary: '', | ||
}, | ||
{ | ||
name: 'bool-opt', | ||
summary: '', | ||
type: Boolean, | ||
default: true, | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
async run() {} | ||
} | ||
|
||
class BarCommand extends Command { | ||
async getMetadata(): Promise<CommandMetadata> { | ||
return { | ||
name: 'bar', | ||
summary: '', | ||
options: [ | ||
{ | ||
name: 'str-opt', | ||
summary: '', | ||
}, | ||
{ | ||
name: 'bool-opt', | ||
summary: '', | ||
type: Boolean, | ||
default: true, | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
async run() {} | ||
} | ||
|
||
class BazCommand extends Command { | ||
async getMetadata(): Promise<CommandMetadata> { | ||
return { | ||
name: 'baz', | ||
summary: '', | ||
}; | ||
} | ||
|
||
async run() {} | ||
} | ||
|
||
describe('@ionic/cli-framework', () => { | ||
|
||
describe('lib/completion', () => { | ||
|
||
describe('getCompletionWords', () => { | ||
|
||
it('should have no words for empty namespace', async () => { | ||
const ns = new EmptyNamespace(); | ||
const words = await getCompletionWords(ns, []); | ||
expect(words).toEqual([]); | ||
}); | ||
|
||
it('should return command words for a namespace', async () => { | ||
const ns = new FooNamespace(); | ||
const words = await getCompletionWords(ns, []); | ||
expect(words).toEqual(['bar', 'baz']); | ||
}); | ||
|
||
it('should return command and namespace words for a namespace', async () => { | ||
const ns = new MyNamespace(); | ||
const words = await getCompletionWords(ns, []); | ||
expect(words).toEqual(['defns', 'foo']); | ||
}); | ||
|
||
it('should return options from a default namespace', async () => { | ||
const ns = new MyNamespace(); | ||
debugger; | ||
const words = await getCompletionWords(ns, ['defns']); | ||
expect(words).toEqual(['--no-bool-opt', '--str-opt']); | ||
}); | ||
|
||
it('should return options from a command', async () => { | ||
const ns = new MyNamespace(); | ||
debugger; | ||
const words = await getCompletionWords(ns, ['foo', 'bar']); | ||
expect(words).toEqual(['--no-bool-opt', '--str-opt']); | ||
}); | ||
|
||
it('should return unique options from a command', async () => { | ||
const ns = new MyNamespace(); | ||
debugger; | ||
const words = await getCompletionWords(ns, ['foo', 'bar', '--str-opt']); | ||
expect(words).toEqual(['--no-bool-opt']); | ||
}); | ||
|
||
}) | ||
|
||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import * as lodash from 'lodash'; | ||
|
||
import { CommandMetadata, CommandMetadataInput, CommandMetadataOption, ICommand, INamespace } from '../definitions'; | ||
import { isCommand } from '../guards'; | ||
|
||
import { NO_COLORS } from './colors'; | ||
import { formatOptionName } from './options'; | ||
|
||
export async function getCompletionWords<C extends ICommand<C, N, M, I, O>, N extends INamespace<C, N, M, I, O>, M extends CommandMetadata<I, O>, I extends CommandMetadataInput, O extends CommandMetadataOption>(ns: N, argv: ReadonlyArray<string>): Promise<string[]> { | ||
const { obj } = await ns.locate(argv, { useAliases: false }); | ||
|
||
if (isCommand(obj)) { | ||
const metadata = await obj.getMetadata(); | ||
const options = metadata.options ? metadata.options : []; | ||
|
||
if (options.length === 0) { | ||
return []; | ||
} | ||
|
||
const optionNames = options | ||
.map(option => formatOptionName(option, { showAliases: false, showValueSpec: false, colors: NO_COLORS })) | ||
.filter(name => !argv.includes(name)); | ||
|
||
const aliasNames = lodash.flatten(options.map(option => option.aliases ? option.aliases : [])) | ||
.map(alias => `-${alias}`); | ||
|
||
return [...optionNames, ...aliasNames].sort(); | ||
} | ||
|
||
return [ | ||
...(await obj.getCommands()).keysWithoutAliases(), | ||
...(await obj.getNamespaces()).keysWithoutAliases(), | ||
].sort(); | ||
} | ||
|
||
export interface CompletionFormatterDeps<C extends ICommand<C, N, M, I, O>, N extends INamespace<C, N, M, I, O>, M extends CommandMetadata<I, O>, I extends CommandMetadataInput, O extends CommandMetadataOption> { | ||
readonly namespace: N; | ||
} | ||
|
||
export abstract class CompletionFormatter<C extends ICommand<C, N, M, I, O>, N extends INamespace<C, N, M, I, O>, M extends CommandMetadata<I, O>, I extends CommandMetadataInput, O extends CommandMetadataOption> { | ||
protected readonly namespace: N; | ||
|
||
constructor({ namespace }: CompletionFormatterDeps<C, N, M, I, O>) { | ||
this.namespace = namespace; | ||
} | ||
|
||
abstract format(): Promise<string>; | ||
} | ||
|
||
export class ZshCompletionFormatter<C extends ICommand<C, N, M, I, O>, N extends INamespace<C, N, M, I, O>, M extends CommandMetadata<I, O>, I extends CommandMetadataInput, O extends CommandMetadataOption> extends CompletionFormatter<C, N, M, I, O> { | ||
async format(): Promise<string> { | ||
const { name } = await this.namespace.getMetadata(); | ||
|
||
return ` | ||
###-begin-${name}-completion-### | ||
if type compdef &>/dev/null; then | ||
__${name}() { | ||
compadd -- $(${name} completion -- "$\{words[@]}" 2>/dev/null) | ||
} | ||
compdef __${name} ${name} | ||
fi | ||
###-end-${name}-completion-### | ||
`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { MetadataGroup, ZshCompletionFormatter, getCompletionWords } from '@ionic/cli-framework'; | ||
import { TERMINAL_INFO } from '@ionic/utils-terminal'; | ||
import * as path from 'path'; | ||
|
||
import { CommandLineInputs, CommandLineOptions, CommandMetadata } from '../definitions'; | ||
import { strong } from '../lib/color'; | ||
import { Command } from '../lib/command'; | ||
import { FatalException } from '../lib/errors'; | ||
|
||
export class CompletionCommand extends Command { | ||
async getMetadata(): Promise<CommandMetadata> { | ||
return { | ||
name: 'completion', | ||
type: 'global', | ||
summary: 'Enables tab-completion for Ionic CLI commands.', | ||
description: ` | ||
This command is experimental and only works for Z shell (zsh) and non-Windows platforms. | ||
To enable completions for the Ionic CLI, you can add the completion code that this command prints to your ${strong('~/.zshrc')} (or any other file loaded with your shell). See the examples. | ||
`, | ||
groups: [MetadataGroup.EXPERIMENTAL], | ||
exampleCommands: [ | ||
'', | ||
'>> ~/.zshrc', | ||
], | ||
}; | ||
} | ||
|
||
async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> { | ||
if (TERMINAL_INFO.windows) { | ||
throw new FatalException('Completion is not supported on Windows shells.'); | ||
} | ||
|
||
if (path.basename(TERMINAL_INFO.shell) !== 'zsh') { | ||
throw new FatalException('Completion is currently only available for Z Shell (zsh).'); | ||
} | ||
|
||
const words = options['--']; | ||
|
||
if (!words || words.length === 0) { | ||
const namespace = this.namespace.root; | ||
const formatter = new ZshCompletionFormatter({ namespace }); | ||
|
||
process.stdout.write(await formatter.format()); | ||
|
||
return; | ||
} | ||
|
||
const ns = this.namespace.root; | ||
const outputWords = await getCompletionWords(ns, words.slice(1)); | ||
|
||
process.stdout.write(outputWords.join(' ')); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters