Skip to content

Commit ae7af10

Browse files
committed
fix: coTopics completion
1 parent ad48a58 commit ae7af10

File tree

2 files changed

+146
-156
lines changed

2 files changed

+146
-156
lines changed

src/autocomplete/zsh.ts

Lines changed: 104 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ function sanitizeSummary(description?: string): string {
55
if (description === undefined) {
66
return ''
77
}
8-
return description
9-
.replace(/([`"])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes
10-
// eslint-disable-next-line no-useless-escape
11-
.replace(/([\[\]])/g, '\\\\$1') // square brackets require double-backslashes
12-
.split('\n')[0] // only use the first line
8+
return (
9+
description
10+
.replace(/([`"])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes
11+
// eslint-disable-next-line no-useless-escape
12+
.replace(/([\[\]])/g, '\\\\$1') // square brackets require double-backslashes
13+
.split('\n')[0]
14+
) // only use the first line
1315
}
1416

15-
const argTemplate = ' "%s")\n %s\n ;;\n'
16-
1717
type CommandCompletion = {
1818
id: string;
1919
summary: string;
@@ -30,34 +30,37 @@ type Topic = {
3030
}
3131

3232
export default class ZshCompWithSpaces {
33-
protected config: Config;
33+
protected config: Config
3434

3535
private topics: Topic[]
3636

3737
private commands: CommandCompletion[]
3838

39-
private _coTopics?: string[]
39+
private coTopics: string[]
4040

4141
constructor(config: Config) {
4242
this.config = config
4343
this.topics = this.getTopics()
4444
this.commands = this.getCommands()
45+
this.coTopics = this.getCoTopics()
4546
}
4647

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

5051
this.topics.forEach(t => {
51-
if (!t.name.includes(':')) firstArgs.push({
52-
id: t.name,
53-
summary: t.description,
54-
})
52+
if (!t.name.includes(':'))
53+
firstArgs.push({
54+
id: t.name,
55+
summary: t.description,
56+
})
5557
})
5658
this.commands.forEach(c => {
57-
if (!firstArgs.find(a => a.id === c.id) && !c.id.includes(':')) firstArgs.push({
58-
id: c.id,
59-
summary: c.summary,
60-
})
59+
if (!firstArgs.find(a => a.id === c.id) && !c.id.includes(':'))
60+
firstArgs.push({
61+
id: c.id,
62+
summary: c.summary,
63+
})
6164
})
6265

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

7376
if (cmd) {
7477
// if it's a command and has flags, inline flag completion statement.
7578
// skip it from the args statement if it doesn't accept any flag.
7679
if (Object.keys(cmd.flags).length > 0) {
77-
caseBlock += `${arg.id})\n${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n`
80+
caseBlock += ` ${arg.id})\n ${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n`
7881
}
7982
} else {
8083
// it's a topic, redirect to its completion function.
81-
caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n`
84+
caseBlock += ` ${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n`
8285
}
8386
}
8487
}
8588

86-
caseBlock += 'esac\n'
89+
caseBlock += ' esac'
8790

8891
return caseBlock
8992
}
@@ -92,7 +95,6 @@ export default class ZshCompWithSpaces {
9295
`#compdef ${this.config.bin}
9396
9497
${this.topics.map(t => this.genZshTopicCompFun(t.name)).join('\n')}
95-
9698
_${this.config.bin}() {
9799
local context state state_descr line
98100
typeset -A opt_args
@@ -101,11 +103,11 @@ _${this.config.bin}() {
101103
102104
case "$state" in
103105
cmds)
104-
${this.genZshValuesBlock(firstArgs)}
105-
;;
106+
${this.genZshValuesBlock({subArgs: firstArgs})}
107+
;;
106108
args)
107109
${mainArgsCaseBlock()}
108-
;;
110+
;;
109111
esac
110112
}
111113
@@ -114,18 +116,18 @@ _${this.config.bin}
114116
return compFunc
115117
}
116118

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

122124
const flagNames = Object.keys(flags)
123125

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

130132
for (const flagName of flagNames) {
131133
const f = flags[flagName]
@@ -145,27 +147,11 @@ _${this.config.bin}
145147
} else {
146148
flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}`
147149
}
148-
149-
flagSpec += `"[${f.summary}]`
150-
151-
if (f.options) {
152-
flagSpec += `:${f.name} options:(${f.options?.join(' ')})"`
153-
} else {
154-
flagSpec += ':file:_files"'
155-
}
150+
} else if (f.multiple) {
151+
// this flag can be present multiple times on the line
152+
flagSpec += `"*"--${f.name}`
156153
} else {
157-
if (f.multiple) {
158-
// this flag can be present multiple times on the line
159-
flagSpec += '"*"'
160-
}
161-
162-
flagSpec += `--${f.name}"[${f.summary}]:`
163-
164-
if (f.options) {
165-
flagSpec += `${f.name} options:(${f.options.join(' ')})"`
166-
} else {
167-
flagSpec += 'file:_files"'
168-
}
154+
flagSpec += `--${f.name}`
169155
}
170156
} else if (f.char) {
171157
// Flag.Boolean
@@ -175,6 +161,16 @@ _${this.config.bin}
175161
flagSpec += `--${f.name}"[${f.summary}]"`
176162
}
177163

164+
if (f.type === 'option') {
165+
flagSpec += `"[${f.summary}]`
166+
167+
if (f.options) {
168+
flagSpec += `:${f.name} options:(${f.options?.join(' ')})"`
169+
} else {
170+
flagSpec += ':file:_files"'
171+
}
172+
}
173+
178174
flagSpec += ' \\\n'
179175
argumentsBlock += flagSpec
180176
}
@@ -186,65 +182,78 @@ _${this.config.bin}
186182
return argumentsBlock
187183
}
188184

189-
private genZshValuesBlock(subArgs: {id: string; summary?: string}[]): string {
190-
let valuesBlock = '_values "completions" \\\n'
191-
192-
subArgs.forEach(subArg => {
193-
valuesBlock += `"${subArg.id}[${subArg.summary}]" \\\n`
185+
private genZshFlagArgumentsBlock(flags?: CommandFlags): string {
186+
let argumentsBlock = '_arguments -S \\'
187+
this.genZshFlagArguments(flags)
188+
.split('\n')
189+
.forEach(f => {
190+
argumentsBlock += `\n ${f}`
194191
})
195192

196-
return valuesBlock
193+
return argumentsBlock
197194
}
198195

199-
private genZshTopicCompFun(id: string): string {
200-
const coTopics: string[] = []
196+
private genZshValuesBlock(options: {id?: string; subArgs: Array<{id: string; summary?: string}>}): string {
197+
let valuesBlock = '_values "completions"'
198+
const {id, subArgs} = options
201199

202-
for (const topic of this.topics) {
203-
for (const cmd of this.commands) {
204-
if (topic.name === cmd.id) {
205-
coTopics.push(topic.name)
206-
}
200+
subArgs.forEach(subArg => {
201+
valuesBlock += ` \\\n "${subArg.id}[${subArg.summary}]"`
202+
})
203+
204+
if (id) {
205+
const cflags = this.commands.find(c => c.id === id)?.flags
206+
207+
if (cflags) {
208+
// eslint-disable-next-line no-template-curly-in-string
209+
valuesBlock += ' \\\n "${flags[@]}"'
207210
}
208211
}
209212

210-
const flagArgsTemplate = ' "%s")\n %s\n ;;\n'
213+
return valuesBlock
214+
}
211215

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

215-
const isCotopic = coTopics.includes(id)
220+
const isCotopic = this.coTopics.includes(id)
216221

217-
if (isCotopic) {
218-
const compFuncName = `${this.config.bin}_${underscoreSepId}`
222+
let flags = ''
219223

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

226-
${this.genZshFlagArgumentsBlock(this.commands.find(c => c.id === id)?.flags)}
227-
}
227+
if (cflags) {
228+
flags += '\n'
229+
this.genZshFlagArguments(cflags)
230+
.split('\n')
231+
.forEach(f => {
232+
flags += ` ${f}\n`
233+
})
234+
flags += ' '
235+
}
236+
}
228237

238+
if (isCotopic) {
239+
const coTopicCompFunc = `_${this.config.bin}_${underscoreSepId}() {
229240
local context state state_descr line
230241
typeset -A opt_args
231242
243+
local -a flags=(%s)
244+
232245
_arguments -C "1: :->cmds" "*: :->args"
233246
234247
case "$state" in
235248
cmds)
236-
if [[ "\${words[CURRENT]}" == -* ]]; then
237-
_${compFuncName}_flags
238-
else
239-
%s
240-
fi
249+
%s
241250
;;
242251
args)
243-
case $line[1] in
244-
%s
245-
*)
246-
_${compFuncName}_flags
247-
;;
252+
case $line[1] in%s
253+
*)
254+
_arguments -S \\
255+
"\${flags[@]}"
256+
;;
248257
esac
249258
;;
250259
esac
@@ -264,24 +273,24 @@ _${this.config.bin}
264273
summary: t.description,
265274
})
266275

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

270279
this.commands
271280
.filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1)
272281
.forEach(c => {
273-
if (coTopics.includes(c.id)) return
282+
if (this.coTopics.includes(c.id)) return
274283
const subArg = c.id.split(':')[depth]
275284

276285
subArgs.push({
277286
id: subArg,
278287
summary: c.summary,
279288
})
280289

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

284-
return util.format(coTopicCompFunc, this.genZshValuesBlock(subArgs), argsBlock)
293+
return util.format(coTopicCompFunc, flags, this.genZshValuesBlock({id, subArgs}), argsBlock)
285294
}
286295
let argsBlock = ''
287296

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

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

302311
this.commands
303312
.filter(c => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1)
304313
.forEach(c => {
305-
if (coTopics.includes(c.id)) return
314+
if (this.coTopics.includes(c.id)) return
306315
const subArg = c.id.split(':')[depth]
307316

308317
subArgs.push({
309318
id: subArg,
310319
summary: c.summary,
311320
})
312321

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

316325
const topicCompFunc =
@@ -322,22 +331,19 @@ _${this.config.bin}
322331
323332
case "$state" in
324333
cmds)
325-
%s
334+
%s
326335
;;
327336
args)
328-
case $line[1] in
329-
%s
337+
case $line[1] in%s
330338
esac
331339
;;
332-
esac
340+
esac
333341
}
334342
`
335-
return util.format(topicCompFunc, this.genZshValuesBlock(subArgs), argsBlock)
343+
return util.format(topicCompFunc, this.genZshValuesBlock({subArgs}), argsBlock)
336344
}
337345

338-
private get coTopics(): string [] {
339-
if (this._coTopics) return this._coTopics
340-
346+
private getCoTopics(): string[] {
341347
const coTopics: string[] = []
342348

343349
for (const topic of this.topics) {
@@ -348,9 +354,7 @@ _${this.config.bin}
348354
}
349355
}
350356

351-
this._coTopics = coTopics
352-
353-
return this._coTopics
357+
return coTopics
354358
}
355359

356360
private getTopics(): Topic[] {

0 commit comments

Comments
 (0)