diff --git a/src/app.rs b/src/app.rs index 1888523d..b48ba0f6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,7 @@ use crate::{ ExternalEditorComponent, HelpComponent, InspectCommitComponent, MsgComponent, PullComponent, PushComponent, PushTagsComponent, RenameBranchComponent, - ResetComponent, RevisionFilesComponent, StashMsgComponent, + ResetComponent, RevisionFilesPopup, StashMsgComponent, TagCommitComponent, TagListComponent, }, input::{Input, InputEvent, InputState}, @@ -16,7 +16,7 @@ use crate::{ queue::{Action, InternalEvent, NeedsUpdate, Queue}, setup_popups, strings::{self, order}, - tabs::{Revlog, StashList, Stashing, Status}, + tabs::{FilesTab, Revlog, StashList, Stashing, Status}, ui::style::{SharedTheme, Theme}, }; use anyhow::{bail, Result}; @@ -47,7 +47,7 @@ pub struct App { stashmsg_popup: StashMsgComponent, inspect_commit_popup: InspectCommitComponent, external_editor_popup: ExternalEditorComponent, - revision_files_popup: RevisionFilesComponent, + revision_files_popup: RevisionFilesPopup, push_popup: PushComponent, push_tags_popup: PushTagsComponent, pull_popup: PullComponent, @@ -62,6 +62,7 @@ pub struct App { status_tab: Status, stashing_tab: Stashing, stashlist_tab: StashList, + files_tab: FilesTab, queue: Queue, theme: SharedTheme, key_config: SharedKeyConfig, @@ -105,7 +106,7 @@ impl App { theme.clone(), key_config.clone(), ), - revision_files_popup: RevisionFilesComponent::new( + revision_files_popup: RevisionFilesPopup::new( &queue, sender, theme.clone(), @@ -203,6 +204,12 @@ impl App { theme.clone(), key_config.clone(), ), + files_tab: FilesTab::new( + sender, + &queue, + theme.clone(), + key_config.clone(), + ), queue, theme, key_config, @@ -237,8 +244,9 @@ impl App { match self.tab { 0 => self.status_tab.draw(f, chunks_main[1])?, 1 => self.revlog.draw(f, chunks_main[1])?, - 2 => self.stashing_tab.draw(f, chunks_main[1])?, - 3 => self.stashlist_tab.draw(f, chunks_main[1])?, + 2 => self.files_tab.draw(f, chunks_main[1])?, + 3 => self.stashing_tab.draw(f, chunks_main[1])?, + 4 => self.stashlist_tab.draw(f, chunks_main[1])?, _ => bail!("unknown tab"), }; @@ -271,6 +279,7 @@ impl App { NeedsUpdate::COMMANDS } else if k == self.key_config.tab_status || k == self.key_config.tab_log + || k == self.key_config.tab_files || k == self.key_config.tab_stashing || k == self.key_config.tab_stashes { @@ -322,6 +331,7 @@ impl App { self.commit.update()?; self.status_tab.update()?; self.revlog.update()?; + self.files_tab.update()?; self.stashing_tab.update()?; self.stashlist_tab.update()?; @@ -339,6 +349,7 @@ impl App { self.status_tab.update_git(ev)?; self.stashing_tab.update_git(ev)?; + self.files_tab.update_git(ev)?; self.revlog.update_git(ev)?; self.blame_file_popup.update_git(ev)?; self.inspect_commit_popup.update_git(ev)?; @@ -364,6 +375,7 @@ impl App { self.status_tab.anything_pending() || self.revlog.any_work_pending() || self.stashing_tab.anything_pending() + || self.files_tab.anything_pending() || self.blame_file_popup.any_work_pending() || self.inspect_commit_popup.any_work_pending() || self.input.is_state_changing() @@ -408,6 +420,7 @@ impl App { help, revlog, status_tab, + files_tab, stashing_tab, stashlist_tab ] @@ -450,6 +463,7 @@ impl App { vec![ &mut self.status_tab, &mut self.revlog, + &mut self.files_tab, &mut self.stashing_tab, &mut self.stashlist_tab, ] @@ -471,10 +485,12 @@ impl App { self.set_tab(0)? } else if k == self.key_config.tab_log { self.set_tab(1)? - } else if k == self.key_config.tab_stashing { + } else if k == self.key_config.tab_files { self.set_tab(2)? - } else if k == self.key_config.tab_stashes { + } else if k == self.key_config.tab_stashing { self.set_tab(3)? + } else if k == self.key_config.tab_stashes { + self.set_tab(4)? } Ok(()) @@ -748,6 +764,7 @@ impl App { let tabs = [ Span::raw(strings::tab_status(&self.key_config)), Span::raw(strings::tab_log(&self.key_config)), + Span::raw(strings::tab_files(&self.key_config)), Span::raw(strings::tab_stashing(&self.key_config)), Span::raw(strings::tab_stashes(&self.key_config)), ] diff --git a/src/components/mod.rs b/src/components/mod.rs index 4e18bd86..b7bd257d 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -19,6 +19,7 @@ mod push_tags; mod rename_branch; mod reset; mod revision_files; +mod revision_files_popup; mod stashmsg; mod syntax_text; mod tag_commit; @@ -46,6 +47,7 @@ pub use push_tags::PushTagsComponent; pub use rename_branch::RenameBranchComponent; pub use reset::ResetComponent; pub use revision_files::RevisionFilesComponent; +pub use revision_files_popup::RevisionFilesPopup; pub use stashmsg::StashMsgComponent; pub use syntax_text::SyntaxTextComponent; pub use tag_commit::TagCommitComponent; diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index a5328e8a..12d343b3 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -1,6 +1,6 @@ use super::{ - visibility_blocking, CommandBlocking, CommandInfo, Component, - DrawableComponent, EventState, SyntaxTextComponent, + CommandBlocking, CommandInfo, Component, DrawableComponent, + EventState, SyntaxTextComponent, }; use crate::{ keys::SharedKeyConfig, @@ -23,7 +23,7 @@ use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, text::Span, - widgets::{Block, Borders, Clear}, + widgets::{Block, Borders}, Frame, }; @@ -38,7 +38,6 @@ enum Focus { pub struct RevisionFilesComponent { queue: Queue, - title: String, theme: SharedTheme, //TODO: store TreeFiles in `tree` files: Vec, @@ -46,7 +45,6 @@ pub struct RevisionFilesComponent { tree: FileTree, scroll_top: Cell, revision: Option, - visible: bool, focus: Focus, key_config: SharedKeyConfig, } @@ -61,7 +59,6 @@ impl RevisionFilesComponent { ) -> Self { Self { queue: queue.clone(), - title: String::new(), tree: FileTree::default(), scroll_top: Cell::new(0), current_file: SyntaxTextComponent::new( @@ -72,30 +69,26 @@ impl RevisionFilesComponent { theme, files: Vec::new(), revision: None, - visible: false, focus: Focus::Tree, key_config, } } /// - pub fn open(&mut self, commit: CommitId) -> Result<()> { - self.files = sync::tree_files(CWD, commit)?; - let filenames: Vec<&str> = self - .files - .iter() - .map(|f| f.path.to_str().unwrap_or_default()) - .collect(); - self.tree = FileTree::new(&filenames, &BTreeSet::new())?; - self.tree.collapse_but_root(); - self.revision = Some(commit); - self.title = format!( - "Files at [{}]", - self.revision - .map(|c| c.get_short_string()) - .unwrap_or_default() - ); - self.show()?; + pub fn set_commit(&mut self, commit: CommitId) -> Result<()> { + let same_id = + self.revision.map(|c| c == commit).unwrap_or_default(); + if !same_id { + self.files = sync::tree_files(CWD, commit)?; + let filenames: Vec<&str> = self + .files + .iter() + .map(|f| f.path.to_str().unwrap_or_default()) + .collect(); + self.tree = FileTree::new(&filenames, &BTreeSet::new())?; + self.tree.collapse_but_root(); + self.revision = Some(commit); + } Ok(()) } @@ -228,35 +221,21 @@ impl DrawableComponent for RevisionFilesComponent { f: &mut Frame, area: Rect, ) -> Result<()> { - if self.is_visible() { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .margin(1) - .constraints( - [ - Constraint::Percentage(40), - Constraint::Percentage(60), - ] - .as_ref(), - ) - .split(area); + let chunks = Layout::default() + .direction(Direction::Horizontal) + .margin(1) + .constraints( + [ + Constraint::Percentage(40), + Constraint::Percentage(60), + ] + .as_ref(), + ) + .split(area); - f.render_widget(Clear, area); - f.render_widget( - Block::default() - .borders(Borders::TOP) - .title(Span::styled( - format!(" {}", self.title), - self.theme.title(true), - )) - .border_style(self.theme.block(true)), - area, - ); + self.draw_tree(f, chunks[0]); - self.draw_tree(f, chunks[0]); - - self.current_file.draw(f, chunks[1])?; - } + self.current_file.draw(f, chunks[1])?; Ok(()) } @@ -268,90 +247,60 @@ impl Component for RevisionFilesComponent { out: &mut Vec, force_all: bool, ) -> CommandBlocking { - if self.is_visible() || force_all { + if matches!(self.focus, Focus::Tree) || force_all { out.push( CommandInfo::new( - strings::commands::close_popup(&self.key_config), - true, + strings::commands::blame_file(&self.key_config), + self.tree.selected_file().is_some(), true, ) - .order(1), + .order(order::NAV), ); - - if matches!(self.focus, Focus::Tree) || force_all { - out.push( - CommandInfo::new( - strings::commands::blame_file( - &self.key_config, - ), - self.tree.selected_file().is_some(), - true, - ) - .order(order::NAV), - ); - tree_nav_cmds(&self.tree, &self.key_config, out); - } else { - self.current_file.commands(out, force_all); - } + tree_nav_cmds(&self.tree, &self.key_config, out); + } else { + self.current_file.commands(out, force_all); } - visibility_blocking(self) + CommandBlocking::PassingOn } fn event( &mut self, event: crossterm::event::Event, ) -> Result { - if self.is_visible() { - if let Event::Key(key) = event { - let is_tree_focused = - matches!(self.focus, Focus::Tree); - if key == self.key_config.exit_popup { + if let Event::Key(key) = event { + let is_tree_focused = matches!(self.focus, Focus::Tree); + if is_tree_focused + && tree_nav(&mut self.tree, &self.key_config, key) + { + self.selection_changed(); + return Ok(EventState::Consumed); + } else if key == self.key_config.blame { + if self.blame() { self.hide(); - } else if is_tree_focused - && tree_nav(&mut self.tree, &self.key_config, key) - { - self.selection_changed(); - } else if key == self.key_config.blame { - if self.blame() { - self.hide(); - } - } else if key == self.key_config.move_right { - if is_tree_focused { - self.focus = Focus::File; - self.current_file.focus(true); - self.focus(true); - } - } else if key == self.key_config.move_left { - if !is_tree_focused { - self.focus = Focus::Tree; - self.current_file.focus(false); - self.focus(false); - } - } else if !is_tree_focused { - self.current_file.event(event)?; + return Ok(EventState::Consumed); } + } else if key == self.key_config.move_right { + if is_tree_focused { + self.focus = Focus::File; + self.current_file.focus(true); + self.focus(true); + return Ok(EventState::Consumed); + } + } else if key == self.key_config.move_left { + if !is_tree_focused { + self.focus = Focus::Tree; + self.current_file.focus(false); + self.focus(false); + return Ok(EventState::Consumed); + } + } else if !is_tree_focused { + return self.current_file.event(event); } - - return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) } - - fn is_visible(&self) -> bool { - self.visible - } - - fn hide(&mut self) { - self.visible = false - } - - fn show(&mut self) -> Result<()> { - self.visible = true; - - Ok(()) - } } //TODO: reuse for other tree usages diff --git a/src/components/revision_files_popup.rs b/src/components/revision_files_popup.rs new file mode 100644 index 00000000..c65401ec --- /dev/null +++ b/src/components/revision_files_popup.rs @@ -0,0 +1,153 @@ +use super::{ + revision_files::RevisionFilesComponent, visibility_blocking, + CommandBlocking, CommandInfo, Component, DrawableComponent, + EventState, +}; +use crate::{ + keys::SharedKeyConfig, + queue::Queue, + strings::{self}, + ui::style::SharedTheme, +}; +use anyhow::Result; +use asyncgit::{sync::CommitId, AsyncNotification}; +use crossbeam_channel::Sender; +use crossterm::event::Event; +use tui::{ + backend::Backend, + layout::Rect, + text::Span, + widgets::{Block, Borders, Clear}, + Frame, +}; + +pub struct RevisionFilesPopup { + title: String, + theme: SharedTheme, + visible: bool, + key_config: SharedKeyConfig, + files: RevisionFilesComponent, +} + +impl RevisionFilesPopup { + /// + pub fn new( + queue: &Queue, + sender: &Sender, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + title: String::new(), + files: RevisionFilesComponent::new( + queue, + sender, + theme.clone(), + key_config.clone(), + ), + theme, + visible: false, + key_config, + } + } + + /// + pub fn open(&mut self, commit: CommitId) -> Result<()> { + self.files.set_commit(commit)?; + self.title = + format!("Files at [{}]", commit.get_short_string()); + self.show()?; + + Ok(()) + } + + /// + pub fn update(&mut self, ev: AsyncNotification) { + self.files.update(ev); + } + + /// + pub fn any_work_pending(&self) -> bool { + self.files.any_work_pending() + } +} + +impl DrawableComponent for RevisionFilesPopup { + fn draw( + &self, + f: &mut Frame, + area: Rect, + ) -> Result<()> { + if self.is_visible() { + f.render_widget(Clear, area); + f.render_widget( + Block::default() + .borders(Borders::TOP) + .title(Span::styled( + format!(" {}", self.title), + self.theme.title(true), + )) + .border_style(self.theme.block(true)), + area, + ); + + self.files.draw(f, area)?; + } + + Ok(()) + } +} + +impl Component for RevisionFilesPopup { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + out.push( + CommandInfo::new( + strings::commands::close_popup(&self.key_config), + true, + true, + ) + .order(1), + ); + + self.files.commands(out, force_all); + } + + visibility_blocking(self) + } + + fn event( + &mut self, + event: crossterm::event::Event, + ) -> Result { + if self.is_visible() { + if let Event::Key(key) = &event { + if *key == self.key_config.exit_popup { + self.hide(); + } + } + + return self.files.event(event); + } + + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } +} diff --git a/src/keys.rs b/src/keys.rs index ad34b895..d424785f 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -23,6 +23,7 @@ pub type SharedKeyConfig = Rc; pub struct KeyConfig { pub tab_status: KeyEvent, pub tab_log: KeyEvent, + pub tab_files: KeyEvent, pub tab_stashing: KeyEvent, pub tab_stashes: KeyEvent, pub tab_toggle: KeyEvent, @@ -88,8 +89,9 @@ impl Default for KeyConfig { Self { tab_status: KeyEvent { code: KeyCode::Char('1'), modifiers: KeyModifiers::empty()}, tab_log: KeyEvent { code: KeyCode::Char('2'), modifiers: KeyModifiers::empty()}, - tab_stashing: KeyEvent { code: KeyCode::Char('3'), modifiers: KeyModifiers::empty()}, - tab_stashes: KeyEvent { code: KeyCode::Char('4'), modifiers: KeyModifiers::empty()}, + tab_files: KeyEvent { code: KeyCode::Char('3'), modifiers: KeyModifiers::empty()}, + tab_stashing: KeyEvent { code: KeyCode::Char('4'), modifiers: KeyModifiers::empty()}, + tab_stashes: KeyEvent { code: KeyCode::Char('5'), modifiers: KeyModifiers::empty()}, tab_toggle: KeyEvent { code: KeyCode::Tab, modifiers: KeyModifiers::empty()}, tab_toggle_reverse: KeyEvent { code: KeyCode::BackTab, modifiers: KeyModifiers::SHIFT}, toggle_workarea: KeyEvent { code: KeyCode::Char('w'), modifiers: KeyModifiers::empty()}, diff --git a/src/strings.rs b/src/strings.rs index 3c91c016..b1eeabf1 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -41,6 +41,9 @@ pub fn tab_status(key_config: &SharedKeyConfig) -> String { pub fn tab_log(key_config: &SharedKeyConfig) -> String { format!("Log [{}]", key_config.get_hint(key_config.tab_log)) } +pub fn tab_files(key_config: &SharedKeyConfig) -> String { + format!("Files [{}]", key_config.get_hint(key_config.tab_files)) +} pub fn tab_stashing(key_config: &SharedKeyConfig) -> String { format!( "Stashing [{}]", diff --git a/src/tabs/files.rs b/src/tabs/files.rs new file mode 100644 index 00000000..17cf2db1 --- /dev/null +++ b/src/tabs/files.rs @@ -0,0 +1,127 @@ +#![allow( + dead_code, + clippy::missing_const_for_fn, + clippy::unused_self +)] + +use crate::{ + components::{ + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, RevisionFilesComponent, + }, + keys::SharedKeyConfig, + queue::Queue, + ui::style::SharedTheme, +}; +use anyhow::Result; +use asyncgit::{sync, AsyncNotification, CWD}; +use crossbeam_channel::Sender; + +pub struct FilesTab { + visible: bool, + theme: SharedTheme, + queue: Queue, + key_config: SharedKeyConfig, + files: RevisionFilesComponent, +} + +impl FilesTab { + /// + pub fn new( + sender: &Sender, + queue: &Queue, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + visible: false, + queue: queue.clone(), + files: RevisionFilesComponent::new( + queue, + sender, + theme.clone(), + key_config.clone(), + ), + theme, + key_config, + } + } + + /// + pub fn update(&mut self) -> Result<()> { + if self.is_visible() { + self.files.set_commit(sync::get_head(CWD)?)?; + } + + Ok(()) + } + + /// + pub fn anything_pending(&self) -> bool { + self.files.any_work_pending() + } + + /// + pub fn update_git( + &mut self, + ev: AsyncNotification, + ) -> Result<()> { + if self.is_visible() { + self.files.update(ev); + } + + Ok(()) + } +} + +impl DrawableComponent for FilesTab { + fn draw( + &self, + f: &mut tui::Frame, + rect: tui::layout::Rect, + ) -> Result<()> { + if self.is_visible() { + self.files.draw(f, rect)?; + } + Ok(()) + } +} + +impl Component for FilesTab { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.visible || force_all { + return self.files.commands(out, force_all); + } + + visibility_blocking(self) + } + + fn event( + &mut self, + ev: crossterm::event::Event, + ) -> Result { + if self.visible { + return self.files.event(ev); + } + + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + self.update()?; + Ok(()) + } +} diff --git a/src/tabs/mod.rs b/src/tabs/mod.rs index 3a546fde..6d8100f6 100644 --- a/src/tabs/mod.rs +++ b/src/tabs/mod.rs @@ -1,8 +1,10 @@ +mod files; mod revlog; mod stashing; mod stashlist; mod status; +pub use files::FilesTab; pub use revlog::Revlog; pub use stashing::{Stashing, StashingOptions}; pub use stashlist::StashList; diff --git a/src/tabs/stashlist.rs b/src/tabs/stashlist.rs index f9650f48..335a09bd 100644 --- a/src/tabs/stashlist.rs +++ b/src/tabs/stashlist.rs @@ -201,7 +201,6 @@ impl Component for StashList { self.drop_stash() } else if k == self.key_config.stash_open { self.inspect() - } else { } } } diff --git a/vim_style_key_config.ron b/vim_style_key_config.ron index 82bdb91a..f23e1d95 100644 --- a/vim_style_key_config.ron +++ b/vim_style_key_config.ron @@ -11,8 +11,9 @@ ( tab_status: ( code: Char('1'), modifiers: ( bits: 0,),), tab_log: ( code: Char('2'), modifiers: ( bits: 0,),), - tab_stashing: ( code: Char('3'), modifiers: ( bits: 0,),), - tab_stashes: ( code: Char('4'), modifiers: ( bits: 0,),), + tab_files: ( code: Char('3'), modifiers: ( bits: 0,),), + tab_stashing: ( code: Char('4'), modifiers: ( bits: 0,),), + tab_stashes: ( code: Char('5'), modifiers: ( bits: 0,),), tab_toggle: ( code: Tab, modifiers: ( bits: 0,),), tab_toggle_reverse: ( code: BackTab, modifiers: ( bits: 1,),),