From 6a30dfc1372fd05b6038c1fe659275854857551b Mon Sep 17 00:00:00 2001 From: Lex Moskovski Date: Sat, 8 Jun 2024 15:12:22 -0700 Subject: [PATCH 1/8] word wrap in preview --- Cargo.lock | 24 +++++++++++++++ yazi-config/preset/yazi.toml | 1 + yazi-config/src/preview/preview.rs | 3 ++ yazi-plugin/Cargo.toml | 1 + yazi-plugin/src/external/highlighter.rs | 39 ++++++++++++++++++------- yazi-plugin/src/utils/preview.rs | 3 +- 6 files changed, 60 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1afd63ed..4e14bd7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1830,6 +1830,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.7" @@ -1941,6 +1947,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.59" @@ -2242,6 +2259,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.23" @@ -2864,6 +2887,7 @@ dependencies = [ "shell-escape", "shell-words", "syntect", + "textwrap", "tokio", "tokio-stream", "tokio-util", diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index 47b37cf2..30e166a8 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -17,6 +17,7 @@ mouse_events = [ "click", "scroll" ] [preview] tab_size = 2 +word_wrap = false max_width = 600 max_height = 900 cache_dir = "" diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index d58b0440..d7b2a9be 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -9,6 +9,7 @@ use crate::{validation::check_validation, Xdg, MERGED_YAZI}; #[derive(Debug, Serialize)] pub struct Preview { pub tab_size: u8, + pub word_wrap: bool, pub max_width: u32, pub max_height: u32, @@ -31,6 +32,7 @@ impl Default for Preview { #[derive(Deserialize, Validate)] struct Shadow { tab_size: u8, + word_wrap: bool, max_width: u32, max_height: u32, @@ -55,6 +57,7 @@ impl Default for Preview { Preview { tab_size: preview.tab_size, + word_wrap: preview.word_wrap, max_width: preview.max_width, max_height: preview.max_height, diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 467999aa..72f13dcb 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -39,6 +39,7 @@ tokio = { version = "1.38.0", features = [ "full" ] } tokio-stream = "0.1.15" tokio-util = "0.7.11" unicode-width = "0.1.13" +textwrap = "0.14.2" yazi-prebuild = "0.1.2" # Logging diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 0e681b96..b6f7b671 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -6,6 +6,8 @@ use syntect::{dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet} use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}}; use yazi_config::{PREVIEW, THEME}; use yazi_shared::PeekError; +use ratatui::prelude::Rect; +use textwrap::wrap as textwrap_wrap; static INCR: AtomicUsize = AtomicUsize::new(0); static SYNTECT_SYNTAX: OnceLock = OnceLock::new(); @@ -56,20 +58,20 @@ impl Highlighter { syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found")) } - pub async fn highlight(&self, skip: usize, limit: usize) -> Result, PeekError> { + pub async fn highlight(&self, skip: usize, area: Rect, wrap: bool) -> Result, PeekError> { let mut reader = BufReader::new(File::open(&self.path).await?); let syntax = Self::find_syntax(&self.path).await; let mut plain = syntax.is_err(); let mut before = Vec::with_capacity(if plain { 0 } else { skip }); - let mut after = Vec::with_capacity(limit); + let mut after = Vec::with_capacity(area.height as usize); let mut i = 0; let mut buf = vec![]; while reader.read_until(b'\n', &mut buf).await.is_ok() { i += 1; - if buf.is_empty() || i > skip + limit { + if buf.is_empty() || i > skip + area.height as usize { break; } @@ -84,23 +86,40 @@ impl Highlighter { buf.push(b'\n'); } + let line = String::from_utf8_lossy(&buf).into_owned(); + if i > skip { - after.push(String::from_utf8_lossy(&buf).into_owned()); + after.push(line); } else if !plain { - before.push(String::from_utf8_lossy(&buf).into_owned()); + before.push(line); } buf.clear(); } - if skip > 0 && i < skip + limit { - return Err(PeekError::Exceed(i.saturating_sub(limit))); + if skip > 0 && i < skip + area.height as usize { + return Err(PeekError::Exceed(i.saturating_sub(area.height as usize))); } + let indent = " ".repeat(PREVIEW.tab_size as usize); + let after_text = after.join("").replace('\t', &indent); + if plain { - let indent = " ".repeat(PREVIEW.tab_size as usize); - Ok(Text::from(after.join("").replace('\t', &indent))) + let wrapped_text = if wrap { + textwrap_wrap(&after_text, area.width as usize).join("\n") + } else { + after_text + }; + Ok(Text::from(wrapped_text)) } else { - Self::highlight_with(before, after, syntax.unwrap()).await + let wrapped_after: Vec = if wrap { + textwrap_wrap(&after_text, area.width as usize) + .into_iter() + .map(|cow| cow.trim_end_matches('\n').to_string()) + .collect() + } else { + after + }; + Self::highlight_with(before, wrapped_after, syntax.unwrap()).await } } diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index 73a6aed9..6cacdf61 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -2,6 +2,7 @@ use mlua::{AnyUserData, IntoLuaMulti, Lua, Table, Value}; use yazi_shared::{emit, event::Cmd, Layer, PeekError}; use super::Utils; +use yazi_config::PREVIEW; use crate::{bindings::{FileRef, Window}, cast_to_renderable, elements::{Paragraph, RectRef, Renderable}, external::{self, Highlighter}}; pub struct PreviewLock { @@ -37,7 +38,7 @@ impl Utils { let mut lock = PreviewLock::try_from(t)?; let text = - match Highlighter::new(&lock.url).highlight(lock.skip, area.height as usize).await { + match Highlighter::new(&lock.url).highlight(lock.skip, *area, PREVIEW.word_wrap).await { Ok(text) => text, Err(PeekError::Exceed(max)) => return (false, max).into_lua_multi(lua), Err(_) => return (false, Value::Nil).into_lua_multi(lua), From 5a6675460439ffef984769b807eb19063e2ff0f6 Mon Sep 17 00:00:00 2001 From: Lex Moskovski Date: Sat, 8 Jun 2024 15:17:05 -0700 Subject: [PATCH 2/8] indentation fix --- yazi-config/preset/yazi.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index 30e166a8..c175d1f9 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -17,7 +17,7 @@ mouse_events = [ "click", "scroll" ] [preview] tab_size = 2 -word_wrap = false +word_wrap = false max_width = 600 max_height = 900 cache_dir = "" From 0cff3ce15c4d636279e31ec910f0c4af707c57e1 Mon Sep 17 00:00:00 2001 From: Artyom Artamonov Date: Sun, 16 Jun 2024 04:52:33 +0500 Subject: [PATCH 3/8] word wrap feature --- Cargo.lock | 24 ---- yazi-config/src/preview/preview.rs | 2 +- yazi-plugin/Cargo.toml | 1 - yazi-plugin/src/external/highlighter.rs | 167 +++++++++++++++++++----- yazi-plugin/src/utils/preview.rs | 2 +- 5 files changed, 137 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5f40088..c3450634 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1830,12 +1830,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - [[package]] name = "socket2" version = "0.5.7" @@ -1947,17 +1941,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "textwrap" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.59" @@ -2259,12 +2242,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - [[package]] name = "unicode-normalization" version = "0.1.23" @@ -2887,7 +2864,6 @@ dependencies = [ "shell-escape", "shell-words", "syntect", - "textwrap", "tokio", "tokio-stream", "tokio-util", diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index d7b2a9be..4f701f54 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -9,7 +9,7 @@ use crate::{validation::check_validation, Xdg, MERGED_YAZI}; #[derive(Debug, Serialize)] pub struct Preview { pub tab_size: u8, - pub word_wrap: bool, + pub word_wrap: bool, pub max_width: u32, pub max_height: u32, diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 72f13dcb..467999aa 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -39,7 +39,6 @@ tokio = { version = "1.38.0", features = [ "full" ] } tokio-stream = "0.1.15" tokio-util = "0.7.11" unicode-width = "0.1.13" -textwrap = "0.14.2" yazi-prebuild = "0.1.2" # Logging diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index b6f7b671..c738fe17 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -1,13 +1,12 @@ use std::{io::Cursor, mem, path::{Path, PathBuf}, sync::{atomic::{AtomicUsize, Ordering}, OnceLock}}; use anyhow::{anyhow, Result}; -use ratatui::text::{Line, Span, Text}; +use ratatui::{prelude::Rect, text::{Line, Span, Text}}; use syntect::{dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}}; use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}}; +use unicode_width::UnicodeWidthStr; use yazi_config::{PREVIEW, THEME}; use yazi_shared::PeekError; -use ratatui::prelude::Rect; -use textwrap::wrap as textwrap_wrap; static INCR: AtomicUsize = AtomicUsize::new(0); static SYNTECT_SYNTAX: OnceLock = OnceLock::new(); @@ -58,13 +57,99 @@ impl Highlighter { syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found")) } - pub async fn highlight(&self, skip: usize, area: Rect, wrap: bool) -> Result, PeekError> { - let mut reader = BufReader::new(File::open(&self.path).await?); + pub async fn highlight( + &self, + skip: usize, + area: Rect, + wrap: bool, + ) -> Result, PeekError> { + let reader = BufReader::new(File::open(&self.path).await?); let syntax = Self::find_syntax(&self.path).await; let mut plain = syntax.is_err(); - let mut before = Vec::with_capacity(if plain { 0 } else { skip }); + let (before, after): (Vec, Vec); + if wrap { + (before, after) = Self::before_after_wrapped(skip, area, &mut plain, reader).await?; + } else { + (before, after) = Self::before_after(skip, area, &mut plain, reader).await?; + } + + if plain { + Ok(Text::from(after.join(""))) + } else { + Self::highlight_with(before, after, syntax.unwrap()).await + } + } + + async fn before_after_wrapped( + skip: usize, + area: Rect, + plain: &mut bool, + mut reader: BufReader, + ) -> Result<(Vec, Vec), PeekError> { + let mut before = Vec::with_capacity(if *plain { 0 } else { skip }); + let mut after = Vec::with_capacity(area.height as usize); + + let mut long_lines = vec![]; + let mut buf = vec![]; + // If we want to indent plain text, we have to decide if it is plain + while reader.read_until(b'\n', &mut buf).await.is_ok() { + if buf.is_empty() { + break; + } + if !*plain && buf.len() > 6000 { + *plain = true; + } + long_lines.push(buf.clone()); + buf.clear() + } + + let mut i = 0; + for mut long_line in long_lines { + if long_line.is_empty() || i > skip + area.height as usize { + break; + } + Self::replace_tabs_with_spaces(&mut long_line, PREVIEW.tab_size as usize); + for line in Self::chunk_by_width(long_line, area.width as usize) { + let mut line = line.to_vec(); + i += 1; + if line.is_empty() || i > skip + area.height as usize { + break; + } + + if line.ends_with(b"\r\n") { + line.pop(); + line.pop(); + line.push(b'\n'); + } else if !line.ends_with(b"\n") { + line.push(b'\n') + } + + let text = String::from_utf8_lossy(&line).into_owned(); + if i > skip { + after.push(text); + } else if !*plain { + before.push(text); + } + } + } + + let no_more_scroll = i < skip + area.height as usize; + if skip > 0 && no_more_scroll { + return Err(PeekError::Exceed(i.saturating_sub(area.height as usize))); + } + + Ok((before, after)) + } + + async fn before_after( + skip: usize, + area: Rect, + plain: &mut bool, + mut reader: BufReader, + ) -> Result<(Vec, Vec), PeekError> { + let mut before = Vec::with_capacity(if *plain { 0 } else { skip }); let mut after = Vec::with_capacity(area.height as usize); let mut i = 0; @@ -75,8 +160,8 @@ impl Highlighter { break; } - if !plain && buf.len() > 6000 { - plain = true; + if !*plain && buf.len() > 6000 { + *plain = true; drop(mem::take(&mut before)); } @@ -86,40 +171,58 @@ impl Highlighter { buf.push(b'\n'); } - let line = String::from_utf8_lossy(&buf).into_owned(); - if i > skip { - after.push(line); - } else if !plain { - before.push(line); + after.push(String::from_utf8_lossy(&buf).into_owned()); + } else if !*plain { + before.push(String::from_utf8_lossy(&buf).into_owned()); } buf.clear(); } - if skip > 0 && i < skip + area.height as usize { + let no_more_scroll = i < skip + area.height as usize; + if skip > 0 && no_more_scroll { return Err(PeekError::Exceed(i.saturating_sub(area.height as usize))); } - let indent = " ".repeat(PREVIEW.tab_size as usize); - let after_text = after.join("").replace('\t', &indent); + Ok((before, after)) + } - if plain { - let wrapped_text = if wrap { - textwrap_wrap(&after_text, area.width as usize).join("\n") + fn chunk_by_width(line: Vec, width: usize) -> Vec> { + let mut res = vec![]; + let mut buf = vec![]; + let mut char_buf = vec![]; + for b in &line { + buf.push(*b); + char_buf.push(*b); + if String::from_utf8(char_buf.clone()).is_ok() { + let buf_width = String::from_utf8_lossy(&buf).width(); + if buf_width == width { + res.push(buf.clone()); + buf.clear(); + } else if buf_width > width { + buf = buf[..buf.len() - char_buf.len()].to_vec(); + res.push(buf.clone()); + buf.clear(); + buf.extend_from_slice(&char_buf); + } + char_buf.clear(); + } + } + if !buf.is_empty() { + res.push(buf); + } + res + } + + fn replace_tabs_with_spaces(buf: &mut Vec, tab_size: usize) { + let mut i = 0; + while i < buf.len() { + if buf[i] == b'\t' { + buf.splice(i..i + 1, vec![b' '; tab_size]); + i += tab_size; } else { - after_text - }; - Ok(Text::from(wrapped_text)) - } else { - let wrapped_after: Vec = if wrap { - textwrap_wrap(&after_text, area.width as usize) - .into_iter() - .map(|cow| cow.trim_end_matches('\n').to_string()) - .collect() - } else { - after - }; - Self::highlight_with(before, wrapped_after, syntax.unwrap()).await + i += 1; + } } } diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index 6cacdf61..e59720cd 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -1,8 +1,8 @@ use mlua::{AnyUserData, IntoLuaMulti, Lua, Table, Value}; +use yazi_config::PREVIEW; use yazi_shared::{emit, event::Cmd, Layer, PeekError}; use super::Utils; -use yazi_config::PREVIEW; use crate::{bindings::{FileRef, Window}, cast_to_renderable, elements::{Paragraph, RectRef, Renderable}, external::{self, Highlighter}}; pub struct PreviewLock { From cd734ee69f6ae59003fd83778cd62cd777f36fb4 Mon Sep 17 00:00:00 2001 From: Artyom Artamonov Date: Sun, 16 Jun 2024 17:04:24 +0500 Subject: [PATCH 4/8] word wrapping feature refactor --- yazi-plugin/src/external/highlighter.rs | 104 +++++++++++++++--------- 1 file changed, 64 insertions(+), 40 deletions(-) diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index c738fe17..14846bb6 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -76,6 +76,10 @@ impl Highlighter { } if plain { + if !wrap { + let indent = " ".repeat(PREVIEW.tab_size as usize); + return Ok(Text::from(after.join("").replace('\t', &indent))); + } Ok(Text::from(after.join(""))) } else { Self::highlight_with(before, after, syntax.unwrap()).await @@ -105,39 +109,33 @@ impl Highlighter { buf.clear() } - let mut i = 0; + let mut lines_handled = 0; for mut long_line in long_lines { - if long_line.is_empty() || i > skip + area.height as usize { + if long_line.is_empty() || lines_handled > skip + area.height as usize { break; } Self::replace_tabs_with_spaces(&mut long_line, PREVIEW.tab_size as usize); for line in Self::chunk_by_width(long_line, area.width as usize) { let mut line = line.to_vec(); - i += 1; - if line.is_empty() || i > skip + area.height as usize { + lines_handled += 1; + let must_break = Self::handle_single_line( + lines_handled, + skip, + *plain, + area.height as usize, + &mut line, + &mut before, + &mut after, + ); + if must_break { break; } - - if line.ends_with(b"\r\n") { - line.pop(); - line.pop(); - line.push(b'\n'); - } else if !line.ends_with(b"\n") { - line.push(b'\n') - } - - let text = String::from_utf8_lossy(&line).into_owned(); - if i > skip { - after.push(text); - } else if !*plain { - before.push(text); - } } } - let no_more_scroll = i < skip + area.height as usize; + let no_more_scroll = lines_handled < skip + area.height as usize; if skip > 0 && no_more_scroll { - return Err(PeekError::Exceed(i.saturating_sub(area.height as usize))); + return Err(PeekError::Exceed(lines_handled.saturating_sub(area.height as usize))); } Ok((before, after)) @@ -152,41 +150,67 @@ impl Highlighter { let mut before = Vec::with_capacity(if *plain { 0 } else { skip }); let mut after = Vec::with_capacity(area.height as usize); - let mut i = 0; + let mut lines_handled = 0; let mut buf = vec![]; while reader.read_until(b'\n', &mut buf).await.is_ok() { - i += 1; - if buf.is_empty() || i > skip + area.height as usize { - break; - } - + lines_handled += 1; if !*plain && buf.len() > 6000 { *plain = true; drop(mem::take(&mut before)); } - - if buf.ends_with(b"\r\n") { - buf.pop(); - buf.pop(); - buf.push(b'\n'); - } - - if i > skip { - after.push(String::from_utf8_lossy(&buf).into_owned()); - } else if !*plain { - before.push(String::from_utf8_lossy(&buf).into_owned()); + let must_break = Self::handle_single_line( + lines_handled, + skip, + *plain, + area.height as usize, + &mut buf, + &mut before, + &mut after, + ); + if must_break { + break; } buf.clear(); } - let no_more_scroll = i < skip + area.height as usize; + let no_more_scroll = lines_handled < skip + area.height as usize; if skip > 0 && no_more_scroll { - return Err(PeekError::Exceed(i.saturating_sub(area.height as usize))); + return Err(PeekError::Exceed(lines_handled.saturating_sub(area.height as usize))); } Ok((before, after)) } + fn handle_single_line( + lines_handled: usize, + skip: usize, + plain: bool, + limit: usize, + line: &mut Vec, + before: &mut Vec, + after: &mut Vec, + ) -> bool { + if line.is_empty() || lines_handled > skip + limit { + return true; + } + + if line.ends_with(b"\r\n") { + line.pop(); + line.pop(); + line.push(b'\n'); + } else if !line.ends_with(b"\n") { + line.push(b'\n') + } + + let text = String::from_utf8_lossy(&line).into_owned(); + if lines_handled > skip { + after.push(text); + } else if !plain { + before.push(text); + } + false + } + fn chunk_by_width(line: Vec, width: usize) -> Vec> { let mut res = vec![]; let mut buf = vec![]; From 693ab4562502a225f28d3e6f82dc53f62acceeef Mon Sep 17 00:00:00 2001 From: Artyom Artamonov Date: Wed, 19 Jun 2024 02:12:28 +0500 Subject: [PATCH 5/8] soft wrap on some characters --- yazi-plugin/src/external/highlighter.rs | 87 ++++++++++++++++++------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 14846bb6..4b516130 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -12,6 +12,8 @@ static INCR: AtomicUsize = AtomicUsize::new(0); static SYNTECT_SYNTAX: OnceLock = OnceLock::new(); static SYNTECT_THEME: OnceLock = OnceLock::new(); +const MAX_LINE_BYTES_TO_PLAINTEXT_FALLBACK: usize = 6000; + pub struct Highlighter { path: PathBuf, } @@ -102,7 +104,7 @@ impl Highlighter { if buf.is_empty() { break; } - if !*plain && buf.len() > 6000 { + if !*plain && buf.len() > MAX_LINE_BYTES_TO_PLAINTEXT_FALLBACK { *plain = true; } long_lines.push(buf.clone()); @@ -154,7 +156,7 @@ impl Highlighter { let mut buf = vec![]; while reader.read_until(b'\n', &mut buf).await.is_ok() { lines_handled += 1; - if !*plain && buf.len() > 6000 { + if !*plain && buf.len() > MAX_LINE_BYTES_TO_PLAINTEXT_FALLBACK { *plain = true; drop(mem::take(&mut before)); } @@ -202,7 +204,7 @@ impl Highlighter { line.push(b'\n') } - let text = String::from_utf8_lossy(&line).into_owned(); + let text = String::from_utf8_lossy(line).into_owned(); if lines_handled > skip { after.push(text); } else if !plain { @@ -212,30 +214,69 @@ impl Highlighter { } fn chunk_by_width(line: Vec, width: usize) -> Vec> { - let mut res = vec![]; - let mut buf = vec![]; - let mut char_buf = vec![]; - for b in &line { - buf.push(*b); - char_buf.push(*b); - if String::from_utf8(char_buf.clone()).is_ok() { - let buf_width = String::from_utf8_lossy(&buf).width(); - if buf_width == width { - res.push(buf.clone()); - buf.clear(); - } else if buf_width > width { - buf = buf[..buf.len() - char_buf.len()].to_vec(); - res.push(buf.clone()); - buf.clear(); - buf.extend_from_slice(&char_buf); + if String::from_utf8_lossy(&line).width() <= width { + return vec![line]; + } + + let mut resulted_lines = vec![]; + let mut buf_line = vec![]; + let mut buf_char = vec![]; + let mut last_break_char_idx = 0; + let mut last_break_idx = 0; + for (i, byte) in line.iter().enumerate() { + let byte = *byte; + + buf_line.push(byte); + buf_char.push(byte); + + if let Ok(char) = String::from_utf8(buf_char.clone()) { + if ",.; ".contains(&char) { + last_break_char_idx = i + 1 } - char_buf.clear(); + + let buf_line_width = String::from_utf8_lossy(&buf_line).width(); + if buf_line_width < width { + buf_char.clear(); + continue; + } + + if last_break_char_idx == 0 { + // no spaces in line, break right here + match buf_line_width.cmp(&width) { + std::cmp::Ordering::Equal => { + resulted_lines.push(buf_line.clone()); + buf_line.clear(); + last_break_idx = i + 1; + } + std::cmp::Ordering::Greater => { + let take_bytes = buf_line.len() - buf_char.len(); + buf_line = buf_line[..take_bytes].to_vec(); + resulted_lines.push(buf_line.clone()); + buf_line.clear(); + buf_line.extend_from_slice(&buf_char); + last_break_idx = i - take_bytes + 1; + } + _ => {} + } + } else { + let break_idx = last_break_char_idx - last_break_idx; + resulted_lines.push(buf_line[..break_idx].to_vec()); + buf_line = if last_break_char_idx == buf_line.len() { + vec![] + } else { + buf_line[break_idx..].to_vec() + }; + last_break_idx = last_break_char_idx; + } + last_break_char_idx = 0; + buf_char.clear(); } } - if !buf.is_empty() { - res.push(buf); + if !buf_line.is_empty() && String::from_utf8_lossy(&buf_line) != "\n" { + resulted_lines.push(buf_line); } - res + + resulted_lines } fn replace_tabs_with_spaces(buf: &mut Vec, tab_size: usize) { From 1f562031f433df53a065e5315bd10ad13e43bd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Wed, 19 Jun 2024 23:10:30 +0800 Subject: [PATCH 6/8] feat: keep file creation time on macOS (#1169) --- Cargo.lock | 13 ++++++------- yazi-dds/Cargo.toml | 2 +- yazi-fm/Cargo.toml | 2 +- yazi-plugin/Cargo.toml | 2 +- yazi-plugin/src/utils/text.rs | 11 ++++++----- yazi-proxy/Cargo.toml | 2 +- yazi-shared/Cargo.toml | 1 - yazi-shared/src/fs/fns.rs | 16 +++++++++++++--- 8 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1474370e..5d0debff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,9 +1205,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e340c022072f3208a4105458286f4985ba5355bfe243c3073afe45cbe9ecf491" +checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" dependencies = [ "bstr", "erased-serde", @@ -1223,9 +1223,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5552e7e4e22ada0463dfdeee6caf6dc057a189fdc83136408a8f950a5e5c5540" +checksum = "a088ed0723df7567f569ba018c5d48c23c501f3878b190b04144dfa5ebfa8abc" dependencies = [ "cc", "cfg-if", @@ -1677,9 +1677,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" @@ -2955,7 +2955,6 @@ dependencies = [ "bitflags 2.5.0", "crossterm", "dirs", - "filetime", "futures", "libc", "parking_lot", diff --git a/yazi-dds/Cargo.toml b/yazi-dds/Cargo.toml index 0b07070b..d364f54d 100644 --- a/yazi-dds/Cargo.toml +++ b/yazi-dds/Cargo.toml @@ -18,7 +18,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies anyhow = "1.0.86" -mlua = { version = "0.9.8", features = [ "lua54" ] } +mlua = { version = "0.9.9", features = [ "lua54" ] } parking_lot = "0.12.3" serde = { version = "1.0.203", features = [ "derive" ] } serde_json = "1.0.117" diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index eaab5478..c06cb4ba 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -28,7 +28,7 @@ better-panic = "0.3.0" crossterm = { version = "0.27.0", features = [ "event-stream" ] } fdlimit = "0.3.0" futures = "0.3.30" -mlua = { version = "0.9.8", features = [ "lua54" ] } +mlua = { version = "0.9.9", features = [ "lua54" ] } ratatui = "0.26.3" scopeguard = "1.2.0" syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 7fe5c8c3..6190239a 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -27,7 +27,7 @@ base64 = "0.22.1" crossterm = "0.27.0" futures = "0.3.30" md-5 = "0.10.6" -mlua = { version = "0.9.8", features = [ "lua54", "serialize", "macros", "async" ] } +mlua = { version = "0.9.9", features = [ "lua54", "serialize", "macros", "async" ] } parking_lot = "0.12.3" ratatui = "0.26.3" serde = "1.0.203" diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index a1045261..b95f9887 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -10,11 +10,12 @@ impl Utils { pub(super) fn text(lua: &Lua, ya: &Table) -> mlua::Result<()> { ya.raw_set( "quote", - lua.create_function(|_, s: mlua::String| { - #[cfg(unix)] - let s = shell_escape::unix::escape(s.to_str()?.into()); - #[cfg(windows)] - let s = shell_escape::windows::escape(s.to_str()?.into()); + lua.create_function(|_, (s, unix): (mlua::String, Option)| { + let s = match unix { + Some(true) => shell_escape::unix::escape(s.to_str()?.into()), + Some(false) => shell_escape::windows::escape(s.to_str()?.into()), + None => shell_escape::escape(s.to_str()?.into()), + }; Ok(s.into_owned()) })?, )?; diff --git a/yazi-proxy/Cargo.toml b/yazi-proxy/Cargo.toml index bb69bf2d..4d1c3ca4 100644 --- a/yazi-proxy/Cargo.toml +++ b/yazi-proxy/Cargo.toml @@ -18,5 +18,5 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies anyhow = "1.0.86" -mlua = { version = "0.9.8", features = [ "lua54" ] } +mlua = { version = "0.9.9", features = [ "lua54" ] } tokio = { version = "1.38.0", features = [ "full" ] } diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index c18b36fe..414bba0d 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -14,7 +14,6 @@ anyhow = "1.0.86" bitflags = "2.5.0" crossterm = "0.27.0" dirs = "5.0.1" -filetime = "0.2.23" futures = "0.3.30" parking_lot = "0.12.3" percent-encoding = "2.3.1" diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index fa67bafa..0e8d9a41 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -1,7 +1,6 @@ use std::{borrow::Cow, collections::{HashMap, VecDeque}, fs::Metadata, path::{Path, PathBuf}}; use anyhow::Result; -use filetime::{set_file_mtime, FileTime}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; #[inline] @@ -104,12 +103,23 @@ pub fn copy_with_progress( tokio::spawn({ let (from, to) = (from.to_owned(), to.to_owned()); - let mtime = FileTime::from_last_modification_time(meta); + + let mut ft = std::fs::FileTimes::new(); + meta.accessed().map(|t| ft = ft.set_accessed(t)).ok(); + meta.modified().map(|t| ft = ft.set_modified(t)).ok(); + #[cfg(target_os = "macos")] + { + use std::os::macos::fs::FileTimesExt; + meta.created().map(|t| ft = ft.set_created(t)).ok(); + } async move { _ = match fs::copy(&from, &to).await { Ok(len) => { - set_file_mtime(to, mtime).ok(); + _ = tokio::task::spawn_blocking(move || { + std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok(); + }) + .await; tick_tx.send(Ok(len)) } Err(e) => tick_tx.send(Err(e)), From 931a63808ae348739ace26aecb15e4908259f674 Mon Sep 17 00:00:00 2001 From: Artyom Artamonov Date: Wed, 19 Jun 2024 22:50:19 +0500 Subject: [PATCH 7/8] chunk_by_width optimization --- yazi-plugin/src/external/highlighter.rs | 135 ++++++++++++------------ 1 file changed, 69 insertions(+), 66 deletions(-) diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 4b516130..89c33056 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -107,7 +107,7 @@ impl Highlighter { if !*plain && buf.len() > MAX_LINE_BYTES_TO_PLAINTEXT_FALLBACK { *plain = true; } - long_lines.push(buf.clone()); + long_lines.push(mem::take(&mut buf)); buf.clear() } @@ -118,14 +118,13 @@ impl Highlighter { } Self::replace_tabs_with_spaces(&mut long_line, PREVIEW.tab_size as usize); for line in Self::chunk_by_width(long_line, area.width as usize) { - let mut line = line.to_vec(); lines_handled += 1; let must_break = Self::handle_single_line( lines_handled, skip, *plain, area.height as usize, - &mut line, + line, &mut before, &mut after, ); @@ -165,7 +164,7 @@ impl Highlighter { skip, *plain, area.height as usize, - &mut buf, + String::from_utf8_lossy(&buf).to_string(), &mut before, &mut after, ); @@ -188,7 +187,7 @@ impl Highlighter { skip: usize, plain: bool, limit: usize, - line: &mut Vec, + mut line: String, before: &mut Vec, after: &mut Vec, ) -> bool { @@ -196,83 +195,87 @@ impl Highlighter { return true; } - if line.ends_with(b"\r\n") { - line.pop(); - line.pop(); - line.push(b'\n'); - } else if !line.ends_with(b"\n") { - line.push(b'\n') + if line.as_str().ends_with("\r\n") { + let mut chars = line.chars(); + chars.next_back(); + chars.next_back(); + line = chars.as_str().to_string(); + } else if !line.as_str().ends_with('\n') { + line.push('\n') } - let text = String::from_utf8_lossy(line).into_owned(); if lines_handled > skip { - after.push(text); + after.push(line); } else if !plain { - before.push(text); + before.push(line); } false } - fn chunk_by_width(line: Vec, width: usize) -> Vec> { - if String::from_utf8_lossy(&line).width() <= width { - return vec![line]; + fn chunk_by_width(line: Vec, width: usize) -> Vec { + let line = String::from_utf8_lossy(&line); + if line.width() <= width { + return vec![line.to_string()]; } let mut resulted_lines = vec![]; - let mut buf_line = vec![]; - let mut buf_char = vec![]; + // Use this buffer to calculate width + let mut buf_line = String::with_capacity(width); + // Use this buffer to slice line + let mut buf_chars = Vec::with_capacity(width); let mut last_break_char_idx = 0; let mut last_break_idx = 0; - for (i, byte) in line.iter().enumerate() { - let byte = *byte; + for (i, char) in line.chars().enumerate() { + buf_line.push(char); + buf_chars.push(char); - buf_line.push(byte); - buf_char.push(byte); - - if let Ok(char) = String::from_utf8(buf_char.clone()) { - if ",.; ".contains(&char) { - last_break_char_idx = i + 1 - } - - let buf_line_width = String::from_utf8_lossy(&buf_line).width(); - if buf_line_width < width { - buf_char.clear(); - continue; - } - - if last_break_char_idx == 0 { - // no spaces in line, break right here - match buf_line_width.cmp(&width) { - std::cmp::Ordering::Equal => { - resulted_lines.push(buf_line.clone()); - buf_line.clear(); - last_break_idx = i + 1; - } - std::cmp::Ordering::Greater => { - let take_bytes = buf_line.len() - buf_char.len(); - buf_line = buf_line[..take_bytes].to_vec(); - resulted_lines.push(buf_line.clone()); - buf_line.clear(); - buf_line.extend_from_slice(&buf_char); - last_break_idx = i - take_bytes + 1; - } - _ => {} - } - } else { - let break_idx = last_break_char_idx - last_break_idx; - resulted_lines.push(buf_line[..break_idx].to_vec()); - buf_line = if last_break_char_idx == buf_line.len() { - vec![] - } else { - buf_line[break_idx..].to_vec() - }; - last_break_idx = last_break_char_idx; - } - last_break_char_idx = 0; - buf_char.clear(); + if ",.; ".contains(char) { + last_break_char_idx = i + 1 } + + let buf_line_width = buf_line.width(); + if buf_line_width < width { + continue; + } + + if last_break_char_idx == 0 { + // no spaces in line, break right here + match buf_line_width.cmp(&width) { + std::cmp::Ordering::Equal => { + resulted_lines.push(mem::take(&mut buf_line)); + last_break_idx = i + 1; + + buf_line = String::with_capacity(width); + buf_chars = Vec::with_capacity(width); + } + std::cmp::Ordering::Greater => { + let last_idx = buf_line.len() - char.len_utf8(); + buf_line = buf_line[..last_idx].to_string(); + resulted_lines.push(mem::take(&mut buf_line)); + last_break_idx = i - last_idx + 1; + + buf_line = String::with_capacity(width); + buf_line.push(char); + buf_chars = Vec::with_capacity(width); + buf_chars.push(char); + } + _ => {} + } + } else { + let break_idx = last_break_char_idx - last_break_idx; + resulted_lines.push(buf_chars[..break_idx].iter().collect()); + last_break_idx = last_break_char_idx; + + buf_chars = if last_break_char_idx == buf_chars.len() { + Vec::with_capacity(width) + } else { + buf_chars[break_idx..].to_vec() + }; + buf_line = buf_chars.iter().collect(); + } + last_break_char_idx = 0; } - if !buf_line.is_empty() && String::from_utf8_lossy(&buf_line) != "\n" { + if !buf_line.is_empty() && buf_line != "\n" { resulted_lines.push(buf_line); } From 6fb569746e37de89fea7ebacea7132c9651f29de Mon Sep 17 00:00:00 2001 From: Artyom Artamonov Date: Sat, 22 Jun 2024 01:09:24 +0500 Subject: [PATCH 8/8] remove second loop in word wrapping, refactoring --- yazi-plugin/src/external/highlighter.rs | 165 +++++++++--------------- 1 file changed, 64 insertions(+), 101 deletions(-) diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 89c33056..ffd694c6 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -65,121 +65,85 @@ impl Highlighter { area: Rect, wrap: bool, ) -> Result, PeekError> { - let reader = BufReader::new(File::open(&self.path).await?); + let mut reader = BufReader::new(File::open(&self.path).await?); let syntax = Self::find_syntax(&self.path).await; let mut plain = syntax.is_err(); - let (before, after): (Vec, Vec); - if wrap { - (before, after) = Self::before_after_wrapped(skip, area, &mut plain, reader).await?; - } else { - (before, after) = Self::before_after(skip, area, &mut plain, reader).await?; + let mut before = Vec::with_capacity(if plain { 0 } else { skip }); + let mut after = Vec::with_capacity(area.height as usize); + + let mut lines_handled = 0; + let mut long_line = vec![]; + while reader.read_until(b'\n', &mut long_line).await.is_ok() { + if long_line.is_empty() || lines_handled > skip + area.height as usize { + break; + } + if !plain && long_line.len() > MAX_LINE_BYTES_TO_PLAINTEXT_FALLBACK { + plain = true; + drop(mem::take(&mut before)); + } + Self::replace_tabs_with_spaces(&mut long_line, PREVIEW.tab_size as usize); + if wrap { + Self::handle_line_wrap( + &long_line, + area, + plain, + skip, + &mut lines_handled, + &mut before, + &mut after, + ); + } else { + lines_handled += 1; + Self::handle_single_line( + lines_handled, + skip, + plain, + area.height as usize, + String::from_utf8_lossy(&long_line).to_string(), + &mut before, + &mut after, + ); + } + long_line.clear(); } + let no_more_scroll = lines_handled < skip + area.height as usize; + if skip > 0 && no_more_scroll { + return Err(PeekError::Exceed(lines_handled.saturating_sub(area.height as usize))); + } if plain { - if !wrap { - let indent = " ".repeat(PREVIEW.tab_size as usize); - return Ok(Text::from(after.join("").replace('\t', &indent))); - } Ok(Text::from(after.join(""))) } else { Self::highlight_with(before, after, syntax.unwrap()).await } } - async fn before_after_wrapped( - skip: usize, + fn handle_line_wrap( + long_line: &[u8], area: Rect, - plain: &mut bool, - mut reader: BufReader, - ) -> Result<(Vec, Vec), PeekError> { - let mut before = Vec::with_capacity(if *plain { 0 } else { skip }); - let mut after = Vec::with_capacity(area.height as usize); - - let mut long_lines = vec![]; - let mut buf = vec![]; - // If we want to indent plain text, we have to decide if it is plain - while reader.read_until(b'\n', &mut buf).await.is_ok() { - if buf.is_empty() { - break; - } - if !*plain && buf.len() > MAX_LINE_BYTES_TO_PLAINTEXT_FALLBACK { - *plain = true; - } - long_lines.push(mem::take(&mut buf)); - buf.clear() - } - - let mut lines_handled = 0; - for mut long_line in long_lines { - if long_line.is_empty() || lines_handled > skip + area.height as usize { - break; - } - Self::replace_tabs_with_spaces(&mut long_line, PREVIEW.tab_size as usize); - for line in Self::chunk_by_width(long_line, area.width as usize) { - lines_handled += 1; - let must_break = Self::handle_single_line( - lines_handled, - skip, - *plain, - area.height as usize, - line, - &mut before, - &mut after, - ); - if must_break { - break; - } - } - } - - let no_more_scroll = lines_handled < skip + area.height as usize; - if skip > 0 && no_more_scroll { - return Err(PeekError::Exceed(lines_handled.saturating_sub(area.height as usize))); - } - - Ok((before, after)) - } - - async fn before_after( + plain: bool, skip: usize, - area: Rect, - plain: &mut bool, - mut reader: BufReader, - ) -> Result<(Vec, Vec), PeekError> { - let mut before = Vec::with_capacity(if *plain { 0 } else { skip }); - let mut after = Vec::with_capacity(area.height as usize); - - let mut lines_handled = 0; - let mut buf = vec![]; - while reader.read_until(b'\n', &mut buf).await.is_ok() { - lines_handled += 1; - if !*plain && buf.len() > MAX_LINE_BYTES_TO_PLAINTEXT_FALLBACK { - *plain = true; - drop(mem::take(&mut before)); - } + lines_handled: &mut usize, + before: &mut Vec, + after: &mut Vec, + ) { + for line in Self::chunk_by_width(long_line, area.width as usize) { + *lines_handled += 1; let must_break = Self::handle_single_line( - lines_handled, + *lines_handled, skip, - *plain, + plain, area.height as usize, - String::from_utf8_lossy(&buf).to_string(), - &mut before, - &mut after, + line, + before, + after, ); if must_break { break; } - buf.clear(); } - - let no_more_scroll = lines_handled < skip + area.height as usize; - if skip > 0 && no_more_scroll { - return Err(PeekError::Exceed(lines_handled.saturating_sub(area.height as usize))); - } - - Ok((before, after)) } fn handle_single_line( @@ -195,13 +159,12 @@ impl Highlighter { return true; } - if line.as_str().ends_with("\r\n") { - let mut chars = line.chars(); - chars.next_back(); - chars.next_back(); - line = chars.as_str().to_string(); - } else if !line.as_str().ends_with('\n') { - line.push('\n') + if line.ends_with("\r\n") { + line.pop(); + line.pop(); + line.push('\n'); + } else if !line.ends_with('\n') { + line.push('\n'); } if lines_handled > skip { @@ -212,8 +175,8 @@ impl Highlighter { false } - fn chunk_by_width(line: Vec, width: usize) -> Vec { - let line = String::from_utf8_lossy(&line); + fn chunk_by_width(line: &[u8], width: usize) -> Vec { + let line = String::from_utf8_lossy(line); if line.width() <= width { return vec![line.to_string()]; }