Skip to main content

diffs_rs/engine/
selection.rs

1use 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    /// The selected original-side range. It can include unavailable patch-only gaps.
36    /// `source_read` rejects those gaps when streaming, before visible publication.
37    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}