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

MitMaro / git-interactive-rebase-tool / 6883077488

15 Nov 2023 09:23PM UTC coverage: 93.248% (-0.4%) from 93.64%
6883077488

Pull #873

github

web-flow
Merge 0ab516642 into d7655157f
Pull Request #873: When editing in the middle of a rebase, dont clear on quit

45 of 72 new or added lines in 14 files covered. (62.5%)

1 existing line in 1 file now uncovered.

4792 of 5139 relevant lines covered (93.25%)

3.67 hits per line

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

76.32
/src/todo_file/src/search.rs
1
use version_track::Version;
2

3
use crate::{Action, TodoFile};
4

5
/// Search handler for the todofile
6
#[derive(Debug)]
7
pub struct Search {
8
        match_start_hint: usize,
9
        matches: Vec<usize>,
10
        rebase_todo_version: Version,
11
        search_term: String,
12
        selected: Option<usize>,
13
}
14

15
impl Search {
16
        /// Create a new instance
17
        #[inline]
18
        #[must_use]
19
        pub const fn new() -> Self {
2✔
20
                Self {
21
                        match_start_hint: 0,
22
                        matches: vec![],
4✔
23
                        rebase_todo_version: Version::sentinel(),
2✔
24
                        search_term: String::new(),
4✔
25
                        selected: None,
26
                }
27
        }
28

29
        /// Generate search results
30
        #[inline]
31
        pub fn search(&mut self, rebase_todo: &TodoFile, term: &str) -> bool {
2✔
32
                if &self.rebase_todo_version != rebase_todo.version() || self.search_term != term || self.matches.is_empty() {
6✔
33
                        self.matches.clear();
2✔
34
                        self.selected = None;
4✔
35
                        self.search_term = String::from(term);
6✔
36
                        self.rebase_todo_version = *rebase_todo.version();
2✔
37
                        for (i, line) in rebase_todo.lines_iter().enumerate() {
4✔
38
                                match *line.get_action() {
2✔
39
                                        Action::Break | Action::Noop => continue,
×
NEW
40
                                        Action::Cut
×
NEW
41
                                        | Action::Drop
×
42
                                        | Action::Edit
×
43
                                        | Action::Fixup
×
NEW
44
                                        | Action::Index
×
45
                                        | Action::Pick
×
46
                                        | Action::Reword
×
47
                                        | Action::Squash
×
48
                                        | Action::UpdateRef => {
×
49
                                                if line.get_hash().starts_with(term) || line.get_content().contains(term) {
6✔
50
                                                        self.matches.push(i);
3✔
51
                                                }
52
                                        },
53
                                        Action::Label | Action::Reset | Action::Merge | Action::Exec => {
×
54
                                                if line.get_content().contains(term) {
1✔
55
                                                        self.matches.push(i);
1✔
56
                                                }
57
                                        },
58
                                }
59
                        }
60
                }
61
                !self.matches.is_empty()
2✔
62
        }
63

64
        /// Select the next search result
65
        #[inline]
66
        #[allow(clippy::missing_panics_doc)]
67
        pub fn next(&mut self, rebase_todo: &TodoFile, term: &str) {
2✔
68
                if !self.search(rebase_todo, term) {
4✔
69
                        return;
×
70
                }
71

72
                if let Some(mut current) = self.selected {
4✔
73
                        current += 1;
4✔
74
                        let new_value = if current >= self.matches.len() { 0 } else { current };
5✔
75
                        self.selected = Some(new_value);
2✔
76
                }
77
                else {
×
78
                        // select the line after the hint that matches
79
                        let mut index_match = 0;
3✔
80
                        for (i, v) in self.matches.iter().enumerate() {
7✔
81
                                if *v >= self.match_start_hint {
3✔
82
                                        index_match = i;
2✔
83
                                        break;
×
84
                                }
85
                        }
86
                        self.selected = Some(index_match);
3✔
87
                };
88

89
                self.match_start_hint = self.matches[self.selected.unwrap()];
3✔
90
        }
91

92
        /// Select the previous search result
93
        #[inline]
94
        #[allow(clippy::missing_panics_doc)]
95
        pub fn previous(&mut self, rebase_todo: &TodoFile, term: &str) {
3✔
96
                if !self.search(rebase_todo, term) {
3✔
97
                        return;
×
98
                }
99

100
                if let Some(current) = self.selected {
6✔
101
                        let new_value = if current == 0 {
3✔
102
                                self.matches.len().saturating_sub(1)
6✔
103
                        }
104
                        else {
×
105
                                current.saturating_sub(1)
2✔
106
                        };
107
                        self.selected = Some(new_value);
2✔
108
                }
109
                else {
×
110
                        // select the line previous to hint that matches
111
                        let mut index_match = self.matches.len().saturating_sub(1);
3✔
112
                        for (i, v) in self.matches.iter().enumerate().rev() {
6✔
113
                                if *v <= self.match_start_hint {
2✔
114
                                        index_match = i;
2✔
115
                                        break;
×
116
                                }
117
                        }
118
                        self.selected = Some(index_match);
2✔
119
                }
120

121
                self.match_start_hint = self.matches[self.selected.unwrap()];
4✔
122
        }
123

124
        /// Set a hint for which result to select first during search
125
        #[inline]
126
        pub fn set_search_start_hint(&mut self, hint: usize) {
3✔
127
                if self.match_start_hint != hint {
6✔
128
                        self.match_start_hint = hint;
3✔
129
                }
130
        }
131

132
        /// Invalidate current search results
133
        #[inline]
134
        pub fn invalidate(&mut self) {
1✔
135
                self.matches.clear();
1✔
136
        }
137

138
        /// Cancel search, clearing results, selected result and search term
139
        #[inline]
140
        pub fn cancel(&mut self) {
2✔
141
                self.selected = None;
2✔
142
                self.search_term.clear();
2✔
143
                self.matches.clear();
2✔
144
        }
145

146
        /// Get the index of the current selected result, if there is one
147
        #[inline]
148
        #[must_use]
149
        pub fn current_match(&self) -> Option<usize> {
2✔
150
                let selected = self.selected?;
4✔
151
                self.matches.get(selected).copied()
2✔
152
        }
153

154
        /// Get the selected result number, if there is one
155
        #[inline]
156
        #[must_use]
157
        pub const fn current_result_selected(&self) -> Option<usize> {
2✔
158
                self.selected
2✔
159
        }
160

161
        /// Get the total number of results
162
        #[inline]
163
        #[must_use]
164
        pub fn total_results(&self) -> usize {
2✔
165
                self.matches.len()
2✔
166
        }
167
}
168

169
#[cfg(test)]
170
mod tests {
171
        use claims::{assert_none, assert_some_eq};
172

173
        use super::*;
174
        use crate::testutil::with_todo_file;
175

176
        #[test]
177
        fn search_empty_rebase_file() {
178
                with_todo_file(&[], |context| {
179
                        let mut search = Search::new();
180
                        assert!(!search.search(context.todo_file(), "foo"));
181
                });
182
        }
183

184
        #[test]
185
        fn search_with_one_line_no_match() {
186
                with_todo_file(&["pick abcdef bar"], |context| {
187
                        let mut search = Search::new();
188
                        assert!(!search.search(context.todo_file(), "foo"));
189
                });
190
        }
191

192
        #[test]
193
        fn search_with_one_line_match() {
194
                with_todo_file(&["pick abcdef foo"], |context| {
195
                        let mut search = Search::new();
196
                        assert!(search.search(context.todo_file(), "foo"));
197
                });
198
        }
199

200
        #[test]
201
        fn search_ignore_break() {
202
                with_todo_file(&["break"], |context| {
203
                        let mut search = Search::new();
204
                        assert!(!search.search(context.todo_file(), "break"));
205
                });
206
        }
207

208
        #[test]
209
        fn search_ignore_noop() {
210
                with_todo_file(&["noop"], |context| {
211
                        let mut search = Search::new();
212
                        assert!(!search.search(context.todo_file(), "noop"));
213
                });
214
        }
215

216
        #[test]
217
        fn search_standard_action_hash() {
218
                with_todo_file(
219
                        &[
220
                                "pick aaaaa no match",
221
                                "drop abcdef foo",
222
                                "edit abcdef foo",
223
                                "fixup abcdef foo",
224
                                "pick abcdef foo",
225
                                "reword abcdef foo",
226
                                "squash abcdef foo",
227
                        ],
228
                        |context| {
229
                                let mut search = Search::new();
230
                                assert!(search.search(context.todo_file(), "abcd"));
231
                                assert_eq!(search.total_results(), 6);
232
                        },
233
                );
234
        }
235

236
        #[test]
237
        fn search_standard_action_content() {
238
                with_todo_file(
239
                        &[
240
                                "pick abcdef no match",
241
                                "drop abcdef foobar",
242
                                "edit abcdef foobar",
243
                                "fixup abcdef foobar",
244
                                "pick abcdef foobar",
245
                                "reword abcdef foobar",
246
                                "squash abcdef foobar",
247
                        ],
248
                        |context| {
249
                                let mut search = Search::new();
250
                                assert!(search.search(context.todo_file(), "ooba"));
251
                                assert_eq!(search.total_results(), 6);
252
                        },
253
                );
254
        }
255

256
        #[test]
257
        fn search_standard_action_hash_starts_only() {
258
                with_todo_file(&["pick abcdef foobar"], |context| {
259
                        let mut search = Search::new();
260
                        assert!(!search.search(context.todo_file(), "def"));
261
                });
262
        }
263

264
        #[test]
265
        fn search_standard_ignore_action() {
266
                with_todo_file(&["pick abcdef foo"], |context| {
267
                        let mut search = Search::new();
268
                        assert!(!search.search(context.todo_file(), "pick"));
269
                });
270
        }
271

272
        #[test]
273
        fn search_editable_content() {
274
                with_todo_file(
275
                        &[
276
                                "label no match",
277
                                "label foobar",
278
                                "reset foobar",
279
                                "merge foobar",
280
                                "exec foobar",
281
                                "update-ref foobar",
282
                        ],
283
                        |context| {
284
                                let mut search = Search::new();
285
                                assert!(search.search(context.todo_file(), "ooba"));
286
                                assert_eq!(search.total_results(), 5);
287
                        },
288
                );
289
        }
290

291
        #[test]
292
        fn search_editable_ignore_action() {
293
                with_todo_file(&["label no match"], |context| {
294
                        let mut search = Search::new();
295
                        assert!(!search.search(context.todo_file(), "label"));
296
                });
297
        }
298

299
        #[test]
300
        fn next_no_match() {
301
                with_todo_file(&["pick aaa foo"], |context| {
302
                        let mut search = Search::new();
303
                        search.next(context.todo_file(), "miss");
304
                        assert_none!(search.current_match());
305
                });
306
        }
307

308
        #[test]
309
        fn next_first_match() {
310
                with_todo_file(&["pick aaa foo", "pick bbb foobar"], |context| {
311
                        let mut search = Search::new();
312
                        search.next(context.todo_file(), "foo");
313
                        assert_some_eq!(search.current_match(), 0);
314
                });
315
        }
316

317
        #[test]
318
        fn next_first_match_with_hint_in_range() {
319
                with_todo_file(&["pick aaa foo", "pick bbb foobar"], |context| {
320
                        let mut search = Search::new();
321
                        search.set_search_start_hint(1);
322
                        search.next(context.todo_file(), "foo");
323
                        assert_some_eq!(search.current_match(), 1);
324
                });
325
        }
326

327
        #[test]
328
        fn next_first_match_with_hint_in_range_but_behind() {
329
                with_todo_file(&["pick aaa foo", "pick bbb miss", "pick bbb foobar"], |context| {
330
                        let mut search = Search::new();
331
                        search.set_search_start_hint(1);
332
                        search.next(context.todo_file(), "foo");
333
                        assert_some_eq!(search.current_match(), 2);
334
                });
335
        }
336

337
        #[test]
338
        fn next_first_match_with_hint_in_range_wrap() {
339
                with_todo_file(
340
                        &["pick bbb miss", "pick aaa foo", "pick aaa foo", "pick bbb miss"],
341
                        |context| {
342
                                let mut search = Search::new();
343
                                search.set_search_start_hint(3);
344
                                search.next(context.todo_file(), "foo");
345
                                assert_some_eq!(search.current_match(), 1);
346
                        },
347
                );
348
        }
349

350
        #[test]
351
        fn next_first_match_with_hint_out_of_range() {
352
                with_todo_file(
353
                        &["pick bbb miss", "pick aaa foo", "pick aaa foo", "pick bbb miss"],
354
                        |context| {
355
                                let mut search = Search::new();
356
                                search.set_search_start_hint(99);
357
                                search.next(context.todo_file(), "foo");
358
                                assert_some_eq!(search.current_match(), 1);
359
                        },
360
                );
361
        }
362

363
        #[test]
364
        fn next_continued_match() {
365
                with_todo_file(&["pick aaa foo", "pick bbb foobar"], |context| {
366
                        let mut search = Search::new();
367
                        search.next(context.todo_file(), "foo");
368
                        search.next(context.todo_file(), "foo");
369
                        assert_some_eq!(search.current_match(), 1);
370
                });
371
        }
372

373
        #[test]
374
        fn next_continued_match_wrap_single_match() {
375
                with_todo_file(&["pick aaa foo", "pick bbb miss"], |context| {
376
                        let mut search = Search::new();
377
                        search.next(context.todo_file(), "foo");
378
                        search.next(context.todo_file(), "foo");
379
                        assert_some_eq!(search.current_match(), 0);
380
                });
381
        }
382

383
        #[test]
384
        fn next_continued_match_wrap() {
385
                with_todo_file(&["pick aaa foo", "pick bbb foobar"], |context| {
386
                        let mut search = Search::new();
387
                        search.next(context.todo_file(), "foo");
388
                        search.next(context.todo_file(), "foo");
389
                        search.next(context.todo_file(), "foo");
390
                        assert_some_eq!(search.current_match(), 0);
391
                });
392
        }
393

394
        #[test]
395
        fn next_updates_match_start_hint() {
396
                with_todo_file(&["pick bbb miss", "pick aaa foo"], |context| {
397
                        let mut search = Search::new();
398
                        search.next(context.todo_file(), "foo");
399
                        assert_eq!(search.match_start_hint, 1);
400
                });
401
        }
402

403
        #[test]
404
        fn previous_no_match() {
405
                with_todo_file(&["pick aaa foo"], |context| {
406
                        let mut search = Search::new();
407
                        search.previous(context.todo_file(), "miss");
408
                        assert_none!(search.current_match());
409
                });
410
        }
411

412
        #[test]
413
        fn previous_first_match() {
414
                with_todo_file(&["pick aaa foo"], |context| {
415
                        let mut search = Search::new();
416
                        search.previous(context.todo_file(), "foo");
417
                        assert_some_eq!(search.current_match(), 0);
418
                });
419
        }
420

421
        #[test]
422
        fn previous_first_match_with_hint_in_range() {
423
                with_todo_file(&["pick aaa foo", "pick bbb foobar"], |context| {
424
                        let mut search = Search::new();
425
                        search.set_search_start_hint(1);
426
                        search.previous(context.todo_file(), "foo");
427
                        assert_some_eq!(search.current_match(), 1);
428
                });
429
        }
430

431
        #[test]
432
        fn previous_first_match_with_hint_in_range_but_ahead() {
433
                with_todo_file(
434
                        &["pick bbb miss", "pick aaa foo", "pick bbb miss", "pick bbb foobar"],
435
                        |context| {
436
                                let mut search = Search::new();
437
                                search.set_search_start_hint(2);
438
                                search.previous(context.todo_file(), "foo");
439
                                assert_some_eq!(search.current_match(), 1);
440
                        },
441
                );
442
        }
443

444
        #[test]
445
        fn previous_first_match_with_hint_in_range_wrap() {
446
                with_todo_file(
447
                        &["pick bbb miss", "pick bbb miss", "pick aaa foo", "pick aaa foo"],
448
                        |context| {
449
                                let mut search = Search::new();
450
                                search.set_search_start_hint(1);
451
                                search.previous(context.todo_file(), "foo");
452
                                assert_some_eq!(search.current_match(), 3);
453
                        },
454
                );
455
        }
456

457
        #[test]
458
        fn previous_first_match_with_hint_out_of_range() {
459
                with_todo_file(
460
                        &["pick bbb miss", "pick aaa foo", "pick aaa foo", "pick bbb miss"],
461
                        |context| {
462
                                let mut search = Search::new();
463
                                search.set_search_start_hint(99);
464
                                search.previous(context.todo_file(), "foo");
465
                                assert_some_eq!(search.current_match(), 2);
466
                        },
467
                );
468
        }
469

470
        #[test]
471
        fn previous_continued_match() {
472
                with_todo_file(&["pick aaa foo", "pick aaa foo", "pick bbb foobar"], |context| {
473
                        let mut search = Search::new();
474
                        search.set_search_start_hint(2);
475
                        search.previous(context.todo_file(), "foo");
476
                        search.previous(context.todo_file(), "foo");
477
                        assert_some_eq!(search.current_match(), 1);
478
                });
479
        }
480

481
        #[test]
482
        fn previous_continued_match_wrap_single_match() {
483
                with_todo_file(&["pick aaa foo", "pick bbb miss"], |context| {
484
                        let mut search = Search::new();
485
                        search.previous(context.todo_file(), "foo");
486
                        search.previous(context.todo_file(), "foo");
487
                        assert_some_eq!(search.current_match(), 0);
488
                });
489
        }
490

491
        #[test]
492
        fn previous_continued_match_wrap() {
493
                with_todo_file(&["pick aaa foo", "pick bbb foobar"], |context| {
494
                        let mut search = Search::new();
495
                        search.previous(context.todo_file(), "foo");
496
                        search.previous(context.todo_file(), "foo");
497
                        assert_some_eq!(search.current_match(), 1);
498
                });
499
        }
500

501
        #[test]
502
        fn previous_updates_match_start_hint() {
503
                with_todo_file(&["pick bbb miss", "pick aaa foo"], |context| {
504
                        let mut search = Search::new();
505
                        search.previous(context.todo_file(), "foo");
506
                        assert_eq!(search.match_start_hint, 1);
507
                });
508
        }
509

510
        #[test]
511
        fn invalidate() {
512
                with_todo_file(&["pick abcdef foo"], |context| {
513
                        let mut search = Search::new();
514
                        search.next(context.todo_file(), "foo");
515
                        search.invalidate();
516
                        assert_eq!(search.total_results(), 0);
517
                });
518
        }
519

520
        #[test]
521
        fn cancel() {
522
                with_todo_file(&["pick abcdef foo"], |context| {
523
                        let mut search = Search::new();
524
                        search.next(context.todo_file(), "foo");
525
                        search.cancel();
526
                        assert_eq!(search.total_results(), 0);
527
                        assert_none!(search.current_match());
528
                        assert!(search.search_term.is_empty());
529
                });
530
        }
531

532
        #[test]
533
        fn current_match_with_match() {
534
                with_todo_file(&["pick abcdef foo"], |context| {
535
                        let mut search = Search::new();
536
                        search.next(context.todo_file(), "foo");
537
                        assert_some_eq!(search.current_match(), 0);
538
                });
539
        }
540

541
        #[test]
542
        fn current_match_with_no_match() {
543
                with_todo_file(&["pick abcdef foo"], |context| {
544
                        let mut search = Search::new();
545
                        search.next(context.todo_file(), "miss");
546
                        assert_none!(search.current_match());
547
                });
548
        }
549

550
        #[test]
551
        fn current_result_selected_with_match() {
552
                with_todo_file(&["pick abcdef foo"], |context| {
553
                        let mut search = Search::new();
554
                        search.next(context.todo_file(), "foo");
555
                        assert_some_eq!(search.current_result_selected(), 0);
556
                });
557
        }
558

559
        #[test]
560
        fn current_result_selected_with_no_match() {
561
                with_todo_file(&["pick abcdef foo"], |context| {
562
                        let mut search = Search::new();
563
                        search.next(context.todo_file(), "miss");
564
                        assert_none!(search.current_result_selected());
565
                });
566
        }
567

568
        #[test]
569
        fn total_results() {
570
                with_todo_file(&["pick abcdef foo"], |context| {
571
                        let mut search = Search::new();
572
                        search.next(context.todo_file(), "foo");
573
                        assert_eq!(search.total_results(), 1);
574
                });
575
        }
576
}
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