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