mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41: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}"
|
title_format = "Yazi: {cwd}"
|
||||||
|
|
||||||
[preview]
|
[preview]
|
||||||
|
wrap = "no"
|
||||||
tab_size = 2
|
tab_size = 2
|
||||||
max_width = 600
|
max_width = 600
|
||||||
max_height = 900
|
max_height = 900
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
mod preview;
|
mod preview;
|
||||||
|
mod wrap;
|
||||||
|
|
||||||
pub use preview::*;
|
pub use preview::*;
|
||||||
|
pub use wrap::*;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,15 @@ use serde::{Deserialize, Serialize};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
use yazi_shared::fs::expand_path;
|
use yazi_shared::fs::expand_path;
|
||||||
|
|
||||||
|
use super::PreviewWrap;
|
||||||
use crate::Xdg;
|
use crate::Xdg;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const TABS: &[&str] = &["", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "];
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct Preview {
|
pub struct Preview {
|
||||||
|
pub wrap: PreviewWrap,
|
||||||
pub tab_size: u8,
|
pub tab_size: u8,
|
||||||
pub max_width: u32,
|
pub max_width: u32,
|
||||||
pub max_height: u32,
|
pub max_height: u32,
|
||||||
|
|
@ -32,19 +37,11 @@ impl Preview {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn indent(&self) -> Cow<'static, str> {
|
pub fn indent(&self) -> Cow<'static, str> { Self::indent_with(self.tab_size as usize) }
|
||||||
match self.tab_size {
|
|
||||||
0 => Cow::Borrowed(""),
|
#[inline]
|
||||||
1 => Cow::Borrowed(" "),
|
pub fn indent_with(n: usize) -> Cow<'static, str> {
|
||||||
2 => Cow::Borrowed(" "),
|
if let Some(s) = TABS.get(n) { Cow::Borrowed(s) } else { Cow::Owned(" ".repeat(n)) }
|
||||||
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)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +55,7 @@ impl FromStr for Preview {
|
||||||
}
|
}
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
struct Shadow {
|
struct Shadow {
|
||||||
|
wrap: PreviewWrap,
|
||||||
tab_size: u8,
|
tab_size: u8,
|
||||||
max_width: u32,
|
max_width: u32,
|
||||||
max_height: 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")?;
|
std::fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
|
||||||
|
|
||||||
Ok(Preview {
|
Ok(Preview {
|
||||||
|
wrap: preview.wrap,
|
||||||
tab_size: preview.tab_size,
|
tab_size: preview.tab_size,
|
||||||
max_width: preview.max_width,
|
max_width: preview.max_width,
|
||||||
max_height: preview.max_height,
|
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)
|
local err, bound = ya.preview_code(self)
|
||||||
if bound then
|
if bound then
|
||||||
ya.manager_emit("peek", { bound, only_if = self.file.url, upper_bound = true })
|
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, {
|
ya.preview_widgets(self, {
|
||||||
ui.Paragraph(self.area, { ui.Line(err):reverse() }),
|
ui.Paragraph(self.area, { ui.Line(err):reverse() }),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ function M:fallback_to_builtin()
|
||||||
local err, bound = ya.preview_code(self)
|
local err, bound = ya.preview_code(self)
|
||||||
if bound then
|
if bound then
|
||||||
ya.manager_emit("peek", { bound, only_if = self.file.url, upper_bound = true })
|
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, {
|
ya.preview_widgets(self, {
|
||||||
ui.Paragraph(self.area, { ui.Line(err):reverse() }),
|
ui.Paragraph(self.area, { ui.Line(err):reverse() }),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ const CENTER: u8 = 1;
|
||||||
const RIGHT: u8 = 2;
|
const RIGHT: u8 = 2;
|
||||||
|
|
||||||
// Wrap
|
// Wrap
|
||||||
const WRAP_NO: u8 = 0;
|
pub const WRAP_NO: u8 = 0;
|
||||||
const WRAP: u8 = 1;
|
pub const WRAP: u8 = 1;
|
||||||
const WRAP_TRIM: u8 = 2;
|
pub const WRAP_TRIM: u8 = 2;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Paragraph {
|
pub struct Paragraph {
|
||||||
|
|
@ -45,7 +45,7 @@ impl Paragraph {
|
||||||
("CENTER", CENTER.into_lua(lua)?),
|
("CENTER", CENTER.into_lua(lua)?),
|
||||||
("RIGHT", RIGHT.into_lua(lua)?),
|
("RIGHT", RIGHT.into_lua(lua)?),
|
||||||
// Wrap
|
// Wrap
|
||||||
("WRAP_OFF", WRAP_NO.into_lua(lua)?),
|
("WRAP_NO", WRAP_NO.into_lua(lua)?),
|
||||||
("WRAP", WRAP.into_lua(lua)?),
|
("WRAP", WRAP.into_lua(lua)?),
|
||||||
("WRAP_TRIM", WRAP_TRIM.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 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 syntect::{dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}, LoadingError};
|
||||||
use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}, sync::OnceCell};
|
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;
|
use yazi_shared::PeekError;
|
||||||
|
|
||||||
static INCR: AtomicUsize = AtomicUsize::new(0);
|
static INCR: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
@ -41,23 +41,20 @@ impl Highlighter {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn abort() { INCR.fetch_add(1, Ordering::Relaxed); }
|
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 mut reader = BufReader::new(File::open(&self.path).await?);
|
||||||
|
|
||||||
let syntax = Self::find_syntax(&self.path).await;
|
let syntax = Self::find_syntax(&self.path).await;
|
||||||
let mut plain = syntax.is_err();
|
let mut plain = syntax.is_err();
|
||||||
|
|
||||||
let mut before = Vec::with_capacity(if plain { 0 } else { skip });
|
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 i = 0;
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
let mut inspected = 0u16;
|
let mut inspected = 0u16;
|
||||||
while reader.read_until(b'\n', &mut buf).await.is_ok() {
|
while reader.read_until(b'\n', &mut buf).await.is_ok_and(|n| n > 0) {
|
||||||
i += 1;
|
if Self::is_binary(&buf, &mut inspected) {
|
||||||
if buf.is_empty() || i > skip + limit {
|
|
||||||
break;
|
|
||||||
} else if Self::is_binary(&buf, &mut inspected) {
|
|
||||||
return Err("Binary file".into());
|
return Err("Binary file".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,17 +69,27 @@ impl Highlighter {
|
||||||
buf.push(b'\n');
|
buf.push(b'\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
if i > skip {
|
i += if i >= skip {
|
||||||
buf.iter_mut().for_each(Self::carriage_return_to_line_feed);
|
buf.iter_mut().for_each(Self::carriage_return_to_line_feed);
|
||||||
after.push(String::from_utf8_lossy(&buf).into_owned());
|
after.push(String::from_utf8_lossy(&buf).into_owned());
|
||||||
|
Self::line_height(&after[after.len() - 1], area.width)
|
||||||
} else if !plain {
|
} else if !plain {
|
||||||
before.push(String::from_utf8_lossy(&buf).into_owned());
|
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();
|
buf.clear();
|
||||||
|
if i > skip + area.height as usize {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if skip > 0 && i < skip + limit {
|
if skip > 0 && i < skip + area.height as usize {
|
||||||
return Err(PeekError::Exceed(i.saturating_sub(limit)));
|
return Err(PeekError::Exceed(i.saturating_sub(area.height as _)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(if plain {
|
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)]
|
#[inline(always)]
|
||||||
fn carriage_return_to_line_feed(c: &mut u8) {
|
fn carriage_return_to_line_feed(c: &mut u8) {
|
||||||
if *c == b'\r' {
|
if *c == b'\r' {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use mlua::{AnyUserData, IntoLuaMulti, Lua, Table, Value};
|
use mlua::{AnyUserData, IntoLuaMulti, Lua, Table, Value};
|
||||||
|
use yazi_config::{preview::PreviewWrap, PREVIEW};
|
||||||
use yazi_shared::{emit, event::Cmd, Layer, PeekError};
|
use yazi_shared::{emit, event::Cmd, Layer, PeekError};
|
||||||
|
|
||||||
use super::Utils;
|
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 struct PreviewLock {
|
||||||
pub url: yazi_shared::fs::Url,
|
pub url: yazi_shared::fs::Url,
|
||||||
|
|
@ -36,15 +37,20 @@ impl Utils {
|
||||||
let area: RectRef = t.raw_get("area")?;
|
let area: RectRef = t.raw_get("area")?;
|
||||||
let mut lock = PreviewLock::try_from(t)?;
|
let mut lock = PreviewLock::try_from(t)?;
|
||||||
|
|
||||||
let text =
|
let text = match Highlighter::new(&lock.url).highlight(lock.skip, *area).await {
|
||||||
match Highlighter::new(&lock.url).highlight(lock.skip, area.height as usize).await {
|
Ok(text) => text,
|
||||||
Ok(text) => text,
|
Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(lua),
|
||||||
Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(lua),
|
Err(e @ PeekError::Unexpected(_)) => {
|
||||||
Err(e @ PeekError::Unexpected(_)) => {
|
return (e.to_string(), Value::Nil).into_lua_multi(lua);
|
||||||
return (e.to_string(), Value::Nil).into_lua_multi(lua);
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
lock.data = vec![Box::new(Paragraph { area: *area, text, ..Default::default() })];
|
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));
|
emit!(Call(Cmd::new("preview").with_any("lock", lock), Layer::Manager));
|
||||||
(Value::Nil, Value::Nil).into_lua_multi(lua)
|
(Value::Nil, Value::Nil).into_lua_multi(lua)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue