diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6e4744335b..d985a4d269 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20475,7 +20475,7 @@ impl Editor { EditorSettings::get_global(cx).gutter.line_numbers } - pub fn relative_line_numbers(&self, cx: &mut App) -> RelativeLineNumbers { + pub fn relative_line_numbers(&self, cx: &App) -> RelativeLineNumbers { match ( self.use_relative_line_numbers, EditorSettings::get_global(cx).relative_line_numbers, @@ -25294,6 +25294,70 @@ impl EditorSnapshot { let digit_count = self.widest_line_number().ilog10() + 1; column_pixels(style, digit_count as usize, window) } + + /// Returns the line delta from `base` to `line` in the multibuffer, ignoring wrapped lines. + /// + /// This is positive if `base` is before `line`. + fn relative_line_delta(&self, base: DisplayRow, line: DisplayRow) -> i64 { + let point = DisplayPoint::new(line, 0).to_point(self); + self.relative_line_delta_to_point(base, point) + } + + /// Returns the line delta from `base` to `point` in the multibuffer, ignoring wrapped lines. + /// + /// This is positive if `base` is before `point`. + pub fn relative_line_delta_to_point(&self, base: DisplayRow, point: Point) -> i64 { + let base_point = DisplayPoint::new(base, 0).to_point(self); + point.row as i64 - base_point.row as i64 + } + + /// Returns the line delta from `base` to `line` in the multibuffer, counting wrapped lines. + /// + /// This is positive if `base` is before `line`. + fn relative_wrapped_line_delta(&self, base: DisplayRow, line: DisplayRow) -> i64 { + let point = DisplayPoint::new(line, 0).to_point(self); + self.relative_wrapped_line_delta_to_point(base, point) + } + + /// Returns the line delta from `base` to `point` in the multibuffer, counting wrapped lines. + /// + /// This is positive if `base` is before `point`. + pub fn relative_wrapped_line_delta_to_point(&self, base: DisplayRow, point: Point) -> i64 { + let base_point = DisplayPoint::new(base, 0).to_point(self); + let wrap_snapshot = self.wrap_snapshot(); + let base_wrap_row = wrap_snapshot.make_wrap_point(base_point, Bias::Left).row(); + let wrap_row = wrap_snapshot.make_wrap_point(point, Bias::Left).row(); + wrap_row.0 as i64 - base_wrap_row.0 as i64 + } + + /// Returns the unsigned relative line number to display for each row in `rows`. + /// + /// Wrapped rows are excluded from the hashmap if `count_relative_lines` is `false`. + pub fn calculate_relative_line_numbers( + &self, + rows: &Range, + relative_to: DisplayRow, + count_wrapped_lines: bool, + ) -> HashMap { + let initial_offset = if count_wrapped_lines { + self.relative_wrapped_line_delta(relative_to, rows.start) + } else { + self.relative_line_delta(relative_to, rows.start) + }; + let display_row_infos = self + .row_infos(rows.start) + .take(rows.len()) + .enumerate() + .map(|(i, row_info)| (DisplayRow(rows.start.0 + i as u32), row_info)); + display_row_infos + .filter(|(_row, row_info)| { + row_info.buffer_row.is_some() + || (count_wrapped_lines && row_info.wrapped_buffer_row.is_some()) + }) + .enumerate() + .map(|(i, (row, _row_info))| (row, (initial_offset + i as i64).unsigned_abs() as u32)) + .collect() + } } pub fn column_pixels(style: &EditorStyle, column: usize, window: &Window) -> Pixels { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 87674d8c50..613850428a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -36,7 +36,8 @@ use languages::markdown_lang; use languages::rust_lang; use lsp::CompletionParams; use multi_buffer::{ - IndentGuide, MultiBufferFilterMode, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey, + ExcerptRange, IndentGuide, MultiBuffer, MultiBufferFilterMode, MultiBufferOffset, + MultiBufferOffsetUtf16, PathKey, }; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; @@ -28633,6 +28634,130 @@ async fn test_sticky_scroll(cx: &mut TestAppContext) { assert_eq!(sticky_headers(10.0), vec![]); } +#[gpui::test] +fn test_relative_line_numbers(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx)); + let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx)); + let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx)); + + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(ReadWrite); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))], + cx, + ); + multibuffer.push_excerpts( + buffer_3.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))], + cx, + ); + multibuffer + }); + + // wrapped contents of multibuffer: + // aaa + // aaa + // aaa + // a + // bbb + // + // ccc + // ccc + // ccc + // c + // ddd + // + // eee + // fff + // fff + // fff + // f + + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx)); + editor.update_in(cx, |editor, window, cx| { + editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters + + // includes trailing newlines. + let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23]; + let expected_wrapped_line_numbers = [ + 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23, + ]; + + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([ + Point::new(7, 0)..Point::new(7, 1), // second row of `ccc` + ]); + }); + + let snapshot = editor.snapshot(window, cx); + + // these are all 0-indexed + let base_display_row = DisplayRow(11); + let base_row = 3; + let wrapped_base_row = 7; + + // test not counting wrapped lines + let expected_relative_numbers = expected_line_numbers + .into_iter() + .enumerate() + .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32)) + .collect_vec(); + let actual_relative_numbers = snapshot + .calculate_relative_line_numbers( + &(DisplayRow(0)..DisplayRow(24)), + base_display_row, + false, + ) + .into_iter() + .sorted() + .collect_vec(); + assert_eq!(expected_relative_numbers, actual_relative_numbers); + // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line + for (display_row, relative_number) in expected_relative_numbers { + assert_eq!( + relative_number, + snapshot + .relative_line_delta(display_row, base_display_row) + .unsigned_abs() as u32, + ); + } + + // test counting wrapped lines + let expected_wrapped_relative_numbers = expected_wrapped_line_numbers + .into_iter() + .enumerate() + .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32)) + .collect_vec(); + let actual_relative_numbers = snapshot + .calculate_relative_line_numbers( + &(DisplayRow(0)..DisplayRow(24)), + base_display_row, + true, + ) + .into_iter() + .sorted() + .collect_vec(); + assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers); + // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line + for (display_row, relative_number) in expected_wrapped_relative_numbers { + assert_eq!( + relative_number, + snapshot + .relative_wrapped_line_delta(display_row, base_display_row) + .unsigned_abs() as u32, + ); + } + }); +} + #[gpui::test] async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4c3b44335b..b2e355dc51 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -66,7 +66,7 @@ use project::{ }; use settings::{ GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring, - Settings, + RelativeLineNumbers, Settings, }; use smallvec::{SmallVec, smallvec}; use std::{ @@ -194,8 +194,6 @@ pub struct EditorElement { style: EditorStyle, } -type DisplayRowDelta = u32; - impl EditorElement { pub(crate) const SCROLLBAR_WIDTH: Pixels = px(15.); @@ -3225,64 +3223,6 @@ impl EditorElement { .collect() } - fn calculate_relative_line_numbers( - &self, - snapshot: &EditorSnapshot, - rows: &Range, - relative_to: Option, - count_wrapped_lines: bool, - ) -> HashMap { - let mut relative_rows: HashMap = Default::default(); - let Some(relative_to) = relative_to else { - return relative_rows; - }; - - let start = rows.start.min(relative_to); - let end = rows.end.max(relative_to); - - let buffer_rows = snapshot - .row_infos(start) - .take(1 + end.minus(start) as usize) - .collect::>(); - - let head_idx = relative_to.minus(start); - let mut delta = 1; - let mut i = head_idx + 1; - let should_count_line = |row_info: &RowInfo| { - if count_wrapped_lines { - row_info.buffer_row.is_some() || row_info.wrapped_buffer_row.is_some() - } else { - row_info.buffer_row.is_some() - } - }; - while i < buffer_rows.len() as u32 { - if should_count_line(&buffer_rows[i as usize]) { - if rows.contains(&DisplayRow(i + start.0)) { - relative_rows.insert(DisplayRow(i + start.0), delta); - } - delta += 1; - } - i += 1; - } - delta = 1; - i = head_idx.min(buffer_rows.len().saturating_sub(1) as u32); - while i > 0 && buffer_rows[i as usize].buffer_row.is_none() && !count_wrapped_lines { - i -= 1; - } - - while i > 0 { - i -= 1; - if should_count_line(&buffer_rows[i as usize]) { - if rows.contains(&DisplayRow(i + start.0)) { - relative_rows.insert(DisplayRow(i + start.0), delta); - } - delta += 1; - } - } - - relative_rows - } - fn layout_line_numbers( &self, gutter_hitbox: Option<&Hitbox>, @@ -3292,7 +3232,7 @@ impl EditorElement { rows: Range, buffer_rows: &[RowInfo], active_rows: &BTreeMap, - newest_selection_head: Option, + relative_line_base: Option, snapshot: &EditorSnapshot, window: &mut Window, cx: &mut App, @@ -3304,32 +3244,16 @@ impl EditorElement { return Arc::default(); } - let (newest_selection_head, relative) = self.editor.update(cx, |editor, cx| { - let newest_selection_head = newest_selection_head.unwrap_or_else(|| { - let newest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - SelectionLayout::new( - newest, - editor.selections.line_mode(), - editor.cursor_offset_on_selection, - editor.cursor_shape, - &snapshot.display_snapshot, - true, - true, - None, - ) - .head - }); - let relative = editor.relative_line_numbers(cx); - (newest_selection_head, relative) - }); + let relative = self.editor.read(cx).relative_line_numbers(cx); let relative_line_numbers_enabled = relative.enabled(); - let relative_to = relative_line_numbers_enabled.then(|| newest_selection_head.row()); + let relative_rows = if relative_line_numbers_enabled && let Some(base) = relative_line_base + { + snapshot.calculate_relative_line_numbers(&rows, base, relative.wrapped()) + } else { + Default::default() + }; - let relative_rows = - self.calculate_relative_line_numbers(snapshot, &rows, relative_to, relative.wrapped()); let mut line_number = String::new(); let segments = buffer_rows.iter().enumerate().flat_map(|(ix, row_info)| { let display_row = DisplayRow(rows.start.0 + ix as u32); @@ -4652,6 +4576,8 @@ impl EditorElement { gutter_hitbox: &Hitbox, text_hitbox: &Hitbox, style: &EditorStyle, + relative_line_numbers: RelativeLineNumbers, + relative_to: Option, window: &mut Window, cx: &mut App, ) -> Option { @@ -4681,9 +4607,21 @@ impl EditorElement { ); let line_number = show_line_numbers.then(|| { - let number = (start_point.row + 1).to_string(); + let relative_number = relative_to.and_then(|base| match relative_line_numbers { + RelativeLineNumbers::Disabled => None, + RelativeLineNumbers::Enabled => { + Some(snapshot.relative_line_delta_to_point(base, start_point)) + } + RelativeLineNumbers::Wrapped => { + Some(snapshot.relative_wrapped_line_delta_to_point(base, start_point)) + } + }); + let number = relative_number + .filter(|&delta| delta != 0) + .map(|delta| delta.unsigned_abs() as u32) + .unwrap_or(start_point.row + 1); let color = cx.theme().colors().editor_line_number; - self.shape_line_number(SharedString::from(number), color, window) + self.shape_line_number(SharedString::from(number.to_string()), color, window) }); lines.push(StickyHeaderLine::new( @@ -9436,6 +9374,28 @@ impl Element for EditorElement { window, cx, ); + + // relative rows are based on newest selection, even outside the visible area + let relative_row_base = self.editor.update(cx, |editor, cx| { + if editor.selections.count()==0 { + return None; + } + let newest = editor + .selections + .newest::(&editor.display_snapshot(cx)); + Some(SelectionLayout::new( + newest, + editor.selections.line_mode(), + editor.cursor_offset_on_selection, + editor.cursor_shape, + &snapshot.display_snapshot, + true, + true, + None, + ) + .head.row()) + }); + let mut breakpoint_rows = self.editor.update(cx, |editor, cx| { editor.active_breakpoints(start_row..end_row, window, cx) }); @@ -9453,7 +9413,7 @@ impl Element for EditorElement { start_row..end_row, &row_infos, &active_rows, - newest_selection_head, + relative_row_base, &snapshot, window, cx, @@ -9773,6 +9733,7 @@ impl Element for EditorElement { && is_singleton && EditorSettings::get_global(cx).sticky_scroll.enabled { + let relative = self.editor.read(cx).relative_line_numbers(cx); self.layout_sticky_headers( &snapshot, editor_width, @@ -9784,6 +9745,8 @@ impl Element for EditorElement { &gutter_hitbox, &text_hitbox, &style, + relative, + relative_row_base, window, cx, ) @@ -11631,7 +11594,7 @@ mod tests { } #[gpui::test] - fn test_shape_line_numbers(cx: &mut TestAppContext) { + fn test_layout_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); let window = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); @@ -11671,7 +11634,7 @@ mod tests { }) .collect::>(), &BTreeMap::default(), - Some(DisplayPoint::new(DisplayRow(0), 0)), + Some(DisplayRow(0)), &snapshot, window, cx, @@ -11683,10 +11646,9 @@ mod tests { let relative_rows = window .update(cx, |editor, window, cx| { let snapshot = editor.snapshot(window, cx); - element.calculate_relative_line_numbers( - &snapshot, + snapshot.calculate_relative_line_numbers( &(DisplayRow(0)..DisplayRow(6)), - Some(DisplayRow(3)), + DisplayRow(3), false, ) }) @@ -11702,10 +11664,9 @@ mod tests { let relative_rows = window .update(cx, |editor, window, cx| { let snapshot = editor.snapshot(window, cx); - element.calculate_relative_line_numbers( - &snapshot, + snapshot.calculate_relative_line_numbers( &(DisplayRow(3)..DisplayRow(6)), - Some(DisplayRow(1)), + DisplayRow(1), false, ) }) @@ -11719,10 +11680,9 @@ mod tests { let relative_rows = window .update(cx, |editor, window, cx| { let snapshot = editor.snapshot(window, cx); - element.calculate_relative_line_numbers( - &snapshot, + snapshot.calculate_relative_line_numbers( &(DisplayRow(0)..DisplayRow(3)), - Some(DisplayRow(6)), + DisplayRow(6), false, ) }) @@ -11759,7 +11719,7 @@ mod tests { }) .collect::>(), &BTreeMap::default(), - Some(DisplayPoint::new(DisplayRow(0), 0)), + Some(DisplayRow(0)), &snapshot, window, cx, @@ -11774,7 +11734,7 @@ mod tests { } #[gpui::test] - fn test_shape_line_numbers_wrapping(cx: &mut TestAppContext) { + fn test_layout_line_numbers_wrapping(cx: &mut TestAppContext) { init_test(cx, |_| {}); let window = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); @@ -11819,7 +11779,7 @@ mod tests { }) .collect::>(), &BTreeMap::default(), - Some(DisplayPoint::new(DisplayRow(0), 0)), + Some(DisplayRow(0)), &snapshot, window, cx, @@ -11831,10 +11791,9 @@ mod tests { let relative_rows = window .update(cx, |editor, window, cx| { let snapshot = editor.snapshot(window, cx); - element.calculate_relative_line_numbers( - &snapshot, + snapshot.calculate_relative_line_numbers( &(DisplayRow(0)..DisplayRow(6)), - Some(DisplayRow(3)), + DisplayRow(3), true, ) }) @@ -11871,7 +11830,7 @@ mod tests { }) .collect::>(), &BTreeMap::from_iter([(DisplayRow(0), LineHighlightSpec::default())]), - Some(DisplayPoint::new(DisplayRow(0), 0)), + Some(DisplayRow(0)), &snapshot, window, cx, @@ -11886,10 +11845,9 @@ mod tests { let relative_rows = window .update(cx, |editor, window, cx| { let snapshot = editor.snapshot(window, cx); - element.calculate_relative_line_numbers( - &snapshot, + snapshot.calculate_relative_line_numbers( &(DisplayRow(0)..DisplayRow(6)), - Some(DisplayRow(3)), + DisplayRow(3), true, ) })