Skip to main content

zsh/zle/
compcore_port.rs

1//! Completion core for ZLE
2//!
3//! Port from zsh/Src/Zle/compcore.c (3,638 lines)
4//!
5//! The full completion engine is implemented in the `compsys` crate
6//! (compsys/compcore.rs, 644 lines). This module provides the ZLE-side
7//! interface that connects the editor to the completion system.
8//!
9//! Key C functions and their Rust locations:
10//! - do_completion     → compsys::compcore::do_completion()
11//! - before_complete   → compsys::compcore::before_complete()
12//! - after_complete    → compsys::compcore::after_complete()
13//! - callcompfunc      → compsys::shell_runner (completion function eval)
14//! - makecomplist      → compsys::compcore::make_comp_list()
15//! - addmatch          → compsys::compadd::add_match()
16//! - addmatches        → compsys::compadd::add_matches()
17//! - comp_str          → compsys::compset (word extraction)
18//! - set_comp_sep      → compsys::compset::set_comp_sep()
19//! - check_param       → compsys::base (parameter completion)
20//! - multiquote        → compsys::base::multiquote()
21//! - tildequote        → compsys::base::tildequote()
22//! - ctokenize         → compsys::base::ctokenize()
23
24/// Completion state passed between ZLE and the completion system
25#[derive(Debug, Clone, Default)]
26pub struct CompState {
27    /// Current word being completed
28    pub current_word: String,
29    /// Words on the command line
30    pub words: Vec<String>,
31    /// Index of current word (1-based, zsh style)
32    pub current: usize,
33    /// Cursor position within current word
34    pub cursor_pos: usize,
35    /// Prefix before cursor in current word
36    pub prefix: String,
37    /// Suffix after cursor in current word
38    pub suffix: String,
39    /// The complete command line
40    pub buffer: String,
41    /// Whether we're in a special context (redirect, assignment, etc.)
42    pub context: CompContext,
43    /// Matches found
44    pub matches: Vec<CompMatch>,
45    /// Whether completion is active
46    pub active: bool,
47    /// Whether to show listing
48    pub list: bool,
49    /// Whether to insert immediately
50    pub insert: bool,
51    /// Number of matches
52    pub nmatches: usize,
53}
54
55/// Completion context
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub enum CompContext {
58    #[default]
59    Command,
60    Argument,
61    Redirect,
62    Assignment,
63    Subscript,
64    Math,
65    Condition,
66    Array,
67    Brace,
68}
69
70/// A completion match
71#[derive(Debug, Clone)]
72pub struct CompMatch {
73    pub word: String,
74    pub description: Option<String>,
75    pub group: Option<String>,
76    pub prefix: String,
77    pub suffix: String,
78    pub display: Option<String>,
79}
80
81impl CompMatch {
82    pub fn new(word: &str) -> Self {
83        CompMatch {
84            word: word.to_string(),
85            description: None,
86            group: None,
87            prefix: String::new(),
88            suffix: String::new(),
89            display: None,
90        }
91    }
92
93    pub fn with_description(mut self, desc: &str) -> Self {
94        self.description = Some(desc.to_string());
95        self
96    }
97}
98
99/// Initialize completion for a line (from compcore.c do_completion)
100pub fn init_completion(buffer: &str, cursor: usize) -> CompState {
101    let mut state = CompState::default();
102    state.buffer = buffer.to_string();
103    state.active = true;
104
105    // Split into words
106    let mut words = Vec::new();
107    let mut current = 0;
108    let mut word_start = 0;
109    let mut in_word = false;
110    let mut in_quote = false;
111    let mut quote_char = '\0';
112
113    for (i, c) in buffer.char_indices() {
114        if in_quote {
115            if c == quote_char {
116                in_quote = false;
117            }
118            continue;
119        }
120        if c == '\'' || c == '"' {
121            in_quote = true;
122            quote_char = c;
123            if !in_word {
124                word_start = i;
125                in_word = true;
126            }
127            continue;
128        }
129        if c.is_whitespace() {
130            if in_word {
131                words.push(buffer[word_start..i].to_string());
132                if cursor >= word_start && cursor <= i {
133                    current = words.len();
134                }
135                in_word = false;
136            }
137        } else if !in_word {
138            word_start = i;
139            in_word = true;
140        }
141    }
142    if in_word {
143        words.push(buffer[word_start..].to_string());
144        if cursor >= word_start {
145            current = words.len();
146        }
147    }
148    if words.is_empty() || cursor >= buffer.len() {
149        words.push(String::new());
150        current = words.len();
151    }
152
153    state.words = words;
154    state.current = current;
155    if current > 0 && current <= state.words.len() {
156        state.current_word = state.words[current - 1].clone();
157    }
158
159    state
160}
161
162/// Add a match to the completion state (from compcore.c addmatch/add_match_data)
163pub fn addmatch(state: &mut CompState, m: CompMatch) {
164    state.matches.push(m);
165    state.nmatches = state.matches.len();
166}
167
168/// Get user variable for completion (from compcore.c get_user_var)
169pub fn get_user_var(
170    name: &str,
171    vars: &std::collections::HashMap<String, String>,
172) -> Option<String> {
173    vars.get(name).cloned()
174}
175
176/// Quote a string for completion insertion (from compcore.c multiquote)
177pub fn multiquote(s: &str, in_quotes: bool) -> String {
178    if in_quotes {
179        s.replace('\\', "\\\\").replace('\'', "\\'")
180    } else {
181        crate::utils::quote_string(s)
182    }
183}
184
185/// Quote tilde in completion (from compcore.c tildequote)
186pub fn tildequote(s: &str) -> String {
187    if s.starts_with('~') {
188        format!("\\{}", s)
189    } else {
190        s.to_string()
191    }
192}
193
194/// Remove backslashes from completion word (from compcore.c rembslash)
195pub fn rembslash(s: &str) -> String {
196    let mut result = String::with_capacity(s.len());
197    let mut escape = false;
198    for c in s.chars() {
199        if escape {
200            result.push(c);
201            escape = false;
202        } else if c == '\\' {
203            escape = true;
204        } else {
205            result.push(c);
206        }
207    }
208    result
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_init_completion() {
217        let state = init_completion("git commit -m ", 14);
218        assert_eq!(state.words, vec!["git", "commit", "-m", ""]);
219        assert!(state.active);
220    }
221
222    #[test]
223    fn test_addmatch() {
224        let mut state = CompState::default();
225        addmatch(&mut state, CompMatch::new("hello"));
226        addmatch(&mut state, CompMatch::new("world"));
227        assert_eq!(state.nmatches, 2);
228    }
229
230    #[test]
231    fn test_multiquote() {
232        assert_eq!(multiquote("it's", false), "'it'\\''s'");
233    }
234
235    #[test]
236    fn test_tildequote() {
237        assert_eq!(tildequote("~user"), "\\~user");
238        assert_eq!(tildequote("/home"), "/home");
239    }
240
241    #[test]
242    fn test_rembslash() {
243        assert_eq!(rembslash("hello\\ world"), "hello world");
244        assert_eq!(rembslash("no\\\\slash"), "no\\slash");
245    }
246}