full commit detail popup (#113)

see #80
This commit is contained in:
Stephan Dilly 2020-06-09 13:30:17 +02:00 committed by GitHub
parent fa8070b1ba
commit 702415c40d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 706 additions and 192 deletions

View file

@ -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<String>,
) -> Result<Diff<'_>> {
// 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;

View file

@ -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<Diff<'a>> {
// 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<FileDiff> {
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<FileDiff> {
// scope_time!("raw_diff_to_file_diff");
let mut res: FileDiff = FileDiff::default();
let mut current_lines = Vec::new();
let mut current_hunk: Option<HunkHeader> = None;

View file

@ -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;

View file

@ -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<B: Backend>(
&mut self,
f: &mut Frame<B>,
) -> 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(())
}

View file

@ -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

View file

@ -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<CommitDetails>,
tags: Vec<String>,
files: Option<Vec<StatusItem>>,
theme: Theme,
git_commit_files: AsyncCommitFiles,
}
impl DrawableComponent for CommitDetailsComponent {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
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<AsyncNotification>,
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<CommitId>,
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<Text<'a>> {
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<Text> {
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<B: Backend>(
&mut self,
f: &mut Frame<B>,
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<CommandInfo>,
_force_all: bool,
) -> CommandBlocking {
// visibility_blocking(self)
CommandBlocking::PassingOn
}
fn event(&mut self, _ev: Event) -> Result<bool> {
Ok(false)
}
}

View file

@ -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<AsyncNotification>,
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<CommitId>,
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<B: Backend>(
&mut self,
f: &mut Frame<B>,
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<CommandInfo>,
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<bool> {
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);
}
}

View file

@ -35,13 +35,13 @@ pub struct DiffComponent {
focused: bool,
current: Current,
selected_hunk: Option<usize>,
queue: Queue,
queue: Option<Queue>,
theme: Theme,
}
impl DiffComponent {
///
pub fn new(queue: Queue, theme: &Theme) -> Self {
pub fn new(queue: Option<Queue>, 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
}

View file

@ -27,7 +27,7 @@ pub struct FileTreeComponent {
current_hash: u64,
focused: bool,
show_selection: bool,
queue: Queue,
queue: Option<Queue>,
theme: Theme,
}
@ -36,7 +36,7 @@ impl FileTreeComponent {
pub fn new(
title: &str,
focus: bool,
queue: Queue,
queue: Option<Queue>,
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<StatusItem> {
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;
}
}

View file

@ -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<CommitId>,
diff: DiffComponent,
details: CommitDetailsComponent,
visible: bool,
}
impl DrawableComponent for InspectCommitComponent {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
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<CommandInfo>,
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<bool> {
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<AsyncNotification>,
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()
}
}

View file

@ -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<Item = &'t Text<'t>>,
{
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<Item = &'t Text<'t>>,
@ -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<Item = &'t Text<'t>>,
{
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)
}

View file

@ -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(())

View file

@ -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,
);
}

View file

@ -41,12 +41,14 @@ pub enum InternalEvent {
ShowErrorMsg(String),
///
Update(NeedsUpdate),
///
/// open commit msg input
OpenCommit,
///
PopupStashing(StashingOptions),
///
TabSwitch,
///
InspectCommit(CommitId),
}
///

View file

@ -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,
);
}

View file

@ -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<AsyncNotification>,
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<CommitId> {
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)
}

View file

@ -52,7 +52,7 @@ impl Stashing {
index: FileTreeComponent::new(
strings::STASHING_FILES_TITLE,
true,
queue.clone(),
Some(queue.clone()),
theme,
),
visible: false,

View file

@ -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()),