From 284c57fb72abd1add2aac6f95583c0053da13775 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Sun, 6 Feb 2022 22:13:05 +0100 Subject: [PATCH] generic popup stacking solution (#1124) * generic popup stacking solution * allow going back to file-revision popup * do not select diff in coming back to files-revlog * handle filetree popup via stacking * allow going back to inspect commit * allow coming back to compare/inspect commit --- filetreelist/src/filetree.rs | 5 + src/app.rs | 76 +++++++++----- src/components/blame_file.rs | 140 ++++++++++++++++--------- src/components/branchlist.rs | 32 ++++-- src/components/changes.rs | 1 + src/components/commit_details/mod.rs | 9 ++ src/components/compare_commits.rs | 65 +++++++++--- src/components/file_revlog.rs | 111 ++++++++++++++++---- src/components/inspect_commit.rs | 106 ++++++++++++++----- src/components/mod.rs | 8 +- src/components/revision_files.rs | 58 ++++++++-- src/components/revision_files_popup.rs | 54 +++++++++- src/components/status_tree.rs | 72 ++++++++----- src/main.rs | 1 + src/popup_stack.rs | 16 +++ src/queue.rs | 40 ++++--- src/tabs/revlog.rs | 62 ++++++----- src/tabs/stashing.rs | 1 + src/tabs/stashlist.rs | 9 +- 19 files changed, 635 insertions(+), 231 deletions(-) create mode 100644 src/popup_stack.rs diff --git a/filetreelist/src/filetree.rs b/filetreelist/src/filetree.rs index bdde9cf0..7dd864ca 100644 --- a/filetreelist/src/filetree.rs +++ b/filetreelist/src/filetree.rs @@ -53,6 +53,11 @@ impl FileTree { self.items.file_count() == 0 } + /// + pub const fn selection(&self) -> Option { + self.selection + } + /// pub fn collapse_but_root(&mut self) { if !self.is_empty() { diff --git a/src/app.rs b/src/app.rs index 654830fc..5cfc7eb3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,7 +15,10 @@ use crate::{ }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, - queue::{Action, InternalEvent, NeedsUpdate, Queue}, + popup_stack::PopupStack, + queue::{ + Action, InternalEvent, NeedsUpdate, Queue, StackablePopupOpen, + }, setup_popups, strings::{self, order}, tabs::{FilesTab, Revlog, StashList, Stashing, Status}, @@ -79,6 +82,7 @@ pub struct App { theme: SharedTheme, key_config: SharedKeyConfig, input: Input, + popup_stack: PopupStack, // "Flags" requires_redraw: Cell, @@ -284,6 +288,7 @@ impl App { requires_redraw: Cell::new(false), file_to_open: None, repo, + popup_stack: PopupStack::default(), } } @@ -666,6 +671,31 @@ impl App { Ok(()) } + fn open_popup( + &mut self, + popup: StackablePopupOpen, + ) -> Result<()> { + match popup { + StackablePopupOpen::BlameFile(params) => { + self.blame_file_popup.open(params)?; + } + StackablePopupOpen::FileRevlog(param) => { + self.file_revlog_popup.open(param)?; + } + StackablePopupOpen::FileTree(param) => { + self.revision_files_popup.open(param)?; + } + StackablePopupOpen::InspectCommit(param) => { + self.inspect_commit_popup.open(param)?; + } + StackablePopupOpen::CompareCommits(param) => { + self.compare_commits_popup.open(param)?; + } + } + + Ok(()) + } + fn process_internal_events(&mut self) -> Result { let mut flags = NeedsUpdate::empty(); @@ -715,16 +745,7 @@ impl App { InternalEvent::TagCommit(id) => { self.tag_commit_popup.open(id)?; } - InternalEvent::BlameFile(path, commit_id) => { - self.blame_file_popup.open(&path, commit_id)?; - flags - .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); - } - InternalEvent::OpenFileRevlog(path) => { - self.file_revlog_popup.open(&path)?; - flags - .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); - } + InternalEvent::CreateBranch => { self.create_branch_popup.open()?; } @@ -739,11 +760,6 @@ impl App { self.tags_popup.open()?; } InternalEvent::TabSwitchStatus => self.set_tab(0)?, - InternalEvent::InspectCommit(id, tags) => { - self.inspect_commit_popup.open(id, tags)?; - flags - .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); - } InternalEvent::SelectCommitInRevlog(id) => { if let Err(error) = self.revlog.select_commit(id) { self.queue.push(InternalEvent::ShowErrorMsg( @@ -788,11 +804,6 @@ impl App { InternalEvent::StatusLastFileMoved => { self.status_tab.last_file_moved()?; } - InternalEvent::OpenFileTree(c) => { - self.revision_files_popup.open(c)?; - flags - .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); - } InternalEvent::OpenFileFinder(files) => { self.find_file_popup.open(&files)?; flags @@ -812,17 +823,30 @@ impl App { flags.insert(NeedsUpdate::ALL); } - InternalEvent::CompareCommits(id, other) => { - self.compare_commits_popup.open(id, other)?; - flags - .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); - } InternalEvent::FileFinderChanged(file) => { self.files_tab.file_finder_update(&file); self.revision_files_popup.file_finder_update(&file); flags .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); } + InternalEvent::OpenPopup(popup) => { + self.open_popup(popup)?; + flags + .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); + } + InternalEvent::PopupStackPop => { + if let Some(popup) = self.popup_stack.pop() { + self.open_popup(popup)?; + flags.insert( + NeedsUpdate::ALL | NeedsUpdate::COMMANDS, + ); + } + } + InternalEvent::PopupStackPush(popup) => { + self.popup_stack.push(popup); + flags + .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); + } }; Ok(flags) diff --git a/src/components/blame_file.rs b/src/components/blame_file.rs index 62512351..7411c297 100644 --- a/src/components/blame_file.rs +++ b/src/components/blame_file.rs @@ -1,11 +1,12 @@ use super::{ utils, visibility_blocking, CommandBlocking, CommandInfo, - Component, DrawableComponent, EventState, + Component, DrawableComponent, EventState, FileRevOpen, + InspectCommitOpen, }; use crate::{ components::{utils::string_width_align, ScrollType}, keys::SharedKeyConfig, - queue::{InternalEvent, Queue}, + queue::{InternalEvent, Queue, StackablePopupOpen}, string_utils::tabs_to_spaces, strings, ui::{self, style::SharedTheme}, @@ -27,41 +28,31 @@ use tui::{ Frame, }; +static NO_COMMIT_ID: &str = "0000000"; +static NO_AUTHOR: &str = ""; +static MIN_AUTHOR_WIDTH: usize = 3; +static MAX_AUTHOR_WIDTH: usize = 20; + +#[derive(Clone, Debug)] +pub struct BlameFileOpen { + pub file_path: String, + pub commit_id: Option, + pub selection: Option, +} + pub struct BlameFileComponent { title: String, theme: SharedTheme, queue: Queue, async_blame: AsyncBlame, visible: bool, + open_request: Option, params: Option, file_blame: Option, table_state: std::cell::Cell, key_config: SharedKeyConfig, current_height: std::cell::Cell, } - -static NO_COMMIT_ID: &str = "0000000"; -static NO_AUTHOR: &str = ""; -static MIN_AUTHOR_WIDTH: usize = 3; -static MAX_AUTHOR_WIDTH: usize = 20; - -fn get_author_width(width: usize) -> usize { - (width.saturating_sub(19) / 3) - .clamp(MIN_AUTHOR_WIDTH, MAX_AUTHOR_WIDTH) -} - -const fn number_of_digits(number: usize) -> usize { - let mut rest = number; - let mut result = 0; - - while rest > 0 { - rest /= 10; - result += 1; - } - - result -} - impl DrawableComponent for BlameFileComponent { fn draw( &self, @@ -197,7 +188,7 @@ impl Component for BlameFileComponent { if self.is_visible() { if let Event::Key(key) = event { if key == self.key_config.keys.exit_popup { - self.hide(); + self.hide_stacked(false); } else if key == self.key_config.keys.move_up { self.move_selection(ScrollType::Up); } else if key == self.key_config.keys.move_down { @@ -215,11 +206,13 @@ impl Component for BlameFileComponent { } else if key == self.key_config.keys.page_up { self.move_selection(ScrollType::PageUp); } else if key == self.key_config.keys.focus_right { - if let Some(id) = self.selected_commit() { - self.hide(); - self.queue.push( - InternalEvent::InspectCommit(id, None), - ); + if let Some(commit_id) = self.selected_commit() { + self.hide_stacked(true); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::InspectCommit( + InspectCommitOpen::new(commit_id), + ), + )); } } else if key == self.key_config.keys.file_history { if let Some(filepath) = self @@ -227,10 +220,12 @@ impl Component for BlameFileComponent { .as_ref() .map(|p| p.file_path.clone()) { - self.hide(); - self.queue.push( - InternalEvent::OpenFileRevlog(filepath), - ); + self.hide_stacked(true); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::FileRevlog( + FileRevOpen::new(filepath), + ), + )); } } @@ -245,10 +240,6 @@ impl Component for BlameFileComponent { self.visible } - fn hide(&mut self) { - self.visible = false; - } - fn show(&mut self) -> Result<()> { self.visible = true; @@ -277,25 +268,40 @@ impl BlameFileComponent { visible: false, params: None, file_blame: None, + open_request: None, table_state: std::cell::Cell::new(TableState::default()), key_config, current_height: std::cell::Cell::new(0), } } + fn hide_stacked(&mut self, stack: bool) { + self.visible = false; + if stack { + if let Some(request) = self.open_request.clone() { + self.queue.push(InternalEvent::PopupStackPush( + StackablePopupOpen::BlameFile(BlameFileOpen { + file_path: request.file_path, + commit_id: request.commit_id, + selection: self.get_selection(), + }), + )); + } + } else { + self.queue.push(InternalEvent::PopupStackPop); + } + } + /// - pub fn open( - &mut self, - file_path: &str, - commit_id: Option, - ) -> Result<()> { + pub fn open(&mut self, open: BlameFileOpen) -> Result<()> { + self.open_request = Some(open.clone()); self.params = Some(BlameParams { - file_path: file_path.into(), - commit_id, + file_path: open.file_path, + commit_id: open.commit_id, }); self.file_blame = None; self.table_state.get_mut().select(Some(0)); - self.show()?; + self.visible = true; self.update()?; @@ -329,6 +335,7 @@ impl BlameFileComponent { { if previous_blame_params == *params { self.file_blame = Some(last_file_blame); + self.set_open_selection(); return Ok(()); } @@ -526,6 +533,28 @@ impl BlameFileComponent { needs_update } + fn set_open_selection(&mut self) { + if let Some(selection) = + self.open_request.as_ref().and_then(|req| req.selection) + { + let mut table_state = self.table_state.take(); + table_state.select(Some(selection)); + self.table_state.set(table_state); + } + } + + fn get_selection(&self) -> Option { + self.file_blame.as_ref().and_then(|_| { + let table_state = self.table_state.take(); + + let selection = table_state.selected(); + + self.table_state.set(table_state); + + selection + }) + } + fn selected_commit(&self) -> Option { self.file_blame.as_ref().and_then(|file_blame| { let table_state = self.table_state.take(); @@ -544,3 +573,20 @@ impl BlameFileComponent { }) } } + +fn get_author_width(width: usize) -> usize { + (width.saturating_sub(19) / 3) + .clamp(MIN_AUTHOR_WIDTH, MAX_AUTHOR_WIDTH) +} + +const fn number_of_digits(number: usize) -> usize { + let mut rest = number; + let mut result = 0; + + while rest > 0 { + rest /= 10; + result += 1; + } + + result +} diff --git a/src/components/branchlist.rs b/src/components/branchlist.rs index 6f79892a..52b819c9 100644 --- a/src/components/branchlist.rs +++ b/src/components/branchlist.rs @@ -1,12 +1,14 @@ use super::{ utils::scroll_vertical::VerticalScroll, visibility_blocking, CommandBlocking, CommandInfo, Component, DrawableComponent, - EventState, + EventState, InspectCommitOpen, }; use crate::{ components::ScrollType, keys::SharedKeyConfig, - queue::{Action, InternalEvent, NeedsUpdate, Queue}, + queue::{ + Action, InternalEvent, NeedsUpdate, Queue, StackablePopupOpen, + }, strings, try_or_popup, ui::{self, Size}, }; @@ -286,18 +288,17 @@ impl Component for BranchListComponent { } else if e == self.key_config.keys.move_right && self.valid_selection() { - self.hide(); - if let Some(b) = self.get_selected() { - self.queue - .push(InternalEvent::InspectCommit(b, None)); - } + self.inspect_head_of_branch(); } else if e == self.key_config.keys.compare_commits && self.valid_selection() { self.hide(); - if let Some(b) = self.get_selected() { - self.queue - .push(InternalEvent::CompareCommits(b, None)); + if let Some(commit_id) = self.get_selected() { + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::CompareCommits( + InspectCommitOpen::new(commit_id), + ), + )); } } else if e == self.key_config.keys.pull && !self.local && self.has_remotes @@ -432,6 +433,17 @@ impl BranchListComponent { Ok(()) } + fn inspect_head_of_branch(&mut self) { + if let Some(commit_id) = self.get_selected() { + self.hide(); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::InspectCommit( + InspectCommitOpen::new(commit_id), + ), + )); + } + } + const fn get_branch_type(&self) -> BranchType { if self.local { BranchType::Local diff --git a/src/components/changes.rs b/src/components/changes.rs index 1f2ec9cd..7483512b 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -61,6 +61,7 @@ impl ChangesComponent { /// pub fn set_items(&mut self, list: &[StatusItem]) -> Result<()> { + self.files.show()?; self.files.update(list)?; Ok(()) } diff --git a/src/components/commit_details/mod.rs b/src/components/commit_details/mod.rs index 61aa5a44..7cf356b2 100644 --- a/src/components/commit_details/mod.rs +++ b/src/components/commit_details/mod.rs @@ -160,6 +160,10 @@ impl DrawableComponent for CommitDetailsComponent { f: &mut Frame, rect: Rect, ) -> Result<()> { + if !self.visible { + return Ok(()); + } + let constraints = if self.is_compare() { [Constraint::Length(10), Constraint::Min(0)] } else { @@ -215,6 +219,10 @@ impl Component for CommitDetailsComponent { if event_pump(ev, self.components_mut().as_mut_slice())? .is_consumed() { + if !self.file_tree.is_visible() { + self.hide(); + } + return Ok(EventState::Consumed); } @@ -250,6 +258,7 @@ impl Component for CommitDetailsComponent { } fn show(&mut self) -> Result<()> { self.visible = true; + self.file_tree.show()?; Ok(()) } diff --git a/src/components/compare_commits.rs b/src/components/compare_commits.rs index ac46c28f..7944c4b5 100644 --- a/src/components/compare_commits.rs +++ b/src/components/compare_commits.rs @@ -1,10 +1,13 @@ use super::{ command_pump, event_pump, visibility_blocking, CommandBlocking, CommandInfo, CommitDetailsComponent, Component, DiffComponent, - DrawableComponent, EventState, + DrawableComponent, EventState, InspectCommitOpen, }; use crate::{ - accessors, keys::SharedKeyConfig, queue::Queue, strings, + accessors, + keys::SharedKeyConfig, + queue::{InternalEvent, Queue, StackablePopupOpen}, + strings, ui::style::SharedTheme, }; use anyhow::Result; @@ -24,12 +27,13 @@ use tui::{ pub struct CompareCommitsComponent { repo: RepoPathRef, - commit_ids: Option<(CommitId, CommitId)>, + open_request: Option, diff: DiffComponent, details: CommitDetailsComponent, git_diff: AsyncDiff, visible: bool, key_config: SharedKeyConfig, + queue: Queue, } impl DrawableComponent for CompareCommitsComponent { @@ -109,12 +113,15 @@ impl Component for CompareCommitsComponent { if event_pump(ev, self.components_mut().as_mut_slice())? .is_consumed() { + if !self.details.is_visible() { + self.hide_stacked(true); + } return Ok(EventState::Consumed); } if let Event::Key(e) = ev { if e == self.key_config.keys.exit_popup { - self.hide(); + self.hide_stacked(false); } else if e == self.key_config.keys.focus_right && self.can_focus_diff() { @@ -126,7 +133,7 @@ impl Component for CompareCommitsComponent { self.details.focus(true); self.diff.focus(false); } else if e == self.key_config.keys.focus_left { - self.hide(); + self.hide_stacked(false); } return Ok(EventState::Consumed); @@ -179,25 +186,26 @@ impl CompareCommitsComponent { key_config.clone(), true, ), - commit_ids: None, + open_request: None, git_diff: AsyncDiff::new(repo.borrow().clone(), sender), visible: false, key_config, + queue: queue.clone(), } } /// - pub fn open( - &mut self, - id: CommitId, - other: Option, - ) -> Result<()> { - let other = if let Some(other) = other { - other + pub fn open(&mut self, open: InspectCommitOpen) -> Result<()> { + let compare_id = if let Some(compare_id) = open.compare_id { + compare_id } else { sync::get_head_tuple(&self.repo.borrow())?.id }; - self.commit_ids = Some((id, other)); + self.open_request = Some(InspectCommitOpen { + commit_id: open.commit_id, + compare_id: Some(compare_id), + tags: open.tags, + }); self.show()?; Ok(()) @@ -224,10 +232,22 @@ impl CompareCommitsComponent { Ok(()) } + fn get_ids(&self) -> Option<(CommitId, CommitId)> { + let other = self + .open_request + .as_ref() + .and_then(|open| open.compare_id); + + self.open_request + .as_ref() + .map(|open| open.commit_id) + .zip(other) + } + /// called when any tree component changed selection pub fn update_diff(&mut self) -> Result<()> { if self.is_visible() { - if let Some(ids) = self.commit_ids { + if let Some(ids) = self.get_ids() { if let Some(f) = self.details.files().selection_file() { let diff_params = DiffParams { @@ -259,7 +279,7 @@ impl CompareCommitsComponent { fn update(&mut self) -> Result<()> { self.details.set_commits( - self.commit_ids.map(CommitFilesParams::from), + self.get_ids().map(CommitFilesParams::from), None, )?; self.update_diff()?; @@ -270,4 +290,17 @@ impl CompareCommitsComponent { fn can_focus_diff(&self) -> bool { self.details.files().selection_file().is_some() } + + fn hide_stacked(&mut self, stack: bool) { + self.hide(); + if stack { + if let Some(request) = self.open_request.clone() { + self.queue.push(InternalEvent::PopupStackPush( + StackablePopupOpen::CompareCommits(request), + )); + } + } else { + self.queue.push(InternalEvent::PopupStackPop); + } + } } diff --git a/src/components/file_revlog.rs b/src/components/file_revlog.rs index 4ce7b122..da383865 100644 --- a/src/components/file_revlog.rs +++ b/src/components/file_revlog.rs @@ -1,5 +1,6 @@ -use super::visibility_blocking; use super::{utils::logitems::ItemBatch, SharedOptions}; +use super::{visibility_blocking, BlameFileOpen, InspectCommitOpen}; +use crate::queue::StackablePopupOpen; use crate::{ components::{ event_pump, CommandBlocking, CommandInfo, Component, @@ -31,6 +32,21 @@ use tui::{ const SLICE_SIZE: usize = 1200; +#[derive(Clone, Debug)] +pub struct FileRevOpen { + pub file_path: String, + pub selection: Option, +} + +impl FileRevOpen { + pub const fn new(file_path: String) -> Self { + Self { + file_path, + selection: None, + } + } +} + /// pub struct FileRevlogComponent { git_log: Option, @@ -41,7 +57,7 @@ pub struct FileRevlogComponent { diff: DiffComponent, visible: bool, repo_path: RepoPathRef, - file_path: Option, + open_request: Option, table_state: std::cell::Cell, items: ItemBatch, count_total: usize, @@ -79,7 +95,7 @@ impl FileRevlogComponent { ), visible: false, repo_path: repo_path.clone(), - file_path: None, + open_request: None, table_state: std::cell::Cell::new(TableState::default()), items: ItemBatch::default(), count_total: 0, @@ -95,12 +111,12 @@ impl FileRevlogComponent { } /// - pub fn open(&mut self, file_path: &str) -> Result<()> { - self.file_path = Some(file_path.into()); + pub fn open(&mut self, open_request: FileRevOpen) -> Result<()> { + self.open_request = Some(open_request.clone()); let filter = diff_contains_file( self.repo_path.borrow().clone(), - file_path.into(), + open_request.file_path, ); self.git_log = Some(AsyncLog::new( self.repo_path.borrow().clone(), @@ -109,6 +125,8 @@ impl FileRevlogComponent { )); self.table_state.get_mut().select(Some(0)); self.show()?; + + self.diff.focus(false); self.diff.clear(false); self.update()?; @@ -139,6 +157,7 @@ impl FileRevlogComponent { || log_changed { self.fetch_commits()?; + self.set_open_selection(); } self.update_diff()?; @@ -167,9 +186,9 @@ impl FileRevlogComponent { pub fn update_diff(&mut self) -> Result<()> { if self.is_visible() { if let Some(commit_id) = self.selected_commit() { - if let Some(file_path) = &self.file_path { + if let Some(open_request) = &self.open_request { let diff_params = DiffParams { - path: file_path.clone(), + path: open_request.file_path.clone(), diff_type: DiffType::Commit(commit_id), options: self.options.borrow().diff, }; @@ -179,7 +198,7 @@ impl FileRevlogComponent { { if params == diff_params { self.diff.update( - file_path.to_string(), + open_request.file_path.to_string(), false, last, ); @@ -253,11 +272,13 @@ impl FileRevlogComponent { }; let revisions = self.get_max_selection(); - self.file_path.as_ref().map_or( + self.open_request.as_ref().map_or( "".into(), - |file_path| { + |open_request| { strings::file_log_title( - file_path, selected, revisions, + &open_request.file_path, + selected, + revisions, ) }, ) @@ -333,6 +354,24 @@ impl FileRevlogComponent { needs_update } + fn set_open_selection(&mut self) { + if let Some(selection) = + self.open_request.as_ref().and_then(|req| req.selection) + { + let mut table_state = self.table_state.take(); + table_state.select(Some(selection)); + self.table_state.set(table_state); + } + } + + fn get_selection(&self) -> Option { + let table_state = self.table_state.take(); + let selection = table_state.selected(); + self.table_state.set(table_state); + + selection + } + fn draw_revlog(&self, f: &mut Frame, area: Rect) { let constraints = [ // type of change: (A)dded, (M)odified, (D)eleted @@ -377,6 +416,23 @@ impl FileRevlogComponent { self.current_width.set(area.width.into()); self.current_height.set(area.height.into()); } + + fn hide_stacked(&mut self, stack: bool) { + self.hide(); + + if stack { + if let Some(open_request) = self.open_request.clone() { + self.queue.push(InternalEvent::PopupStackPush( + StackablePopupOpen::FileRevlog(FileRevOpen { + file_path: open_request.file_path, + selection: self.get_selection(), + }), + )); + } + } else { + self.queue.push(InternalEvent::PopupStackPop); + } + } } impl DrawableComponent for FileRevlogComponent { @@ -427,7 +483,7 @@ impl Component for FileRevlogComponent { if let Event::Key(key) = event { if key == self.key_config.keys.exit_popup { - self.hide(); + self.hide_stacked(false); } else if key == self.key_config.keys.focus_right && self.can_focus_diff() { @@ -437,18 +493,27 @@ impl Component for FileRevlogComponent { self.diff.focus(false); } } else if key == self.key_config.keys.enter { - if let Some(id) = self.selected_commit() { - self.hide(); - self.queue.push( - InternalEvent::InspectCommit(id, None), - ); + if let Some(commit_id) = self.selected_commit() { + self.hide_stacked(true); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::InspectCommit( + InspectCommitOpen::new(commit_id), + ), + )); }; } else if key == self.key_config.keys.blame { - if let Some(file) = self.file_path.clone() { - self.hide(); - self.queue.push(InternalEvent::BlameFile( - file, - self.selected_commit(), + if let Some(open_request) = + self.open_request.clone() + { + self.hide_stacked(true); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::BlameFile( + BlameFileOpen { + file_path: open_request.file_path, + commit_id: self.selected_commit(), + selection: None, + }, + ), )); } } else if key == self.key_config.keys.move_up { diff --git a/src/components/inspect_commit.rs b/src/components/inspect_commit.rs index 2cfbe46f..b8286209 100644 --- a/src/components/inspect_commit.rs +++ b/src/components/inspect_commit.rs @@ -1,20 +1,19 @@ use super::{ command_pump, event_pump, visibility_blocking, CommandBlocking, CommandInfo, CommitDetailsComponent, Component, DiffComponent, - DrawableComponent, EventState, + DrawableComponent, EventState, FileTreeOpen, }; use crate::{ accessors, keys::SharedKeyConfig, - queue::{InternalEvent, Queue}, + queue::{InternalEvent, Queue, StackablePopupOpen}, strings, ui::style::SharedTheme, }; use anyhow::Result; use asyncgit::{ sync::{diff::DiffOptions, CommitId, CommitTags, RepoPathRef}, - AsyncDiff, AsyncGitNotification, CommitFilesParams, DiffParams, - DiffType, + AsyncDiff, AsyncGitNotification, DiffParams, DiffType, }; use crossbeam_channel::Sender; use crossterm::event::Event; @@ -25,10 +24,38 @@ use tui::{ Frame, }; +#[derive(Clone, Debug)] +pub struct InspectCommitOpen { + pub commit_id: CommitId, + /// in case we wanna compare + pub compare_id: Option, + pub tags: Option, +} + +impl InspectCommitOpen { + pub const fn new(commit_id: CommitId) -> Self { + Self { + commit_id, + compare_id: None, + tags: None, + } + } + + pub const fn new_with_tags( + commit_id: CommitId, + tags: Option, + ) -> Self { + Self { + commit_id, + compare_id: None, + tags, + } + } +} + pub struct InspectCommitComponent { queue: Queue, - commit_id: Option, - tags: Option, + open_request: Option, diff: DiffComponent, details: CommitDetailsComponent, git_diff: AsyncDiff, @@ -121,12 +148,16 @@ impl Component for InspectCommitComponent { if event_pump(ev, self.components_mut().as_mut_slice())? .is_consumed() { + if !self.details.is_visible() { + self.hide_stacked(true); + } + return Ok(EventState::Consumed); } if let Event::Key(e) = ev { if e == self.key_config.keys.exit_popup { - self.hide(); + self.hide_stacked(false); } else if e == self.key_config.keys.focus_right && self.can_focus_diff() { @@ -138,14 +169,20 @@ impl Component for InspectCommitComponent { self.details.focus(true); self.diff.focus(false); } else if e == self.key_config.keys.open_file_tree { - if let Some(commit) = self.commit_id { - self.queue.push(InternalEvent::OpenFileTree( - commit, + if let Some(commit) = self + .open_request + .as_ref() + .map(|open| open.commit_id) + { + self.hide_stacked(true); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::FileTree( + FileTreeOpen::new(commit), + ), )); - self.hide(); } } else if e == self.key_config.keys.focus_left { - self.hide(); + self.hide_stacked(false); } return Ok(EventState::Consumed); @@ -198,8 +235,7 @@ impl InspectCommitComponent { key_config.clone(), true, ), - commit_id: None, - tags: None, + open_request: None, git_diff: AsyncDiff::new(repo.borrow().clone(), sender), visible: false, key_config, @@ -207,13 +243,8 @@ impl InspectCommitComponent { } /// - pub fn open( - &mut self, - id: CommitId, - tags: Option, - ) -> Result<()> { - self.commit_id = Some(id); - self.tags = tags; + pub fn open(&mut self, open: InspectCommitOpen) -> Result<()> { + self.open_request = Some(open); self.show()?; Ok(()) @@ -243,12 +274,14 @@ impl InspectCommitComponent { /// 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(request) = &self.open_request { if let Some(f) = self.details.files().selection_file() { let diff_params = DiffParams { path: f.path.clone(), - diff_type: DiffType::Commit(id), + diff_type: DiffType::Commit( + request.commit_id, + ), options: DiffOptions::default(), }; @@ -274,11 +307,14 @@ impl InspectCommitComponent { } fn update(&mut self) -> Result<()> { - self.details.set_commits( - self.commit_id.map(CommitFilesParams::from), - self.tags.clone(), - )?; - self.update_diff()?; + if let Some(request) = &self.open_request { + //TODO: pass as reference and only clone if details changed + self.details.set_commits( + Some(request.commit_id.into()), + request.tags.clone(), + )?; + self.update_diff()?; + } Ok(()) } @@ -286,4 +322,18 @@ impl InspectCommitComponent { fn can_focus_diff(&self) -> bool { self.details.files().selection_file().is_some() } + + fn hide_stacked(&mut self, stack: bool) { + self.hide(); + + if stack { + if let Some(open_request) = self.open_request.take() { + self.queue.push(InternalEvent::PopupStackPush( + StackablePopupOpen::InspectCommit(open_request), + )); + } + } else { + self.queue.push(InternalEvent::PopupStackPop); + } + } } diff --git a/src/components/mod.rs b/src/components/mod.rs index cce907f5..3fc17291 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -33,7 +33,7 @@ mod textinput; mod utils; pub use self::status_tree::StatusTreeComponent; -pub use blame_file::BlameFileComponent; +pub use blame_file::{BlameFileComponent, BlameFileOpen}; pub use branchlist::BranchListComponent; pub use changes::ChangesComponent; pub use command::{CommandInfo, CommandText}; @@ -46,9 +46,9 @@ pub use diff::DiffComponent; pub use externaleditor::ExternalEditorComponent; pub use fetch::FetchComponent; pub use file_find_popup::FileFindPopup; -pub use file_revlog::FileRevlogComponent; +pub use file_revlog::{FileRevOpen, FileRevlogComponent}; pub use help::HelpComponent; -pub use inspect_commit::InspectCommitComponent; +pub use inspect_commit::{InspectCommitComponent, InspectCommitOpen}; pub use msg::MsgComponent; pub use options_popup::{ AppOption, OptionsPopupComponent, SharedOptions, @@ -59,7 +59,7 @@ pub use push_tags::PushTagsComponent; pub use rename_branch::RenameBranchComponent; pub use reset::ConfirmComponent; pub use revision_files::RevisionFilesComponent; -pub use revision_files_popup::RevisionFilesPopup; +pub use revision_files_popup::{FileTreeOpen, 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 df6b9d72..16c74d46 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -1,11 +1,11 @@ use super::{ - utils::scroll_vertical::VerticalScroll, CommandBlocking, - CommandInfo, Component, DrawableComponent, EventState, - SyntaxTextComponent, + utils::scroll_vertical::VerticalScroll, BlameFileOpen, + CommandBlocking, CommandInfo, Component, DrawableComponent, + EventState, FileRevOpen, SyntaxTextComponent, }; use crate::{ keys::SharedKeyConfig, - queue::{InternalEvent, Queue}, + queue::{InternalEvent, Queue, StackablePopupOpen}, strings::{self, order, symbol}, ui::{self, common_nav, style::SharedTheme}, AsyncAppNotification, AsyncNotification, @@ -42,6 +42,7 @@ pub struct RevisionFilesComponent { current_file: SyntaxTextComponent, tree: FileTree, scroll: VerticalScroll, + visible: bool, revision: Option, focus: Focus, key_config: SharedKeyConfig, @@ -72,11 +73,14 @@ impl RevisionFilesComponent { focus: Focus::Tree, key_config, repo, + visible: false, } } /// pub fn set_commit(&mut self, commit: CommitId) -> Result<()> { + self.show()?; + let same_id = self.revision.map(|c| c == commit).unwrap_or_default(); if !same_id { @@ -92,6 +96,16 @@ impl RevisionFilesComponent { Ok(()) } + /// + pub const fn revision(&self) -> Option { + self.revision + } + + /// + pub const fn selection(&self) -> Option { + self.tree.selection() + } + /// pub fn update(&mut self, ev: AsyncNotification) { self.current_file.update(ev); @@ -133,8 +147,13 @@ impl RevisionFilesComponent { fn blame(&self) -> bool { self.selected_file_path().map_or(false, |path| { - self.queue - .push(InternalEvent::BlameFile(path, self.revision)); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::BlameFile(BlameFileOpen { + file_path: path, + commit_id: self.revision, + selection: None, + }), + )); true }) @@ -142,7 +161,11 @@ impl RevisionFilesComponent { fn file_history(&self) -> bool { self.selected_file_path().map_or(false, |path| { - self.queue.push(InternalEvent::OpenFileRevlog(path)); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::FileRevlog(FileRevOpen::new( + path, + )), + )); true }) @@ -278,6 +301,10 @@ impl Component for RevisionFilesComponent { out: &mut Vec, force_all: bool, ) -> CommandBlocking { + if !self.is_visible() && !force_all { + return CommandBlocking::PassingOn; + } + let is_tree_focused = matches!(self.focus, Focus::Tree); if is_tree_focused || force_all { @@ -316,6 +343,10 @@ impl Component for RevisionFilesComponent { &mut self, event: crossterm::event::Event, ) -> Result { + if !self.is_visible() { + return Ok(EventState::NotConsumed); + } + if let Event::Key(key) = event { let is_tree_focused = matches!(self.focus, Focus::Tree); if is_tree_focused @@ -371,6 +402,19 @@ impl Component for RevisionFilesComponent { Ok(EventState::NotConsumed) } + + fn hide(&mut self) { + self.visible = false; + } + + fn is_visible(&self) -> bool { + self.visible + } + + 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 index 6b5123a1..84cc3648 100644 --- a/src/components/revision_files_popup.rs +++ b/src/components/revision_files_popup.rs @@ -7,7 +7,7 @@ use super::{ }; use crate::{ keys::SharedKeyConfig, - queue::Queue, + queue::{InternalEvent, Queue, StackablePopupOpen}, strings::{self}, ui::style::SharedTheme, AsyncAppNotification, AsyncNotification, @@ -18,10 +18,27 @@ use crossbeam_channel::Sender; use crossterm::event::Event; use tui::{backend::Backend, layout::Rect, widgets::Clear, Frame}; +#[derive(Clone, Debug)] +pub struct FileTreeOpen { + pub commit_id: CommitId, + pub selection: Option, +} + +impl FileTreeOpen { + pub const fn new(commit_id: CommitId) -> Self { + Self { + commit_id, + selection: None, + } + } +} + pub struct RevisionFilesPopup { + open_request: Option, visible: bool, key_config: SharedKeyConfig, files: RevisionFilesComponent, + queue: Queue, } impl RevisionFilesPopup { @@ -43,12 +60,15 @@ impl RevisionFilesPopup { ), visible: false, key_config, + open_request: None, + queue: queue.clone(), } } /// - pub fn open(&mut self, commit: CommitId) -> Result<()> { - self.files.set_commit(commit)?; + pub fn open(&mut self, request: FileTreeOpen) -> Result<()> { + self.files.set_commit(request.commit_id)?; + self.open_request = Some(request); self.show()?; Ok(()) @@ -67,6 +87,23 @@ impl RevisionFilesPopup { pub fn file_finder_update(&mut self, file: &Option) { self.files.find_file(file); } + + fn hide_stacked(&mut self, stack: bool) { + self.hide(); + + if stack { + if let Some(revision) = self.files.revision() { + self.queue.push(InternalEvent::PopupStackPush( + StackablePopupOpen::FileTree(FileTreeOpen { + commit_id: revision, + selection: self.files.selection(), + }), + )); + } + } else { + self.queue.push(InternalEvent::PopupStackPop); + } + } } impl DrawableComponent for RevisionFilesPopup { @@ -114,11 +151,18 @@ impl Component for RevisionFilesPopup { if self.is_visible() { if let Event::Key(key) = &event { if *key == self.key_config.keys.exit_popup { - self.hide(); + self.hide_stacked(false); } } - return self.files.event(event); + let res = self.files.event(event)?; + //Note: if this made the files hide we need to stack the popup + if res == EventState::Consumed && !self.files.is_visible() + { + self.hide_stacked(true); + } + + return Ok(res); } Ok(EventState::NotConsumed) diff --git a/src/components/status_tree.rs b/src/components/status_tree.rs index c2a2104b..41bb8f25 100644 --- a/src/components/status_tree.rs +++ b/src/components/status_tree.rs @@ -3,12 +3,12 @@ use super::{ filetree::{FileTreeItem, FileTreeItemKind}, statustree::{MoveSelection, StatusTree}, }, - CommandBlocking, DrawableComponent, + BlameFileOpen, CommandBlocking, DrawableComponent, FileRevOpen, }; use crate::{ components::{CommandInfo, Component, EventState}, keys::SharedKeyConfig, - queue::{InternalEvent, NeedsUpdate, Queue}, + queue::{InternalEvent, NeedsUpdate, Queue, StackablePopupOpen}, strings::{self, order}, ui, ui::style::SharedTheme, @@ -22,6 +22,7 @@ use tui::{backend::Backend, layout::Rect, text::Span, Frame}; //TODO: use new `filetreelist` crate /// +#[allow(clippy::struct_excessive_bools)] pub struct StatusTreeComponent { title: String, tree: StatusTree, @@ -33,6 +34,7 @@ pub struct StatusTreeComponent { theme: SharedTheme, key_config: SharedKeyConfig, scroll_top: Cell, + visible: bool, } impl StatusTreeComponent { @@ -55,6 +57,7 @@ impl StatusTreeComponent { key_config, scroll_top: Cell::new(0), pending: true, + visible: false, } } @@ -313,6 +316,10 @@ impl DrawableComponent for StatusTreeComponent { f: &mut Frame, r: Rect, ) -> Result<()> { + if !self.is_visible() { + return Ok(()); + } + if self.pending { let items = vec![Span::styled( Cow::from(strings::loading_text(&self.key_config)), @@ -416,31 +423,35 @@ impl Component for StatusTreeComponent { if self.focused { if let Event::Key(e) = ev { return if e == self.key_config.keys.blame { - match (&self.queue, self.selection_file()) { - (Some(queue), Some(status_item)) => { - //TODO: use correct revision here - queue.push(InternalEvent::BlameFile( - status_item.path, - None, - )); - - Ok(EventState::Consumed) - } - _ => Ok(EventState::NotConsumed), - } - } else if e == self.key_config.keys.file_history { - match (&self.queue, self.selection_file()) { - (Some(queue), Some(status_item)) => { - queue.push( - InternalEvent::OpenFileRevlog( - status_item.path, + if let Some(status_item) = self.selection_file() { + self.hide(); + if let Some(queue) = &self.queue { + queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::BlameFile( + BlameFileOpen { + file_path: status_item.path, + commit_id: None, + selection: None, + }, ), - ); - - Ok(EventState::Consumed) + )); } - _ => Ok(EventState::NotConsumed), } + Ok(EventState::Consumed) + } else if e == self.key_config.keys.file_history { + if let Some(status_item) = self.selection_file() { + self.hide(); + if let Some(queue) = &self.queue { + queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::FileRevlog( + FileRevOpen::new( + status_item.path, + ), + ), + )); + } + } + Ok(EventState::Consumed) } else if e == self.key_config.keys.move_down { Ok(self .move_selection(MoveSelection::Down) @@ -481,6 +492,19 @@ impl Component for StatusTreeComponent { self.focused = focus; self.show_selection(focus); } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + Ok(()) + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index e36ef645..9f85240f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ mod components; mod input; mod keys; mod notify_mutex; +mod popup_stack; mod profiler; mod queue; mod spinner; diff --git a/src/popup_stack.rs b/src/popup_stack.rs new file mode 100644 index 00000000..dfef56cf --- /dev/null +++ b/src/popup_stack.rs @@ -0,0 +1,16 @@ +use crate::queue::StackablePopupOpen; + +#[derive(Default)] +pub struct PopupStack { + stack: Vec, +} + +impl PopupStack { + pub fn push(&mut self, popup: StackablePopupOpen) { + self.stack.push(popup); + } + + pub fn pop(&mut self) -> Option { + self.stack.pop() + } +} diff --git a/src/queue.rs b/src/queue.rs index d10d6dd1..3c404de8 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,6 +1,12 @@ -use crate::{components::AppOption, tabs::StashingOptions}; +use crate::{ + components::{ + AppOption, BlameFileOpen, FileRevOpen, FileTreeOpen, + InspectCommitOpen, + }, + tabs::StashingOptions, +}; use asyncgit::{ - sync::{diff::DiffLinePosition, CommitId, CommitTags, TreeFile}, + sync::{diff::DiffLinePosition, CommitId, TreeFile}, PushType, }; use bitflags::bitflags; @@ -48,6 +54,20 @@ pub enum Action { AbortRevert, } +#[derive(Debug)] +pub enum StackablePopupOpen { + /// + BlameFile(BlameFileOpen), + /// + FileRevlog(FileRevOpen), + /// + FileTree(FileTreeOpen), + /// + InspectCommit(InspectCommitOpen), + /// + CompareCommits(InspectCommitOpen), +} + /// pub enum InternalEvent { /// @@ -69,20 +89,12 @@ pub enum InternalEvent { /// TabSwitchStatus, /// - InspectCommit(CommitId, Option), - /// - CompareCommits(CommitId, Option), - /// SelectCommitInRevlog(CommitId), /// TagCommit(CommitId), /// Tags, /// - BlameFile(String, Option), - /// - OpenFileRevlog(String), - /// CreateBranch, /// RenameBranch(String, String), @@ -97,8 +109,6 @@ pub enum InternalEvent { /// PushTags, /// - OpenFileTree(CommitId), - /// OptionSwitched(AppOption), /// OpenFileFinder(Vec), @@ -106,6 +116,12 @@ pub enum InternalEvent { FileFinderChanged(Option), /// FetchRemotes, + /// + OpenPopup(StackablePopupOpen), + /// + PopupStackPop, + /// + PopupStackPush(StackablePopupOpen), } /// single threaded simple queue for components to communicate with each other diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index b2b5dff9..41fc7f8b 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -2,10 +2,11 @@ use crate::{ components::{ visibility_blocking, CommandBlocking, CommandInfo, CommitDetailsComponent, CommitList, Component, - DrawableComponent, EventState, + DrawableComponent, EventState, FileTreeOpen, + InspectCommitOpen, }, keys::SharedKeyConfig, - queue::{InternalEvent, Queue}, + queue::{InternalEvent, Queue, StackablePopupOpen}, strings, try_or_popup, ui::style::SharedTheme, }; @@ -199,6 +200,17 @@ impl Revlog { Ok(()) } + + fn inspect_commit(&self) { + if let Some(commit_id) = self.selected_commit() { + let tags = self.selected_commit_tags(&Some(commit_id)); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::InspectCommit( + InspectCommitOpen::new_with_tags(commit_id, tags), + ), + )); + } + } } impl DrawableComponent for Revlog { @@ -260,20 +272,8 @@ impl Component for Revlog { } else if k == self.key_config.keys.focus_right && self.commit_details.is_visible() { - return self.selected_commit().map_or( - Ok(EventState::NotConsumed), - |id| { - self.queue.push( - InternalEvent::InspectCommit( - id, - self.selected_commit_tags(&Some( - id, - )), - ), - ); - Ok(EventState::Consumed) - }, - ); + self.inspect_commit(); + return Ok(EventState::Consumed); } else if k == self.key_config.keys.select_branch { self.queue.push(InternalEvent::SelectBranch); return Ok(EventState::Consumed); @@ -291,7 +291,11 @@ impl Component for Revlog { Ok(EventState::NotConsumed), |id| { self.queue.push( - InternalEvent::OpenFileTree(id), + InternalEvent::OpenPopup( + StackablePopupOpen::FileTree( + FileTreeOpen::new(id), + ), + ), ); Ok(EventState::Consumed) }, @@ -304,22 +308,26 @@ impl Component for Revlog { { if self.list.marked_count() == 1 { // compare against head - self.queue.push( - InternalEvent::CompareCommits( - self.list.marked()[0], - None, + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::CompareCommits( + InspectCommitOpen::new( + self.list.marked()[0], + ), ), - ); + )); return Ok(EventState::Consumed); } else if self.list.marked_count() == 2 { //compare two marked commits let marked = self.list.marked(); - self.queue.push( - InternalEvent::CompareCommits( - marked[0], - Some(marked[1]), + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::CompareCommits( + InspectCommitOpen { + commit_id: marked[0], + compare_id: Some(marked[1]), + tags: None, + }, ), - ); + )); return Ok(EventState::Consumed); } } diff --git a/src/tabs/stashing.rs b/src/tabs/stashing.rs index d0e9a165..63750d52 100644 --- a/src/tabs/stashing.rs +++ b/src/tabs/stashing.rs @@ -269,6 +269,7 @@ impl Component for Stashing { self.options.stash_untracked = !config_untracked_files.include_none(); + self.index.show()?; self.visible = true; self.update()?; Ok(()) diff --git a/src/tabs/stashlist.rs b/src/tabs/stashlist.rs index 0fd0aba9..3b39cf55 100644 --- a/src/tabs/stashlist.rs +++ b/src/tabs/stashlist.rs @@ -2,9 +2,10 @@ use crate::{ components::{ visibility_blocking, CommandBlocking, CommandInfo, CommitList, Component, DrawableComponent, EventState, + InspectCommitOpen, }, keys::SharedKeyConfig, - queue::{Action, InternalEvent, Queue}, + queue::{Action, InternalEvent, Queue, StackablePopupOpen}, strings, ui::style::SharedTheme, }; @@ -96,7 +97,11 @@ impl StashList { fn inspect(&mut self) { if let Some(e) = self.list.selected_entry() { - self.queue.push(InternalEvent::InspectCommit(e.id, None)); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::InspectCommit( + InspectCommitOpen::new(e.id), + ), + )); } }