mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28: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"
|
// "Flags"
|
||||||
requires_redraw: Cell<bool>,
|
requires_redraw: Cell<bool>,
|
||||||
file_to_open: Option<String>,
|
file_to_open: Option<String>,
|
||||||
|
line_to_open: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Environment {
|
pub struct Environment {
|
||||||
|
|
@ -244,6 +245,7 @@ impl App {
|
||||||
key_config: env.key_config,
|
key_config: env.key_config,
|
||||||
requires_redraw: Cell::new(false),
|
requires_redraw: Cell::new(false),
|
||||||
file_to_open: None,
|
file_to_open: None,
|
||||||
|
line_to_open: None,
|
||||||
repo: env.repo,
|
repo: env.repo,
|
||||||
repo_path_text,
|
repo_path_text,
|
||||||
popup_stack: PopupStack::default(),
|
popup_stack: PopupStack::default(),
|
||||||
|
|
@ -376,6 +378,7 @@ impl App {
|
||||||
ExternalEditorPopup::open_file_in_editor(
|
ExternalEditorPopup::open_file_in_editor(
|
||||||
&self.repo.borrow(),
|
&self.repo.borrow(),
|
||||||
Path::new(&path),
|
Path::new(&path),
|
||||||
|
self.line_to_open.take(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let changes =
|
let changes =
|
||||||
|
|
@ -815,10 +818,11 @@ impl App {
|
||||||
flags.insert(NeedsUpdate::ALL);
|
flags.insert(NeedsUpdate::ALL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InternalEvent::OpenExternalEditor(path) => {
|
InternalEvent::OpenExternalEditor(path, line) => {
|
||||||
self.input.set_polling(false);
|
self.input.set_polling(false);
|
||||||
self.external_editor_popup.show()?;
|
self.external_editor_popup.show()?;
|
||||||
self.file_to_open = path;
|
self.file_to_open = path;
|
||||||
|
self.line_to_open = line;
|
||||||
flags.insert(NeedsUpdate::COMMANDS);
|
flags.insert(NeedsUpdate::COMMANDS);
|
||||||
}
|
}
|
||||||
InternalEvent::Push(branch, push_type, force, delete) => {
|
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)
|
} else if key_match(e, self.key_config.keys.edit_file)
|
||||||
&& self.can_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(
|
self.queue.push(
|
||||||
InternalEvent::OpenExternalEditor(Some(
|
InternalEvent::OpenExternalEditor(
|
||||||
self.current.path.clone(),
|
Some(self.current.path.clone()),
|
||||||
)),
|
line,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
Ok(EventState::Consumed)
|
Ok(EventState::Consumed)
|
||||||
} else if key_match(
|
} else if key_match(
|
||||||
|
|
@ -1055,7 +1077,7 @@ mod tests {
|
||||||
let event = env.queue.pop();
|
let event = env.queue.pop();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
event,
|
event,
|
||||||
Some(InternalEvent::OpenExternalEditor(Some(path)))
|
Some(InternalEvent::OpenExternalEditor(Some(path), _))
|
||||||
if path == "src/main.rs"
|
if path == "src/main.rs"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -524,7 +524,10 @@ impl Component for RevisionFilesComponent {
|
||||||
// not altering a file inside a revision here
|
// not altering a file inside a revision here
|
||||||
self.queue.push(InternalEvent::TabSwitchStatus);
|
self.queue.push(InternalEvent::TabSwitchStatus);
|
||||||
self.queue.push(
|
self.queue.push(
|
||||||
InternalEvent::OpenExternalEditor(Some(file)),
|
InternalEvent::OpenExternalEditor(
|
||||||
|
Some(file),
|
||||||
|
None,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return Ok(EventState::Consumed);
|
return Ok(EventState::Consumed);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -509,9 +509,10 @@ impl Component for StatusTreeComponent {
|
||||||
{
|
{
|
||||||
if let Some(status_item) = self.selection_file() {
|
if let Some(status_item) = self.selection_file() {
|
||||||
self.queue.push(
|
self.queue.push(
|
||||||
InternalEvent::OpenExternalEditor(Some(
|
InternalEvent::OpenExternalEditor(
|
||||||
status_item.path,
|
Some(status_item.path),
|
||||||
)),
|
None,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(EventState::Consumed)
|
Ok(EventState::Consumed)
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ impl CommitPopup {
|
||||||
ExternalEditorPopup::open_file_in_editor(
|
ExternalEditorPopup::open_file_in_editor(
|
||||||
&self.repo.borrow(),
|
&self.repo.borrow(),
|
||||||
&file_path,
|
&file_path,
|
||||||
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut message = String::new();
|
let mut message = String::new();
|
||||||
|
|
@ -587,7 +588,9 @@ impl Component for CommitPopup {
|
||||||
self.key_config.keys.open_commit_editor,
|
self.key_config.keys.open_commit_editor,
|
||||||
) {
|
) {
|
||||||
self.queue.push(
|
self.queue.push(
|
||||||
InternalEvent::OpenExternalEditor(None),
|
InternalEvent::OpenExternalEditor(
|
||||||
|
None, None,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
self.hide();
|
self.hide();
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,28 @@ use scopeguard::defer;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::{env, io, path::Path, process::Command};
|
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 {
|
pub struct ExternalEditorPopup {
|
||||||
visible: bool,
|
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(
|
pub fn open_file_in_editor(
|
||||||
repo: &RepoPath,
|
repo: &RepoPath,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
line: Option<u32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let work_dir = repo_work_dir(repo)?;
|
let work_dir = repo_work_dir(repo)?;
|
||||||
|
|
||||||
|
|
@ -107,7 +130,32 @@ impl ExternalEditorPopup {
|
||||||
let mut args: Vec<&OsStr> =
|
let mut args: Vec<&OsStr> =
|
||||||
remainder.map(OsStr::new).collect();
|
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())
|
Command::new(command.clone())
|
||||||
.current_dir(work_dir)
|
.current_dir(work_dir)
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ pub enum InternalEvent {
|
||||||
///
|
///
|
||||||
SelectBranch,
|
SelectBranch,
|
||||||
///
|
///
|
||||||
OpenExternalEditor(Option<String>),
|
OpenExternalEditor(Option<String>, Option<u32>),
|
||||||
///
|
///
|
||||||
Push(String, PushType, bool, bool),
|
Push(String, PushType, bool, bool),
|
||||||
///
|
///
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue