From cfc2449820a0dee6d74a3ff6c6712074f71ca9ab Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 5 Sep 2025 18:47:24 +0800 Subject: [PATCH] input: Fix column position update when text added, to support move vertical keep column. (#1211) Refactor this logic by use Rope line, column. --- crates/story/src/clipboard_story.rs | 2 +- crates/ui/src/input/state.rs | 121 ++++++---------------------- 2 files changed, 24 insertions(+), 99 deletions(-) diff --git a/crates/story/src/clipboard_story.rs b/crates/story/src/clipboard_story.rs index 304aee10..9843e3b6 100644 --- a/crates/story/src/clipboard_story.rs +++ b/crates/story/src/clipboard_story.rs @@ -78,7 +78,7 @@ impl Render for ClipboardStory { Clipboard::new("clipboard2") .value_fn({ let state = self.url_state.clone(); - move |_, cx| state.read(cx).value().into() + move |_, cx| state.read(cx).value() }) .on_copied(|value, window, cx| { window.push_notification(format!("Copied value: {}", value), cx) diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index e9f38646..bbe38493 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -283,7 +283,7 @@ pub struct InputState { diagnostic_popover: Option>, /// To remember the horizontal column (x-coordinate) of the cursor position for keep column for move up/down. - preferred_x_offset: Option, + preferred_column: Option, _subscriptions: Vec, } @@ -349,7 +349,7 @@ impl InputState { scroll_handle: ScrollHandle::new(), scroll_state: ScrollbarState::default(), scroll_size: gpui::size(px(0.), px(0.)), - preferred_x_offset: None, + preferred_column: None, placeholder: SharedString::default(), mask_pattern: MaskPattern::default(), diagnostic_popover: None, @@ -516,18 +516,10 @@ impl InputState { cx.notify(); } - /// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is. - fn update_preferred_x_offset(&mut self, _cx: &mut Context) { - let (Some(_), Some(bounds)) = (&self.last_layout, &self.last_bounds) else { - return; - }; - - // Find which line and sub-line the cursor is on and its position - let (_, _, cursor_pos) = self.line_and_position_for_offset(self.cursor().offset); - - if let Some(pos) = cursor_pos { - self.preferred_x_offset = Some(pos.x + bounds.origin.x); - } + /// Called after moving the cursor. Updates preferred_column if we know where the cursor now is. + fn update_preferred_column(&mut self) { + let column_ix = self.text.line_column(self.cursor().offset).1; + self.preferred_column = Some(column_ix); } /// Find which line and sub-line the given offset belongs to, along with the position within that sub-line. @@ -570,90 +562,23 @@ impl InputState { return; } - let (Some(last_layout), Some(bounds)) = (&self.last_layout, &self.last_bounds) else { - return; - }; - let offset = self.cursor().offset; - let preferred_x_offset = self.preferred_x_offset; - let line_height = last_layout.line_height; - let (current_line, current_sub_line, current_pos) = - self.line_and_position_for_offset(offset); + let was_preferred_column = self.preferred_column; - let Some(current_pos) = current_pos else { - return; - }; + let line_ix = self.text.byte_to_line(offset); + let new_line_ix = line_ix.saturating_add_signed(move_lines); + let line = self.text.line(new_line_ix); + let line_start_offset = self.text.line_to_byte(new_line_ix); + let new_column = self + .preferred_column + .unwrap_or_default() + .min(line.len_bytes().saturating_sub(1)); + let new_offset = line_start_offset + new_column; - let current_x = self - .preferred_x_offset - .unwrap_or_else(|| current_pos.x + bounds.origin.x); - - let mut new_line = current_line; - let mut new_sub_line = current_sub_line as i32; - new_sub_line += if move_lines > 0 { 1 } else { -1 }; - - // Handle moving above the first line - if move_lines < 0 && current_line == 0 && new_sub_line < 0 { - // Move cursor to the beginning of the text - self.move_to(Cursor::new(0), window, cx); - self.preferred_x_offset = preferred_x_offset; - return; - } - - if new_sub_line < 0 { - new_line = new_line - .saturating_add_signed(move_lines) - .max(0) - .min(last_layout.lines.len().saturating_sub(1)); - new_sub_line = last_layout.lines[new_line].wrap_boundaries.len() as i32; - } else { - let max_sub_line = last_layout.lines[current_line].wrap_boundaries.len() as i32; - if new_sub_line > max_sub_line { - if new_line < last_layout.lines.len() - 1 { - new_line = new_line - .saturating_add_signed(move_lines) - .max(0) - .min(last_layout.lines.len().saturating_sub(1)); - new_sub_line = 0; - } else { - new_sub_line = max_sub_line; - } - } - } - - // If after adjustment, still at the same position, do not proceed - if new_line == current_line && new_sub_line == current_sub_line as i32 { - return; - } - - let target_line = &last_layout.lines[new_line]; - let line_x = current_x - bounds.origin.x; - let target_sub_line = new_sub_line as usize; - - let approx_pos = point(line_x, px(target_sub_line as f32 * line_height.0)); - let index_res = target_line.index_for_position(approx_pos, line_height); - - let new_local_index = match index_res { - Ok(i) => i, - Err(i) => i, - }; - - let mut prev_lines_offset = 0; - for (_, line) in last_layout - .lines - .iter() - .enumerate() - .take_while(|(i, _)| *i < new_line) - { - prev_lines_offset += line.len() + 1; - } - - let new_offset = (prev_lines_offset + new_local_index).min(self.text.len_bytes()); - let new_cursor = Cursor::new(new_offset); - self.selected_range = (new_cursor..new_cursor).into(); self.pause_blink_cursor(cx); + self.move_to(Cursor::new(new_offset), window, cx); // Set back the preferred_x_offset - self.preferred_x_offset = preferred_x_offset; + self.preferred_column = was_preferred_column; cx.notify(); } @@ -1744,7 +1669,7 @@ impl InputState { let cursor = Cursor::new(cursor.offset.clamp(0, self.text.len_bytes())); self.selected_range = (cursor..cursor).into(); self.pause_blink_cursor(cx); - self.update_preferred_x_offset(cx); + self.update_preferred_column(); cx.notify() } @@ -1891,7 +1816,7 @@ impl InputState { } } if self.selected_range.is_empty() { - self.update_preferred_x_offset(cx); + self.update_preferred_column(); } cx.notify() } @@ -2239,10 +2164,10 @@ impl EntityInputHandler for InputState { .update_highlighter(&range, &self.text, &new_text, true, cx); self.selected_range = (new_offset..new_offset).into(); self.marked_range.take(); - self.update_preferred_x_offset(cx); + self.update_preferred_column(); self.update_scroll_offset(None, cx); self.mode.update_auto_grow(&self.text_wrapper); - cx.emit(InputEvent::Change(self.unmask_value().into())); + cx.emit(InputEvent::Change(self.unmask_value())); cx.notify(); } @@ -2294,7 +2219,7 @@ impl EntityInputHandler for InputState { .into(); } self.mode.update_auto_grow(&self.text_wrapper); - cx.emit(InputEvent::Change(self.unmask_value().into())); + cx.emit(InputEvent::Change(self.unmask_value())); cx.notify(); }