1use std::collections::HashMap;
5
6use reedline::{Hinter, History};
7
8use super::CommandInfo;
9use super::command_tree::{build_command_map, build_command_tree};
10
11pub struct RapsHinter {
13 commands: Vec<CommandInfo>,
14 command_map: HashMap<String, CommandInfo>,
15 current_completion: String,
17}
18
19impl RapsHinter {
20 pub fn new() -> Self {
21 let commands = build_command_tree();
22 let command_map = build_command_map(&commands);
23 Self {
24 commands,
25 command_map,
26 current_completion: String::new(),
27 }
28 }
29}
30
31impl Default for RapsHinter {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl Hinter for RapsHinter {
38 fn handle(
39 &mut self,
40 line: &str,
41 pos: usize,
42 _history: &dyn History,
43 use_ansi_coloring: bool,
44 _cwd: &str,
45 ) -> String {
46 if pos < line.len() {
48 self.current_completion.clear();
49 return String::new();
50 }
51
52 match get_hint_raw(&self.commands, &self.command_map, line) {
53 Some((display, complete_up_to)) => {
54 self.current_completion = if complete_up_to > 0 {
55 display[..complete_up_to].to_string()
56 } else {
57 String::new()
58 };
59
60 if use_ansi_coloring {
61 format!("\x1b[2;36m{display}\x1b[0m")
63 } else {
64 display
65 }
66 }
67 None => {
68 self.current_completion.clear();
69 String::new()
70 }
71 }
72 }
73
74 fn complete_hint(&self) -> String {
75 self.current_completion.clone()
76 }
77
78 fn next_hint_token(&self) -> String {
79 self.current_completion
80 .split_once(' ')
81 .map(|(first, _)| first.to_string())
82 .unwrap_or_else(|| self.current_completion.clone())
83 }
84}
85
86pub(super) fn get_hint_raw(
88 commands: &[CommandInfo],
89 command_map: &HashMap<String, CommandInfo>,
90 line: &str,
91) -> Option<(String, usize)> {
92 if line.is_empty() {
93 return None;
94 }
95
96 let parts: Vec<&str> = line.split_whitespace().collect();
97 let trailing_space = line.ends_with(' ');
98
99 match parts.len() {
100 1 if !trailing_space => {
101 let partial = parts[0].to_lowercase();
103 for cmd in commands {
104 if cmd.name.starts_with(&partial) && cmd.name != partial {
105 let suffix = &cmd.name[partial.len()..];
106 let mut hint = suffix.to_string();
107
108 if !cmd.subcommands.is_empty() {
110 hint.push_str(" <subcommand>");
111 } else if !cmd.params.is_empty() {
112 hint.push(' ');
113 hint.push_str(&cmd.params.join(" "));
114 }
115
116 return Some((hint, suffix.len()));
117 }
118 }
119 }
120 1 if trailing_space => {
121 let cmd_name = parts[0].to_lowercase();
123 if let Some(cmd) = commands.iter().find(|c| c.name == cmd_name) {
124 if !cmd.subcommands.is_empty() {
125 let subcmd_names: Vec<&str> =
126 cmd.subcommands.iter().take(3).map(|s| s.name).collect();
127 let hint = format!("<{}...>", subcmd_names.join("|"));
128 return Some((hint, 0));
129 } else if !cmd.params.is_empty() {
130 let hint = cmd.params.join(" ");
131 return Some((hint, 0));
132 }
133 }
134 }
135 2 if !trailing_space => {
136 let cmd_name = parts[0].to_lowercase();
138 let partial = parts[1].to_lowercase();
139
140 if let Some(cmd) = commands.iter().find(|c| c.name == cmd_name) {
141 for subcmd in cmd.subcommands {
142 if subcmd.name.starts_with(&partial) && subcmd.name != partial {
143 let suffix = &subcmd.name[partial.len()..];
144 let mut hint = suffix.to_string();
145
146 if !subcmd.params.is_empty() {
147 hint.push(' ');
148 hint.push_str(&subcmd.params.join(" "));
149 }
150
151 return Some((hint, suffix.len()));
152 }
153 }
154 }
155 }
156 2 if trailing_space => {
157 let cmd_name = parts[0].to_lowercase();
159 let sub_name = parts[1].to_lowercase();
160 let key = format!("{} {}", cmd_name, sub_name);
161
162 if let Some(cmd) = command_map.get(&key) {
163 if !cmd.params.is_empty() {
164 let hint = cmd.params.join(" ");
165 return Some((hint, 0));
166 } else if !cmd.flags.is_empty() {
167 let hint = format!("[{}]", cmd.flags.first().unwrap_or(&""));
168 return Some((hint, 0));
169 }
170 }
171 }
172 n if n >= 3 => {
173 let cmd_name = parts[0].to_lowercase();
175 let sub_name = parts[1].to_lowercase();
176 let key = format!("{} {}", cmd_name, sub_name);
177
178 if let Some(cmd) = command_map.get(&key) {
179 let positional_count = parts[2..].iter().filter(|p| !p.starts_with('-')).count();
181
182 if positional_count < cmd.params.len() {
183 let remaining: Vec<&str> =
184 cmd.params.iter().skip(positional_count).copied().collect();
185 if !remaining.is_empty() && trailing_space {
186 let hint = remaining.join(" ");
187 return Some((hint, 0));
188 }
189 }
190 }
191 }
192 _ => {}
193 }
194
195 None
196}