mirror of
https://github.com/clap-rs/clap
synced 2024-11-10 06:44:16 +00:00
refactor: Extract our own display width
This added about 4 KiB to `.text` which makes sense since we duplicated logic.
This commit is contained in:
parent
c6155f62d5
commit
735d6fd1e3
7 changed files with 209 additions and 7 deletions
43
Cargo.lock
generated
43
Cargo.lock
generated
|
@ -137,7 +137,9 @@ dependencies = [
|
|||
"textwrap 0.15.0",
|
||||
"trybuild",
|
||||
"trycmd",
|
||||
"unic-emoji-char",
|
||||
"unicase",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1031,6 +1033,47 @@ dependencies = [
|
|||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
|
||||
dependencies = [
|
||||
"unic-char-range",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-range"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-common"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-emoji-char"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d"
|
||||
dependencies = [
|
||||
"unic-char-property",
|
||||
"unic-char-range",
|
||||
"unic-ucd-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-version"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
|
||||
dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.6.0"
|
||||
|
|
|
@ -73,7 +73,7 @@ derive = ["clap_derive"]
|
|||
cargo = [] # Disable if you're not using Cargo, enables Cargo-env-var-dependent macros
|
||||
wrap_help = ["dep:terminal_size", "textwrap/terminal_size"]
|
||||
env = [] # Use environment variables during arg parsing
|
||||
unicode = ["textwrap/unicode-width", "dep:unicase"] # Support for unicode characters in arguments and help messages
|
||||
unicode = ["textwrap/unicode-width", "dep:unicode-width", "dep:unicase"] # Support for unicode characters in arguments and help messages
|
||||
perf = [] # Optimize for runtime performance
|
||||
|
||||
# In-work features
|
||||
|
@ -95,6 +95,7 @@ atty = { version = "0.2", optional = true }
|
|||
termcolor = { version = "1.1.1", optional = true }
|
||||
terminal_size = { version = "0.2.1", optional = true }
|
||||
backtrace = { version = "0.3", optional = true }
|
||||
unicode-width = { version = "0.1.9", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = "1.0.18"
|
||||
|
@ -105,6 +106,7 @@ humantime = "2"
|
|||
snapbox = "0.2"
|
||||
shlex = "1.1.0"
|
||||
static_assertions = "1.1.0"
|
||||
unic-emoji-char = "0.9.0"
|
||||
|
||||
[[example]]
|
||||
name = "demo"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::output::display_width;
|
||||
|
||||
/// Terminal-styling container
|
||||
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||
pub struct StyledStr {
|
||||
|
@ -37,6 +39,14 @@ impl StyledStr {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn display_width(&self) -> usize {
|
||||
let mut width = 0;
|
||||
for (_, c) in &self.pieces {
|
||||
width += display_width(c);
|
||||
}
|
||||
width
|
||||
}
|
||||
|
||||
/// HACK: Until call sites are updated to handle formatted text, extract the unformatted
|
||||
#[track_caller]
|
||||
pub(crate) fn unwrap_none(&self) -> &str {
|
||||
|
|
|
@ -9,13 +9,11 @@ use crate::builder::PossibleValue;
|
|||
use crate::builder::Str;
|
||||
use crate::builder::StyledStr;
|
||||
use crate::builder::{render_arg_val, Arg, Command};
|
||||
use crate::output::display_width;
|
||||
use crate::output::Usage;
|
||||
use crate::util::FlatSet;
|
||||
use crate::ArgAction;
|
||||
|
||||
// Third party
|
||||
use textwrap::core::display_width;
|
||||
|
||||
/// `clap` Help Writer.
|
||||
///
|
||||
/// Wraps a writer stream providing different methods to generate help for `clap` objects.
|
||||
|
@ -461,7 +459,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
|
|||
.expect("Only called with possible value");
|
||||
let help_longest = possible_vals
|
||||
.iter()
|
||||
.filter_map(|f| f.get_visible_help().map(|h| display_width(h.unwrap_none())))
|
||||
.filter_map(|f| f.get_visible_help().map(|h| h.display_width()))
|
||||
.max()
|
||||
.expect("Only called with possible value with help");
|
||||
// should new line
|
||||
|
@ -532,7 +530,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
|
|||
} else {
|
||||
// force_next_line
|
||||
let h = arg.get_help().unwrap_or_default();
|
||||
let h_w = display_width(h.unwrap_none()) + display_width(spec_vals);
|
||||
let h_w = h.display_width() + display_width(spec_vals);
|
||||
let taken = longest + 12;
|
||||
self.term_w >= taken
|
||||
&& (taken as f32 / self.term_w as f32) > 0.40
|
||||
|
@ -749,7 +747,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
|
|||
} else {
|
||||
// force_next_line
|
||||
let h = cmd.get_about().unwrap_or_default();
|
||||
let h_w = display_width(h.unwrap_none()) + display_width(spec_vals);
|
||||
let h_w = h.display_width() + display_width(spec_vals);
|
||||
let taken = longest + 12;
|
||||
self.term_w >= taken
|
||||
&& (taken as f32 / self.term_w as f32) > 0.40
|
||||
|
@ -1094,6 +1092,7 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unicode")]
|
||||
fn display_width_handles_non_ascii() {
|
||||
// Popular Danish tongue-twister, the name of a fruit dessert.
|
||||
let text = "rødgrød med fløde";
|
||||
|
@ -1104,6 +1103,7 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unicode")]
|
||||
fn display_width_handles_emojis() {
|
||||
let text = "😂";
|
||||
// There is a single `char`...
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
mod help;
|
||||
mod textwrap;
|
||||
mod usage;
|
||||
|
||||
pub(crate) mod fmt;
|
||||
|
||||
pub(crate) use self::help::Help;
|
||||
pub(crate) use self::textwrap::core::display_width;
|
||||
pub(crate) use self::usage::Usage;
|
||||
|
|
144
src/output/textwrap/core.rs
Normal file
144
src/output/textwrap/core.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
/// Compute the display width of `text`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// **Note:** When the `unicode` Cargo feature is disabled, all characters are presumed to take up
|
||||
/// 1 width. With the feature enabled, function will correctly deal with [combining characters] in
|
||||
/// their decomposed form (see [Unicode equivalence]).
|
||||
///
|
||||
/// An example of a decomposed character is “é”, which can be decomposed into: “e” followed by a
|
||||
/// combining acute accent: “◌́”. Without the `unicode` Cargo feature, every `char` has a width of
|
||||
/// 1. This includes the combining accent:
|
||||
///
|
||||
/// ## Emojis and CJK Characters
|
||||
///
|
||||
/// Characters such as emojis and [CJK characters] used in the
|
||||
/// Chinese, Japanese, and Korean langauges are seen as double-width,
|
||||
/// even if the `unicode-width` feature is disabled:
|
||||
///
|
||||
/// # Limitations
|
||||
///
|
||||
/// The displayed width of a string cannot always be computed from the
|
||||
/// string alone. This is because the width depends on the rendering
|
||||
/// engine used. This is particularly visible with [emoji modifier
|
||||
/// sequences] where a base emoji is modified with, e.g., skin tone or
|
||||
/// hair color modifiers. It is up to the rendering engine to detect
|
||||
/// this and to produce a suitable emoji.
|
||||
///
|
||||
/// A simple example is “❤️”, which consists of “❤” (U+2764: Black
|
||||
/// Heart Symbol) followed by U+FE0F (Variation Selector-16). By
|
||||
/// itself, “❤” is a black heart, but if you follow it with the
|
||||
/// variant selector, you may get a wider red heart.
|
||||
///
|
||||
/// A more complex example would be “👨🦰” which should depict a man
|
||||
/// with red hair. Here the computed width is too large — and the
|
||||
/// width differs depending on the use of the `unicode-width` feature:
|
||||
///
|
||||
/// This happens because the grapheme consists of three code points:
|
||||
/// “👨” (U+1F468: Man), Zero Width Joiner (U+200D), and “🦰”
|
||||
/// (U+1F9B0: Red Hair). You can see them above in the test. With
|
||||
/// `unicode-width` enabled, the ZWJ is correctly seen as having zero
|
||||
/// width, without it is counted as a double-width character.
|
||||
///
|
||||
/// ## Terminal Support
|
||||
///
|
||||
/// Modern browsers typically do a great job at combining characters
|
||||
/// as shown above, but terminals often struggle more. As an example,
|
||||
/// Gnome Terminal version 3.38.1, shows “❤️” as a big red heart, but
|
||||
/// shows "👨🦰" as “👨🦰”.
|
||||
///
|
||||
/// [combining characters]: https://en.wikipedia.org/wiki/Combining_character
|
||||
/// [Unicode equivalence]: https://en.wikipedia.org/wiki/Unicode_equivalence
|
||||
/// [CJK characters]: https://en.wikipedia.org/wiki/CJK_characters
|
||||
/// [emoji modifier sequences]: https://unicode.org/emoji/charts/full-emoji-modifiers.html
|
||||
pub(crate) fn display_width(text: &str) -> usize {
|
||||
let mut width = 0;
|
||||
for ch in text.chars() {
|
||||
width += ch_width(ch);
|
||||
}
|
||||
width
|
||||
}
|
||||
|
||||
#[cfg(feature = "unicode")]
|
||||
fn ch_width(ch: char) -> usize {
|
||||
unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unicode"))]
|
||||
fn ch_width(_: char) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "unicode")]
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
#[test]
|
||||
fn emojis_have_correct_width() {
|
||||
use unic_emoji_char::is_emoji;
|
||||
|
||||
// Emojis in the Basic Latin (ASCII) and Latin-1 Supplement
|
||||
// blocks all have a width of 1 column. This includes
|
||||
// characters such as '#' and '©'.
|
||||
for ch in '\u{1}'..'\u{FF}' {
|
||||
if is_emoji(ch) {
|
||||
let desc = format!("{:?} U+{:04X}", ch, ch as u32);
|
||||
|
||||
#[cfg(feature = "unicode")]
|
||||
assert_eq!(ch.width().unwrap(), 1, "char: {}", desc);
|
||||
|
||||
#[cfg(not(feature = "unicode"))]
|
||||
assert_eq!(ch_width(ch), 1, "char: {}", desc);
|
||||
}
|
||||
}
|
||||
|
||||
// Emojis in the remaining blocks of the Basic Multilingual
|
||||
// Plane (BMP), in the Supplementary Multilingual Plane (SMP),
|
||||
// and in the Supplementary Ideographic Plane (SIP), are all 1
|
||||
// or 2 columns wide when unicode-width is used, and always 2
|
||||
// columns wide otherwise. This includes all of our favorite
|
||||
// emojis such as 😊.
|
||||
for ch in '\u{FF}'..'\u{2FFFF}' {
|
||||
if is_emoji(ch) {
|
||||
let desc = format!("{:?} U+{:04X}", ch, ch as u32);
|
||||
|
||||
#[cfg(feature = "unicode")]
|
||||
assert!(ch.width().unwrap() <= 2, "char: {}", desc);
|
||||
|
||||
#[cfg(not(feature = "unicode"))]
|
||||
assert_eq!(ch_width(ch), 1, "char: {}", desc);
|
||||
}
|
||||
}
|
||||
|
||||
// The remaining planes contain almost no assigned code points
|
||||
// and thus also no emojis.
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unicode")]
|
||||
fn display_width_works() {
|
||||
assert_eq!("Café Plain".len(), 11); // “é” is two bytes
|
||||
assert_eq!(display_width("Café Plain"), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unicode")]
|
||||
fn display_width_narrow_emojis() {
|
||||
assert_eq!(display_width("⁉"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unicode")]
|
||||
fn display_width_narrow_emojis_variant_selector() {
|
||||
assert_eq!(display_width("⁉\u{fe0f}"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unicode")]
|
||||
fn display_width_emojis() {
|
||||
assert_eq!(display_width("😂😭🥺🤣✨😍🙏🥰😊🔥"), 20);
|
||||
}
|
||||
}
|
1
src/output/textwrap/mod.rs
Normal file
1
src/output/textwrap/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub(crate) mod core;
|
Loading…
Reference in a new issue