1#![cfg_attr(allow_unknown_lints, allow(unknown_lints))]
5#![cfg_attr(allow_unknown_lints, allow(renamed_and_removed_lints))]
6#![deny(
8 future_incompatible,
9 nonstandard_style,
10 rust_2018_compatibility,
11 rust_2018_idioms,
12 rust_2021_compatibility,
13 unused,
14 warnings
15)]
16#![deny(
18 absolute_paths_not_starting_with_crate,
19 deprecated_in_future,
20 elided_lifetimes_in_paths,
21 explicit_outlives_requirements,
22 ffi_unwind_calls,
23 keyword_idents,
24 let_underscore_drop,
25 macro_use_extern_crate,
26 meta_variable_misuse,
27 missing_abi,
28 missing_copy_implementations,
29 missing_debug_implementations,
30 missing_docs,
31 non_ascii_idents,
32 noop_method_call,
33 pointer_structural_match,
34 rust_2021_incompatible_closure_captures,
35 rust_2021_incompatible_or_patterns,
36 rust_2021_prefixes_incompatible_syntax,
37 rust_2021_prelude_collisions,
38 single_use_lifetimes,
39 trivial_casts,
40 trivial_numeric_casts,
41 unreachable_pub,
42 unsafe_code,
43 unsafe_op_in_unsafe_fn,
44 unused_crate_dependencies,
45 unused_extern_crates,
46 unused_import_braces,
47 unused_lifetimes,
48 unused_macro_rules,
49 unused_qualifications,
50 unused_results,
51 unused_tuple_struct_fields,
52 variant_size_differences
53)]
54#![deny(clippy::all, clippy::cargo, clippy::pedantic, clippy::restriction)]
56#![cfg_attr(include_nightly_lints, deny(clippy::nursery))]
57#![allow(
58 clippy::arithmetic_side_effects,
59 clippy::arithmetic_side_effects,
60 clippy::blanket_clippy_restriction_lints,
61 clippy::bool_to_int_with_if,
62 clippy::default_numeric_fallback,
63 clippy::else_if_without_else,
64 clippy::expect_used,
65 clippy::float_arithmetic,
66 clippy::implicit_return,
67 clippy::indexing_slicing,
68 clippy::map_err_ignore,
69 clippy::missing_docs_in_private_items,
70 clippy::missing_trait_methods,
71 clippy::mod_module_files,
72 clippy::module_name_repetitions,
73 clippy::new_without_default,
74 clippy::non_ascii_literal,
75 clippy::option_if_let_else,
76 clippy::pub_use,
77 clippy::question_mark_used,
78 clippy::redundant_pub_crate,
79 clippy::ref_patterns,
80 clippy::std_instead_of_alloc,
81 clippy::std_instead_of_core,
82 clippy::tabs_in_doc_comments,
83 clippy::tests_outside_test_module,
84 clippy::too_many_lines,
85 clippy::unwrap_used
86)]
87#![deny(
88 rustdoc::bare_urls,
89 rustdoc::broken_intra_doc_links,
90 rustdoc::invalid_codeblock_attributes,
91 rustdoc::invalid_html_tags,
92 rustdoc::missing_crate_level_docs,
93 rustdoc::private_doc_tests,
94 rustdoc::private_intra_doc_links
95)]
96#![cfg_attr(
98 test,
99 allow(
100 let_underscore_drop,
101 clippy::cognitive_complexity,
102 clippy::let_underscore_must_use,
103 clippy::let_underscore_untyped,
104 clippy::needless_pass_by_value,
105 clippy::panic,
106 clippy::shadow_reuse,
107 clippy::shadow_unrelated,
108 clippy::undocumented_unsafe_blocks,
109 clippy::unimplemented,
110 clippy::unreachable
111 )
112)]
113#![cfg_attr(
115 include_nightly_lints,
116 allow(
117 clippy::arc_with_non_send_sync,
118 clippy::min_ident_chars,
119 clippy::needless_raw_strings,
120 clippy::pub_with_shorthand,
121 clippy::redundant_closure_call,
122 clippy::single_call_fn
123 )
124)]
125mod action;
133mod edit_content;
134pub mod errors;
135mod history;
136mod line;
137mod line_parser;
138mod search;
139#[cfg(not(tarpaulin_include))]
140pub mod testutil;
141mod utils;
142
143use std::{
144 fs::{read_to_string, File},
145 io::Write,
146 path::{Path, PathBuf},
147 slice::Iter,
148};
149
150pub use version_track::Version;
151
152pub use self::{action::Action, edit_content::EditContext, line::Line, search::Search};
153use self::{
154 history::{History, HistoryItem},
155 utils::{remove_range, swap_range_down, swap_range_up},
156};
157use crate::errors::{FileReadErrorCause, IoError};
158
159#[derive(Debug)]
161pub struct TodoFile {
162 comment_char: String,
163 filepath: PathBuf,
164 history: History,
165 is_noop: bool,
166 lines: Vec<Line>,
167 selected_line_index: usize,
168 version: Version,
169}
170
171impl TodoFile {
172 #[must_use]
174 #[inline]
175 pub fn new<Path: AsRef<std::path::Path>>(path: Path, undo_limit: u32, comment_char: &str) -> Self {
176 Self {
177 comment_char: String::from(comment_char),
178 filepath: PathBuf::from(path.as_ref()),
179 history: History::new(undo_limit),
180 lines: vec![],
181 is_noop: false,
182 selected_line_index: 0,
183 version: Version::new(),
184 }
185 }
186
187 #[inline]
189 pub fn set_lines(&mut self, lines: Vec<Line>) {
190 self.is_noop = !lines.is_empty() && lines[0].get_action() == &Action::Noop;
191 self.lines = if self.is_noop {
192 vec![]
193 }
194 else {
195 lines.into_iter().filter(|l| l.get_action() != &Action::Noop).collect()
196 };
197 if self.selected_line_index >= self.lines.len() {
198 self.selected_line_index = if self.lines.is_empty() { 0 } else { self.lines.len() - 1 };
199 }
200 self.version.reset();
201 self.history.reset();
202 }
203
204 #[inline]
210 pub fn load_file(&mut self) -> Result<(), IoError> {
211 let lines: Result<Vec<Line>, IoError> = read_to_string(self.filepath.as_path())
212 .map_err(|err| {
213 IoError::FileRead {
214 file: self.filepath.clone(),
215 cause: FileReadErrorCause::from(err),
216 }
217 })?
218 .lines()
219 .filter_map(|l| {
220 if l.starts_with(self.comment_char.as_str()) || l.is_empty() {
221 None
222 }
223 else {
224 Some(Line::new(l).map_err(|err| {
225 IoError::FileRead {
226 file: self.filepath.clone(),
227 cause: FileReadErrorCause::from(err),
228 }
229 }))
230 }
231 })
232 .collect();
233 self.set_lines(lines?);
234 Ok(())
235 }
236
237 #[inline]
242 pub fn write_file(&self) -> Result<(), IoError> {
243 let mut file = File::create(&self.filepath).map_err(|err| {
244 IoError::FileRead {
245 file: self.filepath.clone(),
246 cause: FileReadErrorCause::from(err),
247 }
248 })?;
249 let file_contents = if self.is_noop {
250 String::from("noop")
251 }
252 else {
253 self.lines.iter().map(Line::to_text).collect::<Vec<String>>().join("\n")
254 };
255 writeln!(file, "{file_contents}").map_err(|err| {
256 IoError::FileRead {
257 file: self.filepath.clone(),
258 cause: FileReadErrorCause::from(err),
259 }
260 })?;
261 Ok(())
262 }
263
264 #[inline]
266 pub fn set_selected_line_index(&mut self, selected_line_index: usize) -> usize {
267 self.selected_line_index = if self.lines.is_empty() {
268 0
269 }
270 else if selected_line_index >= self.lines.len() {
271 self.lines.len() - 1
272 }
273 else {
274 selected_line_index
275 };
276 self.selected_line_index
277 }
278
279 #[inline]
281 pub fn swap_range_up(&mut self, start_index: usize, end_index: usize) -> bool {
282 if end_index == 0 || start_index == 0 || self.lines.is_empty() {
283 return false;
284 }
285
286 let max_index = self.lines.len() - 1;
287 let end = if end_index > max_index { max_index } else { end_index };
288 let start = if start_index > max_index {
289 max_index
290 }
291 else {
292 start_index
293 };
294
295 swap_range_up(&mut self.lines, start, end);
296 self.version.increment();
297 self.history.record(HistoryItem::new_swap_up(start, end));
298 true
299 }
300
301 #[inline]
303 pub fn swap_range_down(&mut self, start_index: usize, end_index: usize) -> bool {
304 let len = self.lines.len();
305 let max_index = if len == 0 { 0 } else { len - 1 };
306
307 if end_index == max_index || start_index == max_index {
308 return false;
309 }
310
311 swap_range_down(&mut self.lines, start_index, end_index);
312 self.version.increment();
313 self.history.record(HistoryItem::new_swap_down(start_index, end_index));
314 true
315 }
316
317 #[inline]
319 pub fn add_line(&mut self, index: usize, line: Line) {
320 let i = if index > self.lines.len() {
321 self.lines.len()
322 }
323 else {
324 index
325 };
326 self.lines.insert(i, line);
327 self.version.increment();
328 self.history.record(HistoryItem::new_add(i, i));
329 }
330
331 #[inline]
333 pub fn remove_lines(&mut self, start_index: usize, end_index: usize) {
334 if self.lines.is_empty() {
335 return;
336 }
337
338 let max_index = self.lines.len() - 1;
339 let end = if end_index > max_index { max_index } else { end_index };
340 let start = if start_index > max_index {
341 max_index
342 }
343 else {
344 start_index
345 };
346
347 let removed_lines = remove_range(&mut self.lines, start, end);
348 self.version.increment();
349 self.history.record(HistoryItem::new_remove(start, end, removed_lines));
350 }
351
352 #[inline]
354 pub fn update_range(&mut self, start_index: usize, end_index: usize, edit_context: &EditContext) {
355 if self.lines.is_empty() {
356 return;
357 }
358
359 let max_index = self.lines.len() - 1;
360 let end = if end_index > max_index { max_index } else { end_index };
361 let start = if start_index > max_index {
362 max_index
363 }
364 else {
365 start_index
366 };
367
368 let range = if end <= start { end..=start } else { start..=end };
369
370 let mut lines = vec![];
371 for index in range {
372 let line = &mut self.lines[index];
373 lines.push(line.clone());
374 if let Some(action) = edit_context.get_action() {
375 line.set_action(action);
376 }
377
378 if let Some(content) = edit_context.get_content() {
379 line.edit_content(content);
380 }
381
382 if let Some(option) = edit_context.get_option() {
383 line.toggle_option(option);
384 }
385 }
386 self.version.increment();
387 self.history.record(HistoryItem::new_modify(start, end, lines));
388 }
389
390 #[inline]
392 pub fn undo(&mut self) -> Option<(usize, usize)> {
393 self.version.increment();
394 self.history.undo(&mut self.lines)
395 }
396
397 #[inline]
399 pub fn redo(&mut self) -> Option<(usize, usize)> {
400 self.version.increment();
401 self.history.redo(&mut self.lines)
402 }
403
404 #[must_use]
406 #[inline]
407 pub const fn version(&self) -> &Version {
408 &self.version
409 }
410
411 #[must_use]
413 #[inline]
414 pub fn get_selected_line(&self) -> Option<&Line> {
415 self.lines.get(self.selected_line_index)
416 }
417
418 #[must_use]
420 #[inline]
421 pub fn get_max_selected_line_index(&self) -> usize {
422 let len = self.lines.len();
423 if len == 0 { 0 } else { len - 1 }
424 }
425
426 #[must_use]
428 #[inline]
429 pub const fn get_selected_line_index(&self) -> usize {
430 self.selected_line_index
431 }
432
433 #[must_use]
435 #[inline]
436 pub fn get_filepath(&self) -> &Path {
437 self.filepath.as_path()
438 }
439
440 #[must_use]
442 #[inline]
443 pub fn get_line(&self, index: usize) -> Option<&Line> {
444 self.lines.get(index)
445 }
446
447 #[must_use]
449 #[inline]
450 pub fn get_lines_owned(&self) -> Vec<Line> {
451 self.lines.clone()
452 }
453
454 #[must_use]
456 #[inline]
457 pub const fn is_noop(&self) -> bool {
458 self.is_noop
459 }
460
461 #[inline]
463 pub fn lines_iter(&self) -> Iter<'_, Line> {
464 self.lines.iter()
465 }
466
467 #[must_use]
469 #[inline]
470 pub fn is_empty(&self) -> bool {
471 self.lines.is_empty()
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use claim::{assert_none, assert_some_eq};
478 use tempfile::{Builder, NamedTempFile};
479 use testutils::{assert_empty, assert_not_empty};
480
481 use super::*;
482
483 fn create_line(line: &str) -> Line {
484 Line::new(line).unwrap()
485 }
486
487 fn create_and_load_todo_file(file_contents: &[&str]) -> (TodoFile, NamedTempFile) {
488 let todo_file_path = Builder::new()
489 .prefix("git-rebase-todo-scratch")
490 .suffix("")
491 .tempfile()
492 .unwrap();
493 write!(todo_file_path.as_file(), "{}", file_contents.join("\n")).unwrap();
494 let mut todo_file = TodoFile::new(todo_file_path.path().to_str().unwrap(), 1, "#");
495 todo_file.load_file().unwrap();
496 (todo_file, todo_file_path)
497 }
498
499 macro_rules! assert_read_todo_file {
500 ($todo_file_path:expr, $($arg:expr),*) => {
501 let expected = [$( $arg, )*];
502 let content = read_to_string(Path::new($todo_file_path)).unwrap();
503 pretty_assertions::assert_str_eq!(content, format!("{}\n", expected.join("\n")));
504 };
505 }
506
507 macro_rules! assert_todo_lines {
508 ($todo_file_path:expr, $($arg:expr),*) => {
509 let actual_lines = $todo_file_path.get_lines_owned();
510
511 let expected = vec![$( create_line($arg), )*];
512 pretty_assertions::assert_str_eq!(
513 actual_lines.iter().map(Line::to_text).collect::<Vec<String>>().join("\n"),
514 expected.iter().map(Line::to_text).collect::<Vec<String>>().join("\n")
515 );
516 };
517 }
518
519 #[test]
520 fn load_file() {
521 let (todo_file, _) = create_and_load_todo_file(&["pick aaa foobar"]);
522 assert_todo_lines!(todo_file, "pick aaa foobar");
523 assert_ne!(todo_file.version(), &Version::new());
524 }
525
526 #[test]
527 fn load_noop_file() {
528 let (todo_file, _) = create_and_load_todo_file(&["noop"]);
529 assert_empty!(todo_file);
530 assert!(todo_file.is_noop());
531 }
532
533 #[test]
534 fn load_ignore_comments() {
535 let (todo_file, _) = create_and_load_todo_file(&["# pick aaa comment", "pick aaa foo", "# pick aaa comment"]);
536 assert_todo_lines!(todo_file, "pick aaa foo");
537 }
538
539 #[test]
540 fn load_ignore_newlines() {
541 let (todo_file, _) = create_and_load_todo_file(&["", "pick aaa foobar", ""]);
542 assert_todo_lines!(todo_file, "pick aaa foobar");
543 }
544
545 #[test]
546 fn set_lines() {
547 let (mut todo_file, _) = create_and_load_todo_file(&[]);
548 let old_version = todo_file.version;
549 todo_file.set_lines(vec![create_line("pick bbb comment")]);
550 assert_todo_lines!(todo_file, "pick bbb comment");
551 assert_ne!(todo_file.version(), &old_version);
552 }
553
554 #[test]
555 fn set_lines_reset_history() {
556 let (mut todo_file, _) = create_and_load_todo_file(&[]);
557 todo_file.history.record(HistoryItem::new_add(1, 1));
558 todo_file.set_lines(vec![create_line("pick bbb comment")]);
559 assert_none!(todo_file.undo());
560 }
561
562 #[test]
563 fn set_lines_reset_selected_index() {
564 let (mut todo_file, _) = create_and_load_todo_file(&["pick a a", "pick b b", "pick c c"]);
565 todo_file.selected_line_index = 2;
566 todo_file.set_lines(vec![create_line("pick a a"), create_line("pick b b")]);
567 assert_eq!(todo_file.selected_line_index, 1);
568 }
569
570 #[test]
571 fn set_lines_reset_selected_index_empty_lis() {
572 let (mut todo_file, _) = create_and_load_todo_file(&["pick a a", "pick b b", "pick c c"]);
573 todo_file.selected_line_index = 2;
574 todo_file.set_lines(vec![]);
575 assert_eq!(todo_file.selected_line_index, 0);
576 }
577
578 #[test]
579 fn write_file() {
580 let (mut todo_file, _) = create_and_load_todo_file(&[]);
581 todo_file.set_lines(vec![create_line("pick bbb comment")]);
582 todo_file.write_file().unwrap();
583 assert_todo_lines!(todo_file, "pick bbb comment");
584 }
585
586 #[test]
587 fn write_file_noop() {
588 let (mut todo_file, _) = create_and_load_todo_file(&[]);
589 todo_file.set_lines(vec![create_line("noop")]);
590 todo_file.write_file().unwrap();
591 assert_read_todo_file!(todo_file.get_filepath(), "noop");
592 }
593
594 #[test]
595 fn add_line_index_miss() {
596 let (mut todo_file, _) =
597 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
598 todo_file.add_line(100, create_line("fixup ddd comment"));
599 assert_todo_lines!(
600 todo_file,
601 "pick aaa comment",
602 "drop bbb comment",
603 "edit ccc comment",
604 "fixup ddd comment"
605 );
606 }
607
608 #[test]
609 fn add_line() {
610 let (mut todo_file, _) =
611 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
612 let old_version = *todo_file.version();
613 todo_file.add_line(1, create_line("fixup ddd comment"));
614 assert_todo_lines!(
615 todo_file,
616 "pick aaa comment",
617 "fixup ddd comment",
618 "drop bbb comment",
619 "edit ccc comment"
620 );
621 assert_ne!(todo_file.version(), &old_version);
622 }
623
624 #[test]
625 fn add_line_record_history() {
626 let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
627 todo_file.add_line(1, create_line("fixup ddd comment"));
628 let _undo_result = todo_file.undo();
629 assert_todo_lines!(todo_file, "pick aaa comment");
630 }
631
632 #[test]
633 fn remove_lines_index_miss_start() {
634 let (mut todo_file, _) =
635 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
636 todo_file.remove_lines(100, 1);
637 assert_todo_lines!(todo_file, "pick aaa comment");
638 }
639
640 #[test]
641 fn remove_lines_index_miss_end() {
642 let (mut todo_file, _) =
643 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
644 todo_file.remove_lines(1, 100);
645 assert_todo_lines!(todo_file, "pick aaa comment");
646 }
647
648 #[test]
649 fn remove_lines_index_miss_start_and_end() {
650 let (mut todo_file, _) =
651 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
652 todo_file.remove_lines(100, 100);
653 assert_todo_lines!(todo_file, "pick aaa comment", "drop bbb comment");
654 }
655
656 #[test]
657 fn remove_lines() {
658 let (mut todo_file, _) =
659 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
660 let old_version = *todo_file.version();
661 todo_file.remove_lines(1, 1);
662 assert_todo_lines!(todo_file, "pick aaa comment", "edit ccc comment");
663 assert_ne!(todo_file.version(), &old_version);
664 }
665
666 #[test]
667 fn remove_lines_empty_list() {
668 let (mut todo_file, _) = create_and_load_todo_file(&[]);
669 todo_file.remove_lines(1, 1);
670 }
671
672 #[test]
673 fn remove_lines_record_history() {
674 let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "edit ccc comment"]);
675 todo_file.remove_lines(1, 1);
676 let _undo_result = todo_file.undo();
677 assert_todo_lines!(todo_file, "pick aaa comment", "edit ccc comment");
678 }
679
680 #[test]
681 fn update_range_full_set_action() {
682 let (mut todo_file, _) =
683 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
684 let old_version = *todo_file.version();
685 todo_file.update_range(0, 2, &EditContext::new().action(Action::Reword));
686 assert_todo_lines!(
687 todo_file,
688 "reword aaa comment",
689 "reword bbb comment",
690 "reword ccc comment"
691 );
692 assert_ne!(todo_file.version(), &old_version);
693 }
694
695 #[test]
696 fn update_range_full_set_content() {
697 let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
698 todo_file.update_range(0, 2, &EditContext::new().content("echo"));
699 assert_todo_lines!(todo_file, "exec echo", "exec echo", "exec echo");
700 }
701
702 #[test]
703 fn update_range_set_option() {
704 let (mut todo_file, _) = create_and_load_todo_file(&["fixup aaa comment"]);
705 let old_version = *todo_file.version();
706 todo_file.update_range(0, 2, &EditContext::new().option("-c"));
707 assert_todo_lines!(todo_file, "fixup -c aaa comment");
708 assert_ne!(todo_file.version(), &old_version);
709 }
710
711 #[test]
712 fn update_range_reverse_indexes() {
713 let (mut todo_file, _) =
714 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
715 todo_file.update_range(2, 0, &EditContext::new().action(Action::Reword));
716 assert_todo_lines!(
717 todo_file,
718 "reword aaa comment",
719 "reword bbb comment",
720 "reword ccc comment"
721 );
722 }
723
724 #[test]
725 fn update_range_record_history() {
726 let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
727 todo_file.update_range(0, 0, &EditContext::new().action(Action::Reword));
728 let _undo_result = todo_file.undo();
729 assert_todo_lines!(todo_file, "pick aaa comment");
730 }
731
732 #[test]
733 fn update_range_empty_list() {
734 let (mut todo_file, _) = create_and_load_todo_file(&[]);
735 todo_file.update_range(0, 0, &EditContext::new().action(Action::Reword));
736 }
737
738 #[test]
739 fn update_range_start_index_overflow() {
740 let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "pick bbb comment"]);
741 todo_file.update_range(2, 0, &EditContext::new().action(Action::Reword));
742 assert_todo_lines!(todo_file, "reword aaa comment", "reword bbb comment");
743 }
744
745 #[test]
746 fn update_range_end_index_overflow() {
747 let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "pick bbb comment"]);
748 todo_file.update_range(0, 2, &EditContext::new().action(Action::Reword));
749 assert_todo_lines!(todo_file, "reword aaa comment", "reword bbb comment");
750 }
751
752 #[test]
753 fn history_undo_redo() {
754 let (mut todo_file, _) =
755 create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
756 todo_file.update_range(0, 0, &EditContext::new().action(Action::Drop));
757 let old_version = *todo_file.version();
758 let _undo_result = todo_file.undo();
759 assert_todo_lines!(todo_file, "pick aaa comment", "drop bbb comment", "edit ccc comment");
760 assert_ne!(todo_file.version(), &old_version);
761 let old_version = *todo_file.version();
762 _ = todo_file.redo();
763 assert_todo_lines!(todo_file, "drop aaa comment", "drop bbb comment", "edit ccc comment");
764 assert_ne!(todo_file.version(), &old_version);
765 }
766
767 #[test]
768 fn swap_up() {
769 let (mut todo_file, _) =
770 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
771 let old_version = *todo_file.version();
772 assert!(todo_file.swap_range_up(1, 2));
773 assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
774 assert_ne!(todo_file.version(), &old_version);
775 }
776
777 #[test]
778 fn swap_up_records_history() {
779 let (mut todo_file, _) =
780 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
781 _ = todo_file.swap_range_up(1, 2);
782 let _undo_result = todo_file.undo();
783 assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
784 }
785
786 #[test]
787 fn swap_up_reverse_index() {
788 let (mut todo_file, _) =
789 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
790 assert!(todo_file.swap_range_up(2, 1));
791 assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
792 }
793
794 #[test]
795 fn swap_up_single_line() {
796 let (mut todo_file, _) =
797 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
798 assert!(todo_file.swap_range_up(1, 1));
799 assert_todo_lines!(todo_file, "pick bbb comment", "pick aaa comment", "pick ccc comment");
800 }
801
802 #[test]
803 fn swap_up_at_top_start_index() {
804 let (mut todo_file, _) =
805 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
806 assert!(!todo_file.swap_range_up(0, 1));
807 assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
808 }
809
810 #[test]
811 fn swap_up_at_top_end_index() {
812 let (mut todo_file, _) =
813 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
814 assert!(!todo_file.swap_range_up(1, 0));
815 assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
816 }
817
818 #[test]
819 fn swap_up_start_index_overflow() {
820 let (mut todo_file, _) =
821 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
822 assert!(todo_file.swap_range_up(3, 1));
823 assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
824 }
825
826 #[test]
827 fn swap_up_end_index_overflow() {
828 let (mut todo_file, _) =
829 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
830 assert!(todo_file.swap_range_up(3, 1));
831 assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
832 }
833
834 #[test]
835 fn swap_up_empty_list_index_out_of_bounds() {
836 let (mut todo_file, _) = create_and_load_todo_file(&[]);
837 assert!(!todo_file.swap_range_up(1, 1));
838 }
839
840 #[test]
841 fn swap_down() {
842 let (mut todo_file, _) =
843 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
844 let old_version = *todo_file.version();
845 assert!(todo_file.swap_range_down(0, 1));
846 assert_todo_lines!(todo_file, "pick ccc comment", "pick aaa comment", "pick bbb comment");
847 assert_ne!(todo_file.version(), &old_version);
848 }
849
850 #[test]
851 fn swap_down_records_history() {
852 let (mut todo_file, _) =
853 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
854 let _swap_result = todo_file.swap_range_down(0, 1);
855 let _undo_result = todo_file.undo();
856 assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
857 }
858
859 #[test]
860 fn swap_down_reverse_index() {
861 let (mut todo_file, _) =
862 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
863 assert!(todo_file.swap_range_down(1, 0));
864 assert_todo_lines!(todo_file, "pick ccc comment", "pick aaa comment", "pick bbb comment");
865 }
866
867 #[test]
868 fn swap_down_single_line() {
869 let (mut todo_file, _) =
870 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
871 assert!(todo_file.swap_range_down(0, 0));
872 assert_todo_lines!(todo_file, "pick bbb comment", "pick aaa comment", "pick ccc comment");
873 }
874
875 #[test]
876 fn swap_down_at_bottom_end_index() {
877 let (mut todo_file, _) =
878 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
879 assert!(!todo_file.swap_range_down(1, 2));
880 assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
881 }
882
883 #[test]
884 fn swap_down_at_bottom_start_index() {
885 let (mut todo_file, _) =
886 create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
887 assert!(!todo_file.swap_range_down(2, 1));
888 assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
889 }
890
891 #[test]
892 fn selected_line_index() {
893 let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
894 let selected_line_index = todo_file.set_selected_line_index(1);
895 assert_eq!(selected_line_index, 1);
896 assert_eq!(todo_file.get_selected_line_index(), 1);
897 }
898
899 #[test]
900 fn selected_line_index_overflow() {
901 let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
902 let selected_line_index = todo_file.set_selected_line_index(3);
903 assert_eq!(selected_line_index, 2);
904 assert_eq!(todo_file.get_selected_line_index(), 2);
905 }
906
907 #[test]
908 fn selected_line() {
909 let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
910 _ = todo_file.set_selected_line_index(0);
911 assert_some_eq!(todo_file.get_selected_line(), &create_line("exec foo"));
912 }
913
914 #[test]
915 fn selected_line_empty_list() {
916 let (mut todo_file, _) = create_and_load_todo_file(&[]);
917 _ = todo_file.set_selected_line_index(0);
918 assert_none!(todo_file.get_selected_line());
919 }
920
921 #[test]
922 fn get_max_selected_line() {
923 let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
924 assert_eq!(todo_file.get_max_selected_line_index(), 2);
925 }
926
927 #[test]
928 fn get_max_selected_line_empty_list() {
929 let (todo_file, _) = create_and_load_todo_file(&[]);
930 assert_eq!(todo_file.get_max_selected_line_index(), 0);
931 }
932
933 #[test]
934 fn get_line_miss_high() {
935 let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
936 assert_none!(todo_file.get_line(4));
937 }
938
939 #[test]
940 fn get_line_hit() {
941 let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
942 assert_some_eq!(todo_file.get_line(1), &create_line("exec bar"));
943 }
944
945 #[test]
946 fn get_file_path() {
947 let (todo_file, filepath) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
948 assert_eq!(todo_file.get_filepath(), filepath.path());
949 }
950
951 #[test]
952 fn iter() {
953 let (todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
954 assert_some_eq!(todo_file.lines_iter().next(), &create_line("pick aaa comment"));
955 }
956
957 #[test]
958 fn is_empty_true() {
959 let (todo_file, _) = create_and_load_todo_file(&[]);
960 assert_empty!(todo_file);
961 }
962
963 #[test]
964 fn is_empty_false() {
965 let (todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
966 assert_not_empty!(todo_file);
967 }
968}