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