1#[derive(Debug, Clone, Default)]
26pub struct CompState {
27 pub current_word: String,
29 pub words: Vec<String>,
31 pub current: usize,
33 pub cursor_pos: usize,
35 pub prefix: String,
37 pub suffix: String,
39 pub buffer: String,
41 pub context: CompContext,
43 pub matches: Vec<CompMatch>,
45 pub active: bool,
47 pub list: bool,
49 pub insert: bool,
51 pub nmatches: usize,
53}
54
55#[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#[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
99pub 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 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
162pub fn addmatch(state: &mut CompState, m: CompMatch) {
164 state.matches.push(m);
165 state.nmatches = state.matches.len();
166}
167
168pub fn get_user_var(
170 name: &str,
171 vars: &std::collections::HashMap<String, String>,
172) -> Option<String> {
173 vars.get(name).cloned()
174}
175
176pub 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
185pub fn tildequote(s: &str) -> String {
187 if s.starts_with('~') {
188 format!("\\{}", s)
189 } else {
190 s.to_string()
191 }
192}
193
194pub 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}