Skip to main content

diffs_rs/
app.rs

1use anyhow::{Context, Result};
2
3mod controls;
4pub use controls::{Command, InputEffect};
5mod sidebar;
6mod source;
7pub use sidebar::SidebarPreference;
8
9use crate::{
10    highlight::{Cache, Request, Worker},
11    model::{Kind, Row, RowKind},
12    render,
13    storage::Document,
14    theme::{self, Appearance, Colors},
15    view::{LineId, View},
16};
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
19pub enum Layout {
20    #[default]
21    Auto,
22    Split,
23    Unified,
24}
25
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
27pub struct Position {
28    pub row: usize,
29    pub part: usize,
30}
31
32#[derive(Clone, Copy)]
33struct ReadingAnchor {
34    source: LineId,
35    display_byte: usize,
36    old_side: bool,
37    screen_row: Option<usize>,
38}
39
40pub struct App {
41    pub(crate) view: View,
42    pub(crate) sidebar: sidebar::Sidebar,
43    pub doc: Document,
44    pub loading: bool,
45    pub sources: Option<Vec<crate::input::SourceState>>,
46    pub(crate) source_reads: crate::reading::SourceLeases,
47    pub label: String,
48    pub layout: Layout,
49    pub appearance: Appearance,
50    pub colors: Colors,
51    pub top: Position,
52    pub cursor: Position,
53    pub old_side: bool,
54    pub horizontal: usize,
55    pub wrap: bool,
56    pub numbers: bool,
57    pub help: bool,
58    pub cache: Cache,
59    pub geometry: crate::geometry::Cache,
60    pub epoch: u64,
61    pub width: u16,
62    pub height: u16,
63    pub visible: Vec<(Position, u16)>,
64    pub cursor_screen_row: usize,
65    pub worker: Worker,
66    pub(crate) search_match: Option<crate::reading::SearchMatch>,
67    pub(crate) selection: Option<crate::reading::LineSelection>,
68    reading_anchor: Option<ReadingAnchor>,
69}
70
71impl App {
72    pub fn new(
73        doc: Document,
74        label: String,
75        layout: Layout,
76        appearance: Appearance,
77        wake: std::sync::mpsc::SyncSender<()>,
78    ) -> anyhow::Result<Self> {
79        let colors = theme::load(appearance)?.0;
80        let worker = crate::highlight::start(wake);
81        Ok(Self {
82            view: View::default(),
83            sidebar: sidebar::Sidebar::default(),
84            doc,
85            loading: true,
86            sources: None,
87            source_reads: crate::reading::SourceLeases::default(),
88            label,
89            layout,
90            appearance,
91            colors,
92            worker,
93            search_match: None,
94            selection: None,
95            top: Position::default(),
96            cursor: Position::default(),
97            old_side: false,
98            horizontal: 0,
99            wrap: false,
100            numbers: true,
101            help: false,
102            cache: Cache::default(),
103            geometry: crate::geometry::Cache::default(),
104            epoch: 0,
105            width: 160,
106            height: 50,
107            visible: Vec::new(),
108            cursor_screen_row: 0,
109            reading_anchor: None,
110        })
111    }
112
113    pub fn split(&self) -> bool {
114        self.layout == Layout::Split || (self.layout == Layout::Auto && self.width >= 120)
115    }
116    pub fn row(&self, index: usize) -> Option<Row<LineId>> {
117        self.view.row(&self.doc, index, self.split())
118    }
119    pub fn row_len(&self) -> usize {
120        self.view.len(&self.doc, self.split())
121    }
122
123    fn anchor(&self) -> Option<LineId> {
124        let row = self.row(self.cursor.row)?;
125        let kind = if matches!(row.kind, RowKind::NoNewline { .. }) {
126            self.row(self.cursor.row.checked_sub(1)?)?.kind
127        } else {
128            row.kind
129        };
130        let RowKind::Code { left, right } = kind else {
131            return None;
132        };
133        if self.old_side {
134            left.or(right)
135        } else {
136            right.or(left)
137        }
138    }
139
140    fn screen_row(&self) -> usize {
141        self.visible
142            .iter()
143            .find(|(p, _)| *p == self.cursor)
144            .map_or(self.cursor_screen_row + 2, |(_, y)| *y as usize)
145    }
146
147    fn remember_position(&mut self, clicked_column: Option<u16>) -> Result<()> {
148        if self.reading_anchor.is_none()
149            && let Some(source) = self.anchor()
150        {
151            let old_side = if self.split() {
152                self.old_side
153            } else {
154                match self.line_info(source)?.kind {
155                    Kind::Deletion => true,
156                    Kind::Addition => false,
157                    Kind::Context => self.old_side,
158                }
159            };
160            self.reading_anchor = Some(ReadingAnchor {
161                source,
162                display_byte: render::source_anchor_byte(
163                    self,
164                    source,
165                    self.cursor.part,
166                    old_side,
167                    clicked_column,
168                )?,
169                old_side,
170                screen_row: Some(self.screen_row()),
171            });
172        }
173        Ok(())
174    }
175
176    pub fn reflow(&mut self, layout: Layout, width: u16, height: u16) -> Result<()> {
177        self.remember_position(None)?;
178        let anchor = self.reading_anchor;
179        let screen_row = anchor
180            .and_then(|anchor| anchor.screen_row)
181            .unwrap_or_else(|| self.screen_row());
182        let file = self.row(self.cursor.row).map(|r| r.file);
183        let old_split = self.split();
184        if width < 40 || height < 10 {
185            return Ok(());
186        }
187        self.layout = layout;
188        self.width = width;
189        self.height = height;
190        if let Some(anchor) = anchor {
191            self.cursor.row = self
192                .view
193                .source_row(&self.doc, anchor.source, self.split())
194                .unwrap_or(self.cursor.row);
195            self.old_side = anchor.old_side;
196            (self.cursor.part, self.horizontal) = render::anchored_position(
197                self,
198                anchor.source,
199                anchor.display_byte,
200                anchor.old_side,
201            )?;
202        } else if old_split != self.split() {
203            self.cursor.row = file
204                .map(|f| self.view.file_row(&self.doc, f, self.split()))
205                .unwrap_or(0);
206        }
207        self.cursor.part = self
208            .cursor
209            .part
210            .min(self.row_height(self.cursor.row)?.saturating_sub(1));
211        self.top = self.cursor;
212        for _ in 0..screen_row
213            .min(height.saturating_sub(2) as usize)
214            .saturating_sub(1)
215        {
216            self.top = self.previous(self.top)?;
217        }
218        if self.top < self.cursor
219            && self
220                .row(self.top.row)
221                .is_some_and(|row| row.kind != RowKind::Header)
222        {
223            self.top = self.next(self.top)?;
224        }
225        if let Some(anchor) = &mut self.reading_anchor {
226            anchor.screen_row = Some(screen_row);
227        }
228        self.visible.clear();
229        Ok(())
230    }
231
232    pub fn toggle_wrap(&mut self) -> Result<()> {
233        self.remember_position(None)?;
234        self.wrap = !self.wrap;
235        self.reflow(self.layout, self.width, self.height)
236    }
237
238    pub fn toggle_numbers(&mut self) -> Result<()> {
239        self.remember_position(None)?;
240        self.numbers = !self.numbers;
241        self.reflow(self.layout, self.width, self.height)
242    }
243
244    pub fn move_horizontal(&mut self, forward: bool) -> Result<()> {
245        let horizontal = if forward {
246            self.horizontal
247                .saturating_add(4)
248                .min(render::horizontal_limit(self)?)
249        } else {
250            self.horizontal.saturating_sub(4)
251        };
252        if horizontal != self.horizontal {
253            self.horizontal = horizontal;
254            self.reading_anchor = None;
255        }
256        Ok(())
257    }
258
259    pub fn switch_side(&mut self) {
260        self.selection = None;
261        self.old_side = !self.old_side;
262        self.reading_anchor = None;
263    }
264
265    pub fn select(&mut self, position: Position, column: u16) -> Result<()> {
266        self.cursor = position;
267        if self.split() {
268            self.old_side = column < self.width / 2;
269        }
270        self.reading_anchor = None;
271        self.remember_position(Some(column))?;
272        let old = self
273            .source_at_cursor(None)?
274            .map_or(self.old_side, |(_, old)| old);
275        if self
276            .selection
277            .as_ref()
278            .is_some_and(|selection| (selection.range.side == crate::engine::Side::Old) != old)
279        {
280            self.selection = None;
281        }
282        Ok(())
283    }
284
285    pub(crate) fn reveal_source(
286        &mut self,
287        source: LineId,
288        display_byte: usize,
289        old_side: bool,
290    ) -> Result<()> {
291        let row = self
292            .view
293            .source_row(&self.doc, source, self.split())
294            .context("Search source is not displayed")?;
295        let (part, horizontal) = render::anchored_position(self, source, display_byte, old_side)?;
296        let cursor = Position { row, part };
297        let mut top = self.top;
298        if cursor < top {
299            top = cursor;
300        } else if !self.visible.iter().any(|(position, _)| *position == cursor) {
301            top = cursor;
302            for _ in 0..self.height.saturating_sub(4) {
303                top = self.previous(top)?;
304            }
305        }
306        if self
307            .selection
308            .as_ref()
309            .is_some_and(|selection| (selection.range.side == crate::engine::Side::Old) != old_side)
310        {
311            self.selection = None;
312        }
313        self.cursor = cursor;
314        self.top = top;
315        self.horizontal = horizontal;
316        self.old_side = old_side;
317        self.reading_anchor = Some(ReadingAnchor {
318            source,
319            display_byte,
320            old_side,
321            screen_row: None,
322        });
323        self.visible.clear();
324        Ok(())
325    }
326
327    pub fn row_height(&self, row: usize) -> Result<usize> {
328        render::row_height(self, row)
329    }
330
331    pub fn next(&self, p: Position) -> Result<Position> {
332        Ok(if p.part + 1 < self.row_height(p.row)? {
333            Position {
334                row: p.row,
335                part: p.part + 1,
336            }
337        } else if p.row + 1 < self.row_len() {
338            Position {
339                row: p.row + 1,
340                part: 0,
341            }
342        } else {
343            p
344        })
345    }
346
347    pub fn previous(&self, p: Position) -> Result<Position> {
348        Ok(if p.part > 0 {
349            Position {
350                row: p.row,
351                part: p.part - 1,
352            }
353        } else if p.row > 0 {
354            Position {
355                row: p.row - 1,
356                part: self.row_height(p.row - 1)? - 1,
357            }
358        } else {
359            p
360        })
361    }
362
363    pub fn move_cursor(&mut self, amount: isize) -> Result<()> {
364        let before = self.cursor;
365        for _ in 0..amount.unsigned_abs() {
366            self.cursor = if amount > 0 {
367                self.next(self.cursor)?
368            } else {
369                self.previous(self.cursor)?
370            };
371        }
372        if before != self.cursor {
373            self.reading_anchor = None;
374        }
375        self.reveal()
376    }
377
378    pub fn scroll(&mut self, amount: isize) -> Result<()> {
379        let before = self.cursor;
380        for _ in 0..amount.unsigned_abs() {
381            self.top = if amount > 0 {
382                self.next(self.top)?
383            } else {
384                self.previous(self.top)?
385            };
386        }
387        let sticky_header = self
388            .row(self.top.row)
389            .is_some_and(|row| row.kind != RowKind::Header);
390        let visible_rows = self.height.saturating_sub(2 + u16::from(sticky_header));
391        let mut bottom = self.top;
392        for _ in 1..visible_rows {
393            bottom = self.next(bottom)?;
394        }
395        self.cursor = self.cursor.clamp(self.top, bottom);
396        if self.cursor != before {
397            self.reading_anchor = None;
398        } else if let Some(anchor) = &mut self.reading_anchor {
399            anchor.screen_row = None;
400        }
401        Ok(())
402    }
403
404    pub fn reveal(&mut self) -> Result<()> {
405        if self.cursor < self.top {
406            self.top = self.cursor;
407        } else if !self.visible.iter().any(|(p, _)| *p == self.cursor) {
408            self.top = self.cursor;
409            for _ in 0..self.height.saturating_sub(4) {
410                self.top = self.previous(self.top)?;
411            }
412        }
413        Ok(())
414    }
415
416    pub fn jump(&mut self, end: bool) -> Result<()> {
417        let Some(last) = self.doc.source_len().checked_sub(1) else {
418            return Ok(());
419        };
420        let patch = if end { last } else { 0 };
421        let file = self.doc.file_for_source(patch).id;
422        let mut id = LineId::Patch(patch);
423        let mut row = self
424            .view
425            .source_row(&self.doc, id, self.split())
426            .context("Patch endpoint is missing")?;
427        let mut index = if end {
428            if file + 1 < self.doc.files.len() {
429                self.view.file_row(&self.doc, file + 1, self.split()) - 1
430            } else {
431                self.row_len() - 1
432            }
433        } else {
434            self.view.file_row(&self.doc, file, self.split())
435        };
436        while if end { index > row } else { index < row } {
437            if let Some(Row {
438                kind: RowKind::Code { left, right },
439                ..
440            }) = self.row(index)
441                && let Some(context @ LineId::Context { .. }) =
442                    if end { right.or(left) } else { left.or(right) }
443            {
444                id = context;
445                row = index;
446                break;
447            }
448            if end {
449                index -= 1;
450            } else {
451                index += 1;
452            }
453        }
454        self.reading_anchor = None;
455        self.cursor = Position { row, part: 0 };
456        self.old_side = self.line_info(id)?.new.is_none();
457        self.reveal()
458    }
459
460    pub(crate) fn expand_context(&mut self) -> Result<bool> {
461        let Some((file, gap, _)) = self.view.gap(&self.doc, self.cursor.row, self.split()) else {
462            return Ok(false);
463        };
464        let screen_row = self.screen_row();
465        let source = self.view.expand(&self.doc, file, gap)?;
466        self.reading_anchor = Some(ReadingAnchor {
467            source,
468            display_byte: 0,
469            old_side: self.old_side,
470            screen_row: Some(screen_row),
471        });
472        self.reflow(self.layout, self.width, self.height)?;
473        Ok(true)
474    }
475
476    pub fn jump_section(&mut self, forward: bool, file: bool) {
477        let target = if file {
478            self.row(self.cursor.row)
479                .and_then(|row| {
480                    if forward {
481                        row.file.checked_add(1)
482                    } else {
483                        row.file.checked_sub(1)
484                    }
485                })
486                .and_then(|index| self.doc.files.get(index))
487                .map(|file| self.view.file_row(&self.doc, file.id, self.split()))
488        } else {
489            self.view
490                .boundary(&self.doc, self.cursor.row, self.split(), forward)
491        };
492        if let Some(row) = target {
493            self.reading_anchor = None;
494            self.cursor = Position { row, part: 0 };
495            self.top = self.cursor;
496        }
497    }
498
499    pub fn toggle_theme(&mut self) -> anyhow::Result<()> {
500        self.appearance = self.appearance.toggle();
501        self.colors = theme::load(self.appearance)?.0;
502        self.epoch += 1;
503        self.cache = Cache::default();
504        Ok(())
505    }
506
507    pub fn request_highlights(&self) {
508        let mut ids = Vec::new();
509        let mut files = Vec::new();
510        for (p, _) in &self.visible {
511            if let Some(Row {
512                kind: RowKind::Code { left, right },
513                ..
514            }) = self.row(p.row)
515            {
516                for id in left.into_iter().chain(right) {
517                    if self.cache.get(id).is_none() {
518                        ids.push(id);
519                        let file = self.line_file(id);
520                        if !files
521                            .iter()
522                            .any(|f: &crate::highlight::FileSource| f.file().id == file.id)
523                        {
524                            files.push(self.coloring_source(file));
525                        }
526                    }
527                }
528            }
529        }
530        #[cfg(feature = "delivery-trace")]
531        {
532            let cached = self
533                .visible
534                .iter()
535                .filter_map(|(p, _)| self.row(p.row))
536                .filter_map(|row| match row.kind {
537                    RowKind::Code { left, right } => Some(left.into_iter().chain(right)),
538                    _ => None,
539                })
540                .flatten()
541                .filter(|id| self.cache.get(*id).is_some())
542                .count();
543            crate::delivery_event!("viewport_cache_occurrences", cached, ids.len());
544        }
545        if ids.is_empty() {
546            self.worker.cancel();
547        } else {
548            self.worker.request(Request {
549                epoch: self.epoch,
550                appearance: self.appearance,
551                ids,
552                files,
553            });
554        }
555    }
556
557    pub(crate) fn protect_visible_styles(&mut self) {
558        let ids = self
559            .visible
560            .iter()
561            .filter_map(|(position, _)| self.row(position.row))
562            .flat_map(|row| match row.kind {
563                RowKind::Code { left, right } => [left, right],
564                _ => [None, None],
565            })
566            .flatten()
567            .collect();
568        self.cache.protect_visible(ids);
569    }
570}