mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00:00
feat(diff): open editor at cursor line
- 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 <noreply@anthropic.com> Co-Authored-By: Claude Code 2.1.143 (Claude Code) Signed-Off-By: Paal Øye-Strømme <paal.o.eye@gmail.com>
This commit is contained in:
parent
8619c07f3f
commit
5e304f6ea5
7 changed files with 94 additions and 13 deletions
|
|
@ -119,6 +119,7 @@ pub struct App {
|
|||
// "Flags"
|
||||
requires_redraw: Cell<bool>,
|
||||
file_to_open: Option<String>,
|
||||
line_to_open: Option<u32>,
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<u32>) -> 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<u32>,
|
||||
) -> 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)
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ pub enum InternalEvent {
|
|||
///
|
||||
SelectBranch,
|
||||
///
|
||||
OpenExternalEditor(Option<String>),
|
||||
OpenExternalEditor(Option<String>, Option<u32>),
|
||||
///
|
||||
Push(String, PushType, bool, bool),
|
||||
///
|
||||
|
|
|
|||
Loading…
Reference in a new issue