1use std::borrow::Cow;
2use std::collections::{HashMap, HashSet};
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result, bail};
6use rustyline::completion::{Completer, Pair};
7use rustyline::error::ReadlineError;
8use rustyline::highlight::Highlighter;
9use rustyline::hint::Hinter;
10use rustyline::history::DefaultHistory;
11use rustyline::validate::Validator;
12use rustyline::{Editor, Helper};
13
14use crate::engine::{
15 ExecutionOutcome, ExecutionPayload, LanguageRegistry, LanguageSession, build_install_command,
16};
17use crate::highlight;
18use crate::language::LanguageSpec;
19use crate::output;
20
21const HISTORY_FILE: &str = ".run_history";
22
23struct ReplHelper {
24 language_id: String,
25 session_vars: Vec<String>,
26}
27
28impl ReplHelper {
29 fn new(language_id: String) -> Self {
30 Self {
31 language_id,
32 session_vars: Vec::new(),
33 }
34 }
35
36 fn update_language(&mut self, language_id: String) {
37 self.language_id = language_id;
38 }
39
40 fn update_session_vars(&mut self, vars: Vec<String>) {
41 self.session_vars = vars;
42 }
43}
44
45const META_COMMANDS: &[&str] = &[
46 ":help",
47 ":exit",
48 ":quit",
49 ":languages",
50 ":lang ",
51 ":detect ",
52 ":reset",
53 ":load ",
54 ":run ",
55 ":save ",
56 ":history",
57 ":install ",
58 ":bench ",
59 ":type",
60];
61
62fn language_keywords(lang: &str) -> &'static [&'static str] {
63 match lang {
64 "python" | "py" | "python3" | "py3" => &[
65 "False",
66 "None",
67 "True",
68 "and",
69 "as",
70 "assert",
71 "async",
72 "await",
73 "break",
74 "class",
75 "continue",
76 "def",
77 "del",
78 "elif",
79 "else",
80 "except",
81 "finally",
82 "for",
83 "from",
84 "global",
85 "if",
86 "import",
87 "in",
88 "is",
89 "lambda",
90 "nonlocal",
91 "not",
92 "or",
93 "pass",
94 "raise",
95 "return",
96 "try",
97 "while",
98 "with",
99 "yield",
100 "print",
101 "len",
102 "range",
103 "enumerate",
104 "zip",
105 "map",
106 "filter",
107 "sorted",
108 "list",
109 "dict",
110 "set",
111 "tuple",
112 "str",
113 "int",
114 "float",
115 "bool",
116 "type",
117 "isinstance",
118 "hasattr",
119 "getattr",
120 "setattr",
121 "open",
122 "input",
123 ],
124 "javascript" | "js" | "node" => &[
125 "async",
126 "await",
127 "break",
128 "case",
129 "catch",
130 "class",
131 "const",
132 "continue",
133 "debugger",
134 "default",
135 "delete",
136 "do",
137 "else",
138 "export",
139 "extends",
140 "false",
141 "finally",
142 "for",
143 "function",
144 "if",
145 "import",
146 "in",
147 "instanceof",
148 "let",
149 "new",
150 "null",
151 "of",
152 "return",
153 "static",
154 "super",
155 "switch",
156 "this",
157 "throw",
158 "true",
159 "try",
160 "typeof",
161 "undefined",
162 "var",
163 "void",
164 "while",
165 "with",
166 "yield",
167 "console",
168 "require",
169 "module",
170 "process",
171 "Promise",
172 "Array",
173 "Object",
174 "String",
175 "Number",
176 "Boolean",
177 "Math",
178 "JSON",
179 "Date",
180 "RegExp",
181 "Map",
182 "Set",
183 ],
184 "typescript" | "ts" => &[
185 "abstract",
186 "any",
187 "as",
188 "async",
189 "await",
190 "boolean",
191 "break",
192 "case",
193 "catch",
194 "class",
195 "const",
196 "continue",
197 "debugger",
198 "declare",
199 "default",
200 "delete",
201 "do",
202 "else",
203 "enum",
204 "export",
205 "extends",
206 "false",
207 "finally",
208 "for",
209 "from",
210 "function",
211 "get",
212 "if",
213 "implements",
214 "import",
215 "in",
216 "infer",
217 "instanceof",
218 "interface",
219 "is",
220 "keyof",
221 "let",
222 "module",
223 "namespace",
224 "never",
225 "new",
226 "null",
227 "number",
228 "object",
229 "of",
230 "private",
231 "protected",
232 "public",
233 "readonly",
234 "return",
235 "set",
236 "static",
237 "string",
238 "super",
239 "switch",
240 "symbol",
241 "this",
242 "throw",
243 "true",
244 "try",
245 "type",
246 "typeof",
247 "undefined",
248 "unique",
249 "unknown",
250 "var",
251 "void",
252 "while",
253 "with",
254 "yield",
255 ],
256 "rust" | "rs" => &[
257 "as",
258 "async",
259 "await",
260 "break",
261 "const",
262 "continue",
263 "crate",
264 "dyn",
265 "else",
266 "enum",
267 "extern",
268 "false",
269 "fn",
270 "for",
271 "if",
272 "impl",
273 "in",
274 "let",
275 "loop",
276 "match",
277 "mod",
278 "move",
279 "mut",
280 "pub",
281 "ref",
282 "return",
283 "self",
284 "Self",
285 "static",
286 "struct",
287 "super",
288 "trait",
289 "true",
290 "type",
291 "unsafe",
292 "use",
293 "where",
294 "while",
295 "println!",
296 "eprintln!",
297 "format!",
298 "vec!",
299 "String",
300 "Vec",
301 "Option",
302 "Result",
303 "Some",
304 "None",
305 "Ok",
306 "Err",
307 ],
308 "go" | "golang" => &[
309 "break",
310 "case",
311 "chan",
312 "const",
313 "continue",
314 "default",
315 "defer",
316 "else",
317 "fallthrough",
318 "for",
319 "func",
320 "go",
321 "goto",
322 "if",
323 "import",
324 "interface",
325 "map",
326 "package",
327 "range",
328 "return",
329 "select",
330 "struct",
331 "switch",
332 "type",
333 "var",
334 "fmt",
335 "Println",
336 "Printf",
337 "Sprintf",
338 "errors",
339 "strings",
340 "strconv",
341 ],
342 "ruby" | "rb" => &[
343 "alias",
344 "and",
345 "begin",
346 "break",
347 "case",
348 "class",
349 "def",
350 "defined?",
351 "do",
352 "else",
353 "elsif",
354 "end",
355 "ensure",
356 "false",
357 "for",
358 "if",
359 "in",
360 "module",
361 "next",
362 "nil",
363 "not",
364 "or",
365 "redo",
366 "rescue",
367 "retry",
368 "return",
369 "self",
370 "super",
371 "then",
372 "true",
373 "undef",
374 "unless",
375 "until",
376 "when",
377 "while",
378 "yield",
379 "puts",
380 "print",
381 "require",
382 "require_relative",
383 ],
384 "java" => &[
385 "abstract",
386 "assert",
387 "boolean",
388 "break",
389 "byte",
390 "case",
391 "catch",
392 "char",
393 "class",
394 "const",
395 "continue",
396 "default",
397 "do",
398 "double",
399 "else",
400 "enum",
401 "extends",
402 "final",
403 "finally",
404 "float",
405 "for",
406 "goto",
407 "if",
408 "implements",
409 "import",
410 "instanceof",
411 "int",
412 "interface",
413 "long",
414 "native",
415 "new",
416 "package",
417 "private",
418 "protected",
419 "public",
420 "return",
421 "short",
422 "static",
423 "strictfp",
424 "super",
425 "switch",
426 "synchronized",
427 "this",
428 "throw",
429 "throws",
430 "transient",
431 "try",
432 "void",
433 "volatile",
434 "while",
435 "System",
436 "String",
437 ],
438 _ => &[],
439 }
440}
441
442fn complete_file_path(partial: &str) -> Vec<Pair> {
443 let (dir_part, file_prefix) = if let Some(sep_pos) = partial.rfind('/') {
444 (&partial[..=sep_pos], &partial[sep_pos + 1..])
445 } else {
446 ("", partial)
447 };
448
449 let search_dir = if dir_part.is_empty() { "." } else { dir_part };
450
451 let mut results = Vec::new();
452 if let Ok(entries) = std::fs::read_dir(search_dir) {
453 for entry in entries.flatten() {
454 let name = entry.file_name().to_string_lossy().to_string();
455 if name.starts_with('.') {
456 continue; }
458 if name.starts_with(file_prefix) {
459 let full = format!("{dir_part}{name}");
460 let display = if entry.path().is_dir() {
461 format!("{name}/")
462 } else {
463 name.clone()
464 };
465 results.push(Pair {
466 display,
467 replacement: full,
468 });
469 }
470 }
471 }
472 results
473}
474
475impl Completer for ReplHelper {
476 type Candidate = Pair;
477
478 fn complete(
479 &self,
480 line: &str,
481 pos: usize,
482 _ctx: &rustyline::Context<'_>,
483 ) -> rustyline::Result<(usize, Vec<Pair>)> {
484 let line_up_to = &line[..pos];
485
486 if line_up_to.starts_with(':') {
488 if let Some(rest) = line_up_to
490 .strip_prefix(":load ")
491 .or_else(|| line_up_to.strip_prefix(":run "))
492 .or_else(|| line_up_to.strip_prefix(":save "))
493 {
494 let start = pos - rest.len();
495 return Ok((start, complete_file_path(rest)));
496 }
497
498 let candidates: Vec<Pair> = META_COMMANDS
499 .iter()
500 .filter(|cmd| cmd.starts_with(line_up_to))
501 .map(|cmd| Pair {
502 display: cmd.to_string(),
503 replacement: cmd.to_string(),
504 })
505 .collect();
506 return Ok((0, candidates));
507 }
508
509 let word_start = line_up_to
511 .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '!')
512 .map(|i| i + 1)
513 .unwrap_or(0);
514 let prefix = &line_up_to[word_start..];
515
516 if prefix.is_empty() {
517 return Ok((pos, Vec::new()));
518 }
519
520 let mut candidates: Vec<Pair> = Vec::new();
521
522 for kw in language_keywords(&self.language_id) {
524 if kw.starts_with(prefix) {
525 candidates.push(Pair {
526 display: kw.to_string(),
527 replacement: kw.to_string(),
528 });
529 }
530 }
531
532 for var in &self.session_vars {
534 if var.starts_with(prefix) && !candidates.iter().any(|c| c.replacement == *var) {
535 candidates.push(Pair {
536 display: var.clone(),
537 replacement: var.clone(),
538 });
539 }
540 }
541
542 Ok((word_start, candidates))
543 }
544}
545
546impl Hinter for ReplHelper {
547 type Hint = String;
548}
549
550impl Validator for ReplHelper {}
551
552impl Highlighter for ReplHelper {
553 fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
554 if line.trim_start().starts_with(':') {
555 return Cow::Borrowed(line);
556 }
557
558 let highlighted = highlight::highlight_repl_input(line, &self.language_id);
559 Cow::Owned(highlighted)
560 }
561
562 fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
563 true
564 }
565}
566
567impl Helper for ReplHelper {}
568
569pub fn run_repl(
570 initial_language: LanguageSpec,
571 registry: LanguageRegistry,
572 detect_enabled: bool,
573) -> Result<i32> {
574 let helper = ReplHelper::new(initial_language.canonical_id().to_string());
575 let mut editor = Editor::<ReplHelper, DefaultHistory>::new()?;
576 editor.set_helper(Some(helper));
577
578 if let Some(path) = history_path() {
579 let _ = editor.load_history(&path);
580 }
581
582 let lang_count = registry.known_languages().len();
583 let mut state = ReplState::new(initial_language, registry, detect_enabled)?;
584
585 println!(
586 "\x1b[1mrun\x1b[0m \x1b[2mv{} — {}+ languages. Type :help for commands.\x1b[0m",
587 env!("CARGO_PKG_VERSION"),
588 lang_count
589 );
590 let mut pending: Option<PendingInput> = None;
591
592 loop {
593 let prompt = match &pending {
594 Some(p) => p.prompt(),
595 None => state.prompt(),
596 };
597 let mut pending_indent: Option<String> = None;
598 if let Some(p) = pending.as_ref()
599 && state.current_language().canonical_id() == "python"
600 {
601 let indent = python_prompt_indent(p.buffer());
602 if !indent.is_empty() {
603 pending_indent = Some(indent);
604 }
605 }
606
607 if let Some(helper) = editor.helper_mut() {
608 helper.update_language(state.current_language().canonical_id().to_string());
609 }
610
611 let line_result = match pending_indent.as_deref() {
612 Some(indent) => editor.readline_with_initial(&prompt, (indent, "")),
613 None => editor.readline(&prompt),
614 };
615
616 match line_result {
617 Ok(line) => {
618 let raw = line.trim_end_matches(['\r', '\n']);
619
620 if let Some(p) = pending.as_mut() {
621 if raw.trim() == ":cancel" {
622 pending = None;
623 continue;
624 }
625
626 p.push_line_auto_with_indent(
627 state.current_language().canonical_id(),
628 raw,
629 pending_indent.as_deref(),
630 );
631 if p.needs_more_input(state.current_language().canonical_id()) {
632 continue;
633 }
634
635 let code = p.take();
636 pending = None;
637 let trimmed = code.trim_end();
638 if !trimmed.is_empty() {
639 let _ = editor.add_history_entry(trimmed);
640 state.history_entries.push(trimmed.to_string());
641 state.execute_snippet(trimmed)?;
642 if let Some(helper) = editor.helper_mut() {
643 helper.update_session_vars(state.session_var_names());
644 }
645 }
646 continue;
647 }
648
649 if raw.trim().is_empty() {
650 continue;
651 }
652
653 if raw.trim_start().starts_with(':') {
654 let trimmed = raw.trim();
655 let _ = editor.add_history_entry(trimmed);
656 if state.handle_meta(trimmed)? {
657 break;
658 }
659 continue;
660 }
661
662 let mut p = PendingInput::new();
663 p.push_line(raw);
664 if p.needs_more_input(state.current_language().canonical_id()) {
665 pending = Some(p);
666 continue;
667 }
668
669 let trimmed = raw.trim_end();
670 let _ = editor.add_history_entry(trimmed);
671 state.history_entries.push(trimmed.to_string());
672 state.execute_snippet(trimmed)?;
673 if let Some(helper) = editor.helper_mut() {
674 helper.update_session_vars(state.session_var_names());
675 }
676 }
677 Err(ReadlineError::Interrupted) => {
678 println!("^C");
679 pending = None;
680 continue;
681 }
682 Err(ReadlineError::Eof) => {
683 println!("bye");
684 break;
685 }
686 Err(err) => {
687 bail!("readline error: {err}");
688 }
689 }
690 }
691
692 if let Some(path) = history_path() {
693 let _ = editor.save_history(&path);
694 }
695
696 state.shutdown();
697 Ok(0)
698}
699
700struct ReplState {
701 registry: LanguageRegistry,
702 sessions: HashMap<String, Box<dyn LanguageSession>>, current_language: LanguageSpec,
704 detect_enabled: bool,
705 defined_names: HashSet<String>,
706 history_entries: Vec<String>,
707}
708
709struct PendingInput {
710 buf: String,
711}
712
713impl PendingInput {
714 fn new() -> Self {
715 Self { buf: String::new() }
716 }
717
718 fn prompt(&self) -> String {
719 "... ".to_string()
720 }
721
722 fn buffer(&self) -> &str {
723 &self.buf
724 }
725
726 fn push_line(&mut self, line: &str) {
727 self.buf.push_str(line);
728 self.buf.push('\n');
729 }
730
731 #[cfg(test)]
732 fn push_line_auto(&mut self, language_id: &str, line: &str) {
733 self.push_line_auto_with_indent(language_id, line, None);
734 }
735
736 fn push_line_auto_with_indent(
737 &mut self,
738 language_id: &str,
739 line: &str,
740 expected_indent: Option<&str>,
741 ) {
742 match language_id {
743 "python" | "py" | "python3" | "py3" => {
744 let adjusted = python_auto_indent_with_expected(line, &self.buf, expected_indent);
745 self.push_line(&adjusted);
746 }
747 _ => self.push_line(line),
748 }
749 }
750
751 fn take(&mut self) -> String {
752 std::mem::take(&mut self.buf)
753 }
754
755 fn needs_more_input(&self, language_id: &str) -> bool {
756 needs_more_input(language_id, &self.buf)
757 }
758}
759
760fn needs_more_input(language_id: &str, code: &str) -> bool {
761 match language_id {
762 "python" | "py" | "python3" | "py3" => needs_more_input_python(code),
763
764 _ => has_unclosed_delimiters(code) || generic_line_looks_incomplete(code),
765 }
766}
767
768fn generic_line_looks_incomplete(code: &str) -> bool {
769 let mut last: Option<&str> = None;
770 for line in code.lines().rev() {
771 let trimmed = line.trim_end();
772 if trimmed.trim().is_empty() {
773 continue;
774 }
775 last = Some(trimmed);
776 break;
777 }
778 let Some(line) = last else { return false };
779 let line = line.trim();
780 if line.is_empty() {
781 return false;
782 }
783 if line.starts_with('#') {
784 return false;
785 }
786
787 if line.ends_with('\\') {
788 return true;
789 }
790
791 const TAILS: [&str; 24] = [
792 "=", "+", "-", "*", "/", "%", "&", "|", "^", "!", "<", ">", "&&", "||", "??", "?:", "?",
793 ":", ".", ",", "=>", "->", "::", "..",
794 ];
795 if TAILS.iter().any(|tok| line.ends_with(tok)) {
796 return true;
797 }
798
799 const PREFIXES: [&str; 9] = [
800 "return", "throw", "yield", "await", "import", "from", "export", "case", "else",
801 ];
802 let lowered = line.to_ascii_lowercase();
803 if PREFIXES
804 .iter()
805 .any(|kw| lowered == *kw || lowered.ends_with(&format!(" {kw}")))
806 {
807 return true;
808 }
809
810 false
811}
812
813fn needs_more_input_python(code: &str) -> bool {
814 if has_unclosed_delimiters(code) {
815 return true;
816 }
817
818 let mut last_nonempty: Option<&str> = None;
819 let mut saw_block_header = false;
820 let mut has_body_after_header = false;
821
822 for line in code.lines() {
823 let trimmed = line.trim_end();
824 if trimmed.trim().is_empty() {
825 continue;
826 }
827 last_nonempty = Some(trimmed);
828 if is_python_block_header(trimmed.trim()) {
829 saw_block_header = true;
830 has_body_after_header = false;
831 } else if saw_block_header {
832 has_body_after_header = true;
833 }
834 }
835
836 if !saw_block_header {
837 return false;
838 }
839
840 if code.ends_with("\n\n") {
842 return false;
843 }
844
845 if !has_body_after_header {
847 return true;
848 }
849
850 if let Some(last) = last_nonempty
852 && (last.starts_with(' ') || last.starts_with('\t'))
853 {
854 return true;
855 }
856
857 false
858}
859
860fn is_python_block_header(line: &str) -> bool {
863 if !line.ends_with(':') {
864 return false;
865 }
866 let lowered = line.to_ascii_lowercase();
867 const BLOCK_KEYWORDS: &[&str] = &[
868 "def ",
869 "class ",
870 "if ",
871 "elif ",
872 "else:",
873 "for ",
874 "while ",
875 "try:",
876 "except",
877 "finally:",
878 "with ",
879 "async def ",
880 "async for ",
881 "async with ",
882 ];
883 BLOCK_KEYWORDS.iter().any(|kw| lowered.starts_with(kw))
884}
885
886fn python_auto_indent(line: &str, existing: &str) -> String {
887 python_auto_indent_with_expected(line, existing, None)
888}
889
890fn python_auto_indent_with_expected(
891 line: &str,
892 existing: &str,
893 expected_indent: Option<&str>,
894) -> String {
895 let trimmed = line.trim_end_matches(['\r', '\n']);
896 let raw = trimmed;
897 if raw.trim().is_empty() {
898 return raw.to_string();
899 }
900
901 let (raw_indent, raw_content) = split_indent(raw);
902
903 let mut last_nonempty: Option<&str> = None;
904 for l in existing.lines().rev() {
905 if l.trim().is_empty() {
906 continue;
907 }
908 last_nonempty = Some(l);
909 break;
910 }
911
912 let Some(prev) = last_nonempty else {
913 return raw.to_string();
914 };
915 let prev_trimmed = prev.trim_end();
916 let prev_indent = prev
917 .chars()
918 .take_while(|c| *c == ' ' || *c == '\t')
919 .collect::<String>();
920
921 let lowered = raw.trim().to_ascii_lowercase();
922 let is_dedent_keyword = lowered.starts_with("else:")
923 || lowered.starts_with("elif ")
924 || lowered.starts_with("except")
925 || lowered.starts_with("finally:")
926 || lowered.starts_with("return")
927 || lowered.starts_with("yield")
928 || lowered == "return"
929 || lowered == "yield";
930 let suggested = if lowered.starts_with("else:")
931 || lowered.starts_with("elif ")
932 || lowered.starts_with("except")
933 || lowered.starts_with("finally:")
934 {
935 if prev_indent.is_empty() {
936 None
937 } else {
938 Some(python_dedent_one_level(&prev_indent))
939 }
940 } else if lowered.starts_with("return")
941 || lowered.starts_with("yield")
942 || lowered == "return"
943 || lowered == "yield"
944 {
945 python_last_def_indent(existing).map(|indent| format!("{indent} "))
946 } else if is_python_block_header(prev_trimmed.trim()) && prev_trimmed.ends_with(':') {
947 Some(format!("{prev_indent} "))
948 } else if !prev_indent.is_empty() {
949 Some(prev_indent)
950 } else {
951 None
952 };
953
954 if let Some(indent) = suggested {
955 if let Some(expected) = expected_indent {
956 if raw_indent.len() < expected.len() {
957 return raw.to_string();
958 }
959 if is_dedent_keyword
960 && raw_indent.len() == expected.len()
961 && raw_indent.len() > indent.len()
962 {
963 return format!("{indent}{raw_content}");
964 }
965 }
966 if raw_indent.len() < indent.len() {
967 return format!("{indent}{raw_content}");
968 }
969 }
970
971 raw.to_string()
972}
973
974fn python_prompt_indent(existing: &str) -> String {
975 if existing.trim().is_empty() {
976 return String::new();
977 }
978 let adjusted = python_auto_indent("x", existing);
979 let (indent, _content) = split_indent(&adjusted);
980 indent
981}
982
983fn python_dedent_one_level(indent: &str) -> String {
984 if indent.is_empty() {
985 return String::new();
986 }
987 if let Some(stripped) = indent.strip_suffix('\t') {
988 return stripped.to_string();
989 }
990 let mut trimmed = indent.to_string();
991 let mut removed = 0usize;
992 while removed < 4 && trimmed.ends_with(' ') {
993 trimmed.pop();
994 removed += 1;
995 }
996 trimmed
997}
998
999fn python_last_def_indent(existing: &str) -> Option<String> {
1000 for line in existing.lines().rev() {
1001 let trimmed = line.trim_end();
1002 if trimmed.trim().is_empty() {
1003 continue;
1004 }
1005 let lowered = trimmed.trim_start().to_ascii_lowercase();
1006 if lowered.starts_with("def ") || lowered.starts_with("async def ") {
1007 let indent = line
1008 .chars()
1009 .take_while(|c| *c == ' ' || *c == '\t')
1010 .collect::<String>();
1011 return Some(indent);
1012 }
1013 }
1014 None
1015}
1016
1017fn split_indent(line: &str) -> (String, &str) {
1018 let mut idx = 0;
1019 for (i, ch) in line.char_indices() {
1020 if ch == ' ' || ch == '\t' {
1021 idx = i + ch.len_utf8();
1022 } else {
1023 break;
1024 }
1025 }
1026 (line[..idx].to_string(), &line[idx..])
1027}
1028
1029fn has_unclosed_delimiters(code: &str) -> bool {
1030 let mut paren = 0i32;
1031 let mut bracket = 0i32;
1032 let mut brace = 0i32;
1033
1034 let mut in_single = false;
1035 let mut in_double = false;
1036 let mut in_backtick = false;
1037 let mut in_block_comment = false;
1038 let mut escape = false;
1039
1040 let chars: Vec<char> = code.chars().collect();
1041 let len = chars.len();
1042 let mut i = 0;
1043
1044 while i < len {
1045 let ch = chars[i];
1046
1047 if escape {
1048 escape = false;
1049 i += 1;
1050 continue;
1051 }
1052
1053 if in_block_comment {
1055 if ch == '*' && i + 1 < len && chars[i + 1] == '/' {
1056 in_block_comment = false;
1057 i += 2;
1058 continue;
1059 }
1060 i += 1;
1061 continue;
1062 }
1063
1064 if in_single {
1065 if ch == '\\' {
1066 escape = true;
1067 } else if ch == '\'' {
1068 in_single = false;
1069 }
1070 i += 1;
1071 continue;
1072 }
1073 if in_double {
1074 if ch == '\\' {
1075 escape = true;
1076 } else if ch == '"' {
1077 in_double = false;
1078 }
1079 i += 1;
1080 continue;
1081 }
1082 if in_backtick {
1083 if ch == '\\' {
1084 escape = true;
1085 } else if ch == '`' {
1086 in_backtick = false;
1087 }
1088 i += 1;
1089 continue;
1090 }
1091
1092 if ch == '/' && i + 1 < len && chars[i + 1] == '/' {
1094 while i < len && chars[i] != '\n' {
1096 i += 1;
1097 }
1098 continue;
1099 }
1100 if ch == '#' {
1101 while i < len && chars[i] != '\n' {
1103 i += 1;
1104 }
1105 continue;
1106 }
1107 if ch == '/' && i + 1 < len && chars[i + 1] == '*' {
1109 in_block_comment = true;
1110 i += 2;
1111 continue;
1112 }
1113
1114 match ch {
1115 '\'' => in_single = true,
1116 '"' => in_double = true,
1117 '`' => in_backtick = true,
1118 '(' => paren += 1,
1119 ')' => paren -= 1,
1120 '[' => bracket += 1,
1121 ']' => bracket -= 1,
1122 '{' => brace += 1,
1123 '}' => brace -= 1,
1124 _ => {}
1125 }
1126
1127 i += 1;
1128 }
1129
1130 paren > 0 || bracket > 0 || brace > 0 || in_block_comment
1131}
1132
1133impl ReplState {
1134 fn new(
1135 initial_language: LanguageSpec,
1136 registry: LanguageRegistry,
1137 detect_enabled: bool,
1138 ) -> Result<Self> {
1139 let mut state = Self {
1140 registry,
1141 sessions: HashMap::new(),
1142 current_language: initial_language,
1143 detect_enabled,
1144 defined_names: HashSet::new(),
1145 history_entries: Vec::new(),
1146 };
1147 state.ensure_current_language()?;
1148 Ok(state)
1149 }
1150
1151 fn current_language(&self) -> &LanguageSpec {
1152 &self.current_language
1153 }
1154
1155 fn prompt(&self) -> String {
1156 format!("{}>>> ", self.current_language.canonical_id())
1157 }
1158
1159 fn ensure_current_language(&mut self) -> Result<()> {
1160 if self.registry.resolve(&self.current_language).is_none() {
1161 bail!(
1162 "language '{}' is not available",
1163 self.current_language.canonical_id()
1164 );
1165 }
1166 Ok(())
1167 }
1168
1169 fn handle_meta(&mut self, line: &str) -> Result<bool> {
1170 let command = line.trim_start_matches(':').trim();
1171 if command.is_empty() {
1172 return Ok(false);
1173 }
1174
1175 let mut parts = command.split_whitespace();
1176 let Some(head) = parts.next() else {
1177 return Ok(false);
1178 };
1179 match head {
1180 "exit" | "quit" => return Ok(true),
1181 "help" => {
1182 self.print_help();
1183 return Ok(false);
1184 }
1185 "languages" => {
1186 self.print_languages();
1187 return Ok(false);
1188 }
1189 "versions" => {
1190 if let Some(lang) = parts.next() {
1191 let spec = LanguageSpec::new(lang.to_string());
1192 if self.registry.resolve(&spec).is_some() {
1193 self.print_versions(Some(spec))?;
1194 } else {
1195 let available = self.registry.known_languages().join(", ");
1196 println!(
1197 "language '{}' not supported. Available: {available}",
1198 spec.canonical_id()
1199 );
1200 }
1201 } else {
1202 self.print_versions(None)?;
1203 }
1204 return Ok(false);
1205 }
1206 "detect" => {
1207 if let Some(arg) = parts.next() {
1208 match arg {
1209 "on" | "true" | "1" => {
1210 self.detect_enabled = true;
1211 println!("auto-detect enabled");
1212 }
1213 "off" | "false" | "0" => {
1214 self.detect_enabled = false;
1215 println!("auto-detect disabled");
1216 }
1217 "toggle" => {
1218 self.detect_enabled = !self.detect_enabled;
1219 println!(
1220 "auto-detect {}",
1221 if self.detect_enabled {
1222 "enabled"
1223 } else {
1224 "disabled"
1225 }
1226 );
1227 }
1228 _ => println!("usage: :detect <on|off|toggle>"),
1229 }
1230 } else {
1231 println!(
1232 "auto-detect is {}",
1233 if self.detect_enabled {
1234 "enabled"
1235 } else {
1236 "disabled"
1237 }
1238 );
1239 }
1240 return Ok(false);
1241 }
1242 "lang" => {
1243 if let Some(lang) = parts.next() {
1244 self.switch_language(LanguageSpec::new(lang.to_string()))?;
1245 } else {
1246 println!("usage: :lang <language>");
1247 }
1248 return Ok(false);
1249 }
1250 "reset" => {
1251 self.reset_current_session();
1252 println!(
1253 "session for '{}' reset",
1254 self.current_language.canonical_id()
1255 );
1256 return Ok(false);
1257 }
1258 "load" | "run" => {
1259 if let Some(token) = parts.next() {
1260 let path = PathBuf::from(token);
1261 self.execute_payload(ExecutionPayload::File {
1262 path,
1263 args: Vec::new(),
1264 })?;
1265 } else {
1266 println!("usage: :load <path>");
1267 }
1268 return Ok(false);
1269 }
1270 "save" => {
1271 if let Some(token) = parts.next() {
1272 let path = Path::new(token);
1273 match self.save_session(path) {
1274 Ok(count) => println!(
1275 "\x1b[2m[saved {count} entries to {}]\x1b[0m",
1276 path.display()
1277 ),
1278 Err(e) => println!("error saving session: {e}"),
1279 }
1280 } else {
1281 println!("usage: :save <path>");
1282 }
1283 return Ok(false);
1284 }
1285 "history" => {
1286 let limit: usize = parts.next().and_then(|s| s.parse().ok()).unwrap_or(25);
1287 self.show_history(limit);
1288 return Ok(false);
1289 }
1290 "install" => {
1291 if let Some(pkg) = parts.next() {
1292 self.install_package(pkg);
1293 } else {
1294 println!("usage: :install <package>");
1295 }
1296 return Ok(false);
1297 }
1298 "bench" => {
1299 let n: u32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(10);
1300 let code = parts.collect::<Vec<_>>().join(" ");
1301 if code.is_empty() {
1302 println!("usage: :bench [N] <code>");
1303 println!(" Runs <code> N times (default: 10) and reports timing stats.");
1304 } else {
1305 self.bench_code(&code, n)?;
1306 }
1307 return Ok(false);
1308 }
1309 "type" | "which" => {
1310 let lang = &self.current_language;
1311 println!(
1312 "\x1b[1m{}\x1b[0m \x1b[2m({})\x1b[0m",
1313 lang.canonical_id(),
1314 if self.sessions.contains_key(lang.canonical_id()) {
1315 "session active"
1316 } else {
1317 "no session"
1318 }
1319 );
1320 return Ok(false);
1321 }
1322 alias => {
1323 let spec = LanguageSpec::new(alias);
1324 if self.registry.resolve(&spec).is_some() {
1325 self.switch_language(spec)?;
1326 return Ok(false);
1327 }
1328 println!("unknown command: :{alias}. Type :help for help.");
1329 }
1330 }
1331
1332 Ok(false)
1333 }
1334
1335 fn switch_language(&mut self, spec: LanguageSpec) -> Result<()> {
1336 if self.current_language.canonical_id() == spec.canonical_id() {
1337 println!("already using {}", spec.canonical_id());
1338 return Ok(());
1339 }
1340 if self.registry.resolve(&spec).is_none() {
1341 let available = self.registry.known_languages().join(", ");
1342 bail!(
1343 "language '{}' not supported. Available: {available}",
1344 spec.canonical_id()
1345 );
1346 }
1347 self.current_language = spec;
1348 println!("switched to {}", self.current_language.canonical_id());
1349 Ok(())
1350 }
1351
1352 fn reset_current_session(&mut self) {
1353 let key = self.current_language.canonical_id().to_string();
1354 if let Some(mut session) = self.sessions.remove(&key) {
1355 let _ = session.shutdown();
1356 }
1357 }
1358
1359 fn execute_snippet(&mut self, code: &str) -> Result<()> {
1360 if self.detect_enabled
1361 && let Some(detected) = crate::detect::detect_language_from_snippet(code)
1362 && detected != self.current_language.canonical_id()
1363 {
1364 let spec = LanguageSpec::new(detected.to_string());
1365 if self.registry.resolve(&spec).is_some() {
1366 println!(
1367 "[auto-detect] switching {} -> {}",
1368 self.current_language.canonical_id(),
1369 spec.canonical_id()
1370 );
1371 self.current_language = spec;
1372 }
1373 }
1374
1375 self.defined_names.extend(extract_defined_names(
1377 code,
1378 self.current_language.canonical_id(),
1379 ));
1380
1381 let payload = ExecutionPayload::Inline {
1382 code: code.to_string(),
1383 args: Vec::new(),
1384 };
1385 self.execute_payload(payload)
1386 }
1387
1388 fn session_var_names(&self) -> Vec<String> {
1389 self.defined_names.iter().cloned().collect()
1390 }
1391
1392 fn execute_payload(&mut self, payload: ExecutionPayload) -> Result<()> {
1393 let language = self.current_language.clone();
1394 let outcome = match payload {
1395 ExecutionPayload::Inline { code, .. } => {
1396 if self.engine_supports_sessions(&language)? {
1397 self.eval_in_session(&language, &code)?
1398 } else {
1399 let engine = self
1400 .registry
1401 .resolve(&language)
1402 .context("language engine not found")?;
1403 engine.execute(&ExecutionPayload::Inline {
1404 code,
1405 args: Vec::new(),
1406 })?
1407 }
1408 }
1409 ExecutionPayload::File { ref path, .. } => {
1410 if self.engine_supports_sessions(&language)? {
1412 let code = std::fs::read_to_string(path)
1413 .with_context(|| format!("failed to read file: {}", path.display()))?;
1414 println!("\x1b[2m[loaded {}]\x1b[0m", path.display());
1415 self.eval_in_session(&language, &code)?
1416 } else {
1417 let engine = self
1418 .registry
1419 .resolve(&language)
1420 .context("language engine not found")?;
1421 engine.execute(&payload)?
1422 }
1423 }
1424 ExecutionPayload::Stdin { code, .. } => {
1425 if self.engine_supports_sessions(&language)? {
1426 self.eval_in_session(&language, &code)?
1427 } else {
1428 let engine = self
1429 .registry
1430 .resolve(&language)
1431 .context("language engine not found")?;
1432 engine.execute(&ExecutionPayload::Stdin {
1433 code,
1434 args: Vec::new(),
1435 })?
1436 }
1437 }
1438 };
1439 render_outcome(&outcome);
1440 Ok(())
1441 }
1442
1443 fn engine_supports_sessions(&self, language: &LanguageSpec) -> Result<bool> {
1444 Ok(self
1445 .registry
1446 .resolve(language)
1447 .context("language engine not found")?
1448 .supports_sessions())
1449 }
1450
1451 fn eval_in_session(&mut self, language: &LanguageSpec, code: &str) -> Result<ExecutionOutcome> {
1452 use std::collections::hash_map::Entry;
1453 let key = language.canonical_id().to_string();
1454 match self.sessions.entry(key) {
1455 Entry::Occupied(mut entry) => entry.get_mut().eval(code),
1456 Entry::Vacant(entry) => {
1457 let engine = self
1458 .registry
1459 .resolve(language)
1460 .context("language engine not found")?;
1461 let mut session = engine.start_session().with_context(|| {
1462 format!("failed to start {} session", language.canonical_id())
1463 })?;
1464 let outcome = session.eval(code)?;
1465 entry.insert(session);
1466 Ok(outcome)
1467 }
1468 }
1469 }
1470
1471 fn print_languages(&self) {
1472 let mut languages = self.registry.known_languages();
1473 languages.sort();
1474 println!("available languages: {}", languages.join(", "));
1475 }
1476
1477 fn print_versions(&self, language: Option<LanguageSpec>) -> Result<()> {
1478 println!("language toolchain versions...\n");
1479
1480 let mut available = 0u32;
1481 let mut missing = 0u32;
1482
1483 let mut languages: Vec<String> = if let Some(lang) = language {
1484 vec![lang.canonical_id().to_string()]
1485 } else {
1486 self.registry
1487 .known_languages()
1488 .into_iter()
1489 .map(|value| value.to_string())
1490 .collect()
1491 };
1492 languages.sort();
1493
1494 for lang_id in &languages {
1495 let spec = LanguageSpec::new(lang_id.to_string());
1496 if let Some(engine) = self.registry.resolve(&spec) {
1497 match engine.toolchain_version() {
1498 Ok(Some(version)) => {
1499 available += 1;
1500 println!(
1501 " [\x1b[32m OK \x1b[0m] {:<14} {} - {}",
1502 engine.display_name(),
1503 lang_id,
1504 version
1505 );
1506 }
1507 Ok(None) => {
1508 available += 1;
1509 println!(
1510 " [\x1b[33m ?? \x1b[0m] {:<14} {} - unknown",
1511 engine.display_name(),
1512 lang_id
1513 );
1514 }
1515 Err(_) => {
1516 missing += 1;
1517 println!(
1518 " [\x1b[31mMISS\x1b[0m] {:<14} {}",
1519 engine.display_name(),
1520 lang_id
1521 );
1522 }
1523 }
1524 }
1525 }
1526
1527 println!();
1528 println!(
1529 " {} available, {} missing, {} total",
1530 available,
1531 missing,
1532 available + missing
1533 );
1534
1535 if missing > 0 {
1536 println!("\n Tip: Install missing toolchains to enable those languages.");
1537 }
1538
1539 Ok(())
1540 }
1541
1542 fn install_package(&self, package: &str) {
1543 let lang_id = self.current_language.canonical_id();
1544 let override_key = format!("RUN_INSTALL_COMMAND_{}", lang_id.to_ascii_uppercase());
1545 let override_value = std::env::var(&override_key).ok();
1546 let Some(mut cmd) = build_install_command(lang_id, package) else {
1547 if override_value.is_some() {
1548 println!(
1549 "Error: {override_key} is set but could not be parsed.\n\
1550 Provide a valid command, e.g. {override_key}=\"uv pip install {{package}}\""
1551 );
1552 return;
1553 }
1554 println!(
1555 "No package manager available for '{lang_id}'.\n\
1556 Tip: set {override_key}=\"<cmd> {{package}}\"",
1557 );
1558 return;
1559 };
1560
1561 println!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
1562
1563 match cmd
1564 .stdin(std::process::Stdio::inherit())
1565 .stdout(std::process::Stdio::inherit())
1566 .stderr(std::process::Stdio::inherit())
1567 .status()
1568 {
1569 Ok(status) if status.success() => {
1570 println!("\x1b[32m[run]\x1b[0m Successfully installed '{package}'");
1571 }
1572 Ok(_) => {
1573 println!("\x1b[31m[run]\x1b[0m Failed to install '{package}'");
1574 }
1575 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
1576 let program = cmd.get_program().to_string_lossy();
1577 println!("\x1b[31m[run]\x1b[0m Package manager not found: {program}");
1578 println!("Tip: install it or set {override_key}=\"<cmd> {{package}}\"");
1579 }
1580 Err(e) => {
1581 println!("\x1b[31m[run]\x1b[0m Error running package manager: {e}");
1582 }
1583 }
1584 }
1585
1586 fn bench_code(&mut self, code: &str, iterations: u32) -> Result<()> {
1587 let language = self.current_language.clone();
1588
1589 let warmup = self.eval_in_session(&language, code)?;
1591 if !warmup.success() {
1592 println!("\x1b[31mError:\x1b[0m Code failed during warmup");
1593 if !warmup.stderr.is_empty() {
1594 print!("{}", warmup.stderr);
1595 }
1596 return Ok(());
1597 }
1598 println!("\x1b[2m warmup: {}ms\x1b[0m", warmup.duration.as_millis());
1599
1600 let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
1601 for i in 0..iterations {
1602 let outcome = self.eval_in_session(&language, code)?;
1603 let ms = outcome.duration.as_secs_f64() * 1000.0;
1604 times.push(ms);
1605 if i < 3 || i == iterations - 1 {
1606 println!("\x1b[2m run {}: {:.2}ms\x1b[0m", i + 1, ms);
1607 }
1608 }
1609
1610 times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1611 let total: f64 = times.iter().sum();
1612 let avg = total / times.len() as f64;
1613 let min = times.first().copied().unwrap_or(0.0);
1614 let max = times.last().copied().unwrap_or(0.0);
1615 let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
1616 (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
1617 } else {
1618 times[times.len() / 2]
1619 };
1620 let variance: f64 =
1621 times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
1622 let stddev = variance.sqrt();
1623
1624 println!();
1625 println!("\x1b[1mResults ({iterations} runs):\x1b[0m");
1626 println!(" min: \x1b[32m{min:.2}ms\x1b[0m");
1627 println!(" max: \x1b[33m{max:.2}ms\x1b[0m");
1628 println!(" avg: \x1b[36m{avg:.2}ms\x1b[0m");
1629 println!(" median: \x1b[36m{median:.2}ms\x1b[0m");
1630 println!(" stddev: {stddev:.2}ms");
1631 Ok(())
1632 }
1633
1634 fn save_session(&self, path: &Path) -> Result<usize> {
1635 use std::io::Write;
1636 let mut file = std::fs::File::create(path)
1637 .with_context(|| format!("failed to create {}", path.display()))?;
1638 let count = self.history_entries.len();
1639 for entry in &self.history_entries {
1640 writeln!(file, "{entry}")?;
1641 }
1642 Ok(count)
1643 }
1644
1645 fn show_history(&self, limit: usize) {
1646 let entries = &self.history_entries;
1647 let start = entries.len().saturating_sub(limit);
1648 if entries.is_empty() {
1649 println!("\x1b[2m(no history)\x1b[0m");
1650 return;
1651 }
1652 for (i, entry) in entries[start..].iter().enumerate() {
1653 let num = start + i + 1;
1654 let first_line = entry.lines().next().unwrap_or(entry);
1656 let is_multiline = entry.contains('\n');
1657 if is_multiline {
1658 println!("\x1b[2m[{num:>4}]\x1b[0m {first_line} \x1b[2m(...)\x1b[0m");
1659 } else {
1660 println!("\x1b[2m[{num:>4}]\x1b[0m {entry}");
1661 }
1662 }
1663 }
1664
1665 fn print_help(&self) {
1666 println!("\x1b[1mCommands\x1b[0m");
1667 println!(" \x1b[36m:help\x1b[0m \x1b[2mShow this help\x1b[0m");
1668 println!(" \x1b[36m:lang\x1b[0m <id> \x1b[2mSwitch language\x1b[0m");
1669 println!(" \x1b[36m:languages\x1b[0m \x1b[2mList available languages\x1b[0m");
1670 println!(" \x1b[36m:versions\x1b[0m [id] \x1b[2mShow toolchain versions\x1b[0m");
1671 println!(
1672 " \x1b[36m:detect\x1b[0m on|off \x1b[2mToggle auto language detection\x1b[0m"
1673 );
1674 println!(
1675 " \x1b[36m:reset\x1b[0m \x1b[2mClear current session state\x1b[0m"
1676 );
1677 println!(" \x1b[36m:load\x1b[0m <path> \x1b[2mLoad and execute a file\x1b[0m");
1678 println!(
1679 " \x1b[36m:save\x1b[0m <path> \x1b[2mSave session history to file\x1b[0m"
1680 );
1681 println!(
1682 " \x1b[36m:history\x1b[0m [n] \x1b[2mShow last n entries (default: 25)\x1b[0m"
1683 );
1684 println!(
1685 " \x1b[36m:install\x1b[0m <pkg> \x1b[2mInstall a package for current language\x1b[0m"
1686 );
1687 println!(
1688 " \x1b[36m:bench\x1b[0m [N] <code> \x1b[2mBenchmark code N times (default: 10)\x1b[0m"
1689 );
1690 println!(
1691 " \x1b[36m:type\x1b[0m \x1b[2mShow current language and session status\x1b[0m"
1692 );
1693 println!(" \x1b[36m:exit\x1b[0m \x1b[2mLeave the REPL\x1b[0m");
1694 println!("\x1b[2mLanguage shortcuts: :py, :js, :rs, :go, :cpp, :java, ...\x1b[0m");
1695 }
1696
1697 fn shutdown(&mut self) {
1698 for (_, mut session) in self.sessions.drain() {
1699 let _ = session.shutdown();
1700 }
1701 }
1702}
1703
1704fn render_outcome(outcome: &ExecutionOutcome) {
1705 if !outcome.stdout.is_empty() {
1706 print!("{}", ensure_trailing_newline(&outcome.stdout));
1707 }
1708 if !outcome.stderr.is_empty() {
1709 let formatted =
1710 output::format_stderr(&outcome.language, &outcome.stderr, outcome.success());
1711 eprint!("\x1b[31m{}\x1b[0m", ensure_trailing_newline(&formatted));
1712 }
1713
1714 let millis = outcome.duration.as_millis();
1715 if let Some(code) = outcome.exit_code
1716 && code != 0
1717 {
1718 println!("\x1b[2m[exit {code}] {}\x1b[0m", format_duration(millis));
1719 return;
1720 }
1721
1722 if millis > 0 {
1724 println!("\x1b[2m{}\x1b[0m", format_duration(millis));
1725 }
1726}
1727
1728fn format_duration(millis: u128) -> String {
1729 if millis >= 60_000 {
1730 let mins = millis / 60_000;
1731 let secs = (millis % 60_000) / 1000;
1732 format!("{mins}m {secs}s")
1733 } else if millis >= 1000 {
1734 let secs = millis as f64 / 1000.0;
1735 format!("{secs:.2}s")
1736 } else {
1737 format!("{millis}ms")
1738 }
1739}
1740
1741fn ensure_trailing_newline(text: &str) -> String {
1742 if text.ends_with('\n') {
1743 text.to_string()
1744 } else {
1745 let mut owned = text.to_string();
1746 owned.push('\n');
1747 owned
1748 }
1749}
1750
1751fn history_path() -> Option<PathBuf> {
1752 if let Ok(home) = std::env::var("HOME") {
1753 return Some(Path::new(&home).join(HISTORY_FILE));
1754 }
1755 None
1756}
1757
1758fn extract_defined_names(code: &str, language_id: &str) -> Vec<String> {
1760 let mut names = Vec::new();
1761 for line in code.lines() {
1762 let trimmed = line.trim();
1763 match language_id {
1764 "python" | "py" | "python3" | "py3" => {
1765 if let Some(rest) = trimmed.strip_prefix("def ") {
1767 if let Some(name) = rest.split('(').next() {
1768 let n = name.trim();
1769 if !n.is_empty() {
1770 names.push(n.to_string());
1771 }
1772 }
1773 } else if let Some(rest) = trimmed.strip_prefix("class ") {
1774 let name = rest.split(['(', ':']).next().unwrap_or("").trim();
1775 if !name.is_empty() {
1776 names.push(name.to_string());
1777 }
1778 } else if let Some(rest) = trimmed.strip_prefix("import ") {
1779 for part in rest.split(',') {
1780 let name = if let Some(alias) = part.split(" as ").nth(1) {
1781 alias.trim()
1782 } else {
1783 part.trim().split('.').next_back().unwrap_or("")
1784 };
1785 if !name.is_empty() {
1786 names.push(name.to_string());
1787 }
1788 }
1789 } else if trimmed.starts_with("from ") && trimmed.contains("import ") {
1790 if let Some(imports) = trimmed.split("import ").nth(1) {
1791 for part in imports.split(',') {
1792 let name = if let Some(alias) = part.split(" as ").nth(1) {
1793 alias.trim()
1794 } else {
1795 part.trim()
1796 };
1797 if !name.is_empty() {
1798 names.push(name.to_string());
1799 }
1800 }
1801 }
1802 } else if let Some(eq_pos) = trimmed.find('=') {
1803 let lhs = &trimmed[..eq_pos];
1804 if !lhs.contains('(')
1805 && !lhs.contains('[')
1806 && !trimmed[eq_pos..].starts_with("==")
1807 {
1808 for part in lhs.split(',') {
1809 let name = part.trim().split(':').next().unwrap_or("").trim();
1810 if !name.is_empty()
1811 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
1812 {
1813 names.push(name.to_string());
1814 }
1815 }
1816 }
1817 }
1818 }
1819 "javascript" | "js" | "node" | "typescript" | "ts" => {
1820 for prefix in ["let ", "const ", "var "] {
1822 if let Some(rest) = trimmed.strip_prefix(prefix) {
1823 let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
1824 if !name.is_empty() {
1825 names.push(name.to_string());
1826 }
1827 }
1828 }
1829 if let Some(rest) = trimmed.strip_prefix("function ") {
1830 let name = rest.split('(').next().unwrap_or("").trim();
1831 if !name.is_empty() {
1832 names.push(name.to_string());
1833 }
1834 } else if let Some(rest) = trimmed.strip_prefix("class ") {
1835 let name = rest.split(['{', ' ']).next().unwrap_or("").trim();
1836 if !name.is_empty() {
1837 names.push(name.to_string());
1838 }
1839 }
1840 }
1841 "rust" | "rs" => {
1842 for prefix in ["let ", "let mut "] {
1843 if let Some(rest) = trimmed.strip_prefix(prefix) {
1844 let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
1845 if !name.is_empty() {
1846 names.push(name.to_string());
1847 }
1848 }
1849 }
1850 if let Some(rest) = trimmed.strip_prefix("fn ") {
1851 let name = rest.split(['(', '<']).next().unwrap_or("").trim();
1852 if !name.is_empty() {
1853 names.push(name.to_string());
1854 }
1855 } else if let Some(rest) = trimmed.strip_prefix("struct ") {
1856 let name = rest.split(['{', '(', '<', ' ']).next().unwrap_or("").trim();
1857 if !name.is_empty() {
1858 names.push(name.to_string());
1859 }
1860 }
1861 }
1862 _ => {
1863 if let Some(eq_pos) = trimmed.find('=') {
1865 let lhs = trimmed[..eq_pos].trim();
1866 if !lhs.is_empty()
1867 && !trimmed[eq_pos..].starts_with("==")
1868 && lhs
1869 .chars()
1870 .all(|c| c.is_alphanumeric() || c == '_' || c == ' ')
1871 && let Some(name) = lhs.split_whitespace().last()
1872 {
1873 names.push(name.to_string());
1874 }
1875 }
1876 }
1877 }
1878 }
1879 names
1880}
1881
1882#[cfg(test)]
1883mod tests {
1884 use super::*;
1885
1886 #[test]
1887 fn language_aliases_resolve_in_registry() {
1888 let registry = LanguageRegistry::bootstrap();
1889 let aliases = [
1890 "python",
1891 "py",
1892 "python3",
1893 "rust",
1894 "rs",
1895 "go",
1896 "golang",
1897 "csharp",
1898 "cs",
1899 "c#",
1900 "typescript",
1901 "ts",
1902 "javascript",
1903 "js",
1904 "node",
1905 "ruby",
1906 "rb",
1907 "lua",
1908 "bash",
1909 "sh",
1910 "zsh",
1911 "java",
1912 "php",
1913 "kotlin",
1914 "kt",
1915 "c",
1916 "cpp",
1917 "c++",
1918 "swift",
1919 "swiftlang",
1920 "perl",
1921 "pl",
1922 "julia",
1923 "jl",
1924 ];
1925
1926 for alias in aliases {
1927 let spec = LanguageSpec::new(alias);
1928 assert!(
1929 registry.resolve(&spec).is_some(),
1930 "alias {alias} should resolve to a registered language"
1931 );
1932 }
1933 }
1934
1935 #[test]
1936 fn python_multiline_def_requires_blank_line_to_execute() {
1937 let mut p = PendingInput::new();
1938 p.push_line("def fib(n):");
1939 assert!(p.needs_more_input("python"));
1940 p.push_line(" return n");
1941 assert!(p.needs_more_input("python"));
1942 p.push_line(""); assert!(!p.needs_more_input("python"));
1944 }
1945
1946 #[test]
1947 fn python_dict_literal_colon_does_not_trigger_block() {
1948 let mut p = PendingInput::new();
1949 p.push_line("x = {'key': 'value'}");
1950 assert!(
1951 !p.needs_more_input("python"),
1952 "dict literal should not trigger multi-line"
1953 );
1954 }
1955
1956 #[test]
1957 fn python_class_block_needs_body() {
1958 let mut p = PendingInput::new();
1959 p.push_line("class Foo:");
1960 assert!(p.needs_more_input("python"));
1961 p.push_line(" pass");
1962 assert!(p.needs_more_input("python")); p.push_line(""); assert!(!p.needs_more_input("python"));
1965 }
1966
1967 #[test]
1968 fn python_if_block_with_dedented_body_is_complete() {
1969 let mut p = PendingInput::new();
1970 p.push_line("if True:");
1971 assert!(p.needs_more_input("python"));
1972 p.push_line(" print('yes')");
1973 assert!(p.needs_more_input("python"));
1974 p.push_line(""); assert!(!p.needs_more_input("python"));
1976 }
1977
1978 #[test]
1979 fn python_auto_indents_first_line_after_colon_header() {
1980 let mut p = PendingInput::new();
1981 p.push_line("def cool():");
1982 p.push_line_auto("python", r#"print("ok")"#);
1983 let code = p.take();
1984 assert!(
1985 code.contains(" print(\"ok\")\n"),
1986 "expected auto-indented print line, got:\n{code}"
1987 );
1988 }
1989
1990 #[test]
1991 fn python_auto_indents_nested_blocks() {
1992 let mut p = PendingInput::new();
1993 p.push_line("def bubble_sort(arr):");
1994 p.push_line_auto("python", "n = len(arr)");
1995 p.push_line_auto("python", "for i in range(n):");
1996 p.push_line_auto("python", "for j in range(0, n-i-1):");
1997 p.push_line_auto("python", "if arr[j] > arr[j+1]:");
1998 p.push_line_auto("python", "arr[j], arr[j+1] = arr[j+1], arr[j]");
1999 let code = p.take();
2000 assert!(
2001 code.contains(" n = len(arr)\n"),
2002 "missing indent for len"
2003 );
2004 assert!(
2005 code.contains(" for i in range(n):\n"),
2006 "missing indent for outer loop"
2007 );
2008 assert!(
2009 code.contains(" for j in range(0, n-i-1):\n"),
2010 "missing indent for inner loop"
2011 );
2012 assert!(
2013 code.contains(" if arr[j] > arr[j+1]:\n"),
2014 "missing indent for if"
2015 );
2016 assert!(
2017 code.contains(" arr[j], arr[j+1] = arr[j+1], arr[j]\n"),
2018 "missing indent for swap"
2019 );
2020 }
2021
2022 #[test]
2023 fn python_auto_dedents_else_like_blocks() {
2024 let mut p = PendingInput::new();
2025 p.push_line("def f():");
2026 p.push_line_auto("python", "if True:");
2027 p.push_line_auto("python", "print('yes')");
2028 p.push_line_auto("python", "else:");
2029 let code = p.take();
2030 assert!(
2031 code.contains(" else:\n"),
2032 "expected else to align with if block:\n{code}"
2033 );
2034 }
2035
2036 #[test]
2037 fn python_auto_dedents_return_to_def() {
2038 let mut p = PendingInput::new();
2039 p.push_line("def bubble_sort(arr):");
2040 p.push_line_auto("python", "for i in range(3):");
2041 p.push_line_auto("python", "for j in range(2):");
2042 p.push_line_auto("python", "if j:");
2043 p.push_line_auto("python", "pass");
2044 p.push_line_auto("python", "return arr");
2045 let code = p.take();
2046 assert!(
2047 code.contains(" return arr\n"),
2048 "expected return to align with def block:\n{code}"
2049 );
2050 }
2051
2052 #[test]
2053 fn generic_multiline_tracks_unclosed_delimiters() {
2054 let mut p = PendingInput::new();
2055 p.push_line("func(");
2056 assert!(p.needs_more_input("csharp"));
2057 p.push_line(")");
2058 assert!(!p.needs_more_input("csharp"));
2059 }
2060
2061 #[test]
2062 fn generic_multiline_tracks_trailing_equals() {
2063 let mut p = PendingInput::new();
2064 p.push_line("let x =");
2065 assert!(p.needs_more_input("rust"));
2066 p.push_line("10;");
2067 assert!(!p.needs_more_input("rust"));
2068 }
2069
2070 #[test]
2071 fn generic_multiline_tracks_trailing_dot() {
2072 let mut p = PendingInput::new();
2073 p.push_line("foo.");
2074 assert!(p.needs_more_input("csharp"));
2075 p.push_line("Bar()");
2076 assert!(!p.needs_more_input("csharp"));
2077 }
2078
2079 #[test]
2080 fn generic_multiline_accepts_preprocessor_lines() {
2081 let mut p = PendingInput::new();
2082 p.push_line("#include <stdio.h>");
2083 assert!(
2084 !p.needs_more_input("c"),
2085 "preprocessor lines should not force continuation"
2086 );
2087 }
2088}