sui_tool/db_shell/
completion.rs1use rustyline::Context as RlContext;
14use rustyline::Helper;
15use rustyline::completion::{Completer, Pair};
16use rustyline::error::ReadlineError;
17use rustyline::highlight::Highlighter;
18use rustyline::hint::Hinter;
19use rustyline::validate::Validator;
20use std::sync::Arc;
21
22use crate::db_shell::{
23 backend::Backend,
24 vfs::{VfsPath, resolve_path},
25};
26
27const COMMANDS: &[&str] = &[
28 "ls", "cd", "cat", "dbg", "bcs", "rm", "pwd", "help", "exit", "quit",
29];
30const DEFAULT_COMPLETION_LIMIT: usize = 30;
31
32pub struct ShellHelper {
33 pub backend: Arc<dyn Backend>,
34 pub cwd: VfsPath,
35}
36
37impl Helper for ShellHelper {}
38impl Validator for ShellHelper {}
39impl Highlighter for ShellHelper {}
40impl Hinter for ShellHelper {
41 type Hint = String;
42}
43
44impl Completer for ShellHelper {
45 type Candidate = Pair;
46
47 fn complete(
48 &self,
49 line: &str,
50 pos: usize,
51 _ctx: &RlContext,
52 ) -> Result<(usize, Vec<Pair>), ReadlineError> {
53 let line_so_far = &line[..pos];
54 let tokens: Vec<&str> = line_so_far.split_whitespace().collect();
55
56 let completing_command =
58 tokens.is_empty() || (tokens.len() == 1 && !line_so_far.ends_with(' '));
59
60 if completing_command {
61 let prefix = tokens.first().copied().unwrap_or("");
62 let candidates: Vec<Pair> = COMMANDS
63 .iter()
64 .filter(|cmd| cmd.starts_with(prefix))
65 .map(|cmd| Pair {
66 display: cmd.to_string(),
67 replacement: format!("{cmd} "),
68 })
69 .collect();
70 let start = line_so_far.len() - prefix.len();
71 return Ok((start, candidates));
72 }
73
74 let (path_prefix, token_start) = if line_so_far.ends_with(' ') {
76 ("", pos)
77 } else {
78 let last = tokens.last().copied().unwrap_or("");
79 if last.starts_with('-') {
81 return Ok((pos, vec![]));
82 }
83 let start = pos - last.len();
84 (last, start)
85 };
86
87 let candidates = complete_path(&self.backend, &self.cwd, path_prefix);
88 Ok((token_start, candidates))
89 }
90}
91
92fn complete_path(backend: &Arc<dyn Backend>, cwd: &VfsPath, prefix: &str) -> Vec<Pair> {
93 let (parent_str, name_prefix) = if let Some(slash) = prefix.rfind('/') {
95 (&prefix[..=slash], &prefix[slash + 1..])
96 } else {
97 ("", prefix)
98 };
99
100 let parent_path = if parent_str.is_empty() {
102 cwd.clone()
103 } else {
104 match resolve_path(cwd, parent_str) {
105 Ok(p) => p,
106 Err(_) => return vec![],
107 }
108 };
109
110 if matches!(parent_path, VfsPath::CheckpointsSeqRoot) {
112 return vec![];
113 }
114
115 let entries = match backend.ls_children(&parent_path, DEFAULT_COMPLETION_LIMIT) {
116 Ok(e) => e,
117 Err(_) => return vec![],
118 };
119
120 entries
121 .into_iter()
122 .filter(|e| e.name.starts_with(name_prefix))
123 .map(|e| {
124 let suffix = if e.is_dir { "/" } else { "" };
125 let replacement = format!("{parent_str}{}{suffix}", e.name);
126 Pair {
127 display: format!("{}{suffix}", e.name),
128 replacement,
129 }
130 })
131 .collect()
132}
133
134pub fn is_seq_cursor_level(path: &str) -> bool {
137 path.trim_end_matches('/') == "/checkpoints/seq"
139}