1use super::types::{ExtendedReplCommand, ReplCommand, ReplEvent, ReplInputKind};
6
7#[allow(missing_docs)]
13pub fn is_complete(input: &str) -> bool {
14 let mut paren_depth: i32 = 0;
15 let mut bracket_depth: i32 = 0;
16 let mut brace_depth: i32 = 0;
17 for ch in input.chars() {
18 match ch {
19 '(' => paren_depth += 1,
20 ')' => {
21 paren_depth -= 1;
22 if paren_depth < 0 {
23 return false;
24 }
25 }
26 '[' => bracket_depth += 1,
27 ']' => {
28 bracket_depth -= 1;
29 if bracket_depth < 0 {
30 return false;
31 }
32 }
33 '{' => brace_depth += 1,
34 '}' => {
35 brace_depth -= 1;
36 if brace_depth < 0 {
37 return false;
38 }
39 }
40 _ => {}
41 }
42 }
43 paren_depth == 0 && bracket_depth == 0 && brace_depth == 0
44}
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use crate::repl_parser::*;
49 #[test]
50 fn test_parse_quit() {
51 let parser = ReplParser::new(":quit".to_string());
52 let cmd = parser.parse().expect("parsing should succeed");
53 assert_eq!(cmd, ReplCommand::Quit);
54 }
55 #[test]
56 fn test_parse_quit_short() {
57 let parser = ReplParser::new(":q".to_string());
58 let cmd = parser.parse().expect("parsing should succeed");
59 assert_eq!(cmd, ReplCommand::Quit);
60 }
61 #[test]
62 fn test_parse_help() {
63 let parser = ReplParser::new(":help".to_string());
64 let cmd = parser.parse().expect("parsing should succeed");
65 assert_eq!(cmd, ReplCommand::Help);
66 }
67 #[test]
68 fn test_parse_show_env() {
69 let parser = ReplParser::new(":env".to_string());
70 let cmd = parser.parse().expect("parsing should succeed");
71 assert_eq!(cmd, ReplCommand::ShowEnv);
72 }
73 #[test]
74 fn test_parse_clear() {
75 let parser = ReplParser::new(":clear".to_string());
76 let cmd = parser.parse().expect("parsing should succeed");
77 assert_eq!(cmd, ReplCommand::Clear);
78 }
79 #[test]
80 fn test_parse_load() {
81 let parser = ReplParser::new(":load test.lean".to_string());
82 let cmd = parser.parse().expect("parsing should succeed");
83 assert!(matches!(cmd, ReplCommand::Load(_)));
84 }
85 #[test]
86 fn test_is_complete_balanced() {
87 assert!(is_complete("(1 + 2)"));
88 assert!(is_complete("{x : Nat}"));
89 assert!(is_complete("[1, 2, 3]"));
90 assert!(is_complete("simp only [h1, h2]"));
91 }
92 #[test]
93 fn test_is_complete_unbalanced() {
94 assert!(!is_complete("(1 + 2"));
95 assert!(!is_complete("{x : Nat"));
96 assert!(!is_complete("[1, 2, 3"));
97 }
98 #[test]
99 fn test_is_complete_negative_depth() {
100 assert!(!is_complete(")foo"));
101 assert!(!is_complete("]foo"));
102 assert!(!is_complete("}foo"));
103 }
104 #[test]
105 fn test_parse_eval() {
106 let parser = ReplParser::new("42".to_string());
107 let cmd = parser.parse().expect("parsing should succeed");
108 assert!(matches!(cmd, ReplCommand::Eval(_)));
109 }
110}
111#[allow(missing_docs)]
116pub fn parse_extended_command(
117 cmd: &str,
118 rest: &str,
119) -> Result<Option<ExtendedReplCommand>, String> {
120 match cmd {
121 "set" => {
122 let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect();
123 if parts.len() < 2 {
124 return Err("Usage: :set <option> <value>".to_string());
125 }
126 Ok(Some(ExtendedReplCommand::SetOption(
127 parts[0].trim().to_string(),
128 parts[1].trim().to_string(),
129 )))
130 }
131 "get" => {
132 let name = rest.trim();
133 if name.is_empty() {
134 return Err("Usage: :get <option>".to_string());
135 }
136 Ok(Some(ExtendedReplCommand::GetOption(name.to_string())))
137 }
138 "history" | "hist" => Ok(Some(ExtendedReplCommand::History)),
139 "undo" | "u" => Ok(Some(ExtendedReplCommand::Undo)),
140 "stats" => Ok(Some(ExtendedReplCommand::Stats)),
141 "search" | "s" => {
142 let pattern = rest.trim();
143 if pattern.is_empty() {
144 return Err("Usage: :search <pattern>".to_string());
145 }
146 Ok(Some(ExtendedReplCommand::Search(pattern.to_string())))
147 }
148 "print" | "p" => {
149 let name = rest.trim();
150 if name.is_empty() {
151 return Err("Usage: :print <name>".to_string());
152 }
153 Ok(Some(ExtendedReplCommand::Print(name.to_string())))
154 }
155 _ => Ok(None),
156 }
157}
158#[allow(missing_docs)]
162pub fn complete_command(prefix: &str) -> Vec<String> {
163 let commands = [
164 ":quit", ":q", ":exit", ":help", ":h", ":?", ":env", ":show", ":clear", ":reset", ":type",
165 ":t", ":load", ":l", ":check", ":c", ":set", ":get", ":history", ":hist", ":undo", ":u",
166 ":stats", ":search", ":s", ":print", ":p", ":reduce",
167 ];
168 let mut completions: Vec<String> = commands
169 .iter()
170 .filter(|cmd| cmd.starts_with(prefix))
171 .map(|s| s.to_string())
172 .collect();
173 completions.sort();
174 completions
175}
176#[allow(missing_docs)]
178pub fn help_text() -> &'static str {
179 "OxiLean REPL Commands:
180 <expr> Evaluate an expression
181 theorem/def Elaborate a declaration
182 :type <expr> Show type of expression (:t)
183 :check <decl> Check a declaration (:c)
184 :load <file> Load a file (:l)
185 :env Show environment (:show)
186 :clear Clear environment (:reset)
187 :set <opt> <v> Set a REPL option
188 :get <opt> Get a REPL option value
189 :history Show command history (:hist)
190 :undo Undo last declaration (:u)
191 :stats Show elaboration statistics
192 :search <pat> Search for declarations (:s)
193 :print <name> Print declaration info (:p)
194 :reduce <expr> Print normal form of expr
195 :help Show this help (:h, :?)
196 :quit Exit REPL (:q, :exit)"
197}
198#[allow(missing_docs)]
200pub fn normalize_input(input: &str) -> String {
201 input.split_whitespace().collect::<Vec<_>>().join(" ")
202}
203#[allow(missing_docs)]
205pub fn is_meta_command(input: &str) -> bool {
206 input.trim_start().starts_with(':')
207}
208#[allow(missing_docs)]
210pub fn is_empty_input(input: &str) -> bool {
211 input.trim().is_empty()
212}
213#[allow(missing_docs)]
217pub fn parse_bool(s: &str) -> Result<bool, String> {
218 match s.trim().to_lowercase().as_str() {
219 "true" | "yes" | "1" | "on" => Ok(true),
220 "false" | "no" | "0" | "off" => Ok(false),
221 other => Err(format!("Expected boolean, got '{}'", other)),
222 }
223}
224#[cfg(test)]
225mod extended_repl_tests {
226 use super::*;
227 use crate::repl_parser::*;
228 #[test]
229 fn test_repl_options_default() {
230 let opts = ReplOptions::default();
231 assert!(opts.print_types);
232 assert!(!opts.show_timing);
233 assert_eq!(opts.max_history, 100);
234 }
235 #[test]
236 fn test_repl_options_set_get() {
237 let mut opts = ReplOptions::new();
238 opts.set("timing", "true")
239 .expect("test operation should succeed");
240 assert_eq!(opts.get("timing"), Some("true".to_string()));
241 opts.set("timing", "false")
242 .expect("test operation should succeed");
243 assert_eq!(opts.get("timing"), Some("false".to_string()));
244 }
245 #[test]
246 fn test_repl_options_unknown_option() {
247 let mut opts = ReplOptions::new();
248 let result = opts.set("nonexistent", "value");
249 assert!(result.is_err());
250 }
251 #[test]
252 fn test_command_history_push() {
253 let mut h = CommandHistory::new(10);
254 h.push("foo".to_string());
255 h.push("bar".to_string());
256 assert_eq!(h.len(), 2);
257 }
258 #[test]
259 fn test_command_history_no_duplicates() {
260 let mut h = CommandHistory::new(10);
261 h.push("foo".to_string());
262 h.push("foo".to_string());
263 assert_eq!(h.len(), 1);
264 }
265 #[test]
266 fn test_command_history_search() {
267 let mut h = CommandHistory::new(10);
268 h.push("theorem foo : Nat".to_string());
269 h.push("def bar : Type".to_string());
270 h.push("theorem baz : Prop".to_string());
271 let results = h.search("theorem");
272 assert_eq!(results.len(), 2);
273 }
274 #[test]
275 fn test_multiline_state_complete() {
276 let mut m = MultilineState::new();
277 m.push_line("(1 + 2)");
278 assert!(m.is_complete());
279 }
280 #[test]
281 fn test_multiline_state_incomplete() {
282 let mut m = MultilineState::new();
283 m.push_line("(1 + 2");
284 assert!(!m.is_complete());
285 }
286 #[test]
287 fn test_multiline_state_reset() {
288 let mut m = MultilineState::new();
289 m.push_line("(foo");
290 m.reset();
291 assert_eq!(m.depth(), 0);
292 assert_eq!(m.line_count(), 0);
293 }
294 #[test]
295 fn test_complete_command() {
296 let completions = complete_command(":q");
297 assert!(completions.contains(&":quit".to_string()));
298 assert!(completions.contains(&":q".to_string()));
299 }
300 #[test]
301 fn test_complete_command_help() {
302 let completions = complete_command(":h");
303 assert!(!completions.is_empty());
304 assert!(completions.iter().any(|c| c.starts_with(":h")));
305 }
306 #[test]
307 fn test_parse_bool_true_variants() {
308 assert_eq!(parse_bool("true"), Ok(true));
309 assert_eq!(parse_bool("yes"), Ok(true));
310 assert_eq!(parse_bool("1"), Ok(true));
311 assert_eq!(parse_bool("on"), Ok(true));
312 }
313 #[test]
314 fn test_parse_bool_false_variants() {
315 assert_eq!(parse_bool("false"), Ok(false));
316 assert_eq!(parse_bool("no"), Ok(false));
317 assert_eq!(parse_bool("0"), Ok(false));
318 assert_eq!(parse_bool("off"), Ok(false));
319 }
320 #[test]
321 fn test_parse_bool_invalid() {
322 assert!(parse_bool("maybe").is_err());
323 assert!(parse_bool("").is_err());
324 }
325 #[test]
326 fn test_is_meta_command() {
327 assert!(is_meta_command(":quit"));
328 assert!(is_meta_command(" :help"));
329 assert!(!is_meta_command("42"));
330 assert!(!is_meta_command("def foo"));
331 }
332 #[test]
333 fn test_is_empty_input() {
334 assert!(is_empty_input(""));
335 assert!(is_empty_input(" "));
336 assert!(!is_empty_input("x"));
337 }
338 #[test]
339 fn test_normalize_input() {
340 let n = normalize_input(" foo bar baz ");
341 assert_eq!(n, "foo bar baz");
342 }
343 #[test]
344 fn test_help_text_nonempty() {
345 assert!(!help_text().is_empty());
346 assert!(help_text().contains(":quit"));
347 assert!(help_text().contains(":help"));
348 }
349 #[test]
350 fn test_repl_stats_success_rate() {
351 let mut stats = ReplStats::new();
352 stats.record_success();
353 stats.record_success();
354 stats.record_error();
355 let rate = stats.success_rate();
356 assert!((rate - 2.0 / 3.0).abs() < 1e-10);
357 }
358 #[test]
359 fn test_repl_session_process_quit() {
360 let mut session = ReplSession::new();
361 let cmd = session
362 .process(":quit")
363 .expect("test operation should succeed");
364 assert_eq!(cmd, ReplCommand::Quit);
365 }
366 #[test]
367 fn test_repl_session_history_tracking() {
368 let mut session = ReplSession::new();
369 let _ = session.process(":help");
370 let _ = session.process(":quit");
371 assert_eq!(session.history.len(), 2);
372 }
373 #[test]
374 fn test_parse_extended_set() {
375 let result =
376 parse_extended_command("set", "timing true").expect("test operation should succeed");
377 assert!(matches!(result, Some(ExtendedReplCommand::SetOption(_, _))));
378 }
379 #[test]
380 fn test_parse_extended_history() {
381 let result = parse_extended_command("history", "").expect("test operation should succeed");
382 assert!(matches!(result, Some(ExtendedReplCommand::History)));
383 }
384 #[test]
385 fn test_parse_extended_search() {
386 let result =
387 parse_extended_command("search", "Nat").expect("test operation should succeed");
388 assert!(matches!(result, Some(ExtendedReplCommand::Search(s)) if s == "Nat"));
389 }
390 #[test]
391 fn test_parse_extended_unknown() {
392 let result =
393 parse_extended_command("nonexistent", "").expect("test operation should succeed");
394 assert!(result.is_none());
395 }
396}
397#[allow(missing_docs)]
399pub fn format_prompt(depth: i32, session_num: u64) -> String {
400 if depth > 0 {
401 format!(" {}> ", " ".repeat(depth as usize))
402 } else {
403 format!("oxilean[{}]> ", session_num)
404 }
405}
406#[allow(missing_docs)]
410pub fn highlight_keyword(token: &str, use_color: bool) -> String {
411 if !use_color {
412 return token.to_string();
413 }
414 let keywords = [
415 "theorem",
416 "def",
417 "axiom",
418 "inductive",
419 "fun",
420 "let",
421 "in",
422 "match",
423 "with",
424 ];
425 if keywords.contains(&token) {
426 format!("\x1b[1;34m{}\x1b[0m", token)
427 } else {
428 token.to_string()
429 }
430}
431#[cfg(test)]
432mod repl_extended_tests {
433 use super::*;
434 use crate::repl_parser::*;
435 #[test]
436 fn test_repl_option_from_name() {
437 assert_eq!(ReplOption::from_name("timing"), Some(ReplOption::Timing));
438 assert_eq!(ReplOption::from_name("verbose"), Some(ReplOption::Verbose));
439 assert_eq!(ReplOption::from_name("unknown"), None);
440 }
441 #[test]
442 fn test_repl_option_name() {
443 assert_eq!(ReplOption::Timing.name(), "timing");
444 assert_eq!(ReplOption::PrettyPrint.name(), "pretty_print");
445 }
446 #[test]
447 fn test_option_store_set_get() {
448 let mut store = OptionStore::new();
449 store.set("timing", "true");
450 assert_eq!(store.get("timing"), Some("true"));
451 assert!(store.has("timing"));
452 }
453 #[test]
454 fn test_option_store_get_bool() {
455 let mut store = OptionStore::new();
456 store.set("verbose", "yes");
457 assert!(store.get_bool("verbose"));
458 assert!(!store.get_bool("timing"));
459 }
460 #[test]
461 fn test_option_store_get_u64() {
462 let mut store = OptionStore::new();
463 store.set("max_lines", "50");
464 assert_eq!(store.get_u64("max_lines", 100), 50);
465 assert_eq!(store.get_u64("other", 42), 42);
466 }
467 #[test]
468 fn test_option_store_remove() {
469 let mut store = OptionStore::new();
470 store.set("k", "v");
471 assert!(store.remove("k"));
472 assert!(!store.has("k"));
473 }
474 #[test]
475 fn test_option_store_len() {
476 let mut store = OptionStore::new();
477 store.set("a", "1");
478 store.set("b", "2");
479 assert_eq!(store.len(), 2);
480 }
481 #[test]
482 fn test_input_splitter_complete() {
483 let mut s = InputSplitter::new();
484 s.push("(1 + 2)");
485 assert!(s.is_complete());
486 }
487 #[test]
488 fn test_input_splitter_incomplete() {
489 let mut s = InputSplitter::new();
490 s.push("(1 + 2");
491 assert!(!s.is_complete());
492 }
493 #[test]
494 fn test_input_splitter_flush() {
495 let mut s = InputSplitter::new();
496 s.push("hello");
497 let out = s.flush();
498 assert_eq!(out, "hello");
499 assert!(s.is_empty());
500 }
501 #[test]
502 fn test_input_splitter_multiline() {
503 let mut s = InputSplitter::new();
504 s.push("fun x =>");
505 s.push(" x + 1");
506 assert_eq!(s.line_count(), 2);
507 let out = s.flush();
508 assert!(out.contains("fun x =>"));
509 assert!(out.contains("x + 1"));
510 }
511 #[test]
512 fn test_format_prompt_toplevel() {
513 let p = format_prompt(0, 1);
514 assert!(p.contains("oxilean[1]"));
515 }
516 #[test]
517 fn test_format_prompt_nested() {
518 let p = format_prompt(2, 5);
519 assert!(p.contains(">"));
520 assert!(!p.contains("oxilean"));
521 }
522 #[test]
523 fn test_highlight_keyword_no_color() {
524 let s = highlight_keyword("theorem", false);
525 assert_eq!(s, "theorem");
526 }
527 #[test]
528 fn test_highlight_keyword_with_color() {
529 let s = highlight_keyword("theorem", true);
530 assert!(s.contains("theorem"));
531 assert!(s.contains("\x1b["));
532 }
533 #[test]
534 fn test_highlight_non_keyword() {
535 let s = highlight_keyword("myFunc", true);
536 assert_eq!(s, "myFunc");
537 }
538}
539#[allow(dead_code)]
541#[allow(missing_docs)]
542pub trait ReplEventListener {
543 fn on_event(&mut self, event: &ReplEvent);
545}
546#[allow(dead_code)]
548#[allow(missing_docs)]
549pub trait InputFilter {
550 fn filter(&self, input: &str) -> String;
552}
553#[allow(dead_code)]
555#[allow(missing_docs)]
556pub fn word_count(input: &str) -> usize {
557 input.split_whitespace().count()
558}
559#[allow(dead_code)]
561#[allow(missing_docs)]
562pub fn is_tactic_block_start(input: &str) -> bool {
563 let trimmed = input.trim();
564 trimmed == "by" || trimmed.starts_with("by ") || trimmed.starts_with("by\t")
565}
566#[allow(dead_code)]
568#[allow(missing_docs)]
569pub fn split_meta_command(cmd: &str) -> (&str, &str) {
570 let parts: Vec<&str> = cmd.splitn(2, char::is_whitespace).collect();
571 let name = parts.first().copied().unwrap_or("");
572 let rest = parts.get(1).copied().unwrap_or("").trim_start();
573 (name, rest)
574}
575#[allow(dead_code)]
577#[allow(missing_docs)]
578pub fn is_safe_command(cmd: &ReplCommand) -> bool {
579 matches!(
580 cmd,
581 ReplCommand::Eval(_) | ReplCommand::Type(_) | ReplCommand::Help | ReplCommand::ShowEnv
582 )
583}
584#[allow(dead_code)]
586#[allow(missing_docs)]
587pub fn command_summary(cmd: &ReplCommand) -> &'static str {
588 match cmd {
589 ReplCommand::Eval(_) => "evaluate expression",
590 ReplCommand::Type(_) => "show type",
591 ReplCommand::Check(_) => "check declaration",
592 ReplCommand::Load(_) => "load file",
593 ReplCommand::ShowEnv => "show environment",
594 ReplCommand::Clear => "clear environment",
595 ReplCommand::Help => "show help",
596 ReplCommand::Quit => "quit REPL",
597 }
598}
599#[allow(dead_code)]
601#[allow(missing_docs)]
602pub fn all_option_names() -> Vec<&'static str> {
603 vec![
604 "timing",
605 "types",
606 "color",
607 "verbose",
608 "history",
609 "max_lines",
610 "pretty_print",
611 ]
612}
613#[allow(dead_code)]
615#[allow(missing_docs)]
616pub fn suggest_option(input: &str) -> Option<&'static str> {
617 let names = all_option_names();
618 names
619 .into_iter()
620 .min_by_key(|name| simple_distance(input, name))
621 .filter(|name| simple_distance(input, name) <= 3)
622}
623#[allow(dead_code)]
625pub(super) fn simple_distance(a: &str, b: &str) -> usize {
626 let a: Vec<char> = a.chars().collect();
627 let b: Vec<char> = b.chars().collect();
628 let m = a.len();
629 let n = b.len();
630 let mut dp = vec![vec![0usize; n + 1]; m + 1];
631 for (i, row) in dp.iter_mut().enumerate().take(m + 1) {
632 row[0] = i;
633 }
634 for (j, cell) in dp[0].iter_mut().enumerate().take(n + 1) {
635 *cell = j;
636 }
637 for i in 1..=m {
638 for j in 1..=n {
639 if a[i - 1] == b[j - 1] {
640 dp[i][j] = dp[i - 1][j - 1];
641 } else {
642 dp[i][j] = 1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1]);
643 }
644 }
645 }
646 dp[m][n]
647}
648#[cfg(test)]
649mod repl_extra_tests {
650 use super::*;
651 use crate::repl_parser::*;
652 #[test]
653 fn test_repl_mode_name() {
654 assert_eq!(ReplMode::Normal.name(), "normal");
655 assert_eq!(ReplMode::Tactic.name(), "tactic");
656 }
657 #[test]
658 fn test_repl_mode_from_name() {
659 assert_eq!(ReplMode::from_name("tactic"), Some(ReplMode::Tactic));
660 assert_eq!(ReplMode::from_name("unknown"), None);
661 }
662 #[test]
663 fn test_repl_mode_display() {
664 assert_eq!(format!("{}", ReplMode::Search), "search");
665 }
666 #[test]
667 fn test_repl_event_display() {
668 let e = ReplEvent::Parsed("foo".to_string());
669 let s = format!("{}", e);
670 assert!(s.contains("foo"));
671 }
672 #[test]
673 fn test_event_log_listener() {
674 let mut log = EventLog::new();
675 log.on_event(&ReplEvent::Reset);
676 log.on_event(&ReplEvent::Exit);
677 assert_eq!(log.len(), 2);
678 }
679 #[test]
680 fn test_event_log_clear() {
681 let mut log = EventLog::new();
682 log.on_event(&ReplEvent::Reset);
683 log.clear();
684 assert!(log.is_empty());
685 }
686 #[test]
687 fn test_command_alias_new() {
688 let a = CommandAlias::new("q", ":quit");
689 assert_eq!(a.from, "q");
690 assert_eq!(a.to, ":quit");
691 }
692 #[test]
693 fn test_alias_registry_expand() {
694 let mut reg = AliasRegistry::new();
695 reg.register("quit", ":quit");
696 assert_eq!(reg.expand("quit"), ":quit");
697 assert_eq!(reg.expand("help"), "help");
698 }
699 #[test]
700 fn test_alias_registry_names() {
701 let mut reg = AliasRegistry::new();
702 reg.register("a", "b");
703 reg.register("c", "d");
704 let names = reg.names();
705 assert!(names.contains(&"a"));
706 assert!(names.contains(&"c"));
707 }
708 #[test]
709 fn test_strip_semicolon_filter() {
710 let f = StripSemicolonFilter;
711 assert_eq!(f.filter("foo;"), "foo");
712 assert_eq!(f.filter("foo"), "foo");
713 }
714 #[test]
715 fn test_lowercase_command_filter() {
716 let f = LowercaseCommandFilter;
717 let out = f.filter(":QUIT");
718 assert!(out.starts_with(":quit"));
719 }
720 #[test]
721 fn test_filter_pipeline_apply() {
722 let mut pipeline = FilterPipeline::new();
723 pipeline.add(StripSemicolonFilter);
724 let result = pipeline.apply("foo;");
725 assert_eq!(result, "foo");
726 }
727 #[test]
728 fn test_filter_pipeline_len() {
729 let mut pipeline = FilterPipeline::new();
730 assert_eq!(pipeline.len(), 0);
731 pipeline.add(StripSemicolonFilter);
732 assert_eq!(pipeline.len(), 1);
733 }
734 #[test]
735 fn test_repl_completer_complete() {
736 let completer = ReplCompleter::new();
737 let results = completer.complete(":q");
738 assert!(!results.is_empty());
739 assert!(results.iter().any(|c| c.text == ":quit"));
740 }
741 #[test]
742 fn test_repl_completer_add() {
743 let mut completer = ReplCompleter::new();
744 let initial = completer.len();
745 completer.add(CompletionItem::new(
746 "myCmd",
747 "custom",
748 CompletionKind::Command,
749 ));
750 assert_eq!(completer.len(), initial + 1);
751 }
752 #[test]
753 fn test_repl_formatter_success() {
754 let fmt = ReplFormatter::new(false, 80);
755 let s = fmt.success("all good");
756 assert!(s.contains("all good"));
757 }
758 #[test]
759 fn test_repl_formatter_error() {
760 let fmt = ReplFormatter::new(false, 80);
761 let s = fmt.error("bad input");
762 assert!(s.contains("bad input"));
763 }
764 #[test]
765 fn test_repl_formatter_truncate() {
766 let fmt = ReplFormatter::new(false, 5);
767 let s = fmt.truncate("hello world");
768 assert!(s.len() <= 6);
769 }
770 #[test]
771 fn test_repl_formatter_list() {
772 let fmt = ReplFormatter::new(false, 80);
773 let items = vec!["foo".to_string(), "bar".to_string()];
774 let s = fmt.list(&items);
775 assert!(s.contains("foo"));
776 assert!(s.contains("bar"));
777 }
778 #[test]
779 fn test_configurable_repl_parser_normalize() {
780 let config = ReplParserConfig::new();
781 let p = ConfigurableReplParser::new(config);
782 let out = p.preprocess(" foo bar ");
783 assert_eq!(out, " foo bar ");
784 }
785 #[test]
786 fn test_configurable_repl_parser_with_normalize() {
787 let config = ReplParserConfig {
788 normalize_whitespace: true,
789 ..Default::default()
790 };
791 let p = ConfigurableReplParser::new(config);
792 let out = p.preprocess(" foo bar ");
793 assert_eq!(out, "foo bar");
794 }
795 #[test]
796 fn test_configurable_repl_parser_strip_comments() {
797 let config = ReplParserConfig {
798 strip_comments: true,
799 ..Default::default()
800 };
801 let p = ConfigurableReplParser::new(config);
802 let out = p.preprocess("def foo -- this is a comment");
803 assert!(!out.contains("comment"));
804 }
805 #[test]
806 fn test_command_tally_record() {
807 let mut tally = CommandTally::new();
808 tally.record(&ReplCommand::Help);
809 tally.record(&ReplCommand::Quit);
810 assert_eq!(tally.helps, 1);
811 assert_eq!(tally.quits, 1);
812 assert_eq!(tally.total(), 2);
813 }
814 #[test]
815 fn test_word_count() {
816 assert_eq!(word_count("hello world foo"), 3);
817 assert_eq!(word_count(""), 0);
818 assert_eq!(word_count(" one "), 1);
819 }
820 #[test]
821 fn test_is_tactic_block_start() {
822 assert!(is_tactic_block_start("by"));
823 assert!(is_tactic_block_start(" by intro h"));
824 assert!(!is_tactic_block_start("theorem foo"));
825 }
826 #[test]
827 fn test_split_meta_command() {
828 let (cmd, rest) = split_meta_command("set timing true");
829 assert_eq!(cmd, "set");
830 assert_eq!(rest, "timing true");
831 }
832 #[test]
833 fn test_split_meta_command_no_rest() {
834 let (cmd, rest) = split_meta_command("history");
835 assert_eq!(cmd, "history");
836 assert_eq!(rest, "");
837 }
838 #[test]
839 fn test_is_safe_command() {
840 assert!(is_safe_command(&ReplCommand::Help));
841 assert!(is_safe_command(&ReplCommand::ShowEnv));
842 assert!(!is_safe_command(&ReplCommand::Clear));
843 assert!(!is_safe_command(&ReplCommand::Quit));
844 }
845 #[test]
846 fn test_command_summary() {
847 assert_eq!(command_summary(&ReplCommand::Help), "show help");
848 assert_eq!(command_summary(&ReplCommand::Quit), "quit REPL");
849 }
850 #[test]
851 fn test_suggest_option_close() {
852 let s = suggest_option("timng");
853 assert!(s.is_some());
854 assert_eq!(s.expect("test operation should succeed"), "timing");
855 }
856 #[test]
857 fn test_suggest_option_no_match() {
858 let s = suggest_option("zzzzzzz");
859 let _ = s;
860 }
861 #[test]
862 fn test_all_option_names_nonempty() {
863 let names = all_option_names();
864 assert!(!names.is_empty());
865 assert!(names.contains(&"timing"));
866 assert!(names.contains(&"verbose"));
867 }
868 #[test]
869 fn test_repl_session_default() {
870 let session = ReplSession::default();
871 assert!(session.history.is_empty());
872 assert_eq!(session.stats.commands_run, 0);
873 }
874 #[test]
875 fn test_option_store_keys() {
876 let mut store = OptionStore::new();
877 store.set("a", "1");
878 store.set("b", "2");
879 let keys = store.keys();
880 assert_eq!(keys.len(), 2);
881 }
882 #[test]
883 fn test_command_history_prev() {
884 let mut h = CommandHistory::new(10);
885 h.push("first".to_string());
886 h.push("second".to_string());
887 let prev = h.prev();
888 assert_eq!(prev, Some("second"));
889 }
890 #[test]
891 fn test_command_history_max() {
892 let mut h = CommandHistory::new(2);
893 h.push("a".to_string());
894 h.push("b".to_string());
895 h.push("c".to_string());
896 assert_eq!(h.len(), 2);
897 assert_eq!(h.entries()[0], "b");
898 }
899 #[test]
900 fn test_simple_distance_equal() {
901 assert_eq!(simple_distance("hello", "hello"), 0);
902 }
903 #[test]
904 fn test_simple_distance_insert() {
905 assert_eq!(simple_distance("", "a"), 1);
906 }
907 #[test]
908 fn test_simple_distance_substitute() {
909 assert_eq!(simple_distance("cat", "bat"), 1);
910 }
911}
912#[cfg(test)]
913mod repl_history_tests {
914 use super::*;
915 use crate::repl_parser::*;
916 #[test]
917 fn test_repl_history() {
918 let mut h = ReplHistory::new();
919 h.push("#check Nat", true);
920 h.push("bad input!", false);
921 assert_eq!(h.len(), 2);
922 assert!(h.entries[0].parse_ok);
923 assert!(!h.entries[1].parse_ok);
924 }
925}
926#[allow(dead_code)]
928#[allow(missing_docs)]
929pub fn classify_repl_input(input: &str) -> ReplInputKind {
930 let trimmed = input.trim();
931 if trimmed.starts_with("def ")
932 || trimmed.starts_with("theorem ")
933 || trimmed.starts_with("lemma ")
934 {
935 ReplInputKind::Definition
936 } else if trimmed.starts_with('#') {
937 ReplInputKind::Command
938 } else if trimmed.starts_with("by ") || trimmed.ends_with(":= by") {
939 ReplInputKind::Tactic
940 } else if trimmed.starts_with("fun ")
941 || trimmed.starts_with("let ")
942 || trimmed.starts_with("match ")
943 {
944 ReplInputKind::Term
945 } else if trimmed.ends_with('(') || trimmed.ends_with(',') || trimmed.ends_with("->") {
946 ReplInputKind::Incomplete
947 } else {
948 ReplInputKind::Term
949 }
950}
951#[cfg(test)]
952mod repl_classify_tests {
953 use super::*;
954 use crate::repl_parser::*;
955 #[test]
956 fn test_classify_repl_input() {
957 assert_eq!(
958 classify_repl_input("def foo := 1"),
959 ReplInputKind::Definition
960 );
961 assert_eq!(classify_repl_input("#check Nat"), ReplInputKind::Command);
962 assert_eq!(classify_repl_input("1 + 2"), ReplInputKind::Term);
963 assert_eq!(classify_repl_input("fun x ->"), ReplInputKind::Term);
964 }
965}
966#[allow(dead_code)]
968#[allow(missing_docs)]
969pub fn is_incomplete_repl_input(s: &str) -> bool {
970 let t = s.trim();
971 t.is_empty() || t.ends_with('(') || t.ends_with(',') || t.ends_with("->") || t.ends_with(':')
972}
973#[cfg(test)]
974mod repl_pad {
975 use super::*;
976 use crate::repl_parser::*;
977 #[test]
978 fn test_is_incomplete_repl_input() {
979 assert!(is_incomplete_repl_input("fun x ->"));
980 assert!(!is_incomplete_repl_input("fun x -> x"));
981 assert!(is_incomplete_repl_input(""));
982 }
983}