input: Improve Input performance by only shape visible lines. (#1222)
Close #1193 Ref issue #1220 This PR aim to shape text in visible ranges to reduce `shape_text` time for large document case. - Fix rendering might be delayed and cause blanks when scrolling to follow the selection. - Fix `RopeExt::lines` method to avoid call `line` method on skipped iter.
This commit is contained in:
parent
84b2d2a6f5
commit
03c2327cdb
5 changed files with 266 additions and 144 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
/target
|
/target
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/docks.json
|
/docks.json
|
||||||
|
/profile.json
|
||||||
.vscode
|
.vscode
|
||||||
index.scip
|
index.scip
|
||||||
*.log
|
*.log
|
||||||
|
|
@ -6,6 +6,7 @@ use gpui::{
|
||||||
Path, Pixels, Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window,
|
Path, Pixels, Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window,
|
||||||
WrappedLine,
|
WrappedLine,
|
||||||
};
|
};
|
||||||
|
use rope::Rope;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -60,13 +61,18 @@ impl TextElement {
|
||||||
/// - current line index
|
/// - current line index
|
||||||
fn layout_cursor(
|
fn layout_cursor(
|
||||||
&self,
|
&self,
|
||||||
lines: &[WrappedLine],
|
last_layout: &LastLayout,
|
||||||
line_height: Pixels,
|
|
||||||
bounds: &mut Bounds<Pixels>,
|
bounds: &mut Bounds<Pixels>,
|
||||||
line_number_width: Pixels,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
|
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
|
||||||
|
let line_height = last_layout.line_height;
|
||||||
|
let visible_range = &last_layout.visible_range;
|
||||||
|
let visible_top = last_layout.visible_top;
|
||||||
|
let visible_start_offset = last_layout.visible_start_offset;
|
||||||
|
let lines = &last_layout.lines;
|
||||||
|
let line_number_width = last_layout.line_number_width;
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let mut selected_range = state.selected_range;
|
let mut selected_range = state.selected_range;
|
||||||
if let Some(marked_range) = &state.marked_range {
|
if let Some(marked_range) = &state.marked_range {
|
||||||
|
|
@ -89,9 +95,11 @@ impl TextElement {
|
||||||
let mut cursor_start = None;
|
let mut cursor_start = None;
|
||||||
let mut cursor_end = None;
|
let mut cursor_end = None;
|
||||||
|
|
||||||
let mut prev_lines_offset = 0;
|
let mut prev_lines_offset = visible_start_offset;
|
||||||
let mut offset_y = px(0.);
|
let mut offset_y = visible_top;
|
||||||
for (line_ix, line) in lines.iter().enumerate() {
|
for (line_ix, line) in lines.iter().enumerate() {
|
||||||
|
let line_ix = visible_range.start + line_ix;
|
||||||
|
|
||||||
// break loop if all cursor positions are found
|
// break loop if all cursor positions are found
|
||||||
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
|
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
|
||||||
break;
|
break;
|
||||||
|
|
@ -124,6 +132,14 @@ impl TextElement {
|
||||||
prev_lines_offset += line.len() + 1;
|
prev_lines_offset += line.len() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If cursor_pos and cursor_end is still None, it means the cursor is at the end of the text.
|
||||||
|
if cursor_pos.is_none() {
|
||||||
|
cursor_pos = Some(point(px(0.), offset_y));
|
||||||
|
}
|
||||||
|
if cursor_end.is_none() {
|
||||||
|
cursor_end = cursor_pos;
|
||||||
|
}
|
||||||
|
|
||||||
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
|
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
|
||||||
(cursor_pos, cursor_start, cursor_end)
|
(cursor_pos, cursor_start, cursor_end)
|
||||||
{
|
{
|
||||||
|
|
@ -195,13 +211,17 @@ impl TextElement {
|
||||||
|
|
||||||
fn layout_selections(
|
fn layout_selections(
|
||||||
&self,
|
&self,
|
||||||
lines: &[WrappedLine],
|
last_layout: &LastLayout,
|
||||||
line_height: Pixels,
|
|
||||||
bounds: &mut Bounds<Pixels>,
|
bounds: &mut Bounds<Pixels>,
|
||||||
line_number_width: Pixels,
|
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<Path<Pixels>> {
|
) -> Option<Path<Pixels>> {
|
||||||
|
let line_height = last_layout.line_height;
|
||||||
|
let visible_top = last_layout.visible_top;
|
||||||
|
let visible_start_offset = last_layout.visible_start_offset;
|
||||||
|
let lines = &last_layout.lines;
|
||||||
|
let line_number_width = last_layout.line_number_width;
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let mut selected_range = state.selected_range;
|
let mut selected_range = state.selected_range;
|
||||||
if let Some(marked_range) = &state.marked_range {
|
if let Some(marked_range) = &state.marked_range {
|
||||||
|
|
@ -219,10 +239,10 @@ impl TextElement {
|
||||||
(selected_range.end, selected_range.start)
|
(selected_range.end, selected_range.start)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut prev_lines_offset = 0;
|
let mut prev_lines_offset = visible_start_offset;
|
||||||
|
let mut offset_y = visible_top;
|
||||||
let mut line_corners = vec![];
|
let mut line_corners = vec![];
|
||||||
|
|
||||||
let mut offset_y = px(0.);
|
|
||||||
for line in lines.iter() {
|
for line in lines.iter() {
|
||||||
let line_size = line.size(line_height);
|
let line_size = line.size(line_height);
|
||||||
let line_wrap_width = line_size.width;
|
let line_wrap_width = line_size.width;
|
||||||
|
|
@ -330,48 +350,54 @@ impl TextElement {
|
||||||
|
|
||||||
/// Calculate the visible range of lines in the viewport.
|
/// Calculate the visible range of lines in the viewport.
|
||||||
///
|
///
|
||||||
/// The visible range is based on unwrapped lines (Zero based).
|
/// Returns
|
||||||
|
///
|
||||||
|
/// - visible_range: The visible range is based on unwrapped lines (Zero based).
|
||||||
|
/// - visible_top: The top position of the first visible line in the scroll viewport.
|
||||||
fn calculate_visible_range(
|
fn calculate_visible_range(
|
||||||
&self,
|
&self,
|
||||||
state: &InputState,
|
state: &InputState,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
input_height: Pixels,
|
input_height: Pixels,
|
||||||
) -> Range<usize> {
|
) -> (Range<usize>, Pixels) {
|
||||||
|
// Add extra rows to avoid showing empty space when scroll to bottom.
|
||||||
|
let extra_rows = 1;
|
||||||
|
let mut visible_top = px(0.);
|
||||||
if state.mode.is_single_line() {
|
if state.mode.is_single_line() {
|
||||||
return 0..1;
|
return (0..1, visible_top);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(last_layout) = state.last_layout.as_ref() else {
|
let total_lines = state.text_wrapper.len();
|
||||||
return 0..1;
|
|
||||||
};
|
|
||||||
|
|
||||||
let scroll_top = state.scroll_handle.offset().y;
|
let scroll_top = state.scroll_handle.offset().y;
|
||||||
let total_lines = last_layout.lines.len();
|
|
||||||
|
|
||||||
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 last_layout.lines.iter().enumerate() {
|
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
|
||||||
line_bottom += (line.wrap_boundaries.len() + 1) * line_height;
|
let wrapped_height = (line.wrap_lines + 1) * line_height;
|
||||||
|
line_bottom += wrapped_height;
|
||||||
|
|
||||||
if line_bottom < -scroll_top {
|
if line_bottom < -scroll_top {
|
||||||
|
visible_top = line_bottom - wrapped_height;
|
||||||
visible_range.start = ix;
|
visible_range.start = ix;
|
||||||
}
|
}
|
||||||
|
|
||||||
if line_bottom + scroll_top >= input_height {
|
if line_bottom + scroll_top >= input_height {
|
||||||
visible_range.end = (ix + 1).min(total_lines);
|
visible_range.end = (ix + extra_rows).min(total_lines);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visible_range
|
(visible_range, visible_top)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// First usize is the offset of skipped.
|
/// First usize is the offset of skipped.
|
||||||
fn highlight_lines(
|
fn highlight_lines(
|
||||||
&mut self,
|
&mut self,
|
||||||
visible_range: &Range<usize>,
|
visible_range: &Range<usize>,
|
||||||
|
_visible_top: Pixels,
|
||||||
|
visible_start_offset: usize,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<(usize, Vec<(Range<usize>, HighlightStyle)>)> {
|
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
|
||||||
let theme = cx.theme().highlight_theme.clone();
|
let theme = cx.theme().highlight_theme.clone();
|
||||||
self.state.update(cx, |state, cx| match &state.mode {
|
self.state.update(cx, |state, cx| match &state.mode {
|
||||||
InputMode::CodeEditor {
|
InputMode::CodeEditor {
|
||||||
|
|
@ -389,22 +415,17 @@ impl TextElement {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut offset = 0;
|
let mut offset = visible_start_offset;
|
||||||
let mut skipped_offset = 0;
|
|
||||||
let mut styles = vec![];
|
let mut styles = vec![];
|
||||||
|
|
||||||
for (ix, line) in state.text.lines().enumerate() {
|
for line in state
|
||||||
|
.text
|
||||||
|
.lines()
|
||||||
|
.skip(visible_range.start)
|
||||||
|
.take(visible_range.len())
|
||||||
|
{
|
||||||
// +1 for `\n`
|
// +1 for `\n`
|
||||||
let line_len = line.len() + 1;
|
let line_len = line.len() + 1;
|
||||||
if ix < visible_range.start {
|
|
||||||
offset += line_len;
|
|
||||||
skipped_offset = offset;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ix > visible_range.end {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let range = offset..offset + line_len;
|
let range = offset..offset + line_len;
|
||||||
let line_styles = highlighter.styles(&range, &theme);
|
let line_styles = highlighter.styles(&range, &theme);
|
||||||
styles = gpui::combine_highlights(styles, line_styles).collect();
|
styles = gpui::combine_highlights(styles, line_styles).collect();
|
||||||
|
|
@ -412,26 +433,24 @@ impl TextElement {
|
||||||
offset = range.end;
|
offset = range.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut marker_styles = vec![];
|
// Combine marker styles
|
||||||
for marker in markers.iter() {
|
if !markers.is_empty() {
|
||||||
if let Some(range) = &marker.range {
|
let mut marker_styles = vec![];
|
||||||
if range.start < skipped_offset {
|
for marker in markers.iter() {
|
||||||
continue;
|
if let Some(range) = &marker.range {
|
||||||
}
|
if range.start < visible_start_offset {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let node_range = range.start..range.end;
|
|
||||||
if node_range.start >= visible_range.start
|
|
||||||
|| node_range.end <= visible_range.end
|
|
||||||
{
|
|
||||||
marker_styles
|
marker_styles
|
||||||
.push((node_range, marker.severity.highlight_style(&theme, cx)));
|
.push((range.clone(), marker.severity.highlight_style(&theme, cx)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
styles = gpui::combine_highlights(marker_styles, styles).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
styles = gpui::combine_highlights(marker_styles, styles).collect();
|
Some(styles)
|
||||||
|
|
||||||
Some((skipped_offset, styles))
|
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
|
|
@ -543,8 +562,12 @@ impl Element for TextElement {
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let line_height = window.line_height();
|
let line_height = window.line_height();
|
||||||
|
|
||||||
let visible_range = self.calculate_visible_range(&state, line_height, bounds.size.height);
|
let (visible_range, visible_top) =
|
||||||
let highlight_styles = self.highlight_lines(&visible_range, cx);
|
self.calculate_visible_range(&state, line_height, bounds.size.height);
|
||||||
|
let visible_start_offset = state.text.line_start_offset(visible_range.start);
|
||||||
|
|
||||||
|
let highlight_styles =
|
||||||
|
self.highlight_lines(&visible_range, visible_top, visible_start_offset, cx);
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let multi_line = state.mode.is_multi_line();
|
let multi_line = state.mode.is_multi_line();
|
||||||
|
|
@ -556,11 +579,17 @@ impl Element for TextElement {
|
||||||
let mut bounds = bounds;
|
let mut bounds = bounds;
|
||||||
|
|
||||||
let (display_text, text_color) = if is_empty {
|
let (display_text, text_color) = if is_empty {
|
||||||
(placeholder, cx.theme().muted_foreground)
|
(
|
||||||
|
Rope::from(placeholder.as_str()),
|
||||||
|
cx.theme().muted_foreground,
|
||||||
|
)
|
||||||
} else if state.masked {
|
} else if state.masked {
|
||||||
("*".repeat(text.chars_count()).into(), cx.theme().foreground)
|
(
|
||||||
|
Rope::from("*".repeat(text.chars_count())),
|
||||||
|
cx.theme().foreground,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(text.to_string().into(), cx.theme().foreground)
|
(text.clone(), cx.theme().foreground)
|
||||||
};
|
};
|
||||||
|
|
||||||
let text_style = window.text_style();
|
let text_style = window.text_style();
|
||||||
|
|
@ -607,14 +636,8 @@ impl Element for TextElement {
|
||||||
};
|
};
|
||||||
|
|
||||||
let runs = if !is_empty {
|
let runs = if !is_empty {
|
||||||
if let Some((skipped_offset, highlight_styles)) = highlight_styles {
|
if let Some(highlight_styles) = highlight_styles {
|
||||||
let mut runs = vec![];
|
let mut runs = vec![];
|
||||||
if skipped_offset > 0 {
|
|
||||||
runs.push(TextRun {
|
|
||||||
len: skipped_offset,
|
|
||||||
..run.clone()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
runs.extend(highlight_styles.iter().map(|(range, style)| {
|
runs.extend(highlight_styles.iter().map(|(range, style)| {
|
||||||
let mut run = text_style.clone().highlight(*style).to_run(range.len());
|
let mut run = text_style.clone().highlight(*style).to_run(range.len());
|
||||||
|
|
@ -663,21 +686,23 @@ impl Element for TextElement {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: If there have about 10K lines, this will take about 5~6ms.
|
// NOTE: Here 50 lines about 150µs
|
||||||
// let measure = Measure::new("shape_text");
|
// let measure = crate::Measure::new("shape_text");
|
||||||
|
let visible_text = display_text
|
||||||
|
.slice_rows(visible_range.start as u32..visible_range.end as u32)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let lines = window
|
let lines = window
|
||||||
.text_system()
|
.text_system()
|
||||||
.shape_text(display_text, font_size, &runs, wrap_width, None)
|
.shape_text(visible_text.into(), font_size, &runs, wrap_width, None)
|
||||||
.expect("failed to shape text");
|
.expect("failed to shape text");
|
||||||
// measure.end();
|
// measure.end();
|
||||||
|
|
||||||
let mut max_line_width = px(0.);
|
let mut max_line_width = px(0.);
|
||||||
let mut total_wrapped_lines = 0;
|
let total_wrapped_lines = state.text_wrapper.len();
|
||||||
for line in lines.iter() {
|
for line in lines.iter() {
|
||||||
// FIXME: The `shape_text` measured width is not stable, sometime will large, sometime small.
|
// FIXME: The `shape_text` measured width is not stable, sometime will large, sometime small.
|
||||||
max_line_width = max_line_width.max(line.width());
|
max_line_width = max_line_width.max(line.width());
|
||||||
// +1 is the first line, `wrap_boundaries` is the wrapped lines after the `\n`.
|
|
||||||
total_wrapped_lines += 1 + line.wrap_boundaries.len();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let scroll_size = size(
|
let scroll_size = size(
|
||||||
|
|
@ -689,6 +714,16 @@ impl Element for TextElement {
|
||||||
(total_wrapped_lines as f32 * line_height).max(bounds.size.height),
|
(total_wrapped_lines as f32 * line_height).max(bounds.size.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let last_layout = LastLayout {
|
||||||
|
visible_range,
|
||||||
|
visible_top,
|
||||||
|
visible_start_offset,
|
||||||
|
line_height,
|
||||||
|
wrap_width,
|
||||||
|
line_number_width,
|
||||||
|
lines: Rc::new(lines),
|
||||||
|
};
|
||||||
|
|
||||||
// `position_for_index` for example
|
// `position_for_index` for example
|
||||||
//
|
//
|
||||||
// #### text
|
// #### text
|
||||||
|
|
@ -720,23 +755,10 @@ impl Element for TextElement {
|
||||||
|
|
||||||
// Calculate the scroll offset to keep the cursor in view
|
// Calculate the scroll offset to keep the cursor in view
|
||||||
|
|
||||||
let (cursor_bounds, cursor_scroll_offset, current_line_index) = self.layout_cursor(
|
let (cursor_bounds, cursor_scroll_offset, current_line_index) =
|
||||||
&lines,
|
self.layout_cursor(&last_layout, &mut bounds, window, cx);
|
||||||
line_height,
|
|
||||||
&mut bounds,
|
|
||||||
line_number_width,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let selection_path = self.layout_selections(
|
let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx);
|
||||||
&lines,
|
|
||||||
line_height,
|
|
||||||
&mut bounds,
|
|
||||||
line_number_width,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let line_numbers = if state.mode.line_number() {
|
let line_numbers = if state.mode.line_number() {
|
||||||
|
|
@ -760,13 +782,8 @@ impl Element for TextElement {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// build line numbers
|
// build line numbers
|
||||||
for (ix, line) in lines
|
for (ix, line) in last_layout.lines.iter().enumerate() {
|
||||||
.iter()
|
let ix = last_layout.visible_range.start + ix;
|
||||||
.skip(visible_range.start)
|
|
||||||
.take(visible_range.len())
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
let ix = ix + visible_range.start;
|
|
||||||
let line_no = ix + 1;
|
let line_no = ix + 1;
|
||||||
|
|
||||||
let mut line_no_text = format!("{:>4}", line_no);
|
let mut line_no_text = format!("{:>4}", line_no);
|
||||||
|
|
@ -793,13 +810,7 @@ impl Element for TextElement {
|
||||||
|
|
||||||
PrepaintState {
|
PrepaintState {
|
||||||
bounds,
|
bounds,
|
||||||
last_layout: LastLayout {
|
last_layout,
|
||||||
lines: Rc::new(lines),
|
|
||||||
line_height,
|
|
||||||
visible_range,
|
|
||||||
line_number_width,
|
|
||||||
wrap_width,
|
|
||||||
},
|
|
||||||
scroll_size,
|
scroll_size,
|
||||||
line_numbers,
|
line_numbers,
|
||||||
cursor_bounds,
|
cursor_bounds,
|
||||||
|
|
@ -859,10 +870,7 @@ impl Element for TextElement {
|
||||||
let line_height = window.line_height();
|
let line_height = window.line_height();
|
||||||
let origin = bounds.origin;
|
let origin = bounds.origin;
|
||||||
|
|
||||||
let mut invisible_top_padding = px(0.);
|
let invisible_top_padding = prepaint.last_layout.visible_top;
|
||||||
for line in prepaint.last_layout.lines.iter().take(visible_range.start) {
|
|
||||||
invisible_top_padding += line.size(line_height).height;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut mask_offset_y = px(0.);
|
let mut mask_offset_y = px(0.);
|
||||||
if self.state.read(cx).masked {
|
if self.state.read(cx).masked {
|
||||||
|
|
@ -910,12 +918,7 @@ impl Element for TextElement {
|
||||||
|
|
||||||
// Paint text
|
// Paint text
|
||||||
let mut offset_y = mask_offset_y + invisible_top_padding;
|
let mut offset_y = mask_offset_y + invisible_top_padding;
|
||||||
for line in prepaint
|
for line in prepaint.last_layout.lines.iter() {
|
||||||
.last_layout
|
|
||||||
.iter()
|
|
||||||
.skip(visible_range.start)
|
|
||||||
.take(visible_range.len())
|
|
||||||
{
|
|
||||||
let p = point(
|
let p = point(
|
||||||
origin.x + prepaint.last_layout.line_number_width,
|
origin.x + prepaint.last_layout.line_number_width,
|
||||||
origin.y + offset_y,
|
origin.y + offset_y,
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,26 @@ use rope::{Point, Rope};
|
||||||
|
|
||||||
/// An extension trait for `Rope` to provide additional utility methods.
|
/// An extension trait for `Rope` to provide additional utility methods.
|
||||||
pub trait RopeExt {
|
pub trait RopeExt {
|
||||||
/// Get the line at the given row index, including the `\r` at the end, but not `\n`.
|
/// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`.
|
||||||
///
|
///
|
||||||
/// Return empty rope if the row is out of bounds.
|
/// Return empty rope if the row (0-based) is out of bounds.
|
||||||
fn line(&self, row: usize) -> Rope;
|
fn line(&self, row: usize) -> Rope;
|
||||||
|
|
||||||
|
/// Start offset of the line at the given row (0-based) index.
|
||||||
|
fn line_start_offset(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
||||||
|
///
|
||||||
|
/// Return the end of the rope if the row is out of bounds.
|
||||||
|
fn line_end_offset(&self, row: usize) -> usize;
|
||||||
|
|
||||||
/// Return the number of lines in the rope.
|
/// Return the number of lines in the rope.
|
||||||
fn lines_len(&self) -> usize;
|
fn lines_len(&self) -> usize;
|
||||||
|
|
||||||
/// Return the lines iterator.
|
/// Return the lines iterator.
|
||||||
///
|
///
|
||||||
/// Each line is including the `\n` at the end, but not `\n`.
|
/// Each line is including the `\n` at the end, but not `\n`.
|
||||||
fn lines(&self) -> impl Iterator<Item = Rope>;
|
fn lines(&self) -> RopeLines;
|
||||||
|
|
||||||
/// Check is equal to another rope.
|
/// Check is equal to another rope.
|
||||||
fn eq(&self, other: &Rope) -> bool;
|
fn eq(&self, other: &Rope) -> bool;
|
||||||
|
|
@ -29,6 +37,55 @@ pub trait RopeExt {
|
||||||
fn char_at(&self, offset: usize) -> Option<char>;
|
fn char_at(&self, offset: usize) -> Option<char>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An iterator over the lines of a `Rope`.
|
||||||
|
pub struct RopeLines {
|
||||||
|
row: usize,
|
||||||
|
end_row: usize,
|
||||||
|
rope: Rope,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RopeLines {
|
||||||
|
/// Create a new `RopeLines` iterator.
|
||||||
|
pub fn new(rope: Rope) -> Self {
|
||||||
|
let end_row = rope.lines_len();
|
||||||
|
Self {
|
||||||
|
row: 0,
|
||||||
|
end_row,
|
||||||
|
rope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for RopeLines {
|
||||||
|
type Item = Rope;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.row >= self.end_row {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = self.rope.line(self.row);
|
||||||
|
self.row += 1;
|
||||||
|
Some(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||||
|
self.row = self.row.saturating_add(n);
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let len = self.end_row - self.row;
|
||||||
|
(len, Some(len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::iter::ExactSizeIterator for RopeLines {}
|
||||||
|
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 row = row as u32;
|
||||||
|
|
@ -37,12 +94,25 @@ impl RopeExt for Rope {
|
||||||
self.slice(start..end)
|
self.slice(start..end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn line_start_offset(&self, row: usize) -> usize {
|
||||||
|
let row = row as u32;
|
||||||
|
self.point_to_offset(Point::new(row, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_end_offset(&self, row: usize) -> usize {
|
||||||
|
if row > self.max_point().row as usize {
|
||||||
|
return self.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.line_start_offset(row) + self.line_len(row as u32) as usize
|
||||||
|
}
|
||||||
|
|
||||||
fn lines_len(&self) -> usize {
|
fn lines_len(&self) -> usize {
|
||||||
self.max_point().row as usize + 1
|
self.max_point().row as usize + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lines(&self) -> impl Iterator<Item = Rope> {
|
fn lines(&self) -> RopeLines {
|
||||||
(0..self.lines_len()).map(move |row| self.line(row))
|
RopeLines::new(self.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eq(&self, other: &Rope) -> bool {
|
fn eq(&self, other: &Rope) -> bool {
|
||||||
|
|
@ -108,6 +178,25 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line_start_end_offset() {
|
||||||
|
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
assert_eq!(rope.line_start_offset(0), 0);
|
||||||
|
assert_eq!(rope.line_end_offset(0), 5);
|
||||||
|
|
||||||
|
assert_eq!(rope.line_start_offset(1), 6);
|
||||||
|
assert_eq!(rope.line_end_offset(1), 12);
|
||||||
|
|
||||||
|
assert_eq!(rope.line_start_offset(2), 13);
|
||||||
|
assert_eq!(rope.line_end_offset(2), 34);
|
||||||
|
|
||||||
|
assert_eq!(rope.line_start_offset(3), 35);
|
||||||
|
assert_eq!(rope.line_end_offset(3), 39);
|
||||||
|
|
||||||
|
assert_eq!(rope.line_start_offset(4), 39);
|
||||||
|
assert_eq!(rope.line_end_offset(4), 39);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_chars_count() {
|
fn test_chars_count() {
|
||||||
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope");
|
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope");
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use rope::Rope;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::ops::{Deref, Range};
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use unicode_segmentation::*;
|
use unicode_segmentation::*;
|
||||||
|
|
@ -31,6 +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::{Cursor, LineColumn, RopeExt as _, Selection};
|
use crate::input::{Cursor, LineColumn, RopeExt as _, Selection};
|
||||||
use crate::{history::History, scroll::ScrollbarState, Root};
|
use crate::{history::History, scroll::ScrollbarState, Root};
|
||||||
|
|
||||||
|
|
@ -215,26 +216,22 @@ pub fn init(cx: &mut App) {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(super) struct LastLayout {
|
pub(super) struct LastLayout {
|
||||||
/// The last layout lines.
|
/// The visible range (no wrap) of lines in the viewport, the value is row (0-based) index.
|
||||||
|
pub(super) visible_range: Range<usize>,
|
||||||
|
/// The first visible line top position in scroll viewport.
|
||||||
|
pub(super) visible_top: Pixels,
|
||||||
|
/// The start byte offset of the first visible line.
|
||||||
|
pub(super) visible_start_offset: usize,
|
||||||
|
/// The last layout lines (Only have visible lines).
|
||||||
pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>,
|
pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>,
|
||||||
/// The line_height of text layout, this will change will InputElement painted.
|
/// The line_height of text layout, this will change will InputElement painted.
|
||||||
pub(super) line_height: Pixels,
|
pub(super) line_height: Pixels,
|
||||||
/// The visible range (no wrap) of lines in the viewport.
|
|
||||||
pub(super) visible_range: Range<usize>,
|
|
||||||
/// The wrap width of text layout, this will change will InputElement painted.
|
/// The wrap width of text layout, this will change will InputElement painted.
|
||||||
pub(super) wrap_width: Option<Pixels>,
|
pub(super) wrap_width: Option<Pixels>,
|
||||||
/// The line number area width of text layout, if not line number, this will be 0px.
|
/// The line number area width of text layout, if not line number, this will be 0px.
|
||||||
pub(super) line_number_width: Pixels,
|
pub(super) line_number_width: Pixels,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for LastLayout {
|
|
||||||
type Target = Rc<SmallVec<[WrappedLine; 1]>>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.lines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// InputState to keep editing state of the [`super::TextInput`].
|
/// InputState to keep editing state of the [`super::TextInput`].
|
||||||
pub struct InputState {
|
pub struct InputState {
|
||||||
pub(super) focus_handle: FocusHandle,
|
pub(super) focus_handle: FocusHandle,
|
||||||
|
|
@ -536,8 +533,8 @@ impl InputState {
|
||||||
};
|
};
|
||||||
let line_height = last_layout.line_height;
|
let line_height = last_layout.line_height;
|
||||||
|
|
||||||
let mut prev_lines_offset = 0;
|
let mut prev_lines_offset = last_layout.visible_start_offset;
|
||||||
let mut y_offset = px(0.);
|
let mut y_offset = last_layout.visible_top;
|
||||||
for (line_index, line) in last_layout.lines.iter().enumerate() {
|
for (line_index, line) in last_layout.lines.iter().enumerate() {
|
||||||
let local_offset = offset.saturating_sub(prev_lines_offset);
|
let local_offset = offset.saturating_sub(prev_lines_offset);
|
||||||
if let Some(pos) = line.position_for_index(local_offset, line_height) {
|
if let Some(pos) = line.position_for_index(local_offset, line_height) {
|
||||||
|
|
@ -682,6 +679,17 @@ impl InputState {
|
||||||
/// Update the soft wrap mode for multi-line input, default is true.
|
/// Update the soft wrap mode for multi-line input, default is true.
|
||||||
pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.soft_wrap = wrap;
|
self.soft_wrap = wrap;
|
||||||
|
if wrap {
|
||||||
|
let wrap_width = self
|
||||||
|
.last_layout
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|b| b.wrap_width)
|
||||||
|
.unwrap_or(self.input_bounds.size.width);
|
||||||
|
|
||||||
|
self.text_wrapper.set_wrap_width(Some(wrap_width), cx);
|
||||||
|
} else {
|
||||||
|
self.text_wrapper.set_wrap_width(None, cx);
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1715,40 +1723,55 @@ impl InputState {
|
||||||
// - included the scroll offset.
|
// - included the scroll offset.
|
||||||
let inner_position = position - bounds.origin - point(line_number_width, px(0.));
|
let inner_position = position - bounds.origin - point(line_number_width, px(0.));
|
||||||
|
|
||||||
let mut index = 0;
|
let mut index = last_layout.visible_start_offset;
|
||||||
let mut y_offset = px(0.);
|
let mut y_offset = last_layout.visible_top;
|
||||||
|
for (ix, line) in self
|
||||||
for (_, line) in last_layout.lines.iter().enumerate() {
|
.text_wrapper
|
||||||
let line_origin = self.line_origin_with_y_offset(&mut y_offset, &line, line_height);
|
.lines
|
||||||
|
.iter()
|
||||||
|
.skip(last_layout.visible_range.start)
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let line_origin = self.line_origin_with_y_offset(&mut y_offset, line, line_height);
|
||||||
let pos = inner_position - line_origin;
|
let pos = inner_position - line_origin;
|
||||||
|
|
||||||
|
let Some(rendered_line) = last_layout.lines.get(ix) else {
|
||||||
|
if pos.y < line_origin.y + line_height {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
// Return offset by use closest_index_for_x if is single line mode.
|
// Return offset by use closest_index_for_x if is single line mode.
|
||||||
if self.mode.is_single_line() {
|
if self.mode.is_single_line() {
|
||||||
return line.unwrapped_layout.closest_index_for_x(pos.x);
|
return rendered_line.unwrapped_layout.closest_index_for_x(pos.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
let index_result = line.closest_index_for_position(pos, line_height);
|
let index_result = rendered_line.closest_index_for_position(pos, line_height);
|
||||||
if let Ok(v) = index_result {
|
if let Ok(v) = index_result {
|
||||||
index += v;
|
index += v;
|
||||||
break;
|
break;
|
||||||
} else if let Ok(_) = line.index_for_position(point(px(0.), pos.y), line_height) {
|
} else if let Ok(_) =
|
||||||
|
rendered_line.index_for_position(point(px(0.), pos.y), line_height)
|
||||||
|
{
|
||||||
// Click in the this line but not in the text, move cursor to the end of the line.
|
// Click in the this line but not in the text, move cursor to the end of the line.
|
||||||
// The fallback index is saved in Err from `index_for_position` method.
|
// The fallback index is saved in Err from `index_for_position` method.
|
||||||
index += index_result.unwrap_err();
|
index += index_result.unwrap_err();
|
||||||
break;
|
break;
|
||||||
} else if line.text.trim_end_matches(|c| c == '\r').len() == 0 {
|
} else if rendered_line.text.trim_end_matches(|c| c == '\r').len() == 0 {
|
||||||
// empty line on Windows is `\r`, other is ''
|
// empty line on Windows is `\r`, other is ''
|
||||||
let line_bounds = Bounds {
|
let line_bounds = Bounds {
|
||||||
origin: line_origin,
|
origin: line_origin,
|
||||||
size: gpui::size(bounds.size.width, line_height),
|
size: gpui::size(bounds.size.width, line_height),
|
||||||
};
|
};
|
||||||
let pos = inner_position;
|
let pos = inner_position;
|
||||||
index += line.len();
|
index += rendered_line.len();
|
||||||
if line_bounds.contains(&pos) {
|
if line_bounds.contains(&pos) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
index += line.len();
|
index += rendered_line.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
// +1 for revert `lines` split `\n`
|
// +1 for revert `lines` split `\n`
|
||||||
|
|
@ -1766,7 +1789,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: &WrappedLine,
|
line: &LineWrap,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
) -> Point<Pixels> {
|
) -> Point<Pixels> {
|
||||||
// NOTE: About line.wrap_boundaries.len()
|
// NOTE: About line.wrap_boundaries.len()
|
||||||
|
|
@ -1775,7 +1798,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_boundaries.len() as f32 * line_height;
|
let height = line_height + line.wrap_lines as f32 * line_height;
|
||||||
*y_offset = *y_offset + height;
|
*y_offset = *y_offset + height;
|
||||||
p
|
p
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2233,8 +2256,8 @@ impl EntityInputHandler for InputState {
|
||||||
let mut start_origin = None;
|
let mut start_origin = None;
|
||||||
let mut end_origin = None;
|
let mut end_origin = None;
|
||||||
let line_number_origin = point(line_number_width, px(0.));
|
let line_number_origin = point(line_number_width, px(0.));
|
||||||
let mut y_offset = px(0.);
|
let mut y_offset = last_layout.visible_top;
|
||||||
let mut index_offset = 0;
|
let mut index_offset = last_layout.visible_start_offset;
|
||||||
|
|
||||||
for line in last_layout.lines.iter() {
|
for line in last_layout.lines.iter() {
|
||||||
if start_origin.is_some() && end_origin.is_some() {
|
if start_origin.is_some() && end_origin.is_some() {
|
||||||
|
|
@ -2282,10 +2305,11 @@ impl EntityInputHandler for InputState {
|
||||||
let last_layout = self.last_layout.as_ref()?;
|
let last_layout = self.last_layout.as_ref()?;
|
||||||
let line_height = last_layout.line_height;
|
let line_height = last_layout.line_height;
|
||||||
let line_point = self.last_bounds?.localize(&point)?;
|
let line_point = self.last_bounds?.localize(&point)?;
|
||||||
|
let offset = last_layout.visible_start_offset;
|
||||||
|
|
||||||
for line in last_layout.lines.iter() {
|
for line in last_layout.lines.iter() {
|
||||||
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
||||||
return Some(self.offset_to_utf16(utf8_index));
|
return Some(self.offset_to_utf16(offset + utf8_index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ pub(super) struct LineWrap {
|
||||||
/// After use lines to calculate the scroll size of the TextArea
|
/// After use lines to calculate the scroll size of the TextArea
|
||||||
pub(super) struct TextWrapper {
|
pub(super) struct TextWrapper {
|
||||||
pub(super) text: Rope,
|
pub(super) text: Rope,
|
||||||
/// The wrapped lines, value is start and end index of the line.
|
/// The wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||||
pub(super) wrapped_lines: Vec<Range<usize>>,
|
pub(super) wrapped_lines: Vec<Range<usize>>,
|
||||||
/// The lines by split \n
|
/// The lines by split \n
|
||||||
pub(super) lines: Vec<LineWrap>,
|
pub(super) lines: Vec<LineWrap>,
|
||||||
|
|
@ -45,6 +45,11 @@ impl TextWrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the total number of lines including wrapped lines.
|
||||||
|
pub(super) fn len(&self) -> usize {
|
||||||
|
self.wrapped_lines.len()
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
if wrap_width == self.wrap_width {
|
if wrap_width == self.wrap_width {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue