Skip to main content

sui_tool/db_shell/
completion.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Tab completion for the db-shell.
5//!
6//! Completes:
7//!   - Command names
8//!   - Path arguments: resolves the parent directory and lists its children
9//!
10//! Sequence numbers inside /checkpoints/seq/ are NOT completed because the
11//! integer space is too large. Digest prefixes are completed up to the 30-entry limit.
12
13use 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        // Determine what we're completing.
57        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        // Completing a path argument — find the token being typed.
75        let (path_prefix, token_start) = if line_so_far.ends_with(' ') {
76            ("", pos)
77        } else {
78            let last = tokens.last().copied().unwrap_or("");
79            // Don't try to complete flags like --limit.
80            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    // Split prefix into (parent_path_str, name_prefix).
94    let (parent_str, name_prefix) = if let Some(slash) = prefix.rfind('/') {
95        (&prefix[..=slash], &prefix[slash + 1..])
96    } else {
97        ("", prefix)
98    };
99
100    // Resolve the parent to a VfsPath.
101    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    // Do NOT complete sequence number children of /checkpoints/seq — too many.
111    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
134/// Parse a path string, returning `None` if it resolves to a sequence-number
135/// level that should not be completed (to avoid the huge integer space).
136pub fn is_seq_cursor_level(path: &str) -> bool {
137    // Don't complete paths that would iterate /checkpoints/seq/ without a prefix
138    path.trim_end_matches('/') == "/checkpoints/seq"
139}