From e64985e5162339a0b4000fb3708b212592ae6eb3 Mon Sep 17 00:00:00 2001 From: Kalaay Date: Mon, 26 Jan 2026 21:51:11 -0800 Subject: [PATCH] Add optional relative line jumps to go-to-line action (#46932) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements relative line jumping, a common feature i missed from Vim/Neovim where you can jump to lines relative to your current cursor position. - New `{"relative": true}` flag for `go_to_line::Toggle` action - Supports both `-5` (5 lines up) and `b5` (backward 5 lines) syntax - Full lines only (no column support in relative mode, wasnt sure if that would be necessary) - Unbound by default - users can add e.g. `"ctrl-j": ["go_to_line::Toggle", {"relative": true}]` to their keymap Example usage: - `5` → jump 5 lines down - `-3` or `b3` → jump 3 lines up - `0` → stay on current line [Screencast_20260115_191312.webm](https://github.com/user-attachments/assets/395c0e7b-8ac1-48c8-a39e-5ade4f9206ec) Release Notes: - Added relative line jump support to go-to-line action via `+/-/f/b/F/B` --- crates/go_to_line/src/go_to_line.rs | 68 ++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 7c42972a75420ae87bf3c5b9caaf041852efc009..9afe95d9f67be37b59f794a230d6afa07cadfdec 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -26,6 +26,7 @@ pub struct GoToLine { active_editor: Entity, current_text: SharedString, prev_scroll_position: Option>, + current_line: u32, _subscriptions: Vec, } @@ -143,6 +144,7 @@ impl GoToLine { active_editor, current_text: current_text.into(), prev_scroll_position: Some(scroll_position), + current_line: line, _subscriptions: vec![line_editor_change, cx.on_release_in(window, Self::release)], } } @@ -210,7 +212,17 @@ impl GoToLine { snapshot: &MultiBufferSnapshot, cx: &Context, ) -> Option { - let (query_row, query_char) = self.line_and_char_from_query(cx)?; + let (query_row, query_char) = if let Some(offset) = self.relative_line_from_query(cx) { + let target = if offset >= 0 { + self.current_line.saturating_add(offset as u32) + } else { + self.current_line.saturating_sub(offset.unsigned_abs()) + }; + (target, None) + } else { + self.line_and_char_from_query(cx)? + }; + let row = query_row.saturating_sub(1); let character = query_char.unwrap_or(0).saturating_sub(1); @@ -241,6 +253,41 @@ impl GoToLine { Some(snapshot.anchor_before(snapshot.clip_offset(end_offset, Bias::Left))) } + fn relative_line_from_query(&self, cx: &App) -> Option { + let input = self.line_editor.read(cx).text(cx); + let trimmed = input.trim(); + + let mut last_direction_char: Option = None; + let mut number_start_index = 0; + + for (i, c) in trimmed.char_indices() { + match c { + '+' | 'f' | 'F' | '-' | 'b' | 'B' => { + last_direction_char = Some(c); + number_start_index = i + c.len_utf8(); + } + _ => break, + } + } + + let direction = last_direction_char?; + + let number_part = &trimmed[number_start_index..]; + let line_part = number_part + .split(FILE_ROW_COLUMN_DELIMITER) + .next() + .unwrap_or(number_part) + .trim(); + + let value = line_part.parse::().ok()?; + + match direction { + '+' | 'f' | 'F' => Some(value as i32), + '-' | 'b' | 'B' => Some(-(value as i32)), + _ => None, + } + } + fn line_and_char_from_query(&self, cx: &App) -> Option<(u32, Option)> { let input = self.line_editor.read(cx).text(cx); let mut components = input @@ -279,12 +326,21 @@ impl GoToLine { impl Render for GoToLine { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let help_text = match self.line_and_char_from_query(cx) { - Some((line, Some(character))) => { - format!("Go to line {line}, character {character}").into() + let help_text = if let Some(offset) = self.relative_line_from_query(cx) { + let target_line = if offset >= 0 { + self.current_line.saturating_add(offset as u32) + } else { + self.current_line.saturating_sub(offset.unsigned_abs()) + }; + format!("Go to line {target_line} ({offset:+} from current)").into() + } else { + match self.line_and_char_from_query(cx) { + Some((line, Some(character))) => { + format!("Go to line {line}, character {character}").into() + } + Some((line, None)) => format!("Go to line {line}").into(), + None => self.current_text.clone(), } - Some((line, None)) => format!("Go to line {line}").into(), - None => self.current_text.clone(), }; v_flex()