From fb410ab3ae5cd649ca0e206b92b1b70bb2d3c610 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 4 Nov 2025 16:24:17 +0100 Subject: [PATCH] Support relative line number on wrapped lines (rework) (#41805) ## Add relative line numbers on wrapped lines, take 2 This is a rework of https://github.com/zed-industries/zed/pull/39268 that excludes https://github.com/zed-industries/zed/pull/39268/commits/e7096d27a6463f6eb7c2a821637c5773b2460c10. This commit introduced some line number rendering issues as described in https://github.com/zed-industries/zed/issues/41422. While @ConradIrwin suggested we try to pass in the buffer rows from the calling method instead of the snapshot, that appears to have had unintended consequences and I don't think the two calculations were intended to do the same thing. Hence, this PR has removed those changes. This PR also includes the migration fix originally done by @MrSubidubi in https://github.com/zed-industries/zed/pull/41351. ## Original PR description and release notes. **Problem:** Current relative line numbering creates a mismatch with vim-style navigation when soft wrap is enabled. Users must mentally calculate whether target lines are wrapped segments or logical lines, making `j/k` navigation unreliable and cognitively demanding. **How things work today:** - Real line navigation (`j/k` moves by logical lines): Requires determining if visible lines are wrapped segments before jumping. Can't jump to wrapped lines directly. - Display line navigation (`j/k` moves by display rows): Line numbers don't correspond to actual row distances for multi-line jumps. **Proposed solution:** Count and number each display line (including wrapped segments) for relative numbering. This creates direct visual-to-navigational correspondence, where the relative number shown always matches the `j/k` distance needed. **Benefits:** - Eliminates mental overhead of distinguishing wrapped vs. logical lines - Makes relative line numbers consistently actionable regardless of wrap state - Preserves intuitive "what you see is what you navigate" principle - Maintains vim workflow efficiency in narrow window scenarios Also explained and discussed in https://github.com/zed-industries/zed/discussions/25733. Release Notes: - Added support for counting wrapped lines as relative lines and for displaying line numbers for wrapped segments. Changes `relative_line_numbers` from a boolean to an enum: `enabled`, `disabled`, or `wrapped`. --- assets/settings/default.json | 2 +- crates/editor/src/display_map/wrap_map.rs | 1 + crates/editor/src/editor.rs | 22 +- crates/editor/src/editor_settings.rs | 4 +- crates/editor/src/element.rs | 312 ++++++++++++------ crates/migrator/src/migrations.rs | 6 + .../src/migrations/m_2025_10_21/settings.rs | 16 + crates/migrator/src/migrator.rs | 1 + crates/multi_buffer/src/multi_buffer.rs | 4 + crates/multi_buffer/src/multi_buffer_tests.rs | 3 + .../settings/src/settings_content/editor.rs | 41 ++- crates/settings/src/vscode_import.rs | 2 +- crates/settings_ui/src/page_data.rs | 2 +- crates/settings_ui/src/settings_ui.rs | 1 + docs/src/configuring-zed.md | 26 +- docs/src/vim.md | 4 +- docs/src/visual-customization.md | 2 +- 17 files changed, 335 insertions(+), 114 deletions(-) create mode 100644 crates/migrator/src/migrations/m_2025_10_21/settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 370124dcf5ac0a362440108e0b473bf2f0c1dfab..ac570edccdd42bcc51932e0de49e23717f52e3c8 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -605,7 +605,7 @@ // to both the horizontal and vertical delta values while scrolling. Fast scrolling // happens when a user holds the alt or option key while scrolling. "fast_scroll_sensitivity": 4.0, - "relative_line_numbers": false, + "relative_line_numbers": "disabled", // If 'search_wrap' is disabled, search result do not wrap around the end of the file. "search_wrap": true, // Search options to enable by default when opening new project and buffer searches. diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 5c71ade6838a9de22859fad92cd547ea8f1ecf67..f8214d9c804906e5760636c2149392ded07fe6ed 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1025,6 +1025,7 @@ impl Iterator for WrapRows<'_> { multibuffer_row: None, diff_status, expand_info: None, + wrapped_buffer_row: buffer_row.buffer_row, } } else { buffer_row diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2d9afd3721a8165caae4cd15a14b2448f1bc7cfa..efd41e7465a0d375d957553d5c4cc283a78db276 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -163,7 +163,10 @@ use rpc::{ErrorCode, ErrorExt, proto::PeerId}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager}; use selections_collection::{MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; -use settings::{GitGutterSetting, Settings, SettingsLocation, SettingsStore, update_settings_file}; +use settings::{ + GitGutterSetting, RelativeLineNumbers, Settings, SettingsLocation, SettingsStore, + update_settings_file, +}; use smallvec::{SmallVec, smallvec}; use snippet::Snippet; use std::{ @@ -19542,9 +19545,16 @@ impl Editor { EditorSettings::get_global(cx).gutter.line_numbers } - pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool { - self.use_relative_line_numbers - .unwrap_or(EditorSettings::get_global(cx).relative_line_numbers) + pub fn relative_line_numbers(&self, cx: &mut App) -> RelativeLineNumbers { + match ( + self.use_relative_line_numbers, + EditorSettings::get_global(cx).relative_line_numbers, + ) { + (None, setting) => setting, + (Some(false), _) => RelativeLineNumbers::Disabled, + (Some(true), RelativeLineNumbers::Wrapped) => RelativeLineNumbers::Wrapped, + (Some(true), _) => RelativeLineNumbers::Enabled, + } } pub fn toggle_relative_line_numbers( @@ -19553,8 +19563,8 @@ impl Editor { _: &mut Window, cx: &mut Context, ) { - let is_relative = self.should_use_relative_line_numbers(cx); - self.set_relative_line_number(Some(!is_relative), cx) + let is_relative = self.relative_line_numbers(cx); + self.set_relative_line_number(Some(!is_relative.enabled()), cx) } pub fn set_relative_line_number(&mut self, is_relative: Option, cx: &mut Context) { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 77c9558eaf4ea49df981b8eb32ee075d069da08f..635bb5f4aca11b3328393b846ca1ea2b3c192899 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -3,12 +3,12 @@ use core::num; use gpui::App; use language::CursorShape; use project::project_settings::DiagnosticSeverity; -use settings::Settings; pub use settings::{ CurrentLineHighlight, DelayMs, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer, GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier, ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder, }; +use settings::{RelativeLineNumbers, Settings}; use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; /// Imports from the VSCode settings at @@ -33,7 +33,7 @@ pub struct EditorSettings { pub horizontal_scroll_margin: f32, pub scroll_sensitivity: f32, pub fast_scroll_sensitivity: f32, - pub relative_line_numbers: bool, + pub relative_line_numbers: RelativeLineNumbers, pub seed_search_query_from_cursor: SeedQuerySetting, pub use_smartcase_search: bool, pub multi_cursor_modifier: MultiCursorModifier, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7fa5daed9c5f5bbc97d39b2e529c39adffdc6eed..a77a868c46a0d907793b00b0acca41651166b5bf 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -766,8 +766,14 @@ impl EditorElement { .row; if line_numbers .get(&MultiBufferRow(multi_buffer_row)) - .and_then(|line_number| line_number.hitbox.as_ref()) - .is_some_and(|hitbox| hitbox.contains(&event.position)) + .is_some_and(|line_layout| { + line_layout.segments.iter().any(|segment| { + segment + .hitbox + .as_ref() + .is_some_and(|hitbox| hitbox.contains(&event.position)) + }) + }) { let line_offset_from_top = display_row - position_map.scroll_position.y as u32; @@ -3157,6 +3163,7 @@ impl EditorElement { 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 { @@ -3174,8 +3181,15 @@ impl EditorElement { 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 buffer_rows[i as usize].buffer_row.is_some() { + if should_count_line(&buffer_rows[i as usize]) { if rows.contains(&DisplayRow(i + start.0)) { relative_rows.insert(DisplayRow(i + start.0), delta); } @@ -3185,13 +3199,13 @@ impl EditorElement { } 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() { + while i > 0 && buffer_rows[i as usize].buffer_row.is_none() && !count_wrapped_lines { i -= 1; } while i > 0 { i -= 1; - if buffer_rows[i as usize].buffer_row.is_some() { + if should_count_line(&buffer_rows[i as usize]) { if rows.contains(&DisplayRow(i + start.0)) { relative_rows.insert(DisplayRow(i + start.0), delta); } @@ -3223,7 +3237,7 @@ impl EditorElement { return Arc::default(); } - let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| { + 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 @@ -3239,79 +3253,93 @@ impl EditorElement { ) .head }); - let is_relative = editor.should_use_relative_line_numbers(cx); - (newest_selection_head, is_relative) + let relative = editor.relative_line_numbers(cx); + (newest_selection_head, relative) }); - let relative_to = if is_relative { + let relative_to = if relative.enabled() { Some(newest_selection_head.row()) } else { None }; - let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to); + let relative_rows = + self.calculate_relative_line_numbers(snapshot, &rows, relative_to, relative.wrapped()); let mut line_number = String::new(); - let line_numbers = buffer_rows - .iter() - .enumerate() - .flat_map(|(ix, row_info)| { - let display_row = DisplayRow(rows.start.0 + ix as u32); - line_number.clear(); - let non_relative_number = row_info.buffer_row? + 1; - let number = relative_rows - .get(&display_row) - .unwrap_or(&non_relative_number); - write!(&mut line_number, "{number}").unwrap(); - if row_info - .diff_status - .is_some_and(|status| status.is_deleted()) - { - return None; - } - - let color = active_rows - .get(&display_row) - .map(|spec| { - if spec.breakpoint { - cx.theme().colors().debugger_accent - } else { - cx.theme().colors().editor_active_line_number - } - }) - .unwrap_or_else(|| cx.theme().colors().editor_line_number); - let shaped_line = - self.shape_line_number(SharedString::from(&line_number), color, window); - let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height); - let line_origin = gutter_hitbox.map(|hitbox| { - hitbox.origin - + point( - hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding, - ix as f32 * line_height - - Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)), - ) - }); + let segments = buffer_rows.iter().enumerate().flat_map(|(ix, row_info)| { + let display_row = DisplayRow(rows.start.0 + ix as u32); + line_number.clear(); + let non_relative_number = if relative.wrapped() { + row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1 + } else { + row_info.buffer_row? + 1 + }; + let number = relative_rows + .get(&display_row) + .unwrap_or(&non_relative_number); + write!(&mut line_number, "{number}").unwrap(); + if row_info + .diff_status + .is_some_and(|status| status.is_deleted()) + { + return None; + } - #[cfg(not(test))] - let hitbox = line_origin.map(|line_origin| { - window.insert_hitbox( - Bounds::new(line_origin, size(shaped_line.width, line_height)), - HitboxBehavior::Normal, + let color = active_rows + .get(&display_row) + .map(|spec| { + if spec.breakpoint { + cx.theme().colors().debugger_accent + } else { + cx.theme().colors().editor_active_line_number + } + }) + .unwrap_or_else(|| cx.theme().colors().editor_line_number); + let shaped_line = + self.shape_line_number(SharedString::from(&line_number), color, window); + let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height); + let line_origin = gutter_hitbox.map(|hitbox| { + hitbox.origin + + point( + hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding, + ix as f32 * line_height + - Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)), ) - }); - #[cfg(test)] - let hitbox = { - let _ = line_origin; - None - }; + }); - let multi_buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row; - let multi_buffer_row = MultiBufferRow(multi_buffer_row); - let line_number = LineNumberLayout { - shaped_line, - hitbox, - }; - Some((multi_buffer_row, line_number)) - }) - .collect(); + #[cfg(not(test))] + let hitbox = line_origin.map(|line_origin| { + window.insert_hitbox( + Bounds::new(line_origin, size(shaped_line.width, line_height)), + HitboxBehavior::Normal, + ) + }); + #[cfg(test)] + let hitbox = { + let _ = line_origin; + None + }; + + let segment = LineNumberSegment { + shaped_line, + hitbox, + }; + + let buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row; + let multi_buffer_row = MultiBufferRow(buffer_row); + + Some((multi_buffer_row, segment)) + }); + + let mut line_numbers: HashMap = HashMap::default(); + for (buffer_row, segment) in segments { + line_numbers + .entry(buffer_row) + .or_insert_with(|| LineNumberLayout { + segments: Default::default(), + }) + .segments + .push(segment); + } Arc::new(line_numbers) } @@ -5852,34 +5880,36 @@ impl EditorElement { let line_height = layout.position_map.line_height; window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox); - for LineNumberLayout { - shaped_line, - hitbox, - } in layout.line_numbers.values() - { - let Some(hitbox) = hitbox else { - continue; - }; + for line_layout in layout.line_numbers.values() { + for LineNumberSegment { + shaped_line, + hitbox, + } in &line_layout.segments + { + let Some(hitbox) = hitbox else { + continue; + }; - let Some(()) = (if !is_singleton && hitbox.is_hovered(window) { - let color = cx.theme().colors().editor_hover_line_number; + let Some(()) = (if !is_singleton && hitbox.is_hovered(window) { + let color = cx.theme().colors().editor_hover_line_number; - let line = self.shape_line_number(shaped_line.text.clone(), color, window); - line.paint(hitbox.origin, line_height, window, cx).log_err() - } else { - shaped_line - .paint(hitbox.origin, line_height, window, cx) - .log_err() - }) else { - continue; - }; + let line = self.shape_line_number(shaped_line.text.clone(), color, window); + line.paint(hitbox.origin, line_height, window, cx).log_err() + } else { + shaped_line + .paint(hitbox.origin, line_height, window, cx) + .log_err() + }) else { + continue; + }; - // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor. - // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor. - if is_singleton { - window.set_cursor_style(CursorStyle::IBeam, hitbox); - } else { - window.set_cursor_style(CursorStyle::PointingHand, hitbox); + // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor. + // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor. + if is_singleton { + window.set_cursor_style(CursorStyle::IBeam, hitbox); + } else { + window.set_cursor_style(CursorStyle::PointingHand, hitbox); + } } } } @@ -9793,11 +9823,17 @@ impl EditorLayout { } } -struct LineNumberLayout { +#[derive(Debug)] +struct LineNumberSegment { shaped_line: ShapedLine, hitbox: Option, } +#[derive(Debug)] +struct LineNumberLayout { + segments: SmallVec<[LineNumberSegment; 1]>, +} + struct ColoredRange { start: T, end: T, @@ -10861,6 +10897,7 @@ mod tests { &snapshot, &(DisplayRow(0)..DisplayRow(6)), Some(DisplayRow(3)), + false, ) }) .unwrap(); @@ -10879,6 +10916,7 @@ mod tests { &snapshot, &(DisplayRow(3)..DisplayRow(6)), Some(DisplayRow(1)), + false, ) }) .unwrap(); @@ -10895,6 +10933,7 @@ mod tests { &snapshot, &(DisplayRow(0)..DisplayRow(3)), Some(DisplayRow(6)), + false, ) }) .unwrap(); @@ -10904,6 +10943,81 @@ mod tests { assert_eq!(relative_rows[&DisplayRow(2)], 3); } + #[gpui::test] + fn test_shape_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); + Editor::new(EditorMode::full(), buffer, None, window, cx) + }); + + update_test_language_settings(cx, |s| { + s.defaults.preferred_line_length = Some(5_u32); + s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); + }); + + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let line_height = window + .update(cx, |_, window, _| { + style.text.line_height_in_pixels(window.rem_size()) + }) + .unwrap(); + let element = EditorElement::new(&editor, style); + let snapshot = window + .update(cx, |editor, window, cx| editor.snapshot(window, cx)) + .unwrap(); + + let layouts = cx + .update_window(*window, |_, window, cx| { + element.layout_line_numbers( + None, + GutterDimensions { + left_padding: Pixels::ZERO, + right_padding: Pixels::ZERO, + width: px(30.0), + margin: Pixels::ZERO, + git_blame_entries_width: None, + }, + line_height, + gpui::Point::default(), + DisplayRow(0)..DisplayRow(6), + &(0..6) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + &BTreeMap::default(), + Some(DisplayPoint::new(DisplayRow(0), 0)), + &snapshot, + window, + cx, + ) + }) + .unwrap(); + assert_eq!(layouts.len(), 3); + + let relative_rows = window + .update(cx, |editor, window, cx| { + let snapshot = editor.snapshot(window, cx); + element.calculate_relative_line_numbers( + &snapshot, + &(DisplayRow(0)..DisplayRow(6)), + Some(DisplayRow(3)), + true, + ) + }) + .unwrap(); + + assert_eq!(relative_rows[&DisplayRow(0)], 3); + assert_eq!(relative_rows[&DisplayRow(1)], 2); + assert_eq!(relative_rows[&DisplayRow(2)], 1); + // current line has no relative number + assert_eq!(relative_rows[&DisplayRow(4)], 1); + assert_eq!(relative_rows[&DisplayRow(5)], 2); + } + #[gpui::test] async fn test_vim_visual_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -11017,7 +11131,13 @@ mod tests { state .line_numbers .get(&MultiBufferRow(0)) - .map(|line_number| line_number.shaped_line.text.as_ref()), + .map(|line_number| line_number + .segments + .first() + .unwrap() + .shaped_line + .text + .as_ref()), Some("1") ); } diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index 084a3348b54acd9d2fc6ba043e1fb1648bbb3f8b..e4358b36b94c9a738ad784eb7269652b29e7cdfb 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -129,3 +129,9 @@ pub(crate) mod m_2025_10_17 { pub(crate) use settings::make_file_finder_include_ignored_an_enum; } + +pub(crate) mod m_2025_10_21 { + mod settings; + + pub(crate) use settings::make_relative_line_numbers_an_enum; +} diff --git a/crates/migrator/src/migrations/m_2025_10_21/settings.rs b/crates/migrator/src/migrations/m_2025_10_21/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..1f78f9332741a50f851006c525863e51abc94784 --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_10_21/settings.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use serde_json::Value; + +pub fn make_relative_line_numbers_an_enum(value: &mut Value) -> Result<()> { + let Some(relative_line_numbers) = value.get_mut("relative_line_numbers") else { + return Ok(()); + }; + + *relative_line_numbers = match relative_line_numbers { + Value::Bool(true) => Value::String("enabled".to_string()), + Value::Bool(false) => Value::String("disabled".to_string()), + Value::String(s) if s == "enabled" || s == "disabled" || s == "wrapped" => return Ok(()), + _ => anyhow::bail!("Expected relative_line_numbers to be a boolean"), + }; + Ok(()) +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index ff9635dcef7664b17eb02a03b7584ea18ac9a91b..3f5c1edaa7939e442c3e5c007579516fcdeb2151 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -214,6 +214,7 @@ pub fn migrate_settings(text: &str) -> Result> { ), MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format), MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum), + MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum), ]; run_migrations(text, migrations) } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index bdfc7a7606a4d6e4a77a74ebb3c42a41449f002e..90f1bcbe39468fcfa390ce8175414451ddb3b2c7 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -340,6 +340,7 @@ pub struct RowInfo { pub multibuffer_row: Option, pub diff_status: Option, pub expand_info: Option, + pub wrapped_buffer_row: Option, } /// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`]. @@ -6650,6 +6651,7 @@ impl Iterator for MultiBufferRows<'_> { multibuffer_row: Some(MultiBufferRow(0)), diff_status: None, expand_info: None, + wrapped_buffer_row: None, }); } @@ -6707,6 +6709,7 @@ impl Iterator for MultiBufferRows<'_> { buffer_row: Some(last_row), multibuffer_row: Some(multibuffer_row), diff_status: None, + wrapped_buffer_row: None, expand_info, }); } else { @@ -6751,6 +6754,7 @@ impl Iterator for MultiBufferRows<'_> { .diff_hunk_status .filter(|_| self.point < region.range.end), expand_info, + wrapped_buffer_row: None, }); self.point += Point::new(1, 0); result diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index a9121b9104400d88d5f22801db1bfebaeeb060d6..22c041267f9c78c1f20609b74e2332516639f39b 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -32,6 +32,7 @@ fn test_empty_singleton(cx: &mut App) { multibuffer_row: Some(MultiBufferRow(0)), diff_status: None, expand_info: None, + wrapped_buffer_row: None, }] ); } @@ -2432,6 +2433,8 @@ impl ReferenceMultibuffer { buffer_id: region.buffer_id, diff_status: region.status, buffer_row, + wrapped_buffer_row: None, + multibuffer_row: Some(MultiBufferRow( text[..ix].matches('\n').count() as u32 )), diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs index a1567f9d0e5b16ed6058cef9ca954c0e842605be..5c33dbc2af48a55e176a5f093afcc83437054932 100644 --- a/crates/settings/src/settings_content/editor.rs +++ b/crates/settings/src/settings_content/editor.rs @@ -97,9 +97,11 @@ pub struct EditorSettingsContent { #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub fast_scroll_sensitivity: Option, /// Whether the line numbers on editors gutter are relative or not. + /// When "enabled" shows relative number of buffer lines, when "wrapped" shows + /// relative number of display lines. /// - /// Default: false - pub relative_line_numbers: Option, + /// Default: "disabled" + pub relative_line_numbers: Option, /// When to populate a new search's query based on the text under the cursor. /// /// Default: always @@ -212,6 +214,41 @@ pub struct EditorSettingsContent { pub completion_menu_scrollbar: Option, } +#[derive( + Debug, + Clone, + Copy, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + PartialEq, + Eq, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum RelativeLineNumbers { + Disabled, + Enabled, + Wrapped, +} + +impl RelativeLineNumbers { + pub fn enabled(&self) -> bool { + match self { + RelativeLineNumbers::Enabled | RelativeLineNumbers::Wrapped => true, + RelativeLineNumbers::Disabled => false, + } + } + pub fn wrapped(&self) -> bool { + match self { + RelativeLineNumbers::Enabled | RelativeLineNumbers::Disabled => false, + RelativeLineNumbers::Wrapped => true, + } + } +} + // Toolbar related settings #[skip_serializing_none] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index fe66e6b686a63ca4bd4ab965b8ed9f1325023d82..a8fd15c32acc130a9cde4948cc2aa66f898708d0 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -275,7 +275,7 @@ impl VsCodeSettings { }), redact_private_values: None, relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s { - "relative" => Some(true), + "relative" => Some(RelativeLineNumbers::Enabled), _ => None, }), rounded_selection: self.read_bool("editor.roundedSelection"), diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 0bb1844c31847507f28e6cc7f5c12c1d31f20fdd..98db1a7efee6b333d258f3db29142532c514aca3 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -1506,7 +1506,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { }), SettingsPageItem::SettingItem(SettingItem { title: "Relative Line Numbers", - description: "Whether the line numbers in the editor's gutter are relative or not.", + description: "Controls line number display in the editor's gutter. \"disabled\" shows absolute line numbers, \"enabled\" shows relative line numbers for each absolute line, and \"wrapped\" shows relative line numbers for every line, absolute or wrapped.", field: Box::new(SettingField { json_path: Some("relative_line_numbers"), pick: |settings_content| { diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index f78d7cd41a137d01fee09b5ef7dbee669c2d7e54..fbe9d86c5d7c8989653d657ed1df1ab0684882a4 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -497,6 +497,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) // please semicolon stay on next line ; } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 77906a83a499a2e09ebbb93842545075bd46b8a7..119cb17ad907620bc22fdde6090d6d2addb47330 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2994,11 +2994,33 @@ List of `string` glob patterns - Description: Whether to show relative line numbers in the gutter - Setting: `relative_line_numbers` -- Default: `false` +- Default: `"disabled"` **Options** -`boolean` values +1. Show relative line numbers in the gutter whilst counting wrapped lines as one line: + +```json [settings] +{ + "relative_line_numbers": "enabled" +} +``` + +2. Show relative line numbers in the gutter, including wrapped lines in the counting: + +```json [settings] +{ + "relative_line_numbers": "wrapped" +} +``` + +2. Do not use relative line numbers: + +```json [settings] +{ + "relative_line_numbers": "disabled" +} +``` ## Remove Trailing Whitespace On Save diff --git a/docs/src/vim.md b/docs/src/vim.md index b1c1e7a5853ef75f78612e06f437c3fa8aa6260f..c9a0cd09f2dafb9f07a26ef07b71205f5ddbdf15 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -606,7 +606,7 @@ Here are a few general Zed settings that can help you fine-tune your Vim experie | Property | Description | Default Value | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | cursor_blink | If `true`, the cursor blinks. | `true` | -| relative_line_numbers | If `true`, line numbers in the left gutter are relative to the cursor. | `false` | +| relative_line_numbers | If `"enabled"`, line numbers in the left gutter are relative to the cursor. If `"wrapped"`, they also display for wrapped lines. | `"disabled"` | | scrollbar | Object that controls the scrollbar display. Set to `{ "show": "never" }` to hide the scroll bar. | `{ "show": "auto" }` | | scroll_beyond_last_line | If set to `"one_page"`, allows scrolling up to one page beyond the last line. Set to `"off"` to prevent this behavior. | `"one_page"` | | vertical_scroll_margin | The number of lines to keep above or below the cursor when scrolling. Set to `0` to allow the cursor to go up to the edges of the screen vertically. | `3` | @@ -620,7 +620,7 @@ Here's an example of these settings changed: // Disable cursor blink "cursor_blink": false, // Use relative line numbers - "relative_line_numbers": true, + "relative_line_numbers": "enabled", // Hide the scroll bar "scrollbar": { "show": "never" }, // Prevent the buffer from scrolling beyond the last line diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index a5c4ccf8490458ff671fdc3425f480471df077a9..1f80dd61f542aefd38a2c291d0cdcce3534ea050 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -209,7 +209,7 @@ TBD: Centered layout related settings "folds": true, // Show/hide show fold buttons in the gutter. "min_line_number_digits": 4 // Reserve space for N digit line numbers }, - "relative_line_numbers": false, // Show relative line numbers in gutter + "relative_line_numbers": "enabled", // Show relative line numbers in gutter // Indent guides "indent_guides": {