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 package_install_command,
17};
18use crate::highlight;
19use crate::language::LanguageSpec;
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
598 if let Some(helper) = editor.helper_mut() {
599 helper.update_language(state.current_language().canonical_id().to_string());
600 }
601
602 match editor.readline(&prompt) {
603 Ok(line) => {
604 let raw = line.trim_end_matches(['\r', '\n']);
605
606 if let Some(p) = pending.as_mut() {
607 if raw.trim() == ":cancel" {
608 pending = None;
609 continue;
610 }
611
612 p.push_line_auto(state.current_language().canonical_id(), raw);
613 if p.needs_more_input(state.current_language().canonical_id()) {
614 continue;
615 }
616
617 let code = p.take();
618 pending = None;
619 let trimmed = code.trim_end();
620 if !trimmed.is_empty() {
621 let _ = editor.add_history_entry(trimmed);
622 state.history_entries.push(trimmed.to_string());
623 state.execute_snippet(trimmed)?;
624 if let Some(helper) = editor.helper_mut() {
625 helper.update_session_vars(state.session_var_names());
626 }
627 }
628 continue;
629 }
630
631 if raw.trim().is_empty() {
632 continue;
633 }
634
635 if raw.trim_start().starts_with(':') {
636 let trimmed = raw.trim();
637 let _ = editor.add_history_entry(trimmed);
638 if state.handle_meta(trimmed)? {
639 break;
640 }
641 continue;
642 }
643
644 let mut p = PendingInput::new();
645 p.push_line(raw);
646 if p.needs_more_input(state.current_language().canonical_id()) {
647 pending = Some(p);
648 continue;
649 }
650
651 let trimmed = raw.trim_end();
652 let _ = editor.add_history_entry(trimmed);
653 state.history_entries.push(trimmed.to_string());
654 state.execute_snippet(trimmed)?;
655 if let Some(helper) = editor.helper_mut() {
656 helper.update_session_vars(state.session_var_names());
657 }
658 }
659 Err(ReadlineError::Interrupted) => {
660 println!("^C");
661 pending = None;
662 continue;
663 }
664 Err(ReadlineError::Eof) => {
665 println!("bye");
666 break;
667 }
668 Err(err) => {
669 bail!("readline error: {err}");
670 }
671 }
672 }
673
674 if let Some(path) = history_path() {
675 let _ = editor.save_history(&path);
676 }
677
678 state.shutdown();
679 Ok(0)
680}
681
682struct ReplState {
683 registry: LanguageRegistry,
684 sessions: HashMap<String, Box<dyn LanguageSession>>, current_language: LanguageSpec,
686 detect_enabled: bool,
687 defined_names: HashSet<String>,
688 history_entries: Vec<String>,
689}
690
691struct PendingInput {
692 buf: String,
693}
694
695impl PendingInput {
696 fn new() -> Self {
697 Self { buf: String::new() }
698 }
699
700 fn prompt(&self) -> String {
701 "... ".to_string()
702 }
703
704 fn push_line(&mut self, line: &str) {
705 self.buf.push_str(line);
706 self.buf.push('\n');
707 }
708
709 fn push_line_auto(&mut self, language_id: &str, line: &str) {
710 match language_id {
711 "python" | "py" | "python3" | "py3" => {
712 let adjusted = python_auto_indent(line, &self.buf);
713 self.push_line(&adjusted);
714 }
715 _ => self.push_line(line),
716 }
717 }
718
719 fn take(&mut self) -> String {
720 std::mem::take(&mut self.buf)
721 }
722
723 fn needs_more_input(&self, language_id: &str) -> bool {
724 needs_more_input(language_id, &self.buf)
725 }
726}
727
728fn needs_more_input(language_id: &str, code: &str) -> bool {
729 match language_id {
730 "python" | "py" | "python3" | "py3" => needs_more_input_python(code),
731
732 _ => has_unclosed_delimiters(code) || generic_line_looks_incomplete(code),
733 }
734}
735
736fn generic_line_looks_incomplete(code: &str) -> bool {
737 let mut last: Option<&str> = None;
738 for line in code.lines().rev() {
739 let trimmed = line.trim_end();
740 if trimmed.trim().is_empty() {
741 continue;
742 }
743 last = Some(trimmed);
744 break;
745 }
746 let Some(line) = last else { return false };
747 let line = line.trim();
748 if line.is_empty() {
749 return false;
750 }
751
752 if line.ends_with('\\') {
753 return true;
754 }
755
756 const TAILS: [&str; 24] = [
757 "=", "+", "-", "*", "/", "%", "&", "|", "^", "!", "<", ">", "&&", "||", "??", "?:", "?",
758 ":", ".", ",", "=>", "->", "::", "..",
759 ];
760 if TAILS.iter().any(|tok| line.ends_with(tok)) {
761 return true;
762 }
763
764 const PREFIXES: [&str; 9] = [
765 "return", "throw", "yield", "await", "import", "from", "export", "case", "else",
766 ];
767 let lowered = line.to_ascii_lowercase();
768 if PREFIXES
769 .iter()
770 .any(|kw| lowered == *kw || lowered.ends_with(&format!(" {kw}")))
771 {
772 return true;
773 }
774
775 false
776}
777
778fn needs_more_input_python(code: &str) -> bool {
779 if has_unclosed_delimiters(code) {
780 return true;
781 }
782
783 let mut last_nonempty: Option<&str> = None;
784 let mut saw_block_header = false;
785 let mut has_body_after_header = false;
786
787 for line in code.lines() {
788 let trimmed = line.trim_end();
789 if trimmed.trim().is_empty() {
790 continue;
791 }
792 last_nonempty = Some(trimmed);
793 if is_python_block_header(trimmed.trim()) {
794 saw_block_header = true;
795 has_body_after_header = false;
796 } else if saw_block_header {
797 has_body_after_header = true;
798 }
799 }
800
801 if !saw_block_header {
802 return false;
803 }
804
805 if code.ends_with("\n\n") {
807 return false;
808 }
809
810 if !has_body_after_header {
812 return true;
813 }
814
815 if let Some(last) = last_nonempty
817 && (last.starts_with(' ') || last.starts_with('\t'))
818 {
819 return true;
820 }
821
822 false
823}
824
825fn is_python_block_header(line: &str) -> bool {
828 if !line.ends_with(':') {
829 return false;
830 }
831 let lowered = line.to_ascii_lowercase();
832 const BLOCK_KEYWORDS: &[&str] = &[
833 "def ",
834 "class ",
835 "if ",
836 "elif ",
837 "else:",
838 "for ",
839 "while ",
840 "try:",
841 "except",
842 "finally:",
843 "with ",
844 "async def ",
845 "async for ",
846 "async with ",
847 ];
848 BLOCK_KEYWORDS.iter().any(|kw| lowered.starts_with(kw))
849}
850
851fn python_auto_indent(line: &str, existing: &str) -> String {
852 let trimmed = line.trim_end_matches(['\r', '\n']);
853 let raw = trimmed;
854 if raw.trim().is_empty() {
855 return raw.to_string();
856 }
857
858 if raw.starts_with(' ') || raw.starts_with('\t') {
859 return raw.to_string();
860 }
861
862 let mut last_nonempty: Option<&str> = None;
863 for l in existing.lines().rev() {
864 if l.trim().is_empty() {
865 continue;
866 }
867 last_nonempty = Some(l);
868 break;
869 }
870
871 let Some(prev) = last_nonempty else {
872 return raw.to_string();
873 };
874 let prev_trimmed = prev.trim_end();
875
876 if !prev_trimmed.ends_with(':') {
877 return raw.to_string();
878 }
879
880 let lowered = raw.trim().to_ascii_lowercase();
881 if lowered.starts_with("else:")
882 || lowered.starts_with("elif ")
883 || lowered.starts_with("except")
884 || lowered.starts_with("finally:")
885 {
886 return raw.to_string();
887 }
888
889 let base_indent = prev
890 .chars()
891 .take_while(|c| *c == ' ' || *c == '\t')
892 .collect::<String>();
893
894 format!("{base_indent} {raw}")
895}
896
897fn has_unclosed_delimiters(code: &str) -> bool {
898 let mut paren = 0i32;
899 let mut bracket = 0i32;
900 let mut brace = 0i32;
901
902 let mut in_single = false;
903 let mut in_double = false;
904 let mut in_backtick = false;
905 let mut in_block_comment = false;
906 let mut escape = false;
907
908 let chars: Vec<char> = code.chars().collect();
909 let len = chars.len();
910 let mut i = 0;
911
912 while i < len {
913 let ch = chars[i];
914
915 if escape {
916 escape = false;
917 i += 1;
918 continue;
919 }
920
921 if in_block_comment {
923 if ch == '*' && i + 1 < len && chars[i + 1] == '/' {
924 in_block_comment = false;
925 i += 2;
926 continue;
927 }
928 i += 1;
929 continue;
930 }
931
932 if in_single {
933 if ch == '\\' {
934 escape = true;
935 } else if ch == '\'' {
936 in_single = false;
937 }
938 i += 1;
939 continue;
940 }
941 if in_double {
942 if ch == '\\' {
943 escape = true;
944 } else if ch == '"' {
945 in_double = false;
946 }
947 i += 1;
948 continue;
949 }
950 if in_backtick {
951 if ch == '\\' {
952 escape = true;
953 } else if ch == '`' {
954 in_backtick = false;
955 }
956 i += 1;
957 continue;
958 }
959
960 if ch == '/' && i + 1 < len && chars[i + 1] == '/' {
962 while i < len && chars[i] != '\n' {
964 i += 1;
965 }
966 continue;
967 }
968 if ch == '#' {
969 while i < len && chars[i] != '\n' {
971 i += 1;
972 }
973 continue;
974 }
975 if ch == '/' && i + 1 < len && chars[i + 1] == '*' {
977 in_block_comment = true;
978 i += 2;
979 continue;
980 }
981
982 match ch {
983 '\'' => in_single = true,
984 '"' => in_double = true,
985 '`' => in_backtick = true,
986 '(' => paren += 1,
987 ')' => paren -= 1,
988 '[' => bracket += 1,
989 ']' => bracket -= 1,
990 '{' => brace += 1,
991 '}' => brace -= 1,
992 _ => {}
993 }
994
995 i += 1;
996 }
997
998 paren > 0 || bracket > 0 || brace > 0 || in_block_comment
999}
1000
1001impl ReplState {
1002 fn new(
1003 initial_language: LanguageSpec,
1004 registry: LanguageRegistry,
1005 detect_enabled: bool,
1006 ) -> Result<Self> {
1007 let mut state = Self {
1008 registry,
1009 sessions: HashMap::new(),
1010 current_language: initial_language,
1011 detect_enabled,
1012 defined_names: HashSet::new(),
1013 history_entries: Vec::new(),
1014 };
1015 state.ensure_current_language()?;
1016 Ok(state)
1017 }
1018
1019 fn current_language(&self) -> &LanguageSpec {
1020 &self.current_language
1021 }
1022
1023 fn prompt(&self) -> String {
1024 format!("{}>>> ", self.current_language.canonical_id())
1025 }
1026
1027 fn ensure_current_language(&mut self) -> Result<()> {
1028 if self.registry.resolve(&self.current_language).is_none() {
1029 bail!(
1030 "language '{}' is not available",
1031 self.current_language.canonical_id()
1032 );
1033 }
1034 Ok(())
1035 }
1036
1037 fn handle_meta(&mut self, line: &str) -> Result<bool> {
1038 let command = line.trim_start_matches(':').trim();
1039 if command.is_empty() {
1040 return Ok(false);
1041 }
1042
1043 let mut parts = command.split_whitespace();
1044 let Some(head) = parts.next() else {
1045 return Ok(false);
1046 };
1047 match head {
1048 "exit" | "quit" => return Ok(true),
1049 "help" => {
1050 self.print_help();
1051 return Ok(false);
1052 }
1053 "languages" => {
1054 self.print_languages();
1055 return Ok(false);
1056 }
1057 "detect" => {
1058 if let Some(arg) = parts.next() {
1059 match arg {
1060 "on" | "true" | "1" => {
1061 self.detect_enabled = true;
1062 println!("auto-detect enabled");
1063 }
1064 "off" | "false" | "0" => {
1065 self.detect_enabled = false;
1066 println!("auto-detect disabled");
1067 }
1068 "toggle" => {
1069 self.detect_enabled = !self.detect_enabled;
1070 println!(
1071 "auto-detect {}",
1072 if self.detect_enabled {
1073 "enabled"
1074 } else {
1075 "disabled"
1076 }
1077 );
1078 }
1079 _ => println!("usage: :detect <on|off|toggle>"),
1080 }
1081 } else {
1082 println!(
1083 "auto-detect is {}",
1084 if self.detect_enabled {
1085 "enabled"
1086 } else {
1087 "disabled"
1088 }
1089 );
1090 }
1091 return Ok(false);
1092 }
1093 "lang" => {
1094 if let Some(lang) = parts.next() {
1095 self.switch_language(LanguageSpec::new(lang.to_string()))?;
1096 } else {
1097 println!("usage: :lang <language>");
1098 }
1099 return Ok(false);
1100 }
1101 "reset" => {
1102 self.reset_current_session();
1103 println!(
1104 "session for '{}' reset",
1105 self.current_language.canonical_id()
1106 );
1107 return Ok(false);
1108 }
1109 "load" | "run" => {
1110 if let Some(token) = parts.next() {
1111 let path = PathBuf::from(token);
1112 self.execute_payload(ExecutionPayload::File { path })?;
1113 } else {
1114 println!("usage: :load <path>");
1115 }
1116 return Ok(false);
1117 }
1118 "save" => {
1119 if let Some(token) = parts.next() {
1120 let path = Path::new(token);
1121 match self.save_session(path) {
1122 Ok(count) => println!(
1123 "\x1b[2m[saved {count} entries to {}]\x1b[0m",
1124 path.display()
1125 ),
1126 Err(e) => println!("error saving session: {e}"),
1127 }
1128 } else {
1129 println!("usage: :save <path>");
1130 }
1131 return Ok(false);
1132 }
1133 "history" => {
1134 let limit: usize = parts.next().and_then(|s| s.parse().ok()).unwrap_or(25);
1135 self.show_history(limit);
1136 return Ok(false);
1137 }
1138 "install" => {
1139 if let Some(pkg) = parts.next() {
1140 self.install_package(pkg);
1141 } else {
1142 println!("usage: :install <package>");
1143 }
1144 return Ok(false);
1145 }
1146 "bench" => {
1147 let n: u32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(10);
1148 let code = parts.collect::<Vec<_>>().join(" ");
1149 if code.is_empty() {
1150 println!("usage: :bench [N] <code>");
1151 println!(" Runs <code> N times (default: 10) and reports timing stats.");
1152 } else {
1153 self.bench_code(&code, n)?;
1154 }
1155 return Ok(false);
1156 }
1157 "type" | "which" => {
1158 let lang = &self.current_language;
1159 println!(
1160 "\x1b[1m{}\x1b[0m \x1b[2m({})\x1b[0m",
1161 lang.canonical_id(),
1162 if self.sessions.contains_key(lang.canonical_id()) {
1163 "session active"
1164 } else {
1165 "no session"
1166 }
1167 );
1168 return Ok(false);
1169 }
1170 alias => {
1171 let spec = LanguageSpec::new(alias);
1172 if self.registry.resolve(&spec).is_some() {
1173 self.switch_language(spec)?;
1174 return Ok(false);
1175 }
1176 println!("unknown command: :{alias}. Type :help for help.");
1177 }
1178 }
1179
1180 Ok(false)
1181 }
1182
1183 fn switch_language(&mut self, spec: LanguageSpec) -> Result<()> {
1184 if self.current_language.canonical_id() == spec.canonical_id() {
1185 println!("already using {}", spec.canonical_id());
1186 return Ok(());
1187 }
1188 if self.registry.resolve(&spec).is_none() {
1189 let available = self.registry.known_languages().join(", ");
1190 bail!(
1191 "language '{}' not supported. Available: {available}",
1192 spec.canonical_id()
1193 );
1194 }
1195 self.current_language = spec;
1196 println!("switched to {}", self.current_language.canonical_id());
1197 Ok(())
1198 }
1199
1200 fn reset_current_session(&mut self) {
1201 let key = self.current_language.canonical_id().to_string();
1202 if let Some(mut session) = self.sessions.remove(&key) {
1203 let _ = session.shutdown();
1204 }
1205 }
1206
1207 fn execute_snippet(&mut self, code: &str) -> Result<()> {
1208 if self.detect_enabled
1209 && let Some(detected) = crate::detect::detect_language_from_snippet(code)
1210 && detected != self.current_language.canonical_id()
1211 {
1212 let spec = LanguageSpec::new(detected.to_string());
1213 if self.registry.resolve(&spec).is_some() {
1214 println!(
1215 "[auto-detect] switching {} -> {}",
1216 self.current_language.canonical_id(),
1217 spec.canonical_id()
1218 );
1219 self.current_language = spec;
1220 }
1221 }
1222
1223 self.defined_names.extend(extract_defined_names(
1225 code,
1226 self.current_language.canonical_id(),
1227 ));
1228
1229 let payload = ExecutionPayload::Inline {
1230 code: code.to_string(),
1231 };
1232 self.execute_payload(payload)
1233 }
1234
1235 fn session_var_names(&self) -> Vec<String> {
1236 self.defined_names.iter().cloned().collect()
1237 }
1238
1239 fn execute_payload(&mut self, payload: ExecutionPayload) -> Result<()> {
1240 let language = self.current_language.clone();
1241 let outcome = match payload {
1242 ExecutionPayload::Inline { code } => {
1243 if self.engine_supports_sessions(&language)? {
1244 self.eval_in_session(&language, &code)?
1245 } else {
1246 let engine = self
1247 .registry
1248 .resolve(&language)
1249 .context("language engine not found")?;
1250 engine.execute(&ExecutionPayload::Inline { code })?
1251 }
1252 }
1253 ExecutionPayload::File { ref path } => {
1254 if self.engine_supports_sessions(&language)? {
1256 let code = std::fs::read_to_string(path)
1257 .with_context(|| format!("failed to read file: {}", path.display()))?;
1258 println!("\x1b[2m[loaded {}]\x1b[0m", path.display());
1259 self.eval_in_session(&language, &code)?
1260 } else {
1261 let engine = self
1262 .registry
1263 .resolve(&language)
1264 .context("language engine not found")?;
1265 engine.execute(&payload)?
1266 }
1267 }
1268 ExecutionPayload::Stdin { code } => {
1269 if self.engine_supports_sessions(&language)? {
1270 self.eval_in_session(&language, &code)?
1271 } else {
1272 let engine = self
1273 .registry
1274 .resolve(&language)
1275 .context("language engine not found")?;
1276 engine.execute(&ExecutionPayload::Stdin { code })?
1277 }
1278 }
1279 };
1280 render_outcome(&outcome);
1281 Ok(())
1282 }
1283
1284 fn engine_supports_sessions(&self, language: &LanguageSpec) -> Result<bool> {
1285 Ok(self
1286 .registry
1287 .resolve(language)
1288 .context("language engine not found")?
1289 .supports_sessions())
1290 }
1291
1292 fn eval_in_session(&mut self, language: &LanguageSpec, code: &str) -> Result<ExecutionOutcome> {
1293 use std::collections::hash_map::Entry;
1294 let key = language.canonical_id().to_string();
1295 match self.sessions.entry(key) {
1296 Entry::Occupied(mut entry) => entry.get_mut().eval(code),
1297 Entry::Vacant(entry) => {
1298 let engine = self
1299 .registry
1300 .resolve(language)
1301 .context("language engine not found")?;
1302 let mut session = engine.start_session().with_context(|| {
1303 format!("failed to start {} session", language.canonical_id())
1304 })?;
1305 let outcome = session.eval(code)?;
1306 entry.insert(session);
1307 Ok(outcome)
1308 }
1309 }
1310 }
1311
1312 fn print_languages(&self) {
1313 let mut languages = self.registry.known_languages();
1314 languages.sort();
1315 println!("available languages: {}", languages.join(", "));
1316 }
1317
1318 fn install_package(&self, package: &str) {
1319 let lang_id = self.current_language.canonical_id();
1320 if package_install_command(lang_id).is_none() {
1321 println!("No package manager available for '{lang_id}'.");
1322 return;
1323 }
1324
1325 let Some(mut cmd) = build_install_command(lang_id, package) else {
1326 println!("Failed to build install command for '{lang_id}'.");
1327 return;
1328 };
1329
1330 println!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
1331
1332 match cmd
1333 .stdin(std::process::Stdio::inherit())
1334 .stdout(std::process::Stdio::inherit())
1335 .stderr(std::process::Stdio::inherit())
1336 .status()
1337 {
1338 Ok(status) if status.success() => {
1339 println!("\x1b[32m[run]\x1b[0m Successfully installed '{package}'");
1340 }
1341 Ok(_) => {
1342 println!("\x1b[31m[run]\x1b[0m Failed to install '{package}'");
1343 }
1344 Err(e) => {
1345 println!("\x1b[31m[run]\x1b[0m Error running package manager: {e}");
1346 }
1347 }
1348 }
1349
1350 fn bench_code(&mut self, code: &str, iterations: u32) -> Result<()> {
1351 let language = self.current_language.clone();
1352
1353 let warmup = self.eval_in_session(&language, code)?;
1355 if !warmup.success() {
1356 println!("\x1b[31mError:\x1b[0m Code failed during warmup");
1357 if !warmup.stderr.is_empty() {
1358 print!("{}", warmup.stderr);
1359 }
1360 return Ok(());
1361 }
1362 println!("\x1b[2m warmup: {}ms\x1b[0m", warmup.duration.as_millis());
1363
1364 let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
1365 for i in 0..iterations {
1366 let outcome = self.eval_in_session(&language, code)?;
1367 let ms = outcome.duration.as_secs_f64() * 1000.0;
1368 times.push(ms);
1369 if i < 3 || i == iterations - 1 {
1370 println!("\x1b[2m run {}: {:.2}ms\x1b[0m", i + 1, ms);
1371 }
1372 }
1373
1374 times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1375 let total: f64 = times.iter().sum();
1376 let avg = total / times.len() as f64;
1377 let min = times.first().copied().unwrap_or(0.0);
1378 let max = times.last().copied().unwrap_or(0.0);
1379 let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
1380 (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
1381 } else {
1382 times[times.len() / 2]
1383 };
1384 let variance: f64 =
1385 times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
1386 let stddev = variance.sqrt();
1387
1388 println!();
1389 println!("\x1b[1mResults ({iterations} runs):\x1b[0m");
1390 println!(" min: \x1b[32m{min:.2}ms\x1b[0m");
1391 println!(" max: \x1b[33m{max:.2}ms\x1b[0m");
1392 println!(" avg: \x1b[36m{avg:.2}ms\x1b[0m");
1393 println!(" median: \x1b[36m{median:.2}ms\x1b[0m");
1394 println!(" stddev: {stddev:.2}ms");
1395 Ok(())
1396 }
1397
1398 fn save_session(&self, path: &Path) -> Result<usize> {
1399 use std::io::Write;
1400 let mut file = std::fs::File::create(path)
1401 .with_context(|| format!("failed to create {}", path.display()))?;
1402 let count = self.history_entries.len();
1403 for entry in &self.history_entries {
1404 writeln!(file, "{entry}")?;
1405 }
1406 Ok(count)
1407 }
1408
1409 fn show_history(&self, limit: usize) {
1410 let entries = &self.history_entries;
1411 let start = entries.len().saturating_sub(limit);
1412 if entries.is_empty() {
1413 println!("\x1b[2m(no history)\x1b[0m");
1414 return;
1415 }
1416 for (i, entry) in entries[start..].iter().enumerate() {
1417 let num = start + i + 1;
1418 let first_line = entry.lines().next().unwrap_or(entry);
1420 let is_multiline = entry.contains('\n');
1421 if is_multiline {
1422 println!("\x1b[2m[{num:>4}]\x1b[0m {first_line} \x1b[2m(...)\x1b[0m");
1423 } else {
1424 println!("\x1b[2m[{num:>4}]\x1b[0m {entry}");
1425 }
1426 }
1427 }
1428
1429 fn print_help(&self) {
1430 println!("\x1b[1mCommands\x1b[0m");
1431 println!(" \x1b[36m:help\x1b[0m \x1b[2mShow this help\x1b[0m");
1432 println!(" \x1b[36m:lang\x1b[0m <id> \x1b[2mSwitch language\x1b[0m");
1433 println!(" \x1b[36m:languages\x1b[0m \x1b[2mList available languages\x1b[0m");
1434 println!(
1435 " \x1b[36m:detect\x1b[0m on|off \x1b[2mToggle auto language detection\x1b[0m"
1436 );
1437 println!(
1438 " \x1b[36m:reset\x1b[0m \x1b[2mClear current session state\x1b[0m"
1439 );
1440 println!(" \x1b[36m:load\x1b[0m <path> \x1b[2mLoad and execute a file\x1b[0m");
1441 println!(
1442 " \x1b[36m:save\x1b[0m <path> \x1b[2mSave session history to file\x1b[0m"
1443 );
1444 println!(
1445 " \x1b[36m:history\x1b[0m [n] \x1b[2mShow last n entries (default: 25)\x1b[0m"
1446 );
1447 println!(
1448 " \x1b[36m:install\x1b[0m <pkg> \x1b[2mInstall a package for current language\x1b[0m"
1449 );
1450 println!(
1451 " \x1b[36m:bench\x1b[0m [N] <code> \x1b[2mBenchmark code N times (default: 10)\x1b[0m"
1452 );
1453 println!(
1454 " \x1b[36m:type\x1b[0m \x1b[2mShow current language and session status\x1b[0m"
1455 );
1456 println!(" \x1b[36m:exit\x1b[0m \x1b[2mLeave the REPL\x1b[0m");
1457 println!("\x1b[2mLanguage shortcuts: :py, :js, :rs, :go, :cpp, :java, ...\x1b[0m");
1458 }
1459
1460 fn shutdown(&mut self) {
1461 for (_, mut session) in self.sessions.drain() {
1462 let _ = session.shutdown();
1463 }
1464 }
1465}
1466
1467fn render_outcome(outcome: &ExecutionOutcome) {
1468 if !outcome.stdout.is_empty() {
1469 print!("{}", ensure_trailing_newline(&outcome.stdout));
1470 }
1471 if !outcome.stderr.is_empty() {
1472 eprint!(
1473 "\x1b[31m{}\x1b[0m",
1474 ensure_trailing_newline(&outcome.stderr)
1475 );
1476 }
1477
1478 let millis = outcome.duration.as_millis();
1479 if let Some(code) = outcome.exit_code
1480 && code != 0
1481 {
1482 println!("\x1b[2m[exit {code}] {}\x1b[0m", format_duration(millis));
1483 return;
1484 }
1485
1486 if millis > 0 {
1488 println!("\x1b[2m{}\x1b[0m", format_duration(millis));
1489 }
1490}
1491
1492fn format_duration(millis: u128) -> String {
1493 if millis >= 60_000 {
1494 let mins = millis / 60_000;
1495 let secs = (millis % 60_000) / 1000;
1496 format!("{mins}m {secs}s")
1497 } else if millis >= 1000 {
1498 let secs = millis as f64 / 1000.0;
1499 format!("{secs:.2}s")
1500 } else {
1501 format!("{millis}ms")
1502 }
1503}
1504
1505fn ensure_trailing_newline(text: &str) -> String {
1506 if text.ends_with('\n') {
1507 text.to_string()
1508 } else {
1509 let mut owned = text.to_string();
1510 owned.push('\n');
1511 owned
1512 }
1513}
1514
1515fn history_path() -> Option<PathBuf> {
1516 if let Ok(home) = std::env::var("HOME") {
1517 return Some(Path::new(&home).join(HISTORY_FILE));
1518 }
1519 None
1520}
1521
1522fn extract_defined_names(code: &str, language_id: &str) -> Vec<String> {
1524 let mut names = Vec::new();
1525 for line in code.lines() {
1526 let trimmed = line.trim();
1527 match language_id {
1528 "python" | "py" | "python3" | "py3" => {
1529 if let Some(rest) = trimmed.strip_prefix("def ") {
1531 if let Some(name) = rest.split('(').next() {
1532 let n = name.trim();
1533 if !n.is_empty() {
1534 names.push(n.to_string());
1535 }
1536 }
1537 } else if let Some(rest) = trimmed.strip_prefix("class ") {
1538 let name = rest.split(['(', ':']).next().unwrap_or("").trim();
1539 if !name.is_empty() {
1540 names.push(name.to_string());
1541 }
1542 } else if let Some(rest) = trimmed.strip_prefix("import ") {
1543 for part in rest.split(',') {
1544 let name = if let Some(alias) = part.split(" as ").nth(1) {
1545 alias.trim()
1546 } else {
1547 part.trim().split('.').next_back().unwrap_or("")
1548 };
1549 if !name.is_empty() {
1550 names.push(name.to_string());
1551 }
1552 }
1553 } else if trimmed.starts_with("from ") && trimmed.contains("import ") {
1554 if let Some(imports) = trimmed.split("import ").nth(1) {
1555 for part in imports.split(',') {
1556 let name = if let Some(alias) = part.split(" as ").nth(1) {
1557 alias.trim()
1558 } else {
1559 part.trim()
1560 };
1561 if !name.is_empty() {
1562 names.push(name.to_string());
1563 }
1564 }
1565 }
1566 } else if let Some(eq_pos) = trimmed.find('=') {
1567 let lhs = &trimmed[..eq_pos];
1568 if !lhs.contains('(')
1569 && !lhs.contains('[')
1570 && !trimmed[eq_pos..].starts_with("==")
1571 {
1572 for part in lhs.split(',') {
1573 let name = part.trim().split(':').next().unwrap_or("").trim();
1574 if !name.is_empty()
1575 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
1576 {
1577 names.push(name.to_string());
1578 }
1579 }
1580 }
1581 }
1582 }
1583 "javascript" | "js" | "node" | "typescript" | "ts" => {
1584 for prefix in ["let ", "const ", "var "] {
1586 if let Some(rest) = trimmed.strip_prefix(prefix) {
1587 let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
1588 if !name.is_empty() {
1589 names.push(name.to_string());
1590 }
1591 }
1592 }
1593 if let Some(rest) = trimmed.strip_prefix("function ") {
1594 let name = rest.split('(').next().unwrap_or("").trim();
1595 if !name.is_empty() {
1596 names.push(name.to_string());
1597 }
1598 } else if let Some(rest) = trimmed.strip_prefix("class ") {
1599 let name = rest.split(['{', ' ']).next().unwrap_or("").trim();
1600 if !name.is_empty() {
1601 names.push(name.to_string());
1602 }
1603 }
1604 }
1605 "rust" | "rs" => {
1606 for prefix in ["let ", "let mut "] {
1607 if let Some(rest) = trimmed.strip_prefix(prefix) {
1608 let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
1609 if !name.is_empty() {
1610 names.push(name.to_string());
1611 }
1612 }
1613 }
1614 if let Some(rest) = trimmed.strip_prefix("fn ") {
1615 let name = rest.split(['(', '<']).next().unwrap_or("").trim();
1616 if !name.is_empty() {
1617 names.push(name.to_string());
1618 }
1619 } else if let Some(rest) = trimmed.strip_prefix("struct ") {
1620 let name = rest.split(['{', '(', '<', ' ']).next().unwrap_or("").trim();
1621 if !name.is_empty() {
1622 names.push(name.to_string());
1623 }
1624 }
1625 }
1626 _ => {
1627 if let Some(eq_pos) = trimmed.find('=') {
1629 let lhs = trimmed[..eq_pos].trim();
1630 if !lhs.is_empty()
1631 && !trimmed[eq_pos..].starts_with("==")
1632 && lhs
1633 .chars()
1634 .all(|c| c.is_alphanumeric() || c == '_' || c == ' ')
1635 && let Some(name) = lhs.split_whitespace().last()
1636 {
1637 names.push(name.to_string());
1638 }
1639 }
1640 }
1641 }
1642 }
1643 names
1644}
1645
1646#[cfg(test)]
1647mod tests {
1648 use super::*;
1649
1650 #[test]
1651 fn language_aliases_resolve_in_registry() {
1652 let registry = LanguageRegistry::bootstrap();
1653 let aliases = [
1654 "python",
1655 "py",
1656 "python3",
1657 "rust",
1658 "rs",
1659 "go",
1660 "golang",
1661 "csharp",
1662 "cs",
1663 "c#",
1664 "typescript",
1665 "ts",
1666 "javascript",
1667 "js",
1668 "node",
1669 "ruby",
1670 "rb",
1671 "lua",
1672 "bash",
1673 "sh",
1674 "zsh",
1675 "java",
1676 "php",
1677 "kotlin",
1678 "kt",
1679 "c",
1680 "cpp",
1681 "c++",
1682 "swift",
1683 "swiftlang",
1684 "perl",
1685 "pl",
1686 "julia",
1687 "jl",
1688 ];
1689
1690 for alias in aliases {
1691 let spec = LanguageSpec::new(alias);
1692 assert!(
1693 registry.resolve(&spec).is_some(),
1694 "alias {alias} should resolve to a registered language"
1695 );
1696 }
1697 }
1698
1699 #[test]
1700 fn python_multiline_def_requires_blank_line_to_execute() {
1701 let mut p = PendingInput::new();
1702 p.push_line("def fib(n):");
1703 assert!(p.needs_more_input("python"));
1704 p.push_line(" return n");
1705 assert!(p.needs_more_input("python"));
1706 p.push_line(""); assert!(!p.needs_more_input("python"));
1708 }
1709
1710 #[test]
1711 fn python_dict_literal_colon_does_not_trigger_block() {
1712 let mut p = PendingInput::new();
1713 p.push_line("x = {'key': 'value'}");
1714 assert!(
1715 !p.needs_more_input("python"),
1716 "dict literal should not trigger multi-line"
1717 );
1718 }
1719
1720 #[test]
1721 fn python_class_block_needs_body() {
1722 let mut p = PendingInput::new();
1723 p.push_line("class Foo:");
1724 assert!(p.needs_more_input("python"));
1725 p.push_line(" pass");
1726 assert!(p.needs_more_input("python")); p.push_line(""); assert!(!p.needs_more_input("python"));
1729 }
1730
1731 #[test]
1732 fn python_if_block_with_dedented_body_is_complete() {
1733 let mut p = PendingInput::new();
1734 p.push_line("if True:");
1735 assert!(p.needs_more_input("python"));
1736 p.push_line(" print('yes')");
1737 assert!(p.needs_more_input("python"));
1738 p.push_line(""); assert!(!p.needs_more_input("python"));
1740 }
1741
1742 #[test]
1743 fn python_auto_indents_first_line_after_colon_header() {
1744 let mut p = PendingInput::new();
1745 p.push_line("def cool():");
1746 p.push_line_auto("python", r#"print("ok")"#);
1747 let code = p.take();
1748 assert!(
1749 code.contains(" print(\"ok\")\n"),
1750 "expected auto-indented print line, got:\n{code}"
1751 );
1752 }
1753
1754 #[test]
1755 fn generic_multiline_tracks_unclosed_delimiters() {
1756 let mut p = PendingInput::new();
1757 p.push_line("func(");
1758 assert!(p.needs_more_input("csharp"));
1759 p.push_line(")");
1760 assert!(!p.needs_more_input("csharp"));
1761 }
1762
1763 #[test]
1764 fn generic_multiline_tracks_trailing_equals() {
1765 let mut p = PendingInput::new();
1766 p.push_line("let x =");
1767 assert!(p.needs_more_input("rust"));
1768 p.push_line("10;");
1769 assert!(!p.needs_more_input("rust"));
1770 }
1771
1772 #[test]
1773 fn generic_multiline_tracks_trailing_dot() {
1774 let mut p = PendingInput::new();
1775 p.push_line("foo.");
1776 assert!(p.needs_more_input("csharp"));
1777 p.push_line("Bar()");
1778 assert!(!p.needs_more_input("csharp"));
1779 }
1780}