1use std::collections::HashSet;
15
16use crate::ast::{hoisted_names, Params, Stmt};
17use crate::builtins::BUILTINS;
18use crate::keywords::KEYWORDS;
19use crate::stdlib::{self, MODULES};
20use crate::token::{Span, TokenKind};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Completion {
26 pub label: String,
27 pub kind: CompletionKind,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CompletionKind {
34 Keyword,
35 Builtin,
36 Variable,
37 Function,
38 Module,
39 Member,
40}
41
42pub fn complete(path: &str, source: &str, line: u32, col: u32) -> Vec<Completion> {
45 match cursor_context(source, line, col) {
46 Context::Member(base) => member_completions(path, source, &base),
47 Context::Import => module_name_completions(),
48 Context::General => general_completions(path, source, line, col),
49 }
50}
51
52enum Context {
54 Member(String),
56 Import,
58 General,
60}
61
62fn is_ident_char(c: char) -> bool {
63 c.is_alphanumeric() || c == '_'
64}
65
66fn cursor_context(source: &str, line: u32, col: u32) -> Context {
68 let line_text = source
69 .split('\n')
70 .nth((line as usize).saturating_sub(1))
71 .unwrap_or("");
72 let chars: Vec<char> = line_text.chars().collect();
73 let caret = (col.saturating_sub(1) as usize).min(chars.len());
74 let prefix = &chars[..caret];
75
76 let mut word = caret;
78 while word > 0 && is_ident_char(prefix[word - 1]) {
79 word -= 1;
80 }
81
82 if word > 0 && prefix[word - 1] == '.' {
85 let dot = word - 1;
86 let mut start = dot;
87 while start > 0 && is_ident_char(prefix[start - 1]) {
88 start -= 1;
89 }
90 let base: String = prefix[start..dot].iter().collect();
91 if !base.is_empty() {
92 return Context::Member(base);
93 }
94 }
95
96 let mut before = word;
98 while before > 0 && prefix[before - 1].is_whitespace() {
99 before -= 1;
100 }
101 let mut prev_start = before;
102 while prev_start > 0 && is_ident_char(prefix[prev_start - 1]) {
103 prev_start -= 1;
104 }
105 if prefix[prev_start..before].iter().collect::<String>() == "so" {
106 return Context::Import;
107 }
108
109 Context::General
110}
111
112fn member_completions(path: &str, source: &str, base: &str) -> Vec<Completion> {
116 if !imported_modules(path, source).contains(base) {
117 return Vec::new();
118 }
119 let Some(module) = stdlib::module(base) else {
120 return Vec::new();
121 };
122 let mut out = Vec::new();
123 for func in module.funcs {
124 out.push(Completion {
125 label: func.name.to_string(),
126 kind: CompletionKind::Member,
127 });
128 }
129 for (name, _) in module.consts {
130 out.push(Completion {
131 label: name.to_string(),
132 kind: CompletionKind::Member,
133 });
134 }
135 out
136}
137
138fn module_name_completions() -> Vec<Completion> {
140 MODULES
141 .iter()
142 .map(|m| Completion {
143 label: m.name.to_string(),
144 kind: CompletionKind::Module,
145 })
146 .collect()
147}
148
149fn general_completions(path: &str, source: &str, line: u32, col: u32) -> Vec<Completion> {
151 let mut out = Vec::new();
152 let mut seen = HashSet::new();
153 let mut push = |out: &mut Vec<Completion>, label: String, kind: CompletionKind| {
154 if seen.insert(label.clone()) {
155 out.push(Completion { label, kind });
156 }
157 };
158
159 for (spelling, kind) in KEYWORDS {
160 if matches!(kind, TokenKind::Def | TokenKind::Class) {
163 continue;
164 }
165 push(&mut out, spelling.to_string(), CompletionKind::Keyword);
166 }
167 for builtin in BUILTINS {
168 push(&mut out, builtin.name.to_string(), CompletionKind::Builtin);
169 }
170 for module in imported_modules(path, source) {
171 push(&mut out, module, CompletionKind::Module);
172 }
173 for (name, kind) in in_scope_names(path, source, line, col) {
174 push(&mut out, name, kind);
175 }
176 out
177}
178
179fn in_scope_names(path: &str, source: &str, line: u32, col: u32) -> Vec<(String, CompletionKind)> {
182 match crate::parser::parse(path, source) {
183 Ok(script) => {
184 let mut callables = HashSet::new();
185 collect_callables(&script.stmts, &mut callables);
186 let mut names = Vec::new();
187 scope_names_at(&script.stmts, line, col, &mut names);
188 names
189 .into_iter()
190 .map(|name| {
191 let kind = if callables.contains(&name) {
192 CompletionKind::Function
193 } else {
194 CompletionKind::Variable
195 };
196 (name, kind)
197 })
198 .collect()
199 }
200 Err(_) => lexical_names(path, source)
201 .into_iter()
202 .map(|name| (name, CompletionKind::Variable))
203 .collect(),
204 }
205}
206
207fn at_or_after(start: Span, line: u32, col: u32) -> bool {
209 line > start.line || (line == start.line && col >= start.col)
210}
211
212fn within(start: Span, end: Option<Span>, line: u32, col: u32) -> bool {
215 if !at_or_after(start, line, col) {
216 return false;
217 }
218 match end {
219 None => true,
220 Some(end) => line < end.line || (line == end.line && col < end.col),
221 }
222}
223
224fn scope_names_at(stmts: &[Stmt], line: u32, col: u32, out: &mut Vec<String>) {
230 for name in hoisted_names(stmts) {
231 push_unique(out, name);
232 }
233 for (i, stmt) in stmts.iter().enumerate() {
234 let end = stmts.get(i + 1).map(|next| next.span());
235 if !within(stmt.span(), end, line, col) {
236 continue;
237 }
238 match stmt {
239 Stmt::FuncDef { params, body, .. } => {
240 push_params(params, out);
241 scope_names_at(body, line, col, out);
242 }
243 Stmt::ObjDef { methods, .. } => scope_names_at(methods, line, col, out),
244 Stmt::For {
245 vars, rest, body, ..
246 } => {
247 for var in vars {
248 push_unique(out, var.clone());
249 }
250 if let Some(rest) = rest {
251 push_unique(out, rest.clone());
252 }
253 scope_names_at(body, line, col, out);
254 }
255 Stmt::While { body, .. } => scope_names_at(body, line, col, out),
256 Stmt::If {
257 branches,
258 else_body,
259 ..
260 } => {
261 for (_, body) in branches {
262 scope_names_at(body, line, col, out);
263 }
264 if let Some(body) = else_body {
265 scope_names_at(body, line, col, out);
266 }
267 }
268 Stmt::Try {
269 body,
270 err_name,
271 handler,
272 ..
273 } => {
274 scope_names_at(body, line, col, out);
275 push_unique(out, err_name.clone());
276 scope_names_at(handler, line, col, out);
277 }
278 _ => {}
279 }
280 break;
281 }
282}
283
284fn push_params(params: &Params, out: &mut Vec<String>) {
285 for name in params.binding_names() {
286 push_unique(out, name);
287 }
288}
289
290fn push_unique(out: &mut Vec<String>, name: String) {
291 if !out.contains(&name) {
292 out.push(name);
293 }
294}
295
296fn collect_callables(stmts: &[Stmt], out: &mut HashSet<String>) {
300 for stmt in stmts {
301 match stmt {
302 Stmt::FuncDef { name, body, .. } => {
303 out.insert(name.clone());
304 collect_callables(body, out);
305 }
306 Stmt::ObjDef { name, methods, .. } => {
307 out.insert(name.clone());
308 collect_callables(methods, out);
309 }
310 other => {
311 crate::ast::for_each_child_block(other, &mut |block| collect_callables(block, out))
312 }
313 }
314 }
315}
316
317fn imported_modules(path: &str, source: &str) -> HashSet<String> {
320 let mut out = HashSet::new();
321 match crate::parser::parse(path, source) {
322 Ok(script) => collect_imports(&script.stmts, &mut out),
323 Err(_) => {
324 for name in lexical_imports(path, source) {
325 out.insert(name);
326 }
327 }
328 }
329 out
330}
331
332fn collect_imports(stmts: &[Stmt], out: &mut HashSet<String>) {
333 for stmt in stmts {
334 if let Stmt::Import { module, .. } = stmt {
335 out.insert(module.clone());
336 }
337 crate::ast::for_each_child_block(stmt, &mut |block| collect_imports(block, out));
338 }
339}
340
341fn lexical_names(path: &str, source: &str) -> Vec<String> {
346 let tokens = crate::lexer::lex(path, source).unwrap_or_default();
347 let mut out = Vec::new();
348 for (i, token) in tokens.iter().enumerate() {
349 match &token.kind {
350 TokenKind::Such | TokenKind::So | TokenKind::Many => {
351 if let Some(TokenKind::Ident(name)) = tokens.get(i + 1).map(|t| &t.kind) {
352 push_unique(&mut out, name.clone());
353 }
354 }
355 TokenKind::Much | TokenKind::For => {
356 for next in &tokens[i + 1..] {
357 match &next.kind {
358 TokenKind::Ident(name) => push_unique(&mut out, name.clone()),
359 TokenKind::Colon | TokenKind::Newline | TokenKind::In => break,
360 _ => {}
361 }
362 }
363 }
364 TokenKind::OhNo => {
365 if let Some(TokenKind::Ident(name)) = tokens.get(i + 1).map(|t| &t.kind) {
366 push_unique(&mut out, name.clone());
367 }
368 }
369 _ => {}
370 }
371 }
372 out
373}
374
375fn lexical_imports(path: &str, source: &str) -> Vec<String> {
376 let tokens = crate::lexer::lex(path, source).unwrap_or_default();
377 let mut out = Vec::new();
378 for (i, token) in tokens.iter().enumerate() {
379 if matches!(token.kind, TokenKind::So) {
380 if let Some(TokenKind::Ident(name)) = tokens.get(i + 1).map(|t| &t.kind) {
381 push_unique(&mut out, name.clone());
382 }
383 }
384 }
385 out
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 fn labels(source: &str, line: u32, col: u32) -> Vec<String> {
393 complete("buf.doge", source, line, col)
394 .into_iter()
395 .map(|c| c.label)
396 .collect()
397 }
398
399 fn completion(source: &str, line: u32, col: u32, label: &str) -> Option<Completion> {
400 complete("buf.doge", source, line, col)
401 .into_iter()
402 .find(|c| c.label == label)
403 }
404
405 #[test]
406 fn general_offers_keywords_and_builtins() {
407 let got = labels("bark \n", 1, 6);
408 assert!(got.contains(&"such".to_string()));
409 assert!(got.contains(&"if".to_string()));
410 assert!(got.contains(&"len".to_string()));
411 assert!(got.contains(&"range".to_string()));
412 }
413
414 #[test]
415 fn reserved_words_are_never_offered() {
416 let got = labels("\n", 1, 1);
417 assert!(!got.contains(&"def".to_string()));
418 assert!(!got.contains(&"class".to_string()));
419 }
420
421 #[test]
422 fn top_level_names_are_in_scope() {
423 let src = "such greeting = \"hi\"\nsuch greet much name:\n bark name\nwow\n\nwow\n";
426 let got = labels(src, 5, 1);
427 assert!(got.contains(&"greeting".to_string()));
428 assert!(got.contains(&"greet".to_string()));
429 assert_eq!(
431 completion(src, 5, 1, "greet").map(|c| c.kind),
432 Some(CompletionKind::Function)
433 );
434 assert_eq!(
435 completion(src, 5, 1, "greeting").map(|c| c.kind),
436 Some(CompletionKind::Variable)
437 );
438 }
439
440 #[test]
441 fn a_param_is_visible_only_inside_its_function() {
442 let src = "such greet much name:\n bark name\nwow\nwow\n";
444 assert!(labels(src, 2, 3).contains(&"name".to_string()));
445 let src_after = "such greet much name:\n bark name\nwow\nsuch other = 1\nwow\n";
447 let after = labels(src_after, 4, 1);
448 assert!(!after.contains(&"name".to_string()));
449 assert!(after.contains(&"other".to_string()));
450 }
451
452 #[test]
453 fn member_access_offers_module_members() {
454 let src = "so nerd\nbark nerd.\n";
455 let got = labels(src, 2, 11);
456 assert!(got.contains(&"sqrt".to_string()));
457 assert!(got.contains(&"floor".to_string()));
458 assert!(!got.contains(&"such".to_string()));
460 }
461
462 #[test]
463 fn member_access_on_unimported_module_is_empty() {
464 assert!(labels("bark nerd.\n", 1, 11).is_empty());
466 }
467
468 #[test]
469 fn import_position_offers_module_names() {
470 let got = labels("so \n", 1, 4);
471 assert!(got.contains(&"nerd".to_string()));
472 assert!(got.contains(&"strings".to_string()));
473 assert!(!got.contains(&"len".to_string()));
474 }
475
476 #[test]
477 fn unparsable_buffer_falls_back_to_declared_names() {
478 let src = "such total = 0\nif total >\n";
480 let got = labels(src, 2, 11);
481 assert!(got.contains(&"total".to_string()));
482 }
483}