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 && let Ok(n) = v.parse::<u32>()
1350 {
1351 state.precision = Some(n.min(32));
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 let Some(stripped) = command.strip_prefix("!!") {
1395 let shell_cmd = stripped.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 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, 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 if let Ok(cwd) = std::env::current_dir() {
1602 println!("{}", cwd.display());
1603 }
1604 return Ok(false);
1605 }
1606 "dhist" => {
1607 let n: usize = parts.next().and_then(|s| s.parse().ok()).unwrap_or(10);
1608 let len = self.dir_stack.len();
1609 let start = len.saturating_sub(n);
1610 if self.dir_stack.is_empty() {
1611 println!("\x1b[2m(no directory history)\x1b[0m");
1612 } else {
1613 for (i, p) in self.dir_stack[start..].iter().enumerate() {
1614 let num = start + i + 1;
1615 println!("\x1b[2m[{num:>2}]\x1b[0m {}", p.display());
1616 }
1617 }
1618 return Ok(false);
1619 }
1620 "env" => {
1621 let a = parts.next();
1622 let b = parts.next();
1623 match (a, b) {
1624 (None, _) => {
1625 let vars: BTreeMap<String, String> = std::env::vars().collect();
1626 for (k, v) in vars {
1627 println!("{k}={v}");
1628 }
1629 }
1630 (Some(var), None) => {
1631 if let Some((k, v)) = var.split_once('=') {
1632 unsafe { std::env::set_var(k, v) };
1633 } else if let Ok(v) = std::env::var(var) {
1634 println!("{v}");
1635 }
1636 }
1637 (Some(var), Some(val)) => {
1638 if val == "=" {
1639 if let Some(v) = parts.next() {
1640 unsafe { std::env::set_var(var, v) };
1641 }
1642 } else if val.starts_with('=') {
1643 unsafe { std::env::set_var(var, val.trim_start_matches('=')) };
1644 } else {
1645 unsafe { std::env::set_var(var, val) };
1646 }
1647 }
1648 }
1649 return Ok(false);
1650 }
1651 "bookmark" => {
1652 let arg = parts.next();
1653 match arg {
1654 Some("-l") => {
1655 if self.bookmarks.is_empty() {
1656 println!("\x1b[2m(no bookmarks)\x1b[0m");
1657 } else {
1658 let mut names: Vec<_> = self.bookmarks.keys().collect();
1659 names.sort();
1660 for name in names {
1661 let path = self.bookmarks.get(name).unwrap();
1662 println!(" {name}\t{}", path.display());
1663 }
1664 }
1665 }
1666 Some("-d") => {
1667 if let Some(name) = parts.next() {
1668 if self.bookmarks.remove(name).is_some() {
1669 let _ = save_bookmarks(&self.bookmarks);
1670 println!("\x1b[2m[removed bookmark '{name}']\x1b[0m");
1671 } else {
1672 println!("\x1b[31m[run]\x1b[0m bookmark '{}' not found", name);
1673 }
1674 } else {
1675 println!("usage: :bookmark -d <name>");
1676 }
1677 }
1678 Some(name) if !name.starts_with('-') => {
1679 let path = parts
1680 .next()
1681 .map(PathBuf::from)
1682 .or_else(|| std::env::current_dir().ok());
1683 if let Some(p) = path {
1684 if p.is_absolute() {
1685 self.bookmarks.insert(name.to_string(), p.clone());
1686 let _ = save_bookmarks(&self.bookmarks);
1687 println!("\x1b[2m[bookmark '{name}' -> {}]\x1b[0m", p.display());
1688 } else {
1689 println!("\x1b[31m[run]\x1b[0m bookmark path must be absolute");
1690 }
1691 } else {
1692 println!("\x1b[31m[run]\x1b[0m could not get current directory");
1693 }
1694 }
1695 _ => {
1696 println!(
1697 "usage: :bookmark <name> [path] | :bookmark -l | :bookmark -d <name>"
1698 );
1699 }
1700 }
1701 return Ok(false);
1702 }
1703 "load" | "run" => {
1704 if let Some(token) = parts.next() {
1705 if let Some(code) = self.macros.get(token) {
1706 self.execute_payload(ExecutionPayload::Inline {
1707 code: code.clone(),
1708 args: Vec::new(),
1709 })?;
1710 } else {
1711 let path = if token.starts_with("http://") || token.starts_with("https://")
1712 {
1713 match fetch_url_to_temp(token) {
1714 Ok(p) => p,
1715 Err(e) => {
1716 println!("\x1b[31m[run]\x1b[0m fetch failed: {e}");
1717 return Ok(false);
1718 }
1719 }
1720 } else {
1721 PathBuf::from(token)
1722 };
1723 self.execute_payload(ExecutionPayload::File {
1724 path,
1725 args: Vec::new(),
1726 })?;
1727 }
1728 } else {
1729 println!("usage: :load <path|url> or :run <macro|path|url>");
1730 }
1731 return Ok(false);
1732 }
1733 "edit" => {
1734 let path = if let Some(token) = parts.next() {
1735 PathBuf::from(token)
1736 } else {
1737 match edit_temp_file() {
1738 Ok(p) => p,
1739 Err(e) => {
1740 println!("\x1b[31m[run]\x1b[0m edit: {e}");
1741 return Ok(false);
1742 }
1743 }
1744 };
1745 if run_editor(path.as_path()).is_err() {
1746 println!("\x1b[31m[run]\x1b[0m editor failed or $EDITOR not set");
1747 return Ok(false);
1748 }
1749 if path.exists() {
1750 self.execute_payload(ExecutionPayload::File {
1751 path,
1752 args: Vec::new(),
1753 })?;
1754 }
1755 return Ok(false);
1756 }
1757 "last" => {
1758 if let Some(ref s) = self.last_stdout {
1759 print!("{}", ensure_trailing_newline(s));
1760 } else {
1761 println!("\x1b[2m(no last output)\x1b[0m");
1762 }
1763 return Ok(false);
1764 }
1765 "logstart" => {
1766 let path = parts.next().map(PathBuf::from).unwrap_or_else(|| {
1767 std::env::current_dir()
1768 .unwrap_or_else(|_| PathBuf::from("."))
1769 .join("run_log.txt")
1770 });
1771 self.log_path = Some(path.clone());
1772 self.log_input(line);
1773 println!("\x1b[2m[logging to {}]\x1b[0m", path.display());
1774 return Ok(false);
1775 }
1776 "logstop" => {
1777 if self.log_path.take().is_some() {
1778 println!("\x1b[2m[logging stopped]\x1b[0m");
1779 } else {
1780 println!("\x1b[2m(not logging)\x1b[0m");
1781 }
1782 return Ok(false);
1783 }
1784 "logstate" => {
1785 if let Some(ref p) = self.log_path {
1786 println!("\x1b[2mlogging: {}\x1b[0m", p.display());
1787 } else {
1788 println!("\x1b[2m(not logging)\x1b[0m");
1789 }
1790 return Ok(false);
1791 }
1792 "macro" => {
1793 let sub = parts.next();
1794 if sub == Some("run") {
1795 if let Some(name) = parts.next() {
1796 if let Some(code) = self.macros.get(name) {
1797 self.execute_payload(ExecutionPayload::Inline {
1798 code: code.clone(),
1799 args: Vec::new(),
1800 })?;
1801 } else {
1802 println!("\x1b[31m[run]\x1b[0m unknown macro: {name}");
1803 }
1804 } else {
1805 println!("usage: :macro run <NAME>");
1806 }
1807 } else if let Some(name) = sub {
1808 let len = self.history_entries.len();
1809 let mut indices: Vec<usize> = Vec::new();
1810 for part in parts {
1811 let (s, e) = parse_history_range(part, len);
1812 for i in s..e {
1813 indices.push(i);
1814 }
1815 }
1816 indices.sort_unstable();
1817 indices.dedup();
1818 let code: String = indices
1819 .into_iter()
1820 .filter_map(|i| self.history_entries.get(i))
1821 .cloned()
1822 .collect::<Vec<_>>()
1823 .join("\n");
1824 if code.is_empty() {
1825 println!("\x1b[31m[run]\x1b[0m no history entries for range");
1826 } else {
1827 self.macros.insert(name.to_string(), code);
1828 println!("\x1b[2m[macro '{name}' saved]\x1b[0m");
1829 }
1830 } else {
1831 println!("usage: :macro <NAME> <range>... or :macro run <NAME>");
1832 }
1833 return Ok(false);
1834 }
1835 "time" => {
1836 let code = parts.collect::<Vec<_>>().join(" ").trim().to_string();
1837 if code.is_empty() {
1838 println!("usage: :time <CODE>");
1839 return Ok(false);
1840 }
1841 let start = Instant::now();
1842 self.execute_payload(ExecutionPayload::Inline {
1843 code,
1844 args: Vec::new(),
1845 })?;
1846 let elapsed = start.elapsed();
1847 println!("\x1b[2m[elapsed: {:?}]\x1b[0m", elapsed);
1848 return Ok(false);
1849 }
1850 "who" => {
1851 let mut names: Vec<_> = self.defined_names.iter().cloned().collect();
1852 names.sort();
1853 if names.is_empty() {
1854 println!("\x1b[2m(no names tracked)\x1b[0m");
1855 } else {
1856 for n in &names {
1857 println!(" {n}");
1858 }
1859 }
1860 return Ok(false);
1861 }
1862 "whos" => {
1863 let pattern = parts.next();
1864 let mut names: Vec<_> = self.defined_names.iter().cloned().collect();
1865 if let Some(pat) = pattern {
1866 names.retain(|n| n.contains(pat));
1867 }
1868 names.sort();
1869 if names.is_empty() {
1870 println!("\x1b[2m(no names tracked)\x1b[0m");
1871 } else {
1872 for n in &names {
1873 println!(" {n}");
1874 }
1875 }
1876 return Ok(false);
1877 }
1878 "xmode" => {
1879 match parts.next().map(|s| s.to_lowercase()) {
1880 Some(ref m) if m == "plain" => self.xmode = XMode::Plain,
1881 Some(ref m) if m == "context" => self.xmode = XMode::Context,
1882 Some(ref m) if m == "verbose" => self.xmode = XMode::Verbose,
1883 _ => {
1884 let current = match self.xmode {
1885 XMode::Plain => "plain",
1886 XMode::Context => "context",
1887 XMode::Verbose => "verbose",
1888 };
1889 println!(
1890 "\x1b[2mexception display: {current} (plain | context | verbose)\x1b[0m"
1891 );
1892 }
1893 }
1894 return Ok(false);
1895 }
1896 "paste" => {
1897 self.paste_buffer = Some(Vec::new());
1898 println!("\x1b[2m[paste mode — type :end or Ctrl-D to execute]\x1b[0m");
1899 return Ok(false);
1900 }
1901 "end" => {
1902 if self.paste_buffer.is_some() {
1903 println!("\x1b[2m[paste done]\x1b[0m");
1905 } else {
1906 println!("\x1b[31m[run]\x1b[0m not in paste mode");
1907 }
1908 return Ok(false);
1909 }
1910 "precision" => {
1911 match parts.next() {
1912 None => match self.precision {
1913 Some(n) => println!("\x1b[2mprecision: {n}\x1b[0m"),
1914 None => println!("\x1b[2mprecision: (default)\x1b[0m"),
1915 },
1916 Some(s) => {
1917 if let Ok(n) = s.parse::<u32>() {
1918 let n = n.min(32);
1919 self.precision = Some(n);
1920 let mut cfg = load_repl_config().unwrap_or_default();
1921 cfg.insert("precision".to_string(), n.to_string());
1922 if save_repl_config(&cfg).is_err() {
1923 println!("\x1b[31m[run]\x1b[0m failed to save config");
1924 } else {
1925 println!("\x1b[2m[precision = {n}]\x1b[0m");
1926 }
1927 } else {
1928 println!("\x1b[31m[run]\x1b[0m precision must be a number (0–32)");
1929 }
1930 }
1931 }
1932 return Ok(false);
1933 }
1934 "config" => {
1935 let key = parts.next().map(|s| s.to_lowercase());
1936 let val = parts.next();
1937 match (key.as_deref(), val) {
1938 (None, _) => {
1939 let detect = if self.detect_enabled { "on" } else { "off" };
1940 let xmode = match self.xmode {
1941 XMode::Plain => "plain",
1942 XMode::Context => "context",
1943 XMode::Verbose => "verbose",
1944 };
1945 let precision = self
1946 .precision
1947 .map(|n| n.to_string())
1948 .unwrap_or_else(|| "default".to_string());
1949 let numbered = if self.numbered_prompts { "on" } else { "off" };
1950 println!("\x1b[2mdetect\t{detect}\x1b[0m");
1951 println!("\x1b[2mxmode\t{xmode}\x1b[0m");
1952 println!("\x1b[2mprecision\t{precision}\x1b[0m");
1953 println!("\x1b[2mnumbered_prompts\t{numbered}\x1b[0m");
1954 }
1955 (Some(k), None) => {
1956 let v: Option<String> = match k {
1957 "detect" => {
1958 Some(if self.detect_enabled { "on" } else { "off" }.to_string())
1959 }
1960 "xmode" => Some(
1961 match self.xmode {
1962 XMode::Plain => "plain",
1963 XMode::Context => "context",
1964 XMode::Verbose => "verbose",
1965 }
1966 .to_string(),
1967 ),
1968 "precision" => Some(
1969 self.precision
1970 .map(|n| n.to_string())
1971 .unwrap_or_else(|| "default".to_string()),
1972 ),
1973 "numbered_prompts" => {
1974 Some(if self.numbered_prompts { "on" } else { "off" }.to_string())
1975 }
1976 _ => None,
1977 };
1978 if let Some(v) = v {
1979 println!("{v}");
1980 } else {
1981 println!("\x1b[31m[run]\x1b[0m unknown config key: {k}");
1982 }
1983 }
1984 (Some(k), Some(v)) => {
1985 let mut cfg = load_repl_config().unwrap_or_default();
1986 match k {
1987 "detect" => {
1988 self.detect_enabled =
1989 matches!(v.to_lowercase().as_str(), "on" | "true" | "1");
1990 cfg.insert("detect".to_string(), v.to_string());
1991 }
1992 "xmode" => {
1993 self.xmode = match v.to_lowercase().as_str() {
1994 "plain" => XMode::Plain,
1995 "context" => XMode::Context,
1996 _ => XMode::Verbose,
1997 };
1998 cfg.insert("xmode".to_string(), v.to_string());
1999 }
2000 "precision" => {
2001 if let Ok(n) = v.parse::<u32>() {
2002 self.precision = Some(n.min(32));
2003 cfg.insert(
2004 "precision".to_string(),
2005 self.precision.unwrap().to_string(),
2006 );
2007 }
2008 }
2009 "numbered_prompts" => {
2010 self.numbered_prompts =
2011 matches!(v.to_lowercase().as_str(), "on" | "true" | "1");
2012 cfg.insert("numbered_prompts".to_string(), v.to_string());
2013 }
2014 _ => {
2015 println!("\x1b[31m[run]\x1b[0m unknown config key: {k}");
2016 return Ok(false);
2017 }
2018 }
2019 if save_repl_config(&cfg).is_err() {
2020 println!("\x1b[31m[run]\x1b[0m failed to save config");
2021 } else {
2022 println!("\x1b[2m[{k} = {}]\x1b[0m", v.trim());
2023 }
2024 }
2025 }
2026 return Ok(false);
2027 }
2028 "save" => {
2029 if let Some(token) = parts.next() {
2030 let path = Path::new(token);
2031 match self.save_session(path) {
2032 Ok(count) => println!(
2033 "\x1b[2m[saved {count} entries to {}]\x1b[0m",
2034 path.display()
2035 ),
2036 Err(e) => println!("error saving session: {e}"),
2037 }
2038 } else {
2039 println!("usage: :save <path>");
2040 }
2041 return Ok(false);
2042 }
2043 "history" => {
2044 let rest: Vec<&str> = parts.collect();
2045 let mut grep_pattern: Option<&str> = None;
2046 let mut out_file: Option<&str> = None;
2047 let mut unique = false;
2048 let mut range_or_limit: Option<String> = None;
2049 let mut i = 0;
2050 while i < rest.len() {
2051 match rest[i] {
2052 "-g" => {
2053 i += 1;
2054 if i < rest.len() {
2055 grep_pattern = Some(rest[i]);
2056 i += 1;
2057 } else {
2058 println!("usage: :history -g <pattern>");
2059 return Ok(false);
2060 }
2061 }
2062 "-f" => {
2063 i += 1;
2064 if i < rest.len() {
2065 out_file = Some(rest[i]);
2066 i += 1;
2067 } else {
2068 println!("usage: :history -f <file>");
2069 return Ok(false);
2070 }
2071 }
2072 "-u" => {
2073 unique = true;
2074 i += 1;
2075 }
2076 _ => {
2077 range_or_limit = Some(rest[i].to_string());
2078 i += 1;
2079 }
2080 }
2081 }
2082 let entries = &self.history_entries;
2083 let len = entries.len();
2084 let (start, end) = if let Some(ref r) = range_or_limit {
2085 parse_history_range(r, len)
2086 } else {
2087 (len.saturating_sub(25), len)
2088 };
2089 let end = end.min(len);
2090 let slice = if start < len && start < end {
2091 &entries[start..end]
2092 } else {
2093 &entries[0..0]
2094 };
2095 let mut selected: Vec<(usize, &String)> = slice
2096 .iter()
2097 .enumerate()
2098 .map(|(i, e)| (start + i + 1, e))
2099 .filter(|(_, e)| grep_pattern.map(|p| e.contains(p)).unwrap_or(true))
2100 .collect();
2101 if unique {
2102 let mut seen = HashSet::new();
2103 selected.retain(|(_, e)| seen.insert((*e).clone()));
2104 }
2105 if let Some(path) = out_file {
2106 let path = Path::new(path);
2107 if let Ok(mut f) = std::fs::OpenOptions::new()
2108 .create(true)
2109 .append(true)
2110 .open(path)
2111 {
2112 use std::io::Write;
2113 for (_, e) in &selected {
2114 let _ = writeln!(f, "{e}");
2115 }
2116 println!(
2117 "\x1b[2m[appended {} entries to {}]\x1b[0m",
2118 selected.len(),
2119 path.display()
2120 );
2121 } else {
2122 println!(
2123 "\x1b[31m[run]\x1b[0m could not open {} for writing",
2124 path.display()
2125 );
2126 }
2127 } else if selected.is_empty() {
2128 println!("\x1b[2m(no history)\x1b[0m");
2129 } else {
2130 for (num, entry) in selected {
2131 let first_line = entry.lines().next().unwrap_or(entry.as_str());
2132 let is_multiline = entry.contains('\n');
2133 if is_multiline {
2134 println!("\x1b[2m[{num:>4}]\x1b[0m {first_line} \x1b[2m(...)\x1b[0m");
2135 } else {
2136 println!("\x1b[2m[{num:>4}]\x1b[0m {entry}");
2137 }
2138 }
2139 }
2140 return Ok(false);
2141 }
2142 "install" => {
2143 if let Some(pkg) = parts.next() {
2144 self.install_package(pkg);
2145 } else {
2146 println!("usage: :install <package>");
2147 }
2148 return Ok(false);
2149 }
2150 "bench" => {
2151 let n: u32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(10);
2152 let code = parts.collect::<Vec<_>>().join(" ");
2153 if code.is_empty() {
2154 println!("usage: :bench [N] <code>");
2155 println!(" Runs <code> N times (default: 10) and reports timing stats.");
2156 } else {
2157 self.bench_code(&code, n)?;
2158 }
2159 return Ok(false);
2160 }
2161 "type" | "which" => {
2162 let lang = &self.current_language;
2163 println!(
2164 "\x1b[1m{}\x1b[0m \x1b[2m({})\x1b[0m",
2165 lang.canonical_id(),
2166 if self.sessions.contains_key(lang.canonical_id()) {
2167 "session active"
2168 } else {
2169 "no session"
2170 }
2171 );
2172 return Ok(false);
2173 }
2174 alias => {
2175 let spec = LanguageSpec::new(alias);
2176 if self.registry.resolve(&spec).is_some() {
2177 self.switch_language(spec)?;
2178 return Ok(false);
2179 }
2180 println!("unknown command: :{alias}. Type :help for help.");
2181 }
2182 }
2183
2184 Ok(false)
2185 }
2186
2187 fn switch_language(&mut self, spec: LanguageSpec) -> Result<()> {
2188 if self.current_language.canonical_id() == spec.canonical_id() {
2189 println!("already using {}", spec.canonical_id());
2190 return Ok(());
2191 }
2192 if self.registry.resolve(&spec).is_none() {
2193 let available = self.registry.known_languages().join(", ");
2194 bail!(
2195 "language '{}' not supported. Available: {available}",
2196 spec.canonical_id()
2197 );
2198 }
2199 self.current_language = spec;
2200 println!("switched to {}", self.current_language.canonical_id());
2201 Ok(())
2202 }
2203
2204 fn reset_current_session(&mut self) {
2205 let key = self.current_language.canonical_id().to_string();
2206 if let Some(mut session) = self.sessions.remove(&key) {
2207 let _ = session.shutdown();
2208 }
2209 }
2210
2211 fn execute_snippet(&mut self, code: &str) -> Result<()> {
2212 if self.detect_enabled
2213 && let Some(detected) = crate::detect::detect_language_from_snippet(code)
2214 && detected != self.current_language.canonical_id()
2215 {
2216 let spec = LanguageSpec::new(detected.to_string());
2217 if self.registry.resolve(&spec).is_some() {
2218 println!(
2219 "[auto-detect] switching {} -> {}",
2220 self.current_language.canonical_id(),
2221 spec.canonical_id()
2222 );
2223 self.current_language = spec;
2224 }
2225 }
2226
2227 self.defined_names.extend(extract_defined_names(
2229 code,
2230 self.current_language.canonical_id(),
2231 ));
2232
2233 let payload = ExecutionPayload::Inline {
2234 code: code.to_string(),
2235 args: Vec::new(),
2236 };
2237 self.execute_payload(payload)
2238 }
2239
2240 fn session_var_names(&self) -> Vec<String> {
2241 self.defined_names.iter().cloned().collect()
2242 }
2243
2244 fn execute_payload(&mut self, payload: ExecutionPayload) -> Result<()> {
2245 let language = self.current_language.clone();
2246 let outcome = match payload {
2247 ExecutionPayload::Inline { code, .. } => {
2248 if self.engine_supports_sessions(&language)? {
2249 self.eval_in_session(&language, &code)?
2250 } else {
2251 let engine = self
2252 .registry
2253 .resolve(&language)
2254 .context("language engine not found")?;
2255 engine.execute(&ExecutionPayload::Inline {
2256 code,
2257 args: Vec::new(),
2258 })?
2259 }
2260 }
2261 ExecutionPayload::File { ref path, .. } => {
2262 if self.engine_supports_sessions(&language)? {
2264 let code = std::fs::read_to_string(path)
2265 .with_context(|| format!("failed to read file: {}", path.display()))?;
2266 println!("\x1b[2m[loaded {}]\x1b[0m", path.display());
2267 self.eval_in_session(&language, &code)?
2268 } else {
2269 let engine = self
2270 .registry
2271 .resolve(&language)
2272 .context("language engine not found")?;
2273 engine.execute(&payload)?
2274 }
2275 }
2276 ExecutionPayload::Stdin { code, .. } => {
2277 if self.engine_supports_sessions(&language)? {
2278 self.eval_in_session(&language, &code)?
2279 } else {
2280 let engine = self
2281 .registry
2282 .resolve(&language)
2283 .context("language engine not found")?;
2284 engine.execute(&ExecutionPayload::Stdin {
2285 code,
2286 args: Vec::new(),
2287 })?
2288 }
2289 }
2290 };
2291 render_outcome(&outcome, self.xmode);
2292 self.last_stdout = Some(outcome.stdout.clone());
2293 self.in_count += 1;
2294 Ok(())
2295 }
2296
2297 fn engine_supports_sessions(&self, language: &LanguageSpec) -> Result<bool> {
2298 Ok(self
2299 .registry
2300 .resolve(language)
2301 .context("language engine not found")?
2302 .supports_sessions())
2303 }
2304
2305 fn eval_in_session(&mut self, language: &LanguageSpec, code: &str) -> Result<ExecutionOutcome> {
2306 use std::collections::hash_map::Entry;
2307 let key = language.canonical_id().to_string();
2308 match self.sessions.entry(key) {
2309 Entry::Occupied(mut entry) => entry.get_mut().eval(code),
2310 Entry::Vacant(entry) => {
2311 let engine = self
2312 .registry
2313 .resolve(language)
2314 .context("language engine not found")?;
2315 let mut session = engine.start_session().with_context(|| {
2316 format!("failed to start {} session", language.canonical_id())
2317 })?;
2318 let outcome = session.eval(code)?;
2319 entry.insert(session);
2320 Ok(outcome)
2321 }
2322 }
2323 }
2324
2325 fn run_introspect(&mut self, expr: &str) -> Result<()> {
2327 let language = self.current_language.clone();
2328 let lang = language.canonical_id();
2329 if lang == "python" {
2330 let code = format!("help({expr})");
2331 let outcome = self.eval_in_session(&language, &code)?;
2332 render_outcome(&outcome, self.xmode);
2333 Ok(())
2334 } else {
2335 println!(
2336 "\x1b[2mIntrospection not available for {lang}. Use :? in a Python session.\x1b[0m"
2337 );
2338 Ok(())
2339 }
2340 }
2341
2342 fn run_debug(&self, code: &str) -> Result<()> {
2344 let lang = self.current_language.canonical_id();
2345 if lang != "python" {
2346 println!(
2347 "\x1b[2mDebug not available for {lang}. Use :debug in a Python session (pdb).\x1b[0m"
2348 );
2349 return Ok(());
2350 }
2351 let mut tmp = tempfile::NamedTempFile::new().context("create temp file for :debug")?;
2352 tmp.as_file_mut()
2353 .write_all(code.as_bytes())
2354 .context("write debug script")?;
2355 let path = tmp.path();
2356 let status = Command::new("python3")
2357 .args(["-m", "pdb", path.to_str().unwrap_or("")])
2358 .stdin(Stdio::inherit())
2359 .stdout(Stdio::inherit())
2360 .stderr(Stdio::inherit())
2361 .status()
2362 .context("run pdb")?;
2363 if !status.success() {
2364 println!("\x1b[2m[pdb exit {status}]\x1b[0m");
2365 }
2366 Ok(())
2367 }
2368
2369 fn print_languages(&self) {
2370 let mut languages = self.registry.known_languages();
2371 languages.sort();
2372 println!("available languages: {}", languages.join(", "));
2373 }
2374
2375 fn print_versions(&self, language: Option<LanguageSpec>) -> Result<()> {
2376 println!("language toolchain versions...\n");
2377
2378 let mut available = 0u32;
2379 let mut missing = 0u32;
2380
2381 let mut languages: Vec<String> = if let Some(lang) = language {
2382 vec![lang.canonical_id().to_string()]
2383 } else {
2384 self.registry
2385 .known_languages()
2386 .into_iter()
2387 .map(|value| value.to_string())
2388 .collect()
2389 };
2390 languages.sort();
2391
2392 for lang_id in &languages {
2393 let spec = LanguageSpec::new(lang_id.to_string());
2394 if let Some(engine) = self.registry.resolve(&spec) {
2395 match engine.toolchain_version() {
2396 Ok(Some(version)) => {
2397 available += 1;
2398 println!(
2399 " [\x1b[32m OK \x1b[0m] {:<14} {} - {}",
2400 engine.display_name(),
2401 lang_id,
2402 version
2403 );
2404 }
2405 Ok(None) => {
2406 available += 1;
2407 println!(
2408 " [\x1b[33m ?? \x1b[0m] {:<14} {} - unknown",
2409 engine.display_name(),
2410 lang_id
2411 );
2412 }
2413 Err(_) => {
2414 missing += 1;
2415 println!(
2416 " [\x1b[31mMISS\x1b[0m] {:<14} {}",
2417 engine.display_name(),
2418 lang_id
2419 );
2420 }
2421 }
2422 }
2423 }
2424
2425 println!();
2426 println!(
2427 " {} available, {} missing, {} total",
2428 available,
2429 missing,
2430 available + missing
2431 );
2432
2433 if missing > 0 {
2434 println!("\n Tip: Install missing toolchains to enable those languages.");
2435 }
2436
2437 Ok(())
2438 }
2439
2440 fn install_package(&self, package: &str) {
2441 let lang_id = self.current_language.canonical_id();
2442 let override_key = format!("RUN_INSTALL_COMMAND_{}", lang_id.to_ascii_uppercase());
2443 let override_value = std::env::var(&override_key).ok();
2444 let Some(mut cmd) = build_install_command(lang_id, package) else {
2445 if override_value.is_some() {
2446 println!(
2447 "Error: {override_key} is set but could not be parsed.\n\
2448 Provide a valid command, e.g. {override_key}=\"uv pip install {{package}}\""
2449 );
2450 return;
2451 }
2452 println!(
2453 "No package manager available for '{lang_id}'.\n\
2454 Tip: set {override_key}=\"<cmd> {{package}}\"",
2455 );
2456 return;
2457 };
2458
2459 println!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
2460
2461 match cmd
2462 .stdin(std::process::Stdio::inherit())
2463 .stdout(std::process::Stdio::inherit())
2464 .stderr(std::process::Stdio::inherit())
2465 .status()
2466 {
2467 Ok(status) if status.success() => {
2468 println!("\x1b[32m[run]\x1b[0m Successfully installed '{package}'");
2469 }
2470 Ok(_) => {
2471 println!("\x1b[31m[run]\x1b[0m Failed to install '{package}'");
2472 }
2473 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
2474 let program = cmd.get_program().to_string_lossy();
2475 println!("\x1b[31m[run]\x1b[0m Package manager not found: {program}");
2476 println!("Tip: install it or set {override_key}=\"<cmd> {{package}}\"");
2477 }
2478 Err(e) => {
2479 println!("\x1b[31m[run]\x1b[0m Error running package manager: {e}");
2480 }
2481 }
2482 }
2483
2484 fn bench_code(&mut self, code: &str, iterations: u32) -> Result<()> {
2485 let language = self.current_language.clone();
2486
2487 let warmup = self.eval_in_session(&language, code)?;
2489 if !warmup.success() {
2490 println!("\x1b[31mError:\x1b[0m Code failed during warmup");
2491 if !warmup.stderr.is_empty() {
2492 print!("{}", warmup.stderr);
2493 }
2494 return Ok(());
2495 }
2496 println!("\x1b[2m warmup: {}ms\x1b[0m", warmup.duration.as_millis());
2497
2498 let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
2499 for i in 0..iterations {
2500 let outcome = self.eval_in_session(&language, code)?;
2501 let ms = outcome.duration.as_secs_f64() * 1000.0;
2502 times.push(ms);
2503 if i < 3 || i == iterations - 1 {
2504 println!("\x1b[2m run {}: {:.2}ms\x1b[0m", i + 1, ms);
2505 }
2506 }
2507
2508 times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2509 let total: f64 = times.iter().sum();
2510 let avg = total / times.len() as f64;
2511 let min = times.first().copied().unwrap_or(0.0);
2512 let max = times.last().copied().unwrap_or(0.0);
2513 let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
2514 (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
2515 } else {
2516 times[times.len() / 2]
2517 };
2518 let variance: f64 =
2519 times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
2520 let stddev = variance.sqrt();
2521
2522 println!();
2523 println!("\x1b[1mResults ({iterations} runs):\x1b[0m");
2524 println!(" min: \x1b[32m{min:.2}ms\x1b[0m");
2525 println!(" max: \x1b[33m{max:.2}ms\x1b[0m");
2526 println!(" avg: \x1b[36m{avg:.2}ms\x1b[0m");
2527 println!(" median: \x1b[36m{median:.2}ms\x1b[0m");
2528 println!(" stddev: {stddev:.2}ms");
2529 Ok(())
2530 }
2531
2532 fn log_input(&self, line: &str) {
2533 if let Some(ref p) = self.log_path {
2534 let _ = std::fs::OpenOptions::new()
2535 .create(true)
2536 .append(true)
2537 .open(p)
2538 .and_then(|mut f| writeln!(f, "{line}"));
2539 }
2540 }
2541
2542 fn save_session(&self, path: &Path) -> Result<usize> {
2543 use std::io::Write;
2544 let mut file = std::fs::File::create(path)
2545 .with_context(|| format!("failed to create {}", path.display()))?;
2546 let count = self.history_entries.len();
2547 for entry in &self.history_entries {
2548 writeln!(file, "{entry}")?;
2549 }
2550 Ok(count)
2551 }
2552
2553 fn print_help(&self) {
2554 println!("\x1b[1mCommands\x1b[0m");
2555 for (name, desc) in CMD_HELP {
2556 println!(" \x1b[36m:{name}\x1b[0m \x1b[2m{desc}\x1b[0m");
2557 }
2558 println!("\x1b[2mLanguage shortcuts: :py, :js, :rs, :go, :cpp, :java, ...\x1b[0m");
2559 println!(
2560 "\x1b[2mIn session languages (e.g. Python), _ is the last expression result.\x1b[0m"
2561 );
2562 }
2563
2564 fn print_cmd_help(cmd_name: &str) {
2565 let key = cmd_name.trim_start_matches(':').trim().to_lowercase();
2566 for (name, desc) in CMD_HELP {
2567 if name.to_lowercase() == key {
2568 println!(" \x1b[36m:{name}\x1b[0m \x1b[2m{desc}\x1b[0m");
2569 return;
2570 }
2571 }
2572 println!("\x1b[31m[run]\x1b[0m unknown command :{cmd_name}");
2573 }
2574
2575 fn print_commands_machine() {
2576 for (name, desc) in CMD_HELP {
2577 println!(":{name}\t{desc}");
2578 }
2579 }
2580
2581 fn print_quickref() {
2582 println!("\x1b[1mQuick reference\x1b[0m");
2583 for (name, desc) in CMD_HELP {
2584 println!(" :{name}\t{desc}");
2585 }
2586 println!(
2587 "\x1b[2m:py :js :rs :go :cpp :java ... In Python (session), _ = last result.\x1b[0m"
2588 );
2589 }
2590
2591 fn shutdown(&mut self) {
2592 for (_, mut session) in self.sessions.drain() {
2593 let _ = session.shutdown();
2594 }
2595 }
2596}
2597
2598fn strip_paste_prompts(lines: &[String]) -> String {
2600 let stripped: Vec<String> = lines
2601 .iter()
2602 .map(|s| {
2603 let t = s.trim_start();
2604 let t = t.strip_prefix(">>>").map(|r| r.trim_start()).unwrap_or(t);
2605 let t = t.strip_prefix("...").map(|r| r.trim_start()).unwrap_or(t);
2606 t.to_string()
2607 })
2608 .collect();
2609 let non_empty: Vec<&str> = stripped
2610 .iter()
2611 .map(String::as_str)
2612 .filter(|s| !s.is_empty())
2613 .collect();
2614 if non_empty.is_empty() {
2615 return stripped.join("\n");
2616 }
2617 let min_indent = non_empty
2618 .iter()
2619 .map(|s| s.len() - s.trim_start().len())
2620 .min()
2621 .unwrap_or(0);
2622 let out: Vec<String> = stripped
2623 .iter()
2624 .map(|s| {
2625 if s.is_empty() {
2626 s.clone()
2627 } else {
2628 let n = (s.len() - s.trim_start().len()).min(min_indent);
2629 s[n..].to_string()
2630 }
2631 })
2632 .collect();
2633 out.join("\n")
2634}
2635
2636fn apply_xmode(stderr: &str, xmode: XMode) -> String {
2637 let lines: Vec<&str> = stderr.lines().collect();
2638 match xmode {
2639 XMode::Plain => lines.first().map(|s| (*s).to_string()).unwrap_or_default(),
2640 XMode::Context => lines.iter().take(5).cloned().collect::<Vec<_>>().join("\n"),
2641 XMode::Verbose => stderr.to_string(),
2642 }
2643}
2644
2645fn render_outcome(outcome: &ExecutionOutcome, xmode: XMode) {
2647 if !outcome.stdout.is_empty() {
2648 print!("{}", ensure_trailing_newline(&outcome.stdout));
2649 }
2650 if !outcome.stderr.is_empty() {
2651 let formatted =
2652 output::format_stderr(&outcome.language, &outcome.stderr, outcome.success());
2653 let trimmed = apply_xmode(&formatted, xmode);
2654 if !trimmed.is_empty() {
2655 eprint!("\x1b[31m{}\x1b[0m", ensure_trailing_newline(&trimmed));
2656 }
2657 }
2658
2659 let millis = outcome.duration.as_millis();
2660 if let Some(code) = outcome.exit_code
2661 && code != 0
2662 {
2663 println!("\x1b[2m[exit {code}] {}\x1b[0m", format_duration(millis));
2664 return;
2665 }
2666
2667 if millis > 0 {
2669 println!("\x1b[2m{}\x1b[0m", format_duration(millis));
2670 }
2671}
2672
2673fn format_duration(millis: u128) -> String {
2674 if millis >= 60_000 {
2675 let mins = millis / 60_000;
2676 let secs = (millis % 60_000) / 1000;
2677 format!("{mins}m {secs}s")
2678 } else if millis >= 1000 {
2679 let secs = millis as f64 / 1000.0;
2680 format!("{secs:.2}s")
2681 } else {
2682 format!("{millis}ms")
2683 }
2684}
2685
2686fn ensure_trailing_newline(text: &str) -> String {
2687 if text.ends_with('\n') {
2688 text.to_string()
2689 } else {
2690 let mut owned = text.to_string();
2691 owned.push('\n');
2692 owned
2693 }
2694}
2695
2696fn run_editor(path: &Path) -> Result<()> {
2698 let editor = std::env::var("EDITOR").unwrap_or_else(|_| {
2699 #[cfg(unix)]
2700 let default = "vi";
2701 #[cfg(windows)]
2702 let default = "notepad";
2703 default.to_string()
2704 });
2705 let path_str = path.to_string_lossy();
2706 let status = Command::new(&editor)
2707 .arg(path_str.as_ref())
2708 .stdin(std::process::Stdio::inherit())
2709 .stdout(std::process::Stdio::inherit())
2710 .stderr(std::process::Stdio::inherit())
2711 .status()
2712 .context("run editor")?;
2713 if !status.success() {
2714 bail!("editor exited with {}", status);
2715 }
2716 Ok(())
2717}
2718
2719fn edit_temp_file() -> Result<PathBuf> {
2721 let tmp = tempfile::NamedTempFile::new().context("create temp file for :edit")?;
2722 let path = tmp.path().to_path_buf();
2723 run_editor(&path)?;
2724 std::mem::forget(tmp);
2725 Ok(path)
2726}
2727
2728fn fetch_url_to_temp(url: &str) -> Result<PathBuf> {
2730 let tmp = tempfile::NamedTempFile::new().context("create temp file for :load url")?;
2731 let path = tmp.path().to_path_buf();
2732
2733 #[cfg(unix)]
2734 let ok = Command::new("curl")
2735 .args(["-sSL", "-o", path.to_str().unwrap_or(""), url])
2736 .status()
2737 .map(|s| s.success())
2738 .unwrap_or(false);
2739
2740 #[cfg(windows)]
2741 let ok = Command::new("curl")
2742 .args(["-sSL", "-o", path.to_str().unwrap_or(""), url])
2743 .status()
2744 .map(|s| s.success())
2745 .unwrap_or_else(|_| {
2746 let ps = format!(
2748 "Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing",
2749 url.replace('\'', "''"),
2750 path.to_string_lossy().replace('\'', "''")
2751 );
2752 Command::new("powershell")
2753 .args(["-NoProfile", "-Command", &ps])
2754 .status()
2755 .map(|s| s.success())
2756 .unwrap_or(false)
2757 });
2758
2759 if !ok {
2760 let _ = tmp.close();
2761 bail!("fetch failed (curl or download failed)");
2762 }
2763 std::mem::forget(tmp);
2764 Ok(path)
2765}
2766
2767fn run_shell(cmd: &str, capture: bool) {
2769 #[cfg(unix)]
2770 let mut c = Command::new("sh");
2771 #[cfg(unix)]
2772 c.arg("-c").arg(cmd);
2773
2774 #[cfg(windows)]
2775 let mut c = {
2776 let shell = std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string());
2777 let mut com = Command::new(shell);
2778 com.arg("/c").arg(cmd);
2779 com
2780 };
2781
2782 if capture {
2783 match c
2784 .stdout(std::process::Stdio::piped())
2785 .stderr(std::process::Stdio::piped())
2786 .output()
2787 {
2788 Ok(out) => {
2789 let _ = std::io::stdout().write_all(&out.stdout);
2790 let _ = std::io::stderr().write_all(&out.stderr);
2791 if let Some(code) = out.status.code()
2792 && code != 0
2793 {
2794 println!("\x1b[2m[exit {code}]\x1b[0m");
2795 }
2796 }
2797 Err(e) => {
2798 eprintln!("\x1b[31m[run]\x1b[0m shell: {e}");
2799 }
2800 }
2801 } else {
2802 c.stdin(std::process::Stdio::inherit())
2803 .stdout(std::process::Stdio::inherit())
2804 .stderr(std::process::Stdio::inherit());
2805 if let Err(e) = c.status() {
2806 eprintln!("\x1b[31m[run]\x1b[0m shell: {e}");
2807 }
2808 }
2809}
2810
2811fn parse_history_range(s: &str, len: usize) -> (usize, usize) {
2813 if s.contains('-') {
2814 let (a, b) = s.split_once('-').unwrap_or((s, ""));
2815 let a = a.trim();
2816 let b = b.trim();
2817 if a.is_empty() && b.is_empty() {
2818 return (len.saturating_sub(25), len);
2819 }
2820 if b.is_empty() {
2821 if let Ok(n) = a.parse::<usize>() {
2823 let start = n.saturating_sub(1).min(len);
2824 return (start, len);
2825 }
2826 } else if a.is_empty() {
2827 if let Ok(n) = b.parse::<usize>() {
2829 let start = len.saturating_sub(n);
2830 return (start, len);
2831 }
2832 } else if let (Ok(lo), Ok(hi)) = (a.parse::<usize>(), b.parse::<usize>()) {
2833 let start = lo.saturating_sub(1).min(len);
2835 let end = hi.min(len).max(start);
2836 return (start, end);
2837 }
2838 } else if let Ok(n) = s.parse::<usize>() {
2839 let start = len.saturating_sub(n);
2841 return (start, len);
2842 }
2843 (len.saturating_sub(25), len)
2844}
2845
2846fn history_path() -> Option<PathBuf> {
2847 if let Ok(home) = std::env::var("HOME") {
2848 return Some(Path::new(&home).join(HISTORY_FILE));
2849 }
2850 #[cfg(windows)]
2851 if let Ok(home) = std::env::var("USERPROFILE") {
2852 return Some(Path::new(&home).join(HISTORY_FILE));
2853 }
2854 None
2855}
2856
2857fn bookmarks_path() -> Option<PathBuf> {
2858 if let Ok(home) = std::env::var("HOME") {
2859 return Some(Path::new(&home).join(BOOKMARKS_FILE));
2860 }
2861 #[cfg(windows)]
2862 if let Ok(home) = std::env::var("USERPROFILE") {
2863 return Some(Path::new(&home).join(BOOKMARKS_FILE));
2864 }
2865 None
2866}
2867
2868fn load_bookmarks() -> Result<HashMap<String, PathBuf>> {
2869 let path = bookmarks_path().context("no home dir for bookmarks")?;
2870 let content = match std::fs::read_to_string(&path) {
2871 Ok(c) => c,
2872 Err(_) => return Ok(HashMap::new()),
2873 };
2874 let mut out = HashMap::new();
2875 for line in content.lines() {
2876 let line = line.trim();
2877 if line.is_empty() || line.starts_with('#') {
2878 continue;
2879 }
2880 if let Some((name, rest)) = line.split_once('\t') {
2881 let name = name.trim().to_string();
2882 let p = PathBuf::from(rest.trim());
2883 if !name.is_empty() && p.is_absolute() {
2884 out.insert(name, p);
2885 }
2886 }
2887 }
2888 Ok(out)
2889}
2890
2891fn save_bookmarks(bookmarks: &HashMap<String, PathBuf>) -> Result<()> {
2892 let path = bookmarks_path().context("no home dir for bookmarks")?;
2893 let mut lines: Vec<String> = bookmarks
2894 .iter()
2895 .map(|(k, v)| format!("{}\t{}", k, v.display()))
2896 .collect();
2897 lines.sort();
2898 std::fs::write(path, lines.join("\n") + "\n")?;
2899 Ok(())
2900}
2901
2902fn repl_config_path() -> Option<PathBuf> {
2903 #[cfg(unix)]
2904 if let Ok(home) = std::env::var("HOME") {
2905 return Some(PathBuf::from(&home).join(REPL_CONFIG_FILE));
2906 }
2907 #[cfg(windows)]
2908 if let Ok(home) = std::env::var("USERPROFILE") {
2909 return Some(PathBuf::from(&home).join(REPL_CONFIG_FILE));
2910 }
2911 None
2912}
2913
2914fn load_repl_config() -> Result<HashMap<String, String>> {
2915 let path = repl_config_path().context("no home dir for REPL config")?;
2916 let content = match std::fs::read_to_string(&path) {
2917 Ok(c) => c,
2918 Err(_) => return Ok(HashMap::new()),
2919 };
2920 let mut out = HashMap::new();
2921 for line in content.lines() {
2922 let line = line.trim();
2923 if line.is_empty() || line.starts_with('#') {
2924 continue;
2925 }
2926 if let Some((k, v)) = line.split_once('=') {
2927 let k = k.trim().to_lowercase();
2928 let v = v.trim().trim_matches('"').to_string();
2929 if !k.is_empty() {
2930 out.insert(k, v);
2931 }
2932 }
2933 }
2934 Ok(out)
2935}
2936
2937fn save_repl_config(cfg: &HashMap<String, String>) -> Result<()> {
2938 let path = repl_config_path().context("no home dir for REPL config")?;
2939 let mut keys: Vec<_> = cfg.keys().collect();
2940 keys.sort();
2941 let lines: Vec<String> = keys
2942 .iter()
2943 .map(|k| {
2944 let v = cfg.get(*k).unwrap();
2945 format!("{}={}", k, v)
2946 })
2947 .collect();
2948 std::fs::write(path, lines.join("\n") + "\n")?;
2949 Ok(())
2950}
2951
2952fn extract_defined_names(code: &str, language_id: &str) -> Vec<String> {
2954 let mut names = Vec::new();
2955 for line in code.lines() {
2956 let trimmed = line.trim();
2957 match language_id {
2958 "python" | "py" | "python3" | "py3" => {
2959 if let Some(rest) = trimmed.strip_prefix("def ") {
2961 if let Some(name) = rest.split('(').next() {
2962 let n = name.trim();
2963 if !n.is_empty() {
2964 names.push(n.to_string());
2965 }
2966 }
2967 } else if let Some(rest) = trimmed.strip_prefix("class ") {
2968 let name = rest.split(['(', ':']).next().unwrap_or("").trim();
2969 if !name.is_empty() {
2970 names.push(name.to_string());
2971 }
2972 } else if let Some(rest) = trimmed.strip_prefix("import ") {
2973 for part in rest.split(',') {
2974 let name = if let Some(alias) = part.split(" as ").nth(1) {
2975 alias.trim()
2976 } else {
2977 part.trim().split('.').next_back().unwrap_or("")
2978 };
2979 if !name.is_empty() {
2980 names.push(name.to_string());
2981 }
2982 }
2983 } else if trimmed.starts_with("from ") && trimmed.contains("import ") {
2984 if let Some(imports) = trimmed.split("import ").nth(1) {
2985 for part in imports.split(',') {
2986 let name = if let Some(alias) = part.split(" as ").nth(1) {
2987 alias.trim()
2988 } else {
2989 part.trim()
2990 };
2991 if !name.is_empty() {
2992 names.push(name.to_string());
2993 }
2994 }
2995 }
2996 } else if let Some(eq_pos) = trimmed.find('=') {
2997 let lhs = &trimmed[..eq_pos];
2998 if !lhs.contains('(')
2999 && !lhs.contains('[')
3000 && !trimmed[eq_pos..].starts_with("==")
3001 {
3002 for part in lhs.split(',') {
3003 let name = part.trim().split(':').next().unwrap_or("").trim();
3004 if !name.is_empty()
3005 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
3006 {
3007 names.push(name.to_string());
3008 }
3009 }
3010 }
3011 }
3012 }
3013 "javascript" | "js" | "node" | "typescript" | "ts" => {
3014 for prefix in ["let ", "const ", "var "] {
3016 if let Some(rest) = trimmed.strip_prefix(prefix) {
3017 let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
3018 if !name.is_empty() {
3019 names.push(name.to_string());
3020 }
3021 }
3022 }
3023 if let Some(rest) = trimmed.strip_prefix("function ") {
3024 let name = rest.split('(').next().unwrap_or("").trim();
3025 if !name.is_empty() {
3026 names.push(name.to_string());
3027 }
3028 } else if let Some(rest) = trimmed.strip_prefix("class ") {
3029 let name = rest.split(['{', ' ']).next().unwrap_or("").trim();
3030 if !name.is_empty() {
3031 names.push(name.to_string());
3032 }
3033 }
3034 }
3035 "rust" | "rs" => {
3036 for prefix in ["let ", "let mut "] {
3037 if let Some(rest) = trimmed.strip_prefix(prefix) {
3038 let name = rest.split(['=', ':', ';', ' ']).next().unwrap_or("").trim();
3039 if !name.is_empty() {
3040 names.push(name.to_string());
3041 }
3042 }
3043 }
3044 if let Some(rest) = trimmed.strip_prefix("fn ") {
3045 let name = rest.split(['(', '<']).next().unwrap_or("").trim();
3046 if !name.is_empty() {
3047 names.push(name.to_string());
3048 }
3049 } else if let Some(rest) = trimmed.strip_prefix("struct ") {
3050 let name = rest.split(['{', '(', '<', ' ']).next().unwrap_or("").trim();
3051 if !name.is_empty() {
3052 names.push(name.to_string());
3053 }
3054 }
3055 }
3056 _ => {
3057 if let Some(eq_pos) = trimmed.find('=') {
3059 let lhs = trimmed[..eq_pos].trim();
3060 if !lhs.is_empty()
3061 && !trimmed[eq_pos..].starts_with("==")
3062 && lhs
3063 .chars()
3064 .all(|c| c.is_alphanumeric() || c == '_' || c == ' ')
3065 && let Some(name) = lhs.split_whitespace().last()
3066 {
3067 names.push(name.to_string());
3068 }
3069 }
3070 }
3071 }
3072 }
3073 names
3074}
3075
3076#[cfg(test)]
3077mod tests {
3078 use super::*;
3079
3080 #[test]
3081 fn language_aliases_resolve_in_registry() {
3082 let registry = LanguageRegistry::bootstrap();
3083 let aliases = [
3084 "python",
3085 "py",
3086 "python3",
3087 "rust",
3088 "rs",
3089 "go",
3090 "golang",
3091 "csharp",
3092 "cs",
3093 "c#",
3094 "typescript",
3095 "ts",
3096 "javascript",
3097 "js",
3098 "node",
3099 "ruby",
3100 "rb",
3101 "lua",
3102 "bash",
3103 "sh",
3104 "zsh",
3105 "java",
3106 "php",
3107 "kotlin",
3108 "kt",
3109 "c",
3110 "cpp",
3111 "c++",
3112 "swift",
3113 "swiftlang",
3114 "perl",
3115 "pl",
3116 "julia",
3117 "jl",
3118 ];
3119
3120 for alias in aliases {
3121 let spec = LanguageSpec::new(alias);
3122 assert!(
3123 registry.resolve(&spec).is_some(),
3124 "alias {alias} should resolve to a registered language"
3125 );
3126 }
3127 }
3128
3129 #[test]
3130 fn python_multiline_def_requires_blank_line_to_execute() {
3131 let mut p = PendingInput::new();
3132 p.push_line("def fib(n):");
3133 assert!(p.needs_more_input("python"));
3134 p.push_line(" return n");
3135 assert!(p.needs_more_input("python"));
3136 p.push_line(""); assert!(!p.needs_more_input("python"));
3138 }
3139
3140 #[test]
3141 fn python_dict_literal_colon_does_not_trigger_block() {
3142 let mut p = PendingInput::new();
3143 p.push_line("x = {'key': 'value'}");
3144 assert!(
3145 !p.needs_more_input("python"),
3146 "dict literal should not trigger multi-line"
3147 );
3148 }
3149
3150 #[test]
3151 fn python_class_block_needs_body() {
3152 let mut p = PendingInput::new();
3153 p.push_line("class Foo:");
3154 assert!(p.needs_more_input("python"));
3155 p.push_line(" pass");
3156 assert!(p.needs_more_input("python")); p.push_line(""); assert!(!p.needs_more_input("python"));
3159 }
3160
3161 #[test]
3162 fn python_if_block_with_dedented_body_is_complete() {
3163 let mut p = PendingInput::new();
3164 p.push_line("if True:");
3165 assert!(p.needs_more_input("python"));
3166 p.push_line(" print('yes')");
3167 assert!(p.needs_more_input("python"));
3168 p.push_line(""); assert!(!p.needs_more_input("python"));
3170 }
3171
3172 #[test]
3173 fn python_auto_indents_first_line_after_colon_header() {
3174 let mut p = PendingInput::new();
3175 p.push_line("def cool():");
3176 p.push_line_auto("python", r#"print("ok")"#);
3177 let code = p.take();
3178 assert!(
3179 code.contains(" print(\"ok\")\n"),
3180 "expected auto-indented print line, got:\n{code}"
3181 );
3182 }
3183
3184 #[test]
3185 fn python_auto_indents_nested_blocks() {
3186 let mut p = PendingInput::new();
3187 p.push_line("def bubble_sort(arr):");
3188 p.push_line_auto("python", "n = len(arr)");
3189 p.push_line_auto("python", "for i in range(n):");
3190 p.push_line_auto("python", "for j in range(0, n-i-1):");
3191 p.push_line_auto("python", "if arr[j] > arr[j+1]:");
3192 p.push_line_auto("python", "arr[j], arr[j+1] = arr[j+1], arr[j]");
3193 let code = p.take();
3194 assert!(
3195 code.contains(" n = len(arr)\n"),
3196 "missing indent for len"
3197 );
3198 assert!(
3199 code.contains(" for i in range(n):\n"),
3200 "missing indent for outer loop"
3201 );
3202 assert!(
3203 code.contains(" for j in range(0, n-i-1):\n"),
3204 "missing indent for inner loop"
3205 );
3206 assert!(
3207 code.contains(" if arr[j] > arr[j+1]:\n"),
3208 "missing indent for if"
3209 );
3210 assert!(
3211 code.contains(" arr[j], arr[j+1] = arr[j+1], arr[j]\n"),
3212 "missing indent for swap"
3213 );
3214 }
3215
3216 #[test]
3217 fn python_auto_dedents_else_like_blocks() {
3218 let mut p = PendingInput::new();
3219 p.push_line("def f():");
3220 p.push_line_auto("python", "if True:");
3221 p.push_line_auto("python", "print('yes')");
3222 p.push_line_auto("python", "else:");
3223 let code = p.take();
3224 assert!(
3225 code.contains(" else:\n"),
3226 "expected else to align with if block:\n{code}"
3227 );
3228 }
3229
3230 #[test]
3231 fn python_auto_dedents_return_to_def() {
3232 let mut p = PendingInput::new();
3233 p.push_line("def bubble_sort(arr):");
3234 p.push_line_auto("python", "for i in range(3):");
3235 p.push_line_auto("python", "for j in range(2):");
3236 p.push_line_auto("python", "if j:");
3237 p.push_line_auto("python", "pass");
3238 p.push_line_auto("python", "return arr");
3239 let code = p.take();
3240 assert!(
3241 code.contains(" return arr\n"),
3242 "expected return to align with def block:\n{code}"
3243 );
3244 }
3245
3246 #[test]
3247 fn generic_multiline_tracks_unclosed_delimiters() {
3248 let mut p = PendingInput::new();
3249 p.push_line("func(");
3250 assert!(p.needs_more_input("csharp"));
3251 p.push_line(")");
3252 assert!(!p.needs_more_input("csharp"));
3253 }
3254
3255 #[test]
3256 fn generic_multiline_tracks_trailing_equals() {
3257 let mut p = PendingInput::new();
3258 p.push_line("let x =");
3259 assert!(p.needs_more_input("rust"));
3260 p.push_line("10;");
3261 assert!(!p.needs_more_input("rust"));
3262 }
3263
3264 #[test]
3265 fn generic_multiline_tracks_trailing_dot() {
3266 let mut p = PendingInput::new();
3267 p.push_line("foo.");
3268 assert!(p.needs_more_input("csharp"));
3269 p.push_line("Bar()");
3270 assert!(!p.needs_more_input("csharp"));
3271 }
3272
3273 #[test]
3274 fn generic_multiline_accepts_preprocessor_lines() {
3275 let mut p = PendingInput::new();
3276 p.push_line("#include <stdio.h>");
3277 assert!(
3278 !p.needs_more_input("c"),
3279 "preprocessor lines should not force continuation"
3280 );
3281 }
3282}