Files
galaxy/app/src/editor/view/model/display_map/mod.rs
T

517 lines
17 KiB
Rust

mod fold_map;
use super::buffer::{self, Anchor, Buffer, Edit, StylizedChar, ToCharOffset, ToPoint};
use crate::editor::soft_wrap::{self, DisplayPointAndClampDirection, SoftWrapPoint, SoftWrapState};
use anyhow::{Context, Result};
pub use fold_map::BufferRows;
use fold_map::FoldMap;
use galaxyui::text::point::Point;
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle};
use std::cmp;
use std::ops::Range;
use string_offset::CharOffset;
#[derive(Copy, Clone)]
pub enum Bias {
Left,
Right,
}
pub struct DisplayMap {
buffer: ModelHandle<Buffer>,
fold_map: FoldMap,
soft_wrap_state: SoftWrapState,
tab_size: usize,
}
pub struct MovementResult {
pub point_and_clamp_direction: DisplayPointAndClampDirection,
/// True if the resulting point is the same row as the original row.
pub is_same_row: bool,
/// The desired column based on the original point. This could be higher than
/// the returned column in point because the current row may not have had as
/// many columns as the previous row.
pub goal_column: u32,
}
pub enum Event {
Folded,
Unfolded,
}
impl Entity for DisplayMap {
type Event = Event;
}
impl DisplayMap {
fn new_internal(
buffer: ModelHandle<Buffer>,
subscribe_to_buffer: bool,
tab_size: usize,
ctx: &mut ModelContext<Self>,
) -> Self {
if subscribe_to_buffer {
ctx.subscribe_to_model(&buffer, Self::handle_buffer_event);
}
DisplayMap {
buffer: buffer.clone(),
fold_map: FoldMap::new(buffer, ctx),
soft_wrap_state: Default::default(),
tab_size,
}
}
pub fn new(buffer: ModelHandle<Buffer>, tab_size: usize, ctx: &mut ModelContext<Self>) -> Self {
Self::new_internal(buffer, true, tab_size, ctx)
}
pub fn recreate(&mut self, tab_size: usize, ctx: &mut ModelContext<Self>) {
*self = Self::new_internal(self.buffer.clone(), false, tab_size, ctx);
}
pub fn buffer<'a>(&self, app: &'a AppContext) -> &'a Buffer {
self.buffer.as_ref(app)
}
pub fn soft_wrap_state(&self) -> SoftWrapState {
self.soft_wrap_state.clone()
}
pub fn fold<T: ToCharOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
ctx: &mut ModelContext<Self>,
) -> Result<()> {
self.fold_map.fold(ranges, ctx)?;
ctx.emit(Event::Folded);
Ok(())
}
pub fn unfold<T: ToCharOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
ctx: &mut ModelContext<Self>,
) -> Result<()> {
self.fold_map.unfold(ranges, ctx)?;
ctx.emit(Event::Unfolded);
Ok(())
}
/// TODO(zheng) Consolidate logic with `down`.
pub fn up(
&self,
point: DisplayPoint,
goal_column: Option<u32>,
clamp_direction: soft_wrap::ClampDirection,
) -> Result<MovementResult> {
self.soft_wrap_state.read(|frame_layouts| {
let frame_layouts = frame_layouts.map_err(|err| anyhow::anyhow!("Could not read Frame Layouts {:?}", err))?;
let point = frame_layouts.to_soft_wrap_point(point, clamp_direction).context("Point is out of bounds of laid out text")?;
let line = frame_layouts
.get_line(point.row() as usize)
.context("Should have current line as it should not be possible for it to be beyond bounds, but was not able to retrieve it")?;
// Note that the index here will correspond to that of the original text so if
// this line is soft wrapped, it will be higher than 0.
let line_first_index = line.first_glyph().map_or(0, |glyph| glyph.index);
let relative_index = point.column() - line_first_index as u32;
// Note that, as also mentioned in the rustdoc of this function,
// the goal column is a relative index to the first point of the row
// so it starts at 0 and is at most the length of a row (but it could
// be higher than the current row as it is preserved across rows).
let goal_column = match goal_column {
Some(goal_column) => cmp::max(goal_column, relative_index),
None => relative_index,
};
let (point, is_same_row) = if point.row() > 0 {
let prev_line = frame_layouts
.get_line(point.row() as usize - 1)
.context("Should have next line based on max point bounds check, but was not able to retrieve it")?;
let prev_line_first_column = prev_line.first_glyph().map_or(0, |glyph| glyph.index as u32);
// Note that goal column is a relative value, rather than based
// on the original string.
let new_column = prev_line_first_column + goal_column;
let last_index_in_line = prev_line.last_glyph().map(|glyph| glyph.index as u32 + 1)
.unwrap_or(0);
(SoftWrapPoint::new(
point.row() - 1,
cmp::min(new_column, last_index_in_line),
), false)
} else {
(SoftWrapPoint::new(0, 0), true)
};
Ok(MovementResult {
point_and_clamp_direction: frame_layouts.to_display_point(point),
goal_column,
is_same_row,
})
})
}
/// Given a `DisplayPoint` and the goal column (i.e. the rightmost column
/// that we _want_ to be at which could be higher than the current point's
/// column) returns the point and new goal column we would be at if we were
/// to navigate one row down from the point with soft wrap in consideration.
///
/// Note that while `goal_column` is the column number of the row, the column
/// numbers in `SoftWrapPoint`, are relative to the original string.
/// TODO(zheng) Consolidate logic with `up`.
pub fn down(
&self,
point: DisplayPoint,
goal_column: Option<u32>,
clamp_direction: soft_wrap::ClampDirection,
) -> Result<MovementResult> {
self.soft_wrap_state.read(|frame_layouts| {
let frame_layouts = frame_layouts.map_err(|err| anyhow::anyhow!("Could not read Frame Layouts {:?}", err))?;
let point = frame_layouts.to_soft_wrap_point(point, clamp_direction).ok_or_else(|| anyhow::anyhow!("Point is out of bounds of laid out text"))?;
let line = frame_layouts
.get_line(point.row() as usize)
.context("Should have current line as it should not be possible for it to be beyond bounds, but was not able to retrieve it")?;
// Note that the index here will correspond to that of the original text so if
// this line is soft wrapped, it will be higher than 0.
let line_first_index = line.first_glyph().map_or(0, |glyph| glyph.index);
let relative_index = point.column() - line_first_index as u32;
// Note that, as also mentioned in the rustdoc of this function,
// the goal column is a relative index to the first point of the row
// so it starts at 0 and is at most the length of a row (but it could
// be higher than the current row as it is preserved across rows).
let goal_column = match goal_column {
Some(goal_column) => cmp::max(goal_column, relative_index),
None => relative_index,
};
let max_row = frame_layouts.num_lines() - 1;
let max_col = frame_layouts
.get_line(max_row)
.and_then(|line| line.last_glyph().map(|glyph| glyph.index + 1))
.unwrap_or(0);
let max_point = SoftWrapPoint::new(max_row as u32, max_col as u32);
let (point, is_same_row) = if point.row() < max_point.row() {
let next_line = frame_layouts
.get_line(point.row() as usize + 1)
.context("Should have next line based on max point bounds check, but was not able to retrieve it")?;
let next_line_first_column = next_line.first_glyph().map_or(0, |glyph| glyph.index as u32);
// Note that goal column is a relative value, rather than based
// on the original string.
let new_column = next_line_first_column + goal_column;
let last_index_in_line = next_line.last_glyph().map(|glyph| glyph.index as u32 + 1)
.unwrap_or(0);
(SoftWrapPoint::new(
point.row() + 1,
cmp::min(new_column, last_index_in_line),
), false)
} else {
(max_point, true)
};
Ok(MovementResult {
point_and_clamp_direction: frame_layouts.to_display_point(point),
goal_column,
is_same_row,
})
})
}
pub fn to_soft_wrap_point(
&self,
point: DisplayPoint,
clamp_direction: soft_wrap::ClampDirection,
) -> Option<SoftWrapPoint> {
self.soft_wrap_state
.read(|frame_layouts| match frame_layouts {
Ok(frame_layouts) => frame_layouts.to_soft_wrap_point(point, clamp_direction),
Err(err) => {
log::warn!("Error attempting to get soft wrap point {err:?}");
None
}
})
}
pub fn is_line_folded(&self, display_row: u32) -> bool {
self.fold_map.is_line_folded(display_row)
}
pub fn tab_size(&self) -> usize {
self.tab_size
}
#[cfg(test)]
pub fn text(&self, app: &AppContext) -> String {
self.chars_at(DisplayPoint::zero(), app).unwrap().collect()
}
pub fn line(&self, display_row: u32, app: &AppContext) -> Result<String> {
let chars = self.chars_at(DisplayPoint::new(display_row, 0), app)?;
Ok(chars.take_while(|c| *c != '\n').collect())
}
pub fn chars_with_styles_at<'a>(
&'a self,
point: DisplayPoint,
app: &'a AppContext,
) -> Result<impl 'a + Iterator<Item = StylizedChar>> {
let column = point.column() as usize;
let (point, to_next_stop) = point.collapse_tabs(self, Bias::Left, app)?;
let mut fold_chars = self.fold_map.chars_with_style_at(point, app)?;
if to_next_stop > 0 {
fold_chars.next();
}
Ok(CharsWithStyles {
fold_chars,
column,
to_next_stop,
tab_size: self.tab_size,
})
}
pub fn chars_at<'a>(
&'a self,
point: DisplayPoint,
app: &'a AppContext,
) -> Result<impl 'a + Iterator<Item = char>> {
Ok(self.chars_with_styles_at(point, app)?.map(Into::into))
}
pub fn buffer_rows(&self, start_row: u32) -> Result<BufferRows<'_>> {
self.fold_map.buffer_rows(start_row)
}
pub fn line_len(&self, row: u32, ctx: &AppContext) -> Result<u32> {
DisplayPoint::new(row, self.fold_map.line_len(row, ctx)?)
.expand_tabs(self, ctx)
.map(|point| point.column())
}
pub fn max_point(&self, app: &AppContext) -> DisplayPoint {
self.fold_map.max_point().expand_tabs(self, app).unwrap()
}
pub fn rightmost_point(&self) -> DisplayPoint {
self.fold_map.rightmost_point()
}
pub fn anchor_before(
&self,
point: DisplayPoint,
bias: Bias,
app: &AppContext,
) -> Result<Anchor> {
self.buffer
.as_ref(app)
.anchor_before(point.to_buffer_point(self, bias, app)?)
}
#[allow(dead_code)]
pub fn anchor_after(
&self,
point: DisplayPoint,
bias: Bias,
app: &AppContext,
) -> Result<Anchor> {
self.buffer
.as_ref(app)
.anchor_after(point.to_buffer_point(self, bias, app)?)
}
pub fn apply_edits(&mut self, edits: &[Edit], ctx: &AppContext) -> Result<()> {
self.fold_map.apply_edits(edits, ctx)
}
fn handle_buffer_event(&mut self, event: &buffer::Event, ctx: &mut ModelContext<Self>) {
match event {
buffer::Event::Edited { edits, .. } => self.apply_edits(edits, ctx).unwrap(),
buffer::Event::StylesUpdated
| buffer::Event::UpdatePeers { .. }
| buffer::Event::SelectionsChanged => {}
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct DisplayPoint(Point);
impl DisplayPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(Point::new(row, column))
}
#[allow(dead_code)]
pub fn zero() -> Self {
Self::new(0, 0)
}
pub fn row(self) -> u32 {
self.0.row
}
pub fn column(self) -> u32 {
self.0.column
}
pub fn row_mut(&mut self) -> &mut u32 {
&mut self.0.row
}
pub fn column_mut(&mut self) -> &mut u32 {
&mut self.0.column
}
pub fn to_buffer_point(self, map: &DisplayMap, bias: Bias, app: &AppContext) -> Result<Point> {
Ok(map
.fold_map
.to_buffer_point(self.collapse_tabs(map, bias, app)?.0))
}
pub fn to_char_offset(
self,
map: &DisplayMap,
bias: Bias,
buffer: &Buffer,
app: &AppContext,
) -> Result<CharOffset> {
let point = self.to_buffer_point(map, bias, app)?;
point.to_char_offset(buffer)
}
fn expand_tabs(mut self, map: &DisplayMap, app: &AppContext) -> Result<Self> {
let chars = map
.fold_map
.chars_at(DisplayPoint(Point::new(self.row(), 0)), app)?;
let expanded = expand_tabs(chars, self.column() as usize, map.tab_size);
*self.column_mut() = expanded as u32;
Ok(self)
}
fn collapse_tabs(
mut self,
map: &DisplayMap,
bias: Bias,
app: &AppContext,
) -> Result<(Self, usize)> {
let chars = map
.fold_map
.chars_at(DisplayPoint(Point::new(self.0.row, 0)), app)?;
let expanded = self.column() as usize;
let (collapsed, to_next_stop) = collapse_tabs(chars, expanded, bias, map.tab_size);
*self.column_mut() = collapsed as u32;
Ok((self, to_next_stop))
}
}
/// Trait for types that can map onto a display point, accounting for soft-wrapping
/// and text folding.
pub trait ToDisplayPoint {
fn to_display_point(self, map: &DisplayMap, app: &AppContext) -> Result<DisplayPoint>;
}
impl ToDisplayPoint for Point {
fn to_display_point(self, map: &DisplayMap, app: &AppContext) -> Result<DisplayPoint> {
let mut display_point = map.fold_map.to_display_point(self);
let chars = map
.fold_map
.chars_at(DisplayPoint::new(display_point.row(), 0), app)?;
*display_point.column_mut() =
expand_tabs(chars, display_point.column() as usize, map.tab_size) as u32;
Ok(display_point)
}
}
impl ToDisplayPoint for &Anchor {
fn to_display_point(self, map: &DisplayMap, app: &AppContext) -> Result<DisplayPoint> {
self.to_point(map.buffer.as_ref(app))?
.to_display_point(map, app)
}
}
pub struct CharsWithStyles<'a> {
fold_chars: fold_map::CharsWithStyle<'a>,
column: usize,
to_next_stop: usize,
tab_size: usize,
}
impl Iterator for CharsWithStyles<'_> {
type Item = StylizedChar;
fn next(&mut self) -> Option<Self::Item> {
if self.to_next_stop > 0 {
self.to_next_stop -= 1;
self.column += 1;
Some(StylizedChar::new(' ', Default::default()))
} else {
self.fold_chars.next().map(|c| match c.char() {
'\t' => {
self.to_next_stop = self.tab_size - self.column % self.tab_size - 1;
self.column += 1;
StylizedChar::new(' ', c.style())
}
'\n' => {
self.column = 0;
c
}
_ => {
self.column += 1;
c
}
})
}
}
}
pub fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
let mut expanded = 0;
for c in chars.take(column) {
if c == '\t' {
expanded += tab_size - expanded % tab_size;
} else {
expanded += 1;
}
}
expanded
}
pub fn collapse_tabs(
chars: impl Iterator<Item = char>,
column: usize,
bias: Bias,
tab_size: usize,
) -> (usize, usize) {
let mut expanded = 0;
let mut collapsed = 0;
for c in chars {
if expanded == column {
break;
}
if c == '\t' {
expanded += tab_size - (expanded % tab_size);
if expanded > column {
return match bias {
Bias::Left => (collapsed, expanded - column),
Bias::Right => (collapsed + 1, 0),
};
}
} else {
expanded += 1;
}
collapsed += 1;
}
(collapsed, 0)
}
#[cfg(test)]
#[path = "mod_test.rs"]
mod tests;