mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00:00
Merge 9026941a66 into 8619c07f3f
This commit is contained in:
commit
db99af8449
8 changed files with 178 additions and 19 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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(),
|
||||
|
|
@ -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(
|
||||
|
|
@ -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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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