Generators
Generators let you run shell commands to generate a list of Suggestions for a given Argument.
Generators are used to programmatically generate suggestion objects. For instance, a generator can fetch a list of git remotes, grab all the folders in the current working directory, or even hit an API endpoint.
For a high level overview of Generators and their usage, see "Dynamic Suggestions".
Quick Summary
Generators let you run shell commands on the user's device to generate suggestions for arguments
Basic Generators
Run a shell command to provide dynamic suggestions.
See "Basic Generators" to understand the most common usage pattern.
Templates
Some generators, like the ones for filepaths and folders, are so common across CLI tools that it doesn't make sense for every spec to reimplement them from scratch.
See "Templates" to quickly provide rich suggestions for files and folders.
Contextual Generators
Provide suggestions that require context from other flags or options in the current edit buffer.
To handle this case, a Generator needs to run a script
which incorporates text that the user has typed.
heroku addons:remove --app my-example-app |
For example, when completing this heroku
command, Fig should only suggest addons that are associated with the specific app, my-example-app
.
See "Contextual Generators" to learn how to run a
script
that incorporates text from the user's edit buffer.
Triggers & Filtering
Provide a new set of suggestions after the user types a certain character, like the /
in a filepath, for instance.
See "Reimplementing the Filepath Generator" for more information of how to use
triggers
andfilterTerm
.
Caching & Debouncing
Suggestions are computed by an expensive function — a web request, for instance — and should be cached or debounced.
See "Making an HTTP Request from a Generator" for details on how to handle expensive functions.
Custom Generators
Write imperative code where suggestions must be recomputed on every keypress, to handle tools with non-standard parsing, like chmod
.
See "Custom Generators" to learn more.
Properties
template
OptionalA template which is a single TemplateString
or an array of TemplateStrings
.
template?:
| ("filepaths" | "folders" | "history" | "help")
| ("filepaths" | "folders" | "history" | "help")[]
Examples
cd
uses the "folders" template
ls
uses ["filepaths", "folders"]. Why both? Because if I ls
a directory, we want to enable a user to autoexecute on this directory. If we just did "filepaths" they couldn't autoexecute.
Discussion
Templates are generators prebuilt by Fig. Here are the three templates:
- filepaths: show folders and filepaths. Allow autoexecute on filepaths
- folders: show folders only. Allow autoexecute on folders
- history: show suggestions for all items in history matching this pattern
- help: show subcommands. Only includes the 'siblings' of the nearest 'parent' subcommand
filterTemplateSuggestions
OptionalA function to filter and modify suggestions returned by a template
filterTemplateSuggestions?: (
param: Modify<
Suggestion,
{
name?: string
context:
| {
templateType: "filepaths"
}
| {
templateType: "folders"
}
| {
templateType: "help"
}
| ({
templateType: "history"
} & Partial<{
currentWorkingDirectory: string
time: number
exitCode: number
shell: string
}>)
}
>[]
) => Suggestion[]
Examples
The python spec has an arg object which has a template for "filepaths". However, we don't want to suggest non .py
files. Therefore, we take the output of the template, filter out all files that don't end in .py
, keep all folders that end with /
and return the list of suggestions.
Parameters
Name | Description |
---|---|
templateSuggestions | the array of suggestion objects returned by the given template. |
script
OptionalThe script / shell command you wish to run on the user's device at their shell session's current working directory.
script?: string | ((param: string[]) => string)
Examples
git checkout <branch>
takes one argument which is a git branch. Its arg object has a generator with a script: "git branch"
. The output of this shell command is then passed into the postProcess function to generate the final suggestions.
Discussion
You can either specify
- a string to be executed (like
ls
orgit branch
) - a function to generate the string to be executed. The function takes in an array of tokens of the user input and should output a string. You use a function when the script you run is dependent upon one of the tokens the user has already input (for instance an app name, a Kubernetes token etc.)
After executing the script, the output will be passed to one of
splitOn
orpostProcess
for further processing to produce suggestion objects.
scriptTimeout
OptionalSet the execution timeout of the command specified in the script
prop.
scriptTimeout?: number
postProcess
OptionalProcess the string output from the script
prop and return a list of suggestions
postProcess?: (out: string, tokens: string[]) => Suggestion[]
Parameters
Name | Description |
---|---|
out | The output of the script that was executed on the user's device |
tokens | a tokenized array of what the user has typed |
splitOn
OptionalSyntactic sugar for postProcess
function
splitOn?: string
Examples
Specify \n
and Fig will split on new lines, and turn each line into a suggestion object with name
prop equal to the value on the line.
Discussion
This takes in the text output of script
, splits it on the string you provide here, and then automatically generates an array of suggestion objects for each item.
trigger
OptionalA function run on every keystroke that determines whether Fig should invalidate its cached list of suggestions and instead regenerate its list of suggestions.
trigger?:
| string
| ((newToken: string, oldToken: string) => boolean)
| {
/** Trigger on any change to the token */
on: "change"
}
| {
/** Trigger when the length of the token changes past a threshold */
on: "threshold"
length: number
}
| {
/** Trigger when the index of a string changes */
on: "match"
string: string | string[]
}
Examples
chmod
: If I type chmod u
we should generate suggestions for u+x
, u+r
, u-w
etc. Whereas if I typed chmod 7
we should generate suggestions for 755
or 777
etc.
The suggestion we generate depends on the new information we have. The oldToken was an empty string, the new token could be a 7
or a u
etc…
All this function's job is to say whether or not we should generate new suggestions. It does not specify how to create these new suggestions. This is the job of the script
or custom
props. Annoyingly, you may have to implement some of the same parsing logic again. However, because this is javascript, just create a function so you don't have to repeat yourself :)
Note: yes, we could have generate a list of suggestions at the start for every single permutation of 777 or u+x etc, however, there are so many and this is just not performant!
cd
: Let's say a user has "cd desktop" currently typed then the user types a "/" so the changes to "cd ~/desktop/".
The old token is "~/desktop", new token is "desktop/". This is a perfect time for us to generate a new list of suggestions. We previously had all the folders in my ~ directory as suggestions, but after the total number of /
in the tokens changed, we should trigger a new set of suggestions to be generated. This new set of suggestions should then generate suggestions for the desktop directory, not the ~ directory.
Parameters
Name | Description |
---|---|
newToken | The new token that was just typed by the user e.g. "desktop/"" |
oldToken | The old token that was there before e.g. "desktop" |
Discussion
A note on how Fig works: Suggestions vs Filtered Suggestions
Suggestions: Whenever you type a space indicating the start of a new token, Fig will regenerate a new list of suggestions e.g. git[space]
will generate a list of suggestions for every subcommand, option, and arg
Filtered Suggestions: When you type within the same token (e.g. git c
-> git ch
), Fig takes the token you are currently typing in and uses it to filter over the list of suggestions you have cached. e.g. git c
. The list of suggestions is the same as before, but the filtered suggestions are now commit
, clean
, clone
, and checkout
.
Why don't we recalculate suggestions on each keystroke?
- It's expensive
- We don't need to. The caching works nicely
So what does the trigger do?
The trigger function is run on every keystroke and tells us whether or not we should invalidate the cache and regenerate a list of suggestions.
The trigger function is almost always used with a custom
generator and the getQueryTerm
function to make the autocomplete experience really good (it's how suggestions for cd
work)
It is especially beneficial when you want to generate suggestions for an argument contained inside a single string that is not separated by a space.
What is important to remember?
This function looks at the CHANGE in a token, not the current state of the token. If my token goes from desktop
to desktop/
, should I regenerate suggestions? Remember, users can paste text so theoretically any change is possible. It is totally valid for oldToken to be an empty string and newToken to be a 50 character string!
getQueryTerm
OptionalA function that takes the token that the user has typed and determines which part of it should be used to filter over all the suggestions.
getQueryTerm?: string | ((param: string) => string)
Examples
cd
has a getQueryTerm
function that takes the token the user has typed and returns everything after the last "/".
if the user types cd ~/desktop/a
, the list of suggestions will be all the folders on the user's desktop. We want to filter over these folders with the query term "a"
not ~/desktop/a
Parameters
Name | Description |
---|---|
token | The full token the user is currently typing |
Discussion
Read the note above on how triggers work. Triggers and query term may seem similar but are actually different.
The trigger
function defines when to regenerate new suggestions.
The query
term defines what characters we should use to filter over these suggestions.
The getQueryTerm
function defines the queryTerm
custom
OptionalAn async function that is similar to the function version of script
, however, it gives you full control.
custom?: (
tokens: string[],
executeShellCommand: (
commandToExecute: string,
cwd?: string
) => Promise<string>,
generatorContext: {
currentWorkingDirectory: string
environmentVariables: Record<string, string>
currentProcess: string
/**
* @hidden
* @deprecated
*/
sshPrefix: string
} & {
isDangerous?: boolean
searchTerm: string
}
) => Promise<Suggestion[]>
Examples
In cd
the custom function will combine the current working directory with everything up to the last "/"
in the last token. It will then run ls
at this path and generate a list of suggestions accordingly.
e.g. if the user was currently in their home directory and typed "cd desktop/abcdef", then the custom function would return a list of directories at the ~/desktop
directory
if the user was currently in their home directory and typed "cd desktop/myfolder/secondfolder/aaaaa", then the custom function would return a list of directories at the ~/desktop/my_folder/second_folder
directory
if the user was currently in their home directory and typed "cd /usr/bin/", then the custom function would return a list of directories at the /usr/bin/
directory
const generator: Fig.Generator = {
custom: async (tokens, executeShellCommand) => {
const out = await executeShellCommand("ls");
return out.split("\n").map((elm) => ({ name: elm }));
},
};
Parameters
Name | Description |
---|---|
tokens | a tokenized array of what the user has typed |
executeShellCommand | an async function that allows you to execute a shell command on the user's system and get the output as a string. |
shellContext | an object containing a user's currentWorkingDirectory, currentProcess, and if relevant, the sshPrefix string that can be used if the user is in an SSH session. |
Discussion
This function is effectively script
and postProcess
combined. It is very useful in combination with trigger
and getQueryTerm
to generate suggestions as the user is typing inside a token. Read the description of trigger
for more.
cache
OptionalCache the response of generators for a specific period time and optionally by directory the commands were executed in.
cache?: (
| {
strategy: "max-age"
/**
* The time to live for the cache in milliseconds.
* @example
* 3600
*/
ttl: number
}
| {
strategy?: "stale-while-revalidate"
/**
* The time to live for the cache in milliseconds.
* @example
* 3600
*/
ttl?: number
}
) & {
/**
* Whether the cache should be based on the directory the user was currently in or not.
* @defaultValue false
*/
cacheByDirectory?: boolean
/**
* Hardcoded cache key that can be used to cache a single generator across
* multiple argument locations in a spec.
*/
cacheKey?: string
}
Examples
The kubernetes spec makes use of this.
Discussion
For commands that take a long time to run, Fig gives you the option to cache their response. You can cache the response globally or just by the directory they were run in. We currently have two cache strategies:
stale-while-revalidate
(default): when cache becomes stale fig will return the stale data while fetching the updated one. This strategy accepts attl
(time to live) to configure how long it takes for the cache to become stale.max-age
: will show a loading indicator when cache is stale. You need to specify attl
for how long it takes for the cache to become stale. You can also optionally turn on the ability to just cache by directory (cacheByDirectory: true
)