diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index 1b3f983c..eaf670da 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -6,6 +6,7 @@ mod element; mod lsp; mod mask_pattern; mod mode; +mod movement; mod number_input; mod otp_input; pub(crate) mod popovers; diff --git a/crates/ui/src/input/movement.rs b/crates/ui/src/input/movement.rs new file mode 100644 index 00000000..27dd01fc --- /dev/null +++ b/crates/ui/src/input/movement.rs @@ -0,0 +1,234 @@ +use gpui::{Context, Point, Window}; + +use crate::input::{ + InputState, MoveDown, MoveEnd, MoveHome, MoveLeft, MovePageDown, MovePageUp, MoveRight, + MoveToEnd, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveUp, RopeExt as _, +}; + +impl InputState { + /// Called after moving the cursor. Updates preferred_column if we know where the cursor now is. + pub(super) fn update_preferred_column(&mut self) { + let Some(last_layout) = &self.last_layout else { + self.preferred_column = None; + return; + }; + + let point = self.text.offset_to_point(self.cursor()); + let row = point.row.saturating_sub(last_layout.visible_range.start); + let Some(line) = last_layout.lines.get(row) else { + self.preferred_column = None; + return; + }; + + let Some(pos) = line.position_for_index(point.column, last_layout.line_height) else { + self.preferred_column = None; + return; + }; + + self.preferred_column = Some((pos.x, point.column)); + } + + /// Move the cursor to the given offset. + /// + /// The offset is the UTF-8 offset. + /// + /// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset. + pub(crate) fn move_to(&mut self, offset: usize, cx: &mut Context) { + let offset = offset.clamp(0, self.text.len()); + self.selected_range = (offset..offset).into(); + self.scroll_to(offset, cx); + self.pause_blink_cursor(cx); + self.update_preferred_column(); + self.hide_context_menu(cx); + cx.notify() + } + + /// Move the cursor vertically by one line (up or down) while preserving the column if possible. + /// + /// move_lines: Number of lines to move vertically (positive for down, negative for up). + pub(super) fn move_vertical( + &mut self, + move_lines: isize, + _: &mut Window, + cx: &mut Context, + ) { + if self.mode.is_single_line() { + return; + } + let Some(last_layout) = &self.last_layout else { + return; + }; + + let offset = self.cursor(); + let was_preferred_column = self.preferred_column; + + let mut display_point = self.text_wrapper.offset_to_display_point(offset); + display_point.row = display_point.row.saturating_add_signed(move_lines); + display_point.column = 0; + let mut new_offset = self.text_wrapper.display_point_to_offset(display_point); + + if let Some((preferred_x, column)) = was_preferred_column { + // Get display point again to update local_row. + let mut next_display_point = self.text_wrapper.offset_to_display_point(new_offset); + next_display_point.column = 0; + let next_point = self.text_wrapper.display_point_to_point(next_display_point); + let line_start_offset = self.text.line_start_offset(next_point.row); + + // If in visible range, prefer to use position to get column. + if let Some(line) = last_layout.line(next_point.row) { + if let Some(x) = line.closest_index_for_position( + Point { + x: preferred_x, + y: next_display_point.local_row * last_layout.line_height, + }, + last_layout.line_height, + ) { + new_offset = line_start_offset + x; + } + } else { + // Not in visible range, use column directly. + let max_line_len = self.text.slice_line(next_point.row).len(); + new_offset = line_start_offset + column.min(max_line_len); + } + } + + self.pause_blink_cursor(cx); + self.move_to(new_offset, cx); + // Set back the preferred_column + self.preferred_column = was_preferred_column; + cx.notify(); + } + + pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context) { + self.pause_blink_cursor(cx); + if self.selected_range.is_empty() { + self.move_to(self.previous_boundary(self.cursor()), cx); + } else { + self.move_to(self.selected_range.start, cx) + } + } + + pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context) { + self.pause_blink_cursor(cx); + if self.selected_range.is_empty() { + self.move_to(self.next_boundary(self.selected_range.end), cx); + } else { + self.move_to(self.selected_range.end, cx) + } + } + + pub(super) fn up(&mut self, action: &MoveUp, window: &mut Window, cx: &mut Context) { + if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) { + return; + } + + if self.mode.is_single_line() { + return; + } + + if !self.selected_range.is_empty() { + self.move_to( + self.previous_boundary(self.selected_range.start.saturating_sub(1)), + cx, + ); + } + self.pause_blink_cursor(cx); + self.move_vertical(-1, window, cx); + } + + pub(super) fn down(&mut self, action: &MoveDown, window: &mut Window, cx: &mut Context) { + if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) { + return; + } + + if self.mode.is_single_line() { + return; + } + + if !self.selected_range.is_empty() { + self.move_to( + self.next_boundary(self.selected_range.end.saturating_sub(1)), + cx, + ); + } + + self.pause_blink_cursor(cx); + self.move_vertical(1, window, cx); + } + + pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context) { + if self.mode.is_single_line() { + return; + } + + let Some(last_layout) = &self.last_layout else { + return; + }; + + let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize; + self.move_vertical(-display_lines, window, cx); + } + + pub(super) fn page_down( + &mut self, + _: &MovePageDown, + window: &mut Window, + cx: &mut Context, + ) { + if self.mode.is_single_line() { + return; + } + + let Some(last_layout) = &self.last_layout else { + return; + }; + + let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize; + self.move_vertical(display_lines, window, cx); + } + + pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context) { + self.pause_blink_cursor(cx); + let offset = self.start_of_line(); + self.move_to(offset, cx); + } + + pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context) { + self.pause_blink_cursor(cx); + let offset = self.end_of_line(); + self.move_to(offset, cx); + } + + pub(super) fn move_to_start( + &mut self, + _: &MoveToStart, + _: &mut Window, + cx: &mut Context, + ) { + self.move_to(0, cx); + } + + pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context) { + self.move_to(self.text.len(), cx); + } + + pub(super) fn move_to_previous_word( + &mut self, + _: &MoveToPreviousWord, + _: &mut Window, + cx: &mut Context, + ) { + let offset = self.previous_start_of_word(); + self.move_to(offset, cx); + } + + pub(super) fn move_to_next_word( + &mut self, + _: &MoveToNextWord, + _: &mut Window, + cx: &mut Context, + ) { + let offset = self.next_end_of_word(); + self.move_to(offset, cx); + } +} diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 1750046f..3277541b 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -249,6 +249,21 @@ pub(super) struct LastLayout { pub(super) cursor_bounds: Option>, } +impl LastLayout { + /// Get the line layout for the given row (0-based). + /// + /// 0 is the viewport first visible line. + /// + /// Returns None if the row is out of range. + pub(crate) fn line(&self, row: usize) -> Option<&LineLayout> { + if row < self.visible_range.start || row >= self.visible_range.end { + return None; + } + + self.lines.get(row.saturating_sub(self.visible_range.start)) + } +} + /// InputState to keep editing state of the [`super::TextInput`]. pub struct InputState { pub(super) focus_handle: FocusHandle, @@ -315,7 +330,7 @@ pub struct InputState { /// /// The first element is the x-coordinate (Pixels), preferred to use this. /// The second element is the column (usize), fallback to use this. - preferred_column: Option<(Pixels, usize)>, + pub(super) preferred_column: Option<(Pixels, usize)>, _subscriptions: Vec, pub(super) _context_menu_task: Task>, @@ -573,28 +588,6 @@ impl InputState { cx.notify(); } - /// Called after moving the cursor. Updates preferred_column if we know where the cursor now is. - fn update_preferred_column(&mut self) { - let Some(last_layout) = &self.last_layout else { - self.preferred_column = None; - return; - }; - - let point = self.text.offset_to_point(self.cursor()); - let row = point.row.saturating_sub(last_layout.visible_range.start); - let Some(line) = last_layout.lines.get(row) else { - self.preferred_column = None; - return; - }; - - let Some(pos) = line.position_for_index(point.column, last_layout.line_height) else { - self.preferred_column = None; - return; - }; - - self.preferred_column = Some((pos.x, point.column)); - } - /// Find which line and sub-line the given offset belongs to, along with the position within that sub-line. /// /// Returns: @@ -628,55 +621,6 @@ impl InputState { (0, 0, None) } - /// Move the cursor vertically by one line (up or down) while preserving the column if possible. - /// - /// move_lines: Number of lines to move vertically (positive for down, negative for up). - fn move_vertical(&mut self, move_lines: isize, _: &mut Window, cx: &mut Context) { - if self.mode.is_single_line() { - return; - } - let Some(last_layout) = &self.last_layout else { - return; - }; - - let offset = self.cursor(); - let was_preferred_column = self.preferred_column; - - let row = self.text.offset_to_point(offset).row; - let new_row = row.saturating_add_signed(move_lines); - let line_start_offset = self - .text - .point_to_offset(tree_sitter::Point::new(new_row, 0)); - - let mut new_offset = line_start_offset; - - if let Some((preferred_x, column)) = was_preferred_column { - let new_column = column.min(self.text.slice_line(new_row).len()); - new_offset = line_start_offset + new_column; - - // If in visible range, prefer to use position to get column. - 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 Some(x) = line.closest_index_for_position( - Point { - x: preferred_x, - y: px(0.), - }, - last_layout.line_height, - ) { - new_offset = line_start_offset + x; - } - } - } - } - self.pause_blink_cursor(cx); - self.move_to(new_offset, cx); - // Set back the preferred_column - self.preferred_column = was_preferred_column; - cx.notify(); - } - /// Set the text of the input field. /// /// And the selection_range will be reset to 0..0. @@ -904,94 +848,6 @@ impl InputState { }); } - pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context) { - self.pause_blink_cursor(cx); - if self.selected_range.is_empty() { - self.move_to(self.previous_boundary(self.cursor()), cx); - } else { - self.move_to(self.selected_range.start, cx) - } - } - - pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context) { - self.pause_blink_cursor(cx); - if self.selected_range.is_empty() { - self.move_to(self.next_boundary(self.selected_range.end), cx); - } else { - self.move_to(self.selected_range.end, cx) - } - } - - pub(super) fn up(&mut self, action: &MoveUp, window: &mut Window, cx: &mut Context) { - if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) { - return; - } - - if self.mode.is_single_line() { - return; - } - - if !self.selected_range.is_empty() { - self.move_to( - self.previous_boundary(self.selected_range.start.saturating_sub(1)), - cx, - ); - } - self.pause_blink_cursor(cx); - self.move_vertical(-1, window, cx); - } - - pub(super) fn down(&mut self, action: &MoveDown, window: &mut Window, cx: &mut Context) { - if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) { - return; - } - - if self.mode.is_single_line() { - return; - } - - if !self.selected_range.is_empty() { - self.move_to( - self.next_boundary(self.selected_range.end.saturating_sub(1)), - cx, - ); - } - - self.pause_blink_cursor(cx); - self.move_vertical(1, window, cx); - } - - pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context) { - if self.mode.is_single_line() { - return; - } - - let Some(last_layout) = &self.last_layout else { - return; - }; - - let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize; - self.move_vertical(-display_lines, window, cx); - } - - pub(super) fn page_down( - &mut self, - _: &MovePageDown, - window: &mut Window, - cx: &mut Context, - ) { - if self.mode.is_single_line() { - return; - } - - let Some(last_layout) = &self.last_layout else { - return; - }; - - let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize; - self.move_vertical(display_lines, window, cx); - } - pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context) { self.select_to(self.previous_boundary(self.cursor()), cx); } @@ -1021,51 +877,6 @@ impl InputState { cx.notify(); } - pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context) { - self.pause_blink_cursor(cx); - let offset = self.start_of_line(); - self.move_to(offset, cx); - } - - pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context) { - self.pause_blink_cursor(cx); - let offset = self.end_of_line(); - self.move_to(offset, cx); - } - - pub(super) fn move_to_start( - &mut self, - _: &MoveToStart, - _: &mut Window, - cx: &mut Context, - ) { - self.move_to(0, cx); - } - - pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context) { - self.move_to(self.text.len(), cx); - } - - pub(super) fn move_to_previous_word( - &mut self, - _: &MoveToPreviousWord, - _: &mut Window, - cx: &mut Context, - ) { - let offset = self.previous_start_of_word(); - self.move_to(offset, cx); - } - - pub(super) fn move_to_next_word( - &mut self, - _: &MoveToNextWord, - _: &mut Window, - cx: &mut Context, - ) { - let offset = self.next_end_of_word(); - self.move_to(offset, cx); - } - pub(super) fn select_to_start( &mut self, _: &SelectToStart, @@ -1126,7 +937,7 @@ impl InputState { } /// Return the start offset of the previous word. - fn previous_start_of_word(&mut self) -> usize { + pub(super) fn previous_start_of_word(&mut self) -> usize { let offset = self.selected_range.start; let offset = self.offset_from_utf16(self.offset_to_utf16(offset)); // FIXME: Avoid to_string @@ -1140,7 +951,7 @@ impl InputState { } /// Return the next end offset of the next word. - fn next_end_of_word(&mut self) -> usize { + pub(super) fn next_end_of_word(&mut self) -> usize { let offset = self.cursor(); let offset = self.offset_from_utf16(self.offset_to_utf16(offset)); let right_part = self.text.slice(offset..self.text.len()).to_string(); @@ -1152,7 +963,7 @@ impl InputState { } /// Get start of line byte offset of cursor - fn start_of_line(&self) -> usize { + pub(super) fn start_of_line(&self) -> usize { if self.mode.is_single_line() { return 0; } @@ -1162,7 +973,7 @@ impl InputState { } /// Get end of line byte offset of cursor - fn end_of_line(&self) -> usize { + pub(super) fn end_of_line(&self) -> usize { if self.mode.is_single_line() { return self.text.len(); } @@ -1787,21 +1598,6 @@ impl InputState { self.history.ignore = false; } - /// Move the cursor to the given offset. - /// - /// The offset is the UTF-8 offset. - /// - /// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset. - pub(crate) fn move_to(&mut self, offset: usize, cx: &mut Context) { - let offset = offset.clamp(0, self.text.len()); - self.selected_range = (offset..offset).into(); - self.scroll_to(offset, cx); - self.pause_blink_cursor(cx); - self.update_preferred_column(); - self.hide_context_menu(cx); - cx.notify() - } - /// Get byte offset of the cursor. /// /// The offset is the UTF-8 offset. @@ -2023,7 +1819,7 @@ impl InputState { self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) } - fn previous_boundary(&self, offset: usize) -> usize { + pub(super) fn previous_boundary(&self, offset: usize) -> usize { let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left); if let Some(ch) = self.text.char_at(offset) { if ch == '\r' { @@ -2034,7 +1830,7 @@ impl InputState { offset } - fn next_boundary(&self, offset: usize) -> usize { + pub(super) fn next_boundary(&self, offset: usize) -> usize { let mut offset = self.text.clip_offset(offset + 1, Bias::Right); if let Some(ch) = self.text.char_at(offset) { if ch == '\r' { @@ -2078,7 +1874,7 @@ impl InputState { cx.notify(); } - fn pause_blink_cursor(&mut self, cx: &mut Context) { + pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context) { self.blink_cursor.update(cx, |cursor, cx| { cursor.pause(cx); }); diff --git a/crates/ui/src/input/text_wrapper.rs b/crates/ui/src/input/text_wrapper.rs index 73406d70..6149e3c7 100644 --- a/crates/ui/src/input/text_wrapper.rs +++ b/crates/ui/src/input/text_wrapper.rs @@ -9,13 +9,11 @@ use crate::input::RopeExt; /// A line with soft wrapped lines info. #[derive(Debug, Clone)] pub(super) struct LineItem { - /// The original line text. + /// The original line text, without end `\n`. line: Rope, /// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line). /// - /// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different - /// like the `window.text_system().shape_text`. So, this value may not equal - /// the actual rendered lines. + /// Not contains the line end `\n`. pub(super) wrapped_lines: Vec>, } @@ -233,8 +231,100 @@ impl TextWrapper { fn update_all(&mut self, text: &Rope, cx: &mut App) { self.update(text, &(0..text.len()), &text, cx); } + + /// Return display point (with soft wrap) from the given byte offset in the text. + /// + /// Panics if the `offset` is out of bounds. + pub(crate) fn offset_to_display_point(&self, offset: usize) -> DisplayPoint { + let row = self.text.offset_to_point(offset).row; + let start = self.text.line_start_offset(row); + let line = &self.lines[row]; + + let mut wrapped_row = self + .lines + .iter() + .take(row) + .map(|l| l.lines_len()) + .sum::(); + + let local_offset = offset.saturating_sub(start); + for (ix, range) in line.wrapped_lines.iter().enumerate() { + if range.contains(&local_offset) { + return DisplayPoint::new( + wrapped_row + ix, + ix, + local_offset.saturating_sub(range.start), + ); + } + } + + // Otherwise return the eof of the line. + let last_range = line.wrapped_lines.last().unwrap_or(&(0..0)); + let ix = line.lines_len().saturating_sub(1); + return DisplayPoint::new(wrapped_row + ix, ix, last_range.len()); + } + + /// Return byte offset in the text from the given display point (with soft wrap). + /// + /// Panics if the `point.row` is out of bounds. + pub(crate) fn display_point_to_offset(&self, point: DisplayPoint) -> usize { + let mut wrapped_row = 0; + for (row, line) in self.lines.iter().enumerate() { + if wrapped_row + line.lines_len() > point.row { + let line_start = self.text.line_start_offset(row); + let local_row = point.row.saturating_sub(wrapped_row); + if let Some(range) = line.wrapped_lines.get(local_row) { + return line_start + (range.start + point.column).min(range.end); + } else { + // If not found, return the end of the line. + return line_start + line.len(); + } + } + + wrapped_row += line.lines_len(); + } + + return self.text.len(); + } + + pub(crate) fn display_point_to_point(&self, point: DisplayPoint) -> tree_sitter::Point { + let offset = self.display_point_to_offset(point); + self.text.offset_to_point(offset) + } + + pub(crate) fn point_to_display_point(&self, point: tree_sitter::Point) -> DisplayPoint { + let offset = self.text.point_to_offset(point); + self.offset_to_display_point(offset) + } } +/// The actually display point in the text. +/// +/// This is usually used to describe the +/// position in the text with `soft-wrap` mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisplayPoint { + /// The 0-based soft wrapped row index in the text. + pub row: usize, + /// The 0-based row index in local line (include first line). + /// + /// This value only valid when return from [`TextWrapper::offset_to_display_point`], otherwise it will be ignored. + pub local_row: usize, + /// The 0-based column byte index in the display line (with soft wrap). + pub column: usize, +} + +impl DisplayPoint { + pub fn new(row: usize, local_row: usize, column: usize) -> Self { + Self { + row, + local_row, + column, + } + } +} + +/// The layout info of a line with soft wrapped lines. pub(crate) struct LineLayout { /// Total bytes length of this line. len: usize, @@ -285,24 +375,35 @@ impl LineLayout { 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()); + for (i, line) in self.wrapped_lines.iter().enumerate() { + let is_last = i + 1 == self.wrapped_lines.len(); + let line_len = if is_last { line.len + 1 } else { line.len }; + + 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(); + acc_len += line_len; offset_y += line_height; } None } + /// Get the closest index for the given x in this line layout. pub(super) fn closest_index_for_x(&self, x: Pixels) -> usize { let mut acc_len = 0; - for line in self.wrapped_lines.iter() { + for (i, line) in self.wrapped_lines.iter().enumerate() { + let is_last = i + 1 == self.wrapped_lines.len(); if x <= line.width { - let ix = line.closest_index_for_x(x); + let mut ix = line.closest_index_for_x(x); + if !is_last && ix == line.text.len() { + // For soft wrap line, we can't put the cursor at the end of the line. + let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0); + ix = ix.saturating_sub(c_len); + } + return acc_len + ix; } acc_len += line.text.len(); @@ -322,10 +423,16 @@ impl LineLayout { ) -> Option { let mut offset = 0; let mut line_top = px(0.); - for line in self.wrapped_lines.iter() { + for (i, line) in self.wrapped_lines.iter().enumerate() { + let is_last = i + 1 == self.wrapped_lines.len(); 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); + let mut ix = line.closest_index_for_x(pos.x); + if !is_last && ix == line.text.len() { + // For soft wrap line, we can't put the cursor at the end of the line. + let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0); + ix = ix.saturating_sub(c_len); + } return Some(offset + ix); } @@ -592,4 +699,105 @@ mod tests { assert_eq!(line_layout.len(), 150); assert_eq!(line_layout.wrapped_lines.len(), 2); } + + #[test] + fn test_offset_to_display_point() { + let font = gpui::Font { + family: "Arial".into(), + weight: FontWeight::default(), + style: FontStyle::Normal, + features: FontFeatures::default(), + fallbacks: None, + }; + + let mut wrapper = TextWrapper::new(font, px(14.), None); + wrapper.text = Rope::from( + "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。", + ); + wrapper.lines = vec![ + // range: 0..15 + LineItem { + line: Rope::from("Hello, 世界!\r"), + wrapped_lines: vec![0..15], + }, + // range: 16..36 + LineItem { + line: Rope::from("This is second line."), + wrapped_lines: vec![0..10, 10..20], + }, + // range: 37..56 + LineItem { + line: Rope::from("This is third line."), + wrapped_lines: vec![0..9, 9..15, 15..20], + }, + // range: 57..79 + LineItem { + line: Rope::from("这里是第 4 行。"), + wrapped_lines: vec![0..22], + }, + ]; + + assert_eq!( + wrapper.offset_to_display_point(12), + DisplayPoint::new(0, 0, 12) + ); + assert_eq!( + wrapper.offset_to_display_point(15), + DisplayPoint::new(0, 0, 15) + ); + + assert_eq!( + wrapper.offset_to_display_point(16), + DisplayPoint::new(1, 0, 0) + ); + assert_eq!( + wrapper.offset_to_display_point(21), + DisplayPoint::new(1, 0, 5) + ); + assert_eq!( + wrapper.offset_to_display_point(27), + DisplayPoint::new(2, 1, 1) + ); + assert_eq!( + wrapper.offset_to_display_point(37), + DisplayPoint::new(3, 0, 0) + ); + assert_eq!( + wrapper.offset_to_display_point(54), + DisplayPoint::new(5, 2, 2) + ); + assert_eq!( + wrapper.offset_to_display_point(59), + DisplayPoint::new(6, 0, 2) + ); + + assert_eq!( + wrapper.display_point_to_offset(DisplayPoint::new(6, 0, 2)), + 59 + ); + assert_eq!( + wrapper.display_point_to_offset(DisplayPoint::new(5, 2, 2)), + 54 + ); + assert_eq!( + wrapper.display_point_to_offset(DisplayPoint::new(3, 0, 0)), + 37 + ); + assert_eq!( + wrapper.display_point_to_offset(DisplayPoint::new(2, 1, 1)), + 27 + ); + assert_eq!( + wrapper.display_point_to_offset(DisplayPoint::new(1, 0, 5)), + 21 + ); + assert_eq!( + wrapper.display_point_to_offset(DisplayPoint::new(1, 0, 0)), + 16 + ); + assert_eq!( + wrapper.display_point_to_offset(DisplayPoint::new(0, 0, 15)), + 15 + ); + } }