// 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 }