ajhahn.de
← Flash
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
}