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:
Jason Lee 2025-09-09 14:41:52 +08:00 committed by GitHub
parent 84b2d2a6f5
commit 03c2327cdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 266 additions and 144 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
/target /target
.DS_Store .DS_Store
/docks.json /docks.json
/profile.json
.vscode .vscode
index.scip index.scip
*.log *.log

View file

@ -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,

View file

@ -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");

View file

@ -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));
} }
} }

View file

@ -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;