todo_file/
lib.rs

1// LINT-REPLACE-START
2// This section is autogenerated, do not modify directly
3// nightly sometimes removes/renames lints
4#![cfg_attr(allow_unknown_lints, allow(unknown_lints))]
5#![cfg_attr(allow_unknown_lints, allow(renamed_and_removed_lints))]
6// enable all rustc's built-in lints
7#![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// rustc's additional allowed by default lints
17#![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// enable all of Clippy's lints
55#![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// allow some things in tests
97#![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// allowable upcoming nightly lints
114#![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)]
125// LINT-REPLACE-END
126
127//! Git Interactive Rebase Tool - Todo File Module
128//!
129//! # Description
130//! This module is used to handle working with the rebase todo file.
131
132mod 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/// Represents a rebase file.
160#[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	/// Create a new instance.
173	#[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	/// Set the rebase lines.
188	#[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	/// Load the rebase file from disk.
205	///
206	/// # Errors
207	///
208	/// Returns error if the file cannot be read.
209	#[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	/// Write the rebase file to disk.
238	/// # Errors
239	///
240	/// Returns error if the file cannot be written.
241	#[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	/// Set the selected line index returning the new index based after ensuring within range.
265	#[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	/// Swap a range of lines up.
280	#[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	/// Swap a range of lines down.
302	#[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	/// Add a new line.
318	#[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	/// Remove a range of lines.
332	#[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	/// Update a range of lines.
353	#[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	/// Undo the last modification.
391	#[inline]
392	pub fn undo(&mut self) -> Option<(usize, usize)> {
393		self.version.increment();
394		self.history.undo(&mut self.lines)
395	}
396
397	/// Redo the last undone modification.
398	#[inline]
399	pub fn redo(&mut self) -> Option<(usize, usize)> {
400		self.version.increment();
401		self.history.redo(&mut self.lines)
402	}
403
404	/// Get the current version
405	#[must_use]
406	#[inline]
407	pub const fn version(&self) -> &Version {
408		&self.version
409	}
410
411	/// Get the selected line.
412	#[must_use]
413	#[inline]
414	pub fn get_selected_line(&self) -> Option<&Line> {
415		self.lines.get(self.selected_line_index)
416	}
417
418	/// Get the index of the last line that can be selected.
419	#[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	/// Get the selected line index
427	#[must_use]
428	#[inline]
429	pub const fn get_selected_line_index(&self) -> usize {
430		self.selected_line_index
431	}
432
433	/// Get the file path to the rebase file.
434	#[must_use]
435	#[inline]
436	pub fn get_filepath(&self) -> &Path {
437		self.filepath.as_path()
438	}
439
440	/// Get a line by index.
441	#[must_use]
442	#[inline]
443	pub fn get_line(&self, index: usize) -> Option<&Line> {
444		self.lines.get(index)
445	}
446
447	/// Get an owned copy of the lines.
448	#[must_use]
449	#[inline]
450	pub fn get_lines_owned(&self) -> Vec<Line> {
451		self.lines.clone()
452	}
453
454	/// Is the rebase file a noop.
455	#[must_use]
456	#[inline]
457	pub const fn is_noop(&self) -> bool {
458		self.is_noop
459	}
460
461	/// Get an iterator over the lines.
462	#[inline]
463	pub fn lines_iter(&self) -> Iter<'_, Line> {
464		self.lines.iter()
465	}
466
467	/// Does the rebase file contain no lines.
468	#[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}