diffs_rs/engine/
selection.rs1use super::*;
2use crate::reading::{LineSelection, SourceRange};
3use crossterm::event::{MouseButton, MouseEventKind};
4
5pub(super) fn moves(command: Command) -> bool {
6 matches!(
7 command,
8 Command::Down
9 | Command::Up
10 | Command::PageDown
11 | Command::PageUp
12 | Command::FirstSource
13 | Command::LastSource
14 | Command::NextFile
15 | Command::PreviousFile
16 | Command::NextHunk
17 | Command::PreviousHunk
18 )
19}
20
21pub(super) fn extends(event: &Event) -> bool {
22 match event {
23 Event::Key(key) => Command::from_key(*key).is_some_and(moves),
24 Event::Mouse(mouse) => matches!(
25 mouse.kind,
26 MouseEventKind::ScrollDown
27 | MouseEventKind::ScrollUp
28 | MouseEventKind::Down(MouseButton::Left)
29 ),
30 _ => false,
31 }
32}
33
34impl Engine {
35 pub fn selection(&self) -> Option<SourceRange> {
38 self.app
39 .as_ref()?
40 .selection
41 .as_ref()
42 .map(|selection| selection.range)
43 }
44
45 pub fn selection_extending(&self) -> bool {
46 self.app
47 .as_ref()
48 .and_then(|app| app.selection.as_ref())
49 .is_some_and(|selection| selection.extending)
50 }
51
52 pub(super) fn selection_command(&mut self, command: Command) -> Result<InputEffect> {
53 let Some(app) = &mut self.app else {
54 return Ok(InputEffect::Ignored);
55 };
56 if app.help || app.sidebar.focused() || app.sidebar.diff.is_empty() {
57 return Ok(InputEffect::Ignored);
58 }
59 if command == Command::ClearSelection {
60 if app.selection.take().is_none() {
61 return Ok(InputEffect::Ignored);
62 }
63 } else if let Some(selection) = &mut app.selection
64 && selection.extending
65 {
66 selection.extending = false;
67 } else {
68 let Some(location) = self.selected_source()? else {
69 return Ok(InputEffect::Ignored);
70 };
71 let line = match location.side {
72 Side::Old => location.old_line,
73 Side::New => location.new_line,
74 }
75 .context("The selected side has no source line")?;
76 let app = self
77 .app
78 .as_mut()
79 .context("The review has no viewer state")?;
80 app.old_side = location.side == Side::Old;
81 let checked = matches!(
82 app.sources
83 .as_ref()
84 .and_then(|sources| sources.get(location.file)),
85 Some(SourceState::Checked(_))
86 );
87 app.selection = Some(LineSelection {
88 range: SourceRange {
89 review: self.id,
90 file: location.file,
91 side: location.side,
92 first: line,
93 last: line,
94 },
95 anchor: line,
96 extending: true,
97 checked,
98 });
99 }
100 self.changed(InputEffect::Redraw);
101 Ok(InputEffect::Redraw)
102 }
103
104 pub(super) fn track_selection(&mut self, extend: bool) -> Result<()> {
105 let Some(app) = &mut self.app else {
106 return Ok(());
107 };
108 let Some(mut selection) = app.selection.take() else {
109 return Ok(());
110 };
111 if app.row(app.cursor.row).map(|row| row.file) != Some(selection.range.file) {
112 self.changed(InputEffect::Redraw);
113 return Ok(());
114 }
115 let before = selection.range;
116 if extend
117 && selection.extending
118 && !app.help
119 && !app.sidebar.focused()
120 && let Some((id, old)) =
121 app.source_at_cursor(Some(selection.range.side == Side::Old))?
122 {
123 let info = app.line_info(id)?;
124 let line = if old { info.old } else { info.new }
125 .context("Selected range endpoint has no source")?;
126 selection.range.first = selection.anchor.min(line);
127 selection.range.last = selection.anchor.max(line);
128 }
129 let changed = before != selection.range;
130 app.selection = Some(selection);
131 if changed {
132 self.changed(InputEffect::Redraw);
133 }
134 Ok(())
135 }
136}