editor: Add to support move vertical for soft wrapped lines. (#1312)
This commit is contained in:
parent
f59bac8c8b
commit
794f4d6397
4 changed files with 477 additions and 238 deletions
|
|
@ -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;
|
||||
|
|
|
|||
234
crates/ui/src/input/movement.rs
Normal file
234
crates/ui/src/input/movement.rs
Normal file
|
|
@ -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<Self>) {
|
||||
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<Self>,
|
||||
) {
|
||||
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>) {
|
||||
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>) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
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<Self>,
|
||||
) {
|
||||
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>) {
|
||||
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>) {
|
||||
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>,
|
||||
) {
|
||||
self.move_to(0, cx);
|
||||
}
|
||||
|
||||
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.move_to(self.text.len(), cx);
|
||||
}
|
||||
|
||||
pub(super) fn move_to_previous_word(
|
||||
&mut self,
|
||||
_: &MoveToPreviousWord,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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<Self>,
|
||||
) {
|
||||
let offset = self.next_end_of_word();
|
||||
self.move_to(offset, cx);
|
||||
}
|
||||
}
|
||||
|
|
@ -249,6 +249,21 @@ pub(super) struct LastLayout {
|
|||
pub(super) cursor_bounds: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
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<Subscription>,
|
||||
|
||||
pub(super) _context_menu_task: Task<Result<()>>,
|
||||
|
|
@ -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<Self>) {
|
||||
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>) {
|
||||
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>) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
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<Self>,
|
||||
) {
|
||||
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>) {
|
||||
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>) {
|
||||
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>) {
|
||||
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>,
|
||||
) {
|
||||
self.move_to(0, cx);
|
||||
}
|
||||
|
||||
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.move_to(self.text.len(), cx);
|
||||
}
|
||||
|
||||
pub(super) fn move_to_previous_word(
|
||||
&mut self,
|
||||
_: &MoveToPreviousWord,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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<Self>,
|
||||
) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
|
||||
self.blink_cursor.update(cx, |cursor, cx| {
|
||||
cursor.pause(cx);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Range<usize>>,
|
||||
}
|
||||
|
||||
|
|
@ -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::<usize>();
|
||||
|
||||
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<usize> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue