From 5c05350d52cbbb490a353e2df192f36dd9d73218 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Mon, 23 Mar 2026 18:06:04 +0800 Subject: [PATCH] perf: avoid unnecessary allocations in code highlighting (#3804) --- .github/workflows/cachix.yml | 2 +- CHANGELOG.md | 2 + Cargo.lock | 10 +- cspell.json | 2 +- yazi-binding/src/elements/line.rs | 6 + yazi-fs/Cargo.toml | 2 +- yazi-plugin/preset/plugins/json.lua | 17 +- yazi-plugin/src/elements/elements.rs | 48 ++-- yazi-plugin/src/external/highlighter.rs | 319 ++++++++++++------------ yazi-plugin/src/utils/preview.rs | 2 +- yazi-shim/Cargo.toml | 4 + yazi-shim/src/ratatui/grapheme.rs | 47 ++++ yazi-shim/src/ratatui/line.rs | 83 ++++++ yazi-shim/src/ratatui/mod.rs | 2 +- yazi-shim/src/ratatui/paragraph.rs | 52 ---- yazi-shim/src/ratatui/span.rs | 101 ++++++++ yazi-shim/src/ratatui/wrapper.rs | 196 +++++++++++++++ yazi-tty/src/windows.rs | 1 - 18 files changed, 655 insertions(+), 241 deletions(-) create mode 100644 yazi-shim/src/ratatui/grapheme.rs create mode 100644 yazi-shim/src/ratatui/line.rs delete mode 100644 yazi-shim/src/ratatui/paragraph.rs create mode 100644 yazi-shim/src/ratatui/span.rs create mode 100644 yazi-shim/src/ratatui/wrapper.rs diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 193cbf3d..686a1971 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -21,7 +21,7 @@ jobs: uses: cachix/install-nix-action@v31 - name: Authenticate with Cachix - uses: cachix/cachix-action@v16 + uses: cachix/cachix-action@v17 with: name: yazi authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ca6912..9c3ea546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Reduce memory allocations by using Lua 5.5 external strings ([#3634]) - Reuse previewed and spotted widgets when possible ([#3765]) +- Avoid unnecessary allocations in code highlighting ([#3804]) ## [v26.1.22] @@ -1690,3 +1691,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3780]: https://github.com/sxyazi/yazi/pull/3780 [#3781]: https://github.com/sxyazi/yazi/pull/3781 [#3792]: https://github.com/sxyazi/yazi/pull/3792 +[#3804]: https://github.com/sxyazi/yazi/pull/3804 diff --git a/Cargo.lock b/Cargo.lock index e5210dcf..b3ce943e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" dependencies = [ "rustversion", ] @@ -6131,6 +6131,8 @@ dependencies = [ "crossterm 0.29.0", "ratatui", "twox-hash", + "unicode-segmentation", + "unicode-width", "yazi-macro", ] @@ -6289,9 +6291,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" dependencies = [ "zune-core", ] diff --git a/cspell.json b/cspell.json index f1b5f8f0..098cad59 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee"],"language":"en"} \ No newline at end of file +{"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike"],"flagWords":[],"version":"0.2"} \ No newline at end of file diff --git a/yazi-binding/src/elements/line.rs b/yazi-binding/src/elements/line.rs index bd675daf..8d23a4a2 100644 --- a/yazi-binding/src/elements/line.rs +++ b/yazi-binding/src/elements/line.rs @@ -51,6 +51,12 @@ impl Line { } } +impl From> for Line { + fn from(value: ratatui::text::Line<'static>) -> Self { + Self { inner: value, ..Default::default() } + } +} + impl TryFrom for Line { type Error = mlua::Error; diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index 276af823..3457c881 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -20,7 +20,7 @@ yazi-shim = { path = "../yazi-shim", version = "26.2.2" } # External dependencies anyhow = { workspace = true } -arc-swap = "1.8.2" +arc-swap = "1.9.0" bitflags = { workspace = true } dirs = { workspace = true } either = { workspace = true } diff --git a/yazi-plugin/preset/plugins/json.lua b/yazi-plugin/preset/plugins/json.lua index 3c52d410..159bfc65 100644 --- a/yazi-plugin/preset/plugins/json.lua +++ b/yazi-plugin/preset/plugins/json.lua @@ -11,9 +11,9 @@ function M:peek(job) return require("code"):peek(job) end - local wrap = rt.preview.wrap + local opt = { ansi = true, tab_size = rt.preview.tab_size, wrap = rt.preview.wrap, width = job.area.w } local limit = job.area.h - local i, lines = 0, "" + local i, lines = 0, {} repeat local next, event = child:read_line() if event == 1 then @@ -22,9 +22,13 @@ function M:peek(job) break end - i = i + ui.height(next, { width = job.area.w, ansi = true, wrap = wrap }) - if i > job.skip then - lines = lines .. next + local wrapped = ui.lines(next, opt) + local from = math.max(1, job.skip - i + 1) + local to = math.min(#wrapped, job.skip + limit - i) + + i = i + #wrapped + for j = from, to do + lines[#lines + 1] = wrapped[j] end until i >= job.skip + limit @@ -32,8 +36,7 @@ function M:peek(job) if job.skip > 0 and i < job.skip + limit then 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(wrap)) + ya.preview_widget(job, ui.Text(lines):area(job.area)) end end diff --git a/yazi-plugin/src/elements/elements.rs b/yazi-plugin/src/elements/elements.rs index a379ce69..3ce23e65 100644 --- a/yazi-plugin/src/elements/elements.rs +++ b/yazi-plugin/src/elements/elements.rs @@ -1,22 +1,22 @@ -use std::borrow::Cow; +use std::{borrow::Cow, iter}; 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, Wrap}, runtime}; -use yazi_config::{LAYOUT, YAZI}; +use yazi_config::LAYOUT; use yazi_proxy::AppProxy; use yazi_shared::replace_to_printable; -use yazi_shim::ratatui::line_count; +use yazi_shim::ratatui::LineIter; 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"lines" => lines(lua)?, b"printable" => printable(lua)?, b"redraw" => redraw(lua)?, b"render" => render(lua)?, @@ -46,21 +46,6 @@ 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 { @@ -81,6 +66,31 @@ pub(super) fn hide(lua: &Lua) -> mlua::Result { f.into_lua(lua) } +pub(super) fn lines(lua: &Lua) -> mlua::Result { + let f = lua.create_function(|lua, (s, opts): (mlua::String, Table)| { + let b = s.as_bytes(); + let s = &*String::from_utf8_lossy(&b); + + let tab_size = opts.raw_get("tab_size")?; + let mut it = if opts.raw_get("ansi")? { + LineIter::parsed(s.to_text().into_lua_err()?.lines, tab_size) + } else { + LineIter::source(&s, tab_size) + }; + + if let Some(wrap) = *opts.raw_get::("wrap")? { + it = it.wrapped(wrap, opts.raw_get("width")?); + } + + lua.create_sequence_from(iter::from_fn(|| { + let (spans, align) = it.next()?; + Some(Line::from(spans.into_static_line().alignment(align))) + })) + })?; + + f.into_lua(lua) +} + pub(super) fn printable(lua: &Lua) -> mlua::Result { let f = lua.create_function(|lua, s: mlua::String| { Ok(match replace_to_printable(&s.as_bytes(), false, 1, true) { diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 29206ffb..be980fcb 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -1,30 +1,147 @@ -use std::{io::Cursor, path::{Path, PathBuf}, sync::OnceLock}; +use std::{io::{BufRead, BufReader, Cursor, Seek}, path::PathBuf, sync::OnceLock}; -use anyhow::{Result, anyhow}; +use anyhow::{Result, bail}; use ratatui::{layout::Size, text::{Line, Span, Text}}; use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}}; -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; +use yazi_config::{THEME, YAZI}; +use yazi_shared::{Id, Ids, errors::PeekError, replace_to_printable}; +use yazi_shim::ratatui::LineIter; static INCR: Ids = Ids::new(); static SYNTECT: OnceLock<(Theme, SyntaxSet)> = OnceLock::new(); pub struct Highlighter { - path: PathBuf, + path: PathBuf, + reader: BufReader, + + skip: usize, + size: Size, + ticket: Id, + + theme: &'static Theme, + syntaxes: &'static SyntaxSet, + inner: Option>, + syntax: Option<&'static SyntaxReference>, } impl Highlighter { - pub fn new

(path: P) -> Self + pub async fn oneshot

(path: P, skip: usize, size: Size) -> Result, PeekError> where P: Into, { - Self { path: path.into() } + let path = path.into(); + tokio::task::spawn_blocking(move || Self::make(path, skip, size)?.highlight()).await? } - pub fn init() -> (&'static Theme, &'static SyntaxSet) { + fn make

(path: P, skip: usize, size: Size) -> Result + where + P: Into, + { + let path = path.into(); + let (theme, syntaxes) = Self::load(); + + Ok(Self { + reader: BufReader::new(std::fs::File::open(&path)?), + path, + + skip, + size, + ticket: INCR.current(), + + theme, + syntaxes, + inner: None, + syntax: None, + }) + } + + pub fn abort() { INCR.next(); } + + fn highlight(mut self) -> Result, PeekError> { + self.load_syntax()?; + let mut plain = self.syntax.is_none(); + + let mut i = 0; + let mut buf = vec![]; + let mut lines = Vec::with_capacity(self.size.height as usize); + let mut inspected = 0u16; + while self.reader.read_until(b'\n', &mut buf).is_ok_and(|n| n > 0) { + if Self::is_binary(&buf, &mut inspected) { + return Err("Binary file".into()); + } + + let remaining = Self::normalize_control_chars(&mut buf); + if remaining || buf.len() > 5000 { + plain = true; + } + + self.ensure_not_cancelled()?; + if plain && !self.process_plain(&buf, &mut i, &mut lines)? { + break; + } else if !plain && !self.process_hyper(&buf, &mut i, &mut lines)? { + break; + } + buf.clear(); + } + + if self.skip > 0 && i < self.skip + self.size.height as usize { + return Err(PeekError::Exceed(i.saturating_sub(self.size.height as _))); + } + + Ok(Text::from(lines)) + } + + fn process_plain(&mut self, buf: &[u8], i: &mut usize, lines: &mut Vec) -> Result { + let b = replace_to_printable(buf, true, YAZI.preview.tab_size, false); + let s = String::from_utf8_lossy(&b); + + let mut it = LineIter::source(&s, YAZI.preview.tab_size); + if let Some(wrap) = YAZI.preview.wrap.into() { + it = it.wrapped(wrap, self.size.width); + } + + while let Some((spans, _)) = it.next() { + *i += 1; + if *i > self.skip + self.size.height as usize { + return Ok(false); + } else if *i > self.skip { + lines.push(spans.into_static_line()); + } + self.ensure_not_cancelled()?; + } + Ok(true) + } + + fn process_hyper(&mut self, buf: &[u8], i: &mut usize, lines: &mut Vec) -> Result { + let Some(syntax) = self.syntax else { bail!("No syntax") }; + let h = self.inner.get_or_insert_with(|| HighlightLines::new(syntax, self.theme)); + + let s = String::from_utf8_lossy(buf); + let line = Self::to_line_widget(h.highlight_line(&s, self.syntaxes)?); + + let mut it = LineIter::parsed(vec![line], YAZI.preview.tab_size); + if let Some(wrap) = YAZI.preview.wrap.into() { + it = it.wrapped(wrap, self.size.width); + } + + while let Some((spans, _)) = it.next() { + *i += 1; + if *i > self.skip + self.size.height as usize { + return Ok(false); + } else if *i > self.skip { + lines.push(spans.into_static_line()); + } + self.ensure_not_cancelled()?; + } + Ok(true) + } + + #[inline] + fn ensure_not_cancelled(&self) -> Result<(), PeekError> { + if self.ticket != INCR.current() { Err("Highlighting cancelled".into()) } else { Ok(()) } + } + + fn load() -> (&'static Theme, &'static SyntaxSet) { let f = || { let theme = std::fs::File::open(&THEME.mgr.syntect_theme) .map_err(LoadingError::Io) @@ -40,114 +157,25 @@ impl Highlighter { (theme, syntaxes) } - pub fn abort() { INCR.next(); } + fn load_syntax(&mut self) -> Result<()> { + let name = self.path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default(); + if let Some(s) = self.syntaxes.find_syntax_by_extension(&name) { + self.syntax = Some(s); + return Ok(()); + } - 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 ext = self.path.extension().map(|e| e.to_string_lossy()).unwrap_or_default(); + if let Some(s) = self.syntaxes.find_syntax_by_extension(&ext) { + self.syntax = Some(s); + return Ok(()); + } - let syntax = Self::find_syntax(&self.path, &mut reader).await; - let mut plain = syntax.is_err(); - - let mut before = Vec::with_capacity(if plain { 0 } else { skip }); - let mut after = Vec::with_capacity(size.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_and(|n| n > 0) { - if Self::is_binary(&buf, &mut inspected) { - return Err("Binary file".into()); - } - - let remaining = Self::normalize_control_chars(&mut buf); - if !plain && (remaining || buf.len() > 5000) { - plain = true; - before.clear(); - } - - i += if i >= skip { - after.push(String::from_utf8_lossy(&buf).into_owned()); - line_count(&**after.last().unwrap(), size.width, &indent, YAZI.preview.wrap) - } else if !plain { - before.push(String::from_utf8_lossy(&buf).into_owned()); - 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 - }; - - buf.clear(); - if i > skip + size.height as usize { - break; - } + if self.reader.read_until(b'\n', &mut buf).is_ok_and(|n| n > 0) { + self.reader.rewind()?; + self.syntax = self.syntaxes.find_syntax_by_first_line(&String::from_utf8_lossy(&buf)); } - - if skip > 0 && i < skip + size.height as usize { - return Err(PeekError::Exceed(i.saturating_sub(size.height as _))); - } - - Ok(if plain { - Text::from(Self::merge_highlight_lines(&after, YAZI.preview.tab_size)) - } else { - Self::highlight_with(before, after, syntax.unwrap()).await? - }) - } - - async fn highlight_with( - before: Vec, - after: Vec, - syntax: &'static SyntaxReference, - ) -> Result, PeekError> { - let ticket = INCR.current(); - - tokio::task::spawn_blocking(move || { - let (theme, syntaxes) = Self::init(); - let mut h = HighlightLines::new(syntax, theme); - - for line in before { - if ticket != INCR.current() { - return Err("Highlighting cancelled".into()); - } - h.highlight_line(&line, syntaxes).map_err(|e| anyhow!(e))?; - } - - let indent = YAZI.preview.indent(); - let mut lines = Vec::with_capacity(after.len()); - for line in after { - if ticket != INCR.current() { - return Err("Highlighting cancelled".into()); - } - - let regions = h.highlight_line(&line, syntaxes).map_err(|e| anyhow!(e))?; - lines.push(Self::to_line_widget(regions, &indent)); - } - - Ok(Text::from(lines)) - }) - .await? - } - - async fn find_syntax( - path: &Path, - reader: &mut BufReader, - ) -> Result<&'static SyntaxReference> { - let (_, syntaxes) = Self::init(); - let name = path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default(); - if let Some(s) = syntaxes.find_syntax_by_extension(&name) { - return Ok(s); - } - - let ext = path.extension().map(|e| e.to_string_lossy()).unwrap_or_default(); - if let Some(s) = syntaxes.find_syntax_by_extension(&ext) { - return Ok(s); - } - - let mut line = String::new(); - reader.read_line(&mut line).await?; - reader.rewind().await?; - syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found")) + Ok(()) } #[inline(always)] @@ -177,50 +205,35 @@ impl Highlighter { } remaining } - - fn merge_highlight_lines(s: &[String], tab_size: u8) -> String { - let mut buf = Vec::new(); - buf.reserve_exact(s.iter().map(|s| s.len()).sum::() | 15); - - for &b in s.iter().flat_map(|s| s.as_bytes()) { - push_printable_char(&mut buf, b, true, tab_size, false); - } - unsafe { String::from_utf8_unchecked(buf) } - } } impl Highlighter { - pub fn to_line_widget(regions: Vec<(highlighting::Style, &str)>, indent: &str) -> Line<'static> { - let spans: Vec<_> = regions - .into_iter() - .map(|(style, s)| { - let mut modifier = ratatui::style::Modifier::empty(); - if style.font_style.contains(highlighting::FontStyle::BOLD) { - modifier |= ratatui::style::Modifier::BOLD; - } - if style.font_style.contains(highlighting::FontStyle::ITALIC) { - modifier |= ratatui::style::Modifier::ITALIC; - } - if style.font_style.contains(highlighting::FontStyle::UNDERLINE) { - modifier |= ratatui::style::Modifier::UNDERLINED; - } + fn to_line_widget<'a>(regions: Vec<(highlighting::Style, &'a str)>) -> Line<'a> { + Line::from_iter(regions.into_iter().map(|(style, s)| { + let mut modifier = ratatui::style::Modifier::empty(); + if style.font_style.contains(highlighting::FontStyle::BOLD) { + modifier |= ratatui::style::Modifier::BOLD; + } + if style.font_style.contains(highlighting::FontStyle::ITALIC) { + modifier |= ratatui::style::Modifier::ITALIC; + } + if style.font_style.contains(highlighting::FontStyle::UNDERLINE) { + modifier |= ratatui::style::Modifier::UNDERLINED; + } - Span { - content: s.replace('\t', indent).into(), - style: ratatui::style::Style { - fg: Self::to_ansi_color(style.foreground), - // bg: Self::to_ansi_color(style.background), - add_modifier: modifier, - ..Default::default() - }, - } - }) - .collect(); - - Line::from(spans) + Span { + content: s.into(), + style: ratatui::style::Style { + fg: Self::to_ansi_color(style.foreground), + // bg: Self::to_ansi_color(style.background), + add_modifier: modifier, + ..Default::default() + }, + } + })) } - // Copy from https://github.com/sharkdp/bat/blob/master/src/terminal.rs + // Copied from https://github.com/sharkdp/bat/blob/master/src/terminal.rs fn to_ansi_color(color: highlighting::Color) -> Option { if color.a == 0 { // Themes can specify one of the user-configurable terminal colors by diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index 6e509f52..302588b1 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -16,7 +16,7 @@ impl Utils { let mut lock = PreviewLock::try_from(t)?; let path = lock.url.as_url().unified_path(); - let inner = match Highlighter::new(path).highlight(lock.skip, area.size()).await { + let inner = match Highlighter::oneshot(path, lock.skip, area.size()).await { Ok(text) => text, Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua), Err(e @ PeekError::Unexpected(_)) => { diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index 2a13e4f3..49dd1891 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -19,6 +19,10 @@ yazi-macro = { path = "../yazi-macro", version = "26.2.2" } crossterm = { workspace = true } ratatui = { workspace = true } twox-hash = { workspace = true } +unicode-width = { workspace = true } + +[dependencies.unicode-segmentation] +version = "1" [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-shim/src/ratatui/grapheme.rs b/yazi-shim/src/ratatui/grapheme.rs new file mode 100644 index 00000000..a2b8da4c --- /dev/null +++ b/yazi-shim/src/ratatui/grapheme.rs @@ -0,0 +1,47 @@ +// Copied from https://github.com/ratatui/ratatui/blob/main/ratatui-core/src/text/grapheme.rs +use std::borrow::Cow; + +use ratatui::style::{Style, Styled}; + +const NBSP: &str = "\u{00a0}"; +const ZWSP: &str = "\u{200b}"; + +/// A grapheme associated to a style. +/// Note that, although `StyledGrapheme` is the smallest divisible unit of text, +/// it actually is not a member of the text type hierarchy (`Text` -> `Line` -> +/// `Span`). It is a separate type used mostly for rendering purposes. A `Span` +/// consists of components that can be split into `StyledGrapheme`s, but it does +/// not contain a collection of `StyledGrapheme`s. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct StyledGrapheme<'a> { + pub symbol: Cow<'a, str>, + pub style: Style, +} + +impl<'a> StyledGrapheme<'a> { + /// Creates a new `StyledGrapheme` with the given symbol and style. + /// + /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], + /// [`Color`], or your own type that implements [`Into