1use reedline::{Completer, Span, Suggestion};
10
11use crate::interpreter::builtins::{find_builtin, BUILTIN_REGISTRY};
12
13pub struct RCompleter {
14 names: Vec<String>,
15}
16
17impl Default for RCompleter {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23impl RCompleter {
24 pub fn new() -> Self {
25 let mut names: Vec<String> = Vec::new();
26
27 for kw in &[
29 "if",
30 "else",
31 "for",
32 "while",
33 "repeat",
34 "function",
35 "return",
36 "next",
37 "break",
38 "in",
39 "TRUE",
40 "FALSE",
41 "NULL",
42 "NA",
43 "NA_integer_",
44 "NA_real_",
45 "NA_complex_",
46 "NA_character_",
47 "Inf",
48 "NaN",
49 ] {
50 names.push((*kw).to_string());
51 }
52
53 for descriptor in BUILTIN_REGISTRY {
55 for name in std::iter::once(descriptor.name).chain(descriptor.aliases.iter().copied()) {
56 if name
58 .chars()
59 .next()
60 .is_some_and(|c| c.is_alphabetic() || c == '.')
61 {
62 names.push(name.to_string());
63 }
64 }
65 }
66
67 names.sort();
68 names.dedup();
69
70 Self { names }
71 }
72}
73
74fn find_enclosing_function(line: &str) -> Option<&str> {
80 let bytes = line.as_bytes();
81 let mut depth: i32 = 0;
82
83 let mut i = bytes.len();
84 while i > 0 {
85 i -= 1;
86 match bytes[i] {
87 b')' => depth += 1,
88 b'(' => {
89 if depth == 0 {
90 let before = &line[..i];
92 let name_start = before
93 .rfind(|c: char| !c.is_alphanumeric() && c != '.' && c != '_')
94 .map(|j| j + 1)
95 .unwrap_or(0);
96 let name = &before[name_start..i];
97 if !name.is_empty() {
98 return Some(name);
99 }
100 return None;
101 }
102 depth -= 1;
103 }
104 _ => {}
105 }
106 }
107 None
108}
109
110impl Completer for RCompleter {
111 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
112 let line_to_pos = &line[..pos];
113 let word_start = line_to_pos
114 .rfind(|c: char| !c.is_alphanumeric() && c != '.' && c != '_')
115 .map(|i| i + 1)
116 .unwrap_or(0);
117
118 let prefix = &line[word_start..pos];
119
120 if let Some(func_name) = find_enclosing_function(line_to_pos) {
122 if let Some(descriptor) = find_builtin(func_name) {
123 if !descriptor.formals.is_empty() {
124 let param_suggestions = complete_params(
127 descriptor.formals,
128 descriptor.doc,
129 prefix,
130 word_start,
131 pos,
132 );
133 if !param_suggestions.is_empty() {
134 return param_suggestions;
135 }
136 } else {
137 let doc_params = extract_doc_params(descriptor.doc);
139 if !doc_params.is_empty() {
140 let param_suggestions =
141 complete_params_from_strings(&doc_params, prefix, word_start, pos);
142 if !param_suggestions.is_empty() {
143 return param_suggestions;
144 }
145 }
146 }
147 }
148 }
149
150 if prefix.is_empty() {
151 return vec![];
152 }
153
154 self.names
155 .iter()
156 .filter(|name| name.starts_with(prefix) && name.as_str() != prefix)
157 .map(|name| Suggestion {
158 value: name.clone(),
159 description: None,
160 style: None,
161 extra: None,
162 span: Span::new(word_start, pos),
163 append_whitespace: false,
164 display_override: None,
165 match_indices: None,
166 })
167 .collect()
168 }
169}
170
171fn complete_params(
172 formals: &[&str],
173 doc: &str,
174 prefix: &str,
175 word_start: usize,
176 pos: usize,
177) -> Vec<Suggestion> {
178 let mut param_names: Vec<&str> = formals.to_vec();
180
181 for line in doc.lines() {
183 let trimmed = line.trim();
184 if let Some(rest) = trimmed.strip_prefix("@param ") {
185 if let Some(name) = rest.split_whitespace().next() {
186 if name != "..." && !param_names.contains(&name) {
187 param_names.push(name);
188 }
189 }
190 }
191 }
192
193 param_names
194 .iter()
195 .filter(|name| {
196 if prefix.is_empty() {
197 true
198 } else {
199 name.starts_with(prefix) && **name != prefix
200 }
201 })
202 .map(|name| Suggestion {
203 value: format!("{name} = "),
204 description: None,
205 style: None,
206 extra: None,
207 span: Span::new(word_start, pos),
208 append_whitespace: false,
209 display_override: None,
210 match_indices: None,
211 })
212 .collect()
213}
214
215fn complete_params_from_strings(
216 params: &[String],
217 prefix: &str,
218 word_start: usize,
219 pos: usize,
220) -> Vec<Suggestion> {
221 params
222 .iter()
223 .filter(|name| {
224 if prefix.is_empty() {
225 true
226 } else {
227 name.starts_with(prefix) && name.as_str() != prefix
228 }
229 })
230 .map(|name| Suggestion {
231 value: format!("{name} = "),
232 description: None,
233 style: None,
234 extra: None,
235 span: Span::new(word_start, pos),
236 append_whitespace: false,
237 display_override: None,
238 match_indices: None,
239 })
240 .collect()
241}
242
243fn extract_doc_params(doc: &str) -> Vec<String> {
245 doc.lines()
246 .filter_map(|line| {
247 let trimmed = line.trim();
248 let rest = trimmed.strip_prefix("@param ")?;
249 let name = rest.split_whitespace().next()?;
250 if name == "..." {
251 None
252 } else {
253 Some(name.to_string())
254 }
255 })
256 .collect()
257}