input: Fix track scroll to selected end on drag move. (#1231)

This commit is contained in:
Jason Lee 2025-09-10 13:59:02 +08:00 committed by GitHub
parent 7998d64501
commit 844574befc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 106 additions and 66 deletions

30
Cargo.lock generated
View file

@ -709,7 +709,7 @@ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.11.0", "itertools 0.12.1",
"lazy_static", "lazy_static",
"lazycell", "lazycell",
"log", "log",
@ -732,7 +732,7 @@ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.11.0", "itertools 0.13.0",
"log", "log",
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
@ -1401,7 +1401,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"indexmap 2.10.0", "indexmap 2.10.0",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@ -1870,7 +1870,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3049,7 +3049,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@ -3232,7 +3232,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -3501,7 +3501,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -3521,7 +3521,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@ -4466,7 +4466,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@ -6380,7 +6380,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack", "workspace-hack",
@ -6533,7 +6533,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -6617,7 +6617,7 @@ dependencies = [
[[package]] [[package]]
name = "rope" name = "rope"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@ -7124,7 +7124,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@ -7747,7 +7747,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@ -9057,7 +9057,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs 2.1.3", "async-fs 2.1.3",

View file

@ -58,7 +58,9 @@ impl TextElement {
/// ///
/// - cursor bounds /// - cursor bounds
/// - scroll offset /// - scroll offset
/// - current line index /// - current row index (No only the visible lines, but all lines)
///
/// This method also will update for track scroll to cursor.
fn layout_cursor( fn layout_cursor(
&self, &self,
last_layout: &LastLayout, last_layout: &LastLayout,
@ -66,21 +68,21 @@ impl TextElement {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) { ) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
let state = self.state.read(cx);
let line_height = last_layout.line_height; let line_height = last_layout.line_height;
let visible_range = &last_layout.visible_range; 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 lines = &last_layout.lines;
let text_wrapper = &state.text_wrapper;
let line_number_width = last_layout.line_number_width; let line_number_width = last_layout.line_number_width;
let state = self.state.read(cx);
let mut selected_range = state.selected_range; let mut selected_range = state.selected_range;
if let Some(marked_range) = &state.marked_range { if let Some(marked_range) = &state.marked_range {
selected_range = (marked_range.end..marked_range.end).into(); selected_range = (marked_range.end..marked_range.end).into();
} }
let cursor = state.cursor(); let cursor = state.cursor();
let mut current_line_index = None; let mut current_row = None;
let mut scroll_offset = state.scroll_handle.offset(); let mut scroll_offset = state.scroll_handle.offset();
let mut cursor_bounds = None; let mut cursor_bounds = None;
@ -95,49 +97,66 @@ impl TextElement {
let mut cursor_start = None; let mut cursor_start = None;
let mut cursor_end = None; let mut cursor_end = None;
let mut prev_lines_offset = visible_start_offset; let mut prev_lines_offset = 0;
let mut offset_y = visible_top; let mut offset_y = px(0.);
for (line_ix, line) in lines.iter().enumerate() { for (ix, wrap_line) in text_wrapper.lines.iter().enumerate() {
let line_ix = visible_range.start + line_ix; let row = ix;
let line_origin = point(px(0.), offset_y);
// break loop if all cursor positions are found // break loop if all cursor positions are found
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() { if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
break; break;
} }
let line_origin = point(px(0.), offset_y); let in_visible_range = ix >= visible_range.start;
if cursor_pos.is_none() { if let Some(line) = in_visible_range
let offset = cursor.offset.saturating_sub(prev_lines_offset); .then(|| lines.get(ix.saturating_sub(visible_range.start)))
.flatten()
if let Some(pos) = line.position_for_index(offset, line_height) { {
current_line_index = Some(line_ix); // If in visible range lines
cursor_pos = Some(line_origin + pos); if cursor_pos.is_none() {
let offset = cursor.offset.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(offset, line_height) {
current_row = Some(row);
cursor_pos = Some(line_origin + pos);
}
} }
} if cursor_start.is_none() {
if cursor_start.is_none() { let offset = selected_range.start.saturating_sub(prev_lines_offset);
let offset = selected_range.start.saturating_sub(prev_lines_offset); if let Some(pos) = line.position_for_index(offset, line_height) {
if let Some(pos) = line.position_for_index(offset, line_height) { cursor_start = Some(line_origin + pos);
cursor_start = Some(line_origin + pos); }
} }
} if cursor_end.is_none() {
if cursor_end.is_none() { let offset = selected_range.end.saturating_sub(prev_lines_offset);
let offset = selected_range.end.saturating_sub(prev_lines_offset); if let Some(pos) = line.position_for_index(offset, line_height) {
if let Some(pos) = line.position_for_index(offset, line_height) { cursor_end = Some(line_origin + pos);
cursor_end = Some(line_origin + pos); }
} }
offset_y += line.size(line_height).height;
// +1 for the last `\n`
prev_lines_offset += line.len() + 1;
} else {
// If not in the visible range.
// Just increase the offset_y and prev_lines_offset.
// This will let the scroll_offset to track the cursor position correctly.
if prev_lines_offset >= cursor.offset && cursor_pos.is_none() {
current_row = Some(row);
cursor_pos = Some(line_origin);
}
if prev_lines_offset >= selected_range.start && cursor_start.is_none() {
cursor_start = Some(line_origin);
}
if prev_lines_offset >= selected_range.end && cursor_end.is_none() {
cursor_end = Some(line_origin);
}
offset_y += wrap_line.height(line_height);
// +1 for the last `\n`
prev_lines_offset += wrap_line.len() + 1;
} }
offset_y += line.size(line_height).height;
// +1 for skip the last `\n`
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)) = if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
@ -206,7 +225,7 @@ impl TextElement {
bounds.origin = bounds.origin + scroll_offset; bounds.origin = bounds.origin + scroll_offset;
(cursor_bounds, scroll_offset, current_line_index) (cursor_bounds, scroll_offset, current_row)
} }
fn layout_selections( fn layout_selections(
@ -466,8 +485,8 @@ pub(super) struct PrepaintState {
scroll_size: Size<Pixels>, scroll_size: Size<Pixels>,
cursor_bounds: Option<Bounds<Pixels>>, cursor_bounds: Option<Bounds<Pixels>>,
cursor_scroll_offset: Point<Pixels>, cursor_scroll_offset: Point<Pixels>,
/// line index (zero based), no wrap, same line as the cursor. /// row index (zero based), no wrap, same line as the cursor.
current_line_index: Option<usize>, current_row: Option<usize>,
selection_path: Option<Path<Pixels>>, selection_path: Option<Path<Pixels>>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
} }
@ -755,7 +774,7 @@ impl Element for TextElement {
// Calculate the scroll offset to keep the cursor in view // Calculate the scroll offset to keep the cursor in view
let (cursor_bounds, cursor_scroll_offset, current_line_index) = let (cursor_bounds, cursor_scroll_offset, current_row) =
self.layout_cursor(&last_layout, &mut bounds, window, cx); self.layout_cursor(&last_layout, &mut bounds, window, cx);
let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx); let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx);
@ -791,7 +810,7 @@ impl Element for TextElement {
line_no_text.push_str(&"\n ".repeat(line.wrap_boundaries.len())); line_no_text.push_str(&"\n ".repeat(line.wrap_boundaries.len()));
} }
let runs = if current_line_index == Some(ix) { let runs = if current_row == Some(ix) {
&current_line_runs &current_line_runs
} else { } else {
&other_line_runs &other_line_runs
@ -815,7 +834,7 @@ impl Element for TextElement {
line_numbers, line_numbers,
cursor_bounds, cursor_bounds,
cursor_scroll_offset, cursor_scroll_offset,
current_line_index, current_row,
selection_path, selection_path,
} }
} }
@ -891,7 +910,8 @@ impl Element for TextElement {
// Each item is the normal lines. // Each item is the normal lines.
for (ix, lines) in line_numbers.iter().enumerate() { for (ix, lines) in line_numbers.iter().enumerate() {
let is_active = prepaint.current_line_index == Some(visible_range.start + ix); let row = visible_range.start + ix;
let is_active = prepaint.current_row == Some(row);
for line in lines { for line in lines {
let p = point(input_bounds.origin.x, origin.y + offset_y); let p = point(input_bounds.origin.x, origin.y + offset_y);
let line_size = line.size(line_height); let line_size = line.size(line_height);
@ -958,10 +978,11 @@ impl Element for TextElement {
// Each item is the normal lines. // Each item is the normal lines.
for (ix, lines) in line_numbers.iter().enumerate() { for (ix, lines) in line_numbers.iter().enumerate() {
let row = visible_range.start + ix;
for line in lines { for line in lines {
let p = point(input_bounds.origin.x, origin.y + offset_y); let p = point(input_bounds.origin.x, origin.y + offset_y);
let is_active = prepaint.current_line_index == Some(visible_range.start + ix); let is_active = prepaint.current_row == Some(row);
let line_size = line.size(line_height); let line_size = line.size(line_height);
// paint active line number background // paint active line number background

View file

@ -769,15 +769,17 @@ impl InputState {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let max_point = self.text.max_point(); let max_point = self.text.max_point();
let line_ix = line.saturating_sub(1).min(max_point.row as usize); let row = line.saturating_sub(1).min(max_point.row as usize);
let column_ix = column let col = column
.unwrap_or(1) .unwrap_or(1)
.saturating_sub(1) .saturating_sub(1)
.min(self.text.line_len(line_ix as u32) as usize); .min(self.text.line_len(row as u32) as usize);
let offset = self let offset = self
.text .text
.point_to_offset(rope::Point::new(line_ix as u32, column_ix as u32)); .point_to_offset(rope::Point::new(row as u32, col as u32));
// TODO: Scroll to make the row in center of viewport.
self.move_to(Cursor::new(offset), window, cx); self.move_to(Cursor::new(offset), window, cx);
self.update_preferred_column(); self.update_preferred_column();

View file

@ -13,10 +13,27 @@ pub(super) struct LineWrap {
/// like the `window.text_system().shape_text`. So, this value may not equal /// like the `window.text_system().shape_text`. So, this value may not equal
/// the actual rendered lines. /// the actual rendered lines.
pub(super) wrap_lines: usize, pub(super) wrap_lines: usize,
/// The range of the line text in the entire text. /// The range of the line text in the entire text (not includes ending `\n`).
pub(super) range: Range<usize>, pub(super) range: Range<usize>,
} }
impl LineWrap {
/// Return the bytes length of this line.
pub(super) fn len(&self) -> usize {
self.range.end - self.range.start
}
/// Get the total number of lines including wrapped lines.
pub(super) fn lines_len(&self) -> usize {
self.wrap_lines + 1
}
/// Get the height of this line including wrapped lines.
pub(super) fn height(&self, line_height: Pixels) -> Pixels {
self.lines_len() * line_height
}
}
/// Used to prepare the text with soft wrap to be get lines to displayed in the TextArea /// Used to prepare the text with soft wrap to be get lines to displayed in the TextArea
/// ///
/// After use lines to calculate the scroll size of the TextArea /// After use lines to calculate the scroll size of the TextArea