diff --git a/src/annotate/converters/mod.rs b/src/annotate/converters/mod.rs index ff58943..23706d3 100644 --- a/src/annotate/converters/mod.rs +++ b/src/annotate/converters/mod.rs @@ -277,8 +277,15 @@ fn truncate_summary(summary: &str, max_len: usize) -> String { if trimmed.len() <= max_len { trimmed.to_string() } else { - // Find the last space before max_len to avoid cutting words - let truncate_at = trimmed[..max_len].rfind(' ').unwrap_or(max_len); + // max_len is a byte count but may land inside a multi-byte UTF-8 + // character (e.g. box-drawing '─' is 3 bytes) - back off to the + // nearest preceding char boundary before slicing. + let mut boundary = max_len; + while boundary > 0 && !trimmed.is_char_boundary(boundary) { + boundary -= 1; + } + // Find the last space before the boundary to avoid cutting words + let truncate_at = trimmed[..boundary].rfind(' ').unwrap_or(boundary); format!("{}...", &trimmed[..truncate_at]) } } @@ -306,6 +313,17 @@ mod tests { ); } + #[test] + fn test_truncate_summary_multibyte_char_at_boundary() { + // '─' (U+2500) is 3 bytes in UTF-8, so a naive byte-offset slice at + // max_len can land inside the character and panic. This string is + // constructed so byte 100 falls inside a '─' run. + let summary = + "─── Config ─────────────────────────────────────────────────────────────────"; + // Must not panic, and the result must be valid UTF-8. + let _ = truncate_summary(summary, 100); + } + #[test] fn test_parsed_documentation_getters() { let mut doc = ParsedDocumentation::new();