From be91b4111c6d063cd2d9cc0685337b8700263b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Thu, 5 Mar 2026 20:16:03 +0800 Subject: [PATCH] fix: shift of auto-wrap points in both code and JSON previewers (#3739) --- Cargo.lock | 38 +++++++------ Cargo.toml | 4 +- yazi-binding/src/elements/wrap.rs | 14 +++++ yazi-codegen/Cargo.toml | 2 +- yazi-config/src/preview/preview.rs | 16 +++--- yazi-config/src/preview/wrap.rs | 9 ++++ yazi-plugin/Cargo.toml | 1 + yazi-plugin/preset/plugins/json.lua | 8 ++- yazi-plugin/src/elements/elements.rs | 24 +++++++-- yazi-plugin/src/external/highlighter.rs | 72 ++++++++----------------- yazi-plugin/src/runtime/runtime.rs | 4 +- yazi-shim/Cargo.toml | 1 + yazi-shim/src/lib.rs | 2 +- yazi-shim/src/ratatui/mod.rs | 1 + yazi-shim/src/ratatui/paragraph.rs | 52 ++++++++++++++++++ 15 files changed, 163 insertions(+), 85 deletions(-) create mode 100644 yazi-shim/src/ratatui/mod.rs create mode 100644 yazi-shim/src/ratatui/paragraph.rs diff --git a/Cargo.lock b/Cargo.lock index 940ce5c4..1ec82542 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1402,9 +1402,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -1735,19 +1735,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -3366,9 +3366,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3379,6 +3379,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -4619,9 +4625,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -4636,9 +4642,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -4671,9 +4677,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.3+spec-1.1.0" +version = "1.0.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +checksum = "c94c3321114413476740df133f0d8862c61d87c8d26f04c6841e033c8c80db47" dependencies = [ "indexmap 2.13.0", "serde_core", @@ -4902,7 +4908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "atomic", - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -6066,6 +6072,7 @@ dependencies = [ name = "yazi-plugin" version = "26.2.2" dependencies = [ + "ansi-to-tui", "anyhow", "crossterm 0.29.0", "futures", @@ -6192,6 +6199,7 @@ name = "yazi-shim" version = "26.2.2" dependencies = [ "crossterm 0.29.0", + "ratatui", "twox-hash", "yazi-macro", ] diff --git a/Cargo.toml b/Cargo.toml index 1c15398e..cbed9f95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,10 +68,10 @@ serde_json = "1.0.149" serde_with = "3.17.0" syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } thiserror = "2.0.18" -tokio = { version = "1.49.0", features = [ "full" ] } +tokio = { version = "1.50.0", features = [ "full" ] } tokio-stream = "0.1.18" tokio-util = "0.7.18" -toml = { version = "1.0.3" } +toml = { version = "1.0.4" } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } typed-path = "0.12.3" diff --git a/yazi-binding/src/elements/wrap.rs b/yazi-binding/src/elements/wrap.rs index c76c82f8..85f205f8 100644 --- a/yazi-binding/src/elements/wrap.rs +++ b/yazi-binding/src/elements/wrap.rs @@ -18,6 +18,10 @@ impl Wrap { } } +impl From for Option { + fn from(value: Wrap) -> Self { value.0 } +} + impl From for Wrap { fn from(value: ratatui::widgets::Wrap) -> Self { Self(Some(value)) } } @@ -52,3 +56,13 @@ impl FromLua for Wrap { })) } } + +impl IntoLua for Wrap { + fn into_lua(self, lua: &Lua) -> mlua::Result { + match self.0 { + None => 0.into_lua(lua), + Some(ratatui::widgets::Wrap { trim: false }) => 1.into_lua(lua), + Some(ratatui::widgets::Wrap { trim: true }) => 2.into_lua(lua), + } + } +} diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 400bfddc..0634bfd3 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -18,4 +18,4 @@ proc-macro = true [dependencies] # External dependencies syn = { version = "2.0.117", features = [ "full" ] } -quote = "1.0.44" +quote = "1.0.45" diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 02e6da83..490f5c41 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, path::PathBuf}; +use std::path::PathBuf; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; @@ -9,9 +9,6 @@ use yazi_shared::{SStr, timestamp_us}; use super::PreviewWrap; use crate::normalize_path; -#[rustfmt::skip] -const TABS: &[&str] = &["", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]; - #[derive(Debug, Deserialize, DeserializeOver2, Serialize)] pub struct Preview { pub wrap: PreviewWrap, @@ -34,10 +31,15 @@ impl Preview { self.cache_dir.join(format!("{prefix}-{}", timestamp_us())) } - pub fn indent(&self) -> SStr { Self::indent_with(self.tab_size as usize) } + pub fn indent(&self) -> SStr { + #[rustfmt::skip] + const TABS: &[&str] = &["", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]; - pub fn indent_with(n: usize) -> SStr { - if let Some(s) = TABS.get(n) { Cow::Borrowed(s) } else { Cow::Owned(" ".repeat(n)) } + if let Some(&s) = TABS.get(self.tab_size as usize) { + s.into() + } else { + " ".repeat(self.tab_size as usize).into() + } } } diff --git a/yazi-config/src/preview/wrap.rs b/yazi-config/src/preview/wrap.rs index cf9fc6f7..ca9a42bf 100644 --- a/yazi-config/src/preview/wrap.rs +++ b/yazi-config/src/preview/wrap.rs @@ -6,3 +6,12 @@ pub enum PreviewWrap { No, Yes, } + +impl From for Option { + fn from(wrap: PreviewWrap) -> Self { + match wrap { + PreviewWrap::No => None, + PreviewWrap::Yes => Some(ratatui::widgets::Wrap { trim: false }), + } + } +} diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 17a851dd..b79c4d59 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -34,6 +34,7 @@ yazi-vfs = { path = "../yazi-vfs", version = "26.2.2" } yazi-widgets = { path = "../yazi-widgets", version = "26.2.2" } # External dependencies +ansi-to-tui = { workspace = true } anyhow = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } diff --git a/yazi-plugin/preset/plugins/json.lua b/yazi-plugin/preset/plugins/json.lua index 39842ea5..3c52d410 100644 --- a/yazi-plugin/preset/plugins/json.lua +++ b/yazi-plugin/preset/plugins/json.lua @@ -11,6 +11,7 @@ function M:peek(job) return require("code"):peek(job) end + local wrap = rt.preview.wrap local limit = job.area.h local i, lines = 0, "" repeat @@ -21,7 +22,7 @@ function M:peek(job) break end - i = i + 1 + i = i + ui.height(next, { width = job.area.w, ansi = true, wrap = wrap }) if i > job.skip then lines = lines .. next end @@ -32,10 +33,7 @@ function M:peek(job) ya.emit("peek", { math.max(0, i - limit), only_if = job.file.url, upper_bound = true }) else lines = lines:gsub("\t", string.rep(" ", rt.preview.tab_size)) - ya.preview_widget( - job, - ui.Text.parse(lines):area(job.area):wrap(rt.preview.wrap == "yes" and ui.Wrap.YES or ui.Wrap.NO) - ) + ya.preview_widget(job, ui.Text.parse(lines):area(job.area):wrap(wrap)) end end diff --git a/yazi-plugin/src/elements/elements.rs b/yazi-plugin/src/elements/elements.rs index 0a1c5793..a379ce69 100644 --- a/yazi-plugin/src/elements/elements.rs +++ b/yazi-plugin/src/elements/elements.rs @@ -1,18 +1,21 @@ use std::borrow::Cow; -use mlua::{AnyUserData, ExternalError, IntoLua, Lua, ObjectLike, Table, Value}; +use ansi_to_tui::IntoText; +use mlua::{AnyUserData, ExternalError, ExternalResult, IntoLua, Lua, ObjectLike, Table, Value}; use tracing::error; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; -use yazi_binding::{Composer, ComposerGet, ComposerSet, Permit, PermitRef, elements::{Line, Rect, Span}, runtime}; -use yazi_config::LAYOUT; +use yazi_binding::{Composer, ComposerGet, ComposerSet, Permit, PermitRef, elements::{Line, Rect, Span, Wrap}, runtime}; +use yazi_config::{LAYOUT, YAZI}; use yazi_proxy::AppProxy; use yazi_shared::replace_to_printable; +use yazi_shim::ratatui::line_count; use yazi_term::YIELD_TO_SUBPROCESS; pub fn compose() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { match key { b"area" => area(lua)?, + b"height" => height(lua)?, b"hide" => hide(lua)?, b"printable" => printable(lua)?, b"redraw" => redraw(lua)?, @@ -43,6 +46,21 @@ pub(super) fn area(lua: &Lua) -> mlua::Result { f.into_lua(lua) } +pub(super) fn height(lua: &Lua) -> mlua::Result { + let f = lua.create_function(|_, (s, opts): (mlua::String, Table)| { + let width = opts.raw_get("width")?; + let wrap: Wrap = opts.raw_get("wrap")?; + + Ok(if opts.raw_get("ansi")? { + line_count(s.to_string_lossy().to_text().into_lua_err()?, width, YAZI.preview.indent(), wrap) + } else { + line_count(s.to_string_lossy(), width, YAZI.preview.indent(), wrap) + }) + })?; + + f.into_lua(lua) +} + pub(super) fn hide(lua: &Lua) -> mlua::Result { let f = lua.create_async_function(|lua, ()| async move { if runtime!(lua)?.blocking { diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index e79cbbec..29206ffb 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::OnceLock}; +use std::{io::Cursor, path::{Path, PathBuf}, sync::OnceLock}; use anyhow::{Result, anyhow}; use ratatui::{layout::Size, text::{Line, Span, Text}}; @@ -7,6 +7,7 @@ use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader}; use yazi_config::{THEME, YAZI, preview::PreviewWrap}; use yazi_fs::provider::{Provider, local::Local}; use yazi_shared::{Ids, errors::PeekError, push_printable_char}; +use yazi_shim::ratatui::line_count; static INCR: Ids = Ids::new(); static SYNTECT: OnceLock<(Theme, SyntaxSet)> = OnceLock::new(); @@ -16,7 +17,6 @@ pub struct Highlighter { } impl Highlighter { - #[inline] pub fn new

(path: P) -> Self where P: Into, @@ -40,10 +40,10 @@ impl Highlighter { (theme, syntaxes) } - #[inline] pub fn abort() { INCR.next(); } pub async fn highlight(&self, skip: usize, size: Size) -> Result, PeekError> { + let indent = YAZI.preview.indent(); let mut reader = BufReader::new(Local::regular(&self.path).open().await?); let syntax = Self::find_syntax(&self.path, &mut reader).await; @@ -60,26 +60,20 @@ impl Highlighter { return Err("Binary file".into()); } - if !plain && (buf.len() > 5000 || Self::contains_control_chars(&buf)) { + let remaining = Self::normalize_control_chars(&mut buf); + if !plain && (remaining || buf.len() > 5000) { plain = true; - drop(mem::take(&mut before)); - } - - if buf.ends_with(b"\r\n") { - buf.pop(); - buf.pop(); - buf.push(b'\n'); + before.clear(); } i += if i >= skip { - buf.iter_mut().for_each(Self::carriage_return_to_line_feed); after.push(String::from_utf8_lossy(&buf).into_owned()); - Self::line_height(&after[after.len() - 1], size.width) + line_count(&**after.last().unwrap(), size.width, &indent, YAZI.preview.wrap) } else if !plain { before.push(String::from_utf8_lossy(&buf).into_owned()); - Self::line_height(&before[before.len() - 1], size.width) - } else if YAZI.preview.wrap == PreviewWrap::Yes { - Self::line_height(&String::from_utf8_lossy(&buf), size.width) + line_count(&**before.last().unwrap(), size.width, &indent, YAZI.preview.wrap) + } else if YAZI.preview.wrap != PreviewWrap::No { + line_count(String::from_utf8_lossy(&buf), size.width, &indent, YAZI.preview.wrap) } else { 1 }; @@ -166,42 +160,22 @@ impl Highlighter { } } - fn line_height(s: &str, width: u16) -> usize { - if YAZI.preview.wrap != PreviewWrap::Yes { - return 1; + fn normalize_control_chars(buf: &mut Vec) -> bool { + if buf.ends_with(b"\r\n") { + buf.pop(); + buf.pop(); + buf.push(b'\n'); } - let pad = YAZI - .preview - .tab_size - .checked_sub(1) - .map(|n| s.bytes().filter(|&b| b == b'\t').count() * n as usize) - .map(|n| yazi_config::preview::Preview::indent_with(n)) - .unwrap_or_default(); - - let line = Line { - spans: vec![Span { content: pad, style: Default::default() }, Span { - content: Cow::Borrowed(s), - ..Default::default() - }], - ..Default::default() - }; - - ratatui::widgets::Paragraph::new(line) - .wrap(ratatui::widgets::Wrap { trim: false }) - .line_count(width) - } - - #[inline(always)] - fn contains_control_chars(buf: &[u8]) -> bool { - buf.iter().any(|&b| b.is_ascii_control() && !matches!(b, b'\t' | b'\n' | b'\r')) - } - - #[inline(always)] - fn carriage_return_to_line_feed(b: &mut u8) { - if *b == b'\r' { - *b = b'\n'; + let mut remaining = false; + for b in buf.iter_mut() { + if *b == b'\r' { + *b = b'\n'; + } else { + remaining |= matches!(b, 0..=0x08 | 0x0B..=0x1F | 0x7F); + } } + remaining } fn merge_highlight_lines(s: &[String], tab_size: u8) -> String { diff --git a/yazi-plugin/src/runtime/runtime.rs b/yazi-plugin/src/runtime/runtime.rs index 0724e3e6..2f87c811 100644 --- a/yazi-plugin/src/runtime/runtime.rs +++ b/yazi-plugin/src/runtime/runtime.rs @@ -1,5 +1,5 @@ use mlua::{IntoLua, Lua, LuaSerdeExt, Value}; -use yazi_binding::{Composer, ComposerGet, ComposerSet, SER_OPT, Url}; +use yazi_binding::{Composer, ComposerGet, ComposerSet, SER_OPT, Url, elements::Wrap}; use yazi_boot::ARGS; use yazi_config::YAZI; @@ -78,7 +78,7 @@ fn preview() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { let p = &YAZI.preview; match key { - b"wrap" => lua.to_value_with(&p.wrap, SER_OPT)?, + b"wrap" => Wrap::from(p.wrap).into_lua(lua)?, b"tab_size" => p.tab_size.into_lua(lua)?, b"max_width" => p.max_width.into_lua(lua)?, b"max_height" => p.max_height.into_lua(lua)?, diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index 5be68be0..2a13e4f3 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -17,6 +17,7 @@ yazi-macro = { path = "../yazi-macro", version = "26.2.2" } # External dependencies crossterm = { workspace = true } +ratatui = { workspace = true } twox-hash = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] diff --git a/yazi-shim/src/lib.rs b/yazi-shim/src/lib.rs index 7610f38f..a9933ddc 100644 --- a/yazi-shim/src/lib.rs +++ b/yazi-shim/src/lib.rs @@ -1,3 +1,3 @@ -yazi_macro::mod_pub!(crossterm); +yazi_macro::mod_pub!(crossterm ratatui); yazi_macro::mod_flat!(twox); diff --git a/yazi-shim/src/ratatui/mod.rs b/yazi-shim/src/ratatui/mod.rs new file mode 100644 index 00000000..dc4b07ac --- /dev/null +++ b/yazi-shim/src/ratatui/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(paragraph); diff --git a/yazi-shim/src/ratatui/paragraph.rs b/yazi-shim/src/ratatui/paragraph.rs new file mode 100644 index 00000000..8a66de49 --- /dev/null +++ b/yazi-shim/src/ratatui/paragraph.rs @@ -0,0 +1,52 @@ +use ratatui::{text::Text, widgets::{Paragraph, Wrap}}; + +pub fn line_count<'a, T, W, I>(text: T, width: u16, indent: I, wrap: W) -> usize +where + T: Into>, + I: AsRef, + W: Into>, +{ + line_count_impl(text.into(), width, indent.as_ref(), wrap.into()) +} + +fn line_count_impl(mut text: Text<'_>, mut width: u16, indent: &str, wrap: Option) -> usize { + width = width.max(1); + + let Some(wrap) = wrap else { + return Paragraph::new(text).line_count(width); + }; + + if indent.len() == 1 { + return Paragraph::new(text).wrap(wrap).line_count(width); + } + + let extra = indent.len().saturating_sub(1); + for line in &mut text.lines { + for span in &mut line.spans { + let mut out = None::; + let mut start = 0; + for (idx, b) in span.content.bytes().enumerate() { + if b != b'\t' { + continue; + } + + let out = out.get_or_insert_with(|| String::with_capacity(span.content.len() + extra)); + if start < idx { + out.push_str(unsafe { span.content.get_unchecked(start..idx) }); + } + + out.push_str(indent); + start = idx + 1; + } + + if let Some(mut out) = out { + if start < span.content.len() { + out.push_str(unsafe { span.content.get_unchecked(start..) }); + } + span.content = out.into(); + } + } + } + + Paragraph::new(text).wrap(wrap).line_count(width) +}