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.
The minimal shape
Section titled “The minimal shape”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.
What cache = 5s does
Section titled “What cache = 5s does”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.
Adding validate(x)
Section titled “Adding validate(x)”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 nonexistentError: 'nonexistent' is not a valid K8sNamespaceIf 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.
Union types
Section titled “Union types”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.
Using types in rest parameters
Section titled “Using types in rest parameters”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.
Where types live
Section titled “Where types live”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.