Skip to content

Commit

Permalink
fix: coTopics completion
Browse files Browse the repository at this point in the history
  • Loading branch information
jayree committed Feb 28, 2023
1 parent ad48a58 commit ae7af10
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 156 deletions.
204 changes: 104 additions & 100 deletions src/autocomplete/zsh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ function sanitizeSummary(description?: string): string {
if (description === undefined) {
return ''
}
return description
.replace(/([`"])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes
// eslint-disable-next-line no-useless-escape
.replace(/([\[\]])/g, '\\\\$1') // square brackets require double-backslashes
.split('\n')[0] // only use the first line
return (
description
.replace(/([`"])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes
// eslint-disable-next-line no-useless-escape
.replace(/([\[\]])/g, '\\\\$1') // square brackets require double-backslashes
.split('\n')[0]
) // only use the first line
}

const argTemplate = ' "%s")\n %s\n ;;\n'

type CommandCompletion = {
id: string;
summary: string;
Expand All @@ -30,34 +30,37 @@ type Topic = {
}

export default class ZshCompWithSpaces {
protected config: Config;
protected config: Config

private topics: Topic[]

private commands: CommandCompletion[]

private _coTopics?: string[]
private coTopics: string[]

constructor(config: Config) {
this.config = config
this.topics = this.getTopics()
this.commands = this.getCommands()
this.coTopics = this.getCoTopics()
}

public generate(): string {
const firstArgs: {id: string; summary?: string}[] = []

this.topics.forEach(t => {
if (!t.name.includes(':')) firstArgs.push({
id: t.name,
summary: t.description,
})
if (!t.name.includes(':'))
firstArgs.push({
id: t.name,
summary: t.description,
})
})
this.commands.forEach(c => {
if (!firstArgs.find(a => a.id === c.id) && !c.id.includes(':')) firstArgs.push({
id: c.id,
summary: c.summary,
})
if (!firstArgs.find(a => a.id === c.id) && !c.id.includes(':'))
firstArgs.push({
id: c.id,
summary: c.summary,
})
})

const mainArgsCaseBlock = () => {
Expand All @@ -66,24 +69,24 @@ export default class ZshCompWithSpaces {
for (const arg of firstArgs) {
if (this.coTopics.includes(arg.id)) {
// coTopics already have a completion function.
caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n`
caseBlock += ` ${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n`
} else {
const cmd = this.commands.find(c => c.id === arg.id)

if (cmd) {
// if it's a command and has flags, inline flag completion statement.
// skip it from the args statement if it doesn't accept any flag.
if (Object.keys(cmd.flags).length > 0) {
caseBlock += `${arg.id})\n${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n`
caseBlock += ` ${arg.id})\n ${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n`
}
} else {
// it's a topic, redirect to its completion function.
caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n`
caseBlock += ` ${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n`
}
}
}

caseBlock += 'esac\n'
caseBlock += ' esac'

return caseBlock
}
Expand All @@ -92,7 +95,6 @@ export default class ZshCompWithSpaces {
`#compdef ${this.config.bin}
${this.topics.map(t => this.genZshTopicCompFun(t.name)).join('\n')}
_${this.config.bin}() {
local context state state_descr line
typeset -A opt_args
Expand All @@ -101,11 +103,11 @@ _${this.config.bin}() {
case "$state" in
cmds)
${this.genZshValuesBlock(firstArgs)}
;;
${this.genZshValuesBlock({subArgs: firstArgs})}
;;
args)
${mainArgsCaseBlock()}
;;
;;
esac
}
Expand All @@ -114,18 +116,18 @@ _${this.config.bin}
return compFunc
}

private genZshFlagArgumentsBlock(flags?: CommandFlags): string {
private genZshFlagArguments(flags?: CommandFlags): string {
// if a command doesn't have flags make it only complete files
// also add comp for the global `--help` flag.
if (!flags) return '_arguments -S \\\n --help"[Show help for command]" "*: :_files'
if (!flags) return '--help"[Show help for command]" "*: :_files'

const flagNames = Object.keys(flags)

// `-S`:
// Do not complete flags after a ‘--’ appearing on the line, and ignore the ‘--’. For example, with -S, in the line:
// foobar -x -- -y
// the ‘-x’ is considered a flag, the ‘-y’ is considered an argument, and the ‘--’ is considered to be neither.
let argumentsBlock = '_arguments -S \\\n'
let argumentsBlock = ''

for (const flagName of flagNames) {
const f = flags[flagName]
Expand All @@ -145,27 +147,11 @@ _${this.config.bin}
} else {
flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}`
}

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

if (f.options) {
flagSpec += `:${f.name} options:(${f.options?.join(' ')})"`
} else {
flagSpec += ':file:_files"'
}
} else if (f.multiple) {
// this flag can be present multiple times on the line
flagSpec += `"*"--${f.name}`
} else {
if (f.multiple) {
// this flag can be present multiple times on the line
flagSpec += '"*"'
}

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

if (f.options) {
flagSpec += `${f.name} options:(${f.options.join(' ')})"`
} else {
flagSpec += 'file:_files"'
}
flagSpec += `--${f.name}`
}
} else if (f.char) {
// Flag.Boolean
Expand All @@ -175,6 +161,16 @@ _${this.config.bin}
flagSpec += `--${f.name}"[${f.summary}]"`
}

if (f.type === 'option') {
flagSpec += `"[${f.summary}]`

if (f.options) {
flagSpec += `:${f.name} options:(${f.options?.join(' ')})"`
} else {
flagSpec += ':file:_files"'
}
}

flagSpec += ' \\\n'
argumentsBlock += flagSpec
}
Expand All @@ -186,65 +182,78 @@ _${this.config.bin}
return argumentsBlock
}

private genZshValuesBlock(subArgs: {id: string; summary?: string}[]): string {
let valuesBlock = '_values "completions" \\\n'

subArgs.forEach(subArg => {
valuesBlock += `"${subArg.id}[${subArg.summary}]" \\\n`
private genZshFlagArgumentsBlock(flags?: CommandFlags): string {
let argumentsBlock = '_arguments -S \\'
this.genZshFlagArguments(flags)
.split('\n')
.forEach(f => {
argumentsBlock += `\n ${f}`
})

return valuesBlock
return argumentsBlock
}

private genZshTopicCompFun(id: string): string {
const coTopics: string[] = []
private genZshValuesBlock(options: {id?: string; subArgs: Array<{id: string; summary?: string}>}): string {
let valuesBlock = '_values "completions"'
const {id, subArgs} = options

for (const topic of this.topics) {
for (const cmd of this.commands) {
if (topic.name === cmd.id) {
coTopics.push(topic.name)
}
subArgs.forEach(subArg => {
valuesBlock += ` \\\n "${subArg.id}[${subArg.summary}]"`
})

if (id) {
const cflags = this.commands.find(c => c.id === id)?.flags

if (cflags) {
// eslint-disable-next-line no-template-curly-in-string
valuesBlock += ' \\\n "${flags[@]}"'
}
}

const flagArgsTemplate = ' "%s")\n %s\n ;;\n'
return valuesBlock
}

private genZshTopicCompFun(id: string): string {
const underscoreSepId = id.replace(/:/g, '_')
const depth = id.split(':').length

const isCotopic = coTopics.includes(id)
const isCotopic = this.coTopics.includes(id)

if (isCotopic) {
const compFuncName = `${this.config.bin}_${underscoreSepId}`
let flags = ''

const coTopicCompFunc =
`_${compFuncName}() {
_${compFuncName}_flags() {
local context state state_descr line
typeset -A opt_args
if (id) {
const cflags = this.commands.find(c => c.id === id)?.flags

${this.genZshFlagArgumentsBlock(this.commands.find(c => c.id === id)?.flags)}
}
if (cflags) {
flags += '\n'
this.genZshFlagArguments(cflags)
.split('\n')
.forEach(f => {
flags += ` ${f}\n`
})
flags += ' '
}
}

if (isCotopic) {
const coTopicCompFunc = `_${this.config.bin}_${underscoreSepId}() {
local context state state_descr line
typeset -A opt_args
local -a flags=(%s)
_arguments -C "1: :->cmds" "*: :->args"
case "$state" in
cmds)
if [[ "\${words[CURRENT]}" == -* ]]; then
_${compFuncName}_flags
else
%s
fi
%s
;;
args)
case $line[1] in
%s
*)
_${compFuncName}_flags
;;
case $line[1] in%s
*)
_arguments -S \\
"\${flags[@]}"
;;
esac
;;
esac
Expand All @@ -264,24 +273,24 @@ _${this.config.bin}
summary: t.description,
})

argsBlock += util.format(argTemplate, subArg, `_${this.config.bin}_${underscoreSepId}_${subArg}`)
argsBlock += util.format('\n "%s")\n %s\n ;;', subArg, `_${this.config.bin}_${underscoreSepId}_${subArg}`)
})

this.commands
.filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1)
.forEach(c => {
if (coTopics.includes(c.id)) return
if (this.coTopics.includes(c.id)) return
const subArg = c.id.split(':')[depth]

subArgs.push({
id: subArg,
summary: c.summary,
})

argsBlock += util.format(flagArgsTemplate, subArg, this.genZshFlagArgumentsBlock(c.flags))
argsBlock += util.format('\n "%s")\n _arguments -C "*::arg:->args"\n %s\n ;;', subArg, this.genZshFlagArgumentsBlock(c.flags))
})

return util.format(coTopicCompFunc, this.genZshValuesBlock(subArgs), argsBlock)
return util.format(coTopicCompFunc, flags, this.genZshValuesBlock({id, subArgs}), argsBlock)
}
let argsBlock = ''

Expand All @@ -296,21 +305,21 @@ _${this.config.bin}
summary: t.description,
})

argsBlock += util.format(argTemplate, subArg, `_${this.config.bin}_${underscoreSepId}_${subArg}`)
argsBlock += util.format('\n "%s")\n %s\n ;;', subArg, `_${this.config.bin}_${underscoreSepId}_${subArg}`)
})

this.commands
.filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1)
.forEach(c => {
if (coTopics.includes(c.id)) return
if (this.coTopics.includes(c.id)) return
const subArg = c.id.split(':')[depth]

subArgs.push({
id: subArg,
summary: c.summary,
})

argsBlock += util.format(flagArgsTemplate, subArg, this.genZshFlagArgumentsBlock(c.flags))
argsBlock += util.format(`\n "%s")${flags ? '\n _arguments -C "*::arg:->args"' : ''}\n %s\n ;;`, subArg, this.genZshFlagArgumentsBlock(c.flags))
})

const topicCompFunc =
Expand All @@ -322,22 +331,19 @@ _${this.config.bin}
case "$state" in
cmds)
%s
%s
;;
args)
case $line[1] in
%s
case $line[1] in%s
esac
;;
esac
esac
}
`
return util.format(topicCompFunc, this.genZshValuesBlock(subArgs), argsBlock)
return util.format(topicCompFunc, this.genZshValuesBlock({subArgs}), argsBlock)
}

private get coTopics(): string [] {
if (this._coTopics) return this._coTopics

private getCoTopics(): string[] {
const coTopics: string[] = []

for (const topic of this.topics) {
Expand All @@ -348,9 +354,7 @@ _${this.config.bin}
}
}

this._coTopics = coTopics

return this._coTopics
return coTopics
}

private getTopics(): Topic[] {
Expand Down
Loading

0 comments on commit ae7af10

Please sign in to comment.