Skip to main content

xtask_todo_lib/devshell/completion/
candidates.rs

1//! Command and path completion candidates (built-ins, VFS / guest FS listing).
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use super::super::vfs::Vfs;
7use super::super::vm::SessionHolder;
8
9#[cfg(any(unix, feature = "beta-vm"))]
10use super::super::vm::GuestFsOps;
11#[cfg(any(unix, feature = "beta-vm"))]
12use crate::devshell::workspace::logical_path_to_guest;
13
14/// Built-in command names for tab completion (must match command.rs).
15const BUILTIN_COMMANDS: &[&str] = &[
16    "pwd",
17    "cd",
18    "ls",
19    "mkdir",
20    "rustup",
21    "cargo",
22    "cat",
23    "touch",
24    "echo",
25    "save",
26    "export-readonly",
27    "export_readonly",
28    "exit",
29    "quit",
30    "help",
31    "todo",
32];
33
34/// Command completion: case-insensitive prefix match. Returns matching command names.
35#[must_use]
36pub fn complete_commands(prefix: &str) -> Vec<String> {
37    let prefix_lower = prefix.to_lowercase();
38    BUILTIN_COMMANDS
39        .iter()
40        .filter(|c| c.to_lowercase().starts_with(prefix_lower.as_str()))
41        .map(|s| (*s).to_string())
42        .collect()
43}
44
45/// Split path being completed into `(dir_prefix, basename_prefix)`.
46///
47/// `dir_prefix` ends with `/` (or is empty); it is preserved in candidates so readline replaces
48/// the whole token (e.g. `src/` → `src/main.rs`, not `main.rs` alone).
49fn split_dir_and_basename_prefix(prefix: &str) -> (&str, &str) {
50    prefix.rfind('/').map_or(("", prefix), |idx| {
51        let dir = &prefix[..=idx];
52        let rest = &prefix[idx + 1..];
53        (dir, rest)
54    })
55}
56
57/// Path completion: prefix may contain slashes; only the basename segment is matched.
58///
59/// Returned strings are **full token replacements** (include any directory part before the last
60/// `/`), so rustyline's replace-from-`start` keeps paths like `src/main.rs` correct.
61/// `parent_names` are basenames in the resolved parent directory. Empty basename prefix returns all.
62#[must_use]
63pub fn complete_path(prefix: &str, parent_names: &[String]) -> Vec<String> {
64    let (dir_prefix, basename_prefix) = split_dir_and_basename_prefix(prefix);
65    parent_names
66        .iter()
67        .filter(|n| n.starts_with(basename_prefix))
68        .map(|n| format!("{dir_prefix}{n}"))
69        .collect()
70}
71
72/// List directory entry names for path tab completion: **Mode P** (γ or β) uses [`GuestFsOps`] when
73/// guest-primary is active; otherwise [`Vfs::list_dir`].
74#[must_use]
75pub fn list_dir_names_for_completion(
76    vfs: &Rc<RefCell<Vfs>>,
77    vm_session: &Rc<RefCell<SessionHolder>>,
78    abs_parent: &str,
79) -> Vec<String> {
80    #[cfg(any(unix, feature = "beta-vm"))]
81    {
82        let cwd = vfs.borrow().cwd().to_string();
83        let mut session = vm_session.borrow_mut();
84        if let Some((ops, mount)) = session.guest_primary_fs_ops_mut() {
85            if let Ok(guest_path) = logical_path_to_guest(&mount, &cwd, abs_parent) {
86                if let Ok(names) = GuestFsOps::list_dir(ops, &guest_path) {
87                    return names;
88                }
89            }
90        }
91    }
92    #[cfg(not(any(unix, feature = "beta-vm")))]
93    {
94        let _ = vm_session;
95    }
96    vfs.borrow().list_dir(abs_parent).unwrap_or_default()
97}