1use std::borrow::Cow;
2use std::collections::{BTreeMap, HashMap, HashSet};
3use std::io::Write;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Stdio};
6use std::time::Instant;
7
8use anyhow::{Context, Result, bail};
9use rustyline::completion::{Completer, Pair};
10use rustyline::error::ReadlineError;
11use rustyline::highlight::Highlighter;
12use rustyline::hint::Hinter;
13use rustyline::history::DefaultHistory;
14use rustyline::validate::Validator;
15use rustyline::{Config, Editor, Helper};
16
17use crate::engine::{
18 ExecutionOutcome, ExecutionPayload, LanguageRegistry, LanguageSession, build_install_command,
19};
20use crate::highlight;
21use crate::language::LanguageSpec;
22use crate::output;
23
24const BOOKMARKS_FILE: &str = ".run_bookmarks";
25const REPL_CONFIG_FILE: &str = ".run_repl_config";
26const MAX_DIR_STACK: usize = 20;
27
28#[derive(Clone, Copy, PartialEq, Eq)]
30enum XMode {
31 Plain,
33 Context,
35 Verbose,
37}
38
39struct ReplHelper {
40 language_id: String,
41 session_vars: Vec<String>,
42}
43
44impl ReplHelper {
45 fn new(language_id: String) -> Self {
46 Self {
47 language_id,
48 session_vars: Vec::new(),
49 }
50 }
51
52 fn update_language(&mut self, language_id: String) {
53 self.language_id = language_id;
54 }
55
56 fn update_session_vars(&mut self, vars: Vec<String>) {
57 self.session_vars = vars;
58 }
59}
60
61const META_COMMANDS: &[&str] = &[
62 ":! ",
63 ":!! ",
64 ":help",
65 ":help ",
66 ":? ",
67 ":debug",
68 ":debug ",
69 ":commands",
70 ":quickref",
71 ":exit",
72 ":quit",
73 ":languages",
74 ":lang ",
75 ":detect ",
76 ":reset",
77 ":cd ",
78 ":cd -b ",
79 ":dhist",
80 ":bookmark ",
81 ":bookmark -l",
82 ":bookmark -d ",
83 ":env",
84 ":last",
85 ":load ",
86 ":edit",
87 ":edit ",
88 ":run ",
89 ":logstart",
90 ":logstart ",
91 ":logstop",
92 ":logstate",
93 ":macro ",
94 ":macro run ",
95 ":time ",
96 ":who",
97 ":whos",
98 ":whos ",
99 ":xmode",
100 ":xmode ",
101 ":config",
102 ":config ",
103 ":paste",
104 ":end",
105 ":precision",
106 ":precision ",
107 ":save ",
108 ":history",
109 ":install ",
110 ":bench ",
111 ":type",
112];
113
114const CMD_HELP: &[(&str, &str)] = &[
116 ("help", "Show this help"),
117 (
118 "?",
119 "Show doc/source for name (e.g. :? print); Python session only",
120 ),
121 (
122 "debug",
123 "Run last snippet or :debug CODE under debugger (Python: pdb)",
124 ),
125 ("lang", "Switch language"),
126 ("languages", "List available languages"),
127 ("versions", "Show toolchain versions"),
128 ("detect", "Toggle auto language detection"),
129 ("reset", "Clear current session state"),
130 (
131 "cd",
132 "Change directory; :cd - = previous, :cd -b <name> = bookmark",
133 ),
134 ("dhist", "Directory history (default 10)"),
135 ("bookmark", "Save bookmark; -l list, -d <name> delete"),
136 ("env", "List env, get VAR, or set VAR=val"),
137 ("load", "Load and execute a file or http(s) URL"),
138 ("last", "Print last execution stdout"),
139 ("edit", "Open $EDITOR; on save, execute in current session"),
140 ("run", "Load file/URL or run macro by name"),
141 (
142 "logstart",
143 "Start logging input to file (default: run_log.txt)",
144 ),
145 ("logstop", "Stop logging"),
146 ("logstate", "Show whether logging and path"),
147 (
148 "macro",
149 "Save history range as macro; :macro run NAME to run",
150 ),
151 ("time", "Run code once and print elapsed time"),
152 ("who", "List names tracked in current session"),
153 ("whos", "Like :who with optional name filter"),
154 ("save", "Save session history to file"),
155 (
156 "history",
157 "Show history; -g PATTERN, -f FILE, 4-6 or 4- or -6",
158 ),
159 ("install", "Install a package for current language"),
160 ("bench", "Benchmark code N times (default: 10)"),
161 ("type", "Show current language and session status"),
162 ("!", "Run shell command (inherit stdout/stderr)"),
163 ("!!", "Run shell command and print captured output"),
164 ("exit", "Leave the REPL"),
165 ("quit", "Leave the REPL"),
166 (
167 "xmode",
168 "Exception display: plain (first line) | context (5 lines) | verbose (full)",
169 ),
170 (
171 "config",
172 "Get/set REPL config (detect, xmode); persists in ~/.run_repl_config",
173 ),
174 (
175 "paste",
176 "Paste mode: collect lines until :end or Ctrl-D, then execute (strip >>> / ...)",
177 ),
178 (
179 "end",
180 "End paste mode and execute buffer (only in paste mode)",
181 ),
182 (
183 "precision",
184 "Float display precision (0–32) for last result; :precision N to set, persists in config",
185 ),
186];
187
188fn language_keywords(lang: &str) -> &'static [&'static str] {
189 match lang {
190 "python" | "py" | "python3" | "py3" => &[
191 "False",
192 "None",
193 "True",
194 "and",
195 "as",
196 "assert",
197 "async",
198 "await",
199 "break",
200 "class",
201 "continue",
202 "def",
203 "del",
204 "elif",
205 "else",
206 "except",
207 "finally",
208 "for",
209 "from",
210 "global",
211 "if",
212 "import",
213 "in",
214 "is",
215 "lambda",
216 "nonlocal",
217 "not",
218 "or",
219 "pass",
220 "raise",
221 "return",
222 "try",
223 "while",
224 "with",
225 "yield",
226 "print",
227 "len",
228 "range",
229 "enumerate",
230 "zip",
231 "map",
232 "filter",
233 "sorted",
234 "list",
235 "dict",
236 "set",
237 "tuple",
238 "str",
239 "int",
240 "float",
241 "bool",
242 "type",
243 "isinstance",
244 "hasattr",
245 "getattr",
246 "setattr",
247 "open",
248 "input",
249 ],
250 "javascript" | "js" | "node" => &[
251 "async",
252 "await",
253 "break",
254 "case",
255 "catch",
256 "class",
257 "const",
258 "continue",
259 "debugger",
260 "default",
261 "delete",
262 "do",
263 "else",
264 "export",
265 "extends",
266 "false",
267 "finally",
268 "for",
269 "function",
270 "if",
271 "import",
272 "in",
273 "instanceof",
274 "let",
275 "new",
276 "null",
277 "of",
278 "return",
279 "static",
280 "super",
281 "switch",
282 "this",
283 "throw",
284 "true",
285 "try",
286 "typeof",
287 "undefined",
288 "var",
289 "void",
290 "while",
291 "with",
292 "yield",
293 "console",
294 "require",
295 "module",
296 "process",
297 "Promise",
298 "Array",
299 "Object",
300 "String",
301 "Number",
302 "Boolean",
303 "Math",
304 "JSON",
305 "Date",
306 "RegExp",
307 "Map",
308 "Set",
309 ],
310 "typescript" | "ts" => &[
311 "abstract",
312 "any",
313 "as",
314 "async",
315 "await",
316 "boolean",
317 "break",
318 "case",
319 "catch",
320 "class",
321 "const",
322 "continue",
323 "debugger",
324 "declare",
325 "default",
326 "delete",
327 "do",
328 "else",
329 "enum",
330 "export",
331 "extends",
332 "false",
333 "finally",
334 "for",
335 "from",
336 "function",
337 "get",
338 "if",
339 "implements",
340 "import",
341 "in",
342 "infer",
343 "instanceof",
344 "interface",
345 "is",
346 "keyof",
347 "let",
348 "module",
349 "namespace",
350 "never",
351 "new",
352 "null",
353 "number",
354 "object",
355 "of",
356 "private",
357 "protected",
358 "public",
359 "readonly",
360 "return",
361 "set",
362 "static",
363 "string",
364 "super",
365 "switch",
366 "symbol",
367 "this",
368 "throw",
369 "true",
370 "try",
371 "type",
372 "typeof",
373 "undefined",
374 "unique",
375 "unknown",
376 "var",
377 "void",
378 "while",
379 "with",
380 "yield",
381 ],
382 "rust" | "rs" => &[
383 "as",
384 "async",
385 "await",
386 "break",
387 "const",
388 "continue",
389 "crate",
390 "dyn",
391 "else",
392 "enum",
393 "extern",
394 "false",
395 "fn",
396 "for",
397 "if",
398 "impl",
399 "in",
400 "let",
401 "loop",
402 "match",
403 "mod",
404 "move",
405 "mut",
406 "pub",
407 "ref",
408 "return",
409 "self",
410 "Self",
411 "static",
412 "struct",
413 "super",
414 "trait",
415 "true",
416 "type",
417 "unsafe",
418 "use",
419 "where",
420 "while",
421 "println!",
422 "eprintln!",
423 "format!",
424 "vec!",
425 "String",
426 "Vec",
427 "Option",
428 "Result",
429 "Some",
430 "None",
431 "Ok",
432 "Err",
433 ],
434 "go" | "golang" => &[
435 "break",
436 "case",
437 "chan",
438 "const",
439 "continue",
440 "default",
441 "defer",
442 "else",
443 "fallthrough",
444 "for",
445 "func",
446 "go",
447 "goto",
448 "if",
449 "import",
450 "interface",
451 "map",
452 "package",
453 "range",
454 "return",
455 "select",
456 "struct",
457 "switch",
458 "type",
459 "var",
460 "fmt",
461 "Println",
462 "Printf",
463 "Sprintf",
464 "errors",
465 "strings",
466 "strconv",
467 ],
468 "ruby" | "rb" => &[
469 "alias",
470 "and",
471 "begin",
472 "break",
473 "case",
474 "class",
475 "def",
476 "defined?",
477 "do",
478 "else",
479 "elsif",
480 "end",
481 "ensure",
482 "false",
483 "for",
484 "if",
485 "in",
486 "module",
487 "next",
488 "nil",
489 "not",
490 "or",
491 "redo",
492 "rescue",
493 "retry",
494 "return",
495 "self",
496 "super",
497 "then",
498 "true",
499 "undef",
500 "unless",
501 "until",
502 "when",
503 "while",
504 "yield",
505 "puts",
506 "print",
507 "require",
508 "require_relative",
509 ],
510 "java" => &[
511 "abstract",
512 "assert",
513 "boolean",
514 "break",
515 "byte",
516 "case",
517 "catch",
518 "char",
519 "class",
520 "const",
521 "continue",
522 "default",
523 "do",
524 "double",
525 "else",
526 "enum",
527 "extends",
528 "final",
529 "finally",
530 "float",
531 "for",
532 "goto",
533 "if",
534 "implements",
535 "import",
536 "instanceof",
537 "int",
538 "interface",
539 "long",
540 "native",
541 "new",
542 "package",
543 "private",
544 "protected",
545 "public",
546 "return",
547 "short",
548 "static",
549 "strictfp",
550 "super",
551 "switch",
552 "synchronized",
553 "this",
554 "throw",
555 "throws",
556 "transient",
557 "try",
558 "void",
559 "volatile",
560 "while",
561 "System",
562 "String",
563 ],
564 _ => &[],
565 }
566}
567
568fn complete_file_path(partial: &str) -> Vec<Pair> {
569 let (dir_part, file_prefix) = if let Some(sep_pos) = partial.rfind('/') {
570 (&partial[..=sep_pos], &partial[sep_pos + 1..])
571 } else {
572 ("", partial)
573 };
574
575 let search_dir = if dir_part.is_empty() { "." } else { dir_part };
576
577 let mut results = Vec::new();
578 if let Ok(entries) = std::fs::read_dir(search_dir) {
579 for entry in entries.flatten() {
580 let name = entry.file_name().to_string_lossy().to_string();
581 if name.starts_with('.') {
582 continue; }
584 if name.starts_with(file_prefix) {
585 let full = format!("{dir_part}{name}");
586 let display = if entry.path().is_dir() {
587 format!("{name}/")
588 } else {
589 name.clone()
590 };
591 results.push(Pair {
592 display,
593 replacement: full,
594 });
595 }
596 }
597 }
598 results
599}
600
601impl Completer for ReplHelper {
602 type Candidate = Pair;
603
604 fn complete(
605 &self,
606 line: &str,
607 pos: usize,
608 _ctx: &rustyline::Context<'_>,
609 ) -> rustyline::Result<(usize, Vec<Pair>)> {
610 let line_up_to = &line[..pos];
611
612 if line_up_to.starts_with(':') {
614 if let Some(rest) = line_up_to
616 .strip_prefix(":load ")
617 .or_else(|| line_up_to.strip_prefix(":run "))
618 .or_else(|| line_up_to.strip_prefix(":save "))
619 {
620 let start = pos - rest.len();
621 return Ok((start, complete_file_path(rest)));
622 }
623
624 let candidates: Vec<Pair> = META_COMMANDS
625 .iter()
626 .filter(|cmd| cmd.starts_with(line_up_to))
627 .map(|cmd| Pair {
628 display: cmd.to_string(),
629 replacement: cmd.to_string(),
630 })
631 .collect();
632 return Ok((0, candidates));
633 }
634
635 let word_start = line_up_to
637 .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '!')
638 .map(|i| i + 1)
639 .unwrap_or(0);
640 let prefix = &line_up_to[word_start..];
641
642 if prefix.is_empty() {
643 return Ok((pos, Vec::new()));
644 }
645
646 let mut candidates: Vec<Pair> = Vec::new();
647
648 for kw in language_keywords(&self.language_id) {
650 if kw.starts_with(prefix) {
651 candidates.push(Pair {
652 display: kw.to_string(),
653 replacement: kw.to_string(),
654 });
655 }
656 }
657
658 for var in &self.session_vars {
660 if var.starts_with(prefix) && !candidates.iter().any(|c| c.replacement == *var) {
661 candidates.push(Pair {
662 display: var.clone(),
663 replacement: var.clone(),
664 });
665 }
666 }
667
668 Ok((word_start, candidates))
669 }
670}
671
672impl Hinter for ReplHelper {
673 type Hint = String;
674}
675
676impl Validator for ReplHelper {}
677
678impl Highlighter for ReplHelper {
679 fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
680 if line.trim_start().starts_with(':') {
681 return Cow::Borrowed(line);
682 }
683
684 let highlighted = highlight::highlight_repl_input(line, &self.language_id);
685 Cow::Owned(highlighted)
686 }
687
688 fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
689 true
690 }
691}
692
693impl Helper for ReplHelper {}
694
695pub fn run_repl(
696 initial_language: LanguageSpec,
697 registry: LanguageRegistry,
698 detect_enabled: bool,
699) -> Result<i32> {
700 let helper = ReplHelper::new(initial_language.canonical_id().to_string());
701 let config = Config::builder()
702 .history_ignore_space(true)
703 .history_ignore_dups(true)?
704 .build();
705 let mut editor = Editor::<ReplHelper, DefaultHistory>::with_config(config)?;
706 editor.set_helper(Some(helper));
707
708 if let Some(path) = history_path(initial_language.canonical_id()) {
709 if let Some(parent) = path.parent() {
710 let _ = std::fs::create_dir_all(parent);
711 }
712 let _ = editor.load_history(&path);
713 }
714
715 let lang_count = registry.known_languages().len();
716 let mut state = ReplState::new(initial_language, registry, detect_enabled)?;
717
718 println!(
719 "\x1b[1mrun\x1b[0m \x1b[2mv{} — {}+ languages. Type :help for commands.\x1b[0m",
720 env!("CARGO_PKG_VERSION"),
721 lang_count
722 );
723 let mut pending: Option<PendingInput> = None;
724
725 loop {
726 let prompt = match &pending {
727 Some(p) => p.prompt(),
728 None => state.prompt(),
729 };
730 let mut pending_indent: Option<String> = None;
731 if let Some(p) = pending.as_ref()
732 && state.current_language().canonical_id() == "python"
733 {
734 let indent = python_prompt_indent(p.buffer());
735 if !indent.is_empty() {
736 pending_indent = Some(indent);
737 }
738 }
739
740 if let Some(helper) = editor.helper_mut() {
741 helper.update_language(state.current_language().canonical_id().to_string());
742 }
743
744 let line_result = match pending_indent.as_deref() {
745 Some(indent) => editor.readline_with_initial(&prompt, (indent, "")),
746 None => editor.readline(&prompt),
747 };
748
749 match line_result {
750 Ok(line) => {
751 let raw = line.trim_end_matches(['\r', '\n']);
752
753 if let Some(p) = pending.as_mut() {
754 if raw.trim() == ":cancel" {
755 pending = None;
756 continue;
757 }
758
759 p.push_line_auto_with_indent(
760 state.current_language().canonical_id(),
761 raw,
762 pending_indent.as_deref(),
763 );
764 if p.needs_more_input(state.current_language().canonical_id()) {
765 continue;
766 }
767
768 let code = p.take();
769 pending = None;
770 let trimmed = code.trim_end();
771 if !trimmed.is_empty() {
772 let _ = editor.add_history_entry(trimmed);
773 state.history_entries.push(trimmed.to_string());
774 state.log_input(trimmed);
775 state.execute_snippet(trimmed)?;
776 if let Some(helper) = editor.helper_mut() {
777 helper.update_session_vars(state.session_var_names());
778 }
779 }
780 continue;
781 }
782
783 if raw.trim().is_empty() {
784 continue;
785 }
786
787 if state.paste_buffer.is_some() {
788 if raw.trim() == ":end" {
789 let lines = state.paste_buffer.take().unwrap();
790 let code = strip_paste_prompts(&lines);
791 if !code.trim().is_empty() {
792 let _ = editor.add_history_entry(code.trim());
793 state.history_entries.push(code.trim().to_string());
794 state.log_input(code.trim());
795 if let Err(e) = state.execute_snippet(code.trim()) {
796 println!("\x1b[31m[run]\x1b[0m {e}");
797 }
798 if let Some(helper) = editor.helper_mut() {
799 helper.update_session_vars(state.session_var_names());
800 }
801 }
802 println!("\x1b[2m[paste done]\x1b[0m");
803 } else {
804 state.paste_buffer.as_mut().unwrap().push(raw.to_string());
805 }
806 continue;
807 }
808
809 if raw.trim_start().starts_with(':') {
810 let trimmed = raw.trim();
811 let _ = editor.add_history_entry(trimmed);
812 state.log_input(trimmed);
813 if state.handle_meta(trimmed)? {
814 break;
815 }
816 continue;
817 }
818
819 let mut p = PendingInput::new();
820 p.push_line(raw);
821 if p.needs_more_input(state.current_language().canonical_id()) {
822 pending = Some(p);
823 continue;
824 }
825
826 let trimmed = raw.trim_end();
827 let _ = editor.add_history_entry(trimmed);
828 state.history_entries.push(trimmed.to_string());
829 state.log_input(trimmed);
830 state.execute_snippet(trimmed)?;
831 if let Some(helper) = editor.helper_mut() {
832 helper.update_session_vars(state.session_var_names());
833 }
834 }
835 Err(ReadlineError::Interrupted) => {
836 println!("^C");
837 pending = None;
838 continue;
839 }
840 Err(ReadlineError::Eof) => {
841 if let Some(lines) = state.paste_buffer.take() {
842 let code = strip_paste_prompts(&lines);
843 if !code.trim().is_empty() {
844 let _ = editor.add_history_entry(code.trim());
845 state.history_entries.push(code.trim().to_string());
846 state.log_input(code.trim());
847 if let Err(e) = state.execute_snippet(code.trim()) {
848 println!("\x1b[31m[run]\x1b[0m {e}");
849 }
850 }
851 println!("\x1b[2m[paste done]\x1b[0m");
852 }
853 println!("bye");
854 break;
855 }
856 Err(err) => {
857 bail!("readline error: {err}");
858 }
859 }
860 }
861
862 if let Some(path) = history_path(state.current_language.canonical_id()) {
863 if let Some(parent) = path.parent() {
864 let _ = std::fs::create_dir_all(parent);
865 }
866 let _ = editor.save_history(&path);
867 }
868
869 state.shutdown();
870 Ok(0)
871}
872
873struct ReplState {
874 registry: LanguageRegistry,
875 sessions: HashMap<String, Box<dyn LanguageSession>>, current_language: LanguageSpec,
877 detect_enabled: bool,
878 defined_names: HashSet<String>,
879 history_entries: Vec<String>,
880 dir_stack: Vec<PathBuf>,
881 bookmarks: HashMap<String, PathBuf>,
882 log_path: Option<PathBuf>,
883 macros: HashMap<String, String>,
884 xmode: XMode,
885 paste_buffer: Option<Vec<String>>,
887 precision: Option<u32>,
889 in_count: usize,
891 last_stdout: Option<String>,
893 numbered_prompts: bool,
895}
896
897struct PendingInput {
898 buf: String,
899}
900
901impl PendingInput {
902 fn new() -> Self {
903 Self { buf: String::new() }
904 }
905
906 fn prompt(&self) -> String {
907 "... ".to_string()
908 }
909
910 fn buffer(&self) -> &str {
911 &self.buf
912 }
913
914 fn push_line(&mut self, line: &str) {
915 self.buf.push_str(line);
916 self.buf.push('\n');
917 }
918
919 #[cfg(test)]
920 fn push_line_auto(&mut self, language_id: &str, line: &str) {
921 self.push_line_auto_with_indent(language_id, line, None);
922 }
923
924 fn push_line_auto_with_indent(
925 &mut self,
926 language_id: &str,
927 line: &str,
928 expected_indent: Option<&str>,
929 ) {
930 match language_id {
931 "python" | "py" | "python3" | "py3" => {
932 let adjusted = python_auto_indent_with_expected(line, &self.buf, expected_indent);
933 self.push_line(&adjusted);
934 }
935 _ => self.push_line(line),
936 }
937 }
938
939 fn take(&mut self) -> String {
940 std::mem::take(&mut self.buf)
941 }
942
943 fn needs_more_input(&self, language_id: &str) -> bool {
944 needs_more_input(language_id, &self.buf)
945 }
946}
947
948fn needs_more_input(language_id: &str, code: &str) -> bool {
949 match language_id {
950 "python" | "py" | "python3" | "py3" => needs_more_input_python(code),
951
952 _ => has_unclosed_delimiters(code) || generic_line_looks_incomplete(code),
953 }
954}
955
956fn generic_line_looks_incomplete(code: &str) -> bool {
957 let mut last: Option<&str> = None;
958 for line in code.lines().rev() {
959 let trimmed = line.trim_end();
960 if trimmed.trim().is_empty() {
961 continue;
962 }
963 last = Some(trimmed);
964 break;
965 }
966 let Some(line) = last else { return false };
967 let line = line.trim();
968 if line.is_empty() {
969 return false;
970 }
971 if line.starts_with('#') {
972 return false;
973 }
974
975 if line.ends_with('\\') {
976 return true;
977 }
978
979 const TAILS: [&str; 24] = [
980 "=", "+", "-", "*", "/", "%", "&", "|", "^", "!", "<", ">", "&&", "||", "??", "?:", "?",
981 ":", ".", ",", "=>", "->", "::", "..",
982 ];
983 if TAILS.iter().any(|tok| line.ends_with(tok)) {
984 return true;
985 }
986
987 const PREFIXES: [&str; 9] = [
988 "return", "throw", "yield", "await", "import", "from", "export", "case", "else",
989 ];
990 let lowered = line.to_ascii_lowercase();
991 if PREFIXES
992 .iter()
993 .any(|kw| lowered == *kw || lowered.ends_with(&format!(" {kw}")))
994 {
995 return true;
996 }
997
998 false
999}
1000
1001fn needs_more_input_python(code: &str) -> bool {
1002 if has_unclosed_delimiters(code) {
1003 return true;
1004 }
1005
1006 let mut last_nonempty: Option<&str> = None;
1007 let mut saw_block_header = false;
1008 let mut has_body_after_header = false;
1009
1010 for line in code.lines() {
1011 let trimmed = line.trim_end();
1012 if trimmed.trim().is_empty() {
1013 continue;
1014 }
1015 last_nonempty = Some(trimmed);
1016 if is_python_block_header(trimmed.trim()) {
1017 saw_block_header = true;
1018 has_body_after_header = false;
1019 } else if saw_block_header {
1020 has_body_after_header = true;
1021 }
1022 }
1023
1024 if !saw_block_header {
1025 return false;
1026 }
1027
1028 if code.ends_with("\n\n") {
1030 return false;
1031 }
1032
1033 if !has_body_after_header {
1035 return true;
1036 }
1037
1038 if let Some(last) = last_nonempty
1040 && (last.starts_with(' ') || last.starts_with('\t'))
1041 {
1042 return true;
1043 }
1044
1045 false
1046}
1047
1048fn is_python_block_header(line: &str) -> bool {
1051 if !line.ends_with(':') {
1052 return false;
1053 }
1054 let lowered = line.to_ascii_lowercase();
1055 const BLOCK_KEYWORDS: &[&str] = &[
1056 "def ",
1057 "class ",
1058 "if ",
1059 "elif ",
1060 "else:",
1061 "for ",
1062 "while ",
1063 "try:",
1064 "except",
1065 "finally:",
1066 "with ",
1067 "async def ",
1068 "async for ",
1069 "async with ",
1070 ];
1071 BLOCK_KEYWORDS.iter().any(|kw| lowered.starts_with(kw))
1072}
1073
1074fn python_auto_indent(line: &str, existing: &str) -> String {
1075 python_auto_indent_with_expected(line, existing, None)
1076}
1077
1078fn python_auto_indent_with_expected(
1079 line: &str,
1080 existing: &str,
1081 expected_indent: Option<&str>,
1082) -> String {
1083 let trimmed = line.trim_end_matches(['\r', '\n']);
1084 let raw = trimmed;
1085 if raw.trim().is_empty() {
1086 return raw.to_string();
1087 }
1088
1089 let (raw_indent, raw_content) = split_indent(raw);
1090
1091 let mut last_nonempty: Option<&str> = None;
1092 for l in existing.lines().rev() {
1093 if l.trim().is_empty() {
1094 continue;
1095 }
1096 last_nonempty = Some(l);
1097 break;
1098 }
1099
1100 let Some(prev) = last_nonempty else {
1101 return raw.to_string();
1102 };
1103 let prev_trimmed = prev.trim_end();
1104 let prev_indent = prev
1105 .chars()
1106 .take_while(|c| *c == ' ' || *c == '\t')
1107 .collect::<String>();
1108
1109 let lowered = raw.trim().to_ascii_lowercase();
1110 let is_dedent_keyword = lowered.starts_with("else:")
1111 || lowered.starts_with("elif ")
1112 || lowered.starts_with("except")
1113 || lowered.starts_with("finally:")
1114 || lowered.starts_with("return")
1115 || lowered.starts_with("yield")
1116 || lowered == "return"
1117 || lowered == "yield";
1118 let suggested = if lowered.starts_with("else:")
1119 || lowered.starts_with("elif ")
1120 || lowered.starts_with("except")
1121 || lowered.starts_with("finally:")
1122 {
1123 if prev_indent.is_empty() {
1124 None
1125 } else {
1126 Some(python_dedent_one_level(&prev_indent))
1127 }
1128 } else if lowered.starts_with("return")
1129 || lowered.starts_with("yield")
1130 || lowered == "return"
1131 || lowered == "yield"
1132 {
1133 python_last_def_indent(existing).map(|indent| format!("{indent} "))
1134 } else if is_python_block_header(prev_trimmed.trim()) && prev_trimmed.ends_with(':') {
1135 Some(format!("{prev_indent} "))
1136 } else if !prev_indent.is_empty() {
1137 Some(prev_indent)
1138 } else {
1139 None
1140 };
1141
1142 if let Some(indent) = suggested {
1143 if let Some(expected) = expected_indent {
1144 if raw_indent.len() < expected.len() {
1145 return raw.to_string();
1146 }
1147 if is_dedent_keyword
1148 && raw_indent.len() == expected.len()
1149 && raw_indent.len() > indent.len()
1150 {
1151 return format!("{indent}{raw_content}");
1152 }
1153 }
1154 if raw_indent.len() < indent.len() {
1155 return format!("{indent}{raw_content}");
1156 }
1157 }
1158
1159 raw.to_string()
1160}
1161
1162fn python_prompt_indent(existing: &str) -> String {
1163 if existing.trim().is_empty() {
1164 return String::new();
1165 }
1166 let adjusted = python_auto_indent("x", existing);
1167 let (indent, _content) = split_indent(&adjusted);
1168 indent
1169}
1170
1171fn python_dedent_one_level(indent: &str) -> String {
1172 if indent.is_empty() {
1173 return String::new();
1174 }
1175 if let Some(stripped) = indent.strip_suffix('\t') {
1176 return stripped.to_string();
1177 }
1178 let mut trimmed = indent.to_string();
1179 let mut removed = 0usize;
1180 while removed < 4 && trimmed.ends_with(' ') {
1181 trimmed.pop();
1182 removed += 1;
1183 }
1184 trimmed
1185}
1186
1187fn python_last_def_indent(existing: &str) -> Option<String> {
1188 for line in existing.lines().rev() {
1189 let trimmed = line.trim_end();
1190 if trimmed.trim().is_empty() {
1191 continue;
1192 }
1193 let lowered = trimmed.trim_start().to_ascii_lowercase();
1194 if lowered.starts_with("def ") || lowered.starts_with("async def ") {
1195 let indent = line
1196 .chars()
1197 .take_while(|c| *c == ' ' || *c == '\t')
1198 .collect::<String>();
1199 return Some(indent);
1200 }
1201 }
1202 None
1203}
1204
1205fn split_indent(line: &str) -> (String, &str) {
1206 let mut idx = 0;
1207 for (i, ch) in line.char_indices() {
1208 if ch == ' ' || ch == '\t' {
1209 idx = i + ch.len_utf8();
1210 } else {
1211 break;
1212 }
1213 }
1214 (line[..idx].to_string(), &line[idx..])
1215}
1216
1217fn has_unclosed_delimiters(code: &str) -> bool {
1218 let mut paren = 0i32;
1219 let mut bracket = 0i32;
1220 let mut brace = 0i32;
1221
1222 let mut in_single = false;
1223 let mut in_double = false;
1224 let mut in_backtick = false;
1225 let mut in_block_comment = false;
1226 let mut escape = false;
1227
1228 let chars: Vec<char> = code.chars().collect();
1229 let len = chars.len();
1230 let mut i = 0;
1231
1232 while i < len {
1233 let ch = chars[i];
1234
1235 if escape {
1236 escape = false;
1237 i += 1;
1238 continue;
1239 }
1240
1241 if in_block_comment {
1243 if ch == '*' && i + 1 < len && chars[i + 1] == '/' {
1244 in_block_comment = false;
1245 i += 2;
1246 continue;
1247 }
1248 i += 1;
1249 continue;
1250 }
1251
1252 if in_single {
1253 if ch == '\\' {
1254 escape = true;
1255 } else if ch == '\'' {
1256 in_single = false;
1257 }
1258 i += 1;
1259 continue;
1260 }
1261 if in_double {
1262 if ch == '\\' {
1263 escape = true;
1264 } else if ch == '"' {
1265 in_double = false;
1266 }
1267 i += 1;
1268 continue;
1269 }
1270 if in_backtick {
1271 if ch == '\\' {
1272 escape = true;
1273 } else if ch == '`' {
1274 in_backtick = false;
1275 }
1276 i += 1;
1277 continue;
1278 }
1279
1280 if ch == '/' && i + 1 < len && chars[i + 1] == '/' {
1282 while i < len && chars[i] != '\n' {
1284 i += 1;
1285 }
1286 continue;
1287 }
1288 if ch == '#' {
1289 while i < len && chars[i] != '\n' {
1291 i += 1;
1292 }
1293 continue;
1294 }
1295 if ch == '/' && i + 1 < len && chars[i + 1] == '*' {
1297 in_block_comment = true;
1298 i += 2;
1299 continue;
1300 }
1301
1302 match ch {
1303 '\'' => in_single = true,
1304 '"' => in_double = true,
1305 '`' => in_backtick = true,
1306 '(' => paren += 1,
1307 ')' => paren -= 1,
1308 '[' => bracket += 1,
1309 ']' => bracket -= 1,
1310 '{' => brace += 1,
1311 '}' => brace -= 1,
1312 _ => {}
1313 }
1314
1315 i += 1;
1316 }
1317
1318 paren > 0 || bracket > 0 || brace > 0 || in_block_comment
1319}
1320
1321impl ReplState {
1322 fn new(
1323 initial_language: LanguageSpec,
1324 registry: LanguageRegistry,
1325 detect_enabled: bool,
1326 ) -> Result<Self> {
1327 let bookmarks = load_bookmarks().unwrap_or_default();
1328 let mut state = Self {
1329 registry,
1330 sessions: HashMap::new(),
1331 current_language: initial_language,
1332 detect_enabled,
1333 defined_names: HashSet::new(),
1334 history_entries: Vec::new(),
1335 dir_stack: Vec::new(),
1336 bookmarks,
1337 log_path: None,
1338 macros: HashMap::new(),
1339 xmode: XMode::Verbose,
1340 paste_buffer: None,
1341 precision: None,
1342 in_count: 0,
1343 last_stdout: None,
1344 numbered_prompts: false,
1345 };
1346 if let Ok(cfg) = load_repl_config() {
1347 if let Some(v) = cfg.get("detect") {
1348 state.detect_enabled = matches!(v.to_lowercase().as_str(), "on" | "true" | "1");
1349 }
1350 if let Some(v) = cfg.get("xmode") {
1351 state.xmode = match v.to_lowercase().as_str() {
1352 "plain" => XMode::Plain,
1353 "context" => XMode::Context,
1354 _ => XMode::Verbose,
1355 };
1356 }
1357 if let Some(v) = cfg.get("precision")
1358 && let Ok(n) = v.parse::<u32>()
1359 {
1360 state.precision = Some(n.min(32));
1361 }
1362 if let Some(v) = cfg.get("numbered_prompts") {
1363 state.numbered_prompts = matches!(v.to_lowercase().as_str(), "on" | "true" | "1");
1364 }
1365 }
1366 state.ensure_current_language()?;
1367 Ok(state)
1368 }
1369
1370 fn current_language(&self) -> &LanguageSpec {
1371 &self.current_language
1372 }
1373
1374 fn prompt(&self) -> String {
1375 if self.numbered_prompts {
1376 format!(
1377 "{} [{}]>>> ",
1378 self.current_language.canonical_id(),
1379 self.in_count + 1
1380 )
1381 } else {
1382 format!("{}>>> ", self.current_language.canonical_id())
1383 }
1384 }
1385
1386 fn ensure_current_language(&mut self) -> Result<()> {
1387 if self.registry.resolve(&self.current_language).is_none() {
1388 bail!(
1389 "language '{}' is not available",
1390 self.current_language.canonical_id()
1391 );
1392 }
1393 Ok(())
1394 }
1395
1396 fn handle_meta(&mut self, line: &str) -> Result<bool> {
1397 let command = line.trim_start_matches(':').trim();
1398 if command.is_empty() {
1399 return Ok(false);
1400 }
1401
1402 if let Some(stripped) = command.strip_prefix("!!") {
1404 let shell_cmd = stripped.trim_start();
1405 if shell_cmd.is_empty() {
1406 println!("usage: :!! <cmd>");
1407 } else {
1408 run_shell(shell_cmd, true);
1409 }
1410 return Ok(false);
1411 }
1412 if let Some(stripped) = command.strip_prefix('!') {
1413 let shell_cmd = stripped.trim_start();
1414 if shell_cmd.is_empty() {
1415 println!("usage: :! <cmd>");
1416 } else {
1417 run_shell(shell_cmd, false);
1418 }
1419 return Ok(false);
1420 }
1421
1422 let mut parts = command.split_whitespace();
1423 let Some(head) = parts.next() else {
1424 return Ok(false);
1425 };
1426 match head {
1427 "exit" | "quit" => return Ok(true),
1428 "help" => {
1429 if let Some(arg) = parts.next() {
1430 Self::print_cmd_help(arg);
1431 } else {
1432 self.print_help();
1433 }
1434 return Ok(false);
1435 }
1436 "commands" => {
1437 Self::print_commands_machine();
1438 return Ok(false);
1439 }
1440 "quickref" => {
1441 Self::print_quickref();
1442 return Ok(false);
1443 }
1444 "?" => {
1445 let expr = parts.collect::<Vec<_>>().join(" ").trim().to_string();
1446 if expr.is_empty() {
1447 println!(
1448 "usage: :? <name> — show doc/source for <name> (e.g. :? print). Supported in Python session."
1449 );
1450 } else if !expr
1451 .chars()
1452 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
1453 {
1454 println!(
1455 "\x1b[31m[run]\x1b[0m :? only accepts names (letters, digits, dots, underscores)."
1456 );
1457 } else if let Err(e) = self.run_introspect(&expr) {
1458 println!("\x1b[31m[run]\x1b[0m {e}");
1459 }
1460 return Ok(false);
1461 }
1462 "debug" => {
1463 let rest: String = parts.collect::<Vec<_>>().join(" ");
1464 let code = rest.trim();
1465 let code = if code.is_empty() {
1466 self.history_entries
1467 .last()
1468 .map(String::as_str)
1469 .unwrap_or("")
1470 } else {
1471 code
1472 };
1473 if code.is_empty() {
1474 println!(
1475 "usage: :debug [CODE] — run last snippet (or CODE) under debugger. Python: pdb."
1476 );
1477 } else if let Err(e) = self.run_debug(code) {
1478 println!("\x1b[31m[run]\x1b[0m {e}");
1479 }
1480 return Ok(false);
1481 }
1482 "languages" => {
1483 self.print_languages();
1484 return Ok(false);
1485 }
1486 "versions" => {
1487 if let Some(lang) = parts.next() {
1488 let spec = LanguageSpec::new(lang.to_string());
1489 if self.registry.resolve(&spec).is_some() {
1490 self.print_versions(Some(spec))?;
1491 } else {
1492 let available = self.registry.known_languages().join(", ");
1493 println!(
1494 "language '{}' not supported. Available: {available}",
1495 spec.canonical_id()
1496 );
1497 }
1498 } else {
1499 self.print_versions(None)?;
1500 }
1501 return Ok(false);
1502 }
1503 "detect" => {
1504 if let Some(arg) = parts.next() {
1505 match arg {
1506 "on" | "true" | "1" => {
1507 self.detect_enabled = true;
1508 println!("auto-detect enabled");
1509 }
1510 "off" | "false" | "0" => {
1511 self.detect_enabled = false;
1512 println!("auto-detect disabled");
1513 }
1514 "toggle" => {
1515 self.detect_enabled = !self.detect_enabled;
1516 println!(
1517 "auto-detect {}",
1518 if self.detect_enabled {
1519 "enabled"
1520 } else {
1521 "disabled"
1522 }
1523 );
1524 }
1525 _ => println!("usage: :detect <on|off|toggle>"),
1526 }
1527 } else {
1528 println!(
1529 "auto-detect is {}",
1530 if self.detect_enabled {
1531 "enabled"
1532 } else {
1533 "disabled"
1534 }
1535 );
1536 }
1537 return Ok(false);
1538 }
1539 "lang" => {
1540 if let Some(lang) = parts.next() {
1541 self.switch_language(LanguageSpec::new(lang.to_string()))?;
1542 } else {
1543 println!("usage: :lang <language>");
1544 }
1545 return Ok(false);
1546 }
1547 "reset" => {
1548 if parts
1549 .next()
1550 .is_some_and(|arg| arg.eq_ignore_ascii_case("all"))
1551 {
1552 self.reset_all_sessions();
1553 println!("all sessions reset");
1554 } else {
1555 self.reset_current_session();
1556 println!(
1557 "session for '{}' reset",
1558 self.current_language.canonical_id()
1559 );
1560 }
1561 return Ok(false);
1562 }
1563 "cd" => {
1564 let arg = parts.next();
1565 if let Some("-b") = arg {
1566 if let Some(name) = parts.next() {
1567 if let Some(path) = self.bookmarks.get(name) {
1568 if let Ok(cwd) = std::env::current_dir() {
1569 if self.dir_stack.len() < MAX_DIR_STACK {
1570 self.dir_stack.push(cwd);
1571 } else {
1572 self.dir_stack.remove(0);
1573 self.dir_stack.push(cwd);
1574 }
1575 }
1576 if std::env::set_current_dir(path).is_ok() {
1577 println!("{}", path.display());
1578 } else {
1579 println!(
1580 "\x1b[31m[run]\x1b[0m cd: {}: no such directory",
1581 path.display()
1582 );
1583 }
1584 } else {
1585 println!("\x1b[31m[run]\x1b[0m bookmark '{}' not found", name);
1586 }
1587 } else {
1588 println!("usage: :cd -b <bookmark>");
1589 }
1590 } else if let Some(dir) = arg {
1591 if dir == "-" {
1592 if let Some(prev) = self.dir_stack.pop() {
1593 if std::env::set_current_dir(&prev).is_ok() {
1594 println!("{}", prev.display());
1595 }
1596 } else {
1597 println!("\x1b[2m[run]\x1b[0m directory stack empty");
1598 }
1599 } else {
1600 let path = PathBuf::from(dir);
1601 if let Ok(cwd) = std::env::current_dir() {
1602 if self.dir_stack.len() < MAX_DIR_STACK {
1603 self.dir_stack.push(cwd);
1604 } else {
1605 self.dir_stack.remove(0);
1606 self.dir_stack.push(cwd);
1607 }
1608 }
1609 if std::env::set_current_dir(&path).is_ok() {
1610 println!("{}", path.display());
1611 } else {
1612 println!(
1613 "\x1b[31m[run]\x1b[0m cd: {}: no such directory",
1614 path.display()
1615 );
1616 }
1617 }
1618 } else if let Ok(cwd) = std::env::current_dir() {
1619 println!("{}", cwd.display());
1620 }
1621 return Ok(false);
1622 }
1623 "dhist" => {
1624 let n: usize = parts.next().and_then(|s| s.parse().ok()).unwrap_or(10);
1625 let len = self.dir_stack.len();
1626 let start = len.saturating_sub(n);
1627 if self.dir_stack.is_empty() {
1628 println!("\x1b[2m(no directory history)\x1b[0m");
1629 } else {
1630 for (i, p) in self.dir_stack[start..].iter().enumerate() {
1631 let num = start + i + 1;
1632 println!("\x1b[2m[{num:>2}]\x1b[0m {}", p.display());
1633 }
1634 }
1635 return Ok(false);
1636 }
1637 "env" => {
1638 let a = parts.next();
1639 let b = parts.next();
1640 match (a, b) {
1641 (None, _) => {
1642 let vars: BTreeMap<String, String> = std::env::vars().collect();
1643 for (k, v) in vars {
1644 println!("{k}={v}");
1645 }
1646 }
1647 (Some(var), None) => {
1648 if let Some((k, v)) = var.split_once('=') {
1649 unsafe { std::env::set_var(k, v) };
1650 } else if let Ok(v) = std::env::var(var) {
1651 println!("{v}");
1652 }
1653 }
1654 (Some(var), Some(val)) => {
1655 if val == "=" {
1656 if let Some(v) = parts.next() {
1657 unsafe { std::env::set_var(var, v) };
1658 }
1659 } else if val.starts_with('=') {
1660 unsafe { std::env::set_var(var, val.trim_start_matches('=')) };
1661 } else {
1662 unsafe { std::env::set_var(var, val) };
1663 }
1664 }
1665 }
1666 return Ok(false);
1667 }
1668 "bookmark" => {
1669 let arg = parts.next();
1670 match arg {
1671 Some("-l") => {
1672 if self.bookmarks.is_empty() {
1673 println!("\x1b[2m(no bookmarks)\x1b[0m");
1674 } else {
1675 let mut names: Vec<_> = self.bookmarks.keys().collect();
1676 names.sort();
1677 for name in names {
1678 let path = self.bookmarks.get(name).unwrap();
1679 println!(" {name}\t{}", path.display());
1680 }
1681 }
1682 }
1683 Some("-d") => {
1684 if let Some(name) = parts.next() {
1685 if self.bookmarks.remove(name).is_some() {
1686 let _ = save_bookmarks(&self.bookmarks);
1687 println!("\x1b[2m[removed bookmark '{name}']\x1b[0m");
1688 } else {
1689 println!("\x1b[31m[run]\x1b[0m bookmark '{}' not found", name);
1690 }
1691 } else {
1692 println!("usage: :bookmark -d <name>");
1693 }
1694 }
1695 Some(name) if !name.starts_with('-') => {
1696 let path = parts
1697 .next()
1698 .map(PathBuf::from)
1699 .or_else(|| std::env::current_dir().ok());
1700 if let Some(p) = path {
1701 if p.is_absolute() {
1702 self.bookmarks.insert(name.to_string(), p.clone());
1703 let _ = save_bookmarks(&self.bookmarks);
1704 println!("\x1b[2m[bookmark '{name}' -> {}]\x1b[0m", p.display());
1705 } else {
1706 println!("\x1b[31m[run]\x1b[0m bookmark path must be absolute");
1707 }
1708 } else {
1709 println!("\x1b[31m[run]\x1b[0m could not get current directory");
1710 }
1711 }
1712 _ => {
1713 println!(
1714 "usage: :bookmark <name> [path] | :bookmark -l | :bookmark -d <name>"
1715 );
1716 }
1717 }
1718 return Ok(false);
1719 }
1720 "load" | "run" => {
1721 if let Some(token) = parts.next() {
1722 if let Some(code) = self.macros.get(token) {
1723 self.execute_payload(ExecutionPayload::Inline {
1724 code: code.clone(),
1725 args: Vec::new(),
1726 })?;
1727 } else {
1728 let path = if token.starts_with("http://") || token.starts_with("https://")
1729 {
1730 match fetch_url_to_temp(token) {
1731 Ok(p) => p,
1732 Err(e) => {
1733 println!("\x1b[31m[run]\x1b[0m fetch failed: {e}");
1734 return Ok(false);
1735 }
1736 }
1737 } else {
1738 PathBuf::from(token)
1739 };
1740 self.execute_payload(ExecutionPayload::File {
1741 path,
1742 args: Vec::new(),
1743 })?;
1744 }
1745 } else {
1746 println!("usage: :load <path|url> or :run <macro|path|url>");
1747 }
1748 return Ok(false);
1749 }
1750 "edit" => {
1751 let path = if let Some(token) = parts.next() {
1752 PathBuf::from(token)
1753 } else {
1754 match edit_temp_file() {
1755 Ok(p) => p,
1756 Err(e) => {
1757 println!("\x1b[31m[run]\x1b[0m edit: {e}");
1758 return Ok(false);
1759 }
1760 }
1761 };
1762 if run_editor(path.as_path()).is_err() {
1763 println!("\x1b[31m[run]\x1b[0m editor failed or $EDITOR not set");
1764 return Ok(false);
1765 }
1766 if path.exists() {
1767 self.execute_payload(ExecutionPayload::File {
1768 path,
1769 args: Vec::new(),
1770 })?;
1771 }
1772 return Ok(false);
1773 }
1774 "last" => {
1775 if let Some(ref s) = self.last_stdout {
1776 print!("{}", ensure_trailing_newline(s));
1777 } else {
1778 println!("\x1b[2m(no last output)\x1b[0m");
1779 }
1780 return Ok(false);
1781 }
1782 "logstart" => {
1783 let path = parts.next().map(PathBuf::from).unwrap_or_else(|| {
1784 std::env::current_dir()
1785 .unwrap_or_else(|_| PathBuf::from("."))
1786 .join("run_log.txt")
1787 });
1788 self.log_path = Some(path.clone());
1789 self.log_input(line);
1790 println!("\x1b[2m[logging to {}]\x1b[0m", path.display());
1791 return Ok(false);
1792 }
1793 "logstop" => {
1794 if self.log_path.take().is_some() {
1795 println!("\x1b[2m[logging stopped]\x1b[0m");
1796 } else {
1797 println!("\x1b[2m(not logging)\x1b[0m");
1798 }
1799 return Ok(false);
1800 }
1801 "logstate" => {
1802 if let Some(ref p) = self.log_path {
1803 println!("\x1b[2mlogging: {}\x1b[0m", p.display());
1804 } else {
1805 println!("\x1b[2m(not logging)\x1b[0m");
1806 }
1807 return Ok(false);
1808 }
1809 "macro" => {
1810 let sub = parts.next();
1811 if sub == Some("run") {
1812 if let Some(name) = parts.next() {
1813 if let Some(code) = self.macros.get(name) {
1814 self.execute_payload(ExecutionPayload::Inline {
1815 code: code.clone(),
1816 args: Vec::new(),
1817 })?;
1818 } else {
1819 println!("\x1b[31m[run]\x1b[0m unknown macro: {name}");
1820 }
1821 } else {
1822 println!("usage: :macro run <NAME>");
1823 }
1824 } else if let Some(name) = sub {
1825 let len = self.history_entries.len();
1826 let mut indices: Vec<usize> = Vec::new();
1827 for part in parts {
1828 let (s, e) = parse_history_range(part, len);
1829 for i in s..e {
1830 indices.push(i);
1831 }
1832 }
1833 indices.sort_unstable();
1834 indices.dedup();
1835 let code: String = indices
1836 .into_iter()
1837 .filter_map(|i| self.history_entries.get(i))
1838 .cloned()
1839 .collect::<Vec<_>>()
1840 .join("\n");
1841 if code.is_empty() {
1842 println!("\x1b[31m[run]\x1b[0m no history entries for range");
1843 } else {
1844 self.macros.insert(name.to_string(), code);
1845 println!("\x1b[2m[macro '{name}' saved]\x1b[0m");
1846 }
1847 } else {
1848 println!("usage: :macro <NAME> <range>... or :macro run <NAME>");
1849 }
1850 return Ok(false);
1851 }
1852 "time" => {
1853 let code = parts.collect::<Vec<_>>().join(" ").trim().to_string();
1854 if code.is_empty() {
1855 println!("usage: :time <CODE>");
1856 return Ok(false);
1857 }
1858 let start = Instant::now();
1859 self.execute_payload(ExecutionPayload::Inline {
1860 code,
1861 args: Vec::new(),
1862 })?;
1863 let elapsed = start.elapsed();
1864 println!("\x1b[2m[elapsed: {:?}]\x1b[0m", elapsed);
1865 return Ok(false);
1866 }
1867 "who" => {
1868 let mut names: Vec<_> = self.defined_names.iter().cloned().collect();
1869 names.sort();
1870 if names.is_empty() {
1871 println!("\x1b[2m(no names tracked)\x1b[0m");
1872 } else {
1873 for n in &names {
1874 println!(" {n}");
1875 }
1876 }
1877 return Ok(false);
1878 }
1879 "whos" => {
1880 let pattern = parts.next();
1881 let mut names: Vec<_> = self.defined_names.iter().cloned().collect();
1882 if let Some(pat) = pattern {
1883 names.retain(|n| n.contains(pat));
1884 }
1885 names.sort();
1886 if names.is_empty() {
1887 println!("\x1b[2m(no names tracked)\x1b[0m");
1888 } else {
1889 for n in &names {
1890 println!(" {n}");
1891 }
1892 }
1893 return Ok(false);
1894 }
1895 "xmode" => {
1896 match parts.next().map(|s| s.to_lowercase()) {
1897 Some(ref m) if m == "plain" => self.xmode = XMode::Plain,
1898 Some(ref m) if m == "context" => self.xmode = XMode::Context,
1899 Some(ref m) if m == "verbose" => self.xmode = XMode::Verbose,
1900 _ => {
1901 let current = match self.xmode {
1902 XMode::Plain => "plain",
1903 XMode::Context => "context",
1904 XMode::Verbose => "verbose",
1905 };
1906 println!(
1907 "\x1b[2mexception display: {current} (plain | context | verbose)\x1b[0m"
1908 );
1909 }
1910 }
1911 return Ok(false);
1912 }
1913 "paste" => {
1914 self.paste_buffer = Some(Vec::new());
1915 println!("\x1b[2m[paste mode — type :end or Ctrl-D to execute]\x1b[0m");
1916 return Ok(false);
1917 }
1918 "end" => {
1919 if self.paste_buffer.is_some() {
1920 println!("\x1b[2m[paste done]\x1b[0m");
1922 } else {
1923 println!("\x1b[31m[run]\x1b[0m not in paste mode");
1924 }
1925 return Ok(false);
1926 }
1927 "precision" => {
1928 match parts.next() {
1929 None => match self.precision {
1930 Some(n) => println!("\x1b[2mprecision: {n}\x1b[0m"),
1931 None => println!("\x1b[2mprecision: (default)\x1b[0m"),
1932 },
1933 Some(s) => {
1934 if let Ok(n) = s.parse::<u32>() {
1935 let n = n.min(32);
1936 self.precision = Some(n);
1937 let mut cfg = load_repl_config().unwrap_or_default();
1938 cfg.insert("precision".to_string(), n.to_string());
1939 if save_repl_config(&cfg).is_err() {
1940 println!("\x1b[31m[run]\x1b[0m failed to save config");
1941 } else {
1942 println!("\x1b[2m[precision = {n}]\x1b[0m");
1943 }
1944 } else {
1945 println!("\x1b[31m[run]\x1b[0m precision must be a number (0–32)");
1946 }
1947 }
1948 }
1949 return Ok(false);
1950 }
1951 "config" => {
1952 let key = parts.next().map(|s| s.to_lowercase());
1953 let val = parts.next();
1954 match (key.as_deref(), val) {
1955 (None, _) => {
1956 let detect = if self.detect_enabled { "on" } else { "off" };
1957 let xmode = match self.xmode {
1958 XMode::Plain => "plain",
1959 XMode::Context => "context",
1960 XMode::Verbose => "verbose",
1961 };
1962 let precision = self
1963 .precision
1964 .map(|n| n.to_string())
1965 .unwrap_or_else(|| "default".to_string());
1966 let numbered = if self.numbered_prompts { "on" } else { "off" };
1967 println!("\x1b[2mdetect\t{detect}\x1b[0m");
1968 println!("\x1b[2mxmode\t{xmode}\x1b[0m");
1969 println!("\x1b[2mprecision\t{precision}\x1b[0m");
1970 println!("\x1b[2mnumbered_prompts\t{numbered}\x1b[0m");
1971 }
1972 (Some(k), None) => {
1973 let v: Option<String> = match k {
1974 "detect" => {
1975 Some(if self.detect_enabled { "on" } else { "off" }.to_string())
1976 }
1977 "xmode" => Some(
1978 match self.xmode {
1979 XMode::Plain => "plain",
1980 XMode::Context => "context",
1981 XMode::Verbose => "verbose",
1982 }
1983 .to_string(),
1984 ),
1985 "precision" => Some(
1986 self.precision
1987 .map(|n| n.to_string())
1988 .unwrap_or_else(|| "default".to_string()),
1989 ),
1990 "numbered_prompts" => {
1991 Some(if self.numbered_prompts { "on" } else { "off" }.to_string())
1992 }
1993 _ => None,
1994 };
1995 if let Some(v) = v {
1996 println!("{v}");
1997 } else {
1998 println!("\x1b[31m[run]\x1b[0m unknown config key: {k}");
1999 }
2000 }
2001 (Some(k), Some(v)) => {
2002 let mut cfg = load_repl_config().unwrap_or_default();
2003 match k {
2004 "detect" => {
2005 self.detect_enabled =
2006 matches!(v.to_lowercase().as_str(), "on" | "true" | "1");
2007 cfg.insert("detect".to_string(), v.to_string());
2008 }
2009 "xmode" => {
2010 self.xmode = match v.to_lowercase().as_str() {
2011 "plain" => XMode::Plain,
2012 "context" => XMode::Context,
2013 _ => XMode::Verbose,
2014 };
2015 cfg.insert("xmode".to_string(), v.to_string());
2016 }
2017 "precision" => {
2018 if let Ok(n) = v.parse::<u32>() {
2019 self.precision = Some(n.min(32));
2020 cfg.insert(
2021 "precision".to_string(),
2022 self.precision.unwrap().to_string(),
2023 );
2024 }
2025 }
2026 "numbered_prompts" => {
2027 self.numbered_prompts =
2028 matches!(v.to_lowercase().as_str(), "on" | "true" | "1");
2029 cfg.insert("numbered_prompts".to_string(), v.to_string());
2030 }
2031 _ => {
2032 println!("\x1b[31m[run]\x1b[0m unknown config key: {k}");
2033 return Ok(false);
2034 }
2035 }
2036 if save_repl_config(&cfg).is_err() {
2037 println!("\x1b[31m[run]\x1b[0m failed to save config");
2038 } else {
2039 println!("\x1b[2m[{k} = {}]\x1b[0m", v.trim());
2040 }
2041 }
2042 }
2043 return Ok(false);
2044 }
2045 "save" => {
2046 if let Some(token) = parts.next() {
2047 let path = Path::new(token);
2048 match self.save_session(path) {
2049 Ok(count) => println!(
2050 "\x1b[2m[saved {count} entries to {}]\x1b[0m",
2051 path.display()
2052 ),
2053 Err(e) => println!("error saving session: {e}"),
2054 }
2055 } else {
2056 println!("usage: :save <path>");
2057 }
2058 return Ok(false);
2059 }
2060 "history" => {
2061 let rest: Vec<&str> = parts.collect();
2062 let mut grep_pattern: Option<&str> = None;
2063 let mut out_file: Option<&str> = None;
2064 let mut unique = false;
2065 let mut range_or_limit: Option<String> = None;
2066 let mut i = 0;
2067 while i < rest.len() {
2068 match rest[i] {
2069 "-g" => {
2070 i += 1;
2071 if i < rest.len() {
2072 grep_pattern = Some(rest[i]);
2073 i += 1;
2074 } else {
2075 println!("usage: :history -g <pattern>");
2076 return Ok(false);
2077 }
2078 }
2079 "-f" => {
2080 i += 1;
2081 if i < rest.len() {
2082 out_file = Some(rest[i]);
2083 i += 1;
2084 } else {
2085 println!("usage: :history -f <file>");
2086 return Ok(false);
2087 }
2088 }
2089 "-u" => {
2090 unique = true;
2091 i += 1;
2092 }
2093 _ => {
2094 range_or_limit = Some(rest[i].to_string());
2095 i += 1;
2096 }
2097 }
2098 }
2099 let entries = &self.history_entries;
2100 let len = entries.len();
2101 let (start, end) = if let Some(ref r) = range_or_limit {
2102 parse_history_range(r, len)
2103 } else {
2104 (len.saturating_sub(25), len)
2105 };
2106 let end = end.min(len);
2107 let slice = if start < len && start < end {
2108 &entries[start..end]
2109 } else {
2110 &entries[0..0]
2111 };
2112 let mut selected: Vec<(usize, &String)> = slice
2113 .iter()
2114 .enumerate()
2115 .map(|(i, e)| (start + i + 1, e))
2116 .filter(|(_, e)| grep_pattern.map(|p| e.contains(p)).unwrap_or(true))
2117 .collect();
2118 if unique {
2119 let mut seen = HashSet::new();
2120 selected.retain(|(_, e)| seen.insert((*e).clone()));
2121 }
2122 if let Some(path) = out_file {
2123 let path = Path::new(path);
2124 if let Ok(mut f) = std::fs::OpenOptions::new()
2125 .create(true)
2126 .append(true)
2127 .open(path)
2128 {
2129 use std::io::Write;
2130 for (_, e) in &selected {
2131 let _ = writeln!(f, "{e}");
2132 }
2133 println!(
2134 "\x1b[2m[appended {} entries to {}]\x1b[0m",
2135 selected.len(),
2136 path.display()
2137 );
2138 } else {
2139 println!(
2140 "\x1b[31m[run]\x1b[0m could not open {} for writing",
2141 path.display()
2142 );
2143 }
2144 } else if selected.is_empty() {
2145 println!("\x1b[2m(no history)\x1b[0m");
2146 } else {
2147 for (num, entry) in selected {
2148 let first_line = entry.lines().next().unwrap_or(entry.as_str());
2149 let is_multiline = entry.contains('\n');
2150 if is_multiline {
2151 println!("\x1b[2m[{num:>4}]\x1b[0m {first_line} \x1b[2m(...)\x1b[0m");
2152 } else {
2153 println!("\x1b[2m[{num:>4}]\x1b[0m {entry}");
2154 }
2155 }
2156 }
2157 return Ok(false);
2158 }
2159 "install" => {
2160 if let Some(pkg) = parts.next() {
2161 self.install_package(pkg);
2162 } else {
2163 println!("usage: :install <package>");
2164 }
2165 return Ok(false);
2166 }
2167 "bench" => {
2168 let n: u32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(10);
2169 let code = parts.collect::<Vec<_>>().join(" ");
2170 if code.is_empty() {
2171 println!("usage: :bench [N] <code>");
2172 println!(" Runs <code> N times (default: 10) and reports timing stats.");
2173 } else {
2174 self.bench_code(&code, n)?;
2175 }
2176 return Ok(false);
2177 }
2178 "type" | "which" => {
2179 let lang = &self.current_language;
2180 println!(
2181 "\x1b[1m{}\x1b[0m \x1b[2m({})\x1b[0m",
2182 lang.canonical_id(),
2183 if self.sessions.contains_key(lang.canonical_id()) {
2184 "session active"
2185 } else {
2186 "no session"
2187 }
2188 );
2189 return Ok(false);
2190 }
2191 alias => {
2192 let spec = LanguageSpec::new(alias);
2193 if self.registry.resolve(&spec).is_some() {
2194 self.switch_language(spec)?;
2195 return Ok(false);
2196 }
2197 println!("unknown command: :{alias}. Type :help for help.");
2198 }
2199 }
2200
2201 Ok(false)
2202 }
2203
2204 fn switch_language(&mut self, spec: LanguageSpec) -> Result<()> {
2205 if self.current_language.canonical_id() == spec.canonical_id() {
2206 println!("already using {}", spec.canonical_id());
2207 return Ok(());
2208 }
2209 if self.registry.resolve(&spec).is_none() {
2210 let available = self.registry.known_languages().join(", ");
2211 bail!(
2212 "language '{}' not supported. Available: {available}",
2213 spec.canonical_id()
2214 );
2215 }
2216 self.current_language = spec;
2217 println!("switched to {}", self.current_language.canonical_id());
2218 Ok(())
2219 }
2220
2221 fn reset_current_session(&mut self) {
2222 let key = self.current_language.canonical_id().to_string();
2223 if let Some(mut session) = self.sessions.remove(&key) {
2224 let _ = session.shutdown();
2225 }
2226 self.defined_names.clear();
2227 self.history_entries.clear();
2228 self.macros.clear();
2229 self.paste_buffer = None;
2230 self.last_stdout = None;
2231 }
2232
2233 fn reset_all_sessions(&mut self) {
2234 for (_, mut session) in self.sessions.drain() {
2235 let _ = session.shutdown();
2236 }
2237 self.defined_names.clear();
2238 self.history_entries.clear();
2239 self.macros.clear();
2240 self.paste_buffer = None;
2241 self.last_stdout = None;
2242 }
2243
2244 fn execute_snippet(&mut self, code: &str) -> Result<()> {
2245 if self.detect_enabled
2246 && let Some(detected) = crate::detect::detect_language_from_snippet(code)
2247 && detected != self.current_language.canonical_id()
2248 {
2249 let spec = LanguageSpec::new(detected.to_string());
2250 if self.registry.resolve(&spec).is_some() {
2251 println!(
2252 "[auto-detect] switching {} -> {}",
2253 self.current_language.canonical_id(),
2254 spec.canonical_id()
2255 );
2256 self.current_language = spec;
2257 }
2258 }
2259
2260 self.defined_names.extend(extract_defined_names(
2262 code,
2263 self.current_language.canonical_id(),
2264 ));
2265
2266 let payload = ExecutionPayload::Inline {
2267 code: code.to_string(),
2268 args: Vec::new(),
2269 };
2270 self.execute_payload(payload)
2271 }
2272
2273 fn session_var_names(&self) -> Vec<String> {
2274 self.defined_names.iter().cloned().collect()
2275 }
2276
2277 fn execute_payload(&mut self, payload: ExecutionPayload) -> Result<()> {
2278 let language = self.current_language.clone();
2279 let outcome_result = (|| -> Result<ExecutionOutcome> {
2280 Ok(match payload {
2281 ExecutionPayload::Inline { code, .. } => {
2282 if self.engine_supports_sessions(&language)? {
2283 self.eval_in_session(&language, &code)?
2284 } else {
2285 let engine = self
2286 .registry
2287 .resolve(&language)
2288 .context("language engine not found")?;
2289 engine.execute(&ExecutionPayload::Inline {
2290 code,
2291 args: Vec::new(),
2292 })?
2293 }
2294 }
2295 ExecutionPayload::File { ref path, .. } => {
2296 if self.engine_supports_sessions(&language)? {
2298 let code = std::fs::read_to_string(path)
2299 .with_context(|| format!("failed to read file: {}", path.display()))?;
2300 println!("\x1b[2m[loaded {}]\x1b[0m", path.display());
2301 self.eval_in_session(&language, &code)?
2302 } else {
2303 let engine = self
2304 .registry
2305 .resolve(&language)
2306 .context("language engine not found")?;
2307 engine.execute(&payload)?
2308 }
2309 }
2310 ExecutionPayload::Stdin { code, .. } => {
2311 if self.engine_supports_sessions(&language)? {
2312 self.eval_in_session(&language, &code)?
2313 } else {
2314 let engine = self
2315 .registry
2316 .resolve(&language)
2317 .context("language engine not found")?;
2318 engine.execute(&ExecutionPayload::Stdin {
2319 code,
2320 args: Vec::new(),
2321 })?
2322 }
2323 }
2324 })
2325 })();
2326 let outcome = match outcome_result {
2327 Ok(outcome) => outcome,
2328 Err(err) if err.to_string().contains("Execution timed out") => {
2329 println!(
2330 "\x1b[31m[run]\x1b[0m Execution timed out after {}s",
2331 crate::runtime::timeout_secs()
2332 );
2333 return Ok(());
2334 }
2335 Err(err) => return Err(err),
2336 };
2337 render_outcome(&outcome, self.xmode);
2338 self.last_stdout = Some(outcome.stdout.clone());
2339 self.in_count += 1;
2340 Ok(())
2341 }
2342
2343 fn engine_supports_sessions(&self, language: &LanguageSpec) -> Result<bool> {
2344 Ok(self
2345 .registry
2346 .resolve(language)
2347 .context("language engine not found")?
2348 .supports_sessions())
2349 }
2350
2351 fn eval_in_session(&mut self, language: &LanguageSpec, code: &str) -> Result<ExecutionOutcome> {
2352 use std::collections::hash_map::Entry;
2353 let key = language.canonical_id().to_string();
2354 match self.sessions.entry(key) {
2355 Entry::Occupied(mut entry) => entry.get_mut().eval(code),
2356 Entry::Vacant(entry) => {
2357 let engine = self
2358 .registry
2359 .resolve(language)
2360 .context("language engine not found")?;
2361 let mut session = engine.start_session().with_context(|| {
2362 format!("failed to start {} session", language.canonical_id())
2363 })?;
2364 let outcome = session.eval(code)?;
2365 entry.insert(session);
2366 Ok(outcome)
2367 }
2368 }
2369 }
2370
2371 fn run_introspect(&mut self, expr: &str) -> Result<()> {
2373 let language = self.current_language.clone();
2374 let lang = language.canonical_id();
2375 if lang == "python" {
2376 let code = format!("help({expr})");
2377 let outcome = self.eval_in_session(&language, &code)?;
2378 render_outcome(&outcome, self.xmode);
2379 Ok(())
2380 } else {
2381 println!(
2382 "\x1b[2mIntrospection not available for {lang}. Use :? in a Python session.\x1b[0m"
2383 );
2384 Ok(())
2385 }
2386 }
2387
2388 fn run_debug(&self, code: &str) -> Result<()> {
2390 let lang = self.current_language.canonical_id();
2391 if lang != "python" {
2392 println!(
2393 "\x1b[2mDebug not available for {lang}. Use :debug in a Python session (pdb).\x1b[0m"
2394 );
2395 return Ok(());
2396 }
2397 let mut tmp = tempfile::NamedTempFile::new().context("create temp file for :debug")?;
2398 tmp.as_file_mut()
2399 .write_all(code.as_bytes())
2400 .context("write debug script")?;
2401 let path = tmp.path();
2402 let status = Command::new("python3")
2403 .args(["-m", "pdb", path.to_str().unwrap_or("")])
2404 .stdin(Stdio::inherit())
2405 .stdout(Stdio::inherit())
2406 .stderr(Stdio::inherit())
2407 .status()
2408 .context("run pdb")?;
2409 if !status.success() {
2410 println!("\x1b[2m[pdb exit {status}]\x1b[0m");
2411 }
2412 Ok(())
2413 }
2414
2415 fn print_languages(&self) {
2416 let mut languages = self.registry.known_languages();
2417 languages.sort();
2418 println!("available languages: {}", languages.join(", "));
2419 }
2420
2421 fn print_versions(&self, language: Option<LanguageSpec>) -> Result<()> {
2422 println!("language toolchain versions...\n");
2423
2424 let mut available = 0u32;
2425 let mut missing = 0u32;
2426
2427 let mut languages: Vec<String> = if let Some(lang) = language {
2428 vec![lang.canonical_id().to_string()]
2429 } else {
2430 self.registry
2431 .known_languages()
2432 .into_iter()
2433 .map(|value| value.to_string())
2434 .collect()
2435 };
2436 languages.sort();
2437
2438 for lang_id in &languages {
2439 let spec = LanguageSpec::new(lang_id.to_string());
2440 if let Some(engine) = self.registry.resolve(&spec) {
2441 match engine.toolchain_version() {
2442 Ok(Some(version)) => {
2443 available += 1;
2444 println!(
2445 " [\x1b[32m OK \x1b[0m] {:<14} {} - {}",
2446 engine.display_name(),
2447 lang_id,
2448 version
2449 );
2450 }
2451 Ok(None) => {
2452 available += 1;
2453 println!(
2454 " [\x1b[33m ?? \x1b[0m] {:<14} {} - unknown",
2455 engine.display_name(),
2456 lang_id
2457 );
2458 }
2459 Err(_) => {
2460 missing += 1;
2461 println!(
2462 " [\x1b[31mMISS\x1b[0m] {:<14} {}",
2463 engine.display_name(),
2464 lang_id
2465 );
2466 }
2467 }
2468 }
2469 }
2470
2471 println!();
2472 println!(
2473 " {} available, {} missing, {} total",
2474 available,
2475 missing,
2476 available + missing
2477 );
2478
2479 if missing > 0 {
2480 println!("\n Tip: Install missing toolchains to enable those languages.");
2481 }
2482
2483 Ok(())
2484 }
2485
2486 fn install_package(&self, package: &str) {
2487 let lang_id = self.current_language.canonical_id();
2488 let override_key = format!("RUN_INSTALL_COMMAND_{}", lang_id.to_ascii_uppercase());
2489 let override_value = std::env::var(&override_key).ok();
2490 let Some(mut cmd) = build_install_command(lang_id, package) else {
2491 if override_value.is_some() {
2492 println!(
2493 "Error: {override_key} is set but could not be parsed.\n\
2494 Provide a valid command, e.g. {override_key}=\"uv pip install {{package}}\""
2495 );
2496 return;
2497 }
2498 println!(
2499 "No package manager available for '{lang_id}'.\n\
2500 Tip: set {override_key}=\"<cmd> {{package}}\"",
2501 );
2502 return;
2503 };
2504
2505 println!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
2506
2507 match cmd
2508 .stdin(std::process::Stdio::inherit())
2509 .stdout(std::process::Stdio::inherit())
2510 .stderr(std::process::Stdio::inherit())
2511 .status()
2512 {
2513 Ok(status) if status.success() => {
2514 println!("\x1b[32m[run]\x1b[0m Successfully installed '{package}'");
2515 }
2516 Ok(_) => {
2517 println!("\x1b[31m[run]\x1b[0m Failed to install '{package}'");
2518 }
2519 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
2520 let program = cmd.get_program().to_string_lossy();
2521 println!("\x1b[31m[run]\x1b[0m Package manager not found: {program}");
2522 println!("Tip: install it or set {override_key}=\"<cmd> {{package}}\"");
2523 }
2524 Err(e) => {
2525 println!("\x1b[31m[run]\x1b[0m Error running package manager: {e}");
2526 }
2527 }
2528 }
2529
2530 fn bench_code(&mut self, code: &str, iterations: u32) -> Result<()> {
2531 if iterations == 0 {
2532 println!("\x1b[31m[run]\x1b[0m :bench requires N >= 1");
2533 return Ok(());
2534 }
2535
2536 let language = self.current_language.clone();
2537
2538 let warmup = self.eval_in_session(&language, code)?;
2540 if !warmup.success() {
2541 println!("\x1b[31m[run]\x1b[0m Code failed during warmup");
2542 if !warmup.stderr.is_empty() {
2543 print!("{}", warmup.stderr);
2544 }
2545 return Ok(());
2546 }
2547 println!("\x1b[2m warmup: {}ms\x1b[0m", warmup.duration.as_millis());
2548
2549 let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
2550 for i in 0..iterations {
2551 let outcome = self.eval_in_session(&language, code)?;
2552 let ms = outcome.duration.as_secs_f64() * 1000.0;
2553 times.push(ms);
2554 if i < 3 || i == iterations - 1 {
2555 println!("\x1b[2m run {}: {:.2}ms\x1b[0m", i + 1, ms);
2556 }
2557 }
2558
2559 times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2560 let total: f64 = times.iter().sum();
2561 let avg = total / times.len() as f64;
2562 let min = times.first().copied().unwrap_or(0.0);
2563 let max = times.last().copied().unwrap_or(0.0);
2564 let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
2565 (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
2566 } else {
2567 times[times.len() / 2]
2568 };
2569 let variance: f64 =
2570 times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
2571 let stddev = variance.sqrt();
2572
2573 println!();
2574 println!("\x1b[1mResults ({iterations} runs):\x1b[0m");
2575 println!(" min: \x1b[32m{min:.2}ms\x1b[0m");
2576 println!(" max: \x1b[33m{max:.2}ms\x1b[0m");
2577 println!(" avg: \x1b[36m{avg:.2}ms\x1b[0m");
2578 println!(" median: \x1b[36m{median:.2}ms\x1b[0m");
2579 println!(" stddev: {stddev:.2}ms");
2580 Ok(())
2581 }
2582
2583 fn log_input(&self, line: &str) {
2584 if let Some(ref p) = self.log_path {
2585 let _ = std::fs::OpenOptions::new()
2586 .create(true)
2587 .append(true)
2588 .open(p)
2589 .and_then(|mut f| writeln!(f, "{line}"));
2590 }
2591 }
2592
2593 fn save_session(&self, path: &Path) -> Result<usize> {
2594 use std::io::Write;
2595 let mut file = std::fs::File::create(path)
2596 .with_context(|| format!("failed to create {}", path.display()))?;
2597 let count = self.history_entries.len();
2598 for entry in &self.history_entries {
2599 writeln!(file, "{entry}")?;
2600 }
2601 Ok(count)
2602 }
2603
2604 fn print_help(&self) {
2605 println!("\x1b[1mCommands\x1b[0m");
2606 for (name, desc) in CMD_HELP {
2607 println!(" \x1b[36m:{name}\x1b[0m \x1b[2m{desc}\x1b[0m");
2608 }
2609 println!("\x1b[2mLanguage shortcuts: :py, :js, :rs, :go, :cpp, :java, ...\x1b[0m");
2610 println!(
2611 "\x1b[2mIn session languages (e.g. Python), _ is the last expression result.\x1b[0m"
2612 );
2613 }
2614
2615 fn print_cmd_help(cmd_name: &str) {
2616 let key = cmd_name.trim_start_matches(':').trim().to_lowercase();
2617 for (name, desc) in CMD_HELP {
2618 if name.to_lowercase() == key {
2619 println!(" \x1b[36m:{name}\x1b[0m \x1b[2m{desc}\x1b[0m");
2620 return;
2621 }
2622 }
2623 println!("\x1b[31m[run]\x1b[0m unknown command :{cmd_name}");
2624 }
2625
2626 fn print_commands_machine() {
2627 for (name, desc) in CMD_HELP {
2628 println!(":{name}\t{desc}");
2629 }
2630 }
2631
2632 fn print_quickref() {
2633 println!("\x1b[1mQuick reference\x1b[0m");
2634 for (name, desc) in CMD_HELP {
2635 println!(" :{name}\t{desc}");
2636 }
2637 println!(
2638 "\x1b[2m:py :js :rs :go :cpp :java ... In Python (session), _ = last result.\x1b[0m"
2639 );
2640 }
2641
2642 fn shutdown(&mut self) {
2643 for (_, mut session) in self.sessions.drain() {
2644 let _ = session.shutdown();
2645 }
2646 }
2647}
2648
2649fn strip_paste_prompts(lines: &[String]) -> String {
2651 let stripped: Vec<String> = lines
2652 .iter()
2653 .map(|s| {
2654 let t = s.trim_start();
2655 let t = t.strip_prefix(">>>").map(|r| r.trim_start()).unwrap_or(t);
2656 let t = t.strip_prefix("...").map(|r| r.trim_start()).unwrap_or(t);
2657 t.to_string()
2658 })
2659 .collect();
2660 let non_empty: Vec<&str> = stripped
2661 .iter()
2662 .map(String::as_str)
2663 .filter(|s| !s.is_empty())
2664 .collect();
2665 if non_empty.is_empty() {
2666 return stripped.join("\n");
2667 }
2668 let min_indent = non_empty
2669 .iter()
2670 .map(|s| s.len() - s.trim_start().len())
2671 .min()
2672 .unwrap_or(0);
2673 let out: Vec<String> = stripped
2674 .iter()
2675 .map(|s| {
2676 if s.is_empty() {
2677 s.clone()
2678 } else {
2679 let n = (s.len() - s.trim_start().len()).min(min_indent);
2680 s[n..].to_string()
2681 }
2682 })
2683 .collect();
2684 out.join("\n")
2685}
2686
2687fn apply_xmode(stderr: &str, xmode: XMode) -> String {
2688 let lines: Vec<&str> = stderr.lines().collect();
2689 match xmode {
2690 XMode::Plain => lines.first().map(|s| (*s).to_string()).unwrap_or_default(),
2691 XMode::Context => lines.iter().take(5).cloned().collect::<Vec<_>>().join("\n"),
2692 XMode::Verbose => stderr.to_string(),
2693 }
2694}
2695
2696fn render_outcome(outcome: &ExecutionOutcome, xmode: XMode) {
2698 if !outcome.stdout.is_empty() {
2699 print!("{}", ensure_trailing_newline(&outcome.stdout));
2700 }
2701 if !outcome.stderr.is_empty() {
2702 let formatted =
2703 output::format_stderr(&outcome.language, &outcome.stderr, outcome.success());
2704 let trimmed = apply_xmode(&formatted, xmode);
2705 if !trimmed.is_empty() {
2706 eprint!("\x1b[31m{}\x1b[0m", ensure_trailing_newline(&trimmed));
2707 }
2708 }
2709
2710 let millis = outcome.duration.as_millis();
2711 if let Some(code) = outcome.exit_code
2712 && code != 0
2713 {
2714 println!("\x1b[2m[exit {code}] {}\x1b[0m", format_duration(millis));
2715 return;
2716 }
2717
2718 if millis > 0 {
2720 println!("\x1b[2m{}\x1b[0m", format_duration(millis));
2721 }
2722}
2723
2724fn format_duration(millis: u128) -> String {
2725 if millis >= 60_000 {
2726 let mins = millis / 60_000;
2727 let secs = (millis % 60_000) / 1000;
2728 format!("{mins}m {secs}s")
2729 } else if millis >= 1000 {
2730 let secs = millis as f64 / 1000.0;
2731 format!("{secs:.2}s")
2732 } else {
2733 format!("{millis}ms")
2734 }
2735}
2736
2737fn ensure_trailing_newline(text: &str) -> String {
2738 if text.ends_with('\n') {
2739 text.to_string()
2740 } else {
2741 let mut owned = text.to_string();
2742 owned.push('\n');
2743 owned
2744 }
2745}
2746
2747fn run_editor(path: &Path) -> Result<()> {
2749 let editor = std::env::var("EDITOR").unwrap_or_else(|_| {
2750 #[cfg(unix)]
2751 let default = "vi";
2752 #[cfg(windows)]
2753 let default = "notepad";
2754 default.to_string()
2755 });
2756 let path_str = path.to_string_lossy();
2757 let status = Command::new(&editor)
2758 .arg(path_str.as_ref())
2759 .stdin(std::process::Stdio::inherit())
2760 .stdout(std::process::Stdio::inherit())
2761 .stderr(std::process::Stdio::inherit())
2762 .status()
2763 .context("run editor")?;
2764 if !status.success() {
2765 bail!("editor exited with {}", status);
2766 }
2767 Ok(())
2768}
2769
2770fn edit_temp_file() -> Result<PathBuf> {
2772 let tmp = tempfile::NamedTempFile::new().context("create temp file for :edit")?;
2773 let path = tmp.path().to_path_buf();
2774 run_editor(&path)?;
2775 std::mem::forget(tmp);
2776 Ok(path)
2777}
2778
2779fn fetch_url_to_temp(url: &str) -> Result<PathBuf> {
2781 let tmp = tempfile::NamedTempFile::new().context("create temp file for :load url")?;
2782 let path = tmp.path().to_path_buf();
2783
2784 #[cfg(unix)]
2785 let ok = Command::new("curl")
2786 .args(["-sSL", "-o", path.to_str().unwrap_or(""), url])
2787 .status()
2788 .map(|s| s.success())
2789 .unwrap_or(false);
2790
2791 #[cfg(windows)]
2792 let ok = Command::new("curl")
2793 .args(["-sSL", "-o", path.to_str().unwrap_or(""), url])
2794 .status()
2795 .map(|s| s.success())
2796 .unwrap_or_else(|_| {
2797 let ps = format!(
2799 "Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing",
2800 url.replace('\'', "''"),
2801 path.to_string_lossy().replace('\'', "''")
2802 );
2803 Command::new("powershell")
2804 .args(["-NoProfile", "-Command", &ps])
2805 .status()
2806 .map(|s| s.success())
2807 .unwrap_or(false)
2808 });
2809
2810 if !ok {
2811 let _ = tmp.close();
2812 bail!("fetch failed (curl or download failed)");
2813 }
2814 std::mem::forget(tmp);
2815 Ok(path)
2816}
2817
2818fn run_shell(cmd: &str, capture: bool) {
2820 #[cfg(unix)]
2821 let mut c = Command::new("sh");
2822 #[cfg(unix)]
2823 c.arg("-c").arg(cmd);
2824
2825 #[cfg(windows)]
2826 let mut c = {
2827 let shell = std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string());
2828 let mut com = Command::new(shell);
2829 com.arg("/c").arg(cmd);
2830 com
2831 };
2832
2833 if capture {
2834 match c
2835 .stdout(std::process::Stdio::piped())
2836 .stderr(std::process::Stdio::piped())
2837 .output()
2838 {
2839 Ok(out) => {
2840 let _ = std::io::stdout().write_all(&out.stdout);
2841 let _ = std::io::stderr().write_all(&out.stderr);
2842 if let Some(code) = out.status.code()
2843 && code != 0
2844 {
2845 println!("\x1b[2m[exit {code}]\x1b[0m");
2846 }
2847 }
2848 Err(e) => {
2849 eprintln!("\x1b[31m[run]\x1b[0m shell: {e}");
2850 }
2851 }
2852 } else {
2853 c.stdin(std::process::Stdio::inherit())
2854 .stdout(std::process::Stdio::inherit())
2855 .stderr(std::process::Stdio::inherit());
2856 if let Err(e) = c.status() {
2857 eprintln!("\x1b[31m[run]\x1b[0m shell: {e}");
2858 }
2859 }
2860}
2861
2862fn parse_history_range(s: &str, len: usize) -> (usize, usize) {
2864 if s.contains('-') {
2865 let (a, b) = s.split_once('-').unwrap_or((s, ""));
2866 let a = a.trim();
2867 let b = b.trim();
2868 if a.is_empty() && b.is_empty() {
2869 return (len.saturating_sub(25), len);
2870 }
2871 if b.is_empty() {
2872 if let Ok(n) = a.parse::<usize>() {
2874 let start = n.saturating_sub(1).min(len);
2875 return (start, len);
2876 }
2877 } else if a.is_empty() {
2878 if let Ok(n) = b.parse::<usize>() {
2880 let start = len.saturating_sub(n);
2881 return (start, len);
2882 }
2883 } else if let (Ok(lo), Ok(hi)) = (a.parse::<usize>(), b.parse::<usize>()) {
2884 let start = lo.saturating_sub(1).min(len);
2886 let end = hi.min(len).max(start);
2887 return (start, end);
2888 }
2889 } else if let Ok(n) = s.parse::<usize>() {
2890 let start = len.saturating_sub(n);
2892 return (start, len);
2893 }
2894 (len.saturating_sub(25), len)
2895}
2896
2897fn history_path(language_id: &str) -> Option<PathBuf> {
2898 dirs::data_local_dir().map(|dir| {
2899 dir.join("run-kit")
2900 .join(format!("history_{language_id}.txt"))
2901 })
2902}
2903
2904fn bookmarks_path() -> Option<PathBuf> {
2905 if let Ok(home) = std::env::var("HOME") {
2906 return Some(Path::new(&home).join(BOOKMARKS_FILE));
2907 }
2908 #[cfg(windows)]
2909 if let Ok(home) = std::env::var("USERPROFILE") {
2910 return Some(Path::new(&home).join(BOOKMARKS_FILE));
2911 }
2912 None
2913}
2914
2915fn load_bookmarks() -> Result<HashMap<String, PathBuf>> {
2916 let path = bookmarks_path().context("no home dir for bookmarks")?;
2917 let content = match std::fs::read_to_string(&path) {
2918 Ok(c) => c,
2919 Err(_) => return Ok(HashMap::new()),
2920 };
2921 let mut out = HashMap::new();
2922 for line in content.lines() {
2923 let line = line.trim();
2924 if line.is_empty() || line.starts_with('#') {
2925 continue;
2926 }
2927 if let Some((name, rest)) = line.split_once('\t') {
2928 let name = name.trim().to_string();
2929 let p = PathBuf::from(rest.trim());
2930 if !name.is_empty() && p.is_absolute() {
2931 out.insert(name, p);
2932 }
2933 }
2934 }
2935 Ok(out)
2936}
2937
2938fn save_bookmarks(bookmarks: &HashMap<String, PathBuf>) -> Result<()> {
2939 let path = bookmarks_path().context("no home dir for bookmarks")?;
2940 let mut lines: Vec<String> = bookmarks
2941 .iter()
2942 .map(|(k, v)| format!("{}\t{}", k, v.display()))
2943 .collect();
2944 lines.sort();
2945 std::fs::write(path, lines.join("\n") + "\n")?;
2946 Ok(())
2947}
2948
2949fn repl_config_path() -> Option<PathBuf> {
2950 #[cfg(unix)]
2951 if let Ok(home) = std::env::var("HOME") {
2952 return Some(PathBuf::from(&home).join(REPL_CONFIG_FILE));
2953 }
2954 #[cfg(windows)]
2955 if let Ok(home) = std::env::var("USERPROFILE") {
2956 return Some(PathBuf::from(&home).join(REPL_CONFIG_FILE));
2957 }
2958 None
2959}
2960
2961fn load_repl_config() -> Result<HashMap<String, String>> {
2962 let path = repl_config_path().context("no home dir for REPL config")?;
2963 let content = match std::fs::read_to_string(&path) {
2964 Ok(c) => c,
2965 Err(_) => return Ok(HashMap::new()),
2966 };
2967 let mut out = HashMap::new();
2968 for line in content.lines() {
2969 let line = line.trim();
2970 if line.is_empty() || line.starts_with('#') {
2971 continue;
2972 }
2973 if let Some((k, v)) = line.split_once('=') {
2974 let k = k.trim().to_lowercase();
2975 let v = v.trim().trim_matches('"').to_string();
2976 if !k.is_empty() {
2977 out.insert(k, v);
2978 }
2979 }
2980 }
2981 Ok(out)
2982}
2983
2984fn save_repl_config(cfg: &HashMap<String, String>) -> Result<()> {
2985 let path = repl_config_path().context("no home dir for REPL config")?;
2986 let mut keys: Vec<_> = cfg.keys().collect();
2987 keys.sort();
2988 let lines: Vec<String> = keys
2989 .iter()
2990 .map(|k| {
2991 let v = cfg.get(*k).unwrap();
2992 format!("{}={}", k, v)
2993 })
2994 .collect();
2995 std::fs::write(path, lines.join("\n") + "\n")?;
2996 Ok(())
2997}
2998
2999fn extract_defined_names(code: &str, language_id: &str) -> Vec<String> {
3001 let mut names = Vec::new();
3002 for line in code.lines() {
3003 let trimmed = line.trim();
3004 match language_id {
3005 "python" | "py" | "python3" | "py3" => {
3006 if let Some(rest) = trimmed.strip_prefix("def ") {
3008 if let Some(name) = rest.split('(').next() {
3009 let n = name.trim();
3010 if !n.is_empty() {
3011 names.push(n.to_string());
3012 }
3013 }
3014 } else if let Some(rest) = trimmed.strip_prefix("class ") {
3015 let name = rest.split(['(', ':']).next().unwrap_or("").trim();
3016 if !name.is_empty() {
3017 names.push(name.to_string());
3018 }
3019 } else if let Some(rest) = trimmed.strip_prefix("import ") {
3020 for part in rest.split(',') {
3021 let name = if let Some(alias) = part.split(" as ").nth(1) {
3022 alias.trim()
3023 } else {
3024 part.trim().split('.').next_back().unwrap_or("")
3025 };
3026 if !name.is_empty() {
3027 names.push(name.to_string());
3028 }
3029 }
3030 } else if trimmed.starts_with("from ") && trimmed.contains("import ") {
3031 if let Some(imports) = trimmed.split("import ").nth(1) {
3032 for part in imports.split(',') {
3033 let name = if let Some(alias) = part.split(" as ").nth(1) {
3034 alias.trim()
3035 } else {
3036 part.trim()
3037 };
3038 if !name.is_empty() {
3039 names.push(name.to_string());
3040 }
3041 }
3042 }
3043 } else if let Some(eq_pos) = trimmed.find('=') {
3044 let lhs = &trimmed[..eq_pos];
3045 if !lhs.contains('(')
3046 && !lhs.contains('[')
3047 && !trimmed[eq_pos..].starts_with("==")
3048 {
3049 for part in lhs.split(',') {
3050 let name = part.trim().split(':').next().unwrap_or("").trim();
3051 if !name.is_empty()
3052 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
3053 {
3054 names.push(name.to_string());
3055 }
3056 }
3057 }
3058 }
3059 }
3060 "javascript" | "js" | "node" | "typescript" | "ts" => {
3061 for prefix in ["let ", "const ", "var "] {
3063 if let Some(rest) = trimmed.strip_prefix(prefix) {
3064 let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
3065 if !name.is_empty() {
3066 names.push(name.to_string());
3067 }
3068 }
3069 }
3070 if let Some(rest) = trimmed.strip_prefix("function ") {
3071 let name = rest.split('(').next().unwrap_or("").trim();
3072 if !name.is_empty() {
3073 names.push(name.to_string());
3074 }
3075 } else if let Some(rest) = trimmed.strip_prefix("class ") {
3076 let name = rest.split(['{', ' ']).next().unwrap_or("").trim();
3077 if !name.is_empty() {
3078 names.push(name.to_string());
3079 }
3080 }
3081 }
3082 "rust" | "rs" => {
3083 for prefix in ["let ", "let mut "] {
3084 if let Some(rest) = trimmed.strip_prefix(prefix) {
3085 let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
3086 if !name.is_empty() {
3087 names.push(name.to_string());
3088 }
3089 }
3090 }
3091 if let Some(rest) = trimmed.strip_prefix("fn ") {
3092 let name = rest.split(['(', '<']).next().unwrap_or("").trim();
3093 if !name.is_empty() {
3094 names.push(name.to_string());
3095 }
3096 } else if let Some(rest) = trimmed.strip_prefix("struct ") {
3097 let name = rest.split(['{', '(', '<', ' ']).next().unwrap_or("").trim();
3098 if !name.is_empty() {
3099 names.push(name.to_string());
3100 }
3101 }
3102 }
3103 _ => {
3104 if let Some(eq_pos) = trimmed.find('=') {
3106 let lhs = trimmed[..eq_pos].trim();
3107 if !lhs.is_empty()
3108 && !trimmed[eq_pos..].starts_with("==")
3109 && lhs
3110 .chars()
3111 .all(|c| c.is_alphanumeric() || c == '_' || c == ' ')
3112 && let Some(name) = lhs.split_whitespace().last()
3113 {
3114 names.push(name.to_string());
3115 }
3116 }
3117 }
3118 }
3119 }
3120 names
3121}
3122
3123#[cfg(test)]
3124mod tests {
3125 use super::*;
3126
3127 #[test]
3128 fn language_aliases_resolve_in_registry() {
3129 let registry = LanguageRegistry::bootstrap();
3130 let aliases = [
3131 "python",
3132 "py",
3133 "python3",
3134 "rust",
3135 "rs",
3136 "go",
3137 "golang",
3138 "csharp",
3139 "cs",
3140 "c#",
3141 "typescript",
3142 "ts",
3143 "javascript",
3144 "js",
3145 "node",
3146 "ruby",
3147 "rb",
3148 "lua",
3149 "bash",
3150 "sh",
3151 "zsh",
3152 "java",
3153 "php",
3154 "kotlin",
3155 "kt",
3156 "c",
3157 "cpp",
3158 "c++",
3159 "swift",
3160 "swiftlang",
3161 "perl",
3162 "pl",
3163 "julia",
3164 "jl",
3165 ];
3166
3167 for alias in aliases {
3168 let spec = LanguageSpec::new(alias);
3169 assert!(
3170 registry.resolve(&spec).is_some(),
3171 "alias {alias} should resolve to a registered language"
3172 );
3173 }
3174 }
3175
3176 #[test]
3177 fn python_multiline_def_requires_blank_line_to_execute() {
3178 let mut p = PendingInput::new();
3179 p.push_line("def fib(n):");
3180 assert!(p.needs_more_input("python"));
3181 p.push_line(" return n");
3182 assert!(p.needs_more_input("python"));
3183 p.push_line(""); assert!(!p.needs_more_input("python"));
3185 }
3186
3187 #[test]
3188 fn python_dict_literal_colon_does_not_trigger_block() {
3189 let mut p = PendingInput::new();
3190 p.push_line("x = {'key': 'value'}");
3191 assert!(
3192 !p.needs_more_input("python"),
3193 "dict literal should not trigger multi-line"
3194 );
3195 }
3196
3197 #[test]
3198 fn python_class_block_needs_body() {
3199 let mut p = PendingInput::new();
3200 p.push_line("class Foo:");
3201 assert!(p.needs_more_input("python"));
3202 p.push_line(" pass");
3203 assert!(p.needs_more_input("python")); p.push_line(""); assert!(!p.needs_more_input("python"));
3206 }
3207
3208 #[test]
3209 fn python_if_block_with_dedented_body_is_complete() {
3210 let mut p = PendingInput::new();
3211 p.push_line("if True:");
3212 assert!(p.needs_more_input("python"));
3213 p.push_line(" print('yes')");
3214 assert!(p.needs_more_input("python"));
3215 p.push_line(""); assert!(!p.needs_more_input("python"));
3217 }
3218
3219 #[test]
3220 fn python_auto_indents_first_line_after_colon_header() {
3221 let mut p = PendingInput::new();
3222 p.push_line("def cool():");
3223 p.push_line_auto("python", r#"print("ok")"#);
3224 let code = p.take();
3225 assert!(
3226 code.contains(" print(\"ok\")\n"),
3227 "expected auto-indented print line, got:\n{code}"
3228 );
3229 }
3230
3231 #[test]
3232 fn python_auto_indents_nested_blocks() {
3233 let mut p = PendingInput::new();
3234 p.push_line("def bubble_sort(arr):");
3235 p.push_line_auto("python", "n = len(arr)");
3236 p.push_line_auto("python", "for i in range(n):");
3237 p.push_line_auto("python", "for j in range(0, n-i-1):");
3238 p.push_line_auto("python", "if arr[j] > arr[j+1]:");
3239 p.push_line_auto("python", "arr[j], arr[j+1] = arr[j+1], arr[j]");
3240 let code = p.take();
3241 assert!(
3242 code.contains(" n = len(arr)\n"),
3243 "missing indent for len"
3244 );
3245 assert!(
3246 code.contains(" for i in range(n):\n"),
3247 "missing indent for outer loop"
3248 );
3249 assert!(
3250 code.contains(" for j in range(0, n-i-1):\n"),
3251 "missing indent for inner loop"
3252 );
3253 assert!(
3254 code.contains(" if arr[j] > arr[j+1]:\n"),
3255 "missing indent for if"
3256 );
3257 assert!(
3258 code.contains(" arr[j], arr[j+1] = arr[j+1], arr[j]\n"),
3259 "missing indent for swap"
3260 );
3261 }
3262
3263 #[test]
3264 fn python_auto_dedents_else_like_blocks() {
3265 let mut p = PendingInput::new();
3266 p.push_line("def f():");
3267 p.push_line_auto("python", "if True:");
3268 p.push_line_auto("python", "print('yes')");
3269 p.push_line_auto("python", "else:");
3270 let code = p.take();
3271 assert!(
3272 code.contains(" else:\n"),
3273 "expected else to align with if block:\n{code}"
3274 );
3275 }
3276
3277 #[test]
3278 fn python_auto_dedents_return_to_def() {
3279 let mut p = PendingInput::new();
3280 p.push_line("def bubble_sort(arr):");
3281 p.push_line_auto("python", "for i in range(3):");
3282 p.push_line_auto("python", "for j in range(2):");
3283 p.push_line_auto("python", "if j:");
3284 p.push_line_auto("python", "pass");
3285 p.push_line_auto("python", "return arr");
3286 let code = p.take();
3287 assert!(
3288 code.contains(" return arr\n"),
3289 "expected return to align with def block:\n{code}"
3290 );
3291 }
3292
3293 #[test]
3294 fn generic_multiline_tracks_unclosed_delimiters() {
3295 let mut p = PendingInput::new();
3296 p.push_line("func(");
3297 assert!(p.needs_more_input("csharp"));
3298 p.push_line(")");
3299 assert!(!p.needs_more_input("csharp"));
3300 }
3301
3302 #[test]
3303 fn generic_multiline_tracks_trailing_equals() {
3304 let mut p = PendingInput::new();
3305 p.push_line("let x =");
3306 assert!(p.needs_more_input("rust"));
3307 p.push_line("10;");
3308 assert!(!p.needs_more_input("rust"));
3309 }
3310
3311 #[test]
3312 fn generic_multiline_tracks_trailing_dot() {
3313 let mut p = PendingInput::new();
3314 p.push_line("foo.");
3315 assert!(p.needs_more_input("csharp"));
3316 p.push_line("Bar()");
3317 assert!(!p.needs_more_input("csharp"));
3318 }
3319
3320 #[test]
3321 fn generic_multiline_accepts_preprocessor_lines() {
3322 let mut p = PendingInput::new();
3323 p.push_line("#include <stdio.h>");
3324 assert!(
3325 !p.needs_more_input("c"),
3326 "preprocessor lines should not force continuation"
3327 );
3328 }
3329}