diff --git a/CHANGELOG.md b/CHANGELOG.md index f94682f6fb..baf743c5d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * support 'n'/'p' key to move to the next/prev hunk in diff component [[@hamflx](https://github.com/hamflx)] ([#1523](https://github.com/extrawurst/gitui/issues/1523)) * simplify theme overrides [[@cruessler](https://github.com/cruessler)] ([#1367](https://github.com/extrawurst/gitui/issues/1367)) * support for sign-off of commits [[@domtac](https://github.com/domtac)]([#1757](https://github.com/extrawurst/gitui/issues/1757)) +* switched from textwrap to bwrap for text wrapping [[@TheBlackSheep3](https://github.com/TheBlackSheep3/)] ([#1762](https://github.com/extrawurst/gitui/issues/1762)) ### Fixes * fix commit dialog char count for multibyte characters ([#1726](https://github.com/extrawurst/gitui/issues/1726)) diff --git a/Cargo.lock b/Cargo.lock index 650c1a9290..fcf16b4a64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.3" @@ -173,6 +162,15 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "bwrap" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a552ac54fcbecc5eedf95d6008637809dc01f895c090cd36f461dcd673ce44f" +dependencies = [ + "unicode-width", +] + [[package]] name = "bytemuck" version = "1.13.0" @@ -733,6 +731,7 @@ dependencies = [ "backtrace", "bitflags", "bugreport", + "bwrap", "bytesize", "chrono", "clap", @@ -760,7 +759,6 @@ dependencies = [ "struct-patch", "syntect", "tempfile", - "textwrap", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -772,9 +770,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] [[package]] name = "hermit-abi" @@ -838,7 +833,7 @@ version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fb7c1b80a1dfa604bb4a649a5c5aeef3d913f7c520cb42b40e534e8a61bcdfc" dependencies = [ - "ahash 0.8.3", + "ahash", "indexmap", "is-terminal", "itoa", @@ -1648,12 +1643,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" -[[package]] -name = "smawk" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1787,17 +1776,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.39" @@ -1882,16 +1860,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" -[[package]] -name = "unicode-linebreak" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" -dependencies = [ - "hashbrown", - "regex", -] - [[package]] name = "unicode-normalization" version = "0.1.22" diff --git a/Cargo.toml b/Cargo.toml index 4a7299a7fa..6d897b2989 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ asyncgit = { path = "./asyncgit", version = "0.23", default-features = false } backtrace = "0.3" bitflags = "1.3" bugreport = "0.5" +bwrap = { version = "1.2.1", features = [ "use_std" ] } bytesize = { version = "1.2", default-features = false } chrono = { version = "0.4", default-features = false, features = [ "clock" ] } clap = { version = "4.1", features = [ "env", "cargo" ] } @@ -49,7 +50,6 @@ serde = "1.0" simplelog = { version = "0.12", default-features = false } struct-patch = "0.2" syntect = { version = "5.0", default-features = false, features = ["parsing", "default-syntaxes", "default-themes", "html"] } -textwrap = "0.16" unicode-segmentation = "1.10" unicode-truncate = "0.2" unicode-width = "0.1" diff --git a/src/components/commit_details/details.rs b/src/components/commit_details/details.rs index 8ff2775e81..5d20ef230f 100644 --- a/src/components/commit_details/details.rs +++ b/src/components/commit_details/details.rs @@ -82,15 +82,35 @@ impl DetailsComponent { } } - fn wrap_commit_details( + fn wrap_commit_details<'a>( message: &CommitMessage, width: usize, - ) -> WrappedCommitMessage<'_> { - let wrapped_title = textwrap::wrap(&message.subject, width); + title_buffer: &'a mut [u8], + message_buffer: &'a mut [u8], + ) -> WrappedCommitMessage<'a> { + let _ = bwrap::Wrapper::new( + &message.subject, + width, + title_buffer, + ) + .expect("insufficiant buffer size") + .wrap(); + let wrapped_title = std::str::from_utf8(title_buffer) + .expect("failed to create str from utf8") + .split('\n') + .map(|s| Cow::Borrowed(s.trim_null())) + .collect(); if let Some(ref body) = message.body { + let _ = bwrap::Wrapper::new(body, width, message_buffer) + .expect("insufficiant buffer size") + .wrap(); let wrapped_message: Vec> = - textwrap::wrap(body, width).into_iter().collect(); + std::str::from_utf8(message_buffer) + .expect("failed to create str from utf8") + .split('\n') + .map(|s| Cow::Borrowed(s.trim_null())) + .collect(); (wrapped_title, wrapped_message) } else { @@ -98,13 +118,20 @@ impl DetailsComponent { } } - fn get_wrapped_lines( + fn get_wrapped_lines<'a>( data: &Option, width: usize, - ) -> WrappedCommitMessage<'_> { + title_buffer: &'a mut [u8], + message_buffer: &'a mut [u8], + ) -> WrappedCommitMessage<'a> { if let Some(ref data) = data { if let Some(ref message) = data.message { - return Self::wrap_commit_details(message, width); + return Self::wrap_commit_details( + message, + width, + title_buffer, + message_buffer, + ); } } @@ -115,10 +142,26 @@ impl DetailsComponent { details: &Option, width: usize, ) -> usize { - let (wrapped_title, wrapped_message) = - Self::get_wrapped_lines(details, width); - - wrapped_title.len() + wrapped_message.len() + if let Some(ref data) = details { + if let Some(ref message) = data.message { + let title_len = message.subject.len(); + let mut title_buffer = + vec![0; title_len + title_len / width + 1]; + let message_len = + message.body.as_ref().map_or(0usize, String::len); + let mut message_buffer = + vec![0; message_len + message_len / width + 1]; + let (wrapped_title, wrapped_message) = + Self::wrap_commit_details( + message, + width, + &mut title_buffer, + &mut message_buffer, + ); + return wrapped_title.len() + wrapped_message.len(); + } + } + 0usize } fn get_theme_for_line(&self, bold: bool) -> Style { @@ -129,13 +172,20 @@ impl DetailsComponent { } } - fn get_wrapped_text_message( - &self, + fn get_wrapped_text_message<'a>( + &'a self, width: usize, height: usize, + title_buffer: &'a mut [u8], + message_buffer: &'a mut [u8], ) -> Vec { let (wrapped_title, wrapped_message) = - Self::get_wrapped_lines(&self.data, width); + Self::get_wrapped_lines( + &self.data, + width, + title_buffer, + message_buffer, + ); [&wrapped_title[..], &wrapped_message[..]] .concat() @@ -302,6 +352,27 @@ impl DrawableComponent for DetailsComponent { let can_scroll = usize::from(height) < number_of_lines; + let (title_len, message_len) = + self.data.as_ref().map_or((0usize, 0usize), |data| { + data.message.as_ref().map_or( + (0usize, 0usize), + |message| { + let title_len = message.subject.len(); + let message_len = message + .body + .as_ref() + .map_or(0usize, String::len); + (title_len, message_len) + }, + ) + }); + let mut title_buffer = + vec![0u8; title_len + title_len / usize::from(width) + 1]; + let mut message_buffer = + vec![ + 0u8; + message_len + message_len / usize::from(width) + 1 + ]; f.render_widget( dialog_paragraph( &format!( @@ -318,6 +389,8 @@ impl DrawableComponent for DetailsComponent { Text::from(self.get_wrapped_text_message( width as usize, height as usize, + &mut title_buffer, + &mut message_buffer, )), &self.theme, self.focused, @@ -403,16 +476,34 @@ impl Component for DetailsComponent { } } +trait NullTrim { + fn trim_null(&self) -> &Self; +} + +impl NullTrim for str { + fn trim_null(&self) -> &Self { + let end = self.find('\0').map_or(self.len(), |pos| pos); + &self[..end] + } +} + #[cfg(test)] mod tests { use super::*; - fn get_wrapped_lines( + fn get_wrapped_lines<'a>( message: &CommitMessage, width: usize, - ) -> Vec> { + title_buffer: &'a mut [u8], + message_buffer: &'a mut [u8], + ) -> Vec> { let (wrapped_title, wrapped_message) = - DetailsComponent::wrap_commit_details(message, width); + DetailsComponent::wrap_commit_details( + message, + width, + title_buffer, + message_buffer, + ); [&wrapped_title[..], &wrapped_message[..]].concat() } @@ -420,13 +511,25 @@ mod tests { #[test] fn test_textwrap() { let message = CommitMessage::from("Commit message"); + let mut title_buffer = vec![0; 32]; + let mut message_buffer = vec![0; 32]; assert_eq!( - get_wrapped_lines(&message, 7), + get_wrapped_lines( + &message, + 7, + &mut title_buffer, + &mut message_buffer + ), vec!["Commit", "message"] ); assert_eq!( - get_wrapped_lines(&message, 14), + get_wrapped_lines( + &message, + 14, + &mut title_buffer, + &mut message_buffer + ), vec!["Commit message"] ); @@ -434,11 +537,21 @@ mod tests { CommitMessage::from("Commit message\n"); assert_eq!( - get_wrapped_lines(&message_with_newline, 7), + get_wrapped_lines( + &message_with_newline, + 7, + &mut title_buffer, + &mut message_buffer + ), vec!["Commit", "message"] ); assert_eq!( - get_wrapped_lines(&message_with_newline, 14), + get_wrapped_lines( + &message_with_newline, + 14, + &mut title_buffer, + &mut message_buffer + ), vec!["Commit message"] ); @@ -447,14 +560,24 @@ mod tests { ); assert_eq!( - get_wrapped_lines(&message_with_body, 7), + get_wrapped_lines( + &message_with_body, + 7, + &mut title_buffer, + &mut message_buffer + ), vec![ "Commit", "message", "First", "line", "Second", "line" ] ); assert_eq!( - get_wrapped_lines(&message_with_body, 14), + get_wrapped_lines( + &message_with_body, + 14, + &mut title_buffer, + &mut message_buffer + ), vec!["Commit message", "First line", "Second line"] ); }