mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
stashing support (#70)
This commit is contained in:
parent
5936757538
commit
3c5e86eee9
18 changed files with 1297 additions and 560 deletions
1
assets/stashing.drawio
Normal file
1
assets/stashing.drawio
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -8,12 +8,14 @@ mod diff;
|
|||
mod error;
|
||||
mod revlog;
|
||||
mod status;
|
||||
mod status2;
|
||||
pub mod sync;
|
||||
|
||||
pub use crate::{
|
||||
diff::{AsyncDiff, DiffParams},
|
||||
revlog::AsyncLog,
|
||||
status::AsyncStatus,
|
||||
status2::{AsyncStatus2, StatusParams},
|
||||
sync::{
|
||||
diff::{DiffLine, DiffLineType, FileDiff},
|
||||
status::{StatusItem, StatusItemType},
|
||||
|
|
|
|||
160
asyncgit/src/status2.rs
Normal file
160
asyncgit/src/status2.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
use crate::{
|
||||
current_tick, error::Result, hash, sync, AsyncNotification,
|
||||
StatusItem, CWD,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use log::trace;
|
||||
use std::{
|
||||
hash::Hash,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
use sync::status::StatusType;
|
||||
|
||||
#[derive(Default, Hash, Clone)]
|
||||
pub struct Status2 {
|
||||
pub items: Vec<StatusItem>,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Default, Hash, Clone, PartialEq)]
|
||||
pub struct StatusParams {
|
||||
tick: u64,
|
||||
status_type: StatusType,
|
||||
include_untracked: bool,
|
||||
}
|
||||
|
||||
impl StatusParams {
|
||||
///
|
||||
pub fn new(
|
||||
status_type: StatusType,
|
||||
include_untracked: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
tick: current_tick(),
|
||||
status_type,
|
||||
include_untracked,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Request<R, A>(R, Option<A>);
|
||||
|
||||
///TODO: merge functionality with AsyncStatus
|
||||
pub struct AsyncStatus2 {
|
||||
current: Arc<Mutex<Request<u64, Status2>>>,
|
||||
last: Arc<Mutex<Status2>>,
|
||||
sender: Sender<AsyncNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl AsyncStatus2 {
|
||||
///
|
||||
pub fn new(sender: Sender<AsyncNotification>) -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(Request(0, None))),
|
||||
last: Arc::new(Mutex::new(Status2::default())),
|
||||
sender,
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last(&mut self) -> Result<Status2> {
|
||||
let last = self.last.lock()?;
|
||||
Ok(last.clone())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
|
||||
///
|
||||
pub fn fetch(
|
||||
&mut self,
|
||||
params: StatusParams,
|
||||
) -> Result<Option<Status2>> {
|
||||
let hash_request = hash(¶ms);
|
||||
|
||||
trace!("request: [hash: {}]", hash_request);
|
||||
|
||||
{
|
||||
let mut current = self.current.lock()?;
|
||||
|
||||
if current.0 == hash_request {
|
||||
return Ok(current.1.clone());
|
||||
}
|
||||
|
||||
current.0 = hash_request;
|
||||
current.1 = None;
|
||||
}
|
||||
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let arc_last = Arc::clone(&self.last);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let status_type = params.status_type;
|
||||
let include_untracked = params.include_untracked;
|
||||
rayon_core::spawn(move || {
|
||||
arc_pending.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
Self::fetch_helper(
|
||||
status_type,
|
||||
include_untracked,
|
||||
hash_request,
|
||||
arc_current,
|
||||
arc_last,
|
||||
)
|
||||
.expect("failed to fetch status");
|
||||
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
|
||||
sender
|
||||
.send(AsyncNotification::Status)
|
||||
.expect("error sending status");
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn fetch_helper(
|
||||
status_type: StatusType,
|
||||
include_untracked: bool,
|
||||
hash_request: u64,
|
||||
arc_current: Arc<Mutex<Request<u64, Status2>>>,
|
||||
arc_last: Arc<Mutex<Status2>>,
|
||||
) -> Result<()> {
|
||||
let res = Self::get_status(status_type, include_untracked)?;
|
||||
trace!("status fetched: {}", hash(&res));
|
||||
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
if current.0 == hash_request {
|
||||
current.1 = Some(res.clone());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut last = arc_last.lock()?;
|
||||
*last = res;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_status(
|
||||
status_type: StatusType,
|
||||
include_untracked: bool,
|
||||
) -> Result<Status2> {
|
||||
Ok(Status2 {
|
||||
items: sync::status::get_status_new(
|
||||
CWD,
|
||||
status_type,
|
||||
include_untracked,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ mod hooks;
|
|||
mod hunks;
|
||||
mod logwalker;
|
||||
mod reset;
|
||||
mod stash;
|
||||
pub mod status;
|
||||
mod tags;
|
||||
pub mod utils;
|
||||
|
|
@ -17,6 +18,7 @@ pub use logwalker::LogWalker;
|
|||
pub use reset::{
|
||||
reset_stage, reset_workdir_file, reset_workdir_folder,
|
||||
};
|
||||
pub use stash::stash_save;
|
||||
pub use tags::{get_tags, Tags};
|
||||
pub use utils::{
|
||||
commit, stage_add_all, stage_add_file, stage_addremoved,
|
||||
|
|
|
|||
124
asyncgit/src/sync/stash.rs
Normal file
124
asyncgit/src/sync/stash.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#![allow(dead_code)]
|
||||
use super::utils::repo;
|
||||
use crate::error::Result;
|
||||
use git2::{Oid, StashFlags};
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
pub struct StashItem {
|
||||
pub msg: String,
|
||||
index: usize,
|
||||
id: Oid,
|
||||
}
|
||||
|
||||
///
|
||||
pub struct StashItems(Vec<StashItem>);
|
||||
|
||||
///
|
||||
pub fn get_stashes(repo_path: &str) -> Result<StashItems> {
|
||||
scope_time!("get_stashes");
|
||||
|
||||
let mut repo = repo(repo_path)?;
|
||||
|
||||
let mut list = Vec::new();
|
||||
|
||||
repo.stash_foreach(|index, msg, id| {
|
||||
list.push(StashItem {
|
||||
msg: msg.to_string(),
|
||||
index,
|
||||
id: *id,
|
||||
});
|
||||
true
|
||||
})?;
|
||||
|
||||
Ok(StashItems(list))
|
||||
}
|
||||
|
||||
///
|
||||
pub fn stash_save(
|
||||
repo_path: &str,
|
||||
message: Option<&str>,
|
||||
include_untracked: bool,
|
||||
keep_index: bool,
|
||||
) -> Result<()> {
|
||||
scope_time!("stash_save");
|
||||
|
||||
let mut repo = repo(repo_path)?;
|
||||
|
||||
let sig = repo.signature()?;
|
||||
|
||||
let mut options = StashFlags::DEFAULT;
|
||||
|
||||
if include_untracked {
|
||||
options.insert(StashFlags::INCLUDE_UNTRACKED);
|
||||
}
|
||||
if keep_index {
|
||||
options.insert(StashFlags::KEEP_INDEX)
|
||||
}
|
||||
|
||||
repo.stash_save2(&sig, message, Some(options))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sync::tests::{get_statuses, repo_init};
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
#[test]
|
||||
fn test_smoke() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
stash_save(repo_path, None, true, false).is_ok(),
|
||||
false
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_stashes(repo_path).unwrap().0.is_empty(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stashing() -> Result<()> {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
File::create(&root.join("foo.txt"))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (1, 0));
|
||||
|
||||
stash_save(repo_path, None, true, false)?;
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (0, 0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stashes() -> Result<()> {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
File::create(&root.join("foo.txt"))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
|
||||
stash_save(repo_path, Some("foo"), true, false)?;
|
||||
|
||||
let res = get_stashes(repo_path)?;
|
||||
|
||||
assert_eq!(res.0.len(), 1);
|
||||
assert_eq!(res.0[0].msg, "On master: foo");
|
||||
assert_eq!(res.0[0].index, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -46,12 +46,20 @@ pub struct StatusItem {
|
|||
}
|
||||
|
||||
///
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Hash, PartialEq)]
|
||||
pub enum StatusType {
|
||||
///
|
||||
WorkingDir,
|
||||
///
|
||||
Stage,
|
||||
///
|
||||
Both,
|
||||
}
|
||||
|
||||
impl Default for StatusType {
|
||||
fn default() -> Self {
|
||||
StatusType::WorkingDir
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<StatusShow> for StatusType {
|
||||
|
|
@ -59,23 +67,33 @@ impl Into<StatusShow> for StatusType {
|
|||
match self {
|
||||
StatusType::WorkingDir => StatusShow::Workdir,
|
||||
StatusType::Stage => StatusShow::Index,
|
||||
StatusType::Both => StatusShow::IndexAndWorkdir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// TODO: migrate
|
||||
pub fn get_status(
|
||||
repo_path: &str,
|
||||
status_type: StatusType,
|
||||
) -> Result<Vec<StatusItem>> {
|
||||
scope_time!("get_index");
|
||||
get_status_new(repo_path, status_type, true)
|
||||
}
|
||||
|
||||
/// TODO: migrate
|
||||
pub fn get_status_new(
|
||||
repo_path: &str,
|
||||
status_type: StatusType,
|
||||
include_untracked: bool,
|
||||
) -> Result<Vec<StatusItem>> {
|
||||
scope_time!("get_status");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
|
||||
let statuses = repo.statuses(Some(
|
||||
StatusOptions::default()
|
||||
.show(status_type.into())
|
||||
.include_untracked(true)
|
||||
.include_untracked(include_untracked)
|
||||
.renames_head_to_index(true)
|
||||
.recurse_untracked_dirs(true),
|
||||
))?;
|
||||
|
|
|
|||
60
src/app.rs
60
src/app.rs
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
keys,
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings,
|
||||
tabs::{Revlog, Status},
|
||||
tabs::{Revlog, Stashing, Status},
|
||||
ui::style::Theme,
|
||||
};
|
||||
use asyncgit::{sync, AsyncNotification, CWD};
|
||||
|
|
@ -38,6 +38,7 @@ pub struct App {
|
|||
tab: usize,
|
||||
revlog: Revlog,
|
||||
status_tab: Status,
|
||||
stashing_tab: Stashing,
|
||||
queue: Queue,
|
||||
theme: Theme,
|
||||
}
|
||||
|
|
@ -60,6 +61,7 @@ impl App {
|
|||
tab: 0,
|
||||
revlog: Revlog::new(&sender, &theme),
|
||||
status_tab: Status::new(&sender, &queue, &theme),
|
||||
stashing_tab: Stashing::new(&sender, &queue, &theme),
|
||||
queue,
|
||||
theme,
|
||||
}
|
||||
|
|
@ -81,11 +83,13 @@ impl App {
|
|||
|
||||
self.draw_tabs(f, chunks_main[0]);
|
||||
|
||||
if self.tab == 0 {
|
||||
self.status_tab.draw(f, chunks_main[1]);
|
||||
} else {
|
||||
self.revlog.draw(f, chunks_main[1]);
|
||||
}
|
||||
//TODO: macro because of generic draw call
|
||||
match self.tab {
|
||||
0 => self.status_tab.draw(f, chunks_main[1]),
|
||||
1 => self.revlog.draw(f, chunks_main[1]),
|
||||
2 => self.stashing_tab.draw(f, chunks_main[1]),
|
||||
_ => panic!("unknown tab"),
|
||||
};
|
||||
|
||||
Self::draw_commands(
|
||||
f,
|
||||
|
|
@ -142,6 +146,7 @@ impl App {
|
|||
trace!("update");
|
||||
self.status_tab.update();
|
||||
self.revlog.update();
|
||||
self.stashing_tab.update();
|
||||
}
|
||||
|
||||
///
|
||||
|
|
@ -149,6 +154,7 @@ impl App {
|
|||
trace!("update_git: {:?}", ev);
|
||||
|
||||
self.status_tab.update_git(ev);
|
||||
self.stashing_tab.update_git(ev);
|
||||
|
||||
match ev {
|
||||
AsyncNotification::Diff => (),
|
||||
|
|
@ -167,12 +173,16 @@ impl App {
|
|||
pub fn any_work_pending(&self) -> bool {
|
||||
self.status_tab.anything_pending()
|
||||
|| self.revlog.any_work_pending()
|
||||
|| self.stashing_tab.anything_pending()
|
||||
}
|
||||
}
|
||||
|
||||
// private impls
|
||||
impl App {
|
||||
accessors!(self, [msg, reset, commit, help, revlog, status_tab]);
|
||||
accessors!(
|
||||
self,
|
||||
[msg, reset, commit, help, revlog, status_tab, stashing_tab]
|
||||
);
|
||||
|
||||
fn check_quit(&mut self, ev: Event) -> bool {
|
||||
if let Event::Key(e) = ev {
|
||||
|
|
@ -184,17 +194,29 @@ impl App {
|
|||
false
|
||||
}
|
||||
|
||||
fn toggle_tabs(&mut self) {
|
||||
self.tab += 1;
|
||||
self.tab %= 2;
|
||||
fn get_tabs(&mut self) -> Vec<&mut dyn Component> {
|
||||
vec![
|
||||
&mut self.status_tab,
|
||||
&mut self.revlog,
|
||||
&mut self.stashing_tab,
|
||||
]
|
||||
}
|
||||
|
||||
if self.tab == 1 {
|
||||
self.status_tab.hide();
|
||||
self.revlog.show();
|
||||
} else {
|
||||
self.status_tab.show();
|
||||
self.revlog.hide();
|
||||
fn toggle_tabs(&mut self) {
|
||||
let mut new_tab = self.tab + 1;
|
||||
{
|
||||
let tabs = self.get_tabs();
|
||||
new_tab %= tabs.len();
|
||||
|
||||
for (i, t) in tabs.into_iter().enumerate() {
|
||||
if new_tab == i {
|
||||
t.show();
|
||||
} else {
|
||||
t.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tab = new_tab;
|
||||
}
|
||||
|
||||
fn update_commands(&mut self) {
|
||||
|
|
@ -328,7 +350,11 @@ impl App {
|
|||
f.render_widget(
|
||||
Tabs::default()
|
||||
.block(Block::default().borders(Borders::BOTTOM))
|
||||
.titles(&[strings::TAB_STATUS, strings::TAB_LOG])
|
||||
.titles(&[
|
||||
strings::TAB_STATUS,
|
||||
strings::TAB_LOG,
|
||||
strings::TAB_STASHING,
|
||||
])
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
self.theme
|
||||
|
|
|
|||
|
|
@ -1,31 +1,26 @@
|
|||
use super::{
|
||||
filetree::{FileTreeItem, FileTreeItemKind},
|
||||
statustree::{MoveSelection, StatusTree},
|
||||
filetree::FileTreeComponent,
|
||||
utils::filetree::{FileTreeItem, FileTreeItemKind},
|
||||
CommandBlocking, DrawableComponent,
|
||||
};
|
||||
use crate::{
|
||||
components::{CommandInfo, Component},
|
||||
keys,
|
||||
queue::{InternalEvent, NeedsUpdate, Queue, ResetItem},
|
||||
strings, ui,
|
||||
strings,
|
||||
ui::style::Theme,
|
||||
};
|
||||
use asyncgit::{hash, sync, StatusItem, StatusItemType, CWD};
|
||||
use asyncgit::{sync, StatusItem, StatusItemType, CWD};
|
||||
use crossterm::event::Event;
|
||||
use std::{borrow::Cow, convert::From, path::Path};
|
||||
use std::path::Path;
|
||||
use strings::commands;
|
||||
use tui::{backend::Backend, layout::Rect, widgets::Text, Frame};
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
///
|
||||
pub struct ChangesComponent {
|
||||
title: String,
|
||||
tree: StatusTree,
|
||||
current_hash: u64,
|
||||
focused: bool,
|
||||
show_selection: bool,
|
||||
files: FileTreeComponent,
|
||||
is_working_dir: bool,
|
||||
queue: Queue,
|
||||
theme: Theme,
|
||||
}
|
||||
|
||||
impl ChangesComponent {
|
||||
|
|
@ -38,64 +33,40 @@ impl ChangesComponent {
|
|||
theme: &Theme,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.to_string(),
|
||||
tree: StatusTree::default(),
|
||||
current_hash: 0,
|
||||
focused: focus,
|
||||
show_selection: focus,
|
||||
files: FileTreeComponent::new(
|
||||
title,
|
||||
focus,
|
||||
queue.clone(),
|
||||
theme,
|
||||
),
|
||||
is_working_dir,
|
||||
queue,
|
||||
theme: *theme,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update(&mut self, list: &[StatusItem]) {
|
||||
let new_hash = hash(list);
|
||||
if self.current_hash != new_hash {
|
||||
self.tree.update(list);
|
||||
self.current_hash = new_hash;
|
||||
}
|
||||
self.files.update(list)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn selection(&self) -> Option<FileTreeItem> {
|
||||
self.tree.selected_item()
|
||||
self.files.selection()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn focus_select(&mut self, focus: bool) {
|
||||
self.focus(focus);
|
||||
self.show_selection = focus;
|
||||
self.files.focus_select(focus)
|
||||
}
|
||||
|
||||
/// returns true if list is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
self.files.is_empty()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_file_seleted(&self) -> bool {
|
||||
if let Some(item) = self.tree.selected_item() {
|
||||
match item.kind {
|
||||
FileTreeItemKind::File(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, dir: MoveSelection) -> bool {
|
||||
let changed = self.tree.move_selection(dir);
|
||||
|
||||
if changed {
|
||||
self.queue
|
||||
.borrow_mut()
|
||||
.push_back(InternalEvent::Update(NeedsUpdate::DIFF));
|
||||
}
|
||||
|
||||
changed
|
||||
self.files.is_file_seleted()
|
||||
}
|
||||
|
||||
fn index_add_remove(&mut self) -> bool {
|
||||
|
|
@ -147,140 +118,11 @@ impl ChangesComponent {
|
|||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn item_to_text(
|
||||
item: &FileTreeItem,
|
||||
width: u16,
|
||||
selected: bool,
|
||||
theme: Theme,
|
||||
) -> Option<Text> {
|
||||
let indent_str = if item.info.indent == 0 {
|
||||
String::from("")
|
||||
} else {
|
||||
format!("{:w$}", " ", w = (item.info.indent as usize) * 2)
|
||||
};
|
||||
|
||||
if !item.info.visible {
|
||||
return None;
|
||||
}
|
||||
|
||||
match &item.kind {
|
||||
FileTreeItemKind::File(status_item) => {
|
||||
let status_char =
|
||||
Self::item_status_char(status_item.status);
|
||||
let file = Path::new(&status_item.path)
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let txt = if selected {
|
||||
format!(
|
||||
"{} {}{:w$}",
|
||||
status_char,
|
||||
indent_str,
|
||||
file,
|
||||
w = width as usize
|
||||
)
|
||||
} else {
|
||||
format!("{} {}{}", status_char, indent_str, file)
|
||||
};
|
||||
|
||||
let status = status_item
|
||||
.status
|
||||
.unwrap_or(StatusItemType::Modified);
|
||||
|
||||
Some(Text::Styled(
|
||||
Cow::from(txt),
|
||||
theme.item(status, selected),
|
||||
))
|
||||
}
|
||||
|
||||
FileTreeItemKind::Path(path_collapsed) => {
|
||||
let collapse_char =
|
||||
if path_collapsed.0 { '▸' } else { '▾' };
|
||||
|
||||
let txt = if selected {
|
||||
format!(
|
||||
" {}{}{:w$}",
|
||||
indent_str,
|
||||
collapse_char,
|
||||
item.info.path,
|
||||
w = width as usize
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
" {}{}{}",
|
||||
indent_str, collapse_char, item.info.path,
|
||||
)
|
||||
};
|
||||
|
||||
Some(Text::Styled(
|
||||
Cow::from(txt),
|
||||
theme.text(true, selected),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn item_status_char(item_type: Option<StatusItemType>) -> char {
|
||||
if let Some(item_type) = item_type {
|
||||
match item_type {
|
||||
StatusItemType::Modified => 'M',
|
||||
StatusItemType::New => '+',
|
||||
StatusItemType::Deleted => '-',
|
||||
StatusItemType::Renamed => 'R',
|
||||
_ => ' ',
|
||||
}
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for ChangesComponent {
|
||||
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, r: Rect) {
|
||||
let selection_offset =
|
||||
self.tree.tree.items().iter().enumerate().fold(
|
||||
0,
|
||||
|acc, (idx, e)| {
|
||||
let visible = e.info.visible;
|
||||
let index_above_select =
|
||||
idx < self.tree.selection.unwrap_or(0);
|
||||
|
||||
if !visible && index_above_select {
|
||||
acc + 1
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let items =
|
||||
self.tree.tree.items().iter().enumerate().filter_map(
|
||||
|(idx, e)| {
|
||||
Self::item_to_text(
|
||||
e,
|
||||
r.width,
|
||||
self.show_selection
|
||||
&& self
|
||||
.tree
|
||||
.selection
|
||||
.map_or(false, |e| e == idx),
|
||||
self.theme,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
ui::draw_list(
|
||||
f,
|
||||
r,
|
||||
&self.title.to_string(),
|
||||
items,
|
||||
self.tree.selection.map(|idx| idx - selection_offset),
|
||||
self.focused,
|
||||
self.theme,
|
||||
);
|
||||
self.files.draw(f, r)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,46 +132,46 @@ impl Component for ChangesComponent {
|
|||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
self.files.commands(out, force_all);
|
||||
|
||||
let some_selection = self.selection().is_some();
|
||||
|
||||
if self.is_working_dir {
|
||||
out.push(CommandInfo::new(
|
||||
commands::STAGE_ITEM,
|
||||
some_selection,
|
||||
self.focused,
|
||||
self.focused(),
|
||||
));
|
||||
out.push(CommandInfo::new(
|
||||
commands::RESET_ITEM,
|
||||
some_selection,
|
||||
self.focused,
|
||||
self.focused(),
|
||||
));
|
||||
} else {
|
||||
out.push(CommandInfo::new(
|
||||
commands::UNSTAGE_ITEM,
|
||||
some_selection,
|
||||
self.focused,
|
||||
self.focused(),
|
||||
));
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
commands::COMMIT_OPEN,
|
||||
!self.is_empty(),
|
||||
self.focused || force_all,
|
||||
self.focused() || force_all,
|
||||
)
|
||||
.order(-1),
|
||||
);
|
||||
}
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
commands::NAVIGATE_TREE,
|
||||
!self.is_empty(),
|
||||
self.focused,
|
||||
));
|
||||
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: Event) -> bool {
|
||||
if self.focused {
|
||||
if self.files.event(ev) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.focused() {
|
||||
if let Event::Key(e) = ev {
|
||||
return match e {
|
||||
keys::OPEN_COMMIT
|
||||
|
|
@ -356,24 +198,6 @@ impl Component for ChangesComponent {
|
|||
{
|
||||
self.dispatch_reset_workdir()
|
||||
}
|
||||
keys::MOVE_DOWN => {
|
||||
self.move_selection(MoveSelection::Down)
|
||||
}
|
||||
keys::MOVE_UP => {
|
||||
self.move_selection(MoveSelection::Up)
|
||||
}
|
||||
keys::HOME | keys::SHIFT_UP => {
|
||||
self.move_selection(MoveSelection::Home)
|
||||
}
|
||||
keys::END | keys::SHIFT_DOWN => {
|
||||
self.move_selection(MoveSelection::End)
|
||||
}
|
||||
keys::MOVE_LEFT => {
|
||||
self.move_selection(MoveSelection::Left)
|
||||
}
|
||||
keys::MOVE_RIGHT => {
|
||||
self.move_selection(MoveSelection::Right)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
|
@ -383,9 +207,9 @@ impl Component for ChangesComponent {
|
|||
}
|
||||
|
||||
fn focused(&self) -> bool {
|
||||
self.focused
|
||||
self.files.focused()
|
||||
}
|
||||
fn focus(&mut self, focus: bool) {
|
||||
self.focused = focus
|
||||
self.files.focus(focus)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,360 +1,287 @@
|
|||
use asyncgit::StatusItem;
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
convert::TryFrom,
|
||||
ops::{Index, IndexMut},
|
||||
path::Path,
|
||||
use super::{
|
||||
utils::{
|
||||
filetree::{FileTreeItem, FileTreeItemKind},
|
||||
statustree::{MoveSelection, StatusTree},
|
||||
},
|
||||
CommandBlocking, DrawableComponent,
|
||||
};
|
||||
|
||||
/// holds the information shared among all `FileTreeItem` in a `FileTree`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeItemInfo {
|
||||
/// indent level
|
||||
pub indent: u8,
|
||||
/// currently visible depending on the folder collapse states
|
||||
pub visible: bool,
|
||||
/// just the last path element
|
||||
pub path: String,
|
||||
/// the full path
|
||||
pub full_path: String,
|
||||
}
|
||||
|
||||
impl TreeItemInfo {
|
||||
fn new(indent: u8, path: String, full_path: String) -> Self {
|
||||
Self {
|
||||
indent,
|
||||
visible: true,
|
||||
path,
|
||||
full_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// attribute used to indicate the collapse/expand state of a path item
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct PathCollapsed(pub bool);
|
||||
|
||||
/// `FileTreeItem` can be of two kinds
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum FileTreeItemKind {
|
||||
Path(PathCollapsed),
|
||||
File(StatusItem),
|
||||
}
|
||||
|
||||
/// `FileTreeItem` can be of two kinds: see `FileTreeItem` but shares an info
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileTreeItem {
|
||||
pub info: TreeItemInfo,
|
||||
pub kind: FileTreeItemKind,
|
||||
}
|
||||
|
||||
impl FileTreeItem {
|
||||
fn new_file(item: &StatusItem) -> Self {
|
||||
let item_path = Path::new(&item.path);
|
||||
let indent = u8::try_from(
|
||||
item_path.ancestors().count().saturating_sub(2),
|
||||
)
|
||||
.unwrap();
|
||||
let path = String::from(
|
||||
item_path.file_name().unwrap().to_str().unwrap(),
|
||||
);
|
||||
|
||||
Self {
|
||||
info: TreeItemInfo::new(indent, path, item.path.clone()),
|
||||
kind: FileTreeItemKind::File(item.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_path(
|
||||
path: &Path,
|
||||
path_string: String,
|
||||
collapsed: bool,
|
||||
) -> Self {
|
||||
let indent =
|
||||
u8::try_from(path.ancestors().count().saturating_sub(2))
|
||||
.unwrap();
|
||||
let path = String::from(
|
||||
path.components()
|
||||
.last()
|
||||
.unwrap()
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
Self {
|
||||
info: TreeItemInfo::new(indent, path, path_string),
|
||||
kind: FileTreeItemKind::Path(PathCollapsed(collapsed)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for FileTreeItem {}
|
||||
|
||||
impl PartialEq for FileTreeItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.info.full_path.eq(&other.info.full_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for FileTreeItem {
|
||||
fn partial_cmp(
|
||||
&self,
|
||||
other: &Self,
|
||||
) -> Option<std::cmp::Ordering> {
|
||||
self.info.full_path.partial_cmp(&other.info.full_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for FileTreeItem {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.info.path.cmp(&other.info.path)
|
||||
}
|
||||
}
|
||||
use crate::{
|
||||
components::{CommandInfo, Component},
|
||||
keys,
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings, ui,
|
||||
ui::style::Theme,
|
||||
};
|
||||
use asyncgit::{hash, StatusItem, StatusItemType};
|
||||
use crossterm::event::Event;
|
||||
use std::{borrow::Cow, convert::From, path::Path};
|
||||
use strings::commands;
|
||||
use tui::{backend::Backend, layout::Rect, widgets::Text, Frame};
|
||||
|
||||
///
|
||||
#[derive(Default)]
|
||||
pub struct FileTreeItems(Vec<FileTreeItem>);
|
||||
pub struct FileTreeComponent {
|
||||
title: String,
|
||||
tree: StatusTree,
|
||||
current_hash: u64,
|
||||
focused: bool,
|
||||
show_selection: bool,
|
||||
queue: Queue,
|
||||
theme: Theme,
|
||||
}
|
||||
|
||||
impl FileTreeItems {
|
||||
impl FileTreeComponent {
|
||||
///
|
||||
pub(crate) fn new(
|
||||
list: &[StatusItem],
|
||||
collapsed: &BTreeSet<&String>,
|
||||
pub fn new(
|
||||
title: &str,
|
||||
focus: bool,
|
||||
queue: Queue,
|
||||
theme: &Theme,
|
||||
) -> Self {
|
||||
let mut nodes = Vec::with_capacity(list.len());
|
||||
let mut paths_added = BTreeSet::new();
|
||||
Self {
|
||||
title: title.to_string(),
|
||||
tree: StatusTree::default(),
|
||||
current_hash: 0,
|
||||
focused: focus,
|
||||
show_selection: focus,
|
||||
queue,
|
||||
theme: *theme,
|
||||
}
|
||||
}
|
||||
|
||||
for e in list {
|
||||
{
|
||||
let item_path = Path::new(&e.path);
|
||||
///
|
||||
pub fn update(&mut self, list: &[StatusItem]) {
|
||||
let new_hash = hash(list);
|
||||
if self.current_hash != new_hash {
|
||||
self.tree.update(list);
|
||||
self.current_hash = new_hash;
|
||||
}
|
||||
}
|
||||
|
||||
FileTreeItems::push_dirs(
|
||||
item_path,
|
||||
&mut nodes,
|
||||
&mut paths_added,
|
||||
&collapsed,
|
||||
);
|
||||
///
|
||||
pub fn selection(&self) -> Option<FileTreeItem> {
|
||||
self.tree.selected_item()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn focus_select(&mut self, focus: bool) {
|
||||
self.focus(focus);
|
||||
self.show_selection = focus;
|
||||
}
|
||||
|
||||
/// returns true if list is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_file_seleted(&self) -> bool {
|
||||
if let Some(item) = self.tree.selected_item() {
|
||||
match item.kind {
|
||||
FileTreeItemKind::File(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
nodes.push(FileTreeItem::new_file(&e));
|
||||
fn move_selection(&mut self, dir: MoveSelection) -> bool {
|
||||
let changed = self.tree.move_selection(dir);
|
||||
|
||||
if changed {
|
||||
self.queue
|
||||
.borrow_mut()
|
||||
.push_back(InternalEvent::Update(NeedsUpdate::DIFF));
|
||||
}
|
||||
|
||||
Self(nodes)
|
||||
changed
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) fn items(&self) -> &Vec<FileTreeItem> {
|
||||
&self.0
|
||||
}
|
||||
fn item_to_text(
|
||||
item: &FileTreeItem,
|
||||
width: u16,
|
||||
selected: bool,
|
||||
theme: Theme,
|
||||
) -> Option<Text> {
|
||||
let indent_str = if item.info.indent == 0 {
|
||||
String::from("")
|
||||
} else {
|
||||
format!("{:w$}", " ", w = (item.info.indent as usize) * 2)
|
||||
};
|
||||
|
||||
///
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) fn find_parent_index(
|
||||
&self,
|
||||
path: &str,
|
||||
index: usize,
|
||||
) -> usize {
|
||||
if let Some(parent_path) = Path::new(path).parent() {
|
||||
let parent_path = parent_path.to_str().unwrap();
|
||||
for i in (0..=index).rev() {
|
||||
let item = &self.0[i];
|
||||
let item_path = &item.info.full_path;
|
||||
if item_path == parent_path {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if !item.info.visible {
|
||||
return None;
|
||||
}
|
||||
|
||||
0
|
||||
match &item.kind {
|
||||
FileTreeItemKind::File(status_item) => {
|
||||
let status_char =
|
||||
Self::item_status_char(status_item.status);
|
||||
let file = Path::new(&status_item.path)
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let txt = if selected {
|
||||
format!(
|
||||
"{} {}{:w$}",
|
||||
status_char,
|
||||
indent_str,
|
||||
file,
|
||||
w = width as usize
|
||||
)
|
||||
} else {
|
||||
format!("{} {}{}", status_char, indent_str, file)
|
||||
};
|
||||
|
||||
let status = status_item
|
||||
.status
|
||||
.unwrap_or(StatusItemType::Modified);
|
||||
|
||||
Some(Text::Styled(
|
||||
Cow::from(txt),
|
||||
theme.item(status, selected),
|
||||
))
|
||||
}
|
||||
|
||||
FileTreeItemKind::Path(path_collapsed) => {
|
||||
let collapse_char =
|
||||
if path_collapsed.0 { '▸' } else { '▾' };
|
||||
|
||||
let txt = if selected {
|
||||
format!(
|
||||
" {}{}{:w$}",
|
||||
indent_str,
|
||||
collapse_char,
|
||||
item.info.path,
|
||||
w = width as usize
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
" {}{}{}",
|
||||
indent_str, collapse_char, item.info.path,
|
||||
)
|
||||
};
|
||||
|
||||
Some(Text::Styled(
|
||||
Cow::from(txt),
|
||||
theme.text(true, selected),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_dirs<'a>(
|
||||
item_path: &'a Path,
|
||||
nodes: &mut Vec<FileTreeItem>,
|
||||
paths_added: &mut BTreeSet<&'a Path>,
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) {
|
||||
let mut ancestors =
|
||||
{ item_path.ancestors().skip(1).collect::<Vec<_>>() };
|
||||
ancestors.reverse();
|
||||
|
||||
for c in &ancestors {
|
||||
if c.parent().is_some() {
|
||||
let path_string = String::from(c.to_str().unwrap());
|
||||
if !paths_added.contains(c) {
|
||||
paths_added.insert(c);
|
||||
let is_collapsed =
|
||||
collapsed.contains(&path_string);
|
||||
nodes.push(FileTreeItem::new_path(
|
||||
c,
|
||||
path_string,
|
||||
is_collapsed,
|
||||
));
|
||||
}
|
||||
fn item_status_char(item_type: Option<StatusItemType>) -> char {
|
||||
if let Some(item_type) = item_type {
|
||||
match item_type {
|
||||
StatusItemType::Modified => 'M',
|
||||
StatusItemType::New => '+',
|
||||
StatusItemType::Deleted => '-',
|
||||
StatusItemType::Renamed => 'R',
|
||||
_ => ' ',
|
||||
}
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<usize> for FileTreeItems {
|
||||
fn index_mut(&mut self, idx: usize) -> &mut Self::Output {
|
||||
&mut self.0[idx]
|
||||
}
|
||||
}
|
||||
impl DrawableComponent for FileTreeComponent {
|
||||
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, r: Rect) {
|
||||
let selection_offset =
|
||||
self.tree.tree.items().iter().enumerate().fold(
|
||||
0,
|
||||
|acc, (idx, e)| {
|
||||
let visible = e.info.visible;
|
||||
let index_above_select =
|
||||
idx < self.tree.selection.unwrap_or(0);
|
||||
|
||||
impl Index<usize> for FileTreeItems {
|
||||
type Output = FileTreeItem;
|
||||
|
||||
fn index(&self, idx: usize) -> &Self::Output {
|
||||
&self.0[idx]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn string_vec_to_status(items: &[&str]) -> Vec<StatusItem> {
|
||||
items
|
||||
.iter()
|
||||
.map(|a| StatusItem {
|
||||
path: String::from(*a),
|
||||
status: None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let items = string_vec_to_status(&[
|
||||
"file.txt", //
|
||||
]);
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new());
|
||||
|
||||
assert_eq!(
|
||||
res.0,
|
||||
vec![FileTreeItem {
|
||||
info: TreeItemInfo {
|
||||
path: items[0].path.clone(),
|
||||
full_path: items[0].path.clone(),
|
||||
indent: 0,
|
||||
visible: true,
|
||||
if !visible && index_above_select {
|
||||
acc + 1
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
},
|
||||
kind: FileTreeItemKind::File(items[0].clone())
|
||||
}]
|
||||
);
|
||||
);
|
||||
|
||||
let items = string_vec_to_status(&[
|
||||
"file.txt", //
|
||||
"file2.txt", //
|
||||
]);
|
||||
let items =
|
||||
self.tree.tree.items().iter().enumerate().filter_map(
|
||||
|(idx, e)| {
|
||||
Self::item_to_text(
|
||||
e,
|
||||
r.width,
|
||||
self.show_selection
|
||||
&& self
|
||||
.tree
|
||||
.selection
|
||||
.map_or(false, |e| e == idx),
|
||||
self.theme,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new());
|
||||
|
||||
assert_eq!(res.0.len(), 2);
|
||||
assert_eq!(res.0[1].info.path, items[1].path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folder() {
|
||||
let items = string_vec_to_status(&[
|
||||
"a/file.txt", //
|
||||
]);
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new())
|
||||
.0
|
||||
.iter()
|
||||
.map(|i| i.info.full_path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![String::from("a"), items[0].path.clone(),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent() {
|
||||
let items = string_vec_to_status(&[
|
||||
"a/b/file.txt", //
|
||||
]);
|
||||
|
||||
let list = FileTreeItems::new(&items, &BTreeSet::new());
|
||||
let mut res = list
|
||||
.0
|
||||
.iter()
|
||||
.map(|i| (i.info.indent, i.info.path.as_str()));
|
||||
|
||||
assert_eq!(res.next(), Some((0, "a")));
|
||||
assert_eq!(res.next(), Some((1, "b")));
|
||||
assert_eq!(res.next(), Some((2, "file.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_folder_file_name() {
|
||||
let items = string_vec_to_status(&[
|
||||
"a/b", //
|
||||
"a.txt", //
|
||||
]);
|
||||
|
||||
let list = FileTreeItems::new(&items, &BTreeSet::new());
|
||||
let mut res = list
|
||||
.0
|
||||
.iter()
|
||||
.map(|i| (i.info.indent, i.info.path.as_str()));
|
||||
|
||||
assert_eq!(res.next(), Some((0, "a")));
|
||||
assert_eq!(res.next(), Some((1, "b")));
|
||||
assert_eq!(res.next(), Some((0, "a.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folder_dup() {
|
||||
let items = string_vec_to_status(&[
|
||||
"a/file.txt", //
|
||||
"a/file2.txt", //
|
||||
]);
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new())
|
||||
.0
|
||||
.iter()
|
||||
.map(|i| i.info.full_path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
String::from("a"),
|
||||
items[0].path.clone(),
|
||||
items[1].path.clone()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_parent() {
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let res = FileTreeItems::new(
|
||||
&string_vec_to_status(&[
|
||||
"a/b/c", //
|
||||
"a/b/d", //
|
||||
]),
|
||||
&BTreeSet::new(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
res.find_parent_index(&String::from("a/b/c"), 3),
|
||||
1
|
||||
ui::draw_list(
|
||||
f,
|
||||
r,
|
||||
&self.title.to_string(),
|
||||
items,
|
||||
self.tree.selection.map(|idx| idx - selection_offset),
|
||||
self.focused,
|
||||
self.theme,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FileTreeComponent {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
out.push(CommandInfo::new(
|
||||
commands::NAVIGATE_TREE,
|
||||
!self.is_empty(),
|
||||
self.focused || force_all,
|
||||
));
|
||||
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: Event) -> bool {
|
||||
if self.focused {
|
||||
if let Event::Key(e) = ev {
|
||||
return match e {
|
||||
keys::MOVE_DOWN => {
|
||||
self.move_selection(MoveSelection::Down)
|
||||
}
|
||||
keys::MOVE_UP => {
|
||||
self.move_selection(MoveSelection::Up)
|
||||
}
|
||||
keys::HOME | keys::SHIFT_UP => {
|
||||
self.move_selection(MoveSelection::Home)
|
||||
}
|
||||
keys::END | keys::SHIFT_DOWN => {
|
||||
self.move_selection(MoveSelection::End)
|
||||
}
|
||||
keys::MOVE_LEFT => {
|
||||
self.move_selection(MoveSelection::Left)
|
||||
}
|
||||
keys::MOVE_RIGHT => {
|
||||
self.move_selection(MoveSelection::Right)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn focused(&self) -> bool {
|
||||
self.focused
|
||||
}
|
||||
fn focus(&mut self, focus: bool) {
|
||||
self.focused = focus
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ mod filetree;
|
|||
mod help;
|
||||
mod msg;
|
||||
mod reset;
|
||||
mod statustree;
|
||||
mod utils;
|
||||
pub use changes::ChangesComponent;
|
||||
pub use command::{CommandInfo, CommandText};
|
||||
pub use commit::CommitComponent;
|
||||
pub use diff::DiffComponent;
|
||||
pub use filetree::FileTreeItemKind;
|
||||
pub use filetree::FileTreeComponent;
|
||||
pub use help::HelpComponent;
|
||||
pub use msg::MsgComponent;
|
||||
pub use reset::ResetComponent;
|
||||
pub use utils::filetree::FileTreeItemKind;
|
||||
|
||||
use crossterm::event::Event;
|
||||
use tui::{
|
||||
|
|
|
|||
360
src/components/utils/filetree.rs
Normal file
360
src/components/utils/filetree.rs
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
use asyncgit::StatusItem;
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
convert::TryFrom,
|
||||
ops::{Index, IndexMut},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// holds the information shared among all `FileTreeItem` in a `FileTree`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeItemInfo {
|
||||
/// indent level
|
||||
pub indent: u8,
|
||||
/// currently visible depending on the folder collapse states
|
||||
pub visible: bool,
|
||||
/// just the last path element
|
||||
pub path: String,
|
||||
/// the full path
|
||||
pub full_path: String,
|
||||
}
|
||||
|
||||
impl TreeItemInfo {
|
||||
fn new(indent: u8, path: String, full_path: String) -> Self {
|
||||
Self {
|
||||
indent,
|
||||
visible: true,
|
||||
path,
|
||||
full_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// attribute used to indicate the collapse/expand state of a path item
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct PathCollapsed(pub bool);
|
||||
|
||||
/// `FileTreeItem` can be of two kinds
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum FileTreeItemKind {
|
||||
Path(PathCollapsed),
|
||||
File(StatusItem),
|
||||
}
|
||||
|
||||
/// `FileTreeItem` can be of two kinds: see `FileTreeItem` but shares an info
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileTreeItem {
|
||||
pub info: TreeItemInfo,
|
||||
pub kind: FileTreeItemKind,
|
||||
}
|
||||
|
||||
impl FileTreeItem {
|
||||
fn new_file(item: &StatusItem) -> Self {
|
||||
let item_path = Path::new(&item.path);
|
||||
let indent = u8::try_from(
|
||||
item_path.ancestors().count().saturating_sub(2),
|
||||
)
|
||||
.unwrap();
|
||||
let path = String::from(
|
||||
item_path.file_name().unwrap().to_str().unwrap(),
|
||||
);
|
||||
|
||||
Self {
|
||||
info: TreeItemInfo::new(indent, path, item.path.clone()),
|
||||
kind: FileTreeItemKind::File(item.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_path(
|
||||
path: &Path,
|
||||
path_string: String,
|
||||
collapsed: bool,
|
||||
) -> Self {
|
||||
let indent =
|
||||
u8::try_from(path.ancestors().count().saturating_sub(2))
|
||||
.unwrap();
|
||||
let path = String::from(
|
||||
path.components()
|
||||
.last()
|
||||
.unwrap()
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
Self {
|
||||
info: TreeItemInfo::new(indent, path, path_string),
|
||||
kind: FileTreeItemKind::Path(PathCollapsed(collapsed)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for FileTreeItem {}
|
||||
|
||||
impl PartialEq for FileTreeItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.info.full_path.eq(&other.info.full_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for FileTreeItem {
|
||||
fn partial_cmp(
|
||||
&self,
|
||||
other: &Self,
|
||||
) -> Option<std::cmp::Ordering> {
|
||||
self.info.full_path.partial_cmp(&other.info.full_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for FileTreeItem {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.info.path.cmp(&other.info.path)
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Default)]
|
||||
pub struct FileTreeItems(Vec<FileTreeItem>);
|
||||
|
||||
impl FileTreeItems {
|
||||
///
|
||||
pub(crate) fn new(
|
||||
list: &[StatusItem],
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) -> Self {
|
||||
let mut nodes = Vec::with_capacity(list.len());
|
||||
let mut paths_added = BTreeSet::new();
|
||||
|
||||
for e in list {
|
||||
{
|
||||
let item_path = Path::new(&e.path);
|
||||
|
||||
FileTreeItems::push_dirs(
|
||||
item_path,
|
||||
&mut nodes,
|
||||
&mut paths_added,
|
||||
&collapsed,
|
||||
);
|
||||
}
|
||||
|
||||
nodes.push(FileTreeItem::new_file(&e));
|
||||
}
|
||||
|
||||
Self(nodes)
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) fn items(&self) -> &Vec<FileTreeItem> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) fn find_parent_index(
|
||||
&self,
|
||||
path: &str,
|
||||
index: usize,
|
||||
) -> usize {
|
||||
if let Some(parent_path) = Path::new(path).parent() {
|
||||
let parent_path = parent_path.to_str().unwrap();
|
||||
for i in (0..=index).rev() {
|
||||
let item = &self.0[i];
|
||||
let item_path = &item.info.full_path;
|
||||
if item_path == parent_path {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
fn push_dirs<'a>(
|
||||
item_path: &'a Path,
|
||||
nodes: &mut Vec<FileTreeItem>,
|
||||
paths_added: &mut BTreeSet<&'a Path>,
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) {
|
||||
let mut ancestors =
|
||||
{ item_path.ancestors().skip(1).collect::<Vec<_>>() };
|
||||
ancestors.reverse();
|
||||
|
||||
for c in &ancestors {
|
||||
if c.parent().is_some() {
|
||||
let path_string = String::from(c.to_str().unwrap());
|
||||
if !paths_added.contains(c) {
|
||||
paths_added.insert(c);
|
||||
let is_collapsed =
|
||||
collapsed.contains(&path_string);
|
||||
nodes.push(FileTreeItem::new_path(
|
||||
c,
|
||||
path_string,
|
||||
is_collapsed,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<usize> for FileTreeItems {
|
||||
fn index_mut(&mut self, idx: usize) -> &mut Self::Output {
|
||||
&mut self.0[idx]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for FileTreeItems {
|
||||
type Output = FileTreeItem;
|
||||
|
||||
fn index(&self, idx: usize) -> &Self::Output {
|
||||
&self.0[idx]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn string_vec_to_status(items: &[&str]) -> Vec<StatusItem> {
|
||||
items
|
||||
.iter()
|
||||
.map(|a| StatusItem {
|
||||
path: String::from(*a),
|
||||
status: None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let items = string_vec_to_status(&[
|
||||
"file.txt", //
|
||||
]);
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new());
|
||||
|
||||
assert_eq!(
|
||||
res.0,
|
||||
vec![FileTreeItem {
|
||||
info: TreeItemInfo {
|
||||
path: items[0].path.clone(),
|
||||
full_path: items[0].path.clone(),
|
||||
indent: 0,
|
||||
visible: true,
|
||||
},
|
||||
kind: FileTreeItemKind::File(items[0].clone())
|
||||
}]
|
||||
);
|
||||
|
||||
let items = string_vec_to_status(&[
|
||||
"file.txt", //
|
||||
"file2.txt", //
|
||||
]);
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new());
|
||||
|
||||
assert_eq!(res.0.len(), 2);
|
||||
assert_eq!(res.0[1].info.path, items[1].path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folder() {
|
||||
let items = string_vec_to_status(&[
|
||||
"a/file.txt", //
|
||||
]);
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new())
|
||||
.0
|
||||
.iter()
|
||||
.map(|i| i.info.full_path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![String::from("a"), items[0].path.clone(),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent() {
|
||||
let items = string_vec_to_status(&[
|
||||
"a/b/file.txt", //
|
||||
]);
|
||||
|
||||
let list = FileTreeItems::new(&items, &BTreeSet::new());
|
||||
let mut res = list
|
||||
.0
|
||||
.iter()
|
||||
.map(|i| (i.info.indent, i.info.path.as_str()));
|
||||
|
||||
assert_eq!(res.next(), Some((0, "a")));
|
||||
assert_eq!(res.next(), Some((1, "b")));
|
||||
assert_eq!(res.next(), Some((2, "file.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_folder_file_name() {
|
||||
let items = string_vec_to_status(&[
|
||||
"a/b", //
|
||||
"a.txt", //
|
||||
]);
|
||||
|
||||
let list = FileTreeItems::new(&items, &BTreeSet::new());
|
||||
let mut res = list
|
||||
.0
|
||||
.iter()
|
||||
.map(|i| (i.info.indent, i.info.path.as_str()));
|
||||
|
||||
assert_eq!(res.next(), Some((0, "a")));
|
||||
assert_eq!(res.next(), Some((1, "b")));
|
||||
assert_eq!(res.next(), Some((0, "a.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folder_dup() {
|
||||
let items = string_vec_to_status(&[
|
||||
"a/file.txt", //
|
||||
"a/file2.txt", //
|
||||
]);
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new())
|
||||
.0
|
||||
.iter()
|
||||
.map(|i| i.info.full_path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
String::from("a"),
|
||||
items[0].path.clone(),
|
||||
items[1].path.clone()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_parent() {
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let res = FileTreeItems::new(
|
||||
&string_vec_to_status(&[
|
||||
"a/b/c", //
|
||||
"a/b/d", //
|
||||
]),
|
||||
&BTreeSet::new(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
res.find_parent_index(&String::from("a/b/c"), 3),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
2
src/components/utils/mod.rs
Normal file
2
src/components/utils/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod filetree;
|
||||
pub mod statustree;
|
||||
|
|
@ -41,3 +41,8 @@ pub const ENTER: KeyEvent = no_mod(KeyCode::Enter);
|
|||
pub const STATUS_STAGE_FILE: KeyEvent = no_mod(KeyCode::Enter);
|
||||
pub const STATUS_RESET_FILE: KeyEvent =
|
||||
with_mod(KeyCode::Char('D'), KeyModifiers::SHIFT);
|
||||
pub const STASHING_SAVE: KeyEvent = no_mod(KeyCode::Char('s'));
|
||||
pub const STASHING_TOGGLE_UNTRACKED: KeyEvent =
|
||||
no_mod(KeyCode::Char('u'));
|
||||
pub const STASHING_TOGGLE_INDEX: KeyEvent =
|
||||
no_mod(KeyCode::Char('i'));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pub static TITLE_DIFF: &str = "Diff: ";
|
|||
pub static TITLE_INDEX: &str = "Staged Changes [2]";
|
||||
|
||||
pub static TAB_STATUS: &str = "Status";
|
||||
pub static TAB_STASHING: &str = "Stashing";
|
||||
pub static TAB_LOG: &str = "Log";
|
||||
pub static TAB_DIVIDER: &str = " | ";
|
||||
|
||||
|
|
@ -16,6 +17,9 @@ pub static RESET_MSG: &str = "confirm file reset?";
|
|||
|
||||
pub static HELP_TITLE: &str = "Help: all commands";
|
||||
|
||||
pub static STASHING_FILES_TITLE: &str = "Files to Stash";
|
||||
pub static STASHING_OPTIONS_TITLE: &str = "Options";
|
||||
|
||||
pub mod commands {
|
||||
use crate::components::CommandText;
|
||||
|
||||
|
|
@ -23,6 +27,7 @@ pub mod commands {
|
|||
static CMD_GROUP_DIFF: &str = "Diff";
|
||||
static CMD_GROUP_CHANGES: &str = "Changes";
|
||||
static CMD_GROUP_COMMIT: &str = "Commit";
|
||||
static CMD_GROUP_STASHING: &str = "Stashing";
|
||||
|
||||
///
|
||||
pub static TOGGLE_TABS: CommandText = CommandText::new(
|
||||
|
|
@ -152,4 +157,25 @@ pub mod commands {
|
|||
"resets the file in question",
|
||||
CMD_GROUP_GENERAL,
|
||||
);
|
||||
|
||||
///
|
||||
pub static STASHING_SAVE: CommandText = CommandText::new(
|
||||
"Save [s]",
|
||||
"creates a new stash",
|
||||
CMD_GROUP_STASHING,
|
||||
);
|
||||
///
|
||||
pub static STASHING_TOGGLE_INDEXED: CommandText =
|
||||
CommandText::new(
|
||||
"Toggle Staged [i]",
|
||||
"toggle including staged files into stash",
|
||||
CMD_GROUP_STASHING,
|
||||
);
|
||||
///
|
||||
pub static STASHING_TOGGLE_UNTRACKED: CommandText =
|
||||
CommandText::new(
|
||||
"Toggle Untracked [u]",
|
||||
"toggle including untracked files into stash",
|
||||
CMD_GROUP_STASHING,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
mod revlog;
|
||||
mod stashing;
|
||||
mod status;
|
||||
|
||||
pub use revlog::Revlog;
|
||||
pub use stashing::Stashing;
|
||||
pub use status::Status;
|
||||
|
|
|
|||
249
src/tabs/stashing.rs
Normal file
249
src/tabs/stashing.rs
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
use crate::{
|
||||
components::{
|
||||
CommandBlocking, CommandInfo, Component, DrawableComponent,
|
||||
FileTreeComponent,
|
||||
},
|
||||
keys,
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings,
|
||||
ui::style::Theme,
|
||||
};
|
||||
use asyncgit::{
|
||||
sync::{self, status::StatusType},
|
||||
AsyncNotification, AsyncStatus2, StatusParams, CWD,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::Event;
|
||||
use std::borrow::Cow;
|
||||
use strings::commands;
|
||||
use tui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders, Paragraph, Text},
|
||||
};
|
||||
|
||||
struct Options {
|
||||
stash_untracked: bool,
|
||||
stash_indexed: bool,
|
||||
}
|
||||
|
||||
pub struct Stashing {
|
||||
visible: bool,
|
||||
options: Options,
|
||||
index: FileTreeComponent,
|
||||
theme: Theme,
|
||||
git_status: AsyncStatus2,
|
||||
queue: Queue,
|
||||
}
|
||||
|
||||
impl Stashing {
|
||||
///
|
||||
pub fn new(
|
||||
sender: &Sender<AsyncNotification>,
|
||||
queue: &Queue,
|
||||
theme: &Theme,
|
||||
) -> Self {
|
||||
Self {
|
||||
visible: false,
|
||||
options: Options {
|
||||
stash_indexed: true,
|
||||
stash_untracked: true,
|
||||
},
|
||||
index: FileTreeComponent::new(
|
||||
strings::STASHING_FILES_TITLE,
|
||||
true,
|
||||
queue.clone(),
|
||||
theme,
|
||||
),
|
||||
theme: *theme,
|
||||
git_status: AsyncStatus2::new(sender.clone()),
|
||||
queue: queue.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update(&mut self) {
|
||||
let status_type = if self.options.stash_indexed {
|
||||
StatusType::Both
|
||||
} else {
|
||||
StatusType::WorkingDir
|
||||
};
|
||||
|
||||
self.git_status
|
||||
.fetch(StatusParams::new(
|
||||
status_type,
|
||||
self.options.stash_untracked,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
///
|
||||
pub fn anything_pending(&self) -> bool {
|
||||
self.git_status.is_pending()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update_git(&mut self, ev: AsyncNotification) {
|
||||
if self.visible {
|
||||
if let AsyncNotification::Status = ev {
|
||||
let status = self.git_status.last().unwrap();
|
||||
self.index.update(&status.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_option_text(&self) -> Vec<Text> {
|
||||
let bracket_open = Text::Raw(Cow::from("["));
|
||||
let bracket_close = Text::Raw(Cow::from("]"));
|
||||
let option_on =
|
||||
Text::Styled(Cow::from("x"), self.theme.option(true));
|
||||
|
||||
let option_off =
|
||||
Text::Styled(Cow::from("_"), self.theme.option(false));
|
||||
|
||||
vec![
|
||||
bracket_open.clone(),
|
||||
if self.options.stash_untracked {
|
||||
option_on.clone()
|
||||
} else {
|
||||
option_off.clone()
|
||||
},
|
||||
bracket_close.clone(),
|
||||
Text::Raw(Cow::from(" stash untracked\n")),
|
||||
bracket_open,
|
||||
if self.options.stash_indexed {
|
||||
option_on.clone()
|
||||
} else {
|
||||
option_off.clone()
|
||||
},
|
||||
bracket_close,
|
||||
Text::Raw(Cow::from(" stash staged")),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for Stashing {
|
||||
fn draw<B: tui::backend::Backend>(
|
||||
&mut self,
|
||||
f: &mut tui::Frame<B>,
|
||||
rect: tui::layout::Rect,
|
||||
) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[Constraint::Min(1), Constraint::Length(22)].as_ref(),
|
||||
)
|
||||
.split(rect);
|
||||
|
||||
let right_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[Constraint::Length(4), Constraint::Min(1)].as_ref(),
|
||||
)
|
||||
.split(chunks[1]);
|
||||
|
||||
self.index.draw(f, chunks[0]);
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(self.get_option_text().iter())
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(strings::STASHING_OPTIONS_TITLE),
|
||||
)
|
||||
.alignment(Alignment::Left),
|
||||
right_chunks[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Stashing {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
self.index.commands(out, force_all);
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
commands::STASHING_SAVE,
|
||||
self.visible && !self.index.is_empty(),
|
||||
self.visible || force_all,
|
||||
));
|
||||
out.push(CommandInfo::new(
|
||||
commands::STASHING_TOGGLE_INDEXED,
|
||||
self.visible,
|
||||
self.visible || force_all,
|
||||
));
|
||||
out.push(CommandInfo::new(
|
||||
commands::STASHING_TOGGLE_UNTRACKED,
|
||||
self.visible,
|
||||
self.visible || force_all,
|
||||
));
|
||||
|
||||
if self.visible {
|
||||
CommandBlocking::Blocking
|
||||
} else {
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: crossterm::event::Event) -> bool {
|
||||
if self.visible {
|
||||
let conusmed = self.index.event(ev);
|
||||
|
||||
if conusmed {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Event::Key(k) = ev {
|
||||
return match k {
|
||||
keys::STASHING_SAVE if !self.index.is_empty() => {
|
||||
if sync::stash_save(
|
||||
CWD,
|
||||
None,
|
||||
self.options.stash_untracked,
|
||||
!self.options.stash_indexed,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
self.queue.borrow_mut().push_back(
|
||||
InternalEvent::Update(
|
||||
NeedsUpdate::ALL,
|
||||
),
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
keys::STASHING_TOGGLE_INDEX => {
|
||||
self.options.stash_indexed =
|
||||
!self.options.stash_indexed;
|
||||
self.update();
|
||||
true
|
||||
}
|
||||
keys::STASHING_TOGGLE_UNTRACKED => {
|
||||
self.options.stash_untracked =
|
||||
!self.options.stash_untracked;
|
||||
self.update();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
fn show(&mut self) {
|
||||
self.update();
|
||||
self.visible = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,14 @@ impl Theme {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn option(&self, on: bool) -> Style {
|
||||
if on {
|
||||
Style::default().fg(self.diff_line_add)
|
||||
} else {
|
||||
Style::default().fg(self.diff_line_delete)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_hunk_marker(&self, selected: bool) -> Style {
|
||||
if selected {
|
||||
Style::default().bg(self.selection_bg)
|
||||
|
|
|
|||
Loading…
Reference in a new issue