583 lines
20 KiB
Rust
583 lines
20 KiB
Rust
use std::ops::Range;
|
|
|
|
use gpui::{
|
|
App, Font, Half, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px,
|
|
size,
|
|
};
|
|
use ropey::Rope;
|
|
use smallvec::SmallVec;
|
|
|
|
use crate::input::{LastLayout, Point as TreeSitterPoint, RopeExt, WhitespaceIndicators};
|
|
|
|
/// A line with soft wrapped lines info.
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct LineItem {
|
|
/// The original line text, without end `\n`.
|
|
line: Rope,
|
|
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
|
///
|
|
/// Not contains the line end `\n`.
|
|
pub(crate) wrapped_lines: Vec<Range<usize>>,
|
|
}
|
|
|
|
impl LineItem {
|
|
/// Get the bytes length of this line.
|
|
#[inline]
|
|
pub(crate) fn len(&self) -> usize {
|
|
self.line.len()
|
|
}
|
|
|
|
/// Get number of soft wrapped lines of this line (include the first line).
|
|
#[inline]
|
|
pub(crate) fn lines_len(&self) -> usize {
|
|
self.wrapped_lines.len()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct LongestRow {
|
|
/// The 0-based row index.
|
|
pub row: usize,
|
|
/// The bytes length of the longest line.
|
|
pub len: usize,
|
|
}
|
|
|
|
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
|
|
///
|
|
/// After use lines to calculate the scroll size of the Editor.
|
|
pub(crate) struct TextWrapper {
|
|
text: Rope,
|
|
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
|
soft_lines: usize,
|
|
font: Font,
|
|
font_size: Pixels,
|
|
/// If is none, it means the text is not wrapped
|
|
wrap_width: Option<Pixels>,
|
|
/// The longest (row, bytes len) in characters, used to calculate the horizontal scroll width.
|
|
pub(crate) longest_row: LongestRow,
|
|
/// The lines by split \n
|
|
pub(crate) lines: Vec<LineItem>,
|
|
|
|
_initialized: bool,
|
|
}
|
|
|
|
#[allow(unused)]
|
|
impl TextWrapper {
|
|
pub(crate) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
|
Self {
|
|
text: Rope::new(),
|
|
font,
|
|
font_size,
|
|
wrap_width,
|
|
soft_lines: 0,
|
|
longest_row: LongestRow::default(),
|
|
lines: Vec::new(),
|
|
_initialized: false,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn set_default_text(&mut self, text: &Rope) {
|
|
self.text = text.clone();
|
|
}
|
|
|
|
/// Get reference to the rope text.
|
|
#[inline]
|
|
pub(crate) fn text(&self) -> &Rope {
|
|
&self.text
|
|
}
|
|
|
|
/// Get the total number of lines including wrapped lines.
|
|
#[inline]
|
|
pub(crate) fn len(&self) -> usize {
|
|
self.soft_lines
|
|
}
|
|
|
|
/// Get the line item by row index.
|
|
#[inline]
|
|
pub(crate) fn line(&self, row: usize) -> Option<&LineItem> {
|
|
self.lines.get(row)
|
|
}
|
|
|
|
pub(crate) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
|
if wrap_width == self.wrap_width {
|
|
return;
|
|
}
|
|
|
|
self.wrap_width = wrap_width;
|
|
self.update_all(&self.text.clone(), cx);
|
|
}
|
|
|
|
pub(crate) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
|
if self.font.eq(&font) && self.font_size == font_size {
|
|
return;
|
|
}
|
|
|
|
self.font = font;
|
|
self.font_size = font_size;
|
|
self.update_all(&self.text.clone(), cx);
|
|
}
|
|
|
|
pub(crate) fn prepare_if_need(&mut self, text: &Rope, cx: &mut App) -> bool {
|
|
if self._initialized {
|
|
return false;
|
|
}
|
|
self._initialized = true;
|
|
self.update_all(text, cx);
|
|
true
|
|
}
|
|
|
|
/// Update the text wrapper and recalculate the wrapped lines.
|
|
///
|
|
/// If the `text` is the same as the current text, do nothing.
|
|
///
|
|
/// - `changed_text`: The text [`Rope`] that has changed.
|
|
/// - `range`: The `selected_range` before change.
|
|
/// - `new_text`: The inserted text.
|
|
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
|
|
/// - `cx`: The application context.
|
|
pub(crate) fn update(
|
|
&mut self,
|
|
changed_text: &Rope,
|
|
range: &Range<usize>,
|
|
new_text: &Rope,
|
|
cx: &mut App,
|
|
) {
|
|
let mut line_wrapper = cx
|
|
.text_system()
|
|
.line_wrapper(self.font.clone(), self.font_size);
|
|
self._update(
|
|
changed_text,
|
|
range,
|
|
new_text,
|
|
&mut |line_str, wrap_width| {
|
|
line_wrapper
|
|
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
|
|
.collect()
|
|
},
|
|
);
|
|
}
|
|
|
|
fn _update<F>(
|
|
&mut self,
|
|
changed_text: &Rope,
|
|
range: &Range<usize>,
|
|
new_text: &Rope,
|
|
wrap_line: &mut F,
|
|
) where
|
|
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
|
|
{
|
|
// Remove the old changed lines.
|
|
let start_row = self.text.offset_to_point(range.start).row;
|
|
let start_row = start_row.min(self.lines.len().saturating_sub(1));
|
|
let end_row = self.text.offset_to_point(range.end).row;
|
|
let end_row = end_row.min(self.lines.len().saturating_sub(1));
|
|
let rows_range = start_row..=end_row;
|
|
|
|
if rows_range.contains(&self.longest_row.row) {
|
|
self.longest_row = LongestRow::default();
|
|
}
|
|
|
|
let mut longest_row_ix = self.longest_row.row;
|
|
let mut longest_row_len = self.longest_row.len;
|
|
|
|
// To add the new lines.
|
|
let new_start_row = changed_text.offset_to_point(range.start).row;
|
|
let new_start_offset = changed_text.line_start_offset(new_start_row);
|
|
let new_end_row = changed_text
|
|
.offset_to_point(range.start + new_text.len())
|
|
.row;
|
|
let new_end_offset = changed_text.line_end_offset(new_end_row);
|
|
let new_range = new_start_offset..new_end_offset;
|
|
|
|
let mut new_lines = vec![];
|
|
let wrap_width = self.wrap_width;
|
|
|
|
// line not contains `\n`.
|
|
for (ix, line) in Rope::from(changed_text.slice(new_range))
|
|
.iter_lines()
|
|
.enumerate()
|
|
{
|
|
let line_str = line.to_string();
|
|
let mut wrapped_lines = vec![];
|
|
let mut prev_boundary_ix = 0;
|
|
|
|
if line_str.len() > longest_row_len {
|
|
longest_row_ix = new_start_row + ix;
|
|
longest_row_len = line_str.len();
|
|
}
|
|
|
|
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
|
if let Some(wrap_width) = wrap_width {
|
|
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
|
for boundary in wrap_line(&line_str, wrap_width) {
|
|
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
|
prev_boundary_ix = boundary.ix;
|
|
}
|
|
}
|
|
|
|
// Reset of the line
|
|
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
|
wrapped_lines.push(prev_boundary_ix..line.len());
|
|
}
|
|
|
|
new_lines.push(LineItem {
|
|
line: Rope::from(line),
|
|
wrapped_lines,
|
|
});
|
|
}
|
|
|
|
if self.lines.is_empty() {
|
|
self.lines = new_lines;
|
|
} else {
|
|
self.lines.splice(rows_range, new_lines);
|
|
}
|
|
|
|
self.text = changed_text.clone();
|
|
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
|
self.longest_row = LongestRow {
|
|
row: longest_row_ix,
|
|
len: longest_row_len,
|
|
}
|
|
}
|
|
|
|
/// Update the text wrapper and recalculate the wrapped lines.
|
|
///
|
|
/// If the `text` is the same as the current text, do nothing.
|
|
fn update_all(&mut self, text: &Rope, cx: &mut App) {
|
|
self.update(text, &(0..text.len()), text, cx);
|
|
}
|
|
|
|
/// Return display point (with soft wrap) from the given byte offset in the text.
|
|
///
|
|
/// Panics if the `offset` is out of bounds.
|
|
pub(crate) fn offset_to_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
|
let row = self.text.offset_to_point(offset).row;
|
|
let start = self.text.line_start_offset(row);
|
|
let line = &self.lines[row];
|
|
|
|
let mut wrapped_row = self
|
|
.lines
|
|
.iter()
|
|
.take(row)
|
|
.map(|l| l.lines_len())
|
|
.sum::<usize>();
|
|
|
|
let local_offset = offset.saturating_sub(start);
|
|
for (ix, range) in line.wrapped_lines.iter().enumerate() {
|
|
if range.contains(&local_offset) {
|
|
return WrapDisplayPoint::new(
|
|
wrapped_row + ix,
|
|
ix,
|
|
local_offset.saturating_sub(range.start),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Otherwise return the eof of the line.
|
|
let last_range = line.wrapped_lines.last().unwrap_or(&(0..0));
|
|
let ix = line.lines_len().saturating_sub(1);
|
|
|
|
WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len())
|
|
}
|
|
|
|
/// Return byte offset in the text from the given display point (with soft wrap).
|
|
///
|
|
/// Panics if the `point.row` is out of bounds.
|
|
pub(crate) fn display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
|
let mut wrapped_row = 0;
|
|
for (row, line) in self.lines.iter().enumerate() {
|
|
if wrapped_row + line.lines_len() > point.row {
|
|
let line_start = self.text.line_start_offset(row);
|
|
let local_row = point.row.saturating_sub(wrapped_row);
|
|
if let Some(range) = line.wrapped_lines.get(local_row) {
|
|
return line_start + (range.start + point.column).min(range.end);
|
|
} else {
|
|
// If not found, return the end of the line.
|
|
return line_start + line.len();
|
|
}
|
|
}
|
|
|
|
wrapped_row += line.lines_len();
|
|
}
|
|
|
|
self.text.len()
|
|
}
|
|
|
|
pub(crate) fn display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint {
|
|
let offset = self.display_point_to_offset(point);
|
|
self.text.offset_to_point(offset)
|
|
}
|
|
|
|
pub(crate) fn point_to_display_point(&self, point: TreeSitterPoint) -> WrapDisplayPoint {
|
|
let offset = self.text.point_to_offset(point);
|
|
self.offset_to_display_point(offset)
|
|
}
|
|
}
|
|
|
|
/// A display point within the soft-wrapped text.
|
|
///
|
|
/// This represents a position in the text after soft-wrapping,
|
|
/// with an additional `local_row` field tracking the wrap line
|
|
/// within the original buffer line.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) struct WrapDisplayPoint {
|
|
/// The 0-based soft wrapped row index in the text.
|
|
pub row: usize,
|
|
/// The 0-based row index in local line (include first line).
|
|
///
|
|
/// This value only valid when return from [`TextWrapper::offset_to_display_point`], otherwise it will be ignored.
|
|
pub local_row: usize,
|
|
/// The 0-based column byte index in the display line (with soft wrap).
|
|
pub column: usize,
|
|
}
|
|
|
|
impl WrapDisplayPoint {
|
|
pub fn new(row: usize, local_row: usize, column: usize) -> Self {
|
|
Self {
|
|
row,
|
|
local_row,
|
|
column,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The layout info of a line with soft wrapped lines.
|
|
pub(crate) struct LineLayout {
|
|
/// Total bytes length of this line.
|
|
len: usize,
|
|
/// The soft wrapped lines of this line (Include the first line).
|
|
pub(crate) wrapped_lines: SmallVec<[ShapedLine; 1]>,
|
|
pub(crate) longest_width: Pixels,
|
|
pub(crate) whitespace_indicators: Option<WhitespaceIndicators>,
|
|
/// Whitespace indicators: (line_index, x_position, is_tab)
|
|
pub(crate) whitespace_chars: Vec<(usize, Pixels, bool)>,
|
|
}
|
|
|
|
impl LineLayout {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
len: 0,
|
|
longest_width: px(0.),
|
|
wrapped_lines: SmallVec::new(),
|
|
whitespace_chars: Vec::new(),
|
|
whitespace_indicators: None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn lines(mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) -> Self {
|
|
self.set_wrapped_lines(wrapped_lines);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn set_wrapped_lines(&mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) {
|
|
self.len = wrapped_lines.iter().map(|l| l.len).sum();
|
|
let width = wrapped_lines
|
|
.iter()
|
|
.map(|l| l.width)
|
|
.max()
|
|
.unwrap_or_default();
|
|
self.longest_width = width;
|
|
self.wrapped_lines = wrapped_lines;
|
|
}
|
|
|
|
pub(crate) fn with_whitespaces(mut self, indicators: Option<WhitespaceIndicators>) -> Self {
|
|
self.whitespace_indicators = indicators;
|
|
let Some(indicators) = self.whitespace_indicators.as_ref() else {
|
|
return self;
|
|
};
|
|
|
|
let space_indicator_offset = indicators.space.width.half();
|
|
|
|
for (line_index, wrapped_line) in self.wrapped_lines.iter().enumerate() {
|
|
for (relative_offset, c) in wrapped_line.text.char_indices() {
|
|
if matches!(c, ' ' | '\t') {
|
|
let is_tab = c == '\t';
|
|
let start_x = wrapped_line.x_for_index(relative_offset);
|
|
let end_x = wrapped_line.x_for_index(relative_offset + c.len_utf8());
|
|
// Center the indicator in the actual character's space
|
|
let x_position = if c == ' ' {
|
|
(start_x + end_x).half() - space_indicator_offset
|
|
} else {
|
|
start_x
|
|
};
|
|
|
|
self.whitespace_chars.push((line_index, x_position, is_tab));
|
|
}
|
|
}
|
|
}
|
|
self
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn len(&self) -> usize {
|
|
self.len
|
|
}
|
|
|
|
/// Get the position (x, y) for the given index in this line layout.
|
|
///
|
|
/// - The `offset` is a local byte index in this line layout.
|
|
/// - When `line_end_affinity` is true, an offset at a soft wrap boundary is placed at
|
|
/// the end of the current visual line rather than the start of the next one.
|
|
/// - The return value is relative to the top-left corner of this line layout, start from (0, 0)
|
|
pub(crate) fn position_for_index(
|
|
&self,
|
|
offset: usize,
|
|
last_layout: &LastLayout,
|
|
line_end_affinity: bool,
|
|
) -> Option<Point<Pixels>> {
|
|
let mut acc_len = 0;
|
|
let mut offset_y = px(0.);
|
|
|
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
|
|
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
|
let is_last = i + 1 == self.wrapped_lines.len();
|
|
|
|
let matches = if line.len == 0 {
|
|
// Empty visual lines still own their boundary offset.
|
|
offset == acc_len
|
|
} else if is_last || line_end_affinity {
|
|
// Inclusive: cursor can sit at end of this visual line.
|
|
offset >= acc_len && offset <= acc_len + line.len
|
|
} else {
|
|
// Exclusive: boundary offset belongs to the next visual line.
|
|
offset >= acc_len && offset < acc_len + line.len
|
|
};
|
|
|
|
if matches {
|
|
let x = line.x_for_index(offset.saturating_sub(acc_len)) + x_offset;
|
|
return Some(point(x, offset_y));
|
|
}
|
|
|
|
// Always advance by actual line length. The last line gets +1 so the
|
|
// cursor can be placed after the final character.
|
|
acc_len += if is_last { line.len + 1 } else { line.len };
|
|
offset_y += last_layout.line_height;
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Get the closest index for the given x in this line layout.
|
|
pub(crate) fn closest_index_for_x(&self, x: Pixels, last_layout: &LastLayout) -> usize {
|
|
let mut acc_len = 0;
|
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
|
let x = x - x_offset;
|
|
|
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
|
let is_last = i + 1 == self.wrapped_lines.len();
|
|
if x <= line.width {
|
|
let mut ix = line.closest_index_for_x(x);
|
|
if !is_last && ix == line.text.len() {
|
|
// For soft wrap line, we can't put the cursor at the end of the line.
|
|
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
|
ix = ix.saturating_sub(c_len);
|
|
}
|
|
|
|
return acc_len + ix;
|
|
}
|
|
acc_len += line.text.len();
|
|
}
|
|
|
|
acc_len
|
|
}
|
|
|
|
/// Get the index for the given position (x, y) in this line layout.
|
|
///
|
|
/// The `pos` is relative to the top-left corner of this line layout, start from (0, 0)
|
|
/// The return value is a local byte index in this line layout, start from 0.
|
|
pub(crate) fn closest_index_for_position(
|
|
&self,
|
|
pos: Point<Pixels>,
|
|
last_layout: &LastLayout,
|
|
) -> Option<usize> {
|
|
let mut offset = 0;
|
|
let mut line_top = px(0.);
|
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
|
let is_last = i + 1 == self.wrapped_lines.len();
|
|
let line_bottom = line_top + last_layout.line_height;
|
|
if pos.y >= line_top && pos.y < line_bottom {
|
|
let mut ix = line.closest_index_for_x(pos.x - x_offset);
|
|
if !is_last && ix == line.text.len() {
|
|
// For soft wrap line, we can't put the cursor at the end of the line.
|
|
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
|
ix = ix.saturating_sub(c_len);
|
|
}
|
|
return Some(offset + ix);
|
|
}
|
|
|
|
offset += line.text.len();
|
|
line_top = line_bottom;
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub(crate) fn index_for_position(
|
|
&self,
|
|
pos: Point<Pixels>,
|
|
last_layout: &LastLayout,
|
|
) -> Option<usize> {
|
|
let mut offset = 0;
|
|
let mut line_top = px(0.);
|
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
|
for line in self.wrapped_lines.iter() {
|
|
let line_bottom = line_top + last_layout.line_height;
|
|
if pos.y >= line_top && pos.y < line_bottom {
|
|
let ix = line.index_for_x(pos.x - x_offset)?;
|
|
return Some(offset + ix);
|
|
}
|
|
|
|
offset += line.text.len();
|
|
line_top = line_bottom;
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub(crate) fn size(&self, line_height: Pixels) -> Size<Pixels> {
|
|
size(self.longest_width, self.wrapped_lines.len() * line_height)
|
|
}
|
|
|
|
pub(crate) fn paint(
|
|
&self,
|
|
pos: Point<Pixels>,
|
|
line_height: Pixels,
|
|
text_align: TextAlign,
|
|
align_width: Option<Pixels>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
for (ix, line) in self.wrapped_lines.iter().enumerate() {
|
|
_ = line.paint(
|
|
pos + point(px(0.), ix * line_height),
|
|
line_height,
|
|
text_align,
|
|
align_width,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
|
|
// Paint whitespace indicators
|
|
if let Some(indicators) = self.whitespace_indicators.as_ref() {
|
|
for (line_index, x_position, is_tab) in &self.whitespace_chars {
|
|
let invisible = if *is_tab {
|
|
indicators.tab.clone()
|
|
} else {
|
|
indicators.space.clone()
|
|
};
|
|
|
|
let origin = point(
|
|
pos.x + *x_position,
|
|
pos.y + *line_index as f32 * line_height,
|
|
);
|
|
|
|
_ = invisible.paint(origin, line_height, text_align, align_width, window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|