use anyhow::anyhow; use parking_lot::Mutex; use std::sync::Arc; use warpui::text_layout; use crate::editor::{view::DisplayPoint, Point}; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)] pub struct SoftWrapPoint(Point); impl SoftWrapPoint { pub fn new(row: u32, col: u32) -> Self { Self(Point::new(row, col)) } pub fn row(self) -> u32 { self.0.row } pub fn column(self) -> u32 { self.0.column } pub fn column_mut(&mut self) -> &mut u32 { &mut self.0.column } } /// When a line is soft-wrapped, there can be ambuigity about where the cursor /// should be drawn. For example, if I had the text "hello world" which became /// soft wrapped to "hello \nworld", then if the cursor is just before "w", /// then the cursor could either be at the end of the first line or the very /// beginning of the next line. This field keeps track of whether the cursor /// should be clamped above or below. #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] pub enum ClampDirection { Up, #[default] Down, } #[derive(Default, Clone)] /// Wrapper around `FrameLayouts` to help manage the levels of indirection around /// the Arc, Mutex and Option. /// /// When this is None, we haven't yet laid out the text. pub struct SoftWrapState(Arc>>); impl SoftWrapState { pub fn update(&self, new_value: FrameLayouts) { *self.0.lock() = Some(new_value); } pub fn read) -> T>(&self, callback: F) -> T { let frame_layouts = self.0.lock(); match &*frame_layouts { Some(frame_layouts) => callback(Ok(frame_layouts)), None => callback(Err(anyhow!("No frame layout. This should only happen if we attempt to read before a frame of the editor has been laid out"))), } } } /// We layout more frames than we need to display on the screen because /// we need to know the number of lines in previous frames to calculate /// any row position for any given character. /// /// In particular, we always layout from zero up to the number of lines in /// viewport + number of lines in scrollback because this is the worst case /// and we don't know how many rows we'll need to be laid out until we've laid /// them out. #[derive(Clone)] pub struct FrameLayouts { frames: Vec>, /// The first line that is displayed. The very first line in the editor /// is 0, and each displayed line is incremented by 1. So if soft wrapping /// is off, this is one-to-one with the rows of the buffer, but if soft /// wrapping is on, the line number is the number of lines from the top /// given soft wrapping. start_line: u32, end_line: u32, } impl FrameLayouts { pub fn new(frames: Vec>, start_line: u32, end_line: u32) -> Self { Self { frames, start_line, end_line, } } pub fn frames(&self) -> impl Iterator> { self.frames.iter() } pub fn last_frame(&self) -> Option<&Arc> { self.frames.last() } pub fn displayed_lines(&self) -> impl Iterator { FrameLayoutDisplayedLines::zero(self).skip(self.start_line as usize) } /// Number of lines displayed in the editor. pub fn num_displayed_lines(&self) -> u32 { self.displayed_lines().count() as u32 } pub fn num_lines(&self) -> usize { self.frames().map(|frame| frame.lines().len()).sum() } /// Returns the index of the row that represents the end of some logical row pub fn end_of_logical_row(&self, logical_row: usize) -> usize { self.frames .iter() // adding 1 to include the TextFrame that represents this row (the queried one) .take(logical_row + 1) .fold(0, |acc, text_frame| acc + text_frame.lines().len()) // subtracting one to convert a count of rows into an index - 1 } /// Returns the line at the given index, where the first line is 0 if it exists. pub fn get_line(&self, desired_index: usize) -> Option<&text_layout::Line> { self.frames .iter() .flat_map(|frame| frame.lines()) .nth(desired_index) } /// Given a `DisplayPoint`, converts the row number such that soft wrapping /// is considered. This function will yield a result in the case where the /// provided point is within the bounds of the last laid out text. Otherwise, /// it will return `None`. pub fn to_soft_wrap_point( &self, point: DisplayPoint, clamp_direction: ClampDirection, ) -> Option { let rows_before_frame: u32 = self .frames .iter() .take(point.row() as usize) .fold(0, |acc, text_frame| acc + text_frame.lines().len() as u32); let frame = self.frames.get(point.row() as usize)?; let rows_within_frame = frame.row_within_frame( point.column() as usize, matches!(clamp_direction, ClampDirection::Up), ) as u32; Some(SoftWrapPoint::new( rows_before_frame + rows_within_frame, point.column(), )) } /// Given a `SoftWrapPoint`, converts the row number to a value irrespective /// of soft-wrapping. pub fn to_display_point(&self, point: SoftWrapPoint) -> DisplayPointAndClampDirection { let mut acc_lines = 0; let mut row = 0; let mut clamp_direction = Default::default(); for (i, frame) in self.frames.iter().enumerate() { if acc_lines + frame.lines().len() > point.row() as usize { row = i; break; } acc_lines += frame.lines().len() } if let Some(line) = self.get_line(point.row() as usize) { if let Some(glyph) = line.last_glyph() { if point.column() as usize == glyph.index + 1 { clamp_direction = ClampDirection::Up; } } else if let Some(glyph) = line.first_glyph() { if point.column() as usize == glyph.index { clamp_direction = ClampDirection::Down; } } } DisplayPointAndClampDirection { point: DisplayPoint::new(row as u32, point.column()), clamp_direction, } } } #[derive(Debug)] pub struct DisplayPointAndClampDirection { pub point: DisplayPoint, pub clamp_direction: ClampDirection, } pub struct FrameLayoutDisplayedLines<'a> { frames: &'a FrameLayouts, current_row: usize, current_line_of_row: usize, lines_yielded: u32, } impl<'a> FrameLayoutDisplayedLines<'a> { pub fn zero(frames: &'a FrameLayouts) -> Self { Self { frames, current_row: 0, current_line_of_row: 0, lines_yielded: 0, } } } impl<'a> Iterator for FrameLayoutDisplayedLines<'a> { type Item = &'a text_layout::Line; fn next(&mut self) -> Option { if self.lines_yielded >= self.frames.end_line { return None; } if let Some(frame) = self.frames.frames.get(self.current_row) { if frame.lines().len() > self.current_line_of_row { let line = &frame.lines()[self.current_line_of_row]; self.current_line_of_row += 1; self.lines_yielded += 1; Some(line) } else { self.current_row += 1; self.current_line_of_row = 0; self.next() } } else { None } } } #[cfg(test)] #[path = "soft_wrap_test.rs"] mod tests;