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 word.starts_with('$') {
321 let var_name = &word[1..];
322 if let Ok(val) = std::env::var(var_name) {
323 results.push(val);
324 }
325 }
326
327 if results.is_empty() {
328 results.push(word.to_string());
329 }
330
331 results
332 }
333
334 fn do_expand_hist(&self, line: &str) -> String {
336 let mut result = line.to_string();
337
338 if result.contains("!!") {
340 result = result.replace("!!", "[last-command]");
341 }
342
343 if result.contains("!$") {
345 result = result.replace("!$", "[last-arg]");
346 }
347
348 result
349 }
350
351 fn get_completions(&self, prefix: &str) -> Vec<String> {
353 let mut completions = Vec::new();
354
355 if prefix.contains('/') || prefix.starts_with('.') {
357 let dir = if let Some(pos) = prefix.rfind('/') {
359 &prefix[..=pos]
360 } else {
361 "./"
362 };
363 let file_prefix = if let Some(pos) = prefix.rfind('/') {
364 &prefix[pos + 1..]
365 } else {
366 prefix
367 };
368
369 if let Ok(entries) = std::fs::read_dir(dir) {
370 for entry in entries.flatten() {
371 let name = entry.file_name().to_string_lossy().to_string();
372 if name.starts_with(file_prefix) {
373 let full_path = if dir == "./" {
374 name
375 } else {
376 format!("{}{}", dir, name)
377 };
378 completions.push(full_path);
379 }
380 }
381 }
382 } else {
383 if let Ok(path) = std::env::var("PATH") {
385 for dir in path.split(':') {
386 if let Ok(entries) = std::fs::read_dir(dir) {
387 for entry in entries.flatten() {
388 let name = entry.file_name().to_string_lossy().to_string();
389 if name.starts_with(prefix) {
390 if !completions.contains(&name) {
391 completions.push(name);
392 }
393 }
394 }
395 }
396 }
397 }
398 }
399
400 completions.sort();
401 completions
402 }
403}
404
405pub const META: char = '\u{83}';
407
408pub fn metafy_line(s: &str) -> String {
411 let mut result = String::with_capacity(s.len() * 2);
412 for c in s.chars() {
413 if c == META || (c as u32) >= 0x83 {
414 result.push(META);
415 result.push(char::from_u32((c as u32) ^ 32).unwrap_or(c));
416 } else {
417 result.push(c);
418 }
419 }
420 result
421}
422
423pub fn unmetafy_line(s: &str) -> String {
426 let mut result = String::with_capacity(s.len());
427 let mut chars = s.chars().peekable();
428
429 while let Some(c) = chars.next() {
430 if c == META {
431 if let Some(&next) = chars.peek() {
432 chars.next();
433 result.push(char::from_u32((next as u32) ^ 32).unwrap_or(next));
434 }
435 } else {
436 result.push(c);
437 }
438 }
439
440 result
441}
442
443pub fn get_cur_cmd(line: &[char], cursor: usize) -> Option<String> {
446 let mut cmd_start = 0;
448
449 for i in 0..cursor {
450 let c = line[i];
451 if c == ';' || c == '|' || c == '&' || c == '(' || c == ')' || c == '`' {
452 cmd_start = i + 1;
453 }
454 }
455
456 while cmd_start < cursor && line[cmd_start].is_whitespace() {
458 cmd_start += 1;
459 }
460
461 let mut cmd_end = cmd_start;
463 while cmd_end < cursor && !line[cmd_end].is_whitespace() {
464 cmd_end += 1;
465 }
466
467 if cmd_start < cmd_end {
468 Some(line[cmd_start..cmd_end].iter().collect())
469 } else {
470 None
471 }
472}
473
474pub fn has_real_token(s: &str) -> bool {
477 let special = ['$', '`', '"', '\'', '\\', '{', '}', '[', ']', '*', '?', '~'];
478
479 let mut escaped = false;
480 for c in s.chars() {
481 if escaped {
482 escaped = false;
483 continue;
484 }
485 if c == '\\' {
486 escaped = true;
487 continue;
488 }
489 if special.contains(&c) {
490 return true;
491 }
492 }
493
494 false
495}
496
497pub fn pfx_len(s1: &str, s2: &str) -> usize {
500 s1.chars()
501 .zip(s2.chars())
502 .take_while(|(a, b)| a == b)
503 .count()
504}
505
506pub fn sfx_len(s1: &str, s2: &str) -> usize {
509 s1.chars()
510 .rev()
511 .zip(s2.chars().rev())
512 .take_while(|(a, b)| a == b)
513 .count()
514}
515
516pub fn quote_string(s: &str, style: QuoteStyle) -> String {
519 match style {
520 QuoteStyle::Single => format!("'{}'", s.replace('\'', "'\\''")),
521 QuoteStyle::Double => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
522 QuoteStyle::Dollar => format!("$'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
523 QuoteStyle::Backslash => {
524 let mut result = String::with_capacity(s.len() * 2);
525 for c in s.chars() {
526 if " \t\n\\'\"`$&|;()<>*?[]{}#~".contains(c) {
527 result.push('\\');
528 }
529 result.push(c);
530 }
531 result
532 }
533 }
534}
535
536#[derive(Debug, Clone, Copy)]
537pub enum QuoteStyle {
538 Single,
539 Double,
540 Dollar,
541 Backslash,
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[test]
549 fn test_pfx_len() {
550 assert_eq!(pfx_len("hello", "help"), 3);
551 assert_eq!(pfx_len("abc", "xyz"), 0);
552 assert_eq!(pfx_len("test", "test"), 4);
553 }
554
555 #[test]
556 fn test_sfx_len() {
557 assert_eq!(sfx_len("testing", "running"), 3);
558 assert_eq!(sfx_len("abc", "xyz"), 0);
559 }
560
561 #[test]
562 fn test_quote_string() {
563 assert_eq!(quote_string("hello", QuoteStyle::Single), "'hello'");
564 assert_eq!(quote_string("it's", QuoteStyle::Single), "'it'\\''s'");
565 assert_eq!(quote_string("hello", QuoteStyle::Double), "\"hello\"");
566 }
567
568 #[test]
569 fn test_has_real_token() {
570 assert!(has_real_token("$HOME"));
571 assert!(has_real_token("*.txt"));
572 assert!(!has_real_token("hello"));
573 assert!(!has_real_token("test\\$var")); }
575}