1use super::main::Zle;
14
15#[derive(Debug, Default, Clone)]
17pub struct CompletionState {
18 pub in_menu: bool,
20 pub menu_index: usize,
22 pub completions: Vec<String>,
24 pub prefix: String,
26 pub suffix: String,
28 pub word_start: usize,
30 pub word_end: usize,
32 pub last_menu: bool,
34}
35
36#[derive(Debug, Clone)]
38pub struct BraceInfo {
39 pub str_val: String,
40 pub pos: usize,
41 pub cur_pos: usize,
42 pub qpos: usize,
43 pub curlen: usize,
44}
45
46impl Zle {
47 pub fn complete_word(&mut self, state: &mut CompletionState) {
50 self.do_complete(state, false, false);
51 }
52
53 pub fn menu_complete(&mut self, state: &mut CompletionState) {
56 if state.in_menu && !state.completions.is_empty() {
57 state.menu_index = (state.menu_index + 1) % state.completions.len();
59 self.apply_completion(state);
60 } else {
61 self.do_complete(state, true, false);
62 }
63 }
64
65 pub fn reverse_menu_complete(&mut self, state: &mut CompletionState) {
68 if state.in_menu && !state.completions.is_empty() {
69 if state.menu_index == 0 {
70 state.menu_index = state.completions.len() - 1;
71 } else {
72 state.menu_index -= 1;
73 }
74 self.apply_completion(state);
75 }
76 }
77
78 pub fn expand_or_complete(&mut self, state: &mut CompletionState) {
81 if !self.try_expand() {
83 self.do_complete(state, false, false);
85 }
86 }
87
88 pub fn expand_or_complete_prefix(&mut self, state: &mut CompletionState) {
91 state.suffix = self.zleline[self.zlecs..].iter().collect();
92 self.expand_or_complete(state);
93 }
94
95 pub fn list_choices(&mut self, state: &mut CompletionState) {
98 self.do_complete(state, false, true);
99
100 if !state.completions.is_empty() {
101 println!();
102 for (i, c) in state.completions.iter().enumerate() {
103 if i > 0 && i % 5 == 0 {
104 println!();
105 }
106 print!("{:<16}", c);
107 }
108 println!();
109 self.resetneeded = true;
110 }
111 }
112
113 pub fn list_expand(&mut self) {
116 let word = self.get_word_at_cursor();
117 let expansions = self.do_expansion(&word);
118
119 if !expansions.is_empty() {
120 println!();
121 for exp in &expansions {
122 println!("{}", exp);
123 }
124 self.resetneeded = true;
125 }
126 }
127
128 pub fn expand_word(&mut self) {
131 let _ = self.try_expand();
132 }
133
134 pub fn expand_history(&mut self) {
137 let line: String = self.zleline.iter().collect();
138
139 let expanded = self.do_expand_hist(&line);
141
142 if expanded != line {
143 self.zleline = expanded.chars().collect();
144 self.zlell = self.zleline.len();
145 if self.zlecs > self.zlell {
146 self.zlecs = self.zlell;
147 }
148 self.resetneeded = true;
149 }
150 }
151
152 pub fn magic_space(&mut self) {
155 self.expand_history();
156 self.self_insert(' ');
157 }
158
159 pub fn delete_char_or_list(&mut self, state: &mut CompletionState) {
162 if self.zlecs < self.zlell {
163 self.delete_char();
164 } else {
165 self.list_choices(state);
166 }
167 }
168
169 pub fn accept_and_menu_complete(&mut self, state: &mut CompletionState) -> Option<String> {
172 let line = self.accept_line();
173 state.in_menu = false;
174 Some(line)
175 }
176
177 pub fn spell_word(&mut self) {
180 let word = self.get_word_at_cursor();
182 let _ = word;
184 }
185
186 fn do_complete(&mut self, state: &mut CompletionState, menu_mode: bool, list_only: bool) {
188 let (word_start, word_end) = self.get_word_bounds();
190 let word: String = self.zleline[word_start..word_end].iter().collect();
191
192 state.word_start = word_start;
193 state.word_end = word_end;
194 state.prefix = word.clone();
195
196 state.completions = self.get_completions(&word);
198
199 if state.completions.is_empty() {
200 return;
201 }
202
203 if list_only {
204 return;
205 }
206
207 if menu_mode || state.completions.len() > 1 {
208 state.in_menu = true;
209 state.menu_index = 0;
210 self.apply_completion(state);
211 } else if state.completions.len() == 1 {
212 state.menu_index = 0;
214 self.apply_completion(state);
215 state.in_menu = false;
216 }
217 }
218
219 fn apply_completion(&mut self, state: &CompletionState) {
221 if state.completions.is_empty() {
222 return;
223 }
224
225 let completion = &state.completions[state.menu_index];
226
227 self.zleline.drain(state.word_start..state.word_end);
229 self.zlell = self.zleline.len();
230 self.zlecs = state.word_start;
231
232 for c in completion.chars() {
234 self.zleline.insert(self.zlecs, c);
235 self.zlecs += 1;
236 }
237 self.zlell = self.zleline.len();
238 self.resetneeded = true;
239 }
240
241 fn get_word_at_cursor(&self) -> String {
243 let (start, end) = self.get_word_bounds();
244 self.zleline[start..end].iter().collect()
245 }
246
247 fn get_word_bounds(&self) -> (usize, usize) {
249 let mut start = self.zlecs;
250 let mut end = self.zlecs;
251
252 while start > 0 && !self.zleline[start - 1].is_whitespace() {
254 start -= 1;
255 }
256
257 while end < self.zlell && !self.zleline[end].is_whitespace() {
259 end += 1;
260 }
261
262 (start, end)
263 }
264
265 fn try_expand(&mut self) -> bool {
267 let word = self.get_word_at_cursor();
268
269 if word.is_empty() {
270 return false;
271 }
272
273 let expansions = self.do_expansion(&word);
274
275 if expansions.is_empty() || (expansions.len() == 1 && expansions[0] == word) {
276 return false;
277 }
278
279 let (start, end) = self.get_word_bounds();
280
281 self.zleline.drain(start..end);
283 self.zlecs = start;
284
285 let expanded = expansions.join(" ");
287 for c in expanded.chars() {
288 self.zleline.insert(self.zlecs, c);
289 self.zlecs += 1;
290 }
291 self.zlell = self.zleline.len();
292 self.resetneeded = true;
293
294 true
295 }
296
297 fn do_expansion(&self, word: &str) -> Vec<String> {
299 let mut results = Vec::new();
300
301 if word.contains('*') || word.contains('?') || word.contains('[') {
303 if let Ok(paths) = glob::glob(word) {
305 for path in paths.flatten() {
306 results.push(path.display().to_string());
307 }
308 }
309 }
310
311 if word.starts_with('~') {
313 if let Some(home) = std::env::var_os("HOME") {
314 let expanded = word.replacen('~', home.to_str().unwrap_or("~"), 1);
315 results.push(expanded);
316 }
317 }
318
319 if let Some(var_name) = word.strip_prefix('$') {
321 if let Ok(val) = std::env::var(var_name) {
322 results.push(val);
323 }
324 }
325
326 if results.is_empty() {
327 results.push(word.to_string());
328 }
329
330 results
331 }
332
333 fn do_expand_hist(&self, line: &str) -> String {
335 let mut result = line.to_string();
336
337 if result.contains("!!") {
339 result = result.replace("!!", "[last-command]");
340 }
341
342 if result.contains("!$") {
344 result = result.replace("!$", "[last-arg]");
345 }
346
347 result
348 }
349
350 fn get_completions(&self, prefix: &str) -> Vec<String> {
352 let mut completions = Vec::new();
353
354 if prefix.contains('/') || prefix.starts_with('.') {
356 let dir = if let Some(pos) = prefix.rfind('/') {
358 &prefix[..=pos]
359 } else {
360 "./"
361 };
362 let file_prefix = if let Some(pos) = prefix.rfind('/') {
363 &prefix[pos + 1..]
364 } else {
365 prefix
366 };
367
368 if let Ok(entries) = std::fs::read_dir(dir) {
369 for entry in entries.flatten() {
370 let name = entry.file_name().to_string_lossy().to_string();
371 if name.starts_with(file_prefix) {
372 let full_path = if dir == "./" {
373 name
374 } else {
375 format!("{}{}", dir, name)
376 };
377 completions.push(full_path);
378 }
379 }
380 }
381 } else {
382 if let Ok(path) = std::env::var("PATH") {
384 for dir in path.split(':') {
385 if let Ok(entries) = std::fs::read_dir(dir) {
386 for entry in entries.flatten() {
387 let name = entry.file_name().to_string_lossy().to_string();
388 if name.starts_with(prefix) && !completions.contains(&name) {
389 completions.push(name);
390 }
391 }
392 }
393 }
394 }
395 }
396
397 completions.sort();
398 completions
399 }
400}
401
402pub const META: char = '\u{83}';
404
405pub fn metafy_line(s: &str) -> String {
408 let mut result = String::with_capacity(s.len() * 2);
409 for c in s.chars() {
410 if c == META || (c as u32) >= 0x83 {
411 result.push(META);
412 result.push(char::from_u32((c as u32) ^ 32).unwrap_or(c));
413 } else {
414 result.push(c);
415 }
416 }
417 result
418}
419
420pub fn unmetafy_line(s: &str) -> String {
423 let mut result = String::with_capacity(s.len());
424 let mut chars = s.chars().peekable();
425
426 while let Some(c) = chars.next() {
427 if c == META {
428 if let Some(&next) = chars.peek() {
429 chars.next();
430 result.push(char::from_u32((next as u32) ^ 32).unwrap_or(next));
431 }
432 } else {
433 result.push(c);
434 }
435 }
436
437 result
438}
439
440pub fn get_cur_cmd(line: &[char], cursor: usize) -> Option<String> {
443 let mut cmd_start = 0;
445
446 for i in 0..cursor {
447 let c = line[i];
448 if c == ';' || c == '|' || c == '&' || c == '(' || c == ')' || c == '`' {
449 cmd_start = i + 1;
450 }
451 }
452
453 while cmd_start < cursor && line[cmd_start].is_whitespace() {
455 cmd_start += 1;
456 }
457
458 let mut cmd_end = cmd_start;
460 while cmd_end < cursor && !line[cmd_end].is_whitespace() {
461 cmd_end += 1;
462 }
463
464 if cmd_start < cmd_end {
465 Some(line[cmd_start..cmd_end].iter().collect())
466 } else {
467 None
468 }
469}
470
471pub fn has_real_token(s: &str) -> bool {
474 let special = ['$', '`', '"', '\'', '\\', '{', '}', '[', ']', '*', '?', '~'];
475
476 let mut escaped = false;
477 for c in s.chars() {
478 if escaped {
479 escaped = false;
480 continue;
481 }
482 if c == '\\' {
483 escaped = true;
484 continue;
485 }
486 if special.contains(&c) {
487 return true;
488 }
489 }
490
491 false
492}
493
494pub fn pfx_len(s1: &str, s2: &str) -> usize {
497 s1.chars()
498 .zip(s2.chars())
499 .take_while(|(a, b)| a == b)
500 .count()
501}
502
503pub fn sfx_len(s1: &str, s2: &str) -> usize {
506 s1.chars()
507 .rev()
508 .zip(s2.chars().rev())
509 .take_while(|(a, b)| a == b)
510 .count()
511}
512
513pub fn quote_string(s: &str, style: QuoteStyle) -> String {
516 match style {
517 QuoteStyle::Single => format!("'{}'", s.replace('\'', "'\\''")),
518 QuoteStyle::Double => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
519 QuoteStyle::Dollar => format!("$'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
520 QuoteStyle::Backslash => {
521 let mut result = String::with_capacity(s.len() * 2);
522 for c in s.chars() {
523 if " \t\n\\'\"`$&|;()<>*?[]{}#~".contains(c) {
524 result.push('\\');
525 }
526 result.push(c);
527 }
528 result
529 }
530 }
531}
532
533#[derive(Debug, Clone, Copy)]
534pub enum QuoteStyle {
535 Single,
536 Double,
537 Dollar,
538 Backslash,
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[test]
546 fn test_pfx_len() {
547 assert_eq!(pfx_len("hello", "help"), 3);
548 assert_eq!(pfx_len("abc", "xyz"), 0);
549 assert_eq!(pfx_len("test", "test"), 4);
550 }
551
552 #[test]
553 fn test_sfx_len() {
554 assert_eq!(sfx_len("testing", "running"), 3);
555 assert_eq!(sfx_len("abc", "xyz"), 0);
556 }
557
558 #[test]
559 fn test_quote_string() {
560 assert_eq!(quote_string("hello", QuoteStyle::Single), "'hello'");
561 assert_eq!(quote_string("it's", QuoteStyle::Single), "'it'\\''s'");
562 assert_eq!(quote_string("hello", QuoteStyle::Double), "\"hello\"");
563 }
564
565 #[test]
566 fn test_has_real_token() {
567 assert!(has_real_token("$HOME"));
568 assert!(has_real_token("*.txt"));
569 assert!(!has_real_token("hello"));
570 assert!(!has_real_token("test\\$var")); }
572}