Flash 132 lines
// completion — flibc's tab-completion core, ported to Flash from its
// hand-written Zig. The discovery half of FlashOS's shell-first navigation:
// fsh's line editor calls in here when the user presses TAB. Only the pure,
// allocator-free, target-agnostic pieces live here and are host-tested —
// * parse(line) — classify the token under completion as a
// command (the first token) or a path
// argument, splitting a path into a directory
// and a basename prefix
// * hasPrefix / commonPrefixLen — the string folds the driver uses to filter
// candidates and shrink them to a shared stem
// * classify(count, …) — what a TAB did (progressed / stuck / empty),
// the driver's double-TAB branch point
// The candidate gathering itself (a sys_readdir walk over /bin or the path's
// directory, plus fsh's injected built-in names) is the SVC-driven half and
// lives in readline behind its has_driver gate, so this file stays pure and
// off-target-safe.
//
// The second pure port (after keys): it adds no new grammar. It reuses the
// optional-capture `if slash |s| { … }`, `?usize` optionals, value-form
// `if`-expressions, the range-`for` loop (`for i in 0..line.len`), the slice
// expressions, and `&&` / `||` — all already landed. Its core lowers to Zig
// whose token stream matches the reference.
/// What a TAB is completing.
pub const Kind = enum {
command, // the first token — match against /bin + the shell's built-ins
path, // a later token — match against entries of `dir`
}
/// A parsed completion request. `dir` and `prefix` are slices into the caller's
/// line buffer (or static literals). For a command, `dir` is "" (the driver
/// searches /bin). For a path, `dir` is the directory portion — "" means the
/// cwd, "/" the root, "/bin" an absolute dir — and `prefix` the partial
/// basename to extend.
pub const Context = struct {
kind Kind,
dir []u8,
prefix []u8,
}
/// Parse the completion context from the current line. The token under
/// completion is the last whitespace-delimited run; if no earlier token
/// precedes it, it is a command, otherwise a path.
pub fn parse(line []u8) Context {
// Start of the last token = one past the last space/tab.
var tok_start usize = 0
for i in 0..line.len {
if line[i] == ' ' || line[i] == '\t' {
tok_start = i + 1
}
}
// Is there a non-space byte before the token? (an earlier token exists)
var earlier = false
for j in 0..tok_start {
if line[j] != ' ' && line[j] != '\t' {
earlier = true
break
}
}
token := line[tok_start..]
if !earlier {
return .{ .kind = .command, .dir = "", .prefix = token }
}
// Path token: split at the last '/'.
var slash ?usize = null
for k in 0..token.len {
if token[k] == '/' {
slash = k
}
}
if slash |s| {
const dir []u8 = if (s == 0) "/" else token[0..s]
return .{ .kind = .path, .dir = dir, .prefix = token[s + 1 ..] }
}
return .{ .kind = .path, .dir = "", .prefix = token }
}
/// True when `name` starts with `prefix`.
pub fn hasPrefix(name []u8, prefix []u8) bool {
if name.len < prefix.len {
return false
}
for i in 0..prefix.len {
if name[i] != prefix[i] {
return false
}
}
return true
}
/// Length of the longest common prefix of `a` and `b`.
pub fn commonPrefixLen(a []u8, b []u8) usize {
m := #min(a.len, b.len)
var i usize = 0
while i < m && a[i] == b[i] {
i += 1
}
return i
}
/// What a TAB press did to the line — the driver's branch point for double-TAB
/// listing. `count` candidates share the longest common prefix `best_len`; the
/// user has already typed `prefix_len` bytes of the token.
pub const TabClass = enum {
/// The line grew: either a unique match (which also gets a trailing
/// separator) or the common prefix extended past what was typed. Reset the
/// double-TAB streak.
progressed,
/// Two or more candidates already at their common prefix — nothing left to
/// insert. A second consecutive `stuck` TAB lists them.
stuck,
/// Nothing matched; the TAB is inert.
empty,
}
/// Classify a completion attempt from its candidate tally. A unique match always
/// progresses (the driver appends a ' ' / '/' even when the typed token already
/// equals the name); multiple candidates progress only while their common prefix
/// runs past the typed prefix, otherwise they are `stuck` and a redraw-listing is
/// the only forward move.
pub fn classify(count usize, best_len usize, prefix_len usize) TabClass {
if count == 0 {
return .empty
}
if count == 1 {
return .progressed
}
return if (best_len > prefix_len) .progressed else .stuck
}