diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 4f3e9233..333c1d80 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -3,14 +3,18 @@ use std::{ops::Range, rc::Rc}; use gpui::{ fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler, Entity, GlobalElementId, Half, HighlightStyle, Hitbox, IntoElement, LayoutId, MouseButton, - MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun, - TextStyle, UnderlineStyle, Window, + MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextRun, TextStyle, + UnderlineStyle, Window, }; use ropey::Rope; use smallvec::SmallVec; use crate::{ - input::{blink_cursor::CURSOR_WIDTH, RopeExt as _}, + input::{ + blink_cursor::CURSOR_WIDTH, + text_wrapper::{LineLayout, TextWrapper}, + RopeExt as _, + }, ActiveTheme as _, Colorize, Root, }; @@ -522,6 +526,53 @@ impl TextElement { (line_number_width, line_number_len) } + fn layout_lines( + text: &Rope, + text_wrapper: &TextWrapper, + visible_range: &Range, + font_size: Pixels, + runs: &[TextRun], + window: &mut Window, + ) -> Vec { + let visible_text = text + .slice_lines(visible_range.start..visible_range.end) + .to_string(); + + let mut lines = vec![]; + let mut offset = 0; + for (ix, line) in visible_text.split("\n").enumerate() { + let line_item = text_wrapper + .lines + .get(visible_range.start + ix) + .expect("line should exists in text_wrapper"); + // if line_item.len() != line.len() { + // dbg!(&line, &line_item.wrapped_lines); + // } + debug_assert_eq!(line_item.len(), line.len()); + + let mut line_layout = LineLayout::new(); + let mut wrapped_lines = SmallVec::with_capacity(1); + + for range in &line_item.wrapped_lines { + let line_runs = runs_for_range(runs, offset, &range); + let sub_line: SharedString = line[range.clone()].to_string().into(); + let shaped_line = window + .text_system() + .shape_line(sub_line, font_size, &line_runs, None); + + wrapped_lines.push(shaped_line); + } + + line_layout.set_wrapped_lines(wrapped_lines); + lines.push(line_layout); + + // +1 for the `\n` + offset += line.len() + 1; + } + + lines + } + /// First usize is the offset of skipped. fn highlight_lines( &mut self, @@ -801,17 +852,14 @@ impl Element for TextElement { None }; - // NOTE: Here 50 lines about 150µs - // let measure = crate::Measure::new("shape_text"); - let visible_text = display_text - .slice_lines(visible_range.start..visible_range.end) - .to_string(); - - let lines = window - .text_system() - .shape_text(visible_text.into(), font_size, &runs, wrap_width, None) - .expect("failed to shape text"); - // measure.end(); + let lines = Self::layout_lines( + &text, + &state.text_wrapper, + &visible_range, + font_size, + &runs, + window, + ); let mut longest_line_width = wrap_width.unwrap_or(px(0.)); if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 { @@ -938,7 +986,7 @@ impl Element for TextElement { .text_system() .shape_line(line_no, font_size, &runs, None), ); - for _ in 0..line.wrap_boundaries.len() { + for _ in 0..line.wrapped_lines.len().saturating_sub(1) { sub_lines.push(ShapedLine::default()); } line_numbers.push(sub_lines); @@ -1082,7 +1130,7 @@ impl Element for TextElement { origin.x + prepaint.last_layout.line_number_width, origin.y + offset_y, ); - _ = line.paint(p, line_height, TextAlign::Left, None, window, cx); + _ = line.paint(p, line_height, window, cx); offset_y += line.size(line_height).height; } @@ -1162,3 +1210,105 @@ impl Element for TextElement { self.paint_mouse_listeners(window, cx); } } + +/// Get the runs for the given range. +/// +/// The range is the byte range of the wrapped line. +pub(super) fn runs_for_range( + runs: &[TextRun], + line_offset: usize, + range: &Range, +) -> Vec { + let mut result = vec![]; + let range = (line_offset + range.start)..(line_offset + range.end); + let mut cursor = 0; + + for run in runs { + let run_start = cursor; + let run_end = cursor + run.len; + + if run_end <= range.start { + cursor = run_end; + continue; + } + + if run_start >= range.end { + break; + } + + let start = range.start.max(run_start) - run_start; + let end = range.end.min(run_end) - run_start; + let len = end - start; + + if len > 0 { + result.push(TextRun { len, ..run.clone() }); + } + + cursor = run_end; + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_runs_for_range() { + let run = TextRun { + len: 0, + font: gpui::font(".SystemUIFont"), + color: gpui::black(), + background_color: None, + underline: None, + strikethrough: None, + }; + + // use hello this-is-test + let runs = vec![ + // use + TextRun { + len: 3, + ..run.clone() + }, + // \s + TextRun { + len: 1, + ..run.clone() + }, + // hello + TextRun { + len: 5, + ..run.clone() + }, + // \s + TextRun { + len: 1, + ..run.clone() + }, + // this-is-test + TextRun { + len: 12, + ..run.clone() + }, + ]; + + #[track_caller] + fn assert_runs(actual: Vec, expected: &[usize]) { + let left = actual.iter().map(|run| run.len).collect::>(); + assert_eq!(left, expected); + } + + assert_runs(runs_for_range(&runs, 0, &(0..0)), &[]); + assert_runs(runs_for_range(&runs, 0, &(0..100)), &[3, 1, 5, 1, 12]); + + assert_runs(runs_for_range(&runs, 0, &(0..6)), &[3, 1, 2]); + assert_runs(runs_for_range(&runs, 0, &(1..6)), &[2, 1, 2]); + assert_runs(runs_for_range(&runs, 0, &(3..10)), &[1, 5, 1]); + assert_runs(runs_for_range(&runs, 0, &(5..8)), &[3]); + assert_runs(runs_for_range(&runs, 3, &(0..3)), &[1, 2]); + assert_runs(runs_for_range(&runs, 3, &(2..10)), &[4, 1, 3]); + assert_runs(runs_for_range(&runs, 9, &(0..8)), &[1, 7]); + } +} diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index fbc5dcfb..0366f0f3 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -9,11 +9,9 @@ use gpui::{ InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window, - WrappedLine, }; use ropey::{Rope, RopeSlice}; use serde::Deserialize; -use smallvec::SmallVec; use std::cell::RefCell; use std::ops::Range; use std::rc::Rc; @@ -33,6 +31,7 @@ use crate::input::{ element::RIGHT_MARGIN, popovers::{ContextMenu, DiagnosticPopover, HoverPopover, MouseContextMenu}, search::{self, SearchPanel}, + text_wrapper::LineLayout, HoverDefinition, Lsp, Position, }; use crate::input::{RopeExt as _, Selection}; @@ -239,7 +238,7 @@ pub(super) struct LastLayout { /// The range of byte offset of the visible lines. pub(super) visible_range_offset: Range, /// The last layout lines (Only have visible lines). - pub(super) lines: Rc>, + pub(super) lines: Rc>, /// The line_height of text layout, this will change will InputElement painted. pub(super) line_height: Pixels, /// The wrap width of text layout, this will change will InputElement painted. @@ -659,7 +658,7 @@ impl InputState { if new_row >= last_layout.visible_range.start { let visible_row = new_row.saturating_sub(last_layout.visible_range.start); if let Some(line) = last_layout.lines.get(visible_row) { - if let Ok(x) = line.closest_index_for_position( + if let Some(x) = line.closest_index_for_position( Point { x: preferred_x, y: px(0.), @@ -1544,7 +1543,7 @@ impl InputState { } self.selecting = true; - let offset = self.index_for_mouse_position(event.position, window, cx); + let offset = self.index_for_mouse_position(event.position); if self.handle_click_hover_definition(event, offset, window, cx) { return; @@ -1586,7 +1585,7 @@ impl InputState { cx: &mut Context, ) { // Show diagnostic popover on mouse move - let offset = self.index_for_mouse_position(event.position, window, cx); + let offset = self.index_for_mouse_position(event.position); self.handle_mouse_move(offset, event, window, cx); if self.mode.is_code_editor() { @@ -1814,12 +1813,7 @@ impl InputState { } } - pub(crate) fn index_for_mouse_position( - &self, - position: Point, - _window: &Window, - _cx: &App, - ) -> usize { + pub(crate) fn index_for_mouse_position(&self, position: Point) -> usize { // If the text is empty, always return 0 if self.text.len() == 0 { return 0; @@ -1858,7 +1852,7 @@ impl InputState { 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 { + let Some(line_layout) = last_layout.lines.get(ix) else { if pos.y < line_origin.y + line_height { break; } @@ -1868,37 +1862,18 @@ impl InputState { // Return offset by use closest_index_for_x if is single line mode. if self.mode.is_single_line() { - return rendered_line.unwrapped_layout.closest_index_for_x(pos.x); + return line_layout.closest_index_for_x(pos.x); } - let index_result = rendered_line.closest_index_for_position(pos, line_height); - if let Ok(v) = index_result { + if let Some(v) = line_layout.closest_index_for_position(pos, line_height) { index += v; break; - } 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(); + } else if pos.y < px(0.) { break; - } 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 += rendered_line.len(); - if line_bounds.contains(&pos) { - break; - } - } else { - index += rendered_line.len(); } - // +1 for revert `lines` split `\n` - index += 1; + // +1 for `\n` + index += line_layout.len() + 1; } if index > self.text.len() { @@ -2126,7 +2101,7 @@ impl InputState { return; } - let offset = self.index_for_mouse_position(event.position, window, cx); + let offset = self.index_for_mouse_position(event.position); self.select_to(offset, cx); } @@ -2504,7 +2479,7 @@ impl EntityInputHandler for InputState { let offset = last_layout.visible_range_offset.start; for line in last_layout.lines.iter() { - if let Ok(utf8_index) = line.index_for_position(line_point, line_height) { + if let Some(utf8_index) = line.index_for_position(line_point, line_height) { 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 855139d1..49686874 100644 --- a/crates/ui/src/input/text_wrapper.rs +++ b/crates/ui/src/input/text_wrapper.rs @@ -1,12 +1,13 @@ use std::ops::Range; -use gpui::{App, Font, LineFragment, Pixels}; +use gpui::{point, px, size, App, Font, LineFragment, Pixels, Point, ShapedLine, Size, Window}; use ropey::Rope; +use smallvec::SmallVec; use crate::input::RopeExt; /// A line with soft wrapped lines info. -#[derive(Clone)] +#[derive(Debug, Clone)] pub(super) struct LineItem { /// The original line text. line: Rope, @@ -15,7 +16,7 @@ pub(super) struct LineItem { /// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different /// like the `window.text_system().shape_text`. So, this value may not equal /// the actual rendered lines. - wrapped_lines: Vec>, + pub(super) wrapped_lines: Vec>, } impl LineItem { @@ -176,9 +177,9 @@ impl TextWrapper { let new_range = new_start_offset..new_end_offset; let mut new_lines = vec![]; - let wrap_width = self.wrap_width; + // line not contains `\n`. for (ix, line) in Rope::from(changed_text.slice(new_range)) .iter_lines() .enumerate() @@ -237,6 +238,145 @@ impl TextWrapper { } } +pub(crate) struct LineLayout { + /// Total bytes length of this line. + len: usize, + /// The soft wrapped lines of this line (Include the first line). + pub(crate) wrapped_lines: SmallVec<[ShapedLine; 1]>, + pub(crate) longest_width: Pixels, +} + +impl LineLayout { + pub(crate) fn new() -> Self { + Self { + len: 0, + longest_width: px(0.), + wrapped_lines: SmallVec::new(), + } + } + + pub(crate) fn set_wrapped_lines(&mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) { + self.len = wrapped_lines.iter().map(|l| l.len).sum(); + let width = wrapped_lines + .iter() + .map(|l| l.width) + .max() + .unwrap_or_default(); + self.longest_width = width; + self.wrapped_lines = wrapped_lines; + } + + #[inline] + pub(super) fn len(&self) -> usize { + self.len + } + + /// Get the position (x, y) for the given index in this line layout. + /// + /// - The `offset` is a local byte index in this line layout. + /// - The return value is relative to the top-left corner of this line layout, start from (0, 0) + pub(crate) fn position_for_index( + &self, + offset: usize, + line_height: Pixels, + ) -> Option> { + let mut acc_len = 0; + let mut offset_y = px(0.); + + for line in self.wrapped_lines.iter() { + let range = acc_len..=(acc_len + line.len()); + if range.contains(&offset) { + let x = line.x_for_index(offset.saturating_sub(acc_len)); + return Some(point(x, offset_y)); + } + acc_len += line.text.len(); + offset_y += line_height; + } + + None + } + + pub(super) fn closest_index_for_x(&self, x: Pixels) -> usize { + let mut acc_len = 0; + for line in self.wrapped_lines.iter() { + if x <= line.width { + let ix = line.closest_index_for_x(x); + return acc_len + ix; + } + acc_len += line.text.len(); + } + + acc_len + } + + /// Get the index for the given position (x, y) in this line layout. + /// + /// The `pos` is relative to the top-left corner of this line layout, start from (0, 0) + /// The return value is a local byte index in this line layout, start from 0. + pub(super) fn closest_index_for_position( + &self, + pos: Point, + line_height: Pixels, + ) -> Option { + let mut offset = 0; + let mut line_top = px(0.); + for line in self.wrapped_lines.iter() { + let line_bottom = line_top + line_height; + if pos.y >= line_top && pos.y < line_bottom { + let ix = line.closest_index_for_x(pos.x); + return Some(offset + ix); + } + + offset += line.text.len(); + line_top = line_bottom; + } + + None + } + + pub(super) fn index_for_position( + &self, + pos: Point, + line_height: Pixels, + ) -> Option { + let mut offset = 0; + let mut line_top = px(0.); + for line in self.wrapped_lines.iter() { + let line_bottom = line_top + line_height; + if pos.y >= line_top && pos.y < line_bottom { + let ix = line.index_for_x(pos.x)?; + return Some(offset + ix); + } + + offset += line.text.len(); + line_top = line_bottom; + } + + None + } + + pub(super) fn size(&self, line_height: Pixels) -> Size { + size(self.longest_width, self.wrapped_lines.len() * line_height) + } + + pub(super) fn paint( + &self, + pos: Point, + line_height: Pixels, + window: &mut Window, + cx: &mut App, + ) { + for (ix, line) in self.wrapped_lines.iter().enumerate() { + _ = line.paint( + pos + point(px(0.), ix * line_height), + line_height, + window, + cx, + ); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -253,15 +393,43 @@ mod tests { }; let mut wrapper = TextWrapper::new(font, px(14.), None); - let mut text = - Rope::from("Hello, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。"); + let mut text = Rope::from( + "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。", + ); - fn fake_wrap_line(_line_str: &str, _wrap_width: Pixels) -> Vec { + fn fake_wrap_line(_line: &str, _wrap_width: Pixels) -> Vec { vec![] } + #[track_caller] + fn assert_wrapper_lines(text: &Rope, wrapper: &TextWrapper, expected_lines: &[&[&str]]) { + let mut actual_lines = vec![]; + let mut offset = 0; + for line in wrapper.lines.iter() { + actual_lines.push( + line.wrapped_lines + .iter() + .map(|range| text.slice(offset + range.start..offset + range.end)) + .collect::>(), + ); + // +1 \n + offset += line.len() + 1; + } + assert_eq!(actual_lines, expected_lines); + } + wrapper._update(&text, &(0..text.len()), &text, &mut fake_wrap_line); assert_eq!(wrapper.lines.len(), 4); + assert_wrapper_lines( + &text, + &wrapper, + &[ + &["Hello, 世界!\r"], + &["This is second line."], + &["This is third line."], + &["这里是第 4 行。"], + ], + ); // Add a new text to end let range = text.len()..text.len(); @@ -270,9 +438,20 @@ mod tests { wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); assert_eq!( text.to_string(), - "Hello, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。New text" + "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text" ); assert_eq!(wrapper.lines.len(), 4); + assert_eq!(wrapper.lines.len(), 4); + assert_wrapper_lines( + &text, + &wrapper, + &[ + &["Hello, 世界!\r"], + &["This is second line."], + &["This is third line."], + &["这里是第 4 行。New text"], + ], + ); // Replace first line `Hello` to `AAA` let range = 0..5; @@ -281,9 +460,20 @@ mod tests { wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); assert_eq!( text.to_string(), - "AAA, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。New text" + "AAA, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text" ); + dbg!(&wrapper.lines); assert_eq!(wrapper.lines.len(), 4); + assert_wrapper_lines( + &text, + &wrapper, + &[ + &["AAA, 世界!\r"], + &["This is second line."], + &["This is third line."], + &["这里是第 4 行。New text"], + ], + ); // Remove the second line let start_offset = text.line_start_offset(1); @@ -293,9 +483,18 @@ mod tests { wrapper._update(&text, &range, &Rope::from(""), &mut fake_wrap_line); assert_eq!( text.to_string(), - "AAA, 世界!\nThis is third line.\n这里是第 4 行。New text" + "AAA, 世界!\r\nThis is third line.\n这里是第 4 行。New text" ); assert_eq!(wrapper.lines.len(), 3); + assert_wrapper_lines( + &text, + &wrapper, + &[ + &["AAA, 世界!\r"], + &["This is third line."], + &["这里是第 4 行。New text"], + ], + ); // Replace the first 2 lines to "This is a new line." let range = text.line_start_offset(0)..text.line_end_offset(1) + 1; @@ -307,6 +506,15 @@ mod tests { "This is a new line.\nThis is new line 2.\n这里是第 4 行。New text" ); assert_eq!(wrapper.lines.len(), 3); + assert_wrapper_lines( + &text, + &wrapper, + &[ + &["This is a new line."], + &["This is new line 2."], + &["这里是第 4 行。New text"], + ], + ); // Add a new line at the end let range = text.len()..text.len(); @@ -318,6 +526,16 @@ mod tests { "This is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end." ); assert_eq!(wrapper.lines.len(), 4); + assert_wrapper_lines( + &text, + &wrapper, + &[ + &["This is a new line."], + &["This is new line 2."], + &["这里是第 4 行。New text"], + &["This is a new line at the end."], + ], + ); // Add a new line at the beginning let range = 0..0; @@ -329,6 +547,17 @@ mod tests { "This is a new line at the beginning.\nThis is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end." ); assert_eq!(wrapper.lines.len(), 5); + assert_wrapper_lines( + &text, + &wrapper, + &[ + &["This is a new line at the beginning."], + &["This is a new line."], + &["This is new line 2."], + &["这里是第 4 行。New text"], + &["This is a new line at the end."], + ], + ); // Remove all to at least one line in `lines`. let range = 0..text.len(); @@ -337,6 +566,7 @@ mod tests { wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); assert_eq!(text.to_string(), ""); assert_eq!(wrapper.lines.len(), 1); + assert_eq!(wrapper.lines[0].wrapped_lines, vec![0..0]); // Test update_all let range = 0..text.len(); @@ -349,4 +579,16 @@ mod tests { ); assert_eq!(wrapper.lines.len(), 2); } + + #[test] + fn test_line_layout() { + let mut line_layout = LineLayout::new(); + + let line1 = ShapedLine::default().with_len(100); + let line2 = ShapedLine::default().with_len(50); + let wrapped_lines = smallvec::smallvec![line1, line2]; + line_layout.set_wrapped_lines(wrapped_lines); + assert_eq!(line_layout.len(), 150); + assert_eq!(line_layout.wrapped_lines.len(), 2); + } }