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}