raps_cli/shell/
completer.rs1use std::collections::HashMap;
5
6use reedline::{Completer, Span, Suggestion};
7
8use super::CommandInfo;
9use super::command_tree::{build_command_map, build_command_tree};
10
11pub struct RapsCompleter {
13 commands: Vec<CommandInfo>,
14 command_map: HashMap<String, CommandInfo>,
15}
16
17impl RapsCompleter {
18 pub fn new() -> Self {
19 let commands = build_command_tree();
20 let command_map = build_command_map(&commands);
21 Self {
22 commands,
23 command_map,
24 }
25 }
26}
27
28impl Default for RapsCompleter {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl Completer for RapsCompleter {
35 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
36 let input = line.get(..pos).unwrap_or(line);
37 let raw = get_completions_raw(&self.commands, &self.command_map, input);
38
39 let start = if input.ends_with(' ') {
40 pos
41 } else {
42 input.rfind(' ').map(|i| i + 1).unwrap_or(0)
43 };
44
45 raw.into_iter()
46 .map(|(replacement, description)| Suggestion {
47 value: replacement,
48 description: Some(description),
49 style: None,
50 extra: None,
51 span: Span::new(start, pos),
52 append_whitespace: true,
53 match_indices: None,
54 })
55 .collect()
56 }
57}
58
59pub(super) fn get_completions_raw(
61 commands: &[CommandInfo],
62 command_map: &HashMap<String, CommandInfo>,
63 line: &str,
64) -> Vec<(String, String)> {
65 let parts: Vec<&str> = line.split_whitespace().collect();
66 let mut completions = Vec::new();
67
68 match parts.len() {
69 0 => {
70 for cmd in commands {
72 completions.push((cmd.name.to_string(), cmd.description.to_string()));
73 }
74 }
75 1 => {
76 let partial = parts[0].to_lowercase();
77 let trailing_space = line.ends_with(' ');
78
79 if trailing_space {
80 if let Some(cmd) = commands.iter().find(|c| c.name == partial) {
82 for subcmd in cmd.subcommands {
83 completions.push((subcmd.name.to_string(), subcmd.description.to_string()));
84 }
85 }
86 } else {
87 for cmd in commands {
89 if cmd.name.starts_with(&partial) {
90 completions.push((cmd.name.to_string(), cmd.description.to_string()));
91 }
92 }
93 }
94 }
95 2 => {
96 let cmd_name = parts[0].to_lowercase();
97 let partial = parts[1].to_lowercase();
98 let trailing_space = line.ends_with(' ');
99
100 if let Some(cmd) = commands.iter().find(|c| c.name == cmd_name) {
101 if trailing_space {
102 if let Some(subcmd) = cmd.subcommands.iter().find(|s| s.name == partial) {
104 for flag in subcmd.flags {
105 let flag_name = flag.split_whitespace().next().unwrap_or(flag);
106 completions.push((flag_name.to_string(), "(optional)".to_string()));
107 }
108 }
109 } else {
110 for subcmd in cmd.subcommands {
112 if subcmd.name.starts_with(&partial) {
113 completions
114 .push((subcmd.name.to_string(), subcmd.description.to_string()));
115 }
116 }
117 }
118 }
119 }
120 _ => {
121 let cmd_name = parts[0].to_lowercase();
123 let sub_name = parts[1].to_lowercase();
124 let key = format!("{} {}", cmd_name, sub_name);
125
126 if let Some(cmd) = command_map.get(&key) {
127 let last = parts.last().unwrap_or(&"");
128 let trailing_space = line.ends_with(' ');
129
130 if trailing_space || last.starts_with('-') {
131 for flag in cmd.flags {
132 let flag_name = flag.split_whitespace().next().unwrap_or(flag);
133 if trailing_space || flag_name.starts_with(last) {
134 completions.push((flag_name.to_string(), "(optional)".to_string()));
135 }
136 }
137 }
138 }
139 }
140 }
141
142 completions
143}