feat: word wrapping in code previewer (#1159)

Co-authored-by: Lex Moskovski <alex@moskovski.com>
Co-authored-by: sxyazi <sxyazi@gmail.com>
This commit is contained in:
Artyom Artamonov 2024-08-26 01:00:09 +00:00 committed by GitHub
parent 58ba459b63
commit cd7209c040
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 112 additions and 43 deletions

View file

@ -17,6 +17,7 @@ mouse_events = [ "click", "scroll" ]
title_format = "Yazi: {cwd}"
[preview]
wrap = "no"
tab_size = 2
max_width = 600
max_height = 900

View file

@ -1,3 +1,5 @@
mod preview;
mod wrap;
pub use preview::*;
pub use wrap::*;

View file

@ -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,

View file

@ -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<Self, Self::Err> {
Ok(match s {
"no" => Self::No,
"yes" => Self::Yes,
_ => bail!("Invalid `wrap` value: {s}"),
})
}
}
impl TryFrom<String> for PreviewWrap {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> { Self::from_str(&value) }
}

View file

@ -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() }),
})

View file

@ -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() }),
})

View file

@ -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)?),
])?;

View file

@ -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<Text<'static>, PeekError> {
pub async fn highlight(&self, skip: usize, area: Rect) -> Result<Text<'static>, 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' {

View file

@ -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)