ok/jj
1
0
Fork 0
forked from mirrors/jj

cli: add helper to elide text from left

I don't think this implements unicode layout stuff thoroughly, but it can at
least handle Latin and unambiguous East-Asian characters.
This commit is contained in:
Yuya Nishihara 2023-08-31 16:42:42 +09:00
parent b8f71a4b30
commit 3d54df9806
4 changed files with 147 additions and 0 deletions

1
Cargo.lock generated
View file

@ -1055,6 +1055,7 @@ dependencies = [
"tracing", "tracing",
"tracing-chrome", "tracing-chrome",
"tracing-subscriber", "tracing-subscriber",
"unicode-width",
] ]
[[package]] [[package]]

View file

@ -85,6 +85,7 @@ tracing-subscriber = { version = "0.3.17", default-features = false, features =
"fmt", "fmt",
] } ] }
tokio = { version = "1.32.0" } tokio = { version = "1.32.0" }
unicode-width = "0.1.10"
watchman_client = { version = "0.8.0" } watchman_client = { version = "0.8.0" }
whoami = "1.4.1" whoami = "1.4.1"
version_check = "0.9.4" version_check = "0.9.4"

View file

@ -60,6 +60,7 @@ toml_edit = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-chrome = { workspace = true } tracing-chrome = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
unicode-width = { workspace = true }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = { workspace = true } libc = { workspace = true }

View file

@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::borrow::Cow;
use std::{cmp, io}; use std::{cmp, io};
use unicode_width::UnicodeWidthChar as _;
use crate::formatter::{FormatRecorder, Formatter}; use crate::formatter::{FormatRecorder, Formatter};
pub fn complete_newline(s: impl Into<String>) -> String { pub fn complete_newline(s: impl Into<String>) -> String {
@ -32,6 +35,74 @@ pub fn split_email(email: &str) -> (&str, Option<&str>) {
} }
} }
/// Shortens `text` to `max_width` by removing leading characters. `ellipsis` is
/// added if the `text` gets truncated.
///
/// The returned string (including `ellipsis`) never exceeds the `max_width`.
pub fn elide_start<'a>(
text: &'a str,
ellipsis: &'a str,
max_width: usize,
) -> (Cow<'a, str>, usize) {
let (text_start, text_width) = truncate_start_pos(text, max_width);
if text_start == 0 {
return (Cow::Borrowed(text), text_width);
}
let (ellipsis_start, ellipsis_width) = truncate_start_pos(ellipsis, max_width);
if ellipsis_start != 0 {
let ellipsis = trim_start_zero_width_chars(&ellipsis[ellipsis_start..]);
return (Cow::Borrowed(ellipsis), ellipsis_width);
}
let text = &text[text_start..];
let max_text_width = max_width - ellipsis_width;
let (skip, skipped_width) = skip_start_pos(text, text_width.saturating_sub(max_text_width));
let text = trim_start_zero_width_chars(&text[skip..]);
let concat_width = ellipsis_width + (text_width - skipped_width);
assert!(concat_width <= max_width);
(Cow::Owned([ellipsis, text].concat()), concat_width)
}
/// Shortens `text` to `max_width` by removing leading characters, returning
/// `(start_index, width)`.
///
/// The truncated string may have 0-width decomposed characters at start.
fn truncate_start_pos(text: &str, max_width: usize) -> (usize, usize) {
let mut acc_width = 0;
for (i, c) in text.char_indices().rev() {
let new_width = acc_width + c.width().unwrap_or(0);
if new_width > max_width {
let prev_index = i + c.len_utf8();
return (prev_index, acc_width);
}
acc_width = new_width;
}
(0, acc_width)
}
/// Skips `width` leading characters, returning `(start_index, skipped_width)`.
///
/// The `skipped_width` may exceed the given `width` if `width` is not at
/// character boundary.
///
/// The truncated string may have 0-width decomposed characters at start.
fn skip_start_pos(text: &str, width: usize) -> (usize, usize) {
let mut acc_width = 0;
for (i, c) in text.char_indices() {
if acc_width >= width {
return (i, acc_width);
}
acc_width += c.width().unwrap_or(0);
}
(text.len(), acc_width)
}
/// Removes leading 0-width characters.
fn trim_start_zero_width_chars(text: &str) -> &str {
text.trim_start_matches(|c: char| c.width().unwrap_or(0) == 0)
}
/// Indents each line by the given prefix preserving labels. /// Indents each line by the given prefix preserving labels.
pub fn write_indented( pub fn write_indented(
formatter: &mut dyn Formatter, formatter: &mut dyn Formatter,
@ -214,6 +285,79 @@ mod tests {
String::from_utf8(output).unwrap() String::from_utf8(output).unwrap()
} }
#[test]
fn test_elide_start() {
// Empty string
assert_eq!(elide_start("", "", 1), ("".into(), 0));
// Basic truncation
assert_eq!(elide_start("abcdef", "", 6), ("abcdef".into(), 6));
assert_eq!(elide_start("abcdef", "", 5), ("bcdef".into(), 5));
assert_eq!(elide_start("abcdef", "", 1), ("f".into(), 1));
assert_eq!(elide_start("abcdef", "", 0), ("".into(), 0));
assert_eq!(elide_start("abcdef", "-=~", 6), ("abcdef".into(), 6));
assert_eq!(elide_start("abcdef", "-=~", 5), ("-=~ef".into(), 5));
assert_eq!(elide_start("abcdef", "-=~", 4), ("-=~f".into(), 4));
assert_eq!(elide_start("abcdef", "-=~", 3), ("-=~".into(), 3));
assert_eq!(elide_start("abcdef", "-=~", 2), ("=~".into(), 2));
assert_eq!(elide_start("abcdef", "-=~", 1), ("~".into(), 1));
assert_eq!(elide_start("abcdef", "-=~", 0), ("".into(), 0));
// East Asian characters (char.width() == 2)
assert_eq!(elide_start("一二三", "", 6), ("一二三".into(), 6));
assert_eq!(elide_start("一二三", "", 5), ("二三".into(), 4));
assert_eq!(elide_start("一二三", "", 4), ("二三".into(), 4));
assert_eq!(elide_start("一二三", "", 1), ("".into(), 0));
assert_eq!(elide_start("一二三", "-=~", 6), ("一二三".into(), 6));
assert_eq!(elide_start("一二三", "-=~", 5), ("-=~三".into(), 5));
assert_eq!(elide_start("一二三", "-=~", 4), ("-=~".into(), 3));
assert_eq!(elide_start("一二三", "", 6), ("一二三".into(), 6));
assert_eq!(elide_start("一二三", "", 5), ("略三".into(), 4));
assert_eq!(elide_start("一二三", "", 4), ("略三".into(), 4));
assert_eq!(elide_start("一二三", "", 2), ("".into(), 2));
assert_eq!(elide_start("一二三", "", 1), ("".into(), 0));
assert_eq!(elide_start("一二三", ".", 5), (".二三".into(), 5));
assert_eq!(elide_start("一二三", ".", 4), (".三".into(), 3));
assert_eq!(elide_start("一二三", "略.", 5), ("略.三".into(), 5));
assert_eq!(elide_start("一二三", "略.", 4), ("略.".into(), 3));
// Multi-byte character at boundary
assert_eq!(elide_start("àbcdè", "", 5), ("àbcdè".into(), 5));
assert_eq!(elide_start("àbcdè", "", 4), ("bcdè".into(), 4));
assert_eq!(elide_start("àbcdè", "", 1), ("è".into(), 1));
assert_eq!(elide_start("àbcdè", "", 0), ("".into(), 0));
assert_eq!(elide_start("àbcdè", "ÀÇÈ", 4), ("ÀÇÈè".into(), 4));
assert_eq!(elide_start("àbcdè", "ÀÇÈ", 3), ("ÀÇÈ".into(), 3));
assert_eq!(elide_start("àbcdè", "ÀÇÈ", 2), ("ÇÈ".into(), 2));
// Decomposed character at boundary
assert_eq!(
elide_start("a\u{300}bcde\u{300}", "", 5),
("a\u{300}bcde\u{300}".into(), 5)
);
assert_eq!(
elide_start("a\u{300}bcde\u{300}", "", 4),
("bcde\u{300}".into(), 4)
);
assert_eq!(
elide_start("a\u{300}bcde\u{300}", "", 1),
("e\u{300}".into(), 1)
);
assert_eq!(elide_start("a\u{300}bcde\u{300}", "", 0), ("".into(), 0));
assert_eq!(
elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 4),
("A\u{300}CE\u{300}e\u{300}".into(), 4)
);
assert_eq!(
elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 3),
("A\u{300}CE\u{300}".into(), 3)
);
assert_eq!(
elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 2),
("CE\u{300}".into(), 2)
);
}
#[test] #[test]
fn test_split_byte_line_to_words() { fn test_split_byte_line_to_words() {
assert_eq!(split_byte_line_to_words(b""), vec![]); assert_eq!(split_byte_line_to_words(b""), vec![]);