diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c09db3..9f98af7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +**options** + +![options](assets/options.gif) + **drop multiple stashes** ![drop-multiple-stashes](assets/drop-multiple-stashes.gif) @@ -16,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ![name-validation](assets/branch-validation.gif) ## Added +- new options popup (show untracked files, diff settings) ([#849](https://github.com/extrawurst/gitui/issues/849)) - mark and drop multiple stashes ([#854](https://github.com/extrawurst/gitui/issues/854)) - check branch name validity while typing ([#559](https://github.com/extrawurst/gitui/issues/559)) - support deleting remote branch [[@zcorniere](https://github.com/zcorniere)] ([#622](https://github.com/extrawurst/gitui/issues/622)) diff --git a/assets/options.drawio b/assets/options.drawio new file mode 100644 index 00000000..08d787a1 --- /dev/null +++ b/assets/options.drawio @@ -0,0 +1 @@ +5Zldc6IwFIZ/DZfrEJCvS0Xb7exHZ9bd6WxvdlIIkGlMmBir3V+/iQQVg+12Bqm7eqHwkkB43nMCJ1puPF9fc1gWX1iKiOXY6dpyJ5bjABCG8kcpz5Xi+0El5BynutFOmOHfSIu2Vpc4RYtGQ8EYEbhsigmjFCWioUHO2arZLGOkedUS5sgQZgkkpnqHU1FUaugEO/0jwnlRXxn4UXVkDuvG+k4WBUzZak9yp5Ybc8ZEtTVfx4goeDWXqt/VkaPbgXFExd90KLNx+X10/wl4v+6/5fx28jMVH/RZniBZ6hvWgxXPNQHOljRF6iS25Y5XBRZoVsJEHV1Jz6VWiDmRe0Bu6tMhLtD66DjB9u5l2CA2R4I/yyZ1h0B30REDvGDgVcpq54AbaazFHn0n1CLUrufbs+/AyA3N5g2cnPPj5Hh+g5Njm5S2wbdPCYTeiSi5BqVZIUPesX9QwWHyKOkcUpP3L4c1hgTnVCoEZWpXgcEyCUdanuM0VT3GTcgZJiRmhPHNudxs8+mIrn9A17UNutE2MBtBeKoYHBp0JyiDSyKOQd2Lt4Xg7BHVrCij6ACflmofEokJ8ZecaAvwpjsduDC03RrxCz74bVPBqVzwDBfuFInFhoR8DhWQ5vKB1b0lr6VGL4Y4fjBwDhwJTUcA6Dc1fMOUm5wyjv7nzBgOgteN6DU1AsOFmNENccf+jKnKCh/OFQ76sCi3HC4iS1zbNCfs05zQMGd4UdnRZkCv2REZBmDFqljSRymTKj8uJx+clvepPu2oC8w9P+wT8D/jhGhxoNeEAGbxORNQLM8zDTL5KJvpIQ07cKTtTQq8e40BzEJ3gmVNdamO+O/viFlU35YCM3r+aaL2CXxAZMx4ivjBNbtwzIsOZzUQvb9jZoloWPWmxaIDdzoB11zdcD2TWuva0fBk0MwSLt6U0moAkKby/SiXm0xORfZyt5pky2BW7032DZVf9ePDtrzxNRYJoxmWvWJ1aLMQpba+oWTJF/gJWd7kWAp1uxzVZm8nJgaDenl962PLGiDwt9PavpXuyaxsqQMJWxwtxs9u7Q8EzewYAq9tBbrXOcUs32YrGeFFFezW1LGiqTUC1vTKGodWeKWUUWyF3k7pK9xPMD+1OxBEfTpg1m9f4VOFfzD4p+DWL94vh3c3cOXu7i+uzbG9Pwrd6R8= \ No newline at end of file diff --git a/assets/options.gif b/assets/options.gif new file mode 100644 index 00000000..a99eefa4 Binary files /dev/null and b/assets/options.gif differ diff --git a/asyncgit/src/diff.rs b/asyncgit/src/diff.rs index 06c5e38f..58a506e8 100644 --- a/asyncgit/src/diff.rs +++ b/asyncgit/src/diff.rs @@ -1,7 +1,7 @@ use crate::{ error::Result, hash, - sync::{self, CommitId}, + sync::{self, diff::DiffOptions, CommitId}, AsyncGitNotification, FileDiff, CWD, }; use crossbeam_channel::Sender; @@ -14,7 +14,7 @@ use std::{ }; /// -#[derive(Hash, Clone, PartialEq)] +#[derive(Debug, Hash, Clone, PartialEq)] pub enum DiffType { /// diff in a given commit Commit(CommitId), @@ -25,12 +25,14 @@ pub enum DiffType { } /// -#[derive(Hash, Clone, PartialEq)] +#[derive(Debug, Hash, Clone, PartialEq)] pub struct DiffParams { /// path to the file to diff pub path: String, /// what kind of diff pub diff_type: DiffType, + /// diff options + pub options: DiffOptions, } struct Request(R, Option); @@ -87,7 +89,7 @@ impl AsyncDiff { &mut self, params: DiffParams, ) -> Result> { - log::trace!("request"); + log::trace!("request {:?}", params); let hash = hash(¶ms); @@ -148,12 +150,18 @@ impl AsyncDiff { hash: u64, ) -> Result { let res = match params.diff_type { - DiffType::Stage => { - sync::diff::get_diff(CWD, ¶ms.path, true)? - } - DiffType::WorkDir => { - sync::diff::get_diff(CWD, ¶ms.path, false)? - } + DiffType::Stage => sync::diff::get_diff( + CWD, + ¶ms.path, + true, + Some(params.options), + )?, + DiffType::WorkDir => sync::diff::get_diff( + CWD, + ¶ms.path, + false, + Some(params.options), + )?, DiffType::Commit(id) => sync::diff::get_diff_commit( CWD, id, diff --git a/asyncgit/src/status.rs b/asyncgit/src/status.rs index 7d76e25e..384d2409 100644 --- a/asyncgit/src/status.rs +++ b/asyncgit/src/status.rs @@ -1,7 +1,7 @@ use crate::{ error::Result, hash, - sync::{self, status::StatusType}, + sync::{self, status::StatusType, ShowUntrackedFilesConfig}, AsyncGitNotification, StatusItem, CWD, }; use crossbeam_channel::Sender; @@ -31,14 +31,19 @@ pub struct Status { pub struct StatusParams { tick: u128, status_type: StatusType, + config: Option, } impl StatusParams { /// - pub fn new(status_type: StatusType) -> Self { + pub fn new( + status_type: StatusType, + config: Option, + ) -> Self { Self { tick: current_tick(), status_type, + config, } } } @@ -109,12 +114,14 @@ impl AsyncStatus { let sender = self.sender.clone(); let arc_pending = Arc::clone(&self.pending); let status_type = params.status_type; + let config = params.config; self.pending.fetch_add(1, Ordering::Relaxed); rayon_core::spawn(move || { let ok = Self::fetch_helper( status_type, + config, hash_request, &arc_current, &arc_last, @@ -135,11 +142,12 @@ impl AsyncStatus { fn fetch_helper( status_type: StatusType, + config: Option, hash_request: u64, arc_current: &Arc>>, arc_last: &Arc>, ) -> Result<()> { - let res = Self::get_status(status_type)?; + let res = Self::get_status(status_type, config)?; log::trace!( "status fetched: {} (type: {:?})", hash_request, @@ -161,9 +169,16 @@ impl AsyncStatus { Ok(()) } - fn get_status(status_type: StatusType) -> Result { + fn get_status( + status_type: StatusType, + config: Option, + ) -> Result { Ok(Status { - items: sync::status::get_status(CWD, status_type)?, + items: sync::status::get_status( + CWD, + status_type, + config, + )?, }) } } diff --git a/asyncgit/src/sync/config.rs b/asyncgit/src/sync/config.rs index dacb380f..7c66bf62 100644 --- a/asyncgit/src/sync/config.rs +++ b/asyncgit/src/sync/config.rs @@ -5,6 +5,7 @@ use scopetime::scope_time; // see https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles /// represents the `status.showUntrackedFiles` git config state +#[derive(Hash, Copy, Clone, PartialEq)] pub enum ShowUntrackedFilesConfig { /// No, @@ -14,19 +15,25 @@ pub enum ShowUntrackedFilesConfig { All, } +impl Default for ShowUntrackedFilesConfig { + fn default() -> Self { + Self::No + } +} + impl ShowUntrackedFilesConfig { /// - pub const fn include_none(&self) -> bool { + pub const fn include_none(self) -> bool { matches!(self, Self::No) } /// - pub const fn include_untracked(&self) -> bool { + pub const fn include_untracked(self) -> bool { matches!(self, Self::Normal | Self::All) } /// - pub const fn recurse_untracked_dirs(&self) -> bool { + pub const fn recurse_untracked_dirs(self) -> bool { matches!(self, Self::All) } } diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs index 6c5750b0..4b9b5952 100644 --- a/asyncgit/src/sync/diff.rs +++ b/asyncgit/src/sync/diff.rs @@ -8,8 +8,7 @@ use super::{ use crate::{error::Error, error::Result, hash}; use easy_cast::Conv; use git2::{ - Delta, Diff, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch, - Repository, + Delta, Diff, DiffDelta, DiffFormat, DiffHunk, Patch, Repository, }; use scopetime::scope_time; use std::{cell::RefCell, fs, path::Path, rc::Rc}; @@ -125,18 +124,41 @@ pub struct FileDiff { pub size_delta: i64, } +/// see +#[derive(Debug, Hash, Clone, Copy, PartialEq)] +pub struct DiffOptions { + /// see + pub ignore_whitespace: bool, + /// see + pub context: u32, + /// see + pub interhunk_lines: u32, +} + +impl Default for DiffOptions { + fn default() -> Self { + Self { + ignore_whitespace: false, + context: 3, + interhunk_lines: 0, + } + } +} + pub(crate) fn get_diff_raw<'a>( repo: &'a Repository, p: &str, stage: bool, reverse: bool, - context: Option, + options: Option, ) -> Result> { // scope_time!("get_diff_raw"); - let mut opt = DiffOptions::new(); - if let Some(context) = context { - opt.context_lines(context); + let mut opt = git2::DiffOptions::new(); + if let Some(options) = options { + opt.context_lines(options.context); + opt.ignore_whitespace(options.ignore_whitespace); + opt.interhunk_lines(options.interhunk_lines); } opt.pathspec(p); opt.reverse(reverse); @@ -173,12 +195,13 @@ pub fn get_diff( repo_path: &str, p: &str, stage: bool, + options: Option, ) -> Result { scope_time!("get_diff"); let repo = utils::repo(repo_path)?; let work_dir = work_dir(&repo)?; - let diff = get_diff_raw(&repo, p, stage, false, None)?; + let diff = get_diff_raw(&repo, p, stage, false, options)?; raw_diff_to_file_diff(&diff, work_dir) } @@ -386,7 +409,8 @@ mod tests { assert_eq!(get_statuses(repo_path), (1, 0)); - let diff = get_diff(repo_path, "foo/bar.txt", false).unwrap(); + let diff = + get_diff(repo_path, "foo/bar.txt", false, None).unwrap(); assert_eq!(diff.hunks.len(), 1); assert_eq!(diff.hunks[0].lines[1].content, "test\n"); @@ -412,9 +436,13 @@ mod tests { assert_eq!(get_statuses(repo_path), (0, 1)); - let diff = - get_diff(repo_path, file_path.to_str().unwrap(), true) - .unwrap(); + let diff = get_diff( + repo_path, + file_path.to_str().unwrap(), + true, + None, + ) + .unwrap(); assert_eq!(diff.hunks.len(), 1); } @@ -462,8 +490,8 @@ mod tests { .unwrap(); } - let res = - get_status(repo_path, StatusType::WorkingDir).unwrap(); + let res = get_status(repo_path, StatusType::WorkingDir, None) + .unwrap(); assert_eq!(res.len(), 1); assert_eq!(res[0].path, "bar.txt"); @@ -480,7 +508,8 @@ mod tests { assert_eq!(get_statuses(repo_path), (1, 1)); - let res = get_diff(repo_path, "bar.txt", false).unwrap(); + let res = + get_diff(repo_path, "bar.txt", false, None).unwrap(); assert_eq!(res.hunks.len(), 2) } @@ -503,6 +532,7 @@ mod tests { sub_path.to_str().unwrap(), file_path.to_str().unwrap(), false, + None, ) .unwrap(); @@ -525,9 +555,13 @@ mod tests { File::create(&root.join(file_path))? .write_all(b"\x00\x02")?; - let diff = - get_diff(repo_path, file_path.to_str().unwrap(), false) - .unwrap(); + let diff = get_diff( + repo_path, + file_path.to_str().unwrap(), + false, + None, + ) + .unwrap(); dbg!(&diff); assert_eq!(diff.sizes, (1, 2)); @@ -546,9 +580,13 @@ mod tests { File::create(&root.join(file_path))? .write_all(b"\x00\xc7")?; - let diff = - get_diff(repo_path, file_path.to_str().unwrap(), false) - .unwrap(); + let diff = get_diff( + repo_path, + file_path.to_str().unwrap(), + false, + None, + ) + .unwrap(); dbg!(&diff); assert_eq!(diff.sizes, (0, 2)); diff --git a/asyncgit/src/sync/hunks.rs b/asyncgit/src/sync/hunks.rs index 9cc9b2f0..333cd500 100644 --- a/asyncgit/src/sync/hunks.rs +++ b/asyncgit/src/sync/hunks.rs @@ -173,6 +173,7 @@ mod tests { sub_path.to_str().unwrap(), file_path.to_str().unwrap(), false, + None, )?; assert!(reset_hunk( diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 6d8f4b20..58b128fb 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -255,10 +255,12 @@ mod tests { /// helper returning amount of files with changes in the (wd,stage) pub fn get_statuses(repo_path: &str) -> (usize, usize) { ( - get_status(repo_path, StatusType::WorkingDir) + get_status(repo_path, StatusType::WorkingDir, None) + .unwrap() + .len(), + get_status(repo_path, StatusType::Stage, None) .unwrap() .len(), - get_status(repo_path, StatusType::Stage).unwrap().len(), ) } diff --git a/asyncgit/src/sync/patches.rs b/asyncgit/src/sync/patches.rs index 79db2967..dae51160 100644 --- a/asyncgit/src/sync/patches.rs +++ b/asyncgit/src/sync/patches.rs @@ -1,4 +1,4 @@ -use super::diff::{get_diff_raw, HunkHeader}; +use super::diff::{get_diff_raw, DiffOptions, HunkHeader}; use crate::error::{Error, Result}; use git2::{Diff, DiffLine, Patch, Repository}; @@ -15,7 +15,16 @@ pub(crate) fn get_file_diff_patch_and_hunklines<'a>( is_staged: bool, reverse: bool, ) -> Result<(Patch<'a>, Vec>)> { - let diff = get_diff_raw(repo, file, is_staged, reverse, Some(1))?; + let diff = get_diff_raw( + repo, + file, + is_staged, + reverse, + Some(DiffOptions { + context: 1, + ..DiffOptions::default() + }), + )?; let patches = get_patches(&diff)?; if patches.len() > 1 { return Err(Error::Generic(String::from("patch error"))); diff --git a/asyncgit/src/sync/reset.rs b/asyncgit/src/sync/reset.rs index c1b8c456..4798f07a 100644 --- a/asyncgit/src/sync/reset.rs +++ b/asyncgit/src/sync/reset.rs @@ -88,8 +88,8 @@ mod tests { let root = repo.path().parent().unwrap(); let repo_path = root.as_os_str().to_str().unwrap(); - let res = - get_status(repo_path, StatusType::WorkingDir).unwrap(); + let res = get_status(repo_path, StatusType::WorkingDir, None) + .unwrap(); assert_eq!(res.len(), 0); let file_path = root.join("bar.txt"); diff --git a/asyncgit/src/sync/staging/stage_tracked.rs b/asyncgit/src/sync/staging/stage_tracked.rs index 1a9f7f2b..efb2f15a 100644 --- a/asyncgit/src/sync/staging/stage_tracked.rs +++ b/asyncgit/src/sync/staging/stage_tracked.rs @@ -97,7 +97,7 @@ mod test { ) .unwrap(); - let diff = get_diff(path, "test.txt", true).unwrap(); + let diff = get_diff(path, "test.txt", true, None).unwrap(); assert_eq!(diff.lines, 3); assert_eq!( @@ -139,7 +139,7 @@ c = 4"; ) .unwrap(); - let diff = get_diff(path, "test.txt", true).unwrap(); + let diff = get_diff(path, "test.txt", true, None).unwrap(); assert_eq!(diff.lines, 5); assert_eq!( @@ -172,7 +172,8 @@ c = 4"; assert_eq!(get_statuses(path), (0, 1)); - let diff_before = get_diff(path, "test.txt", true).unwrap(); + let diff_before = + get_diff(path, "test.txt", true, None).unwrap(); assert_eq!(diff_before.lines, 5); @@ -189,7 +190,7 @@ c = 4"; assert_eq!(get_statuses(path), (1, 1)); - let diff = get_diff(path, "test.txt", true).unwrap(); + let diff = get_diff(path, "test.txt", true, None).unwrap(); assert_eq!(diff.lines, 4); } diff --git a/asyncgit/src/sync/status.rs b/asyncgit/src/sync/status.rs index fcecf5b6..5196e6f7 100644 --- a/asyncgit/src/sync/status.rs +++ b/asyncgit/src/sync/status.rs @@ -9,6 +9,8 @@ use git2::{Delta, Status, StatusOptions, StatusShow}; use scopetime::scope_time; use std::path::Path; +use super::ShowUntrackedFilesConfig; + /// #[derive(Copy, Clone, Hash, PartialEq, Debug)] pub enum StatusItemType { @@ -96,12 +98,17 @@ impl From for StatusShow { pub fn get_status( repo_path: &str, status_type: StatusType, + show_untracked: Option, ) -> Result> { scope_time!("get_status"); let repo = utils::repo(repo_path)?; - let show_untracked = untracked_files_config_repo(&repo)?; + let show_untracked = if let Some(config) = show_untracked { + config + } else { + untracked_files_config_repo(&repo)? + }; let mut options = StatusOptions::default(); options diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index 887194be..38016cf5 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -278,7 +278,7 @@ mod tests { let repo_path = root.as_os_str().to_str().unwrap(); let status_count = |s: StatusType| -> usize { - get_status(repo_path, s).unwrap().len() + get_status(repo_path, s, None).unwrap().len() }; fs::create_dir_all(&root.join("a/d"))?; @@ -329,7 +329,8 @@ mod tests { assert_eq!(get_statuses(repo_path), (0, 1)); // And that file is test.txt - let diff = get_diff(repo_path, "test.txt", true).unwrap(); + let diff = + get_diff(repo_path, "test.txt", true, None).unwrap(); assert_eq!( diff.hunks[0].lines[0].content, String::from("@@ -1 +1 @@\n") @@ -371,7 +372,7 @@ mod tests { let repo_path = root.as_os_str().to_str().unwrap(); let status_count = |s: StatusType| -> usize { - get_status(repo_path, s).unwrap().len() + get_status(repo_path, s, None).unwrap().len() }; let full_path = &root.join(file_path); @@ -405,7 +406,7 @@ mod tests { let repo_path = root.as_os_str().to_str().unwrap(); let status_count = |s: StatusType| -> usize { - get_status(repo_path, s).unwrap().len() + get_status(repo_path, s, None).unwrap().len() }; let sub = &root.join("sub"); diff --git a/src/app.rs b/src/app.rs index e938547a..d17c0853 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,14 +2,15 @@ use crate::{ accessors, cmdbar::CommandBar, components::{ - event_pump, BlameFileComponent, BranchListComponent, - CommandBlocking, CommandInfo, CommitComponent, Component, - ConfirmComponent, CreateBranchComponent, DrawableComponent, + event_pump, AppOption, BlameFileComponent, + BranchListComponent, CommandBlocking, CommandInfo, + CommitComponent, Component, ConfirmComponent, + CreateBranchComponent, DrawableComponent, ExternalEditorComponent, HelpComponent, - InspectCommitComponent, MsgComponent, PullComponent, - PushComponent, PushTagsComponent, RenameBranchComponent, - RevisionFilesPopup, StashMsgComponent, TagCommitComponent, - TagListComponent, + InspectCommitComponent, MsgComponent, OptionsPopupComponent, + PullComponent, PushComponent, PushTagsComponent, + RenameBranchComponent, RevisionFilesPopup, SharedOptions, + StashMsgComponent, TagCommitComponent, TagListComponent, }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, @@ -56,6 +57,7 @@ pub struct App { create_branch_popup: CreateBranchComponent, rename_branch_popup: RenameBranchComponent, select_branch_popup: BranchListComponent, + options_popup: OptionsPopupComponent, tags_popup: TagListComponent, cmdbar: RefCell, tab: usize, @@ -88,6 +90,7 @@ impl App { let queue = Queue::new(); let theme = Rc::new(theme); let key_config = Rc::new(key_config); + let options = SharedOptions::default(); Self { input, @@ -173,6 +176,12 @@ impl App { theme.clone(), key_config.clone(), ), + options_popup: OptionsPopupComponent::new( + &queue, + theme.clone(), + key_config.clone(), + options.clone(), + ), do_quit: false, cmdbar: RefCell::new(CommandBar::new( theme.clone(), @@ -195,6 +204,7 @@ impl App { sender, theme.clone(), key_config.clone(), + options, ), stashing_tab: Stashing::new( sender, @@ -291,6 +301,9 @@ impl App { } else if k == self.key_config.cmd_bar_toggle { self.cmdbar.borrow_mut().toggle_more(); NeedsUpdate::empty() + } else if k == self.key_config.open_options { + self.options_popup.show()?; + NeedsUpdate::ALL } else { NeedsUpdate::empty() }; @@ -426,6 +439,7 @@ impl App { select_branch_popup, revision_files_popup, tags_popup, + options_popup, help, revlog, status_tab, @@ -453,6 +467,7 @@ impl App { push_popup, push_tags_popup, pull_popup, + options_popup, reset, msg ] @@ -665,6 +680,20 @@ impl App { flags .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); } + InternalEvent::OptionSwitched(o) => { + match o { + AppOption::StatusShowUntracked => { + self.status_tab.update()?; + } + AppOption::DiffContextLines + | AppOption::DiffIgnoreWhitespaces + | AppOption::DiffInterhunkLines => { + self.status_tab.update_diff()?; + } + } + + flags.insert(NeedsUpdate::ALL); + } }; Ok(flags) @@ -784,6 +813,14 @@ impl App { ) .order(order::NAV), ); + res.push( + CommandInfo::new( + strings::commands::options_popup(&self.key_config), + true, + !self.any_popup_visible(), + ) + .order(order::NAV), + ); res.push( CommandInfo::new( diff --git a/src/components/inspect_commit.rs b/src/components/inspect_commit.rs index ae910486..1e09bb7d 100644 --- a/src/components/inspect_commit.rs +++ b/src/components/inspect_commit.rs @@ -12,7 +12,7 @@ use crate::{ }; use anyhow::Result; use asyncgit::{ - sync::{CommitId, CommitTags}, + sync::{diff::DiffOptions, CommitId, CommitTags}, AsyncDiff, AsyncGitNotification, DiffParams, DiffType, }; use crossbeam_channel::Sender; @@ -245,6 +245,7 @@ impl InspectCommitComponent { let diff_params = DiffParams { path: f.path.clone(), diff_type: DiffType::Commit(id), + options: DiffOptions::default(), }; if let Some((params, last)) = diff --git a/src/components/mod.rs b/src/components/mod.rs index 358013cf..36a75bdc 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -13,6 +13,7 @@ mod filetree; mod help; mod inspect_commit; mod msg; +mod options_popup; mod pull; mod push; mod push_tags; @@ -41,6 +42,9 @@ pub use externaleditor::ExternalEditorComponent; pub use help::HelpComponent; pub use inspect_commit::InspectCommitComponent; pub use msg::MsgComponent; +pub use options_popup::{ + AppOption, OptionsPopupComponent, SharedOptions, +}; pub use pull::PullComponent; pub use push::PushComponent; pub use push_tags::PushTagsComponent; diff --git a/src/components/options_popup.rs b/src/components/options_popup.rs new file mode 100644 index 00000000..73f70a8c --- /dev/null +++ b/src/components/options_popup.rs @@ -0,0 +1,381 @@ +#![allow(dead_code)] + +use std::{cell::RefCell, rc::Rc}; + +use super::{ + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, +}; +use crate::{ + components::utils::string_width_align, + keys::SharedKeyConfig, + queue::{InternalEvent, Queue}, + strings::{self}, + ui::{self, style::SharedTheme}, +}; +use anyhow::Result; +use asyncgit::sync::{diff::DiffOptions, ShowUntrackedFilesConfig}; +use crossterm::event::Event; +use tui::{ + backend::Backend, + layout::{Alignment, Rect}, + style::{Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Clear, Paragraph}, + Frame, +}; + +#[derive(Clone, Copy, PartialEq)] +pub enum AppOption { + StatusShowUntracked, + DiffIgnoreWhitespaces, + DiffContextLines, + DiffInterhunkLines, +} + +#[derive(Default, Copy, Clone)] +pub struct Options { + pub status_show_untracked: Option, + pub diff: DiffOptions, +} + +pub type SharedOptions = Rc>; + +pub struct OptionsPopupComponent { + selection: AppOption, + queue: Queue, + visible: bool, + key_config: SharedKeyConfig, + options: SharedOptions, + theme: SharedTheme, +} + +impl OptionsPopupComponent { + /// + pub fn new( + queue: &Queue, + theme: SharedTheme, + key_config: SharedKeyConfig, + options: SharedOptions, + ) -> Self { + Self { + selection: AppOption::StatusShowUntracked, + queue: queue.clone(), + visible: false, + key_config, + options, + theme, + } + } + + fn get_text(&self, width: u16) -> Vec { + let mut txt: Vec = Vec::with_capacity(10); + + self.add_status(&mut txt, width); + + txt + } + + fn add_status(&self, txt: &mut Vec, width: u16) { + Self::add_header(txt, "Status"); + + self.add_entry( + txt, + width, + "Show untracked", + match self.options.borrow().status_show_untracked { + None => "Gitconfig", + Some(ShowUntrackedFilesConfig::No) => "No", + Some(ShowUntrackedFilesConfig::Normal) => "Normal", + Some(ShowUntrackedFilesConfig::All) => "All", + }, + self.is_select(AppOption::StatusShowUntracked), + ); + Self::add_header(txt, ""); + + Self::add_header(txt, "Diff"); + self.add_entry( + txt, + width, + "Ignore whitespaces", + &self.options.borrow().diff.ignore_whitespace.to_string(), + self.is_select(AppOption::DiffIgnoreWhitespaces), + ); + self.add_entry( + txt, + width, + "Context lines", + &self.options.borrow().diff.context.to_string(), + self.is_select(AppOption::DiffContextLines), + ); + self.add_entry( + txt, + width, + "Inter hunk lines", + &self.options.borrow().diff.interhunk_lines.to_string(), + self.is_select(AppOption::DiffInterhunkLines), + ); + } + + fn is_select(&self, kind: AppOption) -> bool { + self.selection == kind + } + + fn add_header(txt: &mut Vec, header: &'static str) { + txt.push(Spans::from(vec![Span::styled( + header, + //TODO: + Style::default().add_modifier(Modifier::UNDERLINED), + )])); + } + + fn add_entry( + &self, + txt: &mut Vec, + width: u16, + entry: &'static str, + value: &str, + selected: bool, + ) { + let half = usize::from(width / 2); + txt.push(Spans::from(vec![ + Span::styled( + string_width_align(entry, half), + self.theme.text(true, false), + ), + Span::styled( + format!("{:^w$}", value, w = half), + self.theme.text(true, selected), + ), + ])); + } + + fn move_selection(&mut self, up: bool) { + if up { + self.selection = match self.selection { + AppOption::StatusShowUntracked => { + AppOption::DiffInterhunkLines + } + AppOption::DiffIgnoreWhitespaces => { + AppOption::StatusShowUntracked + } + AppOption::DiffContextLines => { + AppOption::DiffIgnoreWhitespaces + } + AppOption::DiffInterhunkLines => { + AppOption::DiffContextLines + } + }; + } else { + self.selection = match self.selection { + AppOption::StatusShowUntracked => { + AppOption::DiffIgnoreWhitespaces + } + AppOption::DiffIgnoreWhitespaces => { + AppOption::DiffContextLines + } + AppOption::DiffContextLines => { + AppOption::DiffInterhunkLines + } + AppOption::DiffInterhunkLines => { + AppOption::StatusShowUntracked + } + }; + } + } + + fn switch_option(&mut self, right: bool) { + if right { + match self.selection { + AppOption::StatusShowUntracked => { + let untracked = + self.options.borrow().status_show_untracked; + + let untracked = match untracked { + None => { + Some(ShowUntrackedFilesConfig::Normal) + } + Some(ShowUntrackedFilesConfig::Normal) => { + Some(ShowUntrackedFilesConfig::All) + } + Some(ShowUntrackedFilesConfig::All) => { + Some(ShowUntrackedFilesConfig::No) + } + Some(ShowUntrackedFilesConfig::No) => None, + }; + + self.options.borrow_mut().status_show_untracked = + untracked; + } + AppOption::DiffIgnoreWhitespaces => { + let old = + self.options.borrow().diff.ignore_whitespace; + self.options + .borrow_mut() + .diff + .ignore_whitespace = !old; + } + AppOption::DiffContextLines => { + let old = self.options.borrow().diff.context; + self.options.borrow_mut().diff.context = + old.saturating_add(1); + } + AppOption::DiffInterhunkLines => { + let old = + self.options.borrow().diff.interhunk_lines; + self.options.borrow_mut().diff.interhunk_lines = + old.saturating_add(1); + } + }; + } else { + match self.selection { + AppOption::StatusShowUntracked => { + let untracked = + self.options.borrow().status_show_untracked; + + let untracked = match untracked { + None => Some(ShowUntrackedFilesConfig::No), + Some(ShowUntrackedFilesConfig::No) => { + Some(ShowUntrackedFilesConfig::All) + } + Some(ShowUntrackedFilesConfig::All) => { + Some(ShowUntrackedFilesConfig::Normal) + } + Some(ShowUntrackedFilesConfig::Normal) => { + None + } + }; + + self.options.borrow_mut().status_show_untracked = + untracked; + } + AppOption::DiffIgnoreWhitespaces => { + let old = + self.options.borrow().diff.ignore_whitespace; + self.options + .borrow_mut() + .diff + .ignore_whitespace = !old; + } + AppOption::DiffContextLines => { + let old = self.options.borrow().diff.context; + self.options.borrow_mut().diff.context = + old.saturating_sub(1); + } + AppOption::DiffInterhunkLines => { + let old = + self.options.borrow().diff.interhunk_lines; + self.options.borrow_mut().diff.interhunk_lines = + old.saturating_sub(1); + } + }; + } + + self.queue + .push(InternalEvent::OptionSwitched(self.selection)); + } +} + +impl DrawableComponent for OptionsPopupComponent { + fn draw( + &self, + f: &mut Frame, + area: Rect, + ) -> Result<()> { + if self.is_visible() { + const SIZE: (u16, u16) = (50, 10); + let area = + ui::centered_rect_absolute(SIZE.0, SIZE.1, area); + + let width = area.width; + + f.render_widget(Clear, area); + f.render_widget( + Paragraph::new(self.get_text(width)) + .block( + Block::default() + .borders(Borders::ALL) + .title(Span::styled( + "Options", + self.theme.title(true), + )) + .border_style(self.theme.block(true)), + ) + .alignment(Alignment::Left), + area, + ); + } + + Ok(()) + } +} + +impl Component for OptionsPopupComponent { + 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), + ); + out.push( + CommandInfo::new( + strings::commands::navigate_tree( + &self.key_config, + ), + true, + true, + ) + .order(1), + ); + } + + 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(); + } else if *key == self.key_config.move_up { + self.move_selection(true); + } else if *key == self.key_config.move_down { + self.move_selection(false); + } else if *key == self.key_config.move_right { + self.switch_option(true); + } else if *key == self.key_config.move_left { + self.switch_option(false); + } + } + + 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(()) + } +} diff --git a/src/keys.rs b/src/keys.rs index 9666abcd..c52d2436 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -39,6 +39,7 @@ pub struct KeyConfig { pub open_commit: KeyEvent, pub open_commit_editor: KeyEvent, pub open_help: KeyEvent, + pub open_options: KeyEvent, pub move_left: KeyEvent, pub move_right: KeyEvent, pub tree_collapse_recursive: KeyEvent, @@ -108,6 +109,7 @@ impl Default for KeyConfig { open_commit: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()}, open_commit_editor: KeyEvent { code: KeyCode::Char('e'), modifiers:KeyModifiers::CONTROL}, open_help: KeyEvent { code: KeyCode::Char('h'), modifiers: KeyModifiers::empty()}, + open_options: KeyEvent { code: KeyCode::Char('o'), modifiers: KeyModifiers::empty()}, move_left: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::empty()}, move_right: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::empty()}, tree_collapse_recursive: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::SHIFT}, diff --git a/src/queue.rs b/src/queue.rs index ff1c00dc..411e16e2 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,4 +1,4 @@ -use crate::tabs::StashingOptions; +use crate::{components::AppOption, tabs::StashingOptions}; use asyncgit::sync::{diff::DiffLinePosition, CommitId, CommitTags}; use bitflags::bitflags; use std::{cell::RefCell, collections::VecDeque, rc::Rc}; @@ -83,6 +83,8 @@ pub enum InternalEvent { PushTags, /// OpenFileTree(CommitId), + /// + OptionSwitched(AppOption), } /// single threaded simple queue for components to communicate with each other diff --git a/src/strings.rs b/src/strings.rs index 6f1f3fef..d802b886 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -373,6 +373,18 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + pub fn options_popup( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Options [{}]", + key_config.get_hint(key_config.open_options), + ), + "open options popup", + CMD_GROUP_GENERAL, + ) + } pub fn help_open(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!( diff --git a/src/tabs/stashing.rs b/src/tabs/stashing.rs index bbbc513d..dde72c88 100644 --- a/src/tabs/stashing.rs +++ b/src/tabs/stashing.rs @@ -74,7 +74,8 @@ impl Stashing { pub fn update(&mut self) -> Result<()> { if self.is_visible() { self.git_status - .fetch(&StatusParams::new(StatusType::Both))?; + //TODO: support options + .fetch(&StatusParams::new(StatusType::Both, None))?; } Ok(()) diff --git a/src/tabs/status.rs b/src/tabs/status.rs index d766fcd1..3163182b 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -4,7 +4,7 @@ use crate::{ command_pump, event_pump, visibility_blocking, ChangesComponent, CommandBlocking, CommandInfo, Component, DiffComponent, DrawableComponent, EventState, - FileTreeItemKind, + FileTreeItemKind, SharedOptions, }, keys::SharedKeyConfig, queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem}, @@ -70,6 +70,7 @@ pub struct Status { git_branch_name: cached::BranchName, queue: Queue, git_action_executed: bool, + options: SharedOptions, key_config: SharedKeyConfig, } @@ -134,6 +135,7 @@ impl Status { sender: &Sender, theme: SharedTheme, key_config: SharedKeyConfig, + options: SharedOptions, ) -> Self { Self { queue: queue.clone(), @@ -169,6 +171,7 @@ impl Status { git_branch_state: None, git_branch_name: cached::BranchName::new(CWD), key_config, + options, } } @@ -317,11 +320,17 @@ impl Status { self.git_branch_name.lookup().map(Some).unwrap_or(None); if self.is_visible() { + let config = self.options.borrow().status_show_untracked; + self.git_diff.refresh()?; - self.git_status_workdir - .fetch(&StatusParams::new(StatusType::WorkingDir))?; - self.git_status_stage - .fetch(&StatusParams::new(StatusType::Stage))?; + self.git_status_workdir.fetch(&StatusParams::new( + StatusType::WorkingDir, + config, + ))?; + self.git_status_stage.fetch(&StatusParams::new( + StatusType::Stage, + config, + ))?; self.branch_compare(); } @@ -394,6 +403,7 @@ impl Status { let diff_params = DiffParams { path: path.clone(), diff_type, + options: self.options.borrow().diff, }; if self.diff.current() == (path.clone(), is_stage) { @@ -401,18 +411,20 @@ impl Status { // maybe the diff changed (outside file change) if let Some((params, last)) = self.git_diff.last()? { if params == diff_params { + // all params match, so we might need to update self.diff.update(path, is_stage, last); + } else { + // params changed, we need to request the right diff + self.request_diff( + diff_params, + path, + is_stage, + )?; } } } else { // we dont show the right diff right now, so we need to request - if let Some(diff) = - self.git_diff.request(diff_params)? - { - self.diff.update(path, is_stage, diff); - } else { - self.diff.clear(true); - } + self.request_diff(diff_params, path, is_stage)?; } } else { self.diff.clear(false); @@ -421,6 +433,21 @@ impl Status { Ok(()) } + fn request_diff( + &mut self, + diff_params: DiffParams, + path: String, + is_stage: bool, + ) -> Result<(), anyhow::Error> { + if let Some(diff) = self.git_diff.request(diff_params)? { + self.diff.update(path, is_stage, diff); + } else { + self.diff.clear(true); + } + + Ok(()) + } + /// called after confirmation pub fn reset(&mut self, item: &ResetItem) -> bool { if let Err(e) = sync::reset_workdir(CWD, item.path.as_str()) { diff --git a/vim_style_key_config.ron b/vim_style_key_config.ron index 75000130..10cf9f10 100644 --- a/vim_style_key_config.ron +++ b/vim_style_key_config.ron @@ -25,6 +25,7 @@ focus_below: ( code: Char('j'), modifiers: ( bits: 0,),), open_help: ( code: F(1), modifiers: ( bits: 0,),), + open_options: ( code: Char('o'), modifiers: ( bits: 0,),), exit: ( code: Char('c'), modifiers: ( bits: 2,),), quit: ( code: Char('q'), modifiers: ( bits: 0,),),