From bd1f1945477696a59b712118bef83dd170d589be Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Mon, 9 Mar 2026 22:57:35 +0100 Subject: [PATCH] fix(message): use unicode char count for text width --- src/commit/types/commit_type.rs | 9 +++++++++ src/commit/types/description.rs | 2 +- src/commit/types/message.rs | 11 ++++------- src/commit/types/scope.rs | 17 ++++++++++++++++- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/commit/types/commit_type.rs b/src/commit/types/commit_type.rs index abb05fa..ee841b3 100644 --- a/src/commit/types/commit_type.rs +++ b/src/commit/types/commit_type.rs @@ -61,6 +61,15 @@ impl CommitType { Self::Revert => "revert", } } + + /// Returns the length in characters + /// + /// `is_empty()` is intentionally absent: `CommitType` is + /// guaranteed non-empty, so the concept does not apply. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.as_str().chars().count() + } } impl std::fmt::Display for CommitType { diff --git a/src/commit/types/description.rs b/src/commit/types/description.rs index b2ee206..5617bf4 100644 --- a/src/commit/types/description.rs +++ b/src/commit/types/description.rs @@ -39,7 +39,7 @@ impl Description { /// non-empty by its constructor, so the concept does not apply. #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { - self.0.len() + self.0.chars().count() } } diff --git a/src/commit/types/message.rs b/src/commit/types/message.rs index 4e4d72b..eb13114 100644 --- a/src/commit/types/message.rs +++ b/src/commit/types/message.rs @@ -70,13 +70,10 @@ impl ConventionalCommit { /// - Without scope: `len(type) + 2 + len(description)` /// (the 2 accounts for colon and space: ": ") pub fn first_line_len(&self) -> usize { - if self.scope.is_empty() { - // type: description - self.commit_type.as_str().len() + 2 + self.description.len() - } else { - // type(scope): description - self.commit_type.as_str().len() + self.scope.as_str().len() + 4 + self.description.len() - } + self.commit_type.len() + + self.scope.header_segment_len() + + 2 // ": " + + self.description.len() } /// Format the complete commit messsage diff --git a/src/commit/types/scope.rs b/src/commit/types/scope.rs index 338383a..99d9453 100644 --- a/src/commit/types/scope.rs +++ b/src/commit/types/scope.rs @@ -25,8 +25,13 @@ impl Scope { }); } match lazy_regex::regex_find!(r"[^-a-zA-Z0-9_/]", &value) { - Some(val) => Err(ScopeError::InvalidCharacter(val.chars().next().unwrap())), None => Ok(Self(value)), + Some(val) => val + .chars() + .next() + .map(ScopeError::InvalidCharacter) + .map(Err) + .unwrap_or_else(|| unreachable!("regex match is always non-empty")), } } @@ -44,6 +49,16 @@ impl Scope { pub fn as_str(&self) -> &str { self.0.as_str() } + + /// Returns itself as a formatted header segment + pub fn header_segment(&self) -> String { + format!("({self})") + } + + /// Returns the visible length of the header segment + pub fn header_segment_len(&self) -> usize { + self.header_segment().chars().count() + } } impl std::fmt::Display for Scope {