mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
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:
parent
58ba459b63
commit
cd7209c040
9 changed files with 112 additions and 43 deletions
|
|
@ -17,6 +17,7 @@ mouse_events = [ "click", "scroll" ]
|
|||
title_format = "Yazi: {cwd}"
|
||||
|
||||
[preview]
|
||||
wrap = "no"
|
||||
tab_size = 2
|
||||
max_width = 600
|
||||
max_height = 900
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
mod preview;
|
||||
mod wrap;
|
||||
|
||||
pub use preview::*;
|
||||
pub use wrap::*;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
29
yazi-config/src/preview/wrap.rs
Normal file
29
yazi-config/src/preview/wrap.rs
Normal 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) }
|
||||
}
|
||||
|
|
@ -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() }),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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() }),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)?),
|
||||
])?;
|
||||
|
|
|
|||
60
yazi-plugin/src/external/highlighter.rs
vendored
60
yazi-plugin/src/external/highlighter.rs
vendored
|
|
@ -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' {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue