1use std::cmp::min;
2use std::path::Path;
3use std::sync::mpsc::Sender;
4
5use cursive::event::Event;
6use cursive::theme::{BaseColor, Effect};
7use cursive::traits::{Nameable, Resizable};
8use cursive::utils::markup::StyledString;
9use cursive::views::{Checkbox, Dialog, HideableView, LinearLayout, ScrollView, TextView};
10use cursive::{CursiveRunnable, CursiveRunner, View};
11use tracing::error;
12
13use crate::cursive_utils::{EventDrivenCursiveApp, EventDrivenCursiveAppExt};
14use crate::tristate::{Tristate, TristateBox};
15use crate::{FileState, RecordError, RecordState, Section, SectionChangedLine};
16
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
18pub enum SectionChangedLineType {
19 Before,
20 After,
21}
22
23#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
24pub struct FileKey {
25 file_num: usize,
26}
27
28impl FileKey {
29 fn view_id(&self) -> String {
30 let Self { file_num } = self;
31 format!("FileKey({})", file_num)
32 }
33}
34
35#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
36pub struct SectionKey {
37 file_num: usize,
38 section_num: usize,
39}
40
41impl SectionKey {
42 fn view_id(&self) -> String {
43 let Self {
44 file_num,
45 section_num,
46 } = self;
47 format!("HunkKey({},{})", *file_num, section_num)
48 }
49}
50
51#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
52pub struct SectionLineKey {
53 file_num: usize,
54 section_num: usize,
55 section_type: SectionChangedLineType,
56 section_line_num: usize,
57}
58
59impl SectionLineKey {
60 fn view_id(&self) -> String {
61 let Self {
62 file_num,
63 section_num,
64 section_type,
65 section_line_num,
66 } = self;
67 format!(
68 "HunkLineKey({},{},{},{})",
69 file_num,
70 section_num,
71 match section_type {
72 SectionChangedLineType::Before => "B",
73 SectionChangedLineType::After => "A",
74 },
75 section_line_num
76 )
77 }
78}
79
80pub struct Recorder<'a> {
82 did_user_confirm_exit: bool,
83 state: RecordState<'a>,
84}
85
86impl<'a> Recorder<'a> {
87 pub fn new(state: RecordState<'a>) -> Self {
89 Self {
90 did_user_confirm_exit: false,
91 state,
92 }
93 }
94
95 pub fn run(self, siv: CursiveRunner<CursiveRunnable>) -> Result<RecordState<'a>, RecordError> {
98 EventDrivenCursiveAppExt::run(self, siv)
99 }
100
101 fn make_main_view(&self, main_tx: Sender<Message>) -> impl View {
102 let mut view = LinearLayout::vertical();
103
104 let RecordState { file_states } = &self.state;
105
106 let global_num_changed_sections: usize = file_states
107 .iter()
108 .map(|(_path, file_state)| file_state.count_changed_sections())
109 .sum();
110 let mut global_changed_section_num = 0;
111
112 for (file_num, (path, file_state)) in file_states.iter().enumerate() {
113 let file_key = FileKey { file_num };
114 view.add_child(self.make_file_view(
115 main_tx.clone(),
116 path,
117 file_key,
118 file_state,
119 &mut global_changed_section_num,
120 global_num_changed_sections,
121 ));
122 if file_num + 1 < file_states.len() {
123 view.add_child(TextView::new(" "));
126 }
127 }
128
129 view
130 }
131
132 fn make_file_view(
133 &self,
134 main_tx: Sender<Message>,
135 path: &Path,
136 file_key: FileKey,
137 file_state: &FileState,
138 global_changed_section_num: &mut usize,
139 global_num_changed_sections: usize,
140 ) -> impl View {
141 let FileKey { file_num } = file_key;
142
143 let mut file_view = LinearLayout::vertical();
144 let mut line_num: usize = 1;
145 let local_num_changed_sections = file_state.count_changed_sections();
146 let mut local_changed_section_num = 0;
147
148 let file_header_view = LinearLayout::horizontal()
149 .child(
150 TristateBox::new()
151 .with_state({
152 all_are_same_value(iter_file_selections(file_state)).into_tristate()
153 })
154 .on_change({
155 let main_tx = main_tx.clone();
156 move |_, new_value| {
157 if main_tx
158 .send(Message::ToggleFile(file_key, new_value))
159 .is_err()
160 {
161 }
163 }
164 })
165 .with_name(file_key.view_id()),
166 )
167 .child(TextView::new({
168 let mut s = StyledString::new();
169 s.append_plain(" ");
170 s.append_styled(path.to_string_lossy(), Effect::Bold);
171 s
172 }));
173 file_view.add_child(file_header_view);
174
175 let FileState {
176 file_mode: _,
177 sections,
178 } = file_state;
179 for (section_num, section) in sections.iter().enumerate() {
180 match section {
181 Section::Unchanged { contents } => {
182 const CONTEXT: usize = 2;
183
184 if section_num > 0 {
186 let end_index = min(CONTEXT, contents.len());
187 for (i, line) in contents[..end_index].iter().enumerate() {
188 file_view.add_child(TextView::new(format!(
189 " {} {}",
190 line_num + i,
191 line
192 )));
193 }
194 }
195
196 if section_num > 0 && section_num + 1 < sections.len() {
198 file_view.add_child(TextView::new(":"));
199 }
200
201 if section_num + 1 < sections.len() {
203 let start_index = contents.len().saturating_sub(CONTEXT);
204 for (i, line) in contents[start_index..].iter().enumerate() {
205 file_view.add_child(TextView::new(format!(
206 " {} {}",
207 line_num + start_index + i,
208 line
209 )));
210 }
211 }
212
213 line_num += contents.len();
214 }
215
216 Section::Changed { before, after } => {
217 local_changed_section_num += 1;
218 *global_changed_section_num += 1;
219 let description = format!(
220 "section {}/{} in current file, {}/{} total",
221 local_changed_section_num,
222 local_num_changed_sections,
223 global_changed_section_num,
224 global_num_changed_sections
225 );
226
227 self.make_changed_section_views(
228 main_tx.clone(),
229 &mut file_view,
230 file_num,
231 section_num,
232 description,
233 before,
234 after,
235 );
236 line_num += before.len();
237 }
238
239 Section::FileMode {
240 is_selected: _,
241 before: _,
242 after: _,
243 } => {
244 unimplemented!("make_file_view with Section::FileMode");
245 }
246 }
247 }
248
249 file_view
250 }
251
252 fn make_changed_section_views(
253 &self,
254 main_tx: Sender<Message>,
255 view: &mut LinearLayout,
256 file_num: usize,
257 section_num: usize,
258 section_description: String,
259 before: &[SectionChangedLine],
260 after: &[SectionChangedLine],
261 ) {
262 let mut section_view = LinearLayout::vertical();
263 let section_key = SectionKey {
264 file_num,
265 section_num,
266 };
267
268 for (section_line_num, section_changed_line) in before.iter().enumerate() {
269 let section_line_key = SectionLineKey {
270 file_num,
271 section_num,
272 section_type: SectionChangedLineType::Before,
273 section_line_num,
274 };
275 section_view.add_child(self.make_changed_line_view(
276 main_tx.clone(),
277 section_line_key,
278 section_changed_line,
279 ));
280 }
281
282 for (section_line_num, section_changed_line) in after.iter().enumerate() {
283 let section_line_key = SectionLineKey {
284 file_num,
285 section_num,
286 section_type: SectionChangedLineType::After,
287 section_line_num,
288 };
289 section_view.add_child(self.make_changed_line_view(
290 main_tx.clone(),
291 section_line_key,
292 section_changed_line,
293 ));
294 }
295
296 view.add_child(
297 LinearLayout::horizontal()
298 .child(TextView::new(" "))
299 .child(
300 TristateBox::new()
301 .with_state(
302 all_are_same_value(before.iter().chain(after.iter()).map(
303 |SectionChangedLine {
304 is_selected,
305 line: _,
306 }| { *is_selected },
307 ))
308 .into_tristate(),
309 )
310 .on_change({
311 let section_key = SectionKey {
312 file_num,
313 section_num,
314 };
315 let main_tx = main_tx;
316 move |_, new_value| {
317 if main_tx
318 .send(Message::ToggleHunk(section_key, new_value))
319 .is_err()
320 {
321 }
323 }
324 })
325 .with_name(section_key.view_id()),
326 )
327 .child(TextView::new({
328 let mut s = StyledString::new();
329 s.append_plain(" ");
330 s.append_plain(section_description);
331 s
332 })),
333 );
334 view.add_child(HideableView::new(section_view));
335 }
336
337 fn make_changed_line_view(
338 &self,
339 main_tx: Sender<Message>,
340 section_line_key: SectionLineKey,
341 section_changed_line: &SectionChangedLine,
342 ) -> impl View {
343 let SectionChangedLine { is_selected, line } = section_changed_line;
344
345 let line_contents = {
346 let (line, style) = match section_line_key.section_type {
347 SectionChangedLineType::Before => (format!(" -{}", line), BaseColor::Red.dark()),
348 SectionChangedLineType::After => (format!(" +{}", line), BaseColor::Green.dark()),
349 };
350 let mut s = StyledString::new();
351 s.append_styled(line, style);
352 s
353 };
354
355 LinearLayout::horizontal()
356 .child(TextView::new(" "))
357 .child(
358 Checkbox::new()
359 .with_checked(*is_selected)
360 .on_change({
361 move |_, is_selected| {
362 if main_tx
363 .send(Message::ToggleHunkLine(section_line_key, is_selected))
364 .is_err()
365 {
366 }
368 }
369 })
370 .with_name(section_line_key.view_id()),
371 )
372 .child(TextView::new(line_contents))
373 }
374
375 fn toggle_file(
376 &mut self,
377 siv: &mut CursiveRunner<CursiveRunnable>,
378 file_key: FileKey,
379 new_value: Tristate,
380 ) {
381 let FileKey { file_num } = file_key;
382 let (_path, file_state) = &mut self.state.file_states[file_num];
383
384 let new_value = match new_value {
385 Tristate::Unchecked => false,
386 Tristate::Partial => {
387 true
389 }
390 Tristate::Checked => true,
391 };
392
393 let FileState {
394 file_mode: _,
395 sections,
396 } = file_state;
397 for (section_num, section) in sections.iter_mut().enumerate() {
398 match section {
399 Section::Unchanged { contents: _ } => {
400 }
402 Section::Changed { before, after } => {
403 for (
404 section_line_num,
405 SectionChangedLine {
406 is_selected,
407 line: _,
408 },
409 ) in before.iter_mut().enumerate()
410 {
411 *is_selected = new_value;
412 let section_line_key = SectionLineKey {
413 file_num,
414 section_num,
415 section_type: SectionChangedLineType::Before,
416 section_line_num,
417 };
418 siv.call_on_name(§ion_line_key.view_id(), |checkbox: &mut Checkbox| {
419 checkbox.set_checked(new_value)
420 });
421 }
422
423 for (
424 section_line_num,
425 SectionChangedLine {
426 is_selected,
427 line: _,
428 },
429 ) in after.iter_mut().enumerate()
430 {
431 *is_selected = new_value;
432 let section_line_key = SectionLineKey {
433 file_num,
434 section_num,
435 section_type: SectionChangedLineType::After,
436 section_line_num,
437 };
438 siv.call_on_name(§ion_line_key.view_id(), |checkbox: &mut Checkbox| {
439 checkbox.set_checked(new_value)
440 });
441 }
442 }
443 Section::FileMode {
444 is_selected: _,
445 before: _,
446 after: _,
447 } => {
448 unimplemented!("toggle_file for Section::FileMode");
449 }
450 }
451
452 let section_key = SectionKey {
453 file_num,
454 section_num,
455 };
456 siv.call_on_name(§ion_key.view_id(), |tristate_box: &mut TristateBox| {
457 tristate_box.set_state(if new_value {
458 Tristate::Checked
459 } else {
460 Tristate::Unchecked
461 });
462 });
463 }
464 }
465
466 fn toggle_section(
467 &mut self,
468 siv: &mut CursiveRunner<CursiveRunnable>,
469 section_key: SectionKey,
470 new_value: Tristate,
471 ) {
472 let SectionKey {
473 file_num,
474 section_num,
475 } = section_key;
476
477 let new_value = match new_value {
478 Tristate::Unchecked => false,
479 Tristate::Partial => {
480 true
482 }
483 Tristate::Checked => true,
484 };
485
486 let (before, after) = {
487 let (
488 path,
489 FileState {
490 file_mode: _,
491 sections,
492 },
493 ) = &mut self.state.file_states[file_num];
494 match &mut sections[section_num] {
495 Section::Unchanged { contents } => {
496 error!(
497 ?section_num,
498 ?path,
499 ?contents,
500 "Invalid section num to change"
501 );
502 panic!("Invalid section num to change");
503 }
504 Section::Changed { before, after } => (before, after),
505 Section::FileMode {
506 is_selected: _,
507 before: _,
508 after: _,
509 } => {
510 unimplemented!("toggle_section for Section::FileMode");
511 }
512 }
513 };
514
515 for changed_line in before.iter_mut() {
517 changed_line.is_selected = new_value;
518 }
519 for changed_line in after.iter_mut() {
520 changed_line.is_selected = new_value;
521 }
522 let section_line_keys = (0..before.len())
523 .map(|section_line_num| SectionLineKey {
524 file_num,
525 section_num,
526 section_type: SectionChangedLineType::Before,
527 section_line_num,
528 })
529 .chain((0..after.len()).map(|section_line_num| SectionLineKey {
530 file_num,
531 section_num,
532 section_type: SectionChangedLineType::After,
533 section_line_num,
534 }));
535 for section_line_key in section_line_keys {
536 siv.call_on_name(§ion_line_key.view_id(), |checkbox: &mut Checkbox| {
537 checkbox.set_checked(new_value);
538 });
539 }
540
541 self.refresh_file(siv, FileKey { file_num });
542 }
543
544 fn toggle_section_line(
545 &mut self,
546 siv: &mut CursiveRunner<CursiveRunnable>,
547 section_line_key: SectionLineKey,
548 new_value: bool,
549 ) {
550 let SectionLineKey {
551 file_num,
552 section_num,
553 section_type,
554 section_line_num,
555 } = section_line_key;
556
557 let (
558 path,
559 FileState {
560 file_mode: _,
561 sections,
562 },
563 ) = &mut self.state.file_states[file_num];
564 let section = &mut sections[section_num];
565 let section_changed_lines = match (section, section_type) {
566 (Section::Unchanged { contents }, _) => {
567 error!(
568 ?section_num,
569 ?path,
570 ?contents,
571 "Invalid section num to change"
572 );
573 panic!("Invalid section num to change");
574 }
575 (Section::Changed { before, .. }, SectionChangedLineType::Before) => before,
576 (Section::Changed { after, .. }, SectionChangedLineType::After) => after,
577 (
578 Section::FileMode {
579 is_selected: _,
580 before: _,
581 after: _,
582 },
583 _,
584 ) => unimplemented!("toggle_section_line for Section::FileMode"),
585 };
586 section_changed_lines[section_line_num].is_selected = new_value;
587
588 self.refresh_section(
589 siv,
590 SectionKey {
591 file_num,
592 section_num,
593 },
594 );
595 self.refresh_file(siv, FileKey { file_num });
596 }
597
598 fn refresh_section(
599 &mut self,
600 siv: &mut CursiveRunner<CursiveRunnable>,
601 section_key: SectionKey,
602 ) {
603 let SectionKey {
604 file_num,
605 section_num,
606 } = section_key;
607 let (
608 _path,
609 FileState {
610 file_mode: _,
611 sections,
612 },
613 ) = &mut self.state.file_states[file_num];
614
615 let section_selections = iter_section_selections(§ions[section_num]);
616 let section_new_value = all_are_same_value(section_selections).into_tristate();
617 let section_key = SectionKey {
618 file_num,
619 section_num,
620 };
621 siv.call_on_name(§ion_key.view_id(), |tristate_box: &mut TristateBox| {
622 tristate_box.set_state(section_new_value);
623 });
624 }
625
626 fn refresh_file(&mut self, siv: &mut CursiveRunner<CursiveRunnable>, file_key: FileKey) {
627 let FileKey { file_num } = file_key;
628 let file_state = &mut self.state.file_states[file_num].1;
629
630 let file_selections = iter_file_selections(file_state);
631 let file_new_value = all_are_same_value(file_selections).into_tristate();
632 siv.call_on_name(&file_key.view_id(), |tristate_box: &mut TristateBox| {
633 tristate_box.set_state(file_new_value);
634 });
635 }
636}
637
638fn iter_file_selections<'a>(file_state: &'a FileState) -> impl Iterator<Item = bool> + 'a {
639 let FileState {
640 file_mode: _,
641 sections,
642 } = file_state;
643 sections.iter().flat_map(iter_section_selections)
644}
645
646fn iter_section_selections<'a>(section: &'a Section) -> impl Iterator<Item = bool> + 'a {
647 let iter: Box<dyn Iterator<Item = bool>> = match section {
648 Section::Changed { before, after } => Box::new(
649 before
650 .iter()
651 .map(|changed_line| changed_line.is_selected)
652 .chain(after.iter().map(|changed_line| changed_line.is_selected)),
653 ),
654 Section::Unchanged { contents: _ } => Box::new(std::iter::empty()),
655 Section::FileMode {
656 is_selected: _,
657 before: _,
658 after: _,
659 } => unimplemented!("iter_section_changed_lines for Section::FileMode"),
660 };
661 iter
662}
663
664#[derive(Clone, Debug)]
665pub enum Message {
666 Init,
667 ToggleFile(FileKey, Tristate),
668 ToggleHunk(SectionKey, Tristate),
669 ToggleHunkLine(SectionLineKey, bool),
670 Confirm,
671 Quit,
672}
673
674impl<'a> EventDrivenCursiveApp for Recorder<'a> {
675 type Message = Message;
676
677 type Return = Result<RecordState<'a>, RecordError>;
678
679 fn get_init_message(&self) -> Self::Message {
680 Message::Init
681 }
682
683 fn get_key_bindings(&self) -> Vec<(Event, Self::Message)> {
684 vec![
685 ('c'.into(), Message::Confirm),
686 ('C'.into(), Message::Confirm),
687 ('q'.into(), Message::Quit),
688 ('Q'.into(), Message::Quit),
689 ]
690 }
691
692 fn handle_message(
693 &mut self,
694 siv: &mut CursiveRunner<CursiveRunnable>,
695 main_tx: Sender<Self::Message>,
696 message: Self::Message,
697 ) {
698 match message {
699 Message::Init => {
700 let main_view = self.make_main_view(main_tx);
701 siv.add_layer(ScrollView::new(
702 main_view.min_width(80),
706 ));
707 }
708
709 Message::ToggleFile(file_key, new_value) => {
710 self.toggle_file(siv, file_key, new_value);
711 }
712
713 Message::ToggleHunk(section_key, new_value) => {
714 self.toggle_section(siv, section_key, new_value);
715 }
716
717 Message::ToggleHunkLine(section_line_key, new_value) => {
718 self.toggle_section_line(siv, section_line_key, new_value);
719 }
720
721 Message::Confirm => {
722 self.did_user_confirm_exit = true;
723 siv.quit();
724 }
725
726 Message::Quit => {
727 let has_changes = {
728 let RecordState { file_states } = &self.state;
729 let changed_lines = file_states
730 .iter()
731 .flat_map(|(_, file_state)| iter_file_selections(file_state));
732 match all_are_same_value(changed_lines) {
733 SameValueResult::Empty | SameValueResult::AllSame(_) => false,
734 SameValueResult::SomeDifferent => true,
735 }
736 };
737
738 if has_changes {
739 siv.add_layer(
740 Dialog::text(
741 "Are you sure you want to quit? Your selections will be lost.",
742 )
743 .button("Ok", |siv| {
744 siv.quit();
745 })
746 .dismiss_button("Cancel"),
747 );
748 } else {
749 siv.quit();
750 }
751 }
752 }
753 }
754
755 fn finish(self) -> Self::Return {
756 if self.did_user_confirm_exit {
757 Ok(self.state)
758 } else {
759 Err(RecordError::Cancelled)
760 }
761 }
762}
763
764enum SameValueResult<T> {
765 Empty,
766 AllSame(T),
767 SomeDifferent,
768}
769
770impl SameValueResult<bool> {
771 fn into_tristate(self) -> Tristate {
772 match self {
773 SameValueResult::Empty | SameValueResult::AllSame(true) => Tristate::Checked,
774 SameValueResult::AllSame(false) => Tristate::Unchecked,
775 SameValueResult::SomeDifferent => Tristate::Partial,
776 }
777 }
778}
779
780fn all_are_same_value<Iter, Item>(iter: Iter) -> SameValueResult<Item>
781where
782 Iter: IntoIterator<Item = Item>,
783 Item: Eq,
784{
785 let mut first_value = None;
786 for value in iter {
787 match &first_value {
788 Some(first_value) => {
789 if &value != first_value {
790 return SameValueResult::SomeDifferent;
791 }
792 }
793 None => {
794 first_value = Some(value);
795 }
796 }
797 }
798
799 match first_value {
800 Some(value) => SameValueResult::AllSame(value),
801 None => SameValueResult::Empty,
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use std::path::PathBuf;
808 use std::rc::Rc;
809 use std::{borrow::Cow, convert::Infallible};
810
811 use cursive::event::Key;
812
813 use crate::cursive_utils::testing::{
814 screen_to_string, CursiveTestingBackend, CursiveTestingEvent,
815 };
816
817 use super::*;
818
819 fn run_test(
820 state: RecordState,
821 events: Vec<CursiveTestingEvent>,
822 ) -> Result<RecordState, RecordError> {
823 let siv = CursiveRunnable::new::<Infallible, _>(move || {
824 Ok(CursiveTestingBackend::init(events.clone()))
825 });
826 let recorder = Recorder::new(state);
827 recorder.run(siv.into_runner())
828 }
829
830 fn example_record_state() -> RecordState<'static> {
831 RecordState {
832 file_states: vec![(
833 PathBuf::from("foo"),
834 FileState {
835 file_mode: None,
836 sections: vec![
837 Section::Unchanged {
838 contents: vec![
839 Cow::Borrowed("unchanged 1\n"),
840 Cow::Borrowed("unchanged 2\n"),
841 ],
842 },
843 Section::Changed {
844 before: vec![
845 SectionChangedLine {
846 is_selected: true,
847 line: Cow::Borrowed("before 1\n"),
848 },
849 SectionChangedLine {
850 is_selected: true,
851 line: Cow::Borrowed("before 2\n"),
852 },
853 ],
854 after: vec![
855 SectionChangedLine {
856 is_selected: true,
857 line: Cow::Borrowed("after 1\n"),
858 },
859 SectionChangedLine {
860 is_selected: false,
861 line: Cow::Borrowed("after 2\n"),
862 },
863 ],
864 },
865 ],
866 },
867 )],
868 }
869 }
870
871 #[test]
872 fn test_cancel() {
873 let screenshot1 = Default::default();
874 let result = run_test(
875 example_record_state(),
876 vec![
877 CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
878 CursiveTestingEvent::Event('q'.into()),
879 CursiveTestingEvent::Event(Key::Enter.into()),
880 ],
881 );
882 insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
883 [~] foo
884 1 unchanged 1
885 2 unchanged 2
886 [~] section 1/1 in current file, 1/1 total
887 [X] -before 1
888 [X] -before 2
889 [X] +after 1
890 [ ] +after 2
891 "###);
892 insta::assert_debug_snapshot!(result, @r###"
893 Err(
894 Cancelled,
895 )
896 "###);
897 }
898
899 #[test]
900 fn test_cancel_no_confirm() {
901 let screenshot1 = Default::default();
902 let result = run_test(
903 example_record_state(),
904 vec![
905 CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(' '.into()),
911 CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
912 CursiveTestingEvent::Event('q'.into()), ],
914 );
915 insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
916 [X] foo
917 1 unchanged 1
918 2 unchanged 2
919 [X] section 1/1 in current file, 1/1 total
920 [X] -before 1
921 [X] -before 2
922 [X] +after 1
923 [X] +after 2
924 "###);
925 insta::assert_debug_snapshot!(result, @r###"
926 Err(
927 Cancelled,
928 )
929 "###);
930 }
931
932 #[test]
933 fn test_section_toggle() {
934 let screenshot1 = Default::default();
935 let screenshot2 = Default::default();
936 let screenshot3 = Default::default();
937 let screenshot4 = Default::default();
938 let result = run_test(
939 example_record_state(),
940 vec![
941 CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(' '.into()), CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
944 CursiveTestingEvent::Event(' '.into()), CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot2)),
946 CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(' '.into()), CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot3)),
949 CursiveTestingEvent::Event(Key::Up.into()), CursiveTestingEvent::Event(' '.into()), CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot4)),
952 CursiveTestingEvent::Event('c'.into()),
953 ],
954 );
955
956 insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
957 [X] foo
958 1 unchanged 1
959 2 unchanged 2
960 [X] section 1/1 in current file, 1/1 total
961 [X] -before 1
962 [X] -before 2
963 [X] +after 1
964 [X] +after 2
965 "###);
966 insta::assert_snapshot!(screen_to_string(&screenshot2), @r###"
967 [ ] foo
968 1 unchanged 1
969 2 unchanged 2
970 [ ] section 1/1 in current file, 1/1 total
971 [ ] -before 1
972 [ ] -before 2
973 [ ] +after 1
974 [ ] +after 2
975 "###);
976 insta::assert_snapshot!(screen_to_string(&screenshot3), @r###"
977 [~] foo
978 1 unchanged 1
979 2 unchanged 2
980 [~] section 1/1 in current file, 1/1 total
981 [X] -before 1
982 [ ] -before 2
983 [ ] +after 1
984 [ ] +after 2
985 "###);
986 insta::assert_snapshot!(screen_to_string(&screenshot4), @r###"
987 [X] foo
988 1 unchanged 1
989 2 unchanged 2
990 [X] section 1/1 in current file, 1/1 total
991 [X] -before 1
992 [X] -before 2
993 [X] +after 1
994 [X] +after 2
995 "###);
996 insta::assert_debug_snapshot!(result, @r###"
997 Ok(
998 RecordState {
999 file_states: [
1000 (
1001 "foo",
1002 FileState {
1003 file_mode: None,
1004 sections: [
1005 Unchanged {
1006 contents: [
1007 "unchanged 1\n",
1008 "unchanged 2\n",
1009 ],
1010 },
1011 Changed {
1012 before: [
1013 SectionChangedLine {
1014 is_selected: true,
1015 line: "before 1\n",
1016 },
1017 SectionChangedLine {
1018 is_selected: true,
1019 line: "before 2\n",
1020 },
1021 ],
1022 after: [
1023 SectionChangedLine {
1024 is_selected: true,
1025 line: "after 1\n",
1026 },
1027 SectionChangedLine {
1028 is_selected: true,
1029 line: "after 2\n",
1030 },
1031 ],
1032 },
1033 ],
1034 },
1035 ),
1036 ],
1037 },
1038 )
1039 "###);
1040 }
1041
1042 #[test]
1043 fn test_file_toggle() {
1044 let screenshot1 = Default::default();
1045 let screenshot2 = Default::default();
1046 let screenshot3 = Default::default();
1047 let screenshot4 = Default::default();
1048 let result = run_test(
1049 example_record_state(),
1050 vec![
1051 CursiveTestingEvent::Event(' '.into()), CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
1053 CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(' '.into()), CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot2)),
1056 CursiveTestingEvent::Event(Key::Down.into()), CursiveTestingEvent::Event(' '.into()), CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot3)),
1059 CursiveTestingEvent::Event(Key::Up.into()),
1060 CursiveTestingEvent::Event(Key::Up.into()), CursiveTestingEvent::Event(' '.into()), CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot4)),
1063 CursiveTestingEvent::Event('c'.into()),
1064 ],
1065 );
1066
1067 insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
1068 [X] foo
1069 1 unchanged 1
1070 2 unchanged 2
1071 [X] section 1/1 in current file, 1/1 total
1072 [X] -before 1
1073 [X] -before 2
1074 [X] +after 1
1075 [X] +after 2
1076 "###);
1077 insta::assert_snapshot!(screen_to_string(&screenshot2), @r###"
1078 [ ] foo
1079 1 unchanged 1
1080 2 unchanged 2
1081 [ ] section 1/1 in current file, 1/1 total
1082 [ ] -before 1
1083 [ ] -before 2
1084 [ ] +after 1
1085 [ ] +after 2
1086 "###);
1087 insta::assert_snapshot!(screen_to_string(&screenshot3), @r###"
1088 [~] foo
1089 1 unchanged 1
1090 2 unchanged 2
1091 [~] section 1/1 in current file, 1/1 total
1092 [X] -before 1
1093 [ ] -before 2
1094 [ ] +after 1
1095 [ ] +after 2
1096 "###);
1097 insta::assert_snapshot!(screen_to_string(&screenshot4), @r###"
1098 [X] foo
1099 1 unchanged 1
1100 2 unchanged 2
1101 [X] section 1/1 in current file, 1/1 total
1102 [X] -before 1
1103 [X] -before 2
1104 [X] +after 1
1105 [X] +after 2
1106 "###);
1107 insta::assert_debug_snapshot!(result, @r###"
1108 Ok(
1109 RecordState {
1110 file_states: [
1111 (
1112 "foo",
1113 FileState {
1114 file_mode: None,
1115 sections: [
1116 Unchanged {
1117 contents: [
1118 "unchanged 1\n",
1119 "unchanged 2\n",
1120 ],
1121 },
1122 Changed {
1123 before: [
1124 SectionChangedLine {
1125 is_selected: true,
1126 line: "before 1\n",
1127 },
1128 SectionChangedLine {
1129 is_selected: true,
1130 line: "before 2\n",
1131 },
1132 ],
1133 after: [
1134 SectionChangedLine {
1135 is_selected: true,
1136 line: "after 1\n",
1137 },
1138 SectionChangedLine {
1139 is_selected: true,
1140 line: "after 2\n",
1141 },
1142 ],
1143 },
1144 ],
1145 },
1146 ),
1147 ],
1148 },
1149 )
1150 "###);
1151 }
1152
1153 #[test]
1154 fn test_initial_tristate_states() {
1155 let state = {
1156 let mut state = example_record_state();
1157 let (
1158 _path,
1159 FileState {
1160 file_mode: _,
1161 sections,
1162 },
1163 ) = &mut state.file_states[0];
1164 for section in sections {
1165 if let Section::Changed { before, after: _ } = section {
1166 before[0].is_selected = true;
1167 }
1168 }
1169 state
1170 };
1171
1172 let screenshot1 = Default::default();
1173 let result = run_test(
1174 state,
1175 vec![
1176 CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
1177 CursiveTestingEvent::Event('q'.into()),
1178 CursiveTestingEvent::Event(Key::Enter.into()),
1179 ],
1180 );
1181 insta::assert_debug_snapshot!(result, @r###"
1182 Err(
1183 Cancelled,
1184 )
1185 "###);
1186 insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
1187 [~] foo
1188 1 unchanged 1
1189 2 unchanged 2
1190 [~] section 1/1 in current file, 1/1 total
1191 [X] -before 1
1192 [X] -before 2
1193 [X] +after 1
1194 [ ] +after 2
1195 "###);
1196 }
1197
1198 #[test]
1199 fn test_context() {
1200 let state = RecordState {
1201 file_states: vec![(
1202 PathBuf::from("foo"),
1203 FileState {
1204 file_mode: None,
1205 sections: vec![
1206 Section::Unchanged {
1207 contents: vec![
1208 Cow::Borrowed("foo"),
1209 Cow::Borrowed("bar"),
1210 Cow::Borrowed("baz"),
1211 Cow::Borrowed("qux"),
1212 ],
1213 },
1214 Section::Changed {
1215 before: vec![SectionChangedLine {
1216 is_selected: false,
1217 line: Cow::Borrowed("changed 1"),
1218 }],
1219 after: vec![
1220 SectionChangedLine {
1221 is_selected: false,
1222 line: Cow::Borrowed("changed 2"),
1223 },
1224 SectionChangedLine {
1225 is_selected: false,
1226 line: Cow::Borrowed("changed 3"),
1227 },
1228 ],
1229 },
1230 Section::Unchanged {
1231 contents: vec![
1232 Cow::Borrowed("foo"),
1233 Cow::Borrowed("bar"),
1234 Cow::Borrowed("baz"),
1235 Cow::Borrowed("qux"),
1236 ],
1237 },
1238 ],
1239 },
1240 )],
1241 };
1242
1243 let screenshot = Default::default();
1244 let result = run_test(
1245 state,
1246 vec![
1247 CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot)),
1248 CursiveTestingEvent::Event('q'.into()),
1249 ],
1250 );
1251
1252 insta::assert_debug_snapshot!(result, @r###"
1253 Err(
1254 Cancelled,
1255 )
1256 "###);
1257 insta::assert_snapshot!(screen_to_string(&screenshot), @r###"
1258 [ ] foo
1259 3 baz
1260 4 qux
1261 [ ] section 1/1 in current file, 1/1 total
1262 [ ] -changed 1
1263 [ ] +changed 2
1264 [ ] +changed 3
1265 6 foo
1266 7 bar
1267 "###);
1268 }
1269
1270 #[test]
1271 fn test_render_multiple_sections() -> eyre::Result<()> {
1272 let mut state = example_record_state();
1273 let sections = {
1274 let (_, FileState { sections, .. }) = &state.file_states[0];
1275 sections.clone()
1276 };
1277 state.file_states[0].1.sections = [sections.clone(), sections].concat();
1278
1279 let screenshot = Default::default();
1280 let result = run_test(
1281 state,
1282 vec![
1283 CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot)),
1284 CursiveTestingEvent::Event('q'.into()),
1285 CursiveTestingEvent::Event(Key::Enter.into()),
1286 ],
1287 );
1288
1289 insta::assert_debug_snapshot!(result, @r###"
1290 Err(
1291 Cancelled,
1292 )
1293 "###);
1294 insta::assert_snapshot!(screen_to_string(&screenshot), @r###"
1295 [~] foo
1296 1 unchanged 1
1297 2 unchanged 2
1298 [~] section 1/2 in current file, 1/2 total
1299 [X] -before 1
1300 [X] -before 2
1301 [X] +after 1
1302 [ ] +after 2
1303 5 unchanged 1
1304 6 unchanged 2
1305 :
1306 5 unchanged 1
1307 6 unchanged 2
1308 [~] section 2/2 in current file, 2/2 total
1309 [X] -before 1
1310 [X] -before 2
1311 [X] +after 1
1312 [ ] +after 2
1313 "###);
1314
1315 Ok(())
1316 }
1317}