diff --git a/Cargo.lock b/Cargo.lock index f15e14f983..fad6a41186 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1194,6 +1194,7 @@ dependencies = [ "shellexpand", "simplelog", "struct-patch", + "strum", "syntect", "tempfile", "tui-textarea", diff --git a/Cargo.toml b/Cargo.toml index 75bb1cd9c6..32c439b4b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ serde = "1.0" shellexpand = "3.1" simplelog = { version = "0.12", default-features = false } struct-patch = "0.8" +strum = "0.26.3" syntect = { version = "5.2", default-features = false, features = [ "parsing", "default-syntaxes", @@ -78,8 +79,9 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] } maintenance = { status = "actively-developed" } [features] -default = ["ghemoji", "regex-fancy", "trace-libgit", "vendor-openssl"] +default = ["ghemoji", "regex-fancy", "trace-libgit", "vendor-openssl", "gitmoji"] ghemoji = ["gh-emoji"] +gitmoji = [] # regex-* features are mutually exclusive. regex-fancy = ["syntect/regex-fancy", "two-face/syntect-fancy"] regex-onig = ["syntect/regex-onig", "two-face/syntect-onig"] diff --git a/src/app.rs b/src/app.rs index 45037f048f..65679fcf9a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,9 +11,9 @@ use crate::{ popup_stack::PopupStack, popups::{ AppOption, BlameFilePopup, BranchListPopup, CommitPopup, - CompareCommitsPopup, ConfirmPopup, CreateBranchPopup, - CreateRemotePopup, ExternalEditorPopup, FetchPopup, - FileRevlogPopup, FuzzyFindPopup, HelpPopup, + CompareCommitsPopup, ConfirmPopup, ConventionalCommitPopup, + CreateBranchPopup, CreateRemotePopup, ExternalEditorPopup, + FetchPopup, FileRevlogPopup, FuzzyFindPopup, HelpPopup, InspectCommitPopup, LogSearchPopupPopup, MsgPopup, OptionsPopup, PullPopup, PushPopup, PushTagsPopup, RemoteListPopup, RenameBranchPopup, RenameRemotePopup, @@ -93,6 +93,7 @@ pub struct App { update_remote_url_popup: UpdateRemoteUrlPopup, remotes_popup: RemoteListPopup, rename_branch_popup: RenameBranchPopup, + conventional_commit_popup: ConventionalCommitPopup, select_branch_popup: BranchListPopup, options_popup: OptionsPopup, submodule_popup: SubmodulesListPopup, @@ -200,6 +201,9 @@ impl App { update_remote_url_popup: UpdateRemoteUrlPopup::new(&env), remotes_popup: RemoteListPopup::new(&env), rename_branch_popup: RenameBranchPopup::new(&env), + conventional_commit_popup: ConventionalCommitPopup::new( + &env, + ), select_branch_popup: BranchListPopup::new(&env), tags_popup: TagListPopup::new(&env), options_popup: OptionsPopup::new(&env), @@ -481,6 +485,7 @@ impl App { msg_popup, confirm_popup, commit_popup, + conventional_commit_popup, blame_file_popup, file_revlog_popup, stashmsg_popup, @@ -520,6 +525,7 @@ impl App { stashmsg_popup, help_popup, inspect_commit_popup, + conventional_commit_popup, compare_commits_popup, blame_file_popup, file_revlog_popup, @@ -738,6 +744,12 @@ impl App { } InternalEvent::Update(u) => flags.insert(u), InternalEvent::OpenCommit => self.commit_popup.show()?, + InternalEvent::AddCommitMessage(s) => { + self.commit_popup.set_msg(s); + } + InternalEvent::OpenConventionalCommit => { + self.conventional_commit_popup.show()?; + } InternalEvent::RewordCommit(id) => { self.commit_popup.open(Some(id))?; } diff --git a/src/components/textinput.rs b/src/components/textinput.rs index e6586b8355..c38df185e0 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -79,6 +79,12 @@ impl TextInputComponent { } } + pub fn move_cursor_to_end(&mut self) { + if let Some(ta) = &mut self.textarea { + ta.move_cursor(CursorMove::End); + } + } + /// pub const fn with_input_type( mut self, diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 0f2909a2fe..db8d0fa140 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -49,7 +49,9 @@ pub struct KeysList { pub quit: GituiKeyEvent, pub exit_popup: GituiKeyEvent, pub open_commit: GituiKeyEvent, + pub open_conventional_commit: GituiKeyEvent, pub open_commit_editor: GituiKeyEvent, + pub breaking: GituiKeyEvent, pub open_help: GituiKeyEvent, pub open_options: GituiKeyEvent, pub move_left: GituiKeyEvent, @@ -128,6 +130,7 @@ pub struct KeysList { pub commit_history_next: GituiKeyEvent, pub commit: GituiKeyEvent, pub newline: GituiKeyEvent, + pub insert: GituiKeyEvent, } #[rustfmt::skip] @@ -146,7 +149,9 @@ impl Default for KeysList { quit: GituiKeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()), exit_popup: GituiKeyEvent::new(KeyCode::Esc, KeyModifiers::empty()), open_commit: GituiKeyEvent::new(KeyCode::Char('c'), KeyModifiers::empty()), + open_conventional_commit: GituiKeyEvent::new(KeyCode::Char('C'), KeyModifiers::SHIFT), open_commit_editor: GituiKeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL), + breaking: GituiKeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()), open_help: GituiKeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty()), open_options: GituiKeyEvent::new(KeyCode::Char('o'), KeyModifiers::empty()), move_left: GituiKeyEvent::new(KeyCode::Left, KeyModifiers::empty()), @@ -175,6 +180,7 @@ impl Default for KeysList { stashing_save: GituiKeyEvent::new(KeyCode::Char('s'), KeyModifiers::empty()), stashing_toggle_untracked: GituiKeyEvent::new(KeyCode::Char('u'), KeyModifiers::empty()), stashing_toggle_index: GituiKeyEvent::new(KeyCode::Char('i'), KeyModifiers::empty()), + insert: GituiKeyEvent::new(KeyCode::Char('i'), KeyModifiers::empty()), stash_apply: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), stash_open: GituiKeyEvent::new(KeyCode::Right, KeyModifiers::empty()), stash_drop: GituiKeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT), diff --git a/src/popups/commit.rs b/src/popups/commit.rs index 4dcfc3241c..17b2e55cad 100644 --- a/src/popups/commit.rs +++ b/src/popups/commit.rs @@ -90,6 +90,11 @@ impl CommitPopup { } } + pub fn set_msg(&mut self, msg: String) { + self.input.set_text(msg); + self.input.move_cursor_to_end(); + } + /// pub fn update(&mut self) { self.git_branch_name.lookup().ok(); diff --git a/src/popups/conventional_commit.rs b/src/popups/conventional_commit.rs new file mode 100644 index 0000000000..5d5a8e870c --- /dev/null +++ b/src/popups/conventional_commit.rs @@ -0,0 +1,896 @@ +use std::borrow::{Borrow, Cow}; + +use anyhow::Result; +use crossterm::event::{Event, KeyCode}; +use itertools::Itertools; +use ratatui::text::Line; +use ratatui::Frame; +use ratatui::{ + layout::{Constraint, Direction, Layout, Margin, Rect}, + text::Span, + widgets::{Block, Borders, Clear}, +}; +use strum::{Display, EnumIter, IntoEnumIterator, IntoStaticStr}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::components::visibility_blocking; +use crate::queue::Queue; +use crate::string_utils::trim_length_left; +use crate::strings; +use crate::ui::style::SharedTheme; +use crate::{ + app::Environment, + components::{ + CommandBlocking, CommandInfo, Component, DrawableComponent, + EventState, InputType, ScrollType, TextInputComponent, + }, + keys::{key_match, SharedKeyConfig}, + ui, +}; + +#[derive(EnumIter, Copy, Clone, IntoStaticStr, Display)] +#[strum(serialize_all = "lowercase")] +enum CommitType { + Refactor, + #[strum(to_string = "feat")] + Feature, + Fix, + Wip, + Debug, + Test, + Docs, + Style, + #[strum(to_string = "perf")] + Performance, + Chore, + Revert, + Initial, + Bump, + Build, + CI, +} + +#[cfg(feature = "gitmoji")] +#[derive(Copy, Clone)] +enum MoreInfoCommit { + /// 🎨 + CodeStyle, + /// 💅 + Formatted, + /// ⚡️ + Performance, + /// 🐛 + Bug, + /// 🚑️ + CriticalBug, + /// ✨ + Feature, + /// 📝 + Documentation, + /// 💄 + UI, + /// 🎉 + Initial, + /// ✅ + TestsPassing, + /// ➕ + Add, + /// ➖ + Remove, + /// 🔒️ + Security, + /// 🔖 + Release, + /// ⚠️ + Warning, + /// 🚧 + Wip, + /// ⬇️ + Down, + /// ⬆️ + Up, + /// 👷 + CI, + /// ♻️ + Refactor, + /// 📈 + TrackCode, + /// ✏️ + Typo, + /// 🌐 + Internationalization, + /// ⏪️ + Revert, + /// 📦️ + Package, + /// 👽️ + ExternalDependencyChange, + /// 🚚 + RenameResources, + /// ♿️ + Accessibility, + /// 📜 + Readme, + /// ⚖️ + License, + /// 💬 + TextLiteral, + /// ⛃ + DatabaseRelated, + /// 🔊 + AddLogs, + /// 🔇 + RemoveLogs, + /// 🚸 + ImproveExperience, + /// 🏗️ + ArchitecturalChanges, + /// 🤡 + WrittingReallyBadCode, + /// 🙈 + GitIgnore, + /// ⚗️ + Experimentations, + /// 🚩 + Flag, + /// 🗑️ + Trash, + /// 🛂 + Authorization, + /// 🩹 + QuickFix, + /// ⚰️ + RemoveDeadCode, + /// 👔 + Business, + /// 🩺 + HealthCheck, + /// 🧱 + Infra, + /// 🦺 + Validation, +} + +#[cfg(feature = "gitmoji")] +impl MoreInfoCommit { + const fn strings( + self, + ) -> (&'static str, &'static str, &'static str) { + match self { + Self::UI => ("💄", "UI", "UI related"), + Self::CodeStyle => ("🎨", "style", "Style of the code"), + Self::Performance => ("⚡️", "", "Performance"), + Self::Bug => ("🐛", "bug", "Normal bug"), + Self::CriticalBug => { + ("🚑️", "critical bug", "Critical Bug") + } + Self::Feature => ("✨", "", "Feature"), + Self::Documentation => ("📝", "", "Documentation"), + Self::Initial => ("🎉", "", "Initial commit!"), + Self::TestsPassing => { + ("✅", "passing", "Test are now passing!") + } + Self::Add => ("➕", "add", "Added"), + Self::Remove => ("➖", "remove", "Removed"), + Self::Security => ("🔒️", "security", "Secutiry related"), + Self::Release => ("🔖", "release", "A new relase"), + Self::Warning => ("⚠️", "warning", "Warning"), + Self::Wip => ("🚧", "", "WIP"), + Self::Down => ("⬇️", "downgrade", "Down"), + Self::Up => ("⬆️", "upgrade", "Up"), + Self::CI => ("👷", "", "CI related"), + Self::Refactor => ("♻️", "", "Refactor related"), + Self::TrackCode => ("📈", "track", "Tracking code"), + Self::Typo => ("✏️", "typo", "Typo"), + Self::Internationalization => { + ("🌐", "i18n", "Internationalization") + } + Self::Revert => ("⏪️", "", "Revert"), + Self::Package => ("📦️", "", "Package related"), + Self::ExternalDependencyChange => ( + "👽️", + "change due to external dep update", + "Code related to change of ext dep", + ), + Self::RenameResources => { + ("🚚", "rename", "Rename some resources") + } + Self::Accessibility => { + ("♿️", "accessibility", "Improved accessibility") + } + Self::Readme => ("📜", "README", "README"), + Self::License => ("⚖️", "LICENSE", "LICENSE"), + Self::TextLiteral => { + ("💬", "raw value", "Modified literal value") + } + Self::DatabaseRelated => ("⛃", "db", "Database related"), + Self::AddLogs => ("🔊", "add logs", "Add logs"), + Self::RemoveLogs => ("🔇", "remove logs", "Remove logs"), + Self::ImproveExperience => { + ("🚸", "experience", "Improve experience") + } + Self::ArchitecturalChanges => { + ("🏗️", "architecture", "Architectural Changes") + } + Self::WrittingReallyBadCode => ( + "🤡", + "really bad code", + "This is some REALLY bad code", + ), + Self::GitIgnore => ("🙈", "gitignore", "GitIgnore"), + Self::Experimentations => { + ("⚗️", "experimentations", "Experimentations") + } + Self::Flag => ("🚩", "flag", "Flag"), + Self::Trash => ("🗑️", "", "Trash"), + Self::Authorization => { + ("🛂", "authorization", "Authorization") + } + Self::QuickFix => ("🩹", "quick-fix", "QuickFix"), + Self::RemoveDeadCode => { + ("⚰️", "remove dead code", "RemoveDeadCode") + } + Self::Business => ("👔", "business", "Business related"), + Self::HealthCheck => ("🩺", "healthcheck", "HealthCheck"), + Self::Infra => ("🧱", "infra", "Infra"), + Self::Validation => ("🦺", "validation", "Validation"), + Self::Formatted => ("💅", "fmt", "Formatted"), + } + } +} + +#[cfg(feature = "gitmoji")] +impl CommitType { + #[allow(clippy::pedantic)] + fn more_info(&self) -> Vec { + match *self { + Self::Fix => { + vec![ + MoreInfoCommit::Bug, + MoreInfoCommit::CriticalBug, + MoreInfoCommit::QuickFix, + MoreInfoCommit::Warning, + MoreInfoCommit::Typo, + MoreInfoCommit::TextLiteral, + MoreInfoCommit::Security, + MoreInfoCommit::TrackCode, + MoreInfoCommit::ExternalDependencyChange, + MoreInfoCommit::DatabaseRelated, + MoreInfoCommit::Authorization, + MoreInfoCommit::HealthCheck, + MoreInfoCommit::Business, + MoreInfoCommit::Infra, + ] + } + Self::Feature => vec![ + MoreInfoCommit::Feature, + MoreInfoCommit::Security, + MoreInfoCommit::TrackCode, + MoreInfoCommit::Internationalization, + MoreInfoCommit::Package, + MoreInfoCommit::Accessibility, + MoreInfoCommit::Readme, + MoreInfoCommit::License, + MoreInfoCommit::DatabaseRelated, + MoreInfoCommit::Flag, + MoreInfoCommit::Authorization, + MoreInfoCommit::Business, + MoreInfoCommit::Validation, + ], + Self::Chore | Self::Refactor => vec![ + MoreInfoCommit::Refactor, + MoreInfoCommit::ArchitecturalChanges, + MoreInfoCommit::RenameResources, + MoreInfoCommit::RemoveLogs, + MoreInfoCommit::TextLiteral, + MoreInfoCommit::RemoveDeadCode, + MoreInfoCommit::DatabaseRelated, + MoreInfoCommit::Security, + MoreInfoCommit::Readme, + MoreInfoCommit::License, + MoreInfoCommit::ImproveExperience, + MoreInfoCommit::TrackCode, + MoreInfoCommit::Internationalization, + MoreInfoCommit::Accessibility, + MoreInfoCommit::GitIgnore, + MoreInfoCommit::Flag, + MoreInfoCommit::Trash, + MoreInfoCommit::Authorization, + MoreInfoCommit::Business, + MoreInfoCommit::Infra, + MoreInfoCommit::Validation, + ], + Self::CI => vec![MoreInfoCommit::CI], + Self::Initial => vec![MoreInfoCommit::Initial], + Self::Performance => { + vec![ + MoreInfoCommit::Performance, + MoreInfoCommit::DatabaseRelated, + ] + } + Self::Wip => vec![ + MoreInfoCommit::Wip, + MoreInfoCommit::WrittingReallyBadCode, + MoreInfoCommit::Experimentations, + ], + Self::Docs => vec![MoreInfoCommit::Documentation], + Self::Test => vec![ + MoreInfoCommit::TestsPassing, + MoreInfoCommit::Add, + MoreInfoCommit::Remove, + MoreInfoCommit::Experimentations, + MoreInfoCommit::HealthCheck, + MoreInfoCommit::Validation, + ], + Self::Bump => { + vec![ + MoreInfoCommit::Add, + MoreInfoCommit::Remove, + MoreInfoCommit::Down, + MoreInfoCommit::Up, + MoreInfoCommit::Release, + MoreInfoCommit::Package, + ] + } + Self::Style => { + vec![ + MoreInfoCommit::Formatted, + MoreInfoCommit::CodeStyle, + MoreInfoCommit::UI, + MoreInfoCommit::ImproveExperience, + ] + } + Self::Build => vec![MoreInfoCommit::CI], + Self::Debug => vec![ + MoreInfoCommit::AddLogs, + MoreInfoCommit::TrackCode, + MoreInfoCommit::HealthCheck, + MoreInfoCommit::RemoveLogs, + ], + Self::Revert => vec![MoreInfoCommit::Revert], + } + } +} + +pub struct ConventionalCommitPopup { + key_config: SharedKeyConfig, + is_visible: bool, + is_insert: bool, + is_breaking: bool, + query: Option, + selected_index: usize, + options: Vec, + query_results_type: Vec, + #[cfg(feature = "gitmoji")] + query_results_more_info: Vec, + input: TextInputComponent, + theme: SharedTheme, + seleted_commit_type: Option, + queue: Queue, +} + +impl ConventionalCommitPopup { + pub fn new(env: &Environment) -> Self { + let mut input = + TextInputComponent::new(env, "", "Filter ", false) + .with_input_type(InputType::Singleline); + input.embed(); + + Self { + selected_index: 0, + input, + options: CommitType::iter().collect_vec(), + query_results_type: CommitType::iter().collect_vec(), + #[cfg(feature = "gitmoji")] + query_results_more_info: Vec::new(), + is_insert: false, + is_breaking: false, + query: None, + is_visible: false, + key_config: env.key_config.clone(), + seleted_commit_type: None, + theme: env.theme.clone(), + queue: env.queue.clone(), + } + } + + #[inline] + fn draw_matches_list(&self, f: &mut Frame, area: Rect) { + let height = usize::from(area.height); + let width = usize::from(area.width); + + let quick_shortcuts = self.quick_shortcuts(); + + let results = { + #[cfg(feature = "gitmoji")] + { + if self.seleted_commit_type.is_some() { + self.query_results_more_info.len() + } else { + self.query_results_type.len() + } + } + #[cfg(not(feature = "gitmoji"))] + { + self.query_results_type.len() + } + }; + + let block = Block::default() + .title(Span::styled( + format!("Results: {results}"), + self.theme.title(true), + )) + .borders(Borders::TOP); + + if self.seleted_commit_type.is_some() { + ui::draw_list_block(f, area, block, { + #[cfg(feature = "gitmoji")] + { + self.query_results_more_info + .iter() + .enumerate() + .take(height) + .map(|(idx, more_info)| { + let (emoji, _, long_name) = + more_info.strings(); + let text_string = + format!("{emoji} {long_name}"); + let text = + trim_length_left(&text_string, width); + self.line_from_text( + self.selected_index == idx, + &format!("{text}{:width$}", " "), + ) + }) + } + #[cfg(not(feature = "gitmoji"))] + { + std::iter::empty::() + } + }); + } else { + let max_len = self + .query_results_type + .iter() + .map(|s| Into::<&str>::into(s).len()) + .max(); + + ui::draw_list_block( + f, + area, + block, + self.query_results_type + .iter() + .enumerate() + .take(height) + .map(|(idx, commit_type)| { + let text_string = format!( + "{:w$} [{}]", + commit_type, + quick_shortcuts[idx], + w = max_len.unwrap_or_default(), + ); + let text = + trim_length_left(&text_string, width); + + self.line_from_text( + self.selected_index == idx, + &format!("{text}{:width$}", " "), + ) + }), + ); + } + } + + fn line_from_text(&self, is_selected: bool, text: &str) -> Line { + Line::from( + text.graphemes(true) + .map(|c| { + Span::styled( + Cow::from(c.to_string()), + self.theme.text(is_selected, is_selected), + ) + }) + .collect_vec(), + ) + } + + pub fn quick_shortcuts(&self) -> Vec { + let mut available_chars = ('a'..='z').collect_vec(); + + for k in [ + self.key_config.keys.popup_up, + self.key_config.keys.popup_down, + self.key_config.keys.exit_popup, + self.key_config.keys.breaking, + self.key_config.keys.exit, + self.key_config.keys.insert, + ] { + if let KeyCode::Char(c) = k.code { + if let Some(char_to_remove_index) = + available_chars.iter().position(|&ch| ch == c) + { + available_chars.remove(char_to_remove_index); + } + } + } + + self.query_results_type + .iter() + .map(|s| { + if let Some(ch) = Into::<&str>::into(s).chars() + .find(|c| available_chars.contains(c)) { + available_chars.retain(|&c| c != ch); + ch + } else { + *available_chars.first().expect("Should already have at least one letter available") + } + }) + .collect_vec() + } + + pub fn move_selection(&mut self, direction: ScrollType) { + let new_selection = match direction { + ScrollType::Up => self.selected_index.saturating_sub(1), + ScrollType::Down => self.selected_index.saturating_add(1), + _ => self.selected_index, + }; + + let new_selection = new_selection.clamp( + 0, + if self.seleted_commit_type.is_some() { + #[cfg(feature = "gitmoji")] + { + self.query_results_more_info.len() + } + #[cfg(not(feature = "gitmoji"))] + { + self.query_results_type.len() + } + } else { + self.query_results_type.len() + } + .saturating_sub(1), + ); + + self.selected_index = new_selection; + } + + fn update_query(&mut self) { + if self + .query + .as_ref() + .map_or(true, |q| q != self.input.get_text()) + { + let text = self.input.get_text(); + self.set_query(text.to_owned()); + } + } + + fn set_query>(&mut self, query: S) { + let query = query.borrow().to_lowercase(); + self.query = Some(query.clone()); + + let new_len = + if let Some(commit_type) = &self.seleted_commit_type { + #[cfg(feature = "gitmoji")] + { + self.query_results_more_info = commit_type + .more_info() + .iter() + .filter(|more_info_commit| { + more_info_commit + .strings() + .2 + .to_lowercase() + .contains(&query) + }) + .copied() + .collect_vec(); + + self.query_results_more_info.len() + } + #[cfg(not(feature = "gitmoji"))] + { + let _ = commit_type; + self.hide(); + self.query_results_type.len() + } + } else { + self.query_results_type = self + .options + .iter() + .filter(|option| { + Into::<&str>::into(*option) + .to_lowercase() + .contains(&query) + }) + .copied() + .collect_vec(); + + self.query_results_type.len() + }; + + if self.selected_index >= new_len { + self.selected_index = new_len.saturating_sub(1); + } + } + + fn validate_escape(&mut self, commit_type: CommitType) { + #[cfg(not(feature = "gitmoji"))] + { + self.queue.push(crate::queue::InternalEvent::OpenCommit); + self.queue.push( + crate::queue::InternalEvent::AddCommitMessage( + format!( + "{commit_type}{}:", + if self.is_breaking { "!" } else { "" }, + ), + ), + ); + self.hide(); + } + #[cfg(feature = "gitmoji")] + { + if let Some((emoji, short_msg, _)) = self + .query_results_more_info + .get(self.selected_index) + .map(|more_info| more_info.strings()) + { + self.queue + .push(crate::queue::InternalEvent::OpenCommit); + self.queue.push( + crate::queue::InternalEvent::AddCommitMessage( + format!( + "{emoji} {commit_type}{}{}{short_msg}", + if self.is_breaking { "!" } else { "" }, + if short_msg.is_empty() { + "" + } else { + ": " + }, + ), + ), + ); + self.hide(); + } + } + } + + fn next_step(&mut self) { + self.selected_index = 0; + self.is_insert = false; + self.query = None; + self.input.clear(); + self.update_query(); + } +} + +impl DrawableComponent for ConventionalCommitPopup { + fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> { + if self.is_visible { + const MAX_SIZE: (u16, u16) = (50, 25); + + let area = ui::centered_rect_absolute( + MAX_SIZE.0, MAX_SIZE.1, area, + ); + + f.render_widget(Clear, area); + f.render_widget( + Block::default() + .borders(Borders::all()) + .style(self.theme.title(true)) + .title(Span::styled( + { + #[cfg(feature = "gitmoji")] + if self.seleted_commit_type.is_some() { + strings::POPUP_TITLE_GITMOJI + } else { + strings::POPUP_TITLE_CONVENTIONAL_COMMIT + } + #[cfg(not(feature = "gitmoji"))] + strings::POPUP_TITLE_CONVENTIONAL_COMMIT + }, + self.theme.title(true), + )) + .title(if self.is_breaking { + Span::styled( + "[BREAKING]", + self.theme.title(true), + ) + } else { + "".into() + }) + .title(if self.is_insert { + Span::styled( + "[INSERT]", + self.theme.title(true), + ) + .into_right_aligned_line() + } else { + "".into() + }), + area, + ); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(1), + Constraint::Percentage(100), + ] + .as_ref(), + ) + .split(area.inner(Margin { + horizontal: 1, + vertical: 1, + })); + + self.input.draw(f, chunks[0])?; + + self.draw_matches_list(f, chunks[1]); + } + + Ok(()) + } +} + +impl Component for ConventionalCommitPopup { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + if self.is_insert { + out.push(CommandInfo::new( + strings::commands::exit_insert(&self.key_config), + true, + true, + )); + } else { + out.push(CommandInfo::new( + strings::commands::insert(&self.key_config), + true, + true, + )); + + out.push(CommandInfo::new( + strings::commands::close_fuzzy_finder( + &self.key_config, + ), + true, + true, + )); + } + + out.push(CommandInfo::new( + strings::commands::scroll_popup(&self.key_config), + true, + true, + )); + + out.push(CommandInfo::new( + strings::commands::open_submodule(&self.key_config), + true, + true, + )); + } + + visibility_blocking(self) + } + + fn event( + &mut self, + event: &crossterm::event::Event, + ) -> Result { + if self.is_visible() { + if let Event::Key(key) = event { + if key_match(key, self.key_config.keys.exit_popup) { + if self.is_insert { + self.is_insert = false; + } else { + self.hide(); + } + } else if key_match(key, self.key_config.keys.enter) { + if let Some(commit_type) = + self.seleted_commit_type + { + self.validate_escape(commit_type); + } else if let Some(&commit) = self + .query_results_type + .get(self.selected_index) + { + self.seleted_commit_type = Some(commit); + + #[cfg(feature = "gitmoji")] + { + self.next_step(); + + if self.query_results_more_info.len() == 1 + { + self.validate_escape(commit); + } + } + #[cfg(not(feature = "gitmoji"))] + self.validate_escape(commit); + } + } else if key_match( + key, + self.key_config.keys.breaking, + ) { + self.is_breaking = !self.is_breaking; + } else if key_match( + key, + self.key_config.keys.popup_down, + ) { + self.move_selection(ScrollType::Down); + } else if key_match( + key, + self.key_config.keys.popup_up, + ) { + self.move_selection(ScrollType::Up); + } else if self.is_insert { + if self.input.event(event)?.is_consumed() { + self.update_query(); + } + } else if key_match(key, self.key_config.keys.insert) + { + self.is_insert = true; + } else if let KeyCode::Char(c) = key.code { + if let Some(idx) = self + .quick_shortcuts() + .into_iter() + .position(|ch| ch == c) + { + self.seleted_commit_type = + Some(self.query_results_type[idx]); + #[cfg(feature = "gitmoji")] + { + self.next_step(); + + if self.query_results_more_info.len() == 1 + { + self.validate_escape( + self.query_results_type[idx], + ); + } + } + #[cfg(not(feature = "gitmoji"))] + self.validate_escape( + self.query_results_type[idx], + ); + } + } + } + + return Ok(EventState::Consumed); + } + + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.is_visible + } + + fn hide(&mut self) { + self.next_step(); + self.is_visible = false; + self.seleted_commit_type = None; + self.query_results_type = CommitType::iter().collect_vec(); + #[cfg(feature = "gitmoji")] + { + self.query_results_more_info.clear(); + } + } + + fn show(&mut self) -> Result<()> { + self.is_visible = true; + self.input.show()?; + self.input.clear(); + Ok(()) + } +} diff --git a/src/popups/mod.rs b/src/popups/mod.rs index cb3ae1af74..3d1e5139db 100644 --- a/src/popups/mod.rs +++ b/src/popups/mod.rs @@ -3,6 +3,7 @@ mod branchlist; mod commit; mod compare_commits; mod confirm; +mod conventional_commit; mod create_branch; mod create_remote; mod externaleditor; @@ -33,6 +34,7 @@ pub use branchlist::BranchListPopup; pub use commit::CommitPopup; pub use compare_commits::CompareCommitsPopup; pub use confirm::ConfirmPopup; +pub use conventional_commit::ConventionalCommitPopup; pub use create_branch::CreateBranchPopup; pub use create_remote::CreateRemotePopup; pub use externaleditor::ExternalEditorPopup; diff --git a/src/queue.rs b/src/queue.rs index 44268a851d..5a043060b6 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -98,6 +98,10 @@ pub enum InternalEvent { /// open commit msg input OpenCommit, /// + AddCommitMessage(String), + /// open conventional commit + OpenConventionalCommit, + /// PopupStashing(StashingOptions), /// TabSwitchStatus, diff --git a/src/strings.rs b/src/strings.rs index c4cff10f70..4c6292708a 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -23,6 +23,9 @@ pub static PUSH_POPUP_STATES_DELTAS: &str = "deltas (2/3)"; pub static PUSH_POPUP_STATES_PUSHING: &str = "pushing (3/3)"; pub static PUSH_POPUP_STATES_TRANSFER: &str = "transfer"; pub static PUSH_POPUP_STATES_DONE: &str = "done"; +pub const POPUP_TITLE_CONVENTIONAL_COMMIT: &str = "Type of Commit"; +#[cfg(feature = "gitmoji")] +pub const POPUP_TITLE_GITMOJI: &str = "Emoji of Commit"; pub static PUSH_TAGS_POPUP_MSG: &str = "Push Tags"; pub static PUSH_TAGS_STATES_FETCHING: &str = "fetching"; @@ -1087,6 +1090,20 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + pub fn conventional_commit_open( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Conventional commit [{}]", + key_config.get_hint( + key_config.keys.open_conventional_commit + ) + ), + "open conventional commit popup (available in non-empty stage)", + CMD_GROUP_GENERAL, + ) + } pub fn commit_open_editor( key_config: &SharedKeyConfig, ) -> CommandText { @@ -1870,4 +1887,26 @@ pub mod commands { CMD_GROUP_LOG, ) } + + pub fn insert(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Insert [{}]", + key_config.get_hint(key_config.keys.insert), + ), + "enter in insert mode", + CMD_GROUP_LOG, + ) + } + + pub fn exit_insert(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Exit Insert [{}]", + key_config.get_hint(key_config.keys.exit_popup), + ), + "exit of insert mode", + CMD_GROUP_LOG, + ) + } } diff --git a/src/tabs/status.rs b/src/tabs/status.rs index 034ffe39e6..5e357a9455 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -804,6 +804,14 @@ impl Component for Status { true, true, )); + + out.push(CommandInfo::new( + strings::commands::conventional_commit_open( + &self.key_config, + ), + true, + self.can_commit() || force_all, + )); } self.commands_nav(out, force_all); @@ -832,6 +840,14 @@ impl Component for Status { { self.queue.push(InternalEvent::OpenCommit); Ok(EventState::Consumed) + } else if key_match( + k, + self.key_config.keys.open_conventional_commit, + ) && self.can_commit() + { + self.queue + .push(InternalEvent::OpenConventionalCommit); + Ok(EventState::Consumed) } else if key_match( k, self.key_config.keys.toggle_workarea,