Skip to main content

run/
repl.rs

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,
16    build_install_command, 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", ":exit", ":quit", ":languages", ":lang ", ":detect ", ":reset",
47    ":load ", ":run ", ":save ", ":history", ":install ", ":bench ", ":type",
48];
49
50fn language_keywords(lang: &str) -> &'static [&'static str] {
51    match lang {
52        "python" | "py" | "python3" | "py3" => &[
53            "False", "None", "True", "and", "as", "assert", "async", "await", "break",
54            "class", "continue", "def", "del", "elif", "else", "except", "finally",
55            "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal",
56            "not", "or", "pass", "raise", "return", "try", "while", "with", "yield",
57            "print", "len", "range", "enumerate", "zip", "map", "filter", "sorted",
58            "list", "dict", "set", "tuple", "str", "int", "float", "bool", "type",
59            "isinstance", "hasattr", "getattr", "setattr", "open", "input",
60        ],
61        "javascript" | "js" | "node" => &[
62            "async", "await", "break", "case", "catch", "class", "const", "continue",
63            "debugger", "default", "delete", "do", "else", "export", "extends", "false",
64            "finally", "for", "function", "if", "import", "in", "instanceof", "let",
65            "new", "null", "of", "return", "static", "super", "switch", "this", "throw",
66            "true", "try", "typeof", "undefined", "var", "void", "while", "with", "yield",
67            "console", "require", "module", "process", "Promise", "Array", "Object",
68            "String", "Number", "Boolean", "Math", "JSON", "Date", "RegExp", "Map", "Set",
69        ],
70        "typescript" | "ts" => &[
71            "abstract", "any", "as", "async", "await", "boolean", "break", "case", "catch",
72            "class", "const", "continue", "debugger", "declare", "default", "delete", "do",
73            "else", "enum", "export", "extends", "false", "finally", "for", "from",
74            "function", "get", "if", "implements", "import", "in", "infer", "instanceof",
75            "interface", "is", "keyof", "let", "module", "namespace", "never", "new",
76            "null", "number", "object", "of", "private", "protected", "public", "readonly",
77            "return", "set", "static", "string", "super", "switch", "symbol", "this",
78            "throw", "true", "try", "type", "typeof", "undefined", "unique", "unknown",
79            "var", "void", "while", "with", "yield",
80        ],
81        "rust" | "rs" => &[
82            "as", "async", "await", "break", "const", "continue", "crate", "dyn",
83            "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in",
84            "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
85            "self", "Self", "static", "struct", "super", "trait", "true", "type",
86            "unsafe", "use", "where", "while", "println!", "eprintln!", "format!",
87            "vec!", "String", "Vec", "Option", "Result", "Some", "None", "Ok", "Err",
88        ],
89        "go" | "golang" => &[
90            "break", "case", "chan", "const", "continue", "default", "defer", "else",
91            "fallthrough", "for", "func", "go", "goto", "if", "import", "interface",
92            "map", "package", "range", "return", "select", "struct", "switch", "type",
93            "var", "fmt", "Println", "Printf", "Sprintf", "errors", "strings", "strconv",
94        ],
95        "ruby" | "rb" => &[
96            "alias", "and", "begin", "break", "case", "class", "def", "defined?",
97            "do", "else", "elsif", "end", "ensure", "false", "for", "if", "in",
98            "module", "next", "nil", "not", "or", "redo", "rescue", "retry",
99            "return", "self", "super", "then", "true", "undef", "unless", "until",
100            "when", "while", "yield", "puts", "print", "require", "require_relative",
101        ],
102        "java" => &[
103            "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char",
104            "class", "const", "continue", "default", "do", "double", "else", "enum",
105            "extends", "final", "finally", "float", "for", "goto", "if", "implements",
106            "import", "instanceof", "int", "interface", "long", "native", "new",
107            "package", "private", "protected", "public", "return", "short", "static",
108            "strictfp", "super", "switch", "synchronized", "this", "throw", "throws",
109            "transient", "try", "void", "volatile", "while", "System", "String",
110        ],
111        _ => &[],
112    }
113}
114
115fn complete_file_path(partial: &str) -> Vec<Pair> {
116    let (dir_part, file_prefix) = if let Some(sep_pos) = partial.rfind('/') {
117        (&partial[..=sep_pos], &partial[sep_pos + 1..])
118    } else {
119        ("", partial)
120    };
121
122    let search_dir = if dir_part.is_empty() { "." } else { dir_part };
123
124    let mut results = Vec::new();
125    if let Ok(entries) = std::fs::read_dir(search_dir) {
126        for entry in entries.flatten() {
127            let name = entry.file_name().to_string_lossy().to_string();
128            if name.starts_with('.') {
129                continue; // skip dotfiles
130            }
131            if name.starts_with(file_prefix) {
132                let full = format!("{dir_part}{name}");
133                let display = if entry.path().is_dir() {
134                    format!("{name}/")
135                } else {
136                    name.clone()
137                };
138                results.push(Pair {
139                    display,
140                    replacement: full,
141                });
142            }
143        }
144    }
145    results
146}
147
148impl Completer for ReplHelper {
149    type Candidate = Pair;
150
151    fn complete(
152        &self,
153        line: &str,
154        pos: usize,
155        _ctx: &rustyline::Context<'_>,
156    ) -> rustyline::Result<(usize, Vec<Pair>)> {
157        let line_up_to = &line[..pos];
158
159        // Meta command completion
160        if line_up_to.starts_with(':') {
161            // File path completion for :load and :run
162            if let Some(rest) = line_up_to
163                .strip_prefix(":load ")
164                .or_else(|| line_up_to.strip_prefix(":run "))
165                .or_else(|| line_up_to.strip_prefix(":save "))
166            {
167                let start = pos - rest.len();
168                return Ok((start, complete_file_path(rest)));
169            }
170
171            let candidates: Vec<Pair> = META_COMMANDS
172                .iter()
173                .filter(|cmd| cmd.starts_with(line_up_to))
174                .map(|cmd| Pair {
175                    display: cmd.to_string(),
176                    replacement: cmd.to_string(),
177                })
178                .collect();
179            return Ok((0, candidates));
180        }
181
182        // Find the word being typed
183        let word_start = line_up_to
184            .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '!')
185            .map(|i| i + 1)
186            .unwrap_or(0);
187        let prefix = &line_up_to[word_start..];
188
189        if prefix.is_empty() {
190            return Ok((pos, Vec::new()));
191        }
192
193        let mut candidates: Vec<Pair> = Vec::new();
194
195        // Language keywords
196        for kw in language_keywords(&self.language_id) {
197            if kw.starts_with(prefix) {
198                candidates.push(Pair {
199                    display: kw.to_string(),
200                    replacement: kw.to_string(),
201                });
202            }
203        }
204
205        // Session variables
206        for var in &self.session_vars {
207            if var.starts_with(prefix) && !candidates.iter().any(|c| c.replacement == *var) {
208                candidates.push(Pair {
209                    display: var.clone(),
210                    replacement: var.clone(),
211                });
212            }
213        }
214
215        Ok((word_start, candidates))
216    }
217}
218
219impl Hinter for ReplHelper {
220    type Hint = String;
221}
222
223impl Validator for ReplHelper {}
224
225impl Highlighter for ReplHelper {
226    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
227        if line.trim_start().starts_with(':') {
228            return Cow::Borrowed(line);
229        }
230
231        let highlighted = highlight::highlight_repl_input(line, &self.language_id);
232        Cow::Owned(highlighted)
233    }
234
235    fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
236        true
237    }
238}
239
240impl Helper for ReplHelper {}
241
242pub fn run_repl(
243    initial_language: LanguageSpec,
244    registry: LanguageRegistry,
245    detect_enabled: bool,
246) -> Result<i32> {
247    let helper = ReplHelper::new(initial_language.canonical_id().to_string());
248    let mut editor = Editor::<ReplHelper, DefaultHistory>::new()?;
249    editor.set_helper(Some(helper));
250
251    if let Some(path) = history_path() {
252        let _ = editor.load_history(&path);
253    }
254
255    let lang_count = registry.known_languages().len();
256    let mut state = ReplState::new(initial_language, registry, detect_enabled)?;
257
258    println!(
259        "\x1b[1mrun\x1b[0m \x1b[2mv{} — {}+ languages. Type :help for commands.\x1b[0m",
260        env!("CARGO_PKG_VERSION"),
261        lang_count
262    );
263    let mut pending: Option<PendingInput> = None;
264
265    loop {
266        let prompt = match &pending {
267            Some(p) => p.prompt(),
268            None => state.prompt(),
269        };
270
271        if let Some(helper) = editor.helper_mut() {
272            helper.update_language(state.current_language().canonical_id().to_string());
273        }
274
275        match editor.readline(&prompt) {
276            Ok(line) => {
277                let raw = line.trim_end_matches(['\r', '\n']);
278
279                if let Some(p) = pending.as_mut() {
280                    if raw.trim() == ":cancel" {
281                        pending = None;
282                        continue;
283                    }
284
285                    p.push_line_auto(state.current_language().canonical_id(), raw);
286                    if p.needs_more_input(state.current_language().canonical_id()) {
287                        continue;
288                    }
289
290                    let code = p.take();
291                    pending = None;
292                    let trimmed = code.trim_end();
293                    if !trimmed.is_empty() {
294                        let _ = editor.add_history_entry(trimmed);
295                        state.history_entries.push(trimmed.to_string());
296                        state.execute_snippet(trimmed)?;
297                        if let Some(helper) = editor.helper_mut() {
298                            helper.update_session_vars(state.session_var_names());
299                        }
300                    }
301                    continue;
302                }
303
304                if raw.trim().is_empty() {
305                    continue;
306                }
307
308                if raw.trim_start().starts_with(':') {
309                    let trimmed = raw.trim();
310                    let _ = editor.add_history_entry(trimmed);
311                    if state.handle_meta(trimmed)? {
312                        break;
313                    }
314                    continue;
315                }
316
317                let mut p = PendingInput::new();
318                p.push_line(raw);
319                if p.needs_more_input(state.current_language().canonical_id()) {
320                    pending = Some(p);
321                    continue;
322                }
323
324                let trimmed = raw.trim_end();
325                let _ = editor.add_history_entry(trimmed);
326                state.history_entries.push(trimmed.to_string());
327                state.execute_snippet(trimmed)?;
328                if let Some(helper) = editor.helper_mut() {
329                    helper.update_session_vars(state.session_var_names());
330                }
331            }
332            Err(ReadlineError::Interrupted) => {
333                println!("^C");
334                pending = None;
335                continue;
336            }
337            Err(ReadlineError::Eof) => {
338                println!("bye");
339                break;
340            }
341            Err(err) => {
342                bail!("readline error: {err}");
343            }
344        }
345    }
346
347    if let Some(path) = history_path() {
348        let _ = editor.save_history(&path);
349    }
350
351    state.shutdown();
352    Ok(0)
353}
354
355struct ReplState {
356    registry: LanguageRegistry,
357    sessions: HashMap<String, Box<dyn LanguageSession>>, // keyed by canonical id
358    current_language: LanguageSpec,
359    detect_enabled: bool,
360    defined_names: HashSet<String>,
361    history_entries: Vec<String>,
362}
363
364struct PendingInput {
365    buf: String,
366}
367
368impl PendingInput {
369    fn new() -> Self {
370        Self { buf: String::new() }
371    }
372
373    fn prompt(&self) -> String {
374        "... ".to_string()
375    }
376
377    fn push_line(&mut self, line: &str) {
378        self.buf.push_str(line);
379        self.buf.push('\n');
380    }
381
382    fn push_line_auto(&mut self, language_id: &str, line: &str) {
383        match language_id {
384            "python" | "py" | "python3" | "py3" => {
385                let adjusted = python_auto_indent(line, &self.buf);
386                self.push_line(&adjusted);
387            }
388            _ => self.push_line(line),
389        }
390    }
391
392    fn take(&mut self) -> String {
393        std::mem::take(&mut self.buf)
394    }
395
396    fn needs_more_input(&self, language_id: &str) -> bool {
397        needs_more_input(language_id, &self.buf)
398    }
399}
400
401fn needs_more_input(language_id: &str, code: &str) -> bool {
402    match language_id {
403        "python" | "py" | "python3" | "py3" => needs_more_input_python(code),
404
405        _ => has_unclosed_delimiters(code) || generic_line_looks_incomplete(code),
406    }
407}
408
409fn generic_line_looks_incomplete(code: &str) -> bool {
410    let mut last: Option<&str> = None;
411    for line in code.lines().rev() {
412        let trimmed = line.trim_end();
413        if trimmed.trim().is_empty() {
414            continue;
415        }
416        last = Some(trimmed);
417        break;
418    }
419    let Some(line) = last else { return false };
420    let line = line.trim();
421    if line.is_empty() {
422        return false;
423    }
424
425    if line.ends_with('\\') {
426        return true;
427    }
428
429    const TAILS: [&str; 24] = [
430        "=", "+", "-", "*", "/", "%", "&", "|", "^", "!", "<", ">", "&&", "||", "??", "?:", "?",
431        ":", ".", ",", "=>", "->", "::", "..",
432    ];
433    if TAILS.iter().any(|tok| line.ends_with(tok)) {
434        return true;
435    }
436
437    const PREFIXES: [&str; 9] = [
438        "return", "throw", "yield", "await", "import", "from", "export", "case", "else",
439    ];
440    let lowered = line.to_ascii_lowercase();
441    if PREFIXES
442        .iter()
443        .any(|kw| lowered == *kw || lowered.ends_with(&format!(" {kw}")))
444    {
445        return true;
446    }
447
448    false
449}
450
451fn needs_more_input_python(code: &str) -> bool {
452    if has_unclosed_delimiters(code) {
453        return true;
454    }
455
456    let mut last_nonempty: Option<&str> = None;
457    let mut saw_block_header = false;
458    let mut has_body_after_header = false;
459
460    for line in code.lines() {
461        let trimmed = line.trim_end();
462        if trimmed.trim().is_empty() {
463            continue;
464        }
465        last_nonempty = Some(trimmed);
466        if is_python_block_header(trimmed.trim()) {
467            saw_block_header = true;
468            has_body_after_header = false;
469        } else if saw_block_header {
470            has_body_after_header = true;
471        }
472    }
473
474    if !saw_block_header {
475        return false;
476    }
477
478    // A blank line terminates a block
479    if code.ends_with("\n\n") {
480        return false;
481    }
482
483    // If we have a header but no body yet, we need more input
484    if !has_body_after_header {
485        return true;
486    }
487
488    // If the last line is still indented, we're still inside the block
489    if let Some(last) = last_nonempty {
490        if last.starts_with(' ') || last.starts_with('\t') {
491            return true;
492        }
493    }
494
495    false
496}
497
498/// Check if a trimmed Python line is a block header (def, class, if, for, etc.)
499/// rather than a line that just happens to end with `:` (dict literal, slice, etc.)
500fn is_python_block_header(line: &str) -> bool {
501    if !line.ends_with(':') {
502        return false;
503    }
504    let lowered = line.to_ascii_lowercase();
505    const BLOCK_KEYWORDS: &[&str] = &[
506        "def ", "class ", "if ", "elif ", "else:", "for ", "while ", "try:", "except",
507        "finally:", "with ", "async def ", "async for ", "async with ",
508    ];
509    BLOCK_KEYWORDS.iter().any(|kw| lowered.starts_with(kw))
510}
511
512fn python_auto_indent(line: &str, existing: &str) -> String {
513    let trimmed = line.trim_end_matches(['\r', '\n']);
514    let raw = trimmed;
515    if raw.trim().is_empty() {
516        return raw.to_string();
517    }
518
519    if raw.starts_with(' ') || raw.starts_with('\t') {
520        return raw.to_string();
521    }
522
523    let mut last_nonempty: Option<&str> = None;
524    for l in existing.lines().rev() {
525        if l.trim().is_empty() {
526            continue;
527        }
528        last_nonempty = Some(l);
529        break;
530    }
531
532    let Some(prev) = last_nonempty else {
533        return raw.to_string();
534    };
535    let prev_trimmed = prev.trim_end();
536
537    if !prev_trimmed.ends_with(':') {
538        return raw.to_string();
539    }
540
541    let lowered = raw.trim().to_ascii_lowercase();
542    if lowered.starts_with("else:")
543        || lowered.starts_with("elif ")
544        || lowered.starts_with("except")
545        || lowered.starts_with("finally:")
546    {
547        return raw.to_string();
548    }
549
550    let base_indent = prev
551        .chars()
552        .take_while(|c| *c == ' ' || *c == '\t')
553        .collect::<String>();
554
555    format!("{base_indent}    {raw}")
556}
557
558fn has_unclosed_delimiters(code: &str) -> bool {
559    let mut paren = 0i32;
560    let mut bracket = 0i32;
561    let mut brace = 0i32;
562
563    let mut in_single = false;
564    let mut in_double = false;
565    let mut in_backtick = false;
566    let mut in_block_comment = false;
567    let mut escape = false;
568
569    let chars: Vec<char> = code.chars().collect();
570    let len = chars.len();
571    let mut i = 0;
572
573    while i < len {
574        let ch = chars[i];
575
576        if escape {
577            escape = false;
578            i += 1;
579            continue;
580        }
581
582        // Inside block comment /* ... */
583        if in_block_comment {
584            if ch == '*' && i + 1 < len && chars[i + 1] == '/' {
585                in_block_comment = false;
586                i += 2;
587                continue;
588            }
589            i += 1;
590            continue;
591        }
592
593        if in_single {
594            if ch == '\\' {
595                escape = true;
596            } else if ch == '\'' {
597                in_single = false;
598            }
599            i += 1;
600            continue;
601        }
602        if in_double {
603            if ch == '\\' {
604                escape = true;
605            } else if ch == '"' {
606                in_double = false;
607            }
608            i += 1;
609            continue;
610        }
611        if in_backtick {
612            if ch == '\\' {
613                escape = true;
614            } else if ch == '`' {
615                in_backtick = false;
616            }
617            i += 1;
618            continue;
619        }
620
621        // Check for line comments (// and #)
622        if ch == '/' && i + 1 < len && chars[i + 1] == '/' {
623            // Skip rest of line
624            while i < len && chars[i] != '\n' {
625                i += 1;
626            }
627            continue;
628        }
629        if ch == '#' {
630            // Python/Ruby/etc. line comment - skip rest of line
631            while i < len && chars[i] != '\n' {
632                i += 1;
633            }
634            continue;
635        }
636        // Check for block comments /* ... */
637        if ch == '/' && i + 1 < len && chars[i + 1] == '*' {
638            in_block_comment = true;
639            i += 2;
640            continue;
641        }
642
643        match ch {
644            '\'' => in_single = true,
645            '"' => in_double = true,
646            '`' => in_backtick = true,
647            '(' => paren += 1,
648            ')' => paren -= 1,
649            '[' => bracket += 1,
650            ']' => bracket -= 1,
651            '{' => brace += 1,
652            '}' => brace -= 1,
653            _ => {}
654        }
655
656        i += 1;
657    }
658
659    paren > 0 || bracket > 0 || brace > 0 || in_block_comment
660}
661
662impl ReplState {
663    fn new(
664        initial_language: LanguageSpec,
665        registry: LanguageRegistry,
666        detect_enabled: bool,
667    ) -> Result<Self> {
668        let mut state = Self {
669            registry,
670            sessions: HashMap::new(),
671            current_language: initial_language,
672            detect_enabled,
673            defined_names: HashSet::new(),
674            history_entries: Vec::new(),
675        };
676        state.ensure_current_language()?;
677        Ok(state)
678    }
679
680    fn current_language(&self) -> &LanguageSpec {
681        &self.current_language
682    }
683
684    fn prompt(&self) -> String {
685        format!("{}>>> ", self.current_language.canonical_id())
686    }
687
688    fn ensure_current_language(&mut self) -> Result<()> {
689        if self.registry.resolve(&self.current_language).is_none() {
690            bail!(
691                "language '{}' is not available",
692                self.current_language.canonical_id()
693            );
694        }
695        Ok(())
696    }
697
698    fn handle_meta(&mut self, line: &str) -> Result<bool> {
699        let command = line.trim_start_matches(':').trim();
700        if command.is_empty() {
701            return Ok(false);
702        }
703
704        let mut parts = command.split_whitespace();
705        let Some(head) = parts.next() else {
706            return Ok(false);
707        };
708        match head {
709            "exit" | "quit" => return Ok(true),
710            "help" => {
711                self.print_help();
712                return Ok(false);
713            }
714            "languages" => {
715                self.print_languages();
716                return Ok(false);
717            }
718            "detect" => {
719                if let Some(arg) = parts.next() {
720                    match arg {
721                        "on" | "true" | "1" => {
722                            self.detect_enabled = true;
723                            println!("auto-detect enabled");
724                        }
725                        "off" | "false" | "0" => {
726                            self.detect_enabled = false;
727                            println!("auto-detect disabled");
728                        }
729                        "toggle" => {
730                            self.detect_enabled = !self.detect_enabled;
731                            println!(
732                                "auto-detect {}",
733                                if self.detect_enabled {
734                                    "enabled"
735                                } else {
736                                    "disabled"
737                                }
738                            );
739                        }
740                        _ => println!("usage: :detect <on|off|toggle>"),
741                    }
742                } else {
743                    println!(
744                        "auto-detect is {}",
745                        if self.detect_enabled {
746                            "enabled"
747                        } else {
748                            "disabled"
749                        }
750                    );
751                }
752                return Ok(false);
753            }
754            "lang" => {
755                if let Some(lang) = parts.next() {
756                    self.switch_language(LanguageSpec::new(lang.to_string()))?;
757                } else {
758                    println!("usage: :lang <language>");
759                }
760                return Ok(false);
761            }
762            "reset" => {
763                self.reset_current_session();
764                println!(
765                    "session for '{}' reset",
766                    self.current_language.canonical_id()
767                );
768                return Ok(false);
769            }
770            "load" | "run" => {
771                if let Some(token) = parts.next() {
772                    let path = PathBuf::from(token);
773                    self.execute_payload(ExecutionPayload::File { path })?;
774                } else {
775                    println!("usage: :load <path>");
776                }
777                return Ok(false);
778            }
779            "save" => {
780                if let Some(token) = parts.next() {
781                    let path = Path::new(token);
782                    match self.save_session(path) {
783                        Ok(count) => println!("\x1b[2m[saved {count} entries to {}]\x1b[0m", path.display()),
784                        Err(e) => println!("error saving session: {e}"),
785                    }
786                } else {
787                    println!("usage: :save <path>");
788                }
789                return Ok(false);
790            }
791            "history" => {
792                let limit: usize = parts
793                    .next()
794                    .and_then(|s| s.parse().ok())
795                    .unwrap_or(25);
796                self.show_history(limit);
797                return Ok(false);
798            }
799            "install" => {
800                if let Some(pkg) = parts.next() {
801                    self.install_package(pkg);
802                } else {
803                    println!("usage: :install <package>");
804                }
805                return Ok(false);
806            }
807            "bench" => {
808                let n: u32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(10);
809                let code = parts.collect::<Vec<_>>().join(" ");
810                if code.is_empty() {
811                    println!("usage: :bench [N] <code>");
812                    println!("  Runs <code> N times (default: 10) and reports timing stats.");
813                } else {
814                    self.bench_code(&code, n)?;
815                }
816                return Ok(false);
817            }
818            "type" | "which" => {
819                let lang = &self.current_language;
820                println!(
821                    "\x1b[1m{}\x1b[0m \x1b[2m({})\x1b[0m",
822                    lang.canonical_id(),
823                    if self.sessions.contains_key(lang.canonical_id()) {
824                        "session active"
825                    } else {
826                        "no session"
827                    }
828                );
829                return Ok(false);
830            }
831            alias => {
832                let spec = LanguageSpec::new(alias);
833                if self.registry.resolve(&spec).is_some() {
834                    self.switch_language(spec)?;
835                    return Ok(false);
836                }
837                println!("unknown command: :{alias}. Type :help for help.");
838            }
839        }
840
841        Ok(false)
842    }
843
844    fn switch_language(&mut self, spec: LanguageSpec) -> Result<()> {
845        if self.current_language.canonical_id() == spec.canonical_id() {
846            println!("already using {}", spec.canonical_id());
847            return Ok(());
848        }
849        if self.registry.resolve(&spec).is_none() {
850            let available = self.registry.known_languages().join(", ");
851            bail!(
852                "language '{}' not supported. Available: {available}",
853                spec.canonical_id()
854            );
855        }
856        self.current_language = spec;
857        println!("switched to {}", self.current_language.canonical_id());
858        Ok(())
859    }
860
861    fn reset_current_session(&mut self) {
862        let key = self.current_language.canonical_id().to_string();
863        if let Some(mut session) = self.sessions.remove(&key) {
864            let _ = session.shutdown();
865        }
866    }
867
868    fn execute_snippet(&mut self, code: &str) -> Result<()> {
869        if self.detect_enabled {
870            if let Some(detected) = crate::detect::detect_language_from_snippet(code) {
871                if detected != self.current_language.canonical_id() {
872                    let spec = LanguageSpec::new(detected.to_string());
873                    if self.registry.resolve(&spec).is_some() {
874                        println!(
875                            "[auto-detect] switching {} -> {}",
876                            self.current_language.canonical_id(),
877                            spec.canonical_id()
878                        );
879                        self.current_language = spec;
880                    }
881                }
882            }
883        }
884
885        // Track defined variable names for tab completion
886        self.defined_names.extend(extract_defined_names(
887            code,
888            self.current_language.canonical_id(),
889        ));
890
891        let payload = ExecutionPayload::Inline {
892            code: code.to_string(),
893        };
894        self.execute_payload(payload)
895    }
896
897    fn session_var_names(&self) -> Vec<String> {
898        self.defined_names.iter().cloned().collect()
899    }
900
901    fn execute_payload(&mut self, payload: ExecutionPayload) -> Result<()> {
902        let language = self.current_language.clone();
903        let outcome = match payload {
904            ExecutionPayload::Inline { code } => {
905                if self.engine_supports_sessions(&language)? {
906                    self.eval_in_session(&language, &code)?
907                } else {
908                    let engine = self
909                        .registry
910                        .resolve(&language)
911                        .context("language engine not found")?;
912                    engine.execute(&ExecutionPayload::Inline { code })?
913                }
914            }
915            ExecutionPayload::File { ref path } => {
916                // Read the file and feed it through the session so variables persist
917                if self.engine_supports_sessions(&language)? {
918                    let code = std::fs::read_to_string(path).with_context(|| {
919                        format!("failed to read file: {}", path.display())
920                    })?;
921                    println!("\x1b[2m[loaded {}]\x1b[0m", path.display());
922                    self.eval_in_session(&language, &code)?
923                } else {
924                    let engine = self
925                        .registry
926                        .resolve(&language)
927                        .context("language engine not found")?;
928                    engine.execute(&payload)?
929                }
930            }
931            ExecutionPayload::Stdin { code } => {
932                if self.engine_supports_sessions(&language)? {
933                    self.eval_in_session(&language, &code)?
934                } else {
935                    let engine = self
936                        .registry
937                        .resolve(&language)
938                        .context("language engine not found")?;
939                    engine.execute(&ExecutionPayload::Stdin { code })?
940                }
941            }
942        };
943        render_outcome(&outcome);
944        Ok(())
945    }
946
947    fn engine_supports_sessions(&self, language: &LanguageSpec) -> Result<bool> {
948        Ok(self
949            .registry
950            .resolve(language)
951            .context("language engine not found")?
952            .supports_sessions())
953    }
954
955    fn eval_in_session(&mut self, language: &LanguageSpec, code: &str) -> Result<ExecutionOutcome> {
956        use std::collections::hash_map::Entry;
957        let key = language.canonical_id().to_string();
958        match self.sessions.entry(key) {
959            Entry::Occupied(mut entry) => entry.get_mut().eval(code),
960            Entry::Vacant(entry) => {
961                let engine = self
962                    .registry
963                    .resolve(language)
964                    .context("language engine not found")?;
965                let mut session = engine.start_session().with_context(|| {
966                    format!("failed to start {} session", language.canonical_id())
967                })?;
968                let outcome = session.eval(code)?;
969                entry.insert(session);
970                Ok(outcome)
971            }
972        }
973    }
974
975    fn print_languages(&self) {
976        let mut languages = self.registry.known_languages();
977        languages.sort();
978        println!("available languages: {}", languages.join(", "));
979    }
980
981    fn install_package(&self, package: &str) {
982        let lang_id = self.current_language.canonical_id();
983        if package_install_command(lang_id).is_none() {
984            println!("No package manager available for '{lang_id}'.");
985            return;
986        }
987
988        let Some(mut cmd) = build_install_command(lang_id, package) else {
989            println!("Failed to build install command for '{lang_id}'.");
990            return;
991        };
992
993        println!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
994
995        match cmd
996            .stdin(std::process::Stdio::inherit())
997            .stdout(std::process::Stdio::inherit())
998            .stderr(std::process::Stdio::inherit())
999            .status()
1000        {
1001            Ok(status) if status.success() => {
1002                println!("\x1b[32m[run]\x1b[0m Successfully installed '{package}'");
1003            }
1004            Ok(_) => {
1005                println!("\x1b[31m[run]\x1b[0m Failed to install '{package}'");
1006            }
1007            Err(e) => {
1008                println!("\x1b[31m[run]\x1b[0m Error running package manager: {e}");
1009            }
1010        }
1011    }
1012
1013    fn bench_code(&mut self, code: &str, iterations: u32) -> Result<()> {
1014        let language = self.current_language.clone();
1015
1016        // Warmup
1017        let warmup = self.eval_in_session(&language, code)?;
1018        if !warmup.success() {
1019            println!("\x1b[31mError:\x1b[0m Code failed during warmup");
1020            if !warmup.stderr.is_empty() {
1021                print!("{}", warmup.stderr);
1022            }
1023            return Ok(());
1024        }
1025        println!(
1026            "\x1b[2m  warmup: {}ms\x1b[0m",
1027            warmup.duration.as_millis()
1028        );
1029
1030        let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
1031        for i in 0..iterations {
1032            let outcome = self.eval_in_session(&language, code)?;
1033            let ms = outcome.duration.as_secs_f64() * 1000.0;
1034            times.push(ms);
1035            if i < 3 || i == iterations - 1 {
1036                println!("\x1b[2m  run {}: {:.2}ms\x1b[0m", i + 1, ms);
1037            }
1038        }
1039
1040        times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1041        let total: f64 = times.iter().sum();
1042        let avg = total / times.len() as f64;
1043        let min = times.first().copied().unwrap_or(0.0);
1044        let max = times.last().copied().unwrap_or(0.0);
1045        let median = if times.len() % 2 == 0 && times.len() >= 2 {
1046            (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
1047        } else {
1048            times[times.len() / 2]
1049        };
1050        let variance: f64 =
1051            times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
1052        let stddev = variance.sqrt();
1053
1054        println!();
1055        println!("\x1b[1mResults ({iterations} runs):\x1b[0m");
1056        println!("  min:    \x1b[32m{min:.2}ms\x1b[0m");
1057        println!("  max:    \x1b[33m{max:.2}ms\x1b[0m");
1058        println!("  avg:    \x1b[36m{avg:.2}ms\x1b[0m");
1059        println!("  median: \x1b[36m{median:.2}ms\x1b[0m");
1060        println!("  stddev: {stddev:.2}ms");
1061        Ok(())
1062    }
1063
1064    fn save_session(&self, path: &Path) -> Result<usize> {
1065        use std::io::Write;
1066        let mut file = std::fs::File::create(path)
1067            .with_context(|| format!("failed to create {}", path.display()))?;
1068        let count = self.history_entries.len();
1069        for entry in &self.history_entries {
1070            writeln!(file, "{entry}")?;
1071        }
1072        Ok(count)
1073    }
1074
1075    fn show_history(&self, limit: usize) {
1076        let entries = &self.history_entries;
1077        let start = entries.len().saturating_sub(limit);
1078        if entries.is_empty() {
1079            println!("\x1b[2m(no history)\x1b[0m");
1080            return;
1081        }
1082        for (i, entry) in entries[start..].iter().enumerate() {
1083            let num = start + i + 1;
1084            // Show multi-line entries with continuation indicator
1085            let first_line = entry.lines().next().unwrap_or(entry);
1086            let is_multiline = entry.contains('\n');
1087            if is_multiline {
1088                println!("\x1b[2m[{num:>4}]\x1b[0m {first_line} \x1b[2m(...)\x1b[0m");
1089            } else {
1090                println!("\x1b[2m[{num:>4}]\x1b[0m {entry}");
1091            }
1092        }
1093    }
1094
1095    fn print_help(&self) {
1096        println!("\x1b[1mCommands\x1b[0m");
1097        println!("  \x1b[36m:help\x1b[0m                 \x1b[2mShow this help\x1b[0m");
1098        println!("  \x1b[36m:lang\x1b[0m <id>            \x1b[2mSwitch language\x1b[0m");
1099        println!("  \x1b[36m:languages\x1b[0m            \x1b[2mList available languages\x1b[0m");
1100        println!("  \x1b[36m:detect\x1b[0m on|off        \x1b[2mToggle auto language detection\x1b[0m");
1101        println!("  \x1b[36m:reset\x1b[0m                \x1b[2mClear current session state\x1b[0m");
1102        println!("  \x1b[36m:load\x1b[0m <path>          \x1b[2mLoad and execute a file\x1b[0m");
1103        println!("  \x1b[36m:save\x1b[0m <path>          \x1b[2mSave session history to file\x1b[0m");
1104        println!("  \x1b[36m:history\x1b[0m [n]          \x1b[2mShow last n entries (default: 25)\x1b[0m");
1105        println!("  \x1b[36m:install\x1b[0m <pkg>        \x1b[2mInstall a package for current language\x1b[0m");
1106        println!("  \x1b[36m:bench\x1b[0m [N] <code>     \x1b[2mBenchmark code N times (default: 10)\x1b[0m");
1107        println!("  \x1b[36m:type\x1b[0m                 \x1b[2mShow current language and session status\x1b[0m");
1108        println!("  \x1b[36m:exit\x1b[0m                 \x1b[2mLeave the REPL\x1b[0m");
1109        println!("\x1b[2mLanguage shortcuts: :py, :js, :rs, :go, :cpp, :java, ...\x1b[0m");
1110    }
1111
1112    fn shutdown(&mut self) {
1113        for (_, mut session) in self.sessions.drain() {
1114            let _ = session.shutdown();
1115        }
1116    }
1117}
1118
1119fn render_outcome(outcome: &ExecutionOutcome) {
1120    if !outcome.stdout.is_empty() {
1121        print!("{}", ensure_trailing_newline(&outcome.stdout));
1122    }
1123    if !outcome.stderr.is_empty() {
1124        eprint!("\x1b[31m{}\x1b[0m", ensure_trailing_newline(&outcome.stderr));
1125    }
1126
1127    let millis = outcome.duration.as_millis();
1128    if let Some(code) = outcome.exit_code {
1129        if code != 0 {
1130            println!("\x1b[2m[exit {code}] {}\x1b[0m", format_duration(millis));
1131            return;
1132        }
1133    }
1134
1135    // Show execution timing
1136    if millis > 0 {
1137        println!("\x1b[2m{}\x1b[0m", format_duration(millis));
1138    }
1139}
1140
1141fn format_duration(millis: u128) -> String {
1142    if millis >= 60_000 {
1143        let mins = millis / 60_000;
1144        let secs = (millis % 60_000) / 1000;
1145        format!("{mins}m {secs}s")
1146    } else if millis >= 1000 {
1147        let secs = millis as f64 / 1000.0;
1148        format!("{secs:.2}s")
1149    } else {
1150        format!("{millis}ms")
1151    }
1152}
1153
1154fn ensure_trailing_newline(text: &str) -> String {
1155    if text.ends_with('\n') {
1156        text.to_string()
1157    } else {
1158        let mut owned = text.to_string();
1159        owned.push('\n');
1160        owned
1161    }
1162}
1163
1164fn history_path() -> Option<PathBuf> {
1165    if let Ok(home) = std::env::var("HOME") {
1166        return Some(Path::new(&home).join(HISTORY_FILE));
1167    }
1168    None
1169}
1170
1171/// Extract variable/function/class names defined in a code snippet for tab completion.
1172fn extract_defined_names(code: &str, language_id: &str) -> Vec<String> {
1173    let mut names = Vec::new();
1174    for line in code.lines() {
1175        let trimmed = line.trim();
1176        match language_id {
1177            "python" | "py" | "python3" | "py3" => {
1178                // x = ..., def foo(...), class Bar:, import x, from x import y
1179                if let Some(rest) = trimmed.strip_prefix("def ") {
1180                    if let Some(name) = rest.split('(').next() {
1181                        let n = name.trim();
1182                        if !n.is_empty() {
1183                            names.push(n.to_string());
1184                        }
1185                    }
1186                } else if let Some(rest) = trimmed.strip_prefix("class ") {
1187                    let name = rest.split(['(', ':']).next().unwrap_or("").trim();
1188                    if !name.is_empty() {
1189                        names.push(name.to_string());
1190                    }
1191                } else if let Some(rest) = trimmed.strip_prefix("import ") {
1192                    for part in rest.split(',') {
1193                        let name = if let Some(alias) = part.split(" as ").nth(1) {
1194                            alias.trim()
1195                        } else {
1196                            part.trim().split('.').last().unwrap_or("")
1197                        };
1198                        if !name.is_empty() {
1199                            names.push(name.to_string());
1200                        }
1201                    }
1202                } else if trimmed.starts_with("from ") && trimmed.contains("import ") {
1203                    if let Some(imports) = trimmed.split("import ").nth(1) {
1204                        for part in imports.split(',') {
1205                            let name = if let Some(alias) = part.split(" as ").nth(1) {
1206                                alias.trim()
1207                            } else {
1208                                part.trim()
1209                            };
1210                            if !name.is_empty() {
1211                                names.push(name.to_string());
1212                            }
1213                        }
1214                    }
1215                } else if let Some(eq_pos) = trimmed.find('=') {
1216                    let lhs = &trimmed[..eq_pos];
1217                    if !lhs.contains('(')
1218                        && !lhs.contains('[')
1219                        && !trimmed[eq_pos..].starts_with("==")
1220                    {
1221                        for part in lhs.split(',') {
1222                            let name = part.trim().split(':').next().unwrap_or("").trim();
1223                            if !name.is_empty()
1224                                && name.chars().all(|c| c.is_alphanumeric() || c == '_')
1225                            {
1226                                names.push(name.to_string());
1227                            }
1228                        }
1229                    }
1230                }
1231            }
1232            "javascript" | "js" | "node" | "typescript" | "ts" => {
1233                // let/const/var x = ..., function foo(...), class Bar
1234                for prefix in ["let ", "const ", "var "] {
1235                    if let Some(rest) = trimmed.strip_prefix(prefix) {
1236                        let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
1237                        if !name.is_empty() {
1238                            names.push(name.to_string());
1239                        }
1240                    }
1241                }
1242                if let Some(rest) = trimmed.strip_prefix("function ") {
1243                    let name = rest.split('(').next().unwrap_or("").trim();
1244                    if !name.is_empty() {
1245                        names.push(name.to_string());
1246                    }
1247                } else if let Some(rest) = trimmed.strip_prefix("class ") {
1248                    let name = rest.split(['{', ' ']).next().unwrap_or("").trim();
1249                    if !name.is_empty() {
1250                        names.push(name.to_string());
1251                    }
1252                }
1253            }
1254            "rust" | "rs" => {
1255                for prefix in ["let ", "let mut "] {
1256                    if let Some(rest) = trimmed.strip_prefix(prefix) {
1257                        let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
1258                        if !name.is_empty() {
1259                            names.push(name.to_string());
1260                        }
1261                    }
1262                }
1263                if let Some(rest) = trimmed.strip_prefix("fn ") {
1264                    let name = rest.split(['(', '<']).next().unwrap_or("").trim();
1265                    if !name.is_empty() {
1266                        names.push(name.to_string());
1267                    }
1268                } else if let Some(rest) = trimmed.strip_prefix("struct ") {
1269                    let name = rest.split(['{', '(', '<', ' ']).next().unwrap_or("").trim();
1270                    if !name.is_empty() {
1271                        names.push(name.to_string());
1272                    }
1273                }
1274            }
1275            _ => {
1276                // Generic: catch x = ... assignments
1277                if let Some(eq_pos) = trimmed.find('=') {
1278                    let lhs = trimmed[..eq_pos].trim();
1279                    if !lhs.is_empty()
1280                        && !trimmed[eq_pos..].starts_with("==")
1281                        && lhs.chars().all(|c| c.is_alphanumeric() || c == '_' || c == ' ')
1282                    {
1283                        if let Some(name) = lhs.split_whitespace().last() {
1284                            names.push(name.to_string());
1285                        }
1286                    }
1287                }
1288            }
1289        }
1290    }
1291    names
1292}
1293
1294#[cfg(test)]
1295mod tests {
1296    use super::*;
1297
1298    #[test]
1299    fn language_aliases_resolve_in_registry() {
1300        let registry = LanguageRegistry::bootstrap();
1301        let aliases = [
1302            "python",
1303            "py",
1304            "python3",
1305            "rust",
1306            "rs",
1307            "go",
1308            "golang",
1309            "csharp",
1310            "cs",
1311            "c#",
1312            "typescript",
1313            "ts",
1314            "javascript",
1315            "js",
1316            "node",
1317            "ruby",
1318            "rb",
1319            "lua",
1320            "bash",
1321            "sh",
1322            "zsh",
1323            "java",
1324            "php",
1325            "kotlin",
1326            "kt",
1327            "c",
1328            "cpp",
1329            "c++",
1330            "swift",
1331            "swiftlang",
1332            "perl",
1333            "pl",
1334            "julia",
1335            "jl",
1336        ];
1337
1338        for alias in aliases {
1339            let spec = LanguageSpec::new(alias);
1340            assert!(
1341                registry.resolve(&spec).is_some(),
1342                "alias {alias} should resolve to a registered language"
1343            );
1344        }
1345    }
1346
1347    #[test]
1348    fn python_multiline_def_requires_blank_line_to_execute() {
1349        let mut p = PendingInput::new();
1350        p.push_line("def fib(n):");
1351        assert!(p.needs_more_input("python"));
1352        p.push_line("    return n");
1353        assert!(p.needs_more_input("python"));
1354        p.push_line(""); // blank line ends block
1355        assert!(!p.needs_more_input("python"));
1356    }
1357
1358    #[test]
1359    fn python_dict_literal_colon_does_not_trigger_block() {
1360        let mut p = PendingInput::new();
1361        p.push_line("x = {'key': 'value'}");
1362        assert!(!p.needs_more_input("python"), "dict literal should not trigger multi-line");
1363    }
1364
1365    #[test]
1366    fn python_class_block_needs_body() {
1367        let mut p = PendingInput::new();
1368        p.push_line("class Foo:");
1369        assert!(p.needs_more_input("python"));
1370        p.push_line("    pass");
1371        assert!(p.needs_more_input("python")); // still indented
1372        p.push_line(""); // blank line ends
1373        assert!(!p.needs_more_input("python"));
1374    }
1375
1376    #[test]
1377    fn python_if_block_with_dedented_body_is_complete() {
1378        let mut p = PendingInput::new();
1379        p.push_line("if True:");
1380        assert!(p.needs_more_input("python"));
1381        p.push_line("    print('yes')");
1382        assert!(p.needs_more_input("python"));
1383        p.push_line(""); // blank line terminates
1384        assert!(!p.needs_more_input("python"));
1385    }
1386
1387    #[test]
1388    fn python_auto_indents_first_line_after_colon_header() {
1389        let mut p = PendingInput::new();
1390        p.push_line("def cool():");
1391        p.push_line_auto("python", r#"print("ok")"#);
1392        let code = p.take();
1393        assert!(
1394            code.contains("    print(\"ok\")\n"),
1395            "expected auto-indented print line, got:\n{code}"
1396        );
1397    }
1398
1399    #[test]
1400    fn generic_multiline_tracks_unclosed_delimiters() {
1401        let mut p = PendingInput::new();
1402        p.push_line("func(");
1403        assert!(p.needs_more_input("csharp"));
1404        p.push_line(")");
1405        assert!(!p.needs_more_input("csharp"));
1406    }
1407
1408    #[test]
1409    fn generic_multiline_tracks_trailing_equals() {
1410        let mut p = PendingInput::new();
1411        p.push_line("let x =");
1412        assert!(p.needs_more_input("rust"));
1413        p.push_line("10;");
1414        assert!(!p.needs_more_input("rust"));
1415    }
1416
1417    #[test]
1418    fn generic_multiline_tracks_trailing_dot() {
1419        let mut p = PendingInput::new();
1420        p.push_line("foo.");
1421        assert!(p.needs_more_input("csharp"));
1422        p.push_line("Bar()");
1423        assert!(!p.needs_more_input("csharp"));
1424    }
1425}