Completion
Overview
This document specifies the completion engine architecture, provider and source interfaces, and integration with the input object.
The completion system provides context-aware candidate suggestions for interactive input. It is built around three layers:
Engine (
term.input.completion) — candidate management, source registry, provider dispatchProvider — domain-specific search logic (e.g. shell commands, Lua symbols); injected into the engine at construction
Sources — individual data backends (filesystem, binaries, environment) queried by the provider
Candidates come from the provider's search() (e.g. matching
filenames, commands) and are held in a single list alongside their
per-candidate metadata.
Rendering is handled by a pluggable renderer attached to the input object. The default renderer draws inline ghost text.
Files
| Path | Role |
|---|---|
src/term/term/input/completion.lua | Core engine |
src/term/term/input/renderer.lua | Renderer interface and inline ghost renderer |
src/term/term/input.lua | Input object integration |
src/shell/shell/completion/shell.lua | Shell mode provider |
src/shell/shell/completion/lua.lua | Lua mode provider |
src/shell/shell/completion/wiki.lua | Wiki mode provider |
src/shell/shell/completion/utils.lua | Shared helpers (match_prefix, match_prefix_keys) |
src/shell/shell/completion/source/ | Completion sources |
src/shell/shell/completion/source/learned.lua | Learned subcommand source (MNEME-backed) |
Engine API
Construction
local completion = require("term.input.completion")
local theme_mod = require("theme")
local comp, err = completion.new({
path = "shell.completion.shell",
tss = theme_mod.subscribe("shell", "completion"),
sources = {
"shell.completion.source.bin",
"shell.completion.source.builtins",
"shell.completion.source.fs",
},
-- additional config fields are passed through as comp.cfg
})
The optional tss field is a theme-subscribe thunk (a closure returned
by theme.subscribe()). When provided, the engine's default get()
uses it for metadata-driven styling — providers no longer need to
implement their own get() method.
The path module is loaded and its exported methods are injected
directly onto the completion object. Each source module path is
loaded and instantiated via its new() constructor.
Candidate methods
| Method | Returns | Description |
|---|---|---|
search(input, history, cursor_pos?) | bool | Provider search; populates candidates via provide() + set_meta() |
available() | bool | Whether candidates exist; auto-selects first |
count() | num | Number of candidates |
get(promoted?) | str | Current candidate; raw when promoted=true, styled otherwise |
chosen_index() | num | Index of selected candidate (1-based, 0 = none) |
set_chosen_index(idx) | Set selected index (clamped to valid range) | |
meta_at(idx) | tbl? | Metadata for candidate at index |
common_prefix() | str? | Longest common prefix across all candidates |
provide(candidates) | Replace candidate list | |
set_meta(metadata) | Replace metadata table | |
flush() | Clear all candidates |
Source management
| Method | Description |
|---|---|
source(name) | Get registered source by name |
update() | Call update() on all sources |
register_source(name, src) | Add a source at runtime |
unregister_source(name) | Remove a source; calls close() if available |
close() | Call close() on all sources |
Promotion
| Method | Returns | Description |
|---|---|---|
promote(candidate, metadata, line) | {line=, action=} | Provider-defined; apply candidate to line |
should_promote_full() | bool | Provider-defined; skip common-prefix narrowing? |
should_auto_promote() | bool | Provider-defined; auto-promote on ENTER? |
When no promote is provided by the provider, the input object
falls back to simple append: line = line .. candidate.
Provider interface
A provider is a Lua module that returns a table of methods. These
are injected onto the completion object at construction time, so
inside all provider methods self refers to the completion instance.
Providers that need to keep state between calls should use the
self.__provider table rather than self.__state (which is
reserved for engine internals like candidates and sources).
Required
-- Populate candidates for the given input.
-- Call self:provide() and self:set_meta() inside.
-- cursor_pos is passed when the input has eol_only = false.
search = function(self, input, history, cursor_pos)
local candidates = self:source("fs"):search(input)
self:provide(candidates)
self:set_meta(metadata)
return self:available()
end
Optional
-- Custom styling for the current candidate.
-- Return raw text when promoted=true, styled text otherwise.
-- Not needed when config.tss is set — the engine's default get()
-- uses the tss thunk with the candidate's source metadata.
get = function(self, promoted)
local l = self.__state.layer
local variant = l.candidates[l.chosen]
if promoted then return variant end
return my_tss:apply(l.meta[l.chosen].source, variant).text
end
-- Apply a candidate to the current line.
-- Return {line = new_line, action = "execute" | nil}.
promote = function(self, candidate, metadata, line)
return { line = line .. candidate }
end
-- Whether the current candidate must skip common-prefix narrowing.
should_promote_full = function(self)
return false
end
-- Whether ENTER should auto-promote the current candidate.
should_auto_promote = function(self)
return false
end
Existing providers
Shell (shell.completion.shell) — command, path, env, history
completion with learned subcommands.
When the input has arguments, a dispatch table routes known
commands (z, x, zx, cd, setenv, unsetenv) to
dedicated handler functions. All other commands fall through to
the generic chain: dedicated builtin handlers (cmds source),
learned subcommands (learned source), then filesystem fallback.
When a builtin handler or learned source returns empty candidates,
the engine falls through to filesystem completion instead of
returning nothing.
Learned subcommands are recorded after each successful command
execution (exit code 0). Recording is chain-aware: the full
positional subcommand chain (up to MAX_CHAIN_DEPTH = 3 levels) is
captured, so git remote add learns remote under git, add
under git → remote, and so on. Each level is a sorted set in the
completions keyspace of shell.mneme, keyed by the binary name plus
preceding subcommands joined by a \x1f separator (depth-0 is just the
binary name). The chain stops at the first flag, path-like token, or
non-subcommand-shaped operand; leading global flags are skipped.
Commands that primarily operate on file paths (rm, ls, cp, mv,
stat, rsync) are excluded entirely, and path-like arguments
(containing /, starting with . or ~, having a file extension, or
resolving to an existing filesystem entry) are never recorded as
subcommands.
Ranking is frecency, not raw frequency: each use increments the
member's score by exp((now - T0) / τ) (τ = 30 days), an ever-growing
weight so recently-used subcommands sort ahead of stale high-frequency
ones. Reverse iteration over the sorted set yields that order
directly.
Flags are learned too: flag tokens (-m, --amend, with any
=value stripped, never the value itself) are stored in a separate
flag_completions keyspace, keyed by the same positional context.
When the partial token starts with -, the learned source suggests
flags for the current context, falling back to shallower contexts down
to the bare command.
Implements promote with replace_prompt / exec_on_prom /
trim_promotion / reduce_spaces metadata.
Lua (shell.completion.lua) — keyword and symbol completion
for Lua mode.
Source interface
Each source is a module that exports new():
local new = function(config)
return {
search = function(self, query, ...)
-- Return array of candidate strings
return { "match_a", "match_b" }
end,
update = function(self)
-- Optional: refresh cached data
end,
close = function(self)
-- Optional: release resources
end,
}
end
return { new = new }
Source utilities
The shell.completion.utils module provides helpers for the
common prefix-match-and-sort pattern used by most sources:
local cu = require("shell.completion.utils")
-- Match array items starting with prefix, return suffixes
cu.match_prefix(items, prefix, opts?)
-- Match table keys starting with prefix, return suffixes
cu.match_prefix_keys(tbl, prefix, opts?)
opts is an optional table: suffix (string to append, default
"") and sort (sort by string length, default true).
Built-in sources (shell)
| Source | Module suffix | Candidates |
|---|---|---|
bin | source.bin | Executables from $PATH |
builtins | source.builtins | Shell builtins and aliases |
fs | source.fs | Filesystem paths |
env | source.env | Environment variables |
cmds | source.cmds | Builtin-specific args (secrets, theme, mneme, lilpack, ...) |
learned | source.learned | Learned subcommand chains and flags from successful executions (frecency-ranked) |
lua_keywords | source.lua_keywords | Lua language keywords |
lua_symbols | source.lua_symbols | Lua symbols and members |
Built-in sources (wiki)
| Source | Module suffix | Candidates |
|---|---|---|
wiki_commands | source.wiki_commands | Wiki mode commands (wiki, search, browse, view, info) |
wiki_dbs | source.wiki_dbs | .mneme files from ~/.local/share/lilush/wiki/ |
wiki_entries | source.wiki_entries | Entry keys from the open wiki database |
wiki_indexes | source.wiki_indexes | Browsing index names from the wiki manifest |
wiki_results | source.wiki_results | Search result keys for the view command |
Renderer interface
A renderer controls how completion candidates are displayed. It is
attached to the input object via the renderer config field.
local renderer = require("term.input.renderer")
-- Use the built-in inline ghost renderer (default)
local r = renderer.new_inline_ghost()
Required methods
-- Render completion candidates at the cursor position.
-- Returns the number of characters drawn (for clearing).
draw = function(self, completion, cursor_col, available_width, tss)
-- completion: the completion object
-- cursor_col: cursor column within the visible area
-- available_width: max drawable characters
-- tss: TSS instance for styling
return draw_length
end
-- Erase previously drawn completion.
clear = function(self, draw_length, blank_char)
end
Inline ghost renderer
The default renderer draws a single candidate as dimmed text after
the cursor, styled as "completion".
TSS rules used:
| Rule | Purpose | Default |
|---|---|---|
completion | Candidate text | Gray ({146, 153, 167}) |
Metadata
Metadata is a table indexed by candidate position (1-based).
Providers can use plain tables or metatables with __index for
lazy generation:
local meta_with_index = function(fn)
local m = {}
setmetatable(m, { __index = fn })
return m
end
self:set_meta(meta_with_index(function(t, key)
if key <= builtins_count then
return { source = "builtin" }
end
return { source = "bin" }
end))
Common fields
| Field | Type | Used by | Description |
|---|---|---|---|
source | str | All providers | TSS rule name for styling |
Shell-specific fields
These are handled by the shell provider's promote() method, not
by the generic engine.
| Field | Type | Description |
|---|---|---|
replace_prompt | str | Replace entire line with this prefix + candidate |
exec_on_prom | bool | Execute immediately after promotion |
trim_promotion | bool | Strip leading whitespace from candidate |
reduce_spaces | bool | Collapse consecutive spaces |
Input integration
The input object (term.input) drives the completion lifecycle.
Configuration
local input = require("term.input")
local inp = input.new({
completion = comp, -- completion object
history = hist, -- history object (passed to search)
renderer = my_renderer, -- custom renderer (default: inline ghost)
eol_only = true, -- complete only at end-of-line (default)
})
Key bindings
| Key | Condition | Action |
|---|---|---|
| Any character | At EOL, no trailing space | search_completion() + draw |
| TAB | Candidates available | promote_completion() — common prefix or full |
| UP | At EOL, candidates available | promote_completion_full() |
| ESC | Buffer not empty | scroll_completion("up") — cycle candidates |
| ENTER | should_auto_promote() | promote_completion_full() — auto-execute |
Completion lifecycle
User types a character at EOL
search_completion()callscompletion:search(line, history)Provider queries sources, calls
provide()+set_meta()draw_completion()delegates to rendererUser presses TAB:
Single candidate or
should_promote_full(): full promotionMultiple candidates: append common prefix, re-search
eol_only flag
When eol_only = false, completion triggers at any cursor position.
The cursor position is passed to the provider's search() as the
cursor_pos parameter. The provider is responsible for determining
what to complete based on cursor position. The promote() method
handles how the candidate is inserted (append, replace token, etc.).
Note: the default inline ghost renderer draws at the cursor, which may overwrite text after the cursor in mid-line mode. A popup renderer is recommended for mid-line completion.
Writing a new provider
Minimal provider for a hypothetical "tags" completion:
-- my_app/completion/tags.lua
local search = function(self, input)
self:flush()
local prefix = input:match("#(%w*)$")
if not prefix then return false end
local tag_source = self:source("tags")
local candidates = tag_source:search(prefix)
self:provide(candidates)
self:set_meta(setmetatable({}, {
__index = function() return { source = "tag" } end,
}))
return self:available()
end
return { search = search }
Usage:
local theme_mod = require("theme")
local comp = completion.new({
path = "my_app.completion.tags",
tss = theme_mod.subscribe("my_app", "completion"),
sources = { "my_app.completion.source.tags" },
})
local inp = input.new({ completion = comp })
The tss thunk handles styling via the source metadata field,
so a custom get() is only needed for non-standard rendering.