diff --git a/.gitignore b/.gitignore index 63aec199..26ca8559 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target .DS_Store /docks.json +/profile.json .vscode index.scip -*.log \ No newline at end of file +*.log diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index b39259bc..455b626e 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -6,6 +6,7 @@ use gpui::{ Path, Pixels, Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine, }; +use rope::Rope; use smallvec::SmallVec; use crate::{ @@ -60,13 +61,18 @@ impl TextElement { /// - current line index fn layout_cursor( &self, - lines: &[WrappedLine], - line_height: Pixels, + last_layout: &LastLayout, bounds: &mut Bounds, - line_number_width: Pixels, window: &mut Window, cx: &mut App, ) -> (Option>, Point, Option) { + 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 mut selected_range = state.selected_range; if let Some(marked_range) = &state.marked_range { @@ -89,9 +95,11 @@ impl TextElement { let mut cursor_start = None; let mut cursor_end = None; - let mut prev_lines_offset = 0; - let mut offset_y = px(0.); + let mut prev_lines_offset = visible_start_offset; + let mut offset_y = visible_top; for (line_ix, line) in lines.iter().enumerate() { + let line_ix = visible_range.start + line_ix; + // break loop if all cursor positions are found if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() { break; @@ -124,6 +132,14 @@ impl TextElement { 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)) = (cursor_pos, cursor_start, cursor_end) { @@ -195,13 +211,17 @@ impl TextElement { fn layout_selections( &self, - lines: &[WrappedLine], - line_height: Pixels, + last_layout: &LastLayout, bounds: &mut Bounds, - line_number_width: Pixels, _: &mut Window, cx: &mut App, ) -> Option> { + 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 mut selected_range = state.selected_range; if let Some(marked_range) = &state.marked_range { @@ -219,10 +239,10 @@ impl TextElement { (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 offset_y = px(0.); for line in lines.iter() { let line_size = line.size(line_height); let line_wrap_width = line_size.width; @@ -330,48 +350,54 @@ impl TextElement { /// 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( &self, state: &InputState, line_height: Pixels, input_height: Pixels, - ) -> Range { + ) -> (Range, 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() { - return 0..1; + return (0..1, visible_top); } - let Some(last_layout) = state.last_layout.as_ref() else { - return 0..1; - }; - + let total_lines = state.text_wrapper.len(); let scroll_top = state.scroll_handle.offset().y; - let total_lines = last_layout.lines.len(); let mut visible_range = 0..total_lines; let mut line_bottom = px(0.); - for (ix, line) in last_layout.lines.iter().enumerate() { - line_bottom += (line.wrap_boundaries.len() + 1) * line_height; + for (ix, line) in state.text_wrapper.lines.iter().enumerate() { + let wrapped_height = (line.wrap_lines + 1) * line_height; + line_bottom += wrapped_height; if line_bottom < -scroll_top { + visible_top = line_bottom - wrapped_height; visible_range.start = ix; } 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; } } - visible_range + (visible_range, visible_top) } /// First usize is the offset of skipped. fn highlight_lines( &mut self, visible_range: &Range, + _visible_top: Pixels, + visible_start_offset: usize, cx: &mut App, - ) -> Option<(usize, Vec<(Range, HighlightStyle)>)> { + ) -> Option, HighlightStyle)>> { let theme = cx.theme().highlight_theme.clone(); self.state.update(cx, |state, cx| match &state.mode { InputMode::CodeEditor { @@ -389,22 +415,17 @@ impl TextElement { return None; }; - let mut offset = 0; - let mut skipped_offset = 0; + let mut offset = visible_start_offset; 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` 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 line_styles = highlighter.styles(&range, &theme); styles = gpui::combine_highlights(styles, line_styles).collect(); @@ -412,26 +433,24 @@ impl TextElement { offset = range.end; } - let mut marker_styles = vec![]; - for marker in markers.iter() { - if let Some(range) = &marker.range { - if range.start < skipped_offset { - continue; - } + // Combine marker styles + if !markers.is_empty() { + let mut marker_styles = vec![]; + for marker in markers.iter() { + 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 - .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((skipped_offset, styles)) + Some(styles) } _ => None, }) @@ -543,8 +562,12 @@ impl Element for TextElement { let state = self.state.read(cx); let line_height = window.line_height(); - let visible_range = self.calculate_visible_range(&state, line_height, bounds.size.height); - let highlight_styles = self.highlight_lines(&visible_range, cx); + let (visible_range, visible_top) = + 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 multi_line = state.mode.is_multi_line(); @@ -556,11 +579,17 @@ impl Element for TextElement { let mut bounds = bounds; 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 { - ("*".repeat(text.chars_count()).into(), cx.theme().foreground) + ( + Rope::from("*".repeat(text.chars_count())), + cx.theme().foreground, + ) } else { - (text.to_string().into(), cx.theme().foreground) + (text.clone(), cx.theme().foreground) }; let text_style = window.text_style(); @@ -607,14 +636,8 @@ impl Element for TextElement { }; 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![]; - if skipped_offset > 0 { - runs.push(TextRun { - len: skipped_offset, - ..run.clone() - }); - } runs.extend(highlight_styles.iter().map(|(range, style)| { let mut run = text_style.clone().highlight(*style).to_run(range.len()); @@ -663,21 +686,23 @@ impl Element for TextElement { None }; - // NOTE: If there have about 10K lines, this will take about 5~6ms. - // let measure = Measure::new("shape_text"); + // NOTE: Here 50 lines about 150µs + // 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 .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"); // measure.end(); 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() { // FIXME: The `shape_text` measured width is not stable, sometime will large, sometime small. 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( @@ -689,6 +714,16 @@ impl Element for TextElement { (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 // // #### text @@ -720,23 +755,10 @@ impl Element for TextElement { // Calculate the scroll offset to keep the cursor in view - let (cursor_bounds, cursor_scroll_offset, current_line_index) = self.layout_cursor( - &lines, - line_height, - &mut bounds, - line_number_width, - window, - cx, - ); + let (cursor_bounds, cursor_scroll_offset, current_line_index) = + self.layout_cursor(&last_layout, &mut bounds, window, cx); - let selection_path = self.layout_selections( - &lines, - line_height, - &mut bounds, - line_number_width, - window, - cx, - ); + let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx); let state = self.state.read(cx); let line_numbers = if state.mode.line_number() { @@ -760,13 +782,8 @@ impl Element for TextElement { }]; // build line numbers - for (ix, line) in lines - .iter() - .skip(visible_range.start) - .take(visible_range.len()) - .enumerate() - { - let ix = ix + visible_range.start; + for (ix, line) in last_layout.lines.iter().enumerate() { + let ix = last_layout.visible_range.start + ix; let line_no = ix + 1; let mut line_no_text = format!("{:>4}", line_no); @@ -793,13 +810,7 @@ impl Element for TextElement { PrepaintState { bounds, - last_layout: LastLayout { - lines: Rc::new(lines), - line_height, - visible_range, - line_number_width, - wrap_width, - }, + last_layout, scroll_size, line_numbers, cursor_bounds, @@ -859,10 +870,7 @@ impl Element for TextElement { let line_height = window.line_height(); let origin = bounds.origin; - let mut invisible_top_padding = px(0.); - for line in prepaint.last_layout.lines.iter().take(visible_range.start) { - invisible_top_padding += line.size(line_height).height; - } + let invisible_top_padding = prepaint.last_layout.visible_top; let mut mask_offset_y = px(0.); if self.state.read(cx).masked { @@ -910,12 +918,7 @@ impl Element for TextElement { // Paint text let mut offset_y = mask_offset_y + invisible_top_padding; - for line in prepaint - .last_layout - .iter() - .skip(visible_range.start) - .take(visible_range.len()) - { + for line in prepaint.last_layout.lines.iter() { let p = point( origin.x + prepaint.last_layout.line_number_width, origin.y + offset_y, diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs index dc140d13..7ae9a819 100644 --- a/crates/ui/src/input/rope_ext.rs +++ b/crates/ui/src/input/rope_ext.rs @@ -2,18 +2,26 @@ use rope::{Point, Rope}; /// An extension trait for `Rope` to provide additional utility methods. 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; + /// 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. fn lines_len(&self) -> usize; /// Return the lines iterator. /// /// Each line is including the `\n` at the end, but not `\n`. - fn lines(&self) -> impl Iterator; + fn lines(&self) -> RopeLines; /// Check is equal to another rope. fn eq(&self, other: &Rope) -> bool; @@ -29,6 +37,55 @@ pub trait RopeExt { fn char_at(&self, offset: usize) -> Option; } +/// 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 { + 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.row = self.row.saturating_add(n); + self.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + 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 { fn line(&self, row: usize) -> Rope { let row = row as u32; @@ -37,12 +94,25 @@ impl RopeExt for Rope { 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 { self.max_point().row as usize + 1 } - fn lines(&self) -> impl Iterator { - (0..self.lines_len()).map(move |row| self.line(row)) + fn lines(&self) -> RopeLines { + RopeLines::new(self.clone()) } 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] fn test_chars_count() { let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope"); diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index c3ba519f..1adbfe75 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -7,7 +7,7 @@ use rope::Rope; use serde::Deserialize; use smallvec::SmallVec; use std::cell::RefCell; -use std::ops::{Deref, Range}; +use std::ops::Range; use std::rc::Rc; use sum_tree::Bias; use unicode_segmentation::*; @@ -31,6 +31,7 @@ use super::{ }; use crate::input::hover_popover::DiagnosticPopover; use crate::input::marker::Marker; +use crate::input::text_wrapper::LineWrap; use crate::input::{Cursor, LineColumn, RopeExt as _, Selection}; use crate::{history::History, scroll::ScrollbarState, Root}; @@ -215,26 +216,22 @@ pub fn init(cx: &mut App) { #[derive(Clone)] 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, + /// 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>, /// The line_height of text layout, this will change will InputElement painted. pub(super) line_height: Pixels, - /// The visible range (no wrap) of lines in the viewport. - pub(super) visible_range: Range, /// The wrap width of text layout, this will change will InputElement painted. pub(super) wrap_width: Option, /// The line number area width of text layout, if not line number, this will be 0px. pub(super) line_number_width: Pixels, } -impl Deref for LastLayout { - type Target = Rc>; - - fn deref(&self) -> &Self::Target { - &self.lines - } -} - /// InputState to keep editing state of the [`super::TextInput`]. pub struct InputState { pub(super) focus_handle: FocusHandle, @@ -536,8 +533,8 @@ impl InputState { }; let line_height = last_layout.line_height; - let mut prev_lines_offset = 0; - let mut y_offset = px(0.); + let mut prev_lines_offset = last_layout.visible_start_offset; + let mut y_offset = last_layout.visible_top; for (line_index, line) in last_layout.lines.iter().enumerate() { let local_offset = offset.saturating_sub(prev_lines_offset); 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. pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context) { 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(); } @@ -1715,40 +1723,55 @@ impl InputState { // - included the scroll offset. let inner_position = position - bounds.origin - point(line_number_width, px(0.)); - let mut index = 0; - let mut y_offset = px(0.); - - for (_, line) in last_layout.lines.iter().enumerate() { - let line_origin = self.line_origin_with_y_offset(&mut y_offset, &line, line_height); + let mut index = last_layout.visible_start_offset; + let mut y_offset = last_layout.visible_top; + for (ix, line) in self + .text_wrapper + .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 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. 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 { index += v; 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. // The fallback index is saved in Err from `index_for_position` method. index += index_result.unwrap_err(); 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 '' let line_bounds = Bounds { origin: line_origin, size: gpui::size(bounds.size.width, line_height), }; let pos = inner_position; - index += line.len(); + index += rendered_line.len(); if line_bounds.contains(&pos) { break; } } else { - index += line.len(); + index += rendered_line.len(); } // +1 for revert `lines` split `\n` @@ -1766,7 +1789,7 @@ impl InputState { fn line_origin_with_y_offset( &self, y_offset: &mut Pixels, - line: &WrappedLine, + line: &LineWrap, line_height: Pixels, ) -> Point { // NOTE: About line.wrap_boundaries.len() @@ -1775,7 +1798,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_boundaries.len() as f32 * line_height; + let height = line_height + line.wrap_lines as f32 * line_height; *y_offset = *y_offset + height; p } else { @@ -2233,8 +2256,8 @@ impl EntityInputHandler for InputState { let mut start_origin = None; let mut end_origin = None; let line_number_origin = point(line_number_width, px(0.)); - let mut y_offset = px(0.); - let mut index_offset = 0; + let mut y_offset = last_layout.visible_top; + let mut index_offset = last_layout.visible_start_offset; for line in last_layout.lines.iter() { 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 line_height = last_layout.line_height; let line_point = self.last_bounds?.localize(&point)?; + let offset = last_layout.visible_start_offset; for line in last_layout.lines.iter() { 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)); } } diff --git a/crates/ui/src/input/text_wrapper.rs b/crates/ui/src/input/text_wrapper.rs index 823b4627..ce5a47c3 100644 --- a/crates/ui/src/input/text_wrapper.rs +++ b/crates/ui/src/input/text_wrapper.rs @@ -22,7 +22,7 @@ pub(super) struct LineWrap { /// After use lines to calculate the scroll size of the TextArea pub(super) struct TextWrapper { 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>, /// The lines by split \n pub(super) lines: Vec, @@ -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, cx: &mut App) { if wrap_width == self.wrap_width { return;