Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
* use [tombi](https://github.com/tombi-toml/tombi) for all toml file formatting
* open the external editor from the status diff view [[@WaterWhisperer](https://github.com/WaterWhisperer)] ([#2805](https://github.com/gitui-org/gitui/issues/2805))
* open external editor at the cursor line from any diff view (status, log, blame), with editor-specific goto syntax (Helix, VS Code, vi-style) [@paaloeye](https://github.com/paaloeye) ([#2952](https://github.com/gitui-org/gitui/pull/2952))

### Fixes
* crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895))
Expand Down
6 changes: 5 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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) => {
Expand Down
119 changes: 109 additions & 10 deletions src/components/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ impl DiffComponent {
}
///
const fn can_edit_file(&self) -> bool {
!self.is_immutable && !self.current.path.is_empty()
!self.current.path.is_empty()
}
///
pub fn clear(&mut self, pending: bool) {
Expand Down Expand Up @@ -773,12 +773,13 @@ impl Component for DiffComponent {
.hidden(),
);

out.push(CommandInfo::new(
strings::commands::edit_item(&self.key_config),
self.can_edit_file(),
self.focused() && self.can_edit_file(),
));

if !self.is_immutable {
out.push(CommandInfo::new(
strings::commands::edit_item(&self.key_config),
self.can_edit_file(),
self.focused() && self.can_edit_file(),
));
out.push(CommandInfo::new(
strings::commands::diff_hunk_remove(&self.key_config),
self.selected_hunk.is_some(),
Expand Down Expand Up @@ -888,10 +889,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(
Expand Down Expand Up @@ -1055,8 +1078,84 @@ 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"
));
}

#[test]
fn diff_component_opens_editor_at_cursor_line() {
use asyncgit::sync::diff::{DiffLinePosition, Hunk};
use asyncgit::FileDiff;

let env = Environment::test_env();
let mut comp = DiffComponent::new(&env, false);
comp.focus(true);
comp.current.path = String::from("src/main.rs");

// build a minimal FileDiff: one hunk with a header line and
// two content lines at known new_lineno values
let hunk = Hunk {
header_hash: 0,
lines: vec![
DiffLine {
content: "@@ -1,2 +1,2 @@".into(),
line_type: DiffLineType::Header,
position: DiffLinePosition {
old_lineno: None,
new_lineno: None,
},
},
DiffLine {
content: "context".into(),
line_type: DiffLineType::None,
position: DiffLinePosition {
old_lineno: Some(1),
new_lineno: Some(1),
},
},
DiffLine {
content: "added line".into(),
line_type: DiffLineType::Add,
position: DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
},
],
};
let file_diff = FileDiff {
hunks: vec![hunk],
lines: 3,
untracked: false,
sizes: (0, 0),
size_delta: 0,
};
comp.update(String::from("src/main.rs"), false, file_diff);

// move cursor to the Add line (index 2 in the flat list)
comp.move_selection(ScrollType::Down);
comp.move_selection(ScrollType::Down);

let event = Event::Key(KeyEvent::new(
KeyCode::Char('e'),
KeyModifiers::empty(),
));
assert!(matches!(
comp.event(&event).unwrap(),
EventState::Consumed
));

let queued = env.queue.pop();
assert!(
matches!(
queued,
Some(InternalEvent::OpenExternalEditor(
Some(_),
Some(2)
))
),
"expected OpenExternalEditor with line Some(2)"
);
}
}
5 changes: 4 additions & 1 deletion src/components/revision_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 4 additions & 3 deletions src/components/status_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/popups/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ impl CommitPopup {
ExternalEditorPopup::open_file_in_editor(
&self.repo.borrow(),
&file_path,
None,
)?;

let mut message = String::new();
Expand Down Expand Up @@ -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
Expand Down
52 changes: 50 additions & 2 deletions src/popups/externaleditor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)?;

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub enum InternalEvent {
///
SelectBranch,
///
OpenExternalEditor(Option<String>),
OpenExternalEditor(Option<String>, Option<u32>),
///
Push(String, PushType, bool, bool),
///
Expand Down