diff --git a/Cargo.toml b/Cargo.toml index 7063d95ddb..5235e177c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ syntect = { version = "5.2", default-features = false, features = [ "default-themes", "html", ] } +tempfile = "3" tui-textarea = "0.6" two-face = { version = "0.4.0", default-features = false } unicode-segmentation = "1.12" @@ -67,7 +68,6 @@ which = "6.0" [dev-dependencies] env_logger = "0.11" pretty_assertions = "1.4" -tempfile = "3" [build-dependencies] chrono = { version = "0.4", default-features = false, features = ["clock"] } diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index 4c61179e77..25673e6faa 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -17,7 +17,8 @@ use anyhow::Result; use asyncgit::{ asyncjob::AsyncSingleJob, sync::{ - get_commit_info, CommitId, CommitInfo, RepoPathRef, TreeFile, + get_commit_info, get_head, tree_file_content, CommitId, + CommitInfo, RepoPathRef, TreeFile, }, AsyncGitNotification, AsyncTreeFilesJob, }; @@ -251,24 +252,62 @@ impl RevisionFilesComponent { }) } - fn selection_changed(&mut self) { + fn selected_tree_file(&self) -> Option<&TreeFile> { //TODO: retrieve TreeFile from tree datastructure - if let Some(file) = self.selected_file_path_with_prefix() { - if let Some(files) = &self.files { - let path = Path::new(&file); - if let Some(item) = - files.iter().find(|f| f.path == path) + self.selected_file_path_with_prefix().and_then(|file| { + let path = Path::new(&file); + self.files.as_ref().and_then(|files| { + files.iter().find(|f| f.path == path) + }) + }) + } + + fn selection_changed(&mut self) { + if let Some(tree_file) = self.selected_tree_file().cloned() { + if let Ok(path) = tree_file.path.strip_prefix("./") { + return self.current_file.load_file( + path.to_string_lossy().to_string(), + &tree_file, + ); + } + self.current_file.clear(); + } + } + + fn dump_selected_file_content_to_tempfile( + &self, + ) -> Option { + if let Some(rev) = self.revision() { + if let Some(file) = self.selected_tree_file() { + if let Ok(content) = + tree_file_content(&self.repo.borrow(), file) { - if let Ok(path) = path.strip_prefix("./") { - return self.current_file.load_file( - path.to_string_lossy().to_string(), - item, - ); - } + let temp_dir = tempfile::Builder::new() + .prefix(&rev.id.to_string()) + .keep(true) + .tempdir() + .ok()?; + + let file_name = file.path.file_name()?; + let file_path = temp_dir.path().join(file_name); + std::fs::File::create(&file_path).ok()?; + std::fs::write(&file_path, content).ok()?; + + let mut perms = std::fs::metadata(&file_path) + .ok()? + .permissions(); + perms.set_readonly(true); + std::fs::set_permissions(&file_path, perms) + .ok()?; + + return Some( + file_path.to_string_lossy().to_string(), + ); } - self.current_file.clear(); } } + + None } fn draw_tree(&self, f: &mut Frame, area: Rect) -> Result<()> { @@ -376,6 +415,12 @@ impl RevisionFilesComponent { commit, )); } + + fn is_head(&self) -> bool { + let head = get_head(&self.repo.borrow()).ok(); + let commit_id = self.revision().map(|rev| rev.id); + commit_id.is_some() && commit_id == head + } } impl DrawableComponent for RevisionFilesComponent { @@ -413,6 +458,8 @@ impl Component for RevisionFilesComponent { let is_tree_focused = matches!(self.focus, Focus::Tree); if is_tree_focused || force_all { + let is_head = self.is_head(); + out.push( CommandInfo::new( strings::commands::blame_file(&self.key_config), @@ -423,7 +470,12 @@ impl Component for RevisionFilesComponent { ); out.push(CommandInfo::new( strings::commands::edit_item(&self.key_config), - self.tree.selected_file().is_some(), + self.tree.selected_file().is_some() && is_head, + true, + )); + out.push(CommandInfo::new( + strings::commands::open_item(&self.key_config), + self.tree.selected_file().is_some() && !is_head, true, )); out.push( @@ -462,6 +514,8 @@ impl Component for RevisionFilesComponent { if let Event::Key(key) = event { let is_tree_focused = matches!(self.focus, Focus::Tree); + let is_head = self.is_head(); + if is_tree_focused && tree_nav(&mut self.tree, &self.key_config, key) { @@ -500,7 +554,9 @@ impl Component for RevisionFilesComponent { self.open_finder(); return Ok(EventState::Consumed); } - } else if key_match(key, self.key_config.keys.edit_file) { + } else if key_match(key, self.key_config.keys.edit_file) + && is_head + { if let Some(file) = self.selected_file_path_with_prefix() { @@ -512,6 +568,20 @@ impl Component for RevisionFilesComponent { ); return Ok(EventState::Consumed); } + } else if key_match(key, self.key_config.keys.open_file) + && !is_head + { + if let Some(file) = + self.dump_selected_file_content_to_tempfile() + { + //Note: switch to status tab so its clear we are + // not altering a file inside a revision here + self.queue.push(InternalEvent::TabSwitchStatus); + self.queue.push( + InternalEvent::OpenExternalEditor(Some(file)), + ); + return Ok(EventState::Consumed); + } } else if key_match(key, self.key_config.keys.copy) { if let Some(file) = self.selected_file_path() { try_or_popup!( diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 2903499587..e82b805c21 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -70,6 +70,7 @@ pub struct KeysList { pub blame: GituiKeyEvent, pub file_history: GituiKeyEvent, pub edit_file: GituiKeyEvent, + pub open_file: GituiKeyEvent, pub status_stage_all: GituiKeyEvent, pub status_reset_item: GituiKeyEvent, pub status_ignore_file: GituiKeyEvent, @@ -167,6 +168,7 @@ impl Default for KeysList { blame: GituiKeyEvent::new(KeyCode::Char('B'), KeyModifiers::SHIFT), file_history: GituiKeyEvent::new(KeyCode::Char('H'), KeyModifiers::SHIFT), edit_file: GituiKeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()), + open_file: GituiKeyEvent::new(KeyCode::Char('O'), KeyModifiers::SHIFT), status_stage_all: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), status_reset_item: GituiKeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT), diff_reset_lines: GituiKeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty()), diff --git a/src/strings.rs b/src/strings.rs index c4cff10f70..3e335315b1 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -1185,6 +1185,16 @@ pub mod commands { CMD_GROUP_CHANGES, ) } + pub fn open_item(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Open [{}]", + key_config.get_hint(key_config.keys.open_file), + ), + "open the currently selected file in an external editor", + CMD_GROUP_CHANGES, + ) + } pub fn stage_item(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!(