Skip to content

Commit

Permalink
feat: powershell autocomplete (#441)
Browse files Browse the repository at this point in the history
* feat: powershell autocomplete

* chore: refactor command tree generator

* chore: linter

* fix: properly escape double quotes in pwsh

* fix: coTopic hashtable gen

* fix: handle cmd/flag withouth a summary

* test: add pwsh completion UT

* fix: use EOL

* fix: update powershell setup script

* chore: fix UTs

* chore: restore skipWin test utility

* chore: maybe fix win UTs?

* chore: summary is already handled

* feat: support flag.multiple, part 1

* feat: support flag.multiple, part 2

* fix: do not block win

* fix(zsh): do not mutate flags

* fix: properly escape double quotes

* fix: escape backticks

* chore: refactor 1

* chore: refactor 2

[skip ci]

* chore: sort suggestions alphabetically

[skip ci]

* chore: update UT

* fix: support top-level cmds

* chore: refactor 3

* fix: support Complete/TabCompleteNext modes

* chore: remove whitespaces between hashtables

* feat: support global help flag

* chore: remove hehe

* chore: update instructions

* fix: support partial id comp
  • Loading branch information
cristiand391 committed May 9, 2023
1 parent ca9e5a4 commit 53697c4
Show file tree
Hide file tree
Showing 10 changed files with 897 additions and 71 deletions.
470 changes: 470 additions & 0 deletions src/autocomplete/powershell.ts

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions src/autocomplete/zsh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ _${this.config.bin}
// skip hidden flags
if (f.hidden) continue

f.summary = sanitizeSummary(f.summary || f.description)
const flagSummary = sanitizeSummary(f.summary || f.description)

let flagSpec = ''

Expand All @@ -146,7 +146,7 @@ _${this.config.bin}
flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}`
}

flagSpec += `"[${f.summary}]`
flagSpec += `"[${flagSummary}]`

if (f.options) {
flagSpec += `:${f.name} options:(${f.options?.join(' ')})"`
Expand All @@ -159,7 +159,7 @@ _${this.config.bin}
flagSpec += '"*"'
}

flagSpec += `--${f.name}"[${f.summary}]:`
flagSpec += `--${f.name}"[${flagSummary}]:`

if (f.options) {
flagSpec += `${f.name} options:(${f.options.join(' ')})"`
Expand All @@ -169,10 +169,10 @@ _${this.config.bin}
}
} else if (f.char) {
// Flag.Boolean
flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${f.summary}]"`
flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${flagSummary}]"`
} else {
// Flag.Boolean
flagSpec += `--${f.name}"[${f.summary}]"`
flagSpec += `--${f.name}"[${flagSummary}]"`
}

flagSpec += ' \\\n'
Expand Down
16 changes: 0 additions & 16 deletions src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,6 @@ export abstract class AutocompleteBase extends Command {
}
}

public errorIfWindows() {
if (this.config.windows && !this.isBashOnWindows(this.config.shell)) {
throw new Error('Autocomplete is not currently supported in Windows')
}
}

public errorIfNotSupportedShell(shell: string) {
if (!shell) {
this.error('Missing required argument shell')
}
this.errorIfWindows()
if (!['bash', 'zsh'].includes(shell)) {
throw new Error(`${shell} is not a supported shell for autocomplete`)
}
}

public get autocompleteCacheDir(): string {
return path.join(this.config.cacheDir, 'autocomplete')
}
Expand Down
16 changes: 15 additions & 1 deletion src/commands/autocomplete/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as fs from 'fs-extra'

import bashAutocomplete from '../../autocomplete/bash'
import ZshCompWithSpaces from '../../autocomplete/zsh'
import PowerShellComp from '../../autocomplete/powershell'
import bashAutocompleteWithSpaces from '../../autocomplete/bash-spaces'
import {AutocompleteBase} from '../../base'

Expand Down Expand Up @@ -34,7 +35,6 @@ export default class Create extends AutocompleteBase {
private _commands?: CommandCompletion[]

async run() {
this.errorIfWindows()
// 1. ensure needed dirs
await this.ensureDirs()
// 2. save (generated) autocomplete files
Expand All @@ -48,6 +48,8 @@ export default class Create extends AutocompleteBase {
await fs.ensureDir(this.bashFunctionsDir)
// ensure autocomplete zsh function dir
await fs.ensureDir(this.zshFunctionsDir)
// ensure autocomplete powershell function dir
await fs.ensureDir(this.pwshFunctionsDir)
}

private async createFiles() {
Expand All @@ -63,6 +65,8 @@ export default class Create extends AutocompleteBase {
} else {
const zshCompWithSpaces = new ZshCompWithSpaces(this.config)
await fs.writeFile(this.zshCompletionFunctionPath, zshCompWithSpaces.generate())
const pwshComp = new PowerShellComp(this.config)
await fs.writeFile(this.pwshCompletionFunctionPath, pwshComp.generate())
}
}

Expand All @@ -76,6 +80,11 @@ export default class Create extends AutocompleteBase {
return path.join(this.autocompleteCacheDir, 'zsh_setup')
}

private get pwshFunctionsDir(): string {
// <cachedir>/autocomplete/functions/powershell
return path.join(this.autocompleteCacheDir, 'functions', 'powershell')
}

private get bashFunctionsDir(): string {
// <cachedir>/autocomplete/functions/bash
return path.join(this.autocompleteCacheDir, 'functions', 'bash')
Expand All @@ -86,6 +95,11 @@ export default class Create extends AutocompleteBase {
return path.join(this.autocompleteCacheDir, 'functions', 'zsh')
}

private get pwshCompletionFunctionPath(): string {
// <cachedir>/autocomplete/functions/powershell/<bin>.ps1
return path.join(this.pwshFunctionsDir, `${this.cliBin}.ps1`)
}

private get bashCompletionFunctionPath(): string {
// <cachedir>/autocomplete/functions/bash/<bin>.bash
return path.join(this.bashFunctionsDir, `${this.cliBin}.bash`)
Expand Down
34 changes: 28 additions & 6 deletions src/commands/autocomplete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ export default class Index extends AutocompleteBase {
static description = 'display autocomplete installation instructions'

static args = {
shell: Args.string({description: 'shell type', required: false}),
shell: Args.string({
description: 'Shell type',
options: ['zsh', 'bash', 'powershell'],
required: false,
}),
}

static flags = {
Expand All @@ -20,13 +24,13 @@ export default class Index extends AutocompleteBase {
'$ <%= config.bin %> autocomplete',
'$ <%= config.bin %> autocomplete bash',
'$ <%= config.bin %> autocomplete zsh',
'$ <%= config.bin %> autocomplete powershell',
'$ <%= config.bin %> autocomplete --refresh-cache',
]

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

ux.action.start(`${chalk.bold('Building the autocomplete cache')}`)
await Create.run([], this.config)
Expand All @@ -35,15 +39,33 @@ 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 instructions = shell === 'powershell' ?
`New-Item -Type Directory -Path (Split-Path -Parent $PROFILE) -ErrorAction SilentlyContinue
Add-Content -Path $PROFILE -Value (Invoke-Expression -Command "${bin} autocomplete${this.config.topicSeparator}script ${shell}"); .$PROFILE` :
`$ printf "eval $(${bin} autocomplete${this.config.topicSeparator}script ${shell})" >> ~/.${shell}rc; source ~/.${shell}rc`

let note = ''

switch (shell) {
case 'zsh':
note = `After sourcing, you can run \`${chalk.cyan('$ compaudit -D')}\` to ensure no permissions conflicts are present`
break
case 'bash':
note = 'If your terminal starts as a login shell you may need to print the init script into ~/.bash_profile or ~/.profile.'
break
case 'powershell':
note = `Use the \`MenuComplete\` mode to get matching completions printed below the command line:\n${chalk.cyan('Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete')}`
}

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) Add the autocomplete ${shell === 'powershell' ? 'file' : 'env var'} to your ${shell} profile and source it
${chalk.cyan(instructions)}
NOTE: ${note}
${chalk.bold('NOTE')}: ${note}
2) Test it out, e.g.:
${chalk.cyan(`$ ${bin} ${tabStr}`)} # Command completion
Expand Down
32 changes: 24 additions & 8 deletions src/commands/autocomplete/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,38 @@ export default class Script extends AutocompleteBase {
static hidden = true

static args = {
shell: Args.string({description: 'shell type', required: false}),
shell: Args.string({
description: 'Shell type',
options: ['zsh', 'bash', 'powershell'],
required: false,
}),
}

async run() {
const {args} = await this.parse(Script)
const shell = args.shell || this.config.shell
this.errorIfNotSupportedShell(shell)

const binUpcase = this.cliBinEnvVar
const shellUpcase = shell.toUpperCase()
this.log(
`${this.prefix}${binUpcase}_AC_${shellUpcase}_SETUP_PATH=${path.join(
this.autocompleteCacheDir,
`${shell}_setup`,
)} && test -f $${binUpcase}_AC_${shellUpcase}_SETUP_PATH && source $${binUpcase}_AC_${shellUpcase}_SETUP_PATH;${this.suffix}`,
)
if (shell === 'powershell') {
const completionFuncPath = path.join(
this.config.cacheDir,
'autocomplete',
'functions',
'powershell',
`${this.cliBin}.ps1`,
)
this.log(
`. ${completionFuncPath}`,
)
} else {
this.log(
`${this.prefix}${binUpcase}_AC_${shellUpcase}_SETUP_PATH=${path.join(
this.autocompleteCacheDir,
`${shell}_setup`,
)} && test -f $${binUpcase}_AC_${shellUpcase}_SETUP_PATH && source $${binUpcase}_AC_${shellUpcase}_SETUP_PATH;${this.suffix}`,
)
}
}

private get prefix(): string {
Expand Down

0 comments on commit 53697c4

Please sign in to comment.