diff --git a/Cargo.lock b/Cargo.lock index bbd73ac..3ce532b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "2.6.0" @@ -384,6 +399,28 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "generic-array" version = "0.14.7" @@ -394,6 +431,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "half" version = "2.4.1" @@ -500,12 +548,24 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.12" @@ -571,6 +631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -648,6 +709,12 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.86" @@ -657,6 +724,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.36" @@ -666,6 +759,45 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "ratatui" version = "0.26.3" @@ -744,12 +876,37 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.18" @@ -790,6 +947,7 @@ dependencies = [ "crossterm", "insta", "num-traits", + "proptest", "ratatui", "serde", "serde_json", @@ -943,6 +1101,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "1.0.61" @@ -1010,6 +1180,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1050,6 +1226,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/scm-record/Cargo.toml b/scm-record/Cargo.toml index 1cce66d..8d30459 100644 --- a/scm-record/Cargo.toml +++ b/scm-record/Cargo.toml @@ -36,6 +36,7 @@ serde_json = { version = "1.0", optional = true } assert_matches = "1.5" criterion = "0.5" insta = "1.39" +proptest = "1.5.0" serde_json = "1.0" [[bench]] diff --git a/scm-record/proptest-regressions/ui.txt b/scm-record/proptest-regressions/ui.txt new file mode 100644 index 0000000..6e0a37b --- /dev/null +++ b/scm-record/proptest-regressions/ui.txt @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 4ad364b7a4bfbb4cf16c5dac6dcfbb3e014356930cd855d76f110aed3856a9ed # shrinks to line = "¡" +cc b7d37d182f61b73b6ef4fcd469caff5ccd21379903ce66bd02e14e1abc9344dd # shrinks to line = "\t" +cc de75c8131f6e198916f45226f06c6a09f6cf805a464bbf6954b1a3b03b0c7940 # shrinks to line = "\0" +cc e1eedb87f79680aebd62a9f3f94e31c009e12ab13b855695eb0ce793f43d29aa diff --git a/scm-record/src/ui.rs b/scm-record/src/ui.rs index fc24005..039dc78 100644 --- a/scm-record/src/ui.rs +++ b/scm-record/src/ui.rs @@ -3092,6 +3092,40 @@ enum SectionLineViewInner<'a> { }, } +fn replace_control_character(character: char) -> Option<&'static str> { + match character { + // Characters end up writing over each-other and end up + // displaying incorrectly if ignored. Replacing tabs + // with a known length string fixes the issue for now. + '\t' => Some("→ "), + '\n' => Some("⏎"), + _ => None, + } +} + +/// Split the line into a sequence of [`Span`]s where control characters are +/// replaced with styled [`Span`]'s and push them to the [`spans`] argument. +fn push_spans_from_line<'line>(line: &'line str, spans: &mut Vec>) { + const CONTROL_CHARACTER_STYLE: Style = Style::new().fg(Color::DarkGray); + + let mut last_index = 0; + // Find index of the start of each character to replace + for (idx, char) in line.match_indices(|char| replace_control_character(char).is_some()) { + // Push the string leading up to the character and the styled replacement string + if let Some(replacement_string) = char.chars().next().and_then(replace_control_character) { + spans.push(Span::raw(&line[last_index..idx])); + spans.push(Span::styled(replacement_string, CONTROL_CHARACTER_STYLE)); + // Move the "cursor" to just after the character we're replacing + last_index = idx + char.len(); + } + } + // Append anything remaining after the last replacement + let remaining_line = &line[last_index..]; + if !remaining_line.is_empty() { + spans.push(Span::raw(remaining_line)); + } +} + #[derive(Clone, Debug)] struct SectionLineView<'a> { line_key: LineKey, @@ -3106,43 +3140,6 @@ impl Component for SectionLineView<'_> { } fn draw(&self, viewport: &mut Viewport, x: isize, y: isize) { - fn replace_control_character(character: char) -> Option<&'static str> { - match character { - // Characters end up writing over each-other and end up - // displaying incorrectly if ignored. Replacing tabs - // with a known length string fixes the issue for now. - '\t' => Some("→ "), - '\n' => Some("⏎"), - _ => None, - } - } - - /// Split the line into a sequence of [`Span`]s where control characters are - /// replaced with styled [`Span`]'s and push them to the [`spans`] argument. - fn push_spans_from_line<'line>(line: &'line str, spans: &mut Vec>) { - const CONTROL_CHARACTER_STYLE: Style = Style::new().fg(Color::DarkGray); - - let mut last_index = 0; - // Find index of the start of each character to replace - for (idx, char) in line.match_indices(|char| replace_control_character(char).is_some()) - { - // Push the string leading up to the character and the styled replacement string - if let Some(replacement_string) = - char.chars().next().and_then(replace_control_character) - { - spans.push(Span::raw(&line[last_index..idx])); - spans.push(Span::styled(replacement_string, CONTROL_CHARACTER_STYLE)); - // Move the "cursor" to just after the character we're replacing - last_index = idx + char.len(); - } - } - // Append anything remaining after the last replacement - let remaining_line = &line[last_index..]; - if !remaining_line.is_empty() { - spans.push(Span::raw(remaining_line)); - } - } - viewport.draw_blank(Rect { x: viewport.mask_rect().x, y, @@ -3413,4 +3410,22 @@ mod tests { let recorder = Recorder::new(state.clone(), &mut input); assert_eq!(recorder.run().unwrap(), state); } + + fn test_push_lines_from_span_impl(line: &str) { + let mut spans = Vec::new(); + push_spans_from_line(line, &mut spans); + let summed_span_width: usize = spans.into_iter().map(|span| span.width()).sum(); + let summed_char_width: usize = line.replace('\t', " ").width(); + assert_eq!( + summed_span_width, summed_char_width, + "width mismatch for line={line:?}" + ); + } + + proptest::proptest! { + #[test] + fn test_push_lines_from_span(line in ".*") { + test_push_lines_from_span_impl(line.as_str()); + } + } }