Skip to content

Defining a custom type for completion

When you write fn gco(ref: GitBranch), how does tab-completion know what to suggest? GitBranch is a plain type declaration in the bundled git plugin — and you can write your own.

A completable type needs one method: values(query). It returns the candidates the user might type.

/// A kubernetes namespace from the current context.
type K8sNamespace {
cache = 5s
values(query) {
`kubectl get ns -o name`
.map(x => x.replace("namespace/", ""))
.filter(x => x.startsWith(query))
}
}
/// Switch to a namespace.
fn kns(name: K8sNamespace) {
kubectl config set-context --current --namespace $name
}

Now kns <Tab> pulls live namespaces from kubectl, and kns ku<Tab> narrows to those starting with ku.

values(query) runs every time the user presses Tab. If the underlying call is slow (kubectl, git over a remote, a cloud API), a fresh call per keystroke is painful. cache = <duration> memoizes the result per-query for that TTL.

Supported units: ms, s, m, h, d. Typical values are between 1s (branches that change often) and 5m (cluster lists).

type AwsRegion {
cache = 1h /* regions don't change that often */
values(query) { ... }
}

Errors are never cached — if kubectl is down, the next Tab press retries.

Completion answers “what could the user type?”. Validation answers “does this specific value count?”. Without an explicit validate, lash derives it as “is x in values("")?”:

type K8sNamespace {
cache = 5s
values(query) { /* as above */ }
}
fn kns(name: K8sNamespace) { kubectl config set-context --current --namespace $name }
$ kns nonexistent
Error: 'nonexistent' is not a valid K8sNamespace

If the values list is large and you’d rather not scan it, write a targeted validate(x):

type HexColor {
values(_) { [] }
validate(x) { x.matches("^#[0-9a-fA-F]{6}$") }
}

validate returns a boolean. Any truthy value accepts the argument.

A parameter can accept several types; lash tries each arm in source order:

/// Show a git object — a branch, a tag, or a raw SHA.
fn gshow(ref: GitBranch | GitTag | string) {
git show $ref
}

GitBranch is tried first — its validate runs against the raw input. If it fails, GitTag is tried. If that fails too, the string arm accepts anything, so raw SHAs work. Completion merges the values(query) of all three arms and de-duplicates.

Types compose with rest parameters. This is how the bundled gst works:

/// Show working tree status.
fn gst(...args: [GitPath | GitArg]) {
git status ...args
}

GitPath suggests files in the repo, GitArg suggests git’s flag names, and ...args collects all of them. Tab completion at any position shows both kinds — validated, cached, merged.

type declarations are top-level just like fn. They go in the same .lash files as the functions that use them — typically in ~/.lash/plugins/. lash --list-plugins shows every loaded type with its method list and cache TTL:

Types:
GitBranch (cache 2000ms)
methods: values
A local or remote git branch name.
K8sNamespace (cache 5000ms)
methods: values
A kubernetes namespace from the current context.