diff --git a/src/lib.rs b/src/lib.rs index 6cbd637..e40dca0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,4 @@ mod short_name; -pub use short_name::ShortName; +pub use short_name::{DisplayShortName, ShortName}; diff --git a/src/short_name.rs b/src/short_name.rs index c0ecbc1..a7808f2 100644 --- a/src/short_name.rs +++ b/src/short_name.rs @@ -1,3 +1,7 @@ +use std::{borrow::Cow, fmt, str}; + +const SPECIAL_TYPE_CHARS: [u8; 9] = *b" <>()[],;"; + /// Lazily shortens a type name to remove all module paths. /// /// The short name of a type is its full name as returned by @@ -46,87 +50,71 @@ impl<'a> From<&'a str> for ShortName<'a> { } } -impl<'a> core::fmt::Debug for ShortName<'a> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let &ShortName(full_name) = self; - // Generics result in nested paths within <..> blocks. - // Consider "bevy_render::camera::camera::extract_cameras". - // To tackle this, we parse the string from left to right, collapsing as we go. - let mut index: usize = 0; - let end_of_string = full_name.len(); - - while index < end_of_string { - let rest_of_string = full_name.get(index..end_of_string).unwrap_or_default(); +impl<'a> fmt::Debug for ShortName<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut remaining = f.as_bytes(); + let mut parsed_name = Vec::new(); + let mut complex_type = false; + loop { // Collapse everything up to the next special character, // then skip over it - if let Some(special_character_index) = rest_of_string.find(|c: char| { - (c == ' ') - || (c == '<') - || (c == '>') - || (c == '(') - || (c == ')') - || (c == '[') - || (c == ']') - || (c == ',') - || (c == ';') - }) { - let segment_to_collapse = rest_of_string - .get(0..special_character_index) - .unwrap_or_default(); - - f.write_str(collapse_type_name(segment_to_collapse))?; - - // Insert the special character - let special_character = - &rest_of_string[special_character_index..=special_character_index]; - - f.write_str(special_character)?; - - match special_character { - ">" | ")" | "]" - if rest_of_string[special_character_index + 1..].starts_with("::") => - { - f.write_str("::")?; + let is_special = |c| SPECIAL_TYPE_CHARS.contains(c); + if let Some(next_special_index) = remaining.iter().position(is_special) { + complex_type = true; + if parsed_name.is_empty() { + parsed_name.reserve(remaining.len()); + } + let (pre_special, post_special) = remaining.split_at(next_special_index + 1); + parsed_name.extend_from_slice(collapse_type_name(pre_special)); + match pre_special.last().unwrap() { + b'>' | b')' | b']' if post_special.get(..2) == Some(b"::") => { + parsed_name.extend_from_slice(b"::"); // Move the index past the "::" - index += special_character_index + 3; + remaining = &post_special[2..]; } // Move the index just past the special character - _ => index += special_character_index + 1, + _ => remaining = post_special, } + } else if !complex_type { + let collapsed = collapse_type_name(remaining); + // SAFETY: We only split on ASCII characters, and the input is valid UTF8, since + // it was a &str + let str = unsafe { str::from_utf8_unchecked(collapsed) }; + return Cow::Borrowed(str); } else { // If there are no special characters left, we're done! - f.write_str(collapse_type_name(rest_of_string))?; - index = end_of_string; + parsed_name.extend_from_slice(collapse_type_name(remaining)); + // SAFETY: see above + let utf8_name = unsafe { String::from_utf8_unchecked(parsed_name) }; + return Cow::Owned(utf8_name); } } + } +} - Ok(()) +impl<'a> fmt::Display for ShortName<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(self, f) } } -impl<'a> core::fmt::Display for ShortName<'a> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - ::fmt(self, f) +/// Wrapper around `AsRef` that uses the [`get_short_name`] format when +/// displayed. +pub struct DisplayShortName>(pub T); + +impl> fmt::Display for DisplayShortName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let as_short_name = ShortName(self.0.as_ref()); + write!(f, "{as_short_name}") } } #[inline(always)] -fn collapse_type_name(string: &str) -> &str { - // Enums types are retained. - // As heuristic, we assume the enum type to be uppercase. - let mut segments = string.rsplit("::"); - let (last, second_last): (&str, Option<&str>) = (segments.next().unwrap(), segments.next()); - let Some(second_last) = second_last else { - return last; - }; - - if second_last.starts_with(char::is_uppercase) { - let index = string.len() - last.len() - second_last.len() - 2; - &string[index..] - } else { - last - } +fn collapse_type_name(string: &[u8]) -> &[u8] { + let find = |(index, window)| (window == b"::").then_some(index + 2); + let split_index = string.windows(2).enumerate().rev().find_map(find); + &string[split_index.unwrap_or(0)..] } #[cfg(all(test, feature = "alloc"))] @@ -187,6 +175,14 @@ mod name_formatting_tests { ); } + #[test] + fn utf8_generics() { + assert_eq!( + fmt("bévï::camérą::łørđ::_öñîòñ<ķràźÿ::Москва::東京>"), + "_öñîòñ<東京>".to_string() + ); + } + #[test] fn nested_generics() { assert_eq!(