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
This commit is contained in:
Stephan Dilly 2022-02-06 22:13:05 +01:00 committed by GitHub
parent e9d8de1be4
commit 284c57fb72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 635 additions and 231 deletions

View file

@ -53,6 +53,11 @@ impl FileTree {
self.items.file_count() == 0
}
///
pub const fn selection(&self) -> Option<usize> {
self.selection
}
///
pub fn collapse_but_root(&mut self) {
if !self.is_empty() {

View file

@ -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<bool>,
@ -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<NeedsUpdate> {
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)

View file

@ -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 = "<no author>";
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<CommitId>,
pub selection: Option<usize>,
}
pub struct BlameFileComponent {
title: String,
theme: SharedTheme,
queue: Queue,
async_blame: AsyncBlame,
visible: bool,
open_request: Option<BlameFileOpen>,
params: Option<BlameParams>,
file_blame: Option<FileBlame>,
table_state: std::cell::Cell<TableState>,
key_config: SharedKeyConfig,
current_height: std::cell::Cell<usize>,
}
static NO_COMMIT_ID: &str = "0000000";
static NO_AUTHOR: &str = "<no author>";
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<B: Backend>(
&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<CommitId>,
) -> 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<usize> {
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<CommitId> {
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
}

View file

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

View file

@ -61,6 +61,7 @@ impl ChangesComponent {
///
pub fn set_items(&mut self, list: &[StatusItem]) -> Result<()> {
self.files.show()?;
self.files.update(list)?;
Ok(())
}

View file

@ -160,6 +160,10 @@ impl DrawableComponent for CommitDetailsComponent {
f: &mut Frame<B>,
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(())
}

View file

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

View file

@ -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<usize>,
}
impl FileRevOpen {
pub const fn new(file_path: String) -> Self {
Self {
file_path,
selection: None,
}
}
}
///
pub struct FileRevlogComponent {
git_log: Option<AsyncLog>,
@ -41,7 +57,7 @@ pub struct FileRevlogComponent {
diff: DiffComponent,
visible: bool,
repo_path: RepoPathRef,
file_path: Option<String>,
open_request: Option<FileRevOpen>,
table_state: std::cell::Cell<TableState>,
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(
"<no history available>".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<usize> {
let table_state = self.table_state.take();
let selection = table_state.selected();
self.table_state.set(table_state);
selection
}
fn draw_revlog<B: Backend>(&self, f: &mut Frame<B>, 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 {

View file

@ -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<CommitId>,
pub tags: Option<CommitTags>,
}
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<CommitTags>,
) -> Self {
Self {
commit_id,
compare_id: None,
tags,
}
}
}
pub struct InspectCommitComponent {
queue: Queue,
commit_id: Option<CommitId>,
tags: Option<CommitTags>,
open_request: Option<InspectCommitOpen>,
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<CommitTags>,
) -> 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);
}
}
}

View file

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

View file

@ -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<CommitId>,
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<CommitId> {
self.revision
}
///
pub const fn selection(&self) -> Option<usize> {
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<CommandInfo>,
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<EventState> {
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

View file

@ -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<usize>,
}
impl FileTreeOpen {
pub const fn new(commit_id: CommitId) -> Self {
Self {
commit_id,
selection: None,
}
}
}
pub struct RevisionFilesPopup {
open_request: Option<FileTreeOpen>,
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<PathBuf>) {
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)

View file

@ -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<usize>,
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<B>,
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)]

View file

@ -27,6 +27,7 @@ mod components;
mod input;
mod keys;
mod notify_mutex;
mod popup_stack;
mod profiler;
mod queue;
mod spinner;

16
src/popup_stack.rs Normal file
View file

@ -0,0 +1,16 @@
use crate::queue::StackablePopupOpen;
#[derive(Default)]
pub struct PopupStack {
stack: Vec<StackablePopupOpen>,
}
impl PopupStack {
pub fn push(&mut self, popup: StackablePopupOpen) {
self.stack.push(popup);
}
pub fn pop(&mut self) -> Option<StackablePopupOpen> {
self.stack.pop()
}
}

View file

@ -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<CommitTags>),
///
CompareCommits(CommitId, Option<CommitId>),
///
SelectCommitInRevlog(CommitId),
///
TagCommit(CommitId),
///
Tags,
///
BlameFile(String, Option<CommitId>),
///
OpenFileRevlog(String),
///
CreateBranch,
///
RenameBranch(String, String),
@ -97,8 +109,6 @@ pub enum InternalEvent {
///
PushTags,
///
OpenFileTree(CommitId),
///
OptionSwitched(AppOption),
///
OpenFileFinder(Vec<TreeFile>),
@ -106,6 +116,12 @@ pub enum InternalEvent {
FileFinderChanged(Option<PathBuf>),
///
FetchRemotes,
///
OpenPopup(StackablePopupOpen),
///
PopupStackPop,
///
PopupStackPush(StackablePopupOpen),
}
/// single threaded simple queue for components to communicate with each other

View file

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

View file

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

View file

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