@@ -94,6 +94,8 @@ enum TextInputAction { |
| 94 | 94 | GitCommit, |
| 95 | 95 | /// Create a git tag |
| 96 | 96 | GitTag, |
| 97 | + /// Go to line (and optionally column) |
| 98 | + GotoLine, |
| 97 | 99 | } |
| 98 | 100 | |
| 99 | 101 | /// LSP UI state |
@@ -1534,6 +1536,9 @@ impl Editor { |
| 1534 | 1536 | // === File operations === |
| 1535 | 1537 | // Open file browser (Fortress mode): Ctrl+O |
| 1536 | 1538 | (Key::Char('o'), Modifiers { ctrl: true, .. }) => self.open_fortress(), |
| 1539 | + // Go to line: Ctrl+G or F5 |
| 1540 | + (Key::Char('g'), Modifiers { ctrl: true, .. }) | |
| 1541 | + (Key::F(5), _) => self.open_goto_line(), |
| 1537 | 1542 | |
| 1538 | 1543 | // === Editing === |
| 1539 | 1544 | (Key::Char(c), Modifiers { ctrl: false, alt: false, .. }) => { |
@@ -3970,7 +3975,74 @@ impl Editor { |
| 3970 | 3975 | let (_, msg) = self.workspace.fuss.git_tag(buffer); |
| 3971 | 3976 | self.message = Some(msg); |
| 3972 | 3977 | } |
| 3978 | + TextInputAction::GotoLine => { |
| 3979 | + self.goto_line_col(buffer); |
| 3980 | + } |
| 3981 | + } |
| 3982 | + } |
| 3983 | + |
| 3984 | + /// Open the goto line prompt |
| 3985 | + fn open_goto_line(&mut self) { |
| 3986 | + self.prompt = PromptState::TextInput { |
| 3987 | + label: "Go to line: ".to_string(), |
| 3988 | + buffer: String::new(), |
| 3989 | + action: TextInputAction::GotoLine, |
| 3990 | + }; |
| 3991 | + self.message = Some("Go to line: ".to_string()); |
| 3992 | + } |
| 3993 | + |
| 3994 | + /// Parse line:col input and jump to position |
| 3995 | + fn goto_line_col(&mut self, input: &str) { |
| 3996 | + let input = input.trim(); |
| 3997 | + if input.is_empty() { |
| 3998 | + return; |
| 3973 | 3999 | } |
| 4000 | + |
| 4001 | + // Parse formats: "line", "line:", "line:col" |
| 4002 | + let (line_str, col_str) = if let Some(colon_pos) = input.find(':') { |
| 4003 | + (&input[..colon_pos], &input[colon_pos + 1..]) |
| 4004 | + } else { |
| 4005 | + (input, "") |
| 4006 | + }; |
| 4007 | + |
| 4008 | + let line: usize = match line_str.parse::<usize>() { |
| 4009 | + Ok(n) if n > 0 => n - 1, // Convert to 0-indexed |
| 4010 | + Ok(_) => { |
| 4011 | + self.message = Some("Invalid line number".to_string()); |
| 4012 | + return; |
| 4013 | + } |
| 4014 | + Err(_) => { |
| 4015 | + self.message = Some("Invalid line number".to_string()); |
| 4016 | + return; |
| 4017 | + } |
| 4018 | + }; |
| 4019 | + |
| 4020 | + let col: usize = if col_str.is_empty() { |
| 4021 | + 0 |
| 4022 | + } else { |
| 4023 | + match col_str.parse::<usize>() { |
| 4024 | + Ok(n) if n > 0 => n - 1, // Convert to 0-indexed |
| 4025 | + Ok(_) => 0, |
| 4026 | + Err(_) => 0, |
| 4027 | + } |
| 4028 | + }; |
| 4029 | + |
| 4030 | + // Clamp to buffer bounds |
| 4031 | + let line_count = self.buffer().line_count(); |
| 4032 | + let line = line.min(line_count.saturating_sub(1)); |
| 4033 | + let line_len = self.buffer().line_len(line); |
| 4034 | + let col = col.min(line_len); |
| 4035 | + |
| 4036 | + // Move cursor |
| 4037 | + self.cursor_mut().line = line; |
| 4038 | + self.cursor_mut().col = col; |
| 4039 | + self.cursor_mut().desired_col = col; |
| 4040 | + self.cursor_mut().clear_selection(); |
| 4041 | + |
| 4042 | + // Center the view on the target line |
| 4043 | + self.scroll_to_cursor(); |
| 4044 | + |
| 4045 | + self.message = Some(format!("Line {}, Column {}", line + 1, col + 1)); |
| 3974 | 4046 | } |
| 3975 | 4047 | |
| 3976 | 4048 | fn restore_backups(&mut self) -> Result<()> { |