• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MitMaro / git-interactive-rebase-tool / 5726345979

01 Aug 2023 11:47AM CUT coverage: 94.212% (-0.004%) from 94.216%
5726345979

Pull #877

github

web-flow
Merge 4c593d764 into f8262b54f
Pull Request #877: Add Operation::Load to History

24 of 24 new or added lines in 3 files covered. (100.0%)

4704 of 4993 relevant lines covered (94.21%)

3.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.67
/src/todo_file/src/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

132
mod action;
133
mod edit_content;
134
pub mod errors;
135
mod history;
136
mod line;
137
mod line_parser;
138
mod search;
139
#[cfg(not(tarpaulin_include))]
140
pub mod testutil;
141
mod utils;
142

143
use std::{
144
        fs::{read_to_string, File},
145
        io::Write,
146
        path::{Path, PathBuf},
147
        slice::Iter,
148
};
149

150
pub use version_track::Version;
151

152
pub use self::{action::Action, edit_content::EditContext, line::Line, search::Search};
153
use self::{
154
        history::{History, HistoryItem},
155
        utils::{remove_range, swap_range_down, swap_range_up},
156
};
157
use crate::{
158
        errors::{FileReadErrorCause, IoError},
159
        history::Operation,
160
};
161

162
/// Represents a rebase file.
163
#[derive(Debug)]
164
pub struct TodoFile {
165
        comment_char: String,
166
        filepath: PathBuf,
167
        history: History,
168
        is_noop: bool,
169
        lines: Vec<Line>,
170
        selected_line_index: usize,
171
        version: Version,
172
}
173

174
impl TodoFile {
175
        /// Create a new instance.
176
        #[must_use]
177
        #[inline]
178
        pub fn new<Path: AsRef<std::path::Path>>(path: Path, undo_limit: u32, comment_char: &str) -> Self {
3✔
179
                Self {
180
                        comment_char: String::from(comment_char),
3✔
181
                        filepath: PathBuf::from(path.as_ref()),
6✔
182
                        history: History::new(undo_limit),
3✔
183
                        lines: vec![],
3✔
184
                        is_noop: false,
185
                        selected_line_index: 0,
186
                        version: Version::new(),
3✔
187
                }
188
        }
189

190
        /// Set the rebase lines.
191
        #[inline]
192
        pub fn set_lines(&mut self, lines: Vec<Line>) {
3✔
193
                self.is_noop = !lines.is_empty() && lines[0].get_action() == &Action::Noop;
9✔
194
                self.lines = if self.is_noop {
9✔
195
                        vec![]
4✔
196
                }
197
                else {
×
198
                        lines.into_iter().filter(|l| l.get_action() != &Action::Noop).collect()
12✔
199
                };
200
                if self.selected_line_index >= self.lines.len() {
5✔
201
                        self.selected_line_index = if self.lines.is_empty() { 0 } else { self.lines.len() - 1 };
2✔
202
                }
203
                self.version.reset();
3✔
204
                self.history.reset();
3✔
205
        }
206

207
        /// Load the rebase file from disk.
208
        ///
209
        /// # Errors
210
        ///
211
        /// Returns error if the file cannot be read.
212
        #[inline]
213
        pub fn load_file(&mut self) -> Result<(), IoError> {
3✔
214
                let lines: Result<Vec<Line>, IoError> = read_to_string(self.filepath.as_path())
13✔
215
                        .map_err(|err| {
4✔
216
                                IoError::FileRead {
1✔
217
                                        file: self.filepath.clone(),
1✔
218
                                        cause: FileReadErrorCause::from(err),
1✔
219
                                }
220
                        })?
221
                        .lines()
222
                        .filter_map(|l| {
6✔
223
                                if l.starts_with(self.comment_char.as_str()) || l.is_empty() {
6✔
224
                                        None
3✔
225
                                }
226
                                else {
×
227
                                        Some(Line::new(l).map_err(|err| {
3✔
228
                                                IoError::FileRead {
×
229
                                                        file: self.filepath.clone(),
×
230
                                                        cause: FileReadErrorCause::from(err),
×
231
                                                }
232
                                        }))
233
                                }
234
                        })
235
                        .collect();
236
                self.set_lines(lines?);
3✔
237
                Ok(())
3✔
238
        }
239

240
        /// Write the rebase file to disk.
241
        /// # Errors
242
        ///
243
        /// Returns error if the file cannot be written.
244
        #[inline]
245
        pub fn write_file(&self) -> Result<(), IoError> {
2✔
246
                let mut file = File::create(&self.filepath).map_err(|err| {
4✔
247
                        IoError::FileRead {
1✔
248
                                file: self.filepath.clone(),
1✔
249
                                cause: FileReadErrorCause::from(err),
1✔
250
                        }
251
                })?;
252
                let file_contents = if self.is_noop {
2✔
253
                        String::from("noop")
2✔
254
                }
255
                else {
×
256
                        self.lines.iter().map(Line::to_text).collect::<Vec<String>>().join("\n")
6✔
257
                };
258
                writeln!(file, "{file_contents}").map_err(|err| {
4✔
259
                        IoError::FileRead {
×
260
                                file: self.filepath.clone(),
×
261
                                cause: FileReadErrorCause::from(err),
×
262
                        }
263
                })?;
264
                Ok(())
2✔
265
        }
266

267
        /// Set the selected line index returning the new index based after ensuring within range.
268
        #[inline]
269
        pub fn set_selected_line_index(&mut self, selected_line_index: usize) -> usize {
3✔
270
                self.selected_line_index = if self.lines.is_empty() {
7✔
271
                        0
1✔
272
                }
273
                else if selected_line_index >= self.lines.len() {
8✔
274
                        self.lines.len() - 1
6✔
275
                }
276
                else {
×
277
                        selected_line_index
2✔
278
                };
279
                self.selected_line_index
3✔
280
        }
281

282
        /// Swap a range of lines up.
283
        #[inline]
284
        pub fn swap_range_up(&mut self, start_index: usize, end_index: usize) -> bool {
2✔
285
                if end_index == 0 || start_index == 0 || self.lines.is_empty() {
2✔
286
                        return false;
4✔
287
                }
288

289
                let max_index = self.lines.len() - 1;
4✔
290
                let end = if end_index > max_index { max_index } else { end_index };
4✔
291
                let start = if start_index > max_index {
4✔
292
                        max_index
1✔
293
                }
294
                else {
×
295
                        start_index
2✔
296
                };
297

298
                swap_range_up(&mut self.lines, start, end);
2✔
299
                self.version.increment();
2✔
300
                self.history.record(HistoryItem::new_swap_up(start, end));
2✔
301
                true
2✔
302
        }
303

304
        /// Swap a range of lines down.
305
        #[inline]
306
        pub fn swap_range_down(&mut self, start_index: usize, end_index: usize) -> bool {
2✔
307
                let len = self.lines.len();
2✔
308
                let max_index = if len == 0 { 0 } else { len - 1 };
4✔
309

310
                if end_index == max_index || start_index == max_index {
4✔
311
                        return false;
4✔
312
                }
313

314
                swap_range_down(&mut self.lines, start_index, end_index);
2✔
315
                self.version.increment();
2✔
316
                self.history.record(HistoryItem::new_swap_down(start_index, end_index));
2✔
317
                true
2✔
318
        }
319

320
        /// Add a new line.
321
        #[inline]
322
        pub fn add_line(&mut self, index: usize, line: Line) {
4✔
323
                let i = if index > self.lines.len() {
10✔
324
                        self.lines.len()
6✔
325
                }
326
                else {
×
327
                        index
2✔
328
                };
329
                self.lines.insert(i, line);
4✔
330
                self.version.increment();
4✔
331
                self.history.record(HistoryItem::new_add(i, i));
4✔
332
        }
333

334
        /// Remove a range of lines.
335
        #[inline]
336
        pub fn remove_lines(&mut self, start_index: usize, end_index: usize) {
2✔
337
                if self.lines.is_empty() {
2✔
338
                        return;
×
339
                }
340

341
                let max_index = self.lines.len() - 1;
4✔
342
                let end = if end_index > max_index { max_index } else { end_index };
4✔
343
                let start = if start_index > max_index {
4✔
344
                        max_index
2✔
345
                }
346
                else {
×
347
                        start_index
2✔
348
                };
349

350
                let removed_lines = remove_range(&mut self.lines, start, end);
2✔
351
                self.version.increment();
2✔
352
                self.history.record(HistoryItem::new_remove(start, end, removed_lines));
2✔
353
        }
354

355
        /// Update a range of lines.
356
        #[inline]
357
        pub fn update_range(&mut self, start_index: usize, end_index: usize, edit_context: &EditContext) {
2✔
358
                if self.lines.is_empty() {
2✔
359
                        return;
×
360
                }
361

362
                let max_index = self.lines.len() - 1;
4✔
363
                let end = if end_index > max_index { max_index } else { end_index };
4✔
364
                let start = if start_index > max_index {
4✔
365
                        max_index
1✔
366
                }
367
                else {
×
368
                        start_index
2✔
369
                };
370

371
                let range = if end <= start { end..=start } else { start..=end };
2✔
372

373
                let mut lines = vec![];
2✔
374
                for index in range {
6✔
375
                        let line = &mut self.lines[index];
4✔
376
                        lines.push(line.clone());
2✔
377
                        if let Some(action) = edit_context.get_action() {
5✔
378
                                line.set_action(action);
8✔
379
                        }
380

381
                        if let Some(content) = edit_context.get_content() {
6✔
382
                                line.edit_content(content);
4✔
383
                        }
384

385
                        if let Some(option) = edit_context.get_option() {
6✔
386
                                line.toggle_option(option);
2✔
387
                        }
388
                }
389
                self.version.increment();
3✔
390
                self.history.record(HistoryItem::new_modify(start, end, lines));
3✔
391
        }
392

393
        /// Undo the last modification.
394
        #[inline]
395
        pub fn undo(&mut self) -> Option<(usize, usize)> {
3✔
396
                self.version.increment();
3✔
397
                if let Some((operation, start, end)) = self.history.undo(&mut self.lines) {
3✔
398
                        return if operation == Operation::Load {
6✔
399
                                None
1✔
400
                        }
401
                        else {
×
402
                                Some((start, end))
3✔
403
                        };
404
                }
405
                None
1✔
406
        }
407

408
        /// Redo the last undone modification.
409
        #[inline]
410
        pub fn redo(&mut self) -> Option<(usize, usize)> {
3✔
411
                self.version.increment();
3✔
412
                self.history.redo(&mut self.lines).map(|(_, start, end)| (start, end))
6✔
413
        }
414

415
        /// Get the current version
416
        #[must_use]
417
        #[inline]
418
        pub const fn version(&self) -> &Version {
3✔
419
                &self.version
3✔
420
        }
421

422
        /// Get the selected line.
423
        #[must_use]
424
        #[inline]
425
        pub fn get_selected_line(&self) -> Option<&Line> {
2✔
426
                self.lines.get(self.selected_line_index)
2✔
427
        }
428

429
        /// Get the index of the last line that can be selected.
430
        #[must_use]
431
        #[inline]
432
        pub fn get_max_selected_line_index(&self) -> usize {
2✔
433
                let len = self.lines.len();
2✔
434
                if len == 0 { 0 } else { len - 1 }
4✔
435
        }
436

437
        /// Get the selected line index
438
        #[must_use]
439
        #[inline]
440
        pub const fn get_selected_line_index(&self) -> usize {
2✔
441
                self.selected_line_index
2✔
442
        }
443

444
        /// Get the file path to the rebase file.
445
        #[must_use]
446
        #[inline]
447
        pub fn get_filepath(&self) -> &Path {
2✔
448
                self.filepath.as_path()
2✔
449
        }
450

451
        /// Get a line by index.
452
        #[must_use]
453
        #[inline]
454
        pub fn get_line(&self, index: usize) -> Option<&Line> {
3✔
455
                self.lines.get(index)
3✔
456
        }
457

458
        /// Get an owned copy of the lines.
459
        #[must_use]
460
        #[inline]
461
        pub fn get_lines_owned(&self) -> Vec<Line> {
3✔
462
                self.lines.clone()
3✔
463
        }
464

465
        /// Is the rebase file a noop.
466
        #[must_use]
467
        #[inline]
468
        pub const fn is_noop(&self) -> bool {
2✔
469
                self.is_noop
2✔
470
        }
471

472
        /// Get an iterator over the lines.
473
        #[inline]
474
        pub fn lines_iter(&self) -> Iter<'_, Line> {
3✔
475
                self.lines.iter()
3✔
476
        }
477

478
        /// Does the rebase file contain no lines.
479
        #[must_use]
480
        #[inline]
481
        pub fn is_empty(&self) -> bool {
3✔
482
                self.lines.is_empty()
3✔
483
        }
484
}
485

486
#[cfg(test)]
487
mod tests {
488
        use claims::{assert_none, assert_some_eq};
489
        use tempfile::{Builder, NamedTempFile};
490
        use testutils::{assert_empty, assert_not_empty};
491

492
        use super::*;
493

494
        fn create_line(line: &str) -> Line {
495
                Line::new(line).unwrap()
496
        }
497

498
        fn create_and_load_todo_file(file_contents: &[&str]) -> (TodoFile, NamedTempFile) {
499
                let todo_file_path = Builder::new()
500
                        .prefix("git-rebase-todo-scratch")
501
                        .suffix("")
502
                        .tempfile()
503
                        .unwrap();
504
                write!(todo_file_path.as_file(), "{}", file_contents.join("\n")).unwrap();
505
                let mut todo_file = TodoFile::new(todo_file_path.path().to_str().unwrap(), 1, "#");
506
                todo_file.load_file().unwrap();
507
                (todo_file, todo_file_path)
508
        }
509

510
        macro_rules! assert_read_todo_file {
511
                ($todo_file_path:expr, $($arg:expr),*) => {
512
                        let expected = [$( $arg, )*];
513
                        let content = read_to_string(Path::new($todo_file_path)).unwrap();
514
                        pretty_assertions::assert_str_eq!(content, format!("{}\n", expected.join("\n")));
515
                };
516
        }
517

518
        macro_rules! assert_todo_lines {
519
                ($todo_file_path:expr, $($arg:expr),*) => {
520
                        let actual_lines = $todo_file_path.get_lines_owned();
521

522
                        let expected = vec![$( create_line($arg), )*];
523
                        pretty_assertions::assert_str_eq!(
524
                                actual_lines.iter().map(Line::to_text).collect::<Vec<String>>().join("\n"),
525
                                expected.iter().map(Line::to_text).collect::<Vec<String>>().join("\n")
526
                        );
527
                };
528
        }
529

530
        #[test]
531
        fn load_file() {
532
                let (todo_file, _) = create_and_load_todo_file(&["pick aaa foobar"]);
533
                assert_todo_lines!(todo_file, "pick aaa foobar");
534
                assert_ne!(todo_file.version(), &Version::new());
535
        }
536

537
        #[test]
538
        fn load_noop_file() {
539
                let (todo_file, _) = create_and_load_todo_file(&["noop"]);
540
                assert_empty!(todo_file);
541
                assert!(todo_file.is_noop());
542
        }
543

544
        #[test]
545
        fn load_ignore_comments() {
546
                let (todo_file, _) = create_and_load_todo_file(&["# pick aaa comment", "pick aaa foo", "# pick aaa comment"]);
547
                assert_todo_lines!(todo_file, "pick aaa foo");
548
        }
549

550
        #[test]
551
        fn load_ignore_newlines() {
552
                let (todo_file, _) = create_and_load_todo_file(&["", "pick aaa foobar", ""]);
553
                assert_todo_lines!(todo_file, "pick aaa foobar");
554
        }
555

556
        #[test]
557
        fn set_lines() {
558
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
559
                let old_version = todo_file.version;
560
                todo_file.set_lines(vec![create_line("pick bbb comment")]);
561
                assert_todo_lines!(todo_file, "pick bbb comment");
562
                assert_ne!(todo_file.version(), &old_version);
563
        }
564

565
        #[test]
566
        fn set_lines_reset_history() {
567
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
568
                todo_file.history.record(HistoryItem::new_add(1, 1));
569
                todo_file.set_lines(vec![create_line("pick bbb comment")]);
570
                assert_none!(todo_file.undo());
571
        }
572

573
        #[test]
574
        fn set_lines_reset_selected_index() {
575
                let (mut todo_file, _) = create_and_load_todo_file(&["pick a a", "pick b b", "pick c c"]);
576
                todo_file.selected_line_index = 2;
577
                todo_file.set_lines(vec![create_line("pick a a"), create_line("pick b b")]);
578
                assert_eq!(todo_file.selected_line_index, 1);
579
        }
580

581
        #[test]
582
        fn set_lines_reset_selected_index_empty_lis() {
583
                let (mut todo_file, _) = create_and_load_todo_file(&["pick a a", "pick b b", "pick c c"]);
584
                todo_file.selected_line_index = 2;
585
                todo_file.set_lines(vec![]);
586
                assert_eq!(todo_file.selected_line_index, 0);
587
        }
588

589
        #[test]
590
        fn write_file() {
591
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
592
                todo_file.set_lines(vec![create_line("pick bbb comment")]);
593
                todo_file.write_file().unwrap();
594
                assert_todo_lines!(todo_file, "pick bbb comment");
595
        }
596

597
        #[test]
598
        fn write_file_noop() {
599
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
600
                todo_file.set_lines(vec![create_line("noop")]);
601
                todo_file.write_file().unwrap();
602
                assert_read_todo_file!(todo_file.get_filepath(), "noop");
603
        }
604

605
        #[test]
606
        fn add_line_index_miss() {
607
                let (mut todo_file, _) =
608
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
609
                todo_file.add_line(100, create_line("fixup ddd comment"));
610
                assert_todo_lines!(
611
                        todo_file,
612
                        "pick aaa comment",
613
                        "drop bbb comment",
614
                        "edit ccc comment",
615
                        "fixup ddd comment"
616
                );
617
        }
618

619
        #[test]
620
        fn add_line() {
621
                let (mut todo_file, _) =
622
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
623
                let old_version = *todo_file.version();
624
                todo_file.add_line(1, create_line("fixup ddd comment"));
625
                assert_todo_lines!(
626
                        todo_file,
627
                        "pick aaa comment",
628
                        "fixup ddd comment",
629
                        "drop bbb comment",
630
                        "edit ccc comment"
631
                );
632
                assert_ne!(todo_file.version(), &old_version);
633
        }
634

635
        #[test]
636
        fn add_line_record_history() {
637
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
638
                todo_file.add_line(1, create_line("fixup ddd comment"));
639
                let _undo_result = todo_file.undo();
640
                assert_todo_lines!(todo_file, "pick aaa comment");
641
        }
642

643
        #[test]
644
        fn remove_lines_index_miss_start() {
645
                let (mut todo_file, _) =
646
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
647
                todo_file.remove_lines(100, 1);
648
                assert_todo_lines!(todo_file, "pick aaa comment");
649
        }
650

651
        #[test]
652
        fn remove_lines_index_miss_end() {
653
                let (mut todo_file, _) =
654
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
655
                todo_file.remove_lines(1, 100);
656
                assert_todo_lines!(todo_file, "pick aaa comment");
657
        }
658

659
        #[test]
660
        fn remove_lines_index_miss_start_and_end() {
661
                let (mut todo_file, _) =
662
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
663
                todo_file.remove_lines(100, 100);
664
                assert_todo_lines!(todo_file, "pick aaa comment", "drop bbb comment");
665
        }
666

667
        #[test]
668
        fn remove_lines() {
669
                let (mut todo_file, _) =
670
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
671
                let old_version = *todo_file.version();
672
                todo_file.remove_lines(1, 1);
673
                assert_todo_lines!(todo_file, "pick aaa comment", "edit ccc comment");
674
                assert_ne!(todo_file.version(), &old_version);
675
        }
676

677
        #[test]
678
        fn remove_lines_empty_list() {
679
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
680
                todo_file.remove_lines(1, 1);
681
        }
682

683
        #[test]
684
        fn remove_lines_record_history() {
685
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "edit ccc comment"]);
686
                todo_file.remove_lines(1, 1);
687
                let _undo_result = todo_file.undo();
688
                assert_todo_lines!(todo_file, "pick aaa comment", "edit ccc comment");
689
        }
690

691
        #[test]
692
        fn update_range_full_set_action() {
693
                let (mut todo_file, _) =
694
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
695
                let old_version = *todo_file.version();
696
                todo_file.update_range(0, 2, &EditContext::new().action(Action::Reword));
697
                assert_todo_lines!(
698
                        todo_file,
699
                        "reword aaa comment",
700
                        "reword bbb comment",
701
                        "reword ccc comment"
702
                );
703
                assert_ne!(todo_file.version(), &old_version);
704
        }
705

706
        #[test]
707
        fn update_range_full_set_content() {
708
                let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
709
                todo_file.update_range(0, 2, &EditContext::new().content("echo"));
710
                assert_todo_lines!(todo_file, "exec echo", "exec echo", "exec echo");
711
        }
712

713
        #[test]
714
        fn update_range_set_option() {
715
                let (mut todo_file, _) = create_and_load_todo_file(&["fixup aaa comment"]);
716
                let old_version = *todo_file.version();
717
                todo_file.update_range(0, 2, &EditContext::new().option("-c"));
718
                assert_todo_lines!(todo_file, "fixup -c aaa comment");
719
                assert_ne!(todo_file.version(), &old_version);
720
        }
721

722
        #[test]
723
        fn update_range_reverse_indexes() {
724
                let (mut todo_file, _) =
725
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
726
                todo_file.update_range(2, 0, &EditContext::new().action(Action::Reword));
727
                assert_todo_lines!(
728
                        todo_file,
729
                        "reword aaa comment",
730
                        "reword bbb comment",
731
                        "reword ccc comment"
732
                );
733
        }
734

735
        #[test]
736
        fn update_range_record_history() {
737
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
738
                todo_file.update_range(0, 0, &EditContext::new().action(Action::Reword));
739
                let _undo_result = todo_file.undo();
740
                assert_todo_lines!(todo_file, "pick aaa comment");
741
        }
742

743
        #[test]
744
        fn update_range_empty_list() {
745
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
746
                todo_file.update_range(0, 0, &EditContext::new().action(Action::Reword));
747
        }
748

749
        #[test]
750
        fn update_range_start_index_overflow() {
751
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "pick bbb comment"]);
752
                todo_file.update_range(2, 0, &EditContext::new().action(Action::Reword));
753
                assert_todo_lines!(todo_file, "reword aaa comment", "reword bbb comment");
754
        }
755

756
        #[test]
757
        fn update_range_end_index_overflow() {
758
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "pick bbb comment"]);
759
                todo_file.update_range(0, 2, &EditContext::new().action(Action::Reword));
760
                assert_todo_lines!(todo_file, "reword aaa comment", "reword bbb comment");
761
        }
762

763
        #[test]
764
        fn undo_load_operation() {
765
                let (mut todo_file, _) =
766
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
767
                assert_none!(todo_file.undo());
768
        }
769

770
        #[test]
771
        fn undo_empty_history() {
772
                let (mut todo_file, _) =
773
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
774
                // set short history, to remove load entry
775
                todo_file.history = History::new(1);
776
                todo_file.update_range(0, 0, &EditContext::new().action(Action::Drop));
777
                _ = todo_file.undo(); // remove Drop operation
778
                assert_none!(todo_file.undo());
779
        }
780

781
        #[test]
782
        fn undo_operation() {
783
                let (mut todo_file, _) =
784
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
785
                todo_file.update_range(0, 1, &EditContext::new().action(Action::Drop));
786
                assert_some_eq!(todo_file.undo(), (0, 1));
787
        }
788

789
        #[test]
790
        fn history_undo_redo() {
791
                let (mut todo_file, _) =
792
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
793
                todo_file.update_range(0, 0, &EditContext::new().action(Action::Drop));
794
                let old_version = *todo_file.version();
795
                let _undo_result = todo_file.undo();
796
                assert_todo_lines!(todo_file, "pick aaa comment", "drop bbb comment", "edit ccc comment");
797
                assert_ne!(todo_file.version(), &old_version);
798
                let old_version = *todo_file.version();
799
                _ = todo_file.redo();
800
                assert_todo_lines!(todo_file, "drop aaa comment", "drop bbb comment", "edit ccc comment");
801
                assert_ne!(todo_file.version(), &old_version);
802
        }
803

804
        #[test]
805
        fn redo_empty_history() {
806
                let (mut todo_file, _) =
807
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
808
                assert_none!(todo_file.redo());
809
        }
810

811
        #[test]
812
        fn redo_operation() {
813
                let (mut todo_file, _) =
814
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
815
                todo_file.update_range(0, 1, &EditContext::new().action(Action::Drop));
816
                _ = todo_file.undo();
817
                assert_some_eq!(todo_file.redo(), (0, 1));
818
        }
819

820
        #[test]
821
        fn swap_up() {
822
                let (mut todo_file, _) =
823
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
824
                let old_version = *todo_file.version();
825
                assert!(todo_file.swap_range_up(1, 2));
826
                assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
827
                assert_ne!(todo_file.version(), &old_version);
828
        }
829

830
        #[test]
831
        fn swap_up_records_history() {
832
                let (mut todo_file, _) =
833
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
834
                _ = todo_file.swap_range_up(1, 2);
835
                let _undo_result = todo_file.undo();
836
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
837
        }
838

839
        #[test]
840
        fn swap_up_reverse_index() {
841
                let (mut todo_file, _) =
842
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
843
                assert!(todo_file.swap_range_up(2, 1));
844
                assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
845
        }
846

847
        #[test]
848
        fn swap_up_single_line() {
849
                let (mut todo_file, _) =
850
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
851
                assert!(todo_file.swap_range_up(1, 1));
852
                assert_todo_lines!(todo_file, "pick bbb comment", "pick aaa comment", "pick ccc comment");
853
        }
854

855
        #[test]
856
        fn swap_up_at_top_start_index() {
857
                let (mut todo_file, _) =
858
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
859
                assert!(!todo_file.swap_range_up(0, 1));
860
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
861
        }
862

863
        #[test]
864
        fn swap_up_at_top_end_index() {
865
                let (mut todo_file, _) =
866
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
867
                assert!(!todo_file.swap_range_up(1, 0));
868
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
869
        }
870

871
        #[test]
872
        fn swap_up_start_index_overflow() {
873
                let (mut todo_file, _) =
874
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
875
                assert!(todo_file.swap_range_up(3, 1));
876
                assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
877
        }
878

879
        #[test]
880
        fn swap_up_end_index_overflow() {
881
                let (mut todo_file, _) =
882
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
883
                assert!(todo_file.swap_range_up(3, 1));
884
                assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
885
        }
886

887
        #[test]
888
        fn swap_up_empty_list_index_out_of_bounds() {
889
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
890
                assert!(!todo_file.swap_range_up(1, 1));
891
        }
892

893
        #[test]
894
        fn swap_down() {
895
                let (mut todo_file, _) =
896
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
897
                let old_version = *todo_file.version();
898
                assert!(todo_file.swap_range_down(0, 1));
899
                assert_todo_lines!(todo_file, "pick ccc comment", "pick aaa comment", "pick bbb comment");
900
                assert_ne!(todo_file.version(), &old_version);
901
        }
902

903
        #[test]
904
        fn swap_down_records_history() {
905
                let (mut todo_file, _) =
906
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
907
                let _swap_result = todo_file.swap_range_down(0, 1);
908
                let _undo_result = todo_file.undo();
909
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
910
        }
911

912
        #[test]
913
        fn swap_down_reverse_index() {
914
                let (mut todo_file, _) =
915
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
916
                assert!(todo_file.swap_range_down(1, 0));
917
                assert_todo_lines!(todo_file, "pick ccc comment", "pick aaa comment", "pick bbb comment");
918
        }
919

920
        #[test]
921
        fn swap_down_single_line() {
922
                let (mut todo_file, _) =
923
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
924
                assert!(todo_file.swap_range_down(0, 0));
925
                assert_todo_lines!(todo_file, "pick bbb comment", "pick aaa comment", "pick ccc comment");
926
        }
927

928
        #[test]
929
        fn swap_down_at_bottom_end_index() {
930
                let (mut todo_file, _) =
931
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
932
                assert!(!todo_file.swap_range_down(1, 2));
933
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
934
        }
935

936
        #[test]
937
        fn swap_down_at_bottom_start_index() {
938
                let (mut todo_file, _) =
939
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
940
                assert!(!todo_file.swap_range_down(2, 1));
941
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
942
        }
943

944
        #[test]
945
        fn selected_line_index() {
946
                let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
947
                let selected_line_index = todo_file.set_selected_line_index(1);
948
                assert_eq!(selected_line_index, 1);
949
                assert_eq!(todo_file.get_selected_line_index(), 1);
950
        }
951

952
        #[test]
953
        fn selected_line_index_overflow() {
954
                let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
955
                let selected_line_index = todo_file.set_selected_line_index(3);
956
                assert_eq!(selected_line_index, 2);
957
                assert_eq!(todo_file.get_selected_line_index(), 2);
958
        }
959

960
        #[test]
961
        fn selected_line() {
962
                let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
963
                _ = todo_file.set_selected_line_index(0);
964
                assert_some_eq!(todo_file.get_selected_line(), &create_line("exec foo"));
965
        }
966

967
        #[test]
968
        fn selected_line_empty_list() {
969
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
970
                _ = todo_file.set_selected_line_index(0);
971
                assert_none!(todo_file.get_selected_line());
972
        }
973

974
        #[test]
975
        fn get_max_selected_line() {
976
                let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
977
                assert_eq!(todo_file.get_max_selected_line_index(), 2);
978
        }
979

980
        #[test]
981
        fn get_max_selected_line_empty_list() {
982
                let (todo_file, _) = create_and_load_todo_file(&[]);
983
                assert_eq!(todo_file.get_max_selected_line_index(), 0);
984
        }
985

986
        #[test]
987
        fn get_line_miss_high() {
988
                let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
989
                assert_none!(todo_file.get_line(4));
990
        }
991

992
        #[test]
993
        fn get_line_hit() {
994
                let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
995
                assert_some_eq!(todo_file.get_line(1), &create_line("exec bar"));
996
        }
997

998
        #[test]
999
        fn get_file_path() {
1000
                let (todo_file, filepath) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
1001
                assert_eq!(todo_file.get_filepath(), filepath.path());
1002
        }
1003

1004
        #[test]
1005
        fn iter() {
1006
                let (todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
1007
                assert_some_eq!(todo_file.lines_iter().next(), &create_line("pick aaa comment"));
1008
        }
1009

1010
        #[test]
1011
        fn is_empty_true() {
1012
                let (todo_file, _) = create_and_load_todo_file(&[]);
1013
                assert_empty!(todo_file);
1014
        }
1015

1016
        #[test]
1017
        fn is_empty_false() {
1018
                let (todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
1019
                assert_not_empty!(todo_file);
1020
        }
1021
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc