diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index 496f2177..bb91c0ce 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -17,6 +17,7 @@ mouse_events = [ "click", "scroll" ] title_format = "Yazi: {cwd}" [preview] +wrap = "no" tab_size = 2 max_width = 600 max_height = 900 diff --git a/yazi-config/src/preview/mod.rs b/yazi-config/src/preview/mod.rs index eacd60e4..c11fc775 100644 --- a/yazi-config/src/preview/mod.rs +++ b/yazi-config/src/preview/mod.rs @@ -1,3 +1,5 @@ mod preview; +mod wrap; pub use preview::*; +pub use wrap::*; diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 901d2e54..8e9f3f8a 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -5,10 +5,15 @@ use serde::{Deserialize, Serialize}; use validator::Validate; use yazi_shared::fs::expand_path; +use super::PreviewWrap; use crate::Xdg; +#[rustfmt::skip] +const TABS: &[&str] = &["", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]; + #[derive(Debug, Serialize)] pub struct Preview { + pub wrap: PreviewWrap, pub tab_size: u8, pub max_width: u32, pub max_height: u32, @@ -32,19 +37,11 @@ impl Preview { } #[inline] - pub fn indent(&self) -> Cow<'static, str> { - match self.tab_size { - 0 => Cow::Borrowed(""), - 1 => Cow::Borrowed(" "), - 2 => Cow::Borrowed(" "), - 3 => Cow::Borrowed(" "), - 4 => Cow::Borrowed(" "), - 5 => Cow::Borrowed(" "), - 6 => Cow::Borrowed(" "), - 7 => Cow::Borrowed(" "), - 8 => Cow::Borrowed(" "), - n => Cow::Owned(" ".repeat(n as usize)), - } + pub fn indent(&self) -> Cow<'static, str> { Self::indent_with(self.tab_size as usize) } + + #[inline] + pub fn indent_with(n: usize) -> Cow<'static, str> { + if let Some(s) = TABS.get(n) { Cow::Borrowed(s) } else { Cow::Owned(" ".repeat(n)) } } } @@ -58,6 +55,7 @@ impl FromStr for Preview { } #[derive(Deserialize, Validate)] struct Shadow { + wrap: PreviewWrap, tab_size: u8, max_width: u32, max_height: u32, @@ -84,6 +82,7 @@ impl FromStr for Preview { std::fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?; Ok(Preview { + wrap: preview.wrap, tab_size: preview.tab_size, max_width: preview.max_width, max_height: preview.max_height, diff --git a/yazi-config/src/preview/wrap.rs b/yazi-config/src/preview/wrap.rs new file mode 100644 index 00000000..cdf28245 --- /dev/null +++ b/yazi-config/src/preview/wrap.rs @@ -0,0 +1,29 @@ +use std::str::FromStr; + +use anyhow::bail; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(try_from = "String")] +pub enum PreviewWrap { + No, + Yes, +} + +impl FromStr for PreviewWrap { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "no" => Self::No, + "yes" => Self::Yes, + _ => bail!("Invalid `wrap` value: {s}"), + }) + } +} + +impl TryFrom for PreviewWrap { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { Self::from_str(&value) } +} diff --git a/yazi-plugin/preset/plugins/code.lua b/yazi-plugin/preset/plugins/code.lua index 05e0b212..714a4828 100644 --- a/yazi-plugin/preset/plugins/code.lua +++ b/yazi-plugin/preset/plugins/code.lua @@ -4,7 +4,7 @@ function M:peek() local err, bound = ya.preview_code(self) if bound then ya.manager_emit("peek", { bound, only_if = self.file.url, upper_bound = true }) - elseif err then + elseif err and not err:find("cancelled", 1, true) then ya.preview_widgets(self, { ui.Paragraph(self.area, { ui.Line(err):reverse() }), }) diff --git a/yazi-plugin/preset/plugins/json.lua b/yazi-plugin/preset/plugins/json.lua index 32314c4d..20c5cf1b 100644 --- a/yazi-plugin/preset/plugins/json.lua +++ b/yazi-plugin/preset/plugins/json.lua @@ -56,7 +56,7 @@ function M:fallback_to_builtin() local err, bound = ya.preview_code(self) if bound then ya.manager_emit("peek", { bound, only_if = self.file.url, upper_bound = true }) - elseif err then + elseif err and not err:find("cancelled", 1, true) then ya.preview_widgets(self, { ui.Paragraph(self.area, { ui.Line(err):reverse() }), }) diff --git a/yazi-plugin/src/elements/paragraph.rs b/yazi-plugin/src/elements/paragraph.rs index fc44d589..68563456 100644 --- a/yazi-plugin/src/elements/paragraph.rs +++ b/yazi-plugin/src/elements/paragraph.rs @@ -10,9 +10,9 @@ const CENTER: u8 = 1; const RIGHT: u8 = 2; // Wrap -const WRAP_NO: u8 = 0; -const WRAP: u8 = 1; -const WRAP_TRIM: u8 = 2; +pub const WRAP_NO: u8 = 0; +pub const WRAP: u8 = 1; +pub const WRAP_TRIM: u8 = 2; #[derive(Clone, Debug, Default)] pub struct Paragraph { @@ -45,7 +45,7 @@ impl Paragraph { ("CENTER", CENTER.into_lua(lua)?), ("RIGHT", RIGHT.into_lua(lua)?), // Wrap - ("WRAP_OFF", WRAP_NO.into_lua(lua)?), + ("WRAP_NO", WRAP_NO.into_lua(lua)?), ("WRAP", WRAP.into_lua(lua)?), ("WRAP_TRIM", WRAP_TRIM.into_lua(lua)?), ])?; diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 04bae7b9..697980f8 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -1,10 +1,10 @@ -use std::{io::Cursor, mem, path::{Path, PathBuf}, sync::atomic::{AtomicUsize, Ordering}}; +use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::atomic::{AtomicUsize, Ordering}}; use anyhow::{anyhow, Result}; -use ratatui::text::{Line, Span, Text}; +use ratatui::{layout::Rect, text::{Line, Span, Text}}; use syntect::{dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}, LoadingError}; use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}, sync::OnceCell}; -use yazi_config::{PREVIEW, THEME}; +use yazi_config::{preview::PreviewWrap, PREVIEW, THEME}; use yazi_shared::PeekError; static INCR: AtomicUsize = AtomicUsize::new(0); @@ -41,23 +41,20 @@ impl Highlighter { #[inline] pub fn abort() { INCR.fetch_add(1, Ordering::Relaxed); } - pub async fn highlight(&self, skip: usize, limit: usize) -> Result, PeekError> { + pub async fn highlight(&self, skip: usize, area: Rect) -> 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 _); let mut i = 0; let mut buf = vec![]; let mut inspected = 0u16; - while reader.read_until(b'\n', &mut buf).await.is_ok() { - i += 1; - if buf.is_empty() || i > skip + limit { - break; - } else if Self::is_binary(&buf, &mut inspected) { + while reader.read_until(b'\n', &mut buf).await.is_ok_and(|n| n > 0) { + if Self::is_binary(&buf, &mut inspected) { return Err("Binary file".into()); } @@ -72,17 +69,27 @@ impl Highlighter { buf.push(b'\n'); } - if i > skip { + 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], area.width) } else if !plain { before.push(String::from_utf8_lossy(&buf).into_owned()); - } + Self::line_height(&before[before.len() - 1], area.width) + } else if PREVIEW.wrap == PreviewWrap::Yes { + Self::line_height(&String::from_utf8_lossy(&buf), area.width) + } else { + 1 + }; + buf.clear(); + if i > skip + area.height as usize { + break; + } } - 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 _))); } Ok(if plain { @@ -153,6 +160,31 @@ impl Highlighter { } } + fn line_height(s: &str, width: u16) -> usize { + if PREVIEW.wrap != PreviewWrap::Yes { + return 1; + } + + let pad = 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 carriage_return_to_line_feed(c: &mut u8) { if *c == b'\r' { diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index f81c8049..834ed70d 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -1,8 +1,9 @@ use mlua::{AnyUserData, IntoLuaMulti, Lua, Table, Value}; +use yazi_config::{preview::PreviewWrap, PREVIEW}; use yazi_shared::{emit, event::Cmd, Layer, PeekError}; use super::Utils; -use crate::{bindings::Window, cast_to_renderable, elements::{Paragraph, RectRef, Renderable}, external::Highlighter, file::FileRef}; +use crate::{bindings::Window, cast_to_renderable, elements::{Paragraph, RectRef, Renderable, WRAP, WRAP_NO}, external::Highlighter, file::FileRef}; pub struct PreviewLock { pub url: yazi_shared::fs::Url, @@ -36,15 +37,20 @@ impl Utils { let area: RectRef = t.raw_get("area")?; let mut lock = PreviewLock::try_from(t)?; - let text = - match Highlighter::new(&lock.url).highlight(lock.skip, area.height as usize).await { - Ok(text) => text, - Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(lua), - Err(e @ PeekError::Unexpected(_)) => { - return (e.to_string(), Value::Nil).into_lua_multi(lua); - } - }; - lock.data = vec![Box::new(Paragraph { area: *area, text, ..Default::default() })]; + let text = match Highlighter::new(&lock.url).highlight(lock.skip, *area).await { + Ok(text) => text, + Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(lua), + Err(e @ PeekError::Unexpected(_)) => { + return (e.to_string(), Value::Nil).into_lua_multi(lua); + } + }; + + lock.data = vec![Box::new(Paragraph { + area: *area, + text, + wrap: if PREVIEW.wrap == PreviewWrap::Yes { WRAP } else { WRAP_NO }, + ..Default::default() + })]; emit!(Call(Cmd::new("preview").with_any("lock", lock), Layer::Manager)); (Value::Nil, Value::Nil).into_lua_multi(lua)