diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 3ec5453f9b..5da2f670f3 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,5 +1,7 @@ use crate::branch_picker::{self, BranchList}; -use crate::git_panel::{GitPanel, commit_message_editor, panel_editor_style}; +use crate::git_panel::{ + GitPanel, commit_message_editor, commit_title_exceeds_limit, panel_editor_style, +}; use crate::git_panel_settings::GitPanelSettings; use git::repository::CommitOptions; use git::{Amend, Commit, GenerateCommitMessage, Signoff}; @@ -548,7 +550,7 @@ impl Render for CommitModal { .text(cx) .lines() .next() - .is_some_and(|title| title.len() > max_title_length) + .is_some_and(|title| commit_title_exceeds_limit(title, max_title_length)) } else { false }; diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 9bf5d047c4..04246fcd64 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4566,7 +4566,7 @@ impl GitPanel { .text(cx) .lines() .next() - .is_some_and(|title| title.len() > max_title_length) + .is_some_and(|title| commit_title_exceeds_limit(title, max_title_length)) } else { false }; @@ -7438,6 +7438,10 @@ fn format_git_error_toast_message(error: &anyhow::Error) -> String { } } +pub(crate) fn commit_title_exceeds_limit(title: &str, max_length: usize) -> bool { + max_length > 0 && title.chars().count() > max_length +} + #[cfg(test)] mod tests { use git::{ @@ -8916,6 +8920,43 @@ mod tests { } } + #[test] + fn test_commit_title_exceeds_limit() { + // ASCII only + let within_ascii = "abcde"; + let exceeds_ascii = "abcdef"; + assert!(!commit_title_exceeds_limit(within_ascii, 5)); + assert!(commit_title_exceeds_limit(exceeds_ascii, 5)); + + // Multi-byte characters are counted as grapheme clusters + let within_japanese = "あいうえお"; // 5 chars, 15 bytes + let exceeds_japanese = "あいうえおか"; // 6 chars, 18 bytes + assert!(!commit_title_exceeds_limit(within_japanese, 5)); + assert!(commit_title_exceeds_limit(exceeds_japanese, 5)); + + // Mixed ASCII + multi-byte + let within_mixed = "abcあ"; + let exceeds_mixed = "abcああ"; + assert!(!commit_title_exceeds_limit(within_mixed, 4)); + assert!(commit_title_exceeds_limit(exceeds_mixed, 4)); + + // Emoji counts as one character each + let within_emoji = "🚀"; + let exceeds_emoji = "🚀🚀"; + assert!(!commit_title_exceeds_limit(within_emoji, 1)); + assert!(commit_title_exceeds_limit(exceeds_emoji, 1)); + + // A max_length of 0 disables the limit check + assert!(!commit_title_exceeds_limit( + "anything goes when disabled", + 0 + )); + assert!(!commit_title_exceeds_limit("", 0)); + + // Empty title never exceeds a positive limit + assert!(!commit_title_exceeds_limit("", 72)); + } + #[gpui::test] async fn test_dispatch_context_with_focus_states(cx: &mut TestAppContext) { init_test(cx);