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