From 5e304f6ea5f12f6064a9427d2cc0b4aebebfc5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paal=20=C3=98ye-Str=C3=B8mme?= Date: Sun, 17 May 2026 18:42:20 +0200 Subject: [PATCH] feat(diff): open editor at cursor line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - extend OpenExternalEditor event to carry an optional line number - use the selection cursor index into the flat line list to find the exact line, using the same index space as selected_lines() - add editor_goto_style() to map editor name to the correct goto-line argument style (hx: file:line, code: --goto file:line, vi/vim/nvim/emacs/nano/etc: +line file) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Sonnet 4.6 Co-Authored-By: Claude Code 2.1.143 (Claude Code) Signed-Off-By: Paal Øye-Strømme --- src/app.rs | 6 +++- src/components/diff.rs | 30 +++++++++++++++--- src/components/revision_files.rs | 5 ++- src/components/status_tree.rs | 7 +++-- src/popups/commit.rs | 5 ++- src/popups/externaleditor.rs | 52 ++++++++++++++++++++++++++++++-- src/queue.rs | 2 +- 7 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8626aa3b..99acba80 100644 --- a/src/app.rs +++ b/src/app.rs @@ -119,6 +119,7 @@ pub struct App { // "Flags" requires_redraw: Cell, file_to_open: Option, + line_to_open: Option, } pub struct Environment { @@ -244,6 +245,7 @@ impl App { key_config: env.key_config, requires_redraw: Cell::new(false), file_to_open: None, + line_to_open: None, repo: env.repo, repo_path_text, popup_stack: PopupStack::default(), @@ -376,6 +378,7 @@ impl App { ExternalEditorPopup::open_file_in_editor( &self.repo.borrow(), Path::new(&path), + self.line_to_open.take(), ) } else { let changes = @@ -815,10 +818,11 @@ impl App { flags.insert(NeedsUpdate::ALL); } } - InternalEvent::OpenExternalEditor(path) => { + InternalEvent::OpenExternalEditor(path, line) => { self.input.set_polling(false); self.external_editor_popup.show()?; self.file_to_open = path; + self.line_to_open = line; flags.insert(NeedsUpdate::COMMANDS); } InternalEvent::Push(branch, push_type, force, delete) => { diff --git a/src/components/diff.rs b/src/components/diff.rs index 04779caa..7c5b2376 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -888,10 +888,32 @@ impl Component for DiffComponent { } else if key_match(e, self.key_config.keys.edit_file) && self.can_edit_file() { + let line = self.diff.as_ref().and_then(|d| { + let cursor = self.selection.get_start(); + // walk the flat line list to the cursor position, + // same index space as selected_lines() + d.hunks + .iter() + .flat_map(|h| h.lines.iter()) + .nth(cursor) + .and_then(|l| l.position.new_lineno) + .or_else(|| { + // cursor is on a deletion — use old_lineno + // as a best-effort fallback + d.hunks + .iter() + .flat_map(|h| h.lines.iter()) + .nth(cursor) + .and_then(|l| { + l.position.old_lineno + }) + }) + }); self.queue.push( - InternalEvent::OpenExternalEditor(Some( - self.current.path.clone(), - )), + InternalEvent::OpenExternalEditor( + Some(self.current.path.clone()), + line, + ), ); Ok(EventState::Consumed) } else if key_match( @@ -1055,7 +1077,7 @@ mod tests { let event = env.queue.pop(); assert!(matches!( event, - Some(InternalEvent::OpenExternalEditor(Some(path))) + Some(InternalEvent::OpenExternalEditor(Some(path), _)) if path == "src/main.rs" )); } diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index 1e15ec08..28ece1a0 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -524,7 +524,10 @@ impl Component for RevisionFilesComponent { // not altering a file inside a revision here self.queue.push(InternalEvent::TabSwitchStatus); self.queue.push( - InternalEvent::OpenExternalEditor(Some(file)), + InternalEvent::OpenExternalEditor( + Some(file), + None, + ), ); return Ok(EventState::Consumed); } diff --git a/src/components/status_tree.rs b/src/components/status_tree.rs index ac4fc9f6..3a6c2ab7 100644 --- a/src/components/status_tree.rs +++ b/src/components/status_tree.rs @@ -509,9 +509,10 @@ impl Component for StatusTreeComponent { { if let Some(status_item) = self.selection_file() { self.queue.push( - InternalEvent::OpenExternalEditor(Some( - status_item.path, - )), + InternalEvent::OpenExternalEditor( + Some(status_item.path), + None, + ), ); } Ok(EventState::Consumed) diff --git a/src/popups/commit.rs b/src/popups/commit.rs index b5dff767..da0f8bff 100644 --- a/src/popups/commit.rs +++ b/src/popups/commit.rs @@ -189,6 +189,7 @@ impl CommitPopup { ExternalEditorPopup::open_file_in_editor( &self.repo.borrow(), &file_path, + None, )?; let mut message = String::new(); @@ -587,7 +588,9 @@ impl Component for CommitPopup { self.key_config.keys.open_commit_editor, ) { self.queue.push( - InternalEvent::OpenExternalEditor(None), + InternalEvent::OpenExternalEditor( + None, None, + ), ); self.hide(); true diff --git a/src/popups/externaleditor.rs b/src/popups/externaleditor.rs index 52a7327b..cd699d05 100644 --- a/src/popups/externaleditor.rs +++ b/src/popups/externaleditor.rs @@ -27,6 +27,28 @@ use scopeguard::defer; use std::ffi::OsStr; use std::{env, io, path::Path, process::Command}; +enum EditorGoto { + None, + PlusLine(u32), + FileColon(u32), + GotoFlag(u32), +} + +fn editor_goto_style(command: &str, line: Option) -> EditorGoto { + let Some(ln) = line else { + return EditorGoto::None; + }; + let cmd = command.to_lowercase(); + if cmd.ends_with("hx") { + EditorGoto::FileColon(ln) + } else if cmd.ends_with("code") || cmd.ends_with("code-insiders") + { + EditorGoto::GotoFlag(ln) + } else { + EditorGoto::PlusLine(ln) + } +} + /// pub struct ExternalEditorPopup { visible: bool, @@ -44,10 +66,11 @@ impl ExternalEditorPopup { } } - /// opens file at given `path` in an available editor + /// opens file at given `path` in an available editor, optionally at `line` pub fn open_file_in_editor( repo: &RepoPath, path: &Path, + line: Option, ) -> Result<()> { let work_dir = repo_work_dir(repo)?; @@ -107,7 +130,32 @@ impl ExternalEditorPopup { let mut args: Vec<&OsStr> = remainder.map(OsStr::new).collect(); - args.push(path.as_os_str()); + let line_arg: String; + let helix_path: std::path::PathBuf; + match editor_goto_style(&command, line) { + EditorGoto::None => { + args.push(path.as_os_str()); + } + EditorGoto::PlusLine(ln) => { + line_arg = format!("+{ln}"); + args.push(OsStr::new(&line_arg)); + args.push(path.as_os_str()); + } + EditorGoto::FileColon(ln) => { + helix_path = path.with_file_name(format!( + "{}:{ln}", + path.file_name() + .unwrap_or_default() + .to_string_lossy(), + )); + args.push(helix_path.as_os_str()); + } + EditorGoto::GotoFlag(ln) => { + args.push(OsStr::new("--goto")); + line_arg = format!("{}:{ln}", path.to_string_lossy()); + args.push(OsStr::new(&line_arg)); + } + } Command::new(command.clone()) .current_dir(work_dir) diff --git a/src/queue.rs b/src/queue.rs index 5cdfe3ce..c49e0ad5 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -121,7 +121,7 @@ pub enum InternalEvent { /// SelectBranch, /// - OpenExternalEditor(Option), + OpenExternalEditor(Option, Option), /// Push(String, PushType, bool, bool), ///