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 {
|
impl From<LineColumn> for tree_sitter::Point {
|
||||||
fn from(value: LineColumn) -> Self {
|
fn from(value: LineColumn) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,7 @@ impl TextElement {
|
||||||
let mut visible_range = 0..total_lines;
|
let mut visible_range = 0..total_lines;
|
||||||
let mut line_bottom = px(0.);
|
let mut line_bottom = px(0.);
|
||||||
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
|
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;
|
line_bottom += wrapped_height;
|
||||||
|
|
||||||
if line_bottom < -scroll_top {
|
if line_bottom < -scroll_top {
|
||||||
|
|
@ -717,12 +717,14 @@ impl Element for TextElement {
|
||||||
.expect("failed to shape text");
|
.expect("failed to shape text");
|
||||||
// measure.end();
|
// measure.end();
|
||||||
|
|
||||||
|
let mut longest_line_width = px(0.);
|
||||||
|
if state.mode.is_multi_line() && lines.len() > 1 {
|
||||||
let longtest_line: SharedString = state
|
let longtest_line: SharedString = state
|
||||||
.text
|
.text
|
||||||
.line(state.text.summary().longest_row as usize)
|
.line(state.text.summary().longest_row as usize)
|
||||||
.to_string()
|
.to_string()
|
||||||
.into();
|
.into();
|
||||||
let max_line_width = window
|
longest_line_width = window
|
||||||
.text_system()
|
.text_system()
|
||||||
.shape_line(
|
.shape_line(
|
||||||
longtest_line.clone(),
|
longtest_line.clone(),
|
||||||
|
|
@ -738,14 +740,15 @@ impl Element for TextElement {
|
||||||
wrap_width,
|
wrap_width,
|
||||||
)
|
)
|
||||||
.width;
|
.width;
|
||||||
|
}
|
||||||
|
|
||||||
let total_wrapped_lines = state.text_wrapper.len();
|
let total_wrapped_lines = state.text_wrapper.len();
|
||||||
|
|
||||||
let scroll_size = size(
|
let scroll_size = size(
|
||||||
if max_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
|
if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
|
||||||
max_line_width + line_number_width + RIGHT_MARGIN
|
longest_line_width + line_number_width + RIGHT_MARGIN
|
||||||
} else {
|
} else {
|
||||||
max_line_width
|
longest_line_width
|
||||||
},
|
},
|
||||||
(total_wrapped_lines as f32 * line_height).max(bounds.size.height),
|
(total_wrapped_lines as f32 * line_height).max(bounds.size.height),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ use crate::{
|
||||||
input::{InputState, LineColumn},
|
input::{InputState, LineColumn},
|
||||||
};
|
};
|
||||||
use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle};
|
use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle};
|
||||||
use itertools::Itertools;
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
/// Marker represents a diagnostic message, such as an error or warning, in the code editor.
|
/// 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.
|
/// Prepare the marker to convert line, column to byte offsets.
|
||||||
pub(super) fn prepare(&mut self, state: &InputState) {
|
pub(super) fn prepare(&mut self, state: &InputState) {
|
||||||
let Some(start_line) = state
|
let mut start_point: rope::Point = self.start.into();
|
||||||
.text_wrapper
|
let mut end_point: rope::Point = self.end.into();
|
||||||
.lines
|
|
||||||
.get(self.start.line.saturating_sub(1))
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
let start = state.text.point_to_offset(start_point);
|
||||||
.text_wrapper
|
let end = state.text.point_to_offset(end_point);
|
||||||
.lines
|
|
||||||
.get(self.end.line.saturating_sub(1))
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let end_line_str = state.text.slice(end_line.range.clone());
|
|
||||||
|
|
||||||
let start_byte = start_line.range.start
|
self.range = Some(start..end);
|
||||||
+ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ impl InputMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
|
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);
|
self.set_rows(wrapped_lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,8 @@ impl std::iter::FusedIterator for RopeLines {}
|
||||||
|
|
||||||
impl RopeExt for Rope {
|
impl RopeExt for Rope {
|
||||||
fn line(&self, row: usize) -> Rope {
|
fn line(&self, row: usize) -> Rope {
|
||||||
let row = row as u32;
|
let start = self.line_start_offset(row);
|
||||||
let start = self.point_to_offset(Point::new(row, 0));
|
let end = start + self.line_len(row as u32) as usize;
|
||||||
let end = start + self.line_len(row) as usize;
|
|
||||||
self.slice(start..end)
|
self.slice(start..end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +144,9 @@ mod tests {
|
||||||
assert_eq!(rope.line(1).to_string(), "World\r");
|
assert_eq!(rope.line(1).to_string(), "World\r");
|
||||||
assert_eq!(rope.line(2).to_string(), "This is a test 中文");
|
assert_eq!(rope.line(2).to_string(), "This is a test 中文");
|
||||||
assert_eq!(rope.line(3).to_string(), "Rope");
|
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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ use super::{
|
||||||
};
|
};
|
||||||
use crate::input::hover_popover::DiagnosticPopover;
|
use crate::input::hover_popover::DiagnosticPopover;
|
||||||
use crate::input::marker::Marker;
|
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::input::{LineColumn, RopeExt as _, Selection};
|
||||||
use crate::{history::History, scroll::ScrollbarState, Root};
|
use crate::{history::History, scroll::ScrollbarState, Root};
|
||||||
|
|
||||||
|
|
@ -687,6 +687,11 @@ impl InputState {
|
||||||
.unwrap_or(self.input_bounds.size.width);
|
.unwrap_or(self.input_bounds.size.width);
|
||||||
|
|
||||||
self.text_wrapper.set_wrap_width(Some(wrap_width), cx);
|
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 {
|
} else {
|
||||||
self.text_wrapper.set_wrap_width(None, cx);
|
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 {
|
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
||||||
let text: SharedString = value.into();
|
let text: SharedString = value.into();
|
||||||
self.text = Rope::from(text.as_str());
|
self.text = Rope::from(text.as_str());
|
||||||
self.text_wrapper.text = self.text.clone();
|
self.text_wrapper.set_default_text(&self.text);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1722,7 +1727,7 @@ impl InputState {
|
||||||
fn line_origin_with_y_offset(
|
fn line_origin_with_y_offset(
|
||||||
&self,
|
&self,
|
||||||
y_offset: &mut Pixels,
|
y_offset: &mut Pixels,
|
||||||
line: &LineWrap,
|
line: &LineItem,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
) -> Point<Pixels> {
|
) -> Point<Pixels> {
|
||||||
// NOTE: About line.wrap_boundaries.len()
|
// NOTE: About line.wrap_boundaries.len()
|
||||||
|
|
@ -1731,8 +1736,7 @@ impl InputState {
|
||||||
// If have 2 line, the value is 1
|
// If have 2 line, the value is 1
|
||||||
if self.mode.is_multi_line() {
|
if self.mode.is_multi_line() {
|
||||||
let p = point(px(0.), *y_offset);
|
let p = point(px(0.), *y_offset);
|
||||||
let height = line_height + line.wrap_lines as f32 * line_height;
|
*y_offset += line.height(line_height);
|
||||||
*y_offset = *y_offset + height;
|
|
||||||
p
|
p
|
||||||
} else {
|
} else {
|
||||||
point(px(0.), px(0.))
|
point(px(0.), px(0.))
|
||||||
|
|
|
||||||
|
|
@ -5,48 +5,51 @@ use rope::Rope;
|
||||||
|
|
||||||
use crate::input::RopeExt as _;
|
use crate::input::RopeExt as _;
|
||||||
|
|
||||||
#[allow(unused)]
|
/// A line with soft wrapped lines info.
|
||||||
pub(super) struct LineWrap {
|
#[derive(Clone)]
|
||||||
/// The number of soft wrapped lines of this line (Not include first line.)
|
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
|
/// 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
|
/// like the `window.text_system().shape_text`. So, this value may not equal
|
||||||
/// the actual rendered lines.
|
/// the actual rendered lines.
|
||||||
pub(super) wrap_lines: usize,
|
wrapped_lines: Vec<Range<usize>>,
|
||||||
/// The range of the line text in the entire text (not includes ending `\n`).
|
|
||||||
pub(super) range: Range<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LineWrap {
|
impl LineItem {
|
||||||
/// Return the bytes length of this line.
|
/// Get the bytes length of this line.
|
||||||
|
#[inline]
|
||||||
pub(super) fn len(&self) -> usize {
|
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 {
|
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 {
|
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) struct TextWrapper {
|
||||||
pub(super) text: Rope,
|
text: Rope,
|
||||||
/// The wrapped lines (Inlucde the first line), value is start and end index of the line.
|
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||||
pub(super) wrapped_lines: Vec<Range<usize>>,
|
soft_lines: usize,
|
||||||
/// The lines by split \n
|
font: Font,
|
||||||
pub(super) lines: Vec<LineWrap>,
|
font_size: Pixels,
|
||||||
pub(super) font: Font,
|
|
||||||
pub(super) font_size: Pixels,
|
|
||||||
/// If is none, it means the text is not wrapped
|
/// 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)]
|
#[allow(unused)]
|
||||||
|
|
@ -57,14 +60,26 @@ impl TextWrapper {
|
||||||
font,
|
font,
|
||||||
font_size,
|
font_size,
|
||||||
wrap_width,
|
wrap_width,
|
||||||
wrapped_lines: Vec::new(),
|
soft_lines: 0,
|
||||||
lines: Vec::new(),
|
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.
|
/// Get the total number of lines including wrapped lines.
|
||||||
|
#[inline]
|
||||||
pub(super) fn len(&self) -> usize {
|
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) {
|
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
|
|
@ -94,45 +109,39 @@ impl TextWrapper {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut wrapped_lines = vec![];
|
|
||||||
let mut lines = vec![];
|
|
||||||
let wrap_width = self.wrap_width;
|
let wrap_width = self.wrap_width;
|
||||||
let mut line_wrapper = cx
|
let mut line_wrapper = cx
|
||||||
.text_system()
|
.text_system()
|
||||||
.line_wrapper(self.font.clone(), self.font_size);
|
.line_wrapper(self.font.clone(), self.font_size);
|
||||||
let mut prev_line_ix = 0;
|
|
||||||
|
|
||||||
|
self.lines.clear();
|
||||||
for line in text.lines() {
|
for line in text.lines() {
|
||||||
let line = line.to_string();
|
let line_str = line.to_string();
|
||||||
let mut line_wraps = vec![];
|
let mut wrapped_lines = vec![];
|
||||||
let mut prev_boundary_ix = 0;
|
let mut prev_boundary_ix = 0;
|
||||||
|
|
||||||
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
||||||
if let Some(wrap_width) = wrap_width {
|
if let Some(wrap_width) = wrap_width {
|
||||||
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
// 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) {
|
for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line_str)], wrap_width)
|
||||||
line_wraps.push(prev_boundary_ix..boundary.ix);
|
{
|
||||||
|
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||||
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
|
// Reset of the line
|
||||||
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||||
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
|
wrapped_lines.push(prev_boundary_ix..line.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
// +1 for \n
|
self.lines.push(LineItem {
|
||||||
prev_line_ix += line.len() + 1;
|
line: line.clone(),
|
||||||
|
wrapped_lines,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.text = text.clone();
|
self.text = text.clone();
|
||||||
self.wrapped_lines = wrapped_lines;
|
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
||||||
self.lines = lines;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue