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

Add Fish autocomplete #175

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/jsLinters/eslint.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/plugin-autocomplete.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export abstract class AutocompleteBase extends Command {
this.error('Missing required argument shell')
}
this.errorIfWindows()
if (!['bash', 'zsh'].includes(shell)) {
if (!['bash', 'fish', 'zsh'].includes(shell)) {
throw new Error(`${shell} is not a supported shell for autocomplete`)
}
}
Expand Down
53 changes: 51 additions & 2 deletions src/commands/autocomplete/create.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as child_process from 'child_process'
import * as fs from 'fs-extra'
import * as path from 'path'

Expand Down Expand Up @@ -51,6 +52,10 @@ export default class Create extends AutocompleteBase {
await fs.writeFile(this.bashCompletionFunctionPath, this.bashCompletionFunction)
await fs.writeFile(this.zshSetupScriptPath, this.zshSetupScript)
await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionFunction)
if (this.config.shell === 'fish') {
debug(`fish shell detected, writing completion to ${this.fishCompletionFunctionPath}`)
await fs.writeFile(this.fishCompletionFunctionPath, this.fishCompletionFunction)
}
}

private get bashSetupScriptPath(): string {
Expand Down Expand Up @@ -78,6 +83,12 @@ export default class Create extends AutocompleteBase {
return path.join(this.bashFunctionsDir, `${this.cliBin}.bash`)
}

private get fishCompletionFunctionPath(): string {
// dynamically load path to completions file
const dir = child_process.execSync('pkg-config --variable completionsdir fish').toString().trimRight()
return `${dir}/${this.cliBin}.fish`
}

private get zshCompletionFunctionPath(): string {
// <cachedir>/autocomplete/functions/zsh/_<bin>
return path.join(this.zshFunctionsDir, `_${this.cliBin}`)
Expand Down Expand Up @@ -221,13 +232,51 @@ complete -o default -F _${cliBin} ${cliBin}
`
}

private get fishCompletionFunction(): string {
const cliBin = this.cliBin
const completions = []
completions.push(`
function __fish_${cliBin}_needs_command
set cmd (commandline -opc)
if [ (count $cmd) -eq 1 ]
return 0
else
return 1
end
end

function __fish_${cliBin}_using_command
set cmd (commandline -opc)
if [ (count $cmd) -gt 1 ]
if [ $argv[1] = $cmd[2] ]
return 0
end
end
return 1
end`,
)

for (const command of this.commands) {
completions.push(`complete -f -c ${cliBin} -n '__fish_${cliBin}_needs_command' -a ${command.id} -d "${command.description}"`)
const flags = command.flags || {}
Object.keys(flags)
.filter(flag => flags[flag] && !flags[flag].hidden)
.forEach(flag => {
const f = flags[flag] || {}
const shortFlag = f.char ? `-s ${f.char}` : ''
const description = f.description ? `-d "${f.description}"` : ''
const options = f.options ? `-r -a "${f.options.join(' ')}"` : ''
completions.push(`complete -f -c ${cliBin} -n ' __fish_${cliBin}_using_command ${command.id}' -l ${flag} ${shortFlag} ${options} ${description}`)
})
}
return completions.join('\n')
}

private get zshCompletionFunction(): string {
const cliBin = this.cliBin
const allCommandsMeta = this.genAllCommandsMetaString
const caseStatementForFlagsMeta = this.genCaseStatementForFlagsMetaString

return `#compdef ${cliBin}

_${cliBin} () {
local _command_id=\${words[2]}
local _cur=\${words[CURRENT]}
Expand Down
50 changes: 45 additions & 5 deletions src/commands/autocomplete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ export default class Index extends AutocompleteBase {
static examples = [
'$ <%= config.bin %> autocomplete',
'$ <%= config.bin %> autocomplete bash',
'$ <%= config.bin %> autocomplete fish',
'$ <%= config.bin %> autocomplete zsh',
'$ <%= config.bin %> autocomplete --refresh-cache',
]

async run() {
const {args, flags} = this.parse(Index)
const shell = args.shell || this.determineShell(this.config.shell)
const shell = args.shell || this.config.shell
this.errorIfNotSupportedShell(shell)

cli.action.start(`${chalk.bold('Building the autocomplete cache')}`)
Expand All @@ -33,14 +34,14 @@ export default class Index extends AutocompleteBase {

if (!flags['refresh-cache']) {
const bin = this.config.bin
const tabStr = shell === 'bash' ? '<TAB><TAB>' : '<TAB>'
const note = shell === 'zsh' ? `After sourcing, you can run \`${chalk.cyan('$ compaudit -D')}\` to ensure no permissions conflicts are present` : 'If your terminal starts as a login shell you may need to print the init script into ~/.bash_profile or ~/.profile.'
const tabStr = this.getTabStr(shell)
const setupStep = this.getSetupStep(shell, bin)
const note = this.getNote(shell)

this.log(`
${chalk.bold(`Setup Instructions for ${bin.toUpperCase()} CLI Autocomplete ---`)}

1) Add the autocomplete env var to your ${shell} profile and source it
${chalk.cyan(`$ printf "eval $(${bin} autocomplete:script ${shell})" >> ~/.${shell}rc; source ~/.${shell}rc`)}
1) ${setupStep}

NOTE: ${note}

Expand All @@ -52,4 +53,43 @@ Enjoy!
`)
}
}

private getSetupStep(shell: string, bin: string): string {
switch (shell) {
case 'bash':
case 'zsh':
return `Add the autocomplete env var to your ${shell} profile and source it
${chalk.cyan(`$ printf "$(${bin} autocomplete:script ${shell})" >> ~/.${shell}rc; source ~/.${shell}rc`)}`
case 'fish':
return `Update your shell to load the new completions
${chalk.cyan('source ~/.config/fish/config.fish')}`
default:
return ''
}
}

private getNote(shell: string): string {
switch (shell) {
case 'bash':
return 'If your terminal starts as a login shell you may need to print the init script into ~/.bash_profile or ~/.profile.'
case 'fish':
return 'This assumes your Fish configuration is stored at ~/.config/fish/config.fish'
case 'zsh':
return `After sourcing, you can run \`${chalk.cyan('$ compaudit -D')}\` to ensure no permissions conflicts are present`
default:
return ''
}
}

private getTabStr(shell: string): string {
switch (shell) {
case 'bash':
return '<TAB><TAB>'
case 'fish':
case 'zsh':
return '<TAB>'
default:
return ''
}
}
}
1 change: 0 additions & 1 deletion test/commands/autocomplete/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ complete -o default -F _oclif-example oclif-example\n`)
it('#zshCompletionFunction', () => {
/* eslint-disable no-useless-escape */
expect(cmd.zshCompletionFunction).to.eq(`#compdef oclif-example

_oclif-example () {
local _command_id=\${words[2]}
local _cur=\${words[CURRENT]}
Expand Down
29 changes: 25 additions & 4 deletions test/commands/autocomplete/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ skipWindows('autocomplete index', () => {
Setup Instructions for OCLIF-EXAMPLE CLI Autocomplete ---

1) Add the autocomplete env var to your bash profile and source it
$ printf \"eval $(oclif-example autocomplete:script bash)\" >> ~/.bashrc; source ~/.bashrc
$ printf \"$(oclif-example autocomplete:script bash)\" >> ~/.bashrc; source ~/.bashrc

NOTE: If your terminal starts as a login shell you may need to print the init script into ~/.bash_profile or ~/.profile.

Expand All @@ -23,8 +23,7 @@ $ oclif-example command --<TAB><TAB> # Flag completion

Enjoy!

`,
)
`)
})

test
Expand All @@ -35,7 +34,7 @@ Enjoy!
Setup Instructions for OCLIF-EXAMPLE CLI Autocomplete ---

1) Add the autocomplete env var to your zsh profile and source it
$ printf \"eval $(oclif-example autocomplete:script zsh)\" >> ~/.zshrc; source ~/.zshrc
$ printf \"$(oclif-example autocomplete:script zsh)\" >> ~/.zshrc; source ~/.zshrc

NOTE: After sourcing, you can run \`$ compaudit -D\` to ensure no permissions conflicts are present

Expand All @@ -45,6 +44,28 @@ $ oclif-example command --<TAB> # Flag completion

Enjoy!

`,
)
})

test
.stdout()
.command(['autocomplete', 'fish'])
.it('provides fish instructions', ctx => {
expect(ctx.stdout).to.equal(`
Setup Instructions for OCLIF-EXAMPLE CLI Autocomplete ---

1) Update your shell to load the new completions
source ~/.config/fish/config.fish

NOTE: This assumes your Fish configuration is stored at ~/.config/fish/config.fish

2) Test it out, e.g.:
$ oclif-example <TAB> # Command completion
$ oclif-example command --<TAB> # Flag completion

Enjoy!

`,
)
})
Expand Down
10 changes: 7 additions & 3 deletions test/commands/autocomplete/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ OCLIF_EXAMPLE_AC_ZSH_SETUP_PATH=${
test
.stdout()
.command(['autocomplete:script', 'fish'])
.catch(error => {
expect(error.message).to.contain('fish is not a supported shell for autocomplete')
.it('outputs fish profile config', ctx => {
expect(ctx.stdout).to.contain(`
OCLIF_EXAMPLE_AC_FISH_SETUP_PATH=${
ctx.config.cacheDir
}/autocomplete/fish_setup && test -f $OCLIF_EXAMPLE_AC_FISH_SETUP_PATH && source $OCLIF_EXAMPLE_AC_FISH_SETUP_PATH; # oclif-example autocomplete setup
`,
)
})
.it('errors on unsupported shell')
})