input: Improve TextWrapper data structure. (#1237)
This commit is contained in:
parent
db5ba1e04c
commit
2a918bf4de
7 changed files with 115 additions and 112 deletions
|
|
@ -70,6 +70,15 @@ impl From<rope::Point> for LineColumn {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<LineColumn> for rope::Point {
|
||||
fn from(value: LineColumn) -> Self {
|
||||
Self {
|
||||
row: value.line.saturating_sub(1) as u32,
|
||||
column: value.column.saturating_sub(1) as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LineColumn> for tree_sitter::Point {
|
||||
fn from(value: LineColumn) -> Self {
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -392,7 +392,7 @@ impl TextElement {
|
|||
let mut visible_range = 0..total_lines;
|
||||
let mut line_bottom = px(0.);
|
||||
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
|
||||
let wrapped_height = (line.wrap_lines + 1) * line_height;
|
||||
let wrapped_height = line.height(line_height);
|
||||
line_bottom += wrapped_height;
|
||||
|
||||
if line_bottom < -scroll_top {
|
||||
|
|
@ -717,35 +717,38 @@ impl Element for TextElement {
|
|||
.expect("failed to shape text");
|
||||
// measure.end();
|
||||
|
||||
let longtest_line: SharedString = state
|
||||
.text
|
||||
.line(state.text.summary().longest_row as usize)
|
||||
.to_string()
|
||||
.into();
|
||||
let max_line_width = window
|
||||
.text_system()
|
||||
.shape_line(
|
||||
longtest_line.clone(),
|
||||
font_size,
|
||||
&[TextRun {
|
||||
len: longtest_line.len(),
|
||||
font: style.font(),
|
||||
color: gpui::black(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
wrap_width,
|
||||
)
|
||||
.width;
|
||||
let mut longest_line_width = px(0.);
|
||||
if state.mode.is_multi_line() && lines.len() > 1 {
|
||||
let longtest_line: SharedString = state
|
||||
.text
|
||||
.line(state.text.summary().longest_row as usize)
|
||||
.to_string()
|
||||
.into();
|
||||
longest_line_width = window
|
||||
.text_system()
|
||||
.shape_line(
|
||||
longtest_line.clone(),
|
||||
font_size,
|
||||
&[TextRun {
|
||||
len: longtest_line.len(),
|
||||
font: style.font(),
|
||||
color: gpui::black(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
wrap_width,
|
||||
)
|
||||
.width;
|
||||
}
|
||||
|
||||
let total_wrapped_lines = state.text_wrapper.len();
|
||||
|
||||
let scroll_size = size(
|
||||
if max_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
|
||||
max_line_width + line_number_width + RIGHT_MARGIN
|
||||
if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
|
||||
longest_line_width + line_number_width + RIGHT_MARGIN
|
||||
} else {
|
||||
max_line_width
|
||||
longest_line_width
|
||||
},
|
||||
(total_wrapped_lines as f32 * line_height).max(bounds.size.height),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ use crate::{
|
|||
input::{InputState, LineColumn},
|
||||
};
|
||||
use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle};
|
||||
use itertools::Itertools;
|
||||
use std::ops::Range;
|
||||
|
||||
/// Marker represents a diagnostic message, such as an error or warning, in the code editor.
|
||||
|
|
@ -36,41 +35,19 @@ impl Marker {
|
|||
|
||||
/// Prepare the marker to convert line, column to byte offsets.
|
||||
pub(super) fn prepare(&mut self, state: &InputState) {
|
||||
let Some(start_line) = state
|
||||
.text_wrapper
|
||||
.lines
|
||||
.get(self.start.line.saturating_sub(1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut start_point: rope::Point = self.start.into();
|
||||
let mut end_point: rope::Point = self.end.into();
|
||||
|
||||
let start_line_str = state.text.slice(start_line.range.clone());
|
||||
// limit column avoid overflow
|
||||
let start_line_len = state.text.line_len(start_point.row);
|
||||
start_point.column = start_point.column.min(start_line_len);
|
||||
let end_line_len = state.text.line_len(end_point.row);
|
||||
end_point.column = end_point.column.min(end_line_len);
|
||||
|
||||
let Some(end_line) = state
|
||||
.text_wrapper
|
||||
.lines
|
||||
.get(self.end.line.saturating_sub(1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let end_line_str = state.text.slice(end_line.range.clone());
|
||||
let start = state.text.point_to_offset(start_point);
|
||||
let end = state.text.point_to_offset(end_point);
|
||||
|
||||
let start_byte = start_line.range.start
|
||||
+ start_line_str
|
||||
.chars()
|
||||
.take(self.start.column.saturating_sub(1))
|
||||
.counts_by(|c| c.len_utf8())
|
||||
.values()
|
||||
.sum::<usize>();
|
||||
let end_byte = end_line.range.start
|
||||
+ end_line_str
|
||||
.chars()
|
||||
.take(self.end.column.saturating_sub(1))
|
||||
.counts_by(|c| c.len_utf8())
|
||||
.values()
|
||||
.sum::<usize>();
|
||||
|
||||
self.range = Some(start_byte..end_byte);
|
||||
self.range = Some(start..end);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ impl InputMode {
|
|||
}
|
||||
|
||||
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
|
||||
let wrapped_lines = text_wrapper.wrapped_lines.len();
|
||||
let wrapped_lines = text_wrapper.len();
|
||||
self.set_rows(wrapped_lines);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,9 +88,8 @@ impl std::iter::FusedIterator for RopeLines {}
|
|||
|
||||
impl RopeExt for Rope {
|
||||
fn line(&self, row: usize) -> Rope {
|
||||
let row = row as u32;
|
||||
let start = self.point_to_offset(Point::new(row, 0));
|
||||
let end = start + self.line_len(row) as usize;
|
||||
let start = self.line_start_offset(row);
|
||||
let end = start + self.line_len(row as u32) as usize;
|
||||
self.slice(start..end)
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +144,9 @@ mod tests {
|
|||
assert_eq!(rope.line(1).to_string(), "World\r");
|
||||
assert_eq!(rope.line(2).to_string(), "This is a test 中文");
|
||||
assert_eq!(rope.line(3).to_string(), "Rope");
|
||||
assert_eq!(rope.line(4).to_string(), "");
|
||||
|
||||
// over bounds
|
||||
assert_eq!(rope.line(6).to_string(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ use super::{
|
|||
};
|
||||
use crate::input::hover_popover::DiagnosticPopover;
|
||||
use crate::input::marker::Marker;
|
||||
use crate::input::text_wrapper::LineWrap;
|
||||
use crate::input::text_wrapper::LineItem;
|
||||
use crate::input::{LineColumn, RopeExt as _, Selection};
|
||||
use crate::{history::History, scroll::ScrollbarState, Root};
|
||||
|
||||
|
|
@ -687,6 +687,11 @@ impl InputState {
|
|||
.unwrap_or(self.input_bounds.size.width);
|
||||
|
||||
self.text_wrapper.set_wrap_width(Some(wrap_width), cx);
|
||||
|
||||
// Reset scroll to left 0
|
||||
let mut offset = self.scroll_handle.offset();
|
||||
offset.x = px(0.);
|
||||
self.scroll_handle.set_offset(offset);
|
||||
} else {
|
||||
self.text_wrapper.set_wrap_width(None, cx);
|
||||
}
|
||||
|
|
@ -725,7 +730,7 @@ impl InputState {
|
|||
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
||||
let text: SharedString = value.into();
|
||||
self.text = Rope::from(text.as_str());
|
||||
self.text_wrapper.text = self.text.clone();
|
||||
self.text_wrapper.set_default_text(&self.text);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -1722,7 +1727,7 @@ impl InputState {
|
|||
fn line_origin_with_y_offset(
|
||||
&self,
|
||||
y_offset: &mut Pixels,
|
||||
line: &LineWrap,
|
||||
line: &LineItem,
|
||||
line_height: Pixels,
|
||||
) -> Point<Pixels> {
|
||||
// NOTE: About line.wrap_boundaries.len()
|
||||
|
|
@ -1731,8 +1736,7 @@ impl InputState {
|
|||
// If have 2 line, the value is 1
|
||||
if self.mode.is_multi_line() {
|
||||
let p = point(px(0.), *y_offset);
|
||||
let height = line_height + line.wrap_lines as f32 * line_height;
|
||||
*y_offset = *y_offset + height;
|
||||
*y_offset += line.height(line_height);
|
||||
p
|
||||
} else {
|
||||
point(px(0.), px(0.))
|
||||
|
|
|
|||
|
|
@ -5,48 +5,51 @@ use rope::Rope;
|
|||
|
||||
use crate::input::RopeExt as _;
|
||||
|
||||
#[allow(unused)]
|
||||
pub(super) struct LineWrap {
|
||||
/// The number of soft wrapped lines of this line (Not include first line.)
|
||||
/// A line with soft wrapped lines info.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct LineItem {
|
||||
/// The original line text.
|
||||
line: Rope,
|
||||
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
||||
///
|
||||
/// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
|
||||
/// like the `window.text_system().shape_text`. So, this value may not equal
|
||||
/// the actual rendered lines.
|
||||
pub(super) wrap_lines: usize,
|
||||
/// The range of the line text in the entire text (not includes ending `\n`).
|
||||
pub(super) range: Range<usize>,
|
||||
wrapped_lines: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
impl LineWrap {
|
||||
/// Return the bytes length of this line.
|
||||
impl LineItem {
|
||||
/// Get the bytes length of this line.
|
||||
#[inline]
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.range.end - self.range.start
|
||||
self.line.len()
|
||||
}
|
||||
|
||||
/// Get the total number of lines including wrapped lines.
|
||||
/// Get number of soft wrapped lines of this line (include the first line).
|
||||
#[inline]
|
||||
pub(super) fn lines_len(&self) -> usize {
|
||||
self.wrap_lines + 1
|
||||
self.wrapped_lines.len()
|
||||
}
|
||||
|
||||
/// Get the height of this line including wrapped lines.
|
||||
/// Get the height of this line item with given line height.
|
||||
pub(super) fn height(&self, line_height: Pixels) -> Pixels {
|
||||
self.lines_len() * line_height
|
||||
self.lines_len() as f32 * line_height
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to prepare the text with soft wrap to be get lines to displayed in the TextArea
|
||||
/// 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 TextArea
|
||||
/// After use lines to calculate the scroll size of the Editor.
|
||||
pub(super) struct TextWrapper {
|
||||
pub(super) text: Rope,
|
||||
/// The wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||
pub(super) wrapped_lines: Vec<Range<usize>>,
|
||||
/// The lines by split \n
|
||||
pub(super) lines: Vec<LineWrap>,
|
||||
pub(super) font: Font,
|
||||
pub(super) font_size: Pixels,
|
||||
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
|
||||
pub(super) wrap_width: Option<Pixels>,
|
||||
wrap_width: Option<Pixels>,
|
||||
/// The lines by split \n
|
||||
pub(super) lines: Vec<LineItem>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
|
|
@ -57,14 +60,26 @@ impl TextWrapper {
|
|||
font,
|
||||
font_size,
|
||||
wrap_width,
|
||||
wrapped_lines: Vec::new(),
|
||||
soft_lines: 0,
|
||||
lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn set_default_text(&mut self, text: &Rope) {
|
||||
self.text = text.clone();
|
||||
}
|
||||
|
||||
/// Get the total number of lines including wrapped lines.
|
||||
#[inline]
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.wrapped_lines.len()
|
||||
self.soft_lines
|
||||
}
|
||||
|
||||
/// Get the line item by row index.
|
||||
#[inline]
|
||||
pub(super) fn line(&self, row: usize) -> Option<&LineItem> {
|
||||
self.lines.iter().skip(row).next()
|
||||
}
|
||||
|
||||
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||
|
|
@ -94,45 +109,39 @@ impl TextWrapper {
|
|||
return;
|
||||
}
|
||||
|
||||
let mut wrapped_lines = vec![];
|
||||
let mut lines = vec![];
|
||||
let wrap_width = self.wrap_width;
|
||||
let mut line_wrapper = cx
|
||||
.text_system()
|
||||
.line_wrapper(self.font.clone(), self.font_size);
|
||||
let mut prev_line_ix = 0;
|
||||
|
||||
self.lines.clear();
|
||||
for line in text.lines() {
|
||||
let line = line.to_string();
|
||||
let mut line_wraps = vec![];
|
||||
let line_str = line.to_string();
|
||||
let mut wrapped_lines = vec![];
|
||||
let mut prev_boundary_ix = 0;
|
||||
|
||||
// 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 line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) {
|
||||
line_wraps.push(prev_boundary_ix..boundary.ix);
|
||||
for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line_str)], wrap_width)
|
||||
{
|
||||
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||
prev_boundary_ix = boundary.ix;
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(LineWrap {
|
||||
wrap_lines: line_wraps.len(),
|
||||
range: prev_line_ix..prev_line_ix + line.len(),
|
||||
});
|
||||
|
||||
wrapped_lines.extend(line_wraps);
|
||||
// Reset of the line
|
||||
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
|
||||
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||
wrapped_lines.push(prev_boundary_ix..line.len());
|
||||
}
|
||||
|
||||
// +1 for \n
|
||||
prev_line_ix += line.len() + 1;
|
||||
self.lines.push(LineItem {
|
||||
line: line.clone(),
|
||||
wrapped_lines,
|
||||
});
|
||||
}
|
||||
|
||||
self.text = text.clone();
|
||||
self.wrapped_lines = wrapped_lines;
|
||||
self.lines = lines;
|
||||
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue