Skip to content

Commit f05bcb9

Browse files
committed
feat: fig arg suggestions
1 parent 279474f commit f05bcb9

File tree

1 file changed

+63
-28
lines changed

1 file changed

+63
-28
lines changed

src/main/utils/completion.ts

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,24 @@ function stripFigCursor(insertion: string) {
5959
return insertion.slice(0, index) + suffix + '\u001b[D'.repeat(suffix.length)
6060
}
6161

62+
function getFigSeparator(spec: Fig.Option) {
63+
return spec.requiresSeparator
64+
? (typeof spec.requiresSeparator === 'string' ? spec.requiresSeparator : '=')
65+
: (spec.requiresEquals ? '=' : undefined)
66+
}
67+
6268
function getFigValues(spec: Fig.Subcommand | Fig.Suggestion | Fig.Option) {
6369
if (spec.insertValue) {
6470
return [stripFigCursor(spec.insertValue)]
6571
}
66-
return normalizeArray(spec.name)
72+
const names = normalizeArray(spec.name)
73+
if ('requiresSeparator' in spec || 'requiresEquals' in spec) {
74+
const separator = getFigSeparator(spec)
75+
if (separator) {
76+
return names.map(name => name + separator)
77+
}
78+
}
79+
return names
6780
}
6881

6982
function getFigArgsLabel(spec: Fig.Subcommand | Fig.Option, name: string) {
@@ -136,22 +149,34 @@ function transformFigSubcommand(spec: Fig.Subcommand, query: string) {
136149
}))
137150
}
138151

152+
function isMatchFigOption(value: string, raw: string | Fig.Option) {
153+
const spec = typeof raw === 'string' ? { name: raw } : raw
154+
const names = normalizeArray(spec.name)
155+
const separator = getFigSeparator(spec)
156+
return names.some(name => {
157+
if (separator) {
158+
return value.startsWith(name + separator)
159+
} else {
160+
if (value === name) return true
161+
// e.g. `-aL` matches both `-a` and `-L`
162+
if (/^-\w$/.test(name)) {
163+
return /^-\w/.test(value) && value.includes(name[1])
164+
}
165+
return false
166+
}
167+
})
168+
}
169+
139170
function transformFigOption(spec: Fig.Option, query: string, args: string[]) {
140171
if (spec.hidden) return []
141-
let values = getFigValues(spec)
142-
const separator = spec.requiresSeparator
143-
? (typeof spec.requiresSeparator === 'string' ? spec.requiresSeparator : '=')
144-
: (spec.requiresEquals ? '=' : undefined)
145-
if (separator) {
146-
values = values.map(value => value + separator)
147-
}
172+
const values = getFigValues(spec)
148173
const max = spec.isRepeatable
149174
? (typeof spec.isRepeatable === 'number' ? spec.isRepeatable : Infinity)
150175
: 1
151-
const times = args.filter(arg => {
152-
return values.some(value => (separator ? arg.startsWith(value) : arg === value))
153-
}).length
176+
const times = args.filter(arg => isMatchFigOption(arg, spec)).length
154177
if (times > max) return []
178+
const exclusiveOn = spec.exclusiveOn ?? []
179+
if (exclusiveOn.some(exclusive => args.some(arg => isMatchFigOption(arg, exclusive)))) return []
155180
return values.map<CommandCompletion>(value => ({
156181
type: 'command',
157182
query,
@@ -166,15 +191,26 @@ function transformFigOption(spec: Fig.Option, query: string, args: string[]) {
166191

167192
function getFigArgCompletions(spec: Fig.Subcommand | Fig.Option, query: string, context: FigContext) {
168193
const specArgs = normalizeArray(spec.args)
169-
return flatAsync(specArgs.map(arg => {
194+
if (!specArgs.length) return []
195+
// When inputting `--foo=bar`, if spec has name `--foo` and separator `=`, just make query to be `bar`
196+
const separator = getFigSeparator(spec)
197+
const names = normalizeArray(spec.name)
198+
if (separator) {
199+
const usage = names.find(name => query.startsWith(name + separator))
200+
if (usage) {
201+
query = query.slice(usage.length + separator.length)
202+
}
203+
}
204+
return flatAsync(specArgs.map(async arg => {
170205
const generators = [
171206
...(arg.template ? [{ template: arg.template }] : []),
172207
...normalizeArray(arg.generators),
173208
]
174-
return flatAsync(generators.map(async generator => {
175-
const suggestions = await generateFigSuggestions(generator, context)
176-
return suggestions.flatMap(suggestion => transformFigSuggestion(suggestion, query))
177-
}))
209+
const suggestions = await flatAsync([
210+
arg.suggestions ?? [],
211+
...generators.map(generator => generateFigSuggestions(generator, context)),
212+
])
213+
return suggestions.flatMap(suggestion => transformFigSuggestion(suggestion, query))
178214
}))
179215
}
180216

@@ -197,18 +233,17 @@ async function getFigCompletions(
197233
suggestions.flatMap(suggestion => transformFigSuggestion(suggestion, query, true)),
198234
)
199235
// Option args
200-
if (args.length > 1) {
201-
// Option args since option spec will not pass to `getFigCompletions`
202-
const previousArg = args[args.length - 2]
203-
const inputtingOption = options.find(item => {
204-
const names = normalizeArray(item.name)
205-
return names.includes(previousArg)
206-
})
207-
if (inputtingOption) {
208-
asyncCompletions.push(
209-
getFigArgCompletions(inputtingOption, query, context),
210-
)
211-
}
236+
// Option args since option spec will not pass to `getFigCompletions`
237+
const previousArg = args.length > 1 ? args[args.length - 2] : undefined
238+
const inputtingOption = options.find(item => {
239+
const separator = getFigSeparator(item)
240+
const optionArg = separator ? query : previousArg
241+
return optionArg === undefined ? false : isMatchFigOption(optionArg, item)
242+
})
243+
if (inputtingOption) {
244+
asyncCompletions.push(
245+
getFigArgCompletions(inputtingOption, query, context),
246+
)
212247
}
213248
// Subcommands (suggest if no subcommand, call recursively otherwise)
214249
const subcommands = spec.subcommands ?? []

0 commit comments

Comments
 (0)