diff --git a/asyncgit/src/sync/commit_files.rs b/asyncgit/src/sync/commit_files.rs index 24d002bb..d3a80dca 100644 --- a/asyncgit/src/sync/commit_files.rs +++ b/asyncgit/src/sync/commit_files.rs @@ -1,6 +1,6 @@ use super::{utils::repo, CommitId}; use crate::{error::Result, StatusItem, StatusItemType}; -use git2::DiffDelta; +use git2::{Diff, DiffDelta, DiffOptions, Repository}; use scopetime::scope_time; /// get all files that are part of a commit @@ -12,19 +12,7 @@ pub fn get_commit_files( let repo = repo(repo_path)?; - let commit = repo.find_commit(id.into())?; - let commit_tree = commit.tree()?; - let parent = if commit.parent_count() > 0 { - Some(repo.find_commit(commit.parent_id(0)?)?.tree()?) - } else { - None - }; - - let diff = repo.diff_tree_to_tree( - parent.as_ref(), - Some(&commit_tree), - None, - )?; + let diff = get_commit_diff(&repo, id, None)?; let mut res = Vec::new(); @@ -48,6 +36,37 @@ pub fn get_commit_files( Ok(res) } +/// +pub(crate) fn get_commit_diff( + repo: &Repository, + id: CommitId, + pathspec: Option, +) -> Result> { + // scope_time!("get_commit_diff"); + + let commit = repo.find_commit(id.into())?; + let commit_tree = commit.tree()?; + let parent = if commit.parent_count() > 0 { + Some(repo.find_commit(commit.parent_id(0)?)?.tree()?) + } else { + None + }; + + let mut opt = pathspec.map(|p| { + let mut opts = DiffOptions::new(); + opts.pathspec(p); + opts + }); + + let diff = repo.diff_tree_to_tree( + parent.as_ref(), + Some(&commit_tree), + opt.as_mut(), + )?; + + Ok(diff) +} + #[cfg(test)] mod tests { use super::get_commit_files; diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs index 4d6b1433..e0bd303a 100644 --- a/asyncgit/src/sync/diff.rs +++ b/asyncgit/src/sync/diff.rs @@ -1,6 +1,6 @@ //! sync git api for fetching a diff -use super::utils; +use super::{commit_files::get_commit_diff, utils, CommitId}; use crate::{error::Error, error::Result, hash}; use git2::{ Delta, Diff, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch, @@ -81,6 +81,8 @@ pub(crate) fn get_diff_raw<'a>( stage: bool, reverse: bool, ) -> Result> { + // scope_time!("get_diff_raw"); + let mut opt = DiffOptions::new(); opt.pathspec(p); opt.reverse(reverse); @@ -119,7 +121,7 @@ pub(crate) fn get_diff_raw<'a>( Ok(diff) } -/// +/// returns diff of a specific file either in `stage` or workdir pub fn get_diff( repo_path: &str, p: String, @@ -131,6 +133,32 @@ pub fn get_diff( let work_dir = work_dir(&repo); let diff = get_diff_raw(&repo, &p, stage, false)?; + raw_diff_to_file_diff(&diff, work_dir) +} + +/// returns diff of a specific file inside a commit +/// see `get_commit_diff` +pub fn get_diff_commit( + repo_path: &str, + id: CommitId, + p: String, +) -> Result { + scope_time!("get_diff_commit"); + + let repo = utils::repo(repo_path)?; + let work_dir = work_dir(&repo); + let diff = get_commit_diff(&repo, id, Some(p))?; + + raw_diff_to_file_diff(&diff, work_dir) +} + +/// +fn raw_diff_to_file_diff<'a>( + diff: &'a Diff, + work_dir: &Path, +) -> Result { + // scope_time!("raw_diff_to_file_diff"); + let mut res: FileDiff = FileDiff::default(); let mut current_lines = Vec::new(); let mut current_hunk: Option = None; diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index d2d25e5a..2a80470c 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -17,6 +17,7 @@ pub mod utils; pub use commit_details::{get_commit_details, CommitDetails}; pub use commit_files::get_commit_files; pub use commits_info::{get_commits_info, CommitId, CommitInfo}; +pub use diff::get_diff_commit; pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult}; pub use hunks::{stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; diff --git a/src/app.rs b/src/app.rs index 8e9579e9..d22b7a97 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,8 +3,9 @@ use crate::{ cmdbar::CommandBar, components::{ event_pump, CommandBlocking, CommandInfo, CommitComponent, - Component, DrawableComponent, HelpComponent, MsgComponent, - ResetComponent, StashMsgComponent, + Component, DrawableComponent, HelpComponent, + InspectCommitComponent, MsgComponent, ResetComponent, + StashMsgComponent, }, keys, queue::{Action, InternalEvent, NeedsUpdate, Queue}, @@ -33,6 +34,7 @@ pub struct App { reset: ResetComponent, commit: CommitComponent, stashmsg_popup: StashMsgComponent, + inspect_commit_popup: InspectCommitComponent, cmdbar: CommandBar, tab: usize, revlog: Revlog, @@ -58,12 +60,15 @@ impl App { queue.clone(), &theme, ), + inspect_commit_popup: InspectCommitComponent::new( + &queue, sender, &theme, + ), do_quit: false, cmdbar: CommandBar::new(&theme), help: HelpComponent::new(&theme), msg: MsgComponent::new(&theme), tab: 0, - revlog: Revlog::new(sender, &theme), + revlog: Revlog::new(&queue, sender, &theme), status_tab: Status::new(sender, &queue, &theme), stashing_tab: Stashing::new(sender, &queue, &theme), stashlist_tab: StashList::new(&queue, &theme), @@ -159,8 +164,11 @@ impl App { if flags.contains(NeedsUpdate::ALL) { self.update()?; } + //TODO: make this a queue event? + //NOTE: set when any tree component changed selection if flags.contains(NeedsUpdate::DIFF) { self.status_tab.update_diff()?; + self.inspect_commit_popup.update_diff()?; } if flags.contains(NeedsUpdate::COMMANDS) { self.update_commands(); @@ -191,6 +199,7 @@ impl App { self.status_tab.update_git(ev)?; self.stashing_tab.update_git(ev)?; self.revlog.update_git(ev)?; + self.inspect_commit_popup.update_git(ev)?; if let AsyncNotification::Status = ev { //TODO: is that needed? @@ -210,6 +219,7 @@ impl App { self.status_tab.anything_pending() || self.revlog.any_work_pending() || self.stashing_tab.anything_pending() + || self.inspect_commit_popup.any_work_pending() } } @@ -222,6 +232,7 @@ impl App { reset, commit, stashmsg_popup, + inspect_commit_popup, help, revlog, status_tab, @@ -356,6 +367,10 @@ impl App { self.stashmsg_popup.show()? } InternalEvent::TabSwitch => self.set_tab(0)?, + InternalEvent::InspectCommit(id) => { + self.inspect_commit_popup.open(id)?; + flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS) + } }; Ok(flags) @@ -407,19 +422,31 @@ impl App { || self.help.is_visible() || self.reset.is_visible() || self.msg.is_visible() + || self.stashmsg_popup.is_visible() + || self.inspect_commit_popup.is_visible() } fn draw_popups( &mut self, f: &mut Frame, ) -> Result<()> { - let size = f.size(); + let size = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Min(1), + Constraint::Length(self.cmdbar.height()), + ] + .as_ref(), + ) + .split(f.size())[0]; self.commit.draw(f, size)?; self.stashmsg_popup.draw(f, size)?; self.reset.draw(f, size)?; self.help.draw(f, size)?; self.msg.draw(f, size)?; + self.inspect_commit_popup.draw(f, size)?; Ok(()) } diff --git a/src/components/changes.rs b/src/components/changes.rs index f1bec925..1b7089c4 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -53,7 +53,7 @@ impl ChangesComponent { files: FileTreeComponent::new( title, focus, - queue.clone(), + Some(queue.clone()), theme, ), is_working_dir, @@ -75,7 +75,8 @@ impl ChangesComponent { /// pub fn focus_select(&mut self, focus: bool) { - self.files.focus_select(focus) + self.files.focus(focus); + self.files.show_selection(focus); } /// returns true if list is empty diff --git a/src/components/commit_details.rs b/src/components/commit_details/details.rs similarity index 69% rename from src/components/commit_details.rs rename to src/components/commit_details/details.rs index e4115a76..ece0ef6f 100644 --- a/src/components/commit_details.rs +++ b/src/components/commit_details/details.rs @@ -1,15 +1,19 @@ -use super::{ - dialog_paragraph, utils::time_to_string, DrawableComponent, +use crate::{ + components::{ + dialog_paragraph, utils::time_to_string, CommandBlocking, + CommandInfo, Component, DrawableComponent, + }, + strings, + ui::style::Theme, }; -use crate::{strings, ui::style::Theme}; use anyhow::Result; use asyncgit::{ - sync::{self, CommitDetails, CommitId}, - AsyncCommitFiles, AsyncNotification, StatusItem, CWD, + sync::{self, CommitDetails}, + CWD, }; -use crossbeam_channel::Sender; +use crossterm::event::Event; use std::borrow::Cow; -use sync::Tags; +use sync::{CommitId, Tags}; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -18,121 +22,39 @@ use tui::{ Frame, }; -pub struct CommitDetailsComponent { +pub struct DetailsComponent { data: Option, tags: Vec, - files: Option>, theme: Theme, - git_commit_files: AsyncCommitFiles, } -impl DrawableComponent for CommitDetailsComponent { - fn draw( - &mut self, - f: &mut Frame, - rect: Rect, - ) -> Result<()> { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(8), - Constraint::Min(10), - Constraint::Length(12), - ] - .as_ref(), - ) - .split(rect); - - f.render_widget( - dialog_paragraph( - strings::commit::DETAILS_INFO_TITLE, - self.get_text_info().iter(), - ), - chunks[0], - ); - - f.render_widget( - dialog_paragraph( - strings::commit::DETAILS_MESSAGE_TITLE, - self.get_text_message().iter(), - ) - .wrap(true), - chunks[1], - ); - - let files_loading = self.files.is_none(); - let files_count = self.files.as_ref().map_or(0, Vec::len); - - let txt = self - .files - .as_ref() - .map_or(vec![], |f| self.get_text_files(f)); - - let title = if files_loading { - strings::commit::DETAILS_FILES_LOADING_TITLE.to_string() - } else { - format!( - "{} {}", - strings::commit::DETAILS_FILES_TITLE, - files_count - ) - }; - f.render_widget( - dialog_paragraph(title.as_str(), txt.iter()), - chunks[2], - ); - - Ok(()) - } -} - -impl CommitDetailsComponent { +impl DetailsComponent { /// - pub fn new( - sender: &Sender, - theme: &Theme, - ) -> Self { + pub const fn new(theme: &Theme) -> Self { Self { - theme: *theme, data: None, tags: Vec::new(), - files: None, - git_commit_files: AsyncCommitFiles::new(sender), + theme: *theme, } } - /// pub fn set_commit( &mut self, id: Option, tags: &Tags, ) -> Result<()> { + self.tags.clear(); + self.data = if let Some(id) = id { sync::get_commit_details(CWD, id).ok() } else { None }; - self.tags.clear(); - self.files = None; - if let Some(id) = id { if let Some(tags) = tags.get(&id) { self.tags.extend(tags.clone()); } - - if let Some((fetched_id, res)) = - self.git_commit_files.current()? - { - if fetched_id == id { - self.files = Some(res); - } else { - self.git_commit_files.fetch(id)?; - } - } else { - self.git_commit_files.fetch(id)?; - } } Ok(()) @@ -161,25 +83,6 @@ impl CommitDetailsComponent { vec![] } - fn get_text_files<'a>( - &self, - files: &'a [StatusItem], - ) -> Vec> { - let new_line = Text::Raw(Cow::from("\n")); - - let mut res = Vec::with_capacity(files.len()); - - for file in files { - res.push(Text::Styled( - Cow::from(file.path.as_str()), - self.theme.text(true, false), - )); - res.push(new_line.clone()); - } - - res - } - fn get_text_info(&self) -> Vec { let new_line = Text::Raw(Cow::from("\n")); @@ -271,9 +174,57 @@ impl CommitDetailsComponent { vec![] } } +} - /// - pub fn any_work_pending(&self) -> bool { - self.git_commit_files.is_pending() +impl DrawableComponent for DetailsComponent { + fn draw( + &mut self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [Constraint::Length(8), Constraint::Min(10)].as_ref(), + ) + .split(rect); + + f.render_widget( + dialog_paragraph( + strings::commit::DETAILS_INFO_TITLE, + self.get_text_info().iter(), + &self.theme, + false, + ), + chunks[0], + ); + + f.render_widget( + dialog_paragraph( + strings::commit::DETAILS_MESSAGE_TITLE, + self.get_text_message().iter(), + &self.theme, + false, + ) + .wrap(true), + chunks[1], + ); + + Ok(()) + } +} + +impl Component for DetailsComponent { + fn commands( + &self, + _out: &mut Vec, + _force_all: bool, + ) -> CommandBlocking { + // visibility_blocking(self) + CommandBlocking::PassingOn + } + + fn event(&mut self, _ev: Event) -> Result { + Ok(false) } } diff --git a/src/components/commit_details/mod.rs b/src/components/commit_details/mod.rs new file mode 100644 index 00000000..97ed21e6 --- /dev/null +++ b/src/components/commit_details/mod.rs @@ -0,0 +1,173 @@ +mod details; + +use super::{ + command_pump, event_pump, CommandBlocking, CommandInfo, + Component, DrawableComponent, FileTreeComponent, +}; +use crate::{accessors, queue::Queue, strings, ui::style::Theme}; +use anyhow::Result; +use asyncgit::{ + sync::{CommitId, Tags}, + AsyncCommitFiles, AsyncNotification, +}; +use crossbeam_channel::Sender; +use crossterm::event::Event; +use details::DetailsComponent; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + Frame, +}; + +pub struct CommitDetailsComponent { + details: DetailsComponent, + file_tree: FileTreeComponent, + git_commit_files: AsyncCommitFiles, + visible: bool, +} + +impl CommitDetailsComponent { + accessors!(self, [details, file_tree]); + + /// + pub fn new( + queue: &Queue, + sender: &Sender, + theme: &Theme, + ) -> Self { + Self { + details: DetailsComponent::new(theme), + git_commit_files: AsyncCommitFiles::new(sender), + file_tree: FileTreeComponent::new( + "", + false, + Some(queue.clone()), + theme, + ), + visible: false, + } + } + + fn get_files_title(&self) -> String { + let files_loading = self.git_commit_files.is_pending(); + let files_count = self.file_tree.file_count(); + + if files_loading { + strings::commit::DETAILS_FILES_LOADING_TITLE.to_string() + } else { + format!( + "{} {}", + strings::commit::DETAILS_FILES_TITLE, + files_count + ) + } + } + + /// + pub fn set_commit( + &mut self, + id: Option, + tags: &Tags, + ) -> Result<()> { + self.details.set_commit(id, tags)?; + + if let Some(id) = id { + if let Some((fetched_id, res)) = + self.git_commit_files.current()? + { + if fetched_id == id { + self.file_tree.update(res.as_slice())?; + } else { + self.file_tree.clear()?; + self.git_commit_files.fetch(id)?; + } + } else { + self.file_tree.clear()?; + self.git_commit_files.fetch(id)?; + } + } + + self.file_tree.set_title(self.get_files_title()); + + Ok(()) + } + + /// + pub fn any_work_pending(&self) -> bool { + self.git_commit_files.is_pending() + } + + /// + pub const fn files(&self) -> &FileTreeComponent { + &self.file_tree + } +} + +impl DrawableComponent for CommitDetailsComponent { + fn draw( + &mut self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(70), + Constraint::Percentage(30), + ] + .as_ref(), + ) + .split(rect); + + self.details.draw(f, chunks[0])?; + self.file_tree.draw(f, chunks[1])?; + + Ok(()) + } +} + +impl Component for CommitDetailsComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.visible || force_all { + command_pump( + out, + force_all, + self.components().as_slice(), + ); + } + + CommandBlocking::PassingOn + } + + fn event(&mut self, ev: Event) -> Result { + if event_pump(ev, self.components_mut().as_mut_slice())? { + return Ok(true); + } + + Ok(false) + } + + fn is_visible(&self) -> bool { + self.visible + } + fn hide(&mut self) { + self.visible = false; + } + fn show(&mut self) -> Result<()> { + self.visible = true; + Ok(()) + } + + fn focused(&self) -> bool { + self.details.focused() || self.file_tree.focused() + } + fn focus(&mut self, focus: bool) { + self.file_tree.focus(focus); + self.file_tree.show_selection(true); + } +} diff --git a/src/components/diff.rs b/src/components/diff.rs index 43fb33f0..891ad35c 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -35,13 +35,13 @@ pub struct DiffComponent { focused: bool, current: Current, selected_hunk: Option, - queue: Queue, + queue: Option, theme: Theme, } impl DiffComponent { /// - pub fn new(queue: Queue, theme: &Theme) -> Self { + pub fn new(queue: Option, theme: &Theme) -> Self { Self { focused: false, queue, @@ -270,12 +270,18 @@ impl DiffComponent { if let Some(hunk) = self.selected_hunk { let hash = self.diff.hunks[hunk].header_hash; self.queue + .as_ref() + .expect("try using queue in immutable diff") .borrow_mut() .push_back(InternalEvent::AddHunk(hash)); } Ok(()) } + + fn is_immutable(&self) -> bool { + self.queue.is_none() + } } impl DrawableComponent for DiffComponent { @@ -325,16 +331,18 @@ impl Component for DiffComponent { .hidden(), ); - out.push(CommandInfo::new( - commands::DIFF_HUNK_REMOVE, - self.selected_hunk.is_some(), - self.focused && self.current.is_stage, - )); - out.push(CommandInfo::new( - commands::DIFF_HUNK_ADD, - self.selected_hunk.is_some(), - self.focused && !self.current.is_stage, - )); + if !self.is_immutable() { + out.push(CommandInfo::new( + commands::DIFF_HUNK_REMOVE, + self.selected_hunk.is_some(), + self.focused && self.current.is_stage, + )); + out.push(CommandInfo::new( + commands::DIFF_HUNK_ADD, + self.selected_hunk.is_some(), + self.focused && !self.current.is_stage, + )); + } CommandBlocking::PassingOn } @@ -367,7 +375,7 @@ impl Component for DiffComponent { self.scroll(ScrollType::PageDown)?; Ok(true) } - keys::ENTER => { + keys::ENTER if !self.is_immutable() => { self.add_hunk()?; Ok(true) } @@ -382,7 +390,6 @@ impl Component for DiffComponent { fn focused(&self) -> bool { self.focused } - fn focus(&mut self, focus: bool) { self.focused = focus } diff --git a/src/components/filetree.rs b/src/components/filetree.rs index aee6fde9..cc522141 100644 --- a/src/components/filetree.rs +++ b/src/components/filetree.rs @@ -27,7 +27,7 @@ pub struct FileTreeComponent { current_hash: u64, focused: bool, show_selection: bool, - queue: Queue, + queue: Option, theme: Theme, } @@ -36,7 +36,7 @@ impl FileTreeComponent { pub fn new( title: &str, focus: bool, - queue: Queue, + queue: Option, theme: &Theme, ) -> Self { Self { @@ -67,9 +67,19 @@ impl FileTreeComponent { } /// - pub fn focus_select(&mut self, focus: bool) { - self.focus(focus); - self.show_selection = focus; + pub fn selection_file(&self) -> Option { + self.tree.selected_item().and_then(|f| { + if let FileTreeItemKind::File(f) = f.kind { + Some(f) + } else { + None + } + }) + } + + /// + pub fn show_selection(&mut self, show: bool) { + self.show_selection = show; } /// returns true if list is empty @@ -77,6 +87,22 @@ impl FileTreeComponent { self.tree.is_empty() } + /// + pub fn file_count(&self) -> usize { + self.tree.tree.len() + } + + /// + pub fn set_title(&mut self, title: String) { + self.title = title; + } + + /// + pub fn clear(&mut self) -> Result<()> { + self.current_hash = 0; + self.tree.update(&[]) + } + /// pub fn is_file_seleted(&self) -> bool { if let Some(item) = self.tree.selected_item() { @@ -93,9 +119,11 @@ impl FileTreeComponent { let changed = self.tree.move_selection(dir); if changed { - self.queue - .borrow_mut() - .push_back(InternalEvent::Update(NeedsUpdate::DIFF)); + if let Some(ref queue) = self.queue { + queue.borrow_mut().push_back(InternalEvent::Update( + NeedsUpdate::DIFF, + )); + } } changed @@ -286,6 +314,6 @@ impl Component for FileTreeComponent { self.focused } fn focus(&mut self, focus: bool) { - self.focused = focus + self.focused = focus; } } diff --git a/src/components/inspect_commit.rs b/src/components/inspect_commit.rs new file mode 100644 index 00000000..5b596239 --- /dev/null +++ b/src/components/inspect_commit.rs @@ -0,0 +1,215 @@ +use super::{ + command_pump, event_pump, visibility_blocking, CommandBlocking, + CommandInfo, CommitDetailsComponent, Component, DiffComponent, + DrawableComponent, +}; +use crate::{ + accessors, keys, queue::Queue, strings, ui::style::Theme, +}; +use anyhow::Result; +use asyncgit::{sync, AsyncNotification, CWD}; +use crossbeam_channel::Sender; +use crossterm::event::Event; +use strings::commands; +use sync::{CommitId, Tags}; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + widgets::Clear, + Frame, +}; + +pub struct InspectCommitComponent { + commit_id: Option, + diff: DiffComponent, + details: CommitDetailsComponent, + visible: bool, +} + +impl DrawableComponent for InspectCommitComponent { + fn draw( + &mut self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + if self.is_visible() { + let percentages = if self.diff.focused() { + (30, 70) + } else { + (50, 50) + }; + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(percentages.0), + Constraint::Percentage(percentages.1), + ] + .as_ref(), + ) + .split(rect); + + f.render_widget(Clear, rect); + + self.details.draw(f, chunks[0])?; + self.diff.draw(f, chunks[1])?; + } + + Ok(()) + } +} + +impl Component for InspectCommitComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + command_pump( + out, + force_all, + self.components().as_slice(), + ); + } + + out.push( + CommandInfo::new( + commands::CLOSE_POPUP, + true, + self.is_visible(), + ) + .order(1), + ); + + visibility_blocking(self) + } + + fn event(&mut self, ev: Event) -> Result { + if self.is_visible() { + if event_pump(ev, self.components_mut().as_mut_slice())? { + return Ok(true); + } + + if let Event::Key(e) = ev { + match e { + keys::EXIT_POPUP => { + self.hide(); + } + keys::FOCUS_RIGHT if self.can_focus_diff() => { + self.details.focus(false); + self.diff.focus(true); + } + keys::FOCUS_LEFT if self.diff.focused() => { + self.details.focus(true); + self.diff.focus(false); + } + _ => (), + } + + // stop key event propagation + return Ok(true); + } + } + + Ok(false) + } + + fn is_visible(&self) -> bool { + self.visible + } + fn hide(&mut self) { + self.visible = false; + } + fn show(&mut self) -> Result<()> { + self.visible = true; + self.details.show()?; + self.details.focus(true); + self.diff.focus(false); + self.update()?; + Ok(()) + } +} + +impl InspectCommitComponent { + accessors!(self, [diff, details]); + + /// + pub fn new( + queue: &Queue, + sender: &Sender, + theme: &Theme, + ) -> Self { + Self { + details: CommitDetailsComponent::new( + queue, sender, theme, + ), + diff: DiffComponent::new(None, theme), + commit_id: None, + visible: false, + } + } + + /// + pub fn open(&mut self, id: CommitId) -> Result<()> { + self.commit_id = Some(id); + self.show()?; + + Ok(()) + } + + /// + pub fn any_work_pending(&self) -> bool { + self.details.any_work_pending() + } + + /// + pub fn update_git( + &mut self, + ev: AsyncNotification, + ) -> Result<()> { + if self.is_visible() { + if let AsyncNotification::CommitFiles = ev { + self.update()? + } else if let AsyncNotification::Diff = ev { + self.update()? + } + } + + Ok(()) + } + + /// called when any tree component changed selection + pub fn update_diff(&mut self) -> Result<()> { + if self.is_visible() { + if let Some(id) = self.commit_id { + if let Some(f) = self.details.files().selection_file() + { + self.diff.update( + f.path.clone(), + false, + sync::get_diff_commit(CWD, id, f.path)?, + )?; + + return Ok(()); + } + } + + self.diff.clear()?; + } + + Ok(()) + } + + fn update(&mut self) -> Result<()> { + self.details.set_commit(self.commit_id, &Tags::new())?; + self.update_diff()?; + + Ok(()) + } + + fn can_focus_diff(&self) -> bool { + self.details.files().selection_file().is_some() + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 165ea269..0f7bc01a 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -6,6 +6,7 @@ mod commitlist; mod diff; mod filetree; mod help; +mod inspect_commit; mod msg; mod reset; mod stashmsg; @@ -21,15 +22,16 @@ use crossterm::event::Event; pub use diff::DiffComponent; pub use filetree::FileTreeComponent; pub use help::HelpComponent; +pub use inspect_commit::InspectCommitComponent; pub use msg::MsgComponent; pub use reset::ResetComponent; pub use stashmsg::StashMsgComponent; pub use utils::filetree::FileTreeItemKind; +use crate::ui::style::Theme; use tui::{ backend::Backend, - layout::Alignment, - layout::Rect, + layout::{Alignment, Rect}, widgets::{Block, BorderType, Borders, Paragraph, Text}, Frame, }; @@ -152,23 +154,23 @@ pub trait Component { fn show(&mut self) -> Result<()> { Ok(()) } + + /// + fn toggle_visible(&mut self) -> Result<()> { + if self.is_visible() { + self.hide(); + Ok(()) + } else { + self.show() + } + } } fn dialog_paragraph<'a, 't, T>( title: &'a str, content: T, -) -> Paragraph<'a, 't, T> -where - T: Iterator>, -{ - Paragraph::new(content) - .block(Block::default().title(title).borders(Borders::ALL)) - .alignment(Alignment::Left) -} - -fn popup_paragraph<'a, 't, T>( - title: &'a str, - content: T, + theme: &Theme, + focused: bool, ) -> Paragraph<'a, 't, T> where T: Iterator>, @@ -178,7 +180,29 @@ where Block::default() .title(title) .borders(Borders::ALL) - .border_type(BorderType::Thick), + .title_style(theme.title(focused)) + .border_style(theme.block(focused)), + ) + .alignment(Alignment::Left) +} + +fn popup_paragraph<'a, 't, T>( + title: &'a str, + content: T, + theme: &Theme, + focused: bool, +) -> Paragraph<'a, 't, T> +where + T: Iterator>, +{ + Paragraph::new(content) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_type(BorderType::Thick) + .title_style(theme.title(focused)) + .border_style(theme.block(focused)), ) .alignment(Alignment::Left) } diff --git a/src/components/reset.rs b/src/components/reset.rs index 70b56519..8fb1e5da 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -42,7 +42,10 @@ impl DrawableComponent for ResetComponent { let area = ui::centered_rect(30, 20, f.size()); f.render_widget(Clear, area); - f.render_widget(popup_paragraph(title, txt.iter()), area); + f.render_widget( + popup_paragraph(title, txt.iter(), &self.theme, true), + area, + ); } Ok(()) diff --git a/src/components/textinput.rs b/src/components/textinput.rs index 0efbdfc2..a546d8a9 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -76,7 +76,12 @@ impl DrawableComponent for TextInputComponent { let area = ui::centered_rect(60, 20, f.size()); f.render_widget(Clear, area); f.render_widget( - popup_paragraph(self.title.as_str(), txt.iter()), + popup_paragraph( + self.title.as_str(), + txt.iter(), + &self.theme, + true, + ), area, ); } diff --git a/src/queue.rs b/src/queue.rs index d0d0f19c..0c19867a 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -41,12 +41,14 @@ pub enum InternalEvent { ShowErrorMsg(String), /// Update(NeedsUpdate), - /// + /// open commit msg input OpenCommit, /// PopupStashing(StashingOptions), /// TabSwitch, + /// + InspectCommit(CommitId), } /// diff --git a/src/strings.rs b/src/strings.rs index efc5bb40..55ae7fec 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -249,9 +249,15 @@ pub mod commands { ); /// - pub static LOG_DETAILS_OPEN: CommandText = CommandText::new( + pub static LOG_DETAILS_TOGGLE: CommandText = CommandText::new( "Details [enter]", "open details of selected commit", CMD_GROUP_LOG, ); + /// + pub static LOG_DETAILS_OPEN: CommandText = CommandText::new( + "Inspect [\u{2192}]", //→ + "inspect selected commit in detail", + CMD_GROUP_LOG, + ); } diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 4fdea591..bf9355bd 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -4,7 +4,9 @@ use crate::{ CommitDetailsComponent, CommitList, Component, DrawableComponent, }, - keys, strings, + keys, + queue::{InternalEvent, Queue}, + strings, ui::style::Theme, }; use anyhow::Result; @@ -12,6 +14,7 @@ use asyncgit::{sync, AsyncLog, AsyncNotification, FetchStatus, CWD}; use crossbeam_channel::Sender; use crossterm::event::Event; use strings::commands; +use sync::CommitId; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -25,24 +28,25 @@ pub struct Revlog { commit_details: CommitDetailsComponent, list: CommitList, git_log: AsyncLog, + queue: Queue, visible: bool, - details_open: bool, } impl Revlog { /// pub fn new( + queue: &Queue, sender: &Sender, theme: &Theme, ) -> Self { Self { + queue: queue.clone(), commit_details: CommitDetailsComponent::new( - sender, theme, + queue, sender, theme, ), list: CommitList::new(strings::LOG_TITLE, theme), git_log: AsyncLog::new(sender), visible: false, - details_open: false, } } @@ -72,9 +76,9 @@ impl Revlog { self.list.set_tags(sync::get_tags(CWD)?); } - if self.details_open { + if self.commit_details.is_visible() { self.commit_details.set_commit( - self.list.selected_entry().map(|e| e.id), + self.selected_commit(), self.list.tags().expect("tags"), )?; } @@ -115,6 +119,10 @@ impl Revlog { Ok(()) } + + fn selected_commit(&self) -> Option { + self.list.selected_entry().map(|e| e.id) + } } impl DrawableComponent for Revlog { @@ -134,7 +142,7 @@ impl DrawableComponent for Revlog { ) .split(area); - if self.details_open { + if self.commit_details.is_visible() { self.list.draw(f, chunks[0])?; self.commit_details.draw(f, chunks[1])?; } else { @@ -154,9 +162,18 @@ impl Component for Revlog { self.update()?; return Ok(true); } else if let Event::Key(keys::LOG_COMMIT_DETAILS) = ev { - self.details_open = !self.details_open; + self.commit_details.toggle_visible()?; self.update()?; return Ok(true); + } else if let Event::Key(keys::FOCUS_RIGHT) = ev { + return if let Some(id) = self.selected_commit() { + self.queue + .borrow_mut() + .push_back(InternalEvent::InspectCommit(id)); + Ok(true) + } else { + Ok(false) + }; } } @@ -173,11 +190,18 @@ impl Component for Revlog { } out.push(CommandInfo::new( - commands::LOG_DETAILS_OPEN, + commands::LOG_DETAILS_TOGGLE, true, self.visible, )); + out.push(CommandInfo::new( + commands::LOG_DETAILS_OPEN, + true, + (self.visible && self.commit_details.is_visible()) + || force_all, + )); + visibility_blocking(self) } diff --git a/src/tabs/stashing.rs b/src/tabs/stashing.rs index 5bb4b0ba..602cee78 100644 --- a/src/tabs/stashing.rs +++ b/src/tabs/stashing.rs @@ -52,7 +52,7 @@ impl Stashing { index: FileTreeComponent::new( strings::STASHING_FILES_TITLE, true, - queue.clone(), + Some(queue.clone()), theme, ), visible: false, diff --git a/src/tabs/status.rs b/src/tabs/status.rs index d9eb6e3b..63c53dfc 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -126,7 +126,7 @@ impl Status { queue.clone(), theme, ), - diff: DiffComponent::new(queue.clone(), theme), + diff: DiffComponent::new(Some(queue.clone()), theme), git_diff: AsyncDiff::new(sender.clone()), git_status_workdir: AsyncStatus::new(sender.clone()), git_status_stage: AsyncStatus::new(sender.clone()),