perf(preview): add partial preview rendering and buffer preview requests (#285)

This commit is contained in:
Alex Pasmantier 2025-01-19 18:16:34 +01:00 committed by GitHub
parent c227b2a201
commit a3dc8196aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 723 additions and 244 deletions

51
Cargo.lock generated
View File

@ -223,6 +223,21 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -949,6 +964,16 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "fancy-regex"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
dependencies = [
"bit-set",
"regex",
]
[[package]] [[package]]
name = "faster-hex" name = "faster-hex"
version = "0.9.0" version = "0.9.0"
@ -2210,28 +2235,6 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags 1.3.2",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]] [[package]]
name = "oorandom" name = "oorandom"
version = "11.1.4" version = "11.1.4"
@ -2979,10 +2982,10 @@ checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1"
dependencies = [ dependencies = [
"bincode", "bincode",
"bitflags 1.3.2", "bitflags 1.3.2",
"fancy-regex",
"flate2", "flate2",
"fnv", "fnv",
"once_cell", "once_cell",
"onig",
"plist", "plist",
"regex-syntax 0.8.5", "regex-syntax 0.8.5",
"serde", "serde",
@ -3093,6 +3096,7 @@ name = "television-screen"
version = "0.0.21" version = "0.0.21"
dependencies = [ dependencies = [
"color-eyre", "color-eyre",
"devicons",
"ratatui", "ratatui",
"rustc-hash", "rustc-hash",
"serde", "serde",
@ -3115,6 +3119,7 @@ dependencies = [
"lazy_static", "lazy_static",
"rustc-hash", "rustc-hash",
"syntect", "syntect",
"tokio",
"tracing", "tracing",
"unicode-width 0.2.0", "unicode-width 0.2.0",
"winapi-util", "winapi-util",

View File

@ -56,13 +56,14 @@ readme = "README.md"
[workspace.dependencies] [workspace.dependencies]
directories = "5.0.1" directories = "5.0.1"
devicons = "0.6.11"
color-eyre = "0.6.3" color-eyre = "0.6.3"
lazy_static = "1.5.0" lazy_static = "1.5.0"
tokio = { version = "1.41.1", features = ["full"] } tokio = { version = "1.41.1", features = ["full"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
rustc-hash = "2.1.0" rustc-hash = "2.1.0"
syntect = "5.2.0" syntect = {version = "5.2.0", default-features = false, features = ["default-fancy"]}
unicode-width = "0.2.0" unicode-width = "0.2.0"
clap = { version = "4.5.20", features = ["derive", "cargo", "string"] } clap = { version = "4.5.20", features = ["derive", "cargo", "string"] }
serde = { version = "1.0.214", features = ["derive"] } serde = { version = "1.0.214", features = ["derive"] }
@ -104,7 +105,7 @@ copypasta = "0.10.1"
[dev-dependencies] [dev-dependencies]
criterion = "0.5.1" criterion = "0.5.1"
devicons = "0.6.11" devicons = { workspace = true }
[[bin]] [[bin]]
bench = false bench = false

View File

@ -20,6 +20,7 @@ television-derive = { path = "../television-derive", version = "0.0.21" }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true, features = ["rt"] } tokio = { workspace = true, features = ["rt"] }
clap = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] }
devicons = { workspace = true }
directories = { workspace = true } directories = { workspace = true }
color-eyre = { workspace = true } color-eyre = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
@ -27,7 +28,6 @@ lazy_static = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
devicons = "0.6.11"
ignore = "0.4.23" ignore = "0.4.23"
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] }
regex = "1.11.1" regex = "1.11.1"

View File

@ -17,6 +17,7 @@ television-channels = { path = "../television-channels", version = "0.0.21" }
television-utils = { path = "../television-utils", version = "0.0.21" } television-utils = { path = "../television-utils", version = "0.0.21" }
syntect = { workspace = true } syntect = { workspace = true }
devicons = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
color-eyre = { workspace = true } color-eyre = { workspace = true }
@ -24,7 +25,6 @@ lazy_static = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
parking_lot = "0.12.3" parking_lot = "0.12.3"
devicons = "0.6.11"
regex = "1.11.1" regex = "1.11.1"
nom = "7.1" nom = "7.1"
tui = { version = "0.29", default-features = false, package = "ratatui" } tui = { version = "0.29", default-features = false, package = "ratatui" }

View File

@ -19,23 +19,44 @@ pub use env::EnvVarPreviewer;
pub use env::EnvVarPreviewerConfig; pub use env::EnvVarPreviewerConfig;
pub use files::FilePreviewer; pub use files::FilePreviewer;
pub use files::FilePreviewerConfig; pub use files::FilePreviewerConfig;
use syntect::highlighting::Style; use television_utils::cache::RingSet;
use television_utils::syntax::HighlightedLines;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum PreviewContent { pub enum PreviewContent {
Empty, Empty,
FileTooLarge, FileTooLarge,
SyntectHighlightedText(Vec<Vec<(Style, String)>>), SyntectHighlightedText(HighlightedLines),
Loading, Loading,
Timeout,
NotSupported, NotSupported,
PlainText(Vec<String>), PlainText(Vec<String>),
PlainTextWrapped(String), PlainTextWrapped(String),
AnsiText(String), AnsiText(String),
} }
impl PreviewContent {
pub fn total_lines(&self) -> u16 {
match self {
PreviewContent::SyntectHighlightedText(hl_lines) => {
hl_lines.lines.len().try_into().unwrap_or(u16::MAX)
}
PreviewContent::PlainText(lines) => {
lines.len().try_into().unwrap_or(u16::MAX)
}
PreviewContent::AnsiText(text) => {
text.lines().count().try_into().unwrap_or(u16::MAX)
}
_ => 0,
}
}
}
pub const PREVIEW_NOT_SUPPORTED_MSG: &str = pub const PREVIEW_NOT_SUPPORTED_MSG: &str =
"Preview for this file type is not supported"; "Preview for this file type is not supported";
pub const FILE_TOO_LARGE_MSG: &str = "File too large"; pub const FILE_TOO_LARGE_MSG: &str = "File too large";
pub const LOADING_MSG: &str = "Loading...";
pub const TIMEOUT_MSG: &str = "Preview timed out";
/// A preview of an entry. /// A preview of an entry.
/// ///
@ -47,7 +68,10 @@ pub struct Preview {
pub title: String, pub title: String,
pub content: PreviewContent, pub content: PreviewContent,
pub icon: Option<FileIcon>, pub icon: Option<FileIcon>,
pub stale: bool, /// If the preview is partial, this field contains the byte offset
/// up to which the preview holds.
pub partial_offset: Option<usize>,
pub total_lines: u16,
} }
impl Default for Preview { impl Default for Preview {
@ -56,7 +80,8 @@ impl Default for Preview {
title: String::new(), title: String::new(),
content: PreviewContent::Empty, content: PreviewContent::Empty,
icon: None, icon: None,
stale: false, partial_offset: None,
total_lines: 0,
} }
} }
} }
@ -66,27 +91,22 @@ impl Preview {
title: String, title: String,
content: PreviewContent, content: PreviewContent,
icon: Option<FileIcon>, icon: Option<FileIcon>,
stale: bool, partial_offset: Option<usize>,
total_lines: u16,
) -> Self { ) -> Self {
Preview { Preview {
title, title,
content, content,
icon, icon,
stale, partial_offset,
} total_lines,
}
pub fn stale(&self) -> Self {
Preview {
stale: true,
..self.clone()
} }
} }
pub fn total_lines(&self) -> u16 { pub fn total_lines(&self) -> u16 {
match &self.content { match &self.content {
PreviewContent::SyntectHighlightedText(lines) => { PreviewContent::SyntectHighlightedText(hl_lines) => {
lines.len().try_into().unwrap_or(u16::MAX) hl_lines.lines.len().try_into().unwrap_or(u16::MAX)
} }
PreviewContent::PlainText(lines) => { PreviewContent::PlainText(lines) => {
lines.len().try_into().unwrap_or(u16::MAX) lines.len().try_into().unwrap_or(u16::MAX)
@ -105,6 +125,7 @@ pub struct Previewer {
file: FilePreviewer, file: FilePreviewer,
env_var: EnvVarPreviewer, env_var: EnvVarPreviewer,
command: CommandPreviewer, command: CommandPreviewer,
requests: RingSet<Entry>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -132,6 +153,8 @@ impl PreviewerConfig {
} }
} }
const REQUEST_STACK_SIZE: usize = 20;
impl Previewer { impl Previewer {
pub fn new(config: Option<PreviewerConfig>) -> Self { pub fn new(config: Option<PreviewerConfig>) -> Self {
let config = config.unwrap_or_default(); let config = config.unwrap_or_default();
@ -140,19 +163,45 @@ impl Previewer {
file: FilePreviewer::new(Some(config.file)), file: FilePreviewer::new(Some(config.file)),
env_var: EnvVarPreviewer::new(Some(config.env_var)), env_var: EnvVarPreviewer::new(Some(config.env_var)),
command: CommandPreviewer::new(Some(config.command)), command: CommandPreviewer::new(Some(config.command)),
requests: RingSet::with_capacity(REQUEST_STACK_SIZE),
} }
} }
pub fn preview(&mut self, entry: &Entry) -> Arc<Preview> { fn dispatch_request(&mut self, entry: &Entry) -> Option<Arc<Preview>> {
match &entry.preview_type { match &entry.preview_type {
PreviewType::Basic => self.basic.preview(entry), PreviewType::Basic => Some(self.basic.preview(entry)),
PreviewType::EnvVar => self.env_var.preview(entry), PreviewType::EnvVar => Some(self.env_var.preview(entry)),
PreviewType::Files => self.file.preview(entry), PreviewType::Files => self.file.preview(entry),
PreviewType::Command(cmd) => self.command.preview(entry, cmd), PreviewType::Command(cmd) => self.command.preview(entry, cmd),
PreviewType::None => Arc::new(Preview::default()), PreviewType::None => Some(Arc::new(Preview::default())),
} }
} }
fn cached(&self, entry: &Entry) -> Option<Arc<Preview>> {
match &entry.preview_type {
PreviewType::Files => self.file.cached(entry),
PreviewType::Command(_) => self.command.cached(entry),
PreviewType::Basic | PreviewType::EnvVar => None,
PreviewType::None => Some(Arc::new(Preview::default())),
}
}
pub fn preview(&mut self, entry: &Entry) -> Option<Arc<Preview>> {
// if we haven't acknowledged the request yet, acknowledge it
self.requests.push(entry.clone());
if let Some(preview) = self.dispatch_request(entry) {
return Some(preview);
}
// lookup request stack and return the most recent preview available
for request in self.requests.back_to_front() {
if let Some(preview) = self.cached(&request) {
return Some(preview);
}
}
None
}
pub fn set_config(&mut self, config: PreviewerConfig) { pub fn set_config(&mut self, config: PreviewerConfig) {
self.basic = BasicPreviewer::new(Some(config.basic)); self.basic = BasicPreviewer::new(Some(config.basic));
self.file = FilePreviewer::new(Some(config.file)); self.file = FilePreviewer::new(Some(config.file));

View File

@ -23,7 +23,8 @@ impl BasicPreviewer {
title: entry.name.clone(), title: entry.name.clone(),
content: PreviewContent::PlainTextWrapped(entry.name.clone()), content: PreviewContent::PlainTextWrapped(entry.name.clone()),
icon: entry.icon, icon: entry.icon,
..Default::default() partial_offset: None,
total_lines: 1,
}) })
} }
} }

View File

@ -16,7 +16,6 @@ pub struct CommandPreviewer {
cache: Arc<Mutex<PreviewCache>>, cache: Arc<Mutex<PreviewCache>>,
config: CommandPreviewerConfig, config: CommandPreviewerConfig,
concurrent_preview_tasks: Arc<AtomicU8>, concurrent_preview_tasks: Arc<AtomicU8>,
last_previewed: Arc<Mutex<Arc<Preview>>>,
in_flight_previews: Arc<Mutex<FxHashSet<String>>>, in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
} }
@ -53,54 +52,65 @@ impl CommandPreviewer {
cache: Arc::new(Mutex::new(PreviewCache::default())), cache: Arc::new(Mutex::new(PreviewCache::default())),
config, config,
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)), concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
last_previewed: Arc::new(Mutex::new(Arc::new(
Preview::default().stale(),
))),
in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())), in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())),
} }
} }
pub fn cached(&self, entry: &Entry) -> Option<Arc<Preview>> {
self.cache.lock().get(&entry.name)
}
pub fn preview( pub fn preview(
&mut self, &mut self,
entry: &Entry, entry: &Entry,
command: &PreviewCommand, command: &PreviewCommand,
) -> Arc<Preview> { ) -> Option<Arc<Preview>> {
// do we have a preview in cache for that entry? if let Some(preview) = self.cached(entry) {
if let Some(preview) = self.cache.lock().get(&entry.name) { Some(preview)
return preview.clone(); } else {
// preview is not in cache, spawn a task to compute the preview
debug!("Preview cache miss for {:?}", entry.name);
self.handle_preview_request(entry, command);
None
} }
debug!("Preview cache miss for {:?}", entry.name); }
// are we already computing a preview in the background for that entry? pub fn handle_preview_request(
&mut self,
entry: &Entry,
command: &PreviewCommand,
) {
if self.in_flight_previews.lock().contains(&entry.name) { if self.in_flight_previews.lock().contains(&entry.name) {
debug!("Preview already in flight for {:?}", entry.name); debug!("Preview already in flight for {:?}", entry.name);
return self.last_previewed.lock().clone(); return;
} }
if self.concurrent_preview_tasks.load(Ordering::Relaxed) if self.concurrent_preview_tasks.load(Ordering::Relaxed)
< MAX_CONCURRENT_PREVIEW_TASKS < MAX_CONCURRENT_PREVIEW_TASKS
{ {
self.in_flight_previews.lock().insert(entry.name.clone());
self.concurrent_preview_tasks self.concurrent_preview_tasks
.fetch_add(1, Ordering::Relaxed); .fetch_add(1, Ordering::Relaxed);
let cache = self.cache.clone(); let cache = self.cache.clone();
let entry_c = entry.clone(); let entry_c = entry.clone();
let concurrent_tasks = self.concurrent_preview_tasks.clone(); let concurrent_tasks = self.concurrent_preview_tasks.clone();
let command = command.clone(); let command = command.clone();
let last_previewed = self.last_previewed.clone(); let in_flight_previews = self.in_flight_previews.clone();
tokio::spawn(async move { tokio::spawn(async move {
try_preview( try_preview(
&command, &command,
&entry_c, &entry_c,
&cache, &cache,
&concurrent_tasks, &concurrent_tasks,
&last_previewed, &in_flight_previews,
); );
}); });
} else { } else {
debug!("Too many concurrent preview tasks running"); debug!(
"Too many concurrent preview tasks, skipping {:?}",
entry.name
);
} }
self.last_previewed.lock().clone()
} }
} }
@ -149,41 +159,42 @@ pub fn try_preview(
entry: &Entry, entry: &Entry,
cache: &Arc<Mutex<PreviewCache>>, cache: &Arc<Mutex<PreviewCache>>,
concurrent_tasks: &Arc<AtomicU8>, concurrent_tasks: &Arc<AtomicU8>,
last_previewed: &Arc<Mutex<Arc<Preview>>>, in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
) { ) {
debug!("Computing preview for {:?}", entry.name); debug!("Computing preview for {:?}", entry.name);
let command = format_command(command, entry); let command = format_command(command, entry);
debug!("Formatted preview command: {:?}", command); debug!("Formatted preview command: {:?}", command);
let output = shell_command() let child = shell_command()
.arg(&command) .arg(&command)
.output() .output()
.expect("failed to execute process"); .expect("failed to execute process");
if output.status.success() { if child.status.success() {
let content = String::from_utf8_lossy(&output.stdout); let content = String::from_utf8_lossy(&child.stdout);
let preview = Arc::new(Preview::new( let preview = Arc::new(Preview::new(
entry.name.clone(), entry.name.clone(),
PreviewContent::AnsiText(content.to_string()), PreviewContent::AnsiText(content.to_string()),
None, None,
false, None,
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
)); ));
cache.lock().insert(entry.name.clone(), &preview); cache.lock().insert(entry.name.clone(), &preview);
let mut tp = last_previewed.lock();
*tp = preview.stale().into();
} else { } else {
let content = String::from_utf8_lossy(&output.stderr); let content = String::from_utf8_lossy(&child.stderr);
let preview = Arc::new(Preview::new( let preview = Arc::new(Preview::new(
entry.name.clone(), entry.name.clone(),
PreviewContent::AnsiText(content.to_string()), PreviewContent::AnsiText(content.to_string()),
None, None,
false, None,
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
)); ));
cache.lock().insert(entry.name.clone(), &preview); cache.lock().insert(entry.name.clone(), &preview);
} }
concurrent_tasks.fetch_sub(1, Ordering::Relaxed); concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
in_flight_previews.lock().remove(&entry.name);
} }
#[cfg(test)] #[cfg(test)]

View File

@ -26,17 +26,22 @@ impl EnvVarPreviewer {
if let Some(preview) = self.cache.get(entry) { if let Some(preview) = self.cache.get(entry) {
return preview.clone(); return preview.clone();
} }
let content = entry.value.as_ref().map(|preview| {
maybe_add_newline_after_colon(preview, &entry.name)
});
let total_lines = content.as_ref().map_or_else(
|| 1,
|c| u16::try_from(c.lines().count()).unwrap_or(u16::MAX),
);
let preview = Arc::new(Preview { let preview = Arc::new(Preview {
title: entry.name.clone(), title: entry.name.clone(),
content: if let Some(preview) = &entry.value { content: match content {
PreviewContent::PlainTextWrapped( Some(content) => PreviewContent::PlainTextWrapped(content),
maybe_add_newline_after_colon(preview, &entry.name), None => PreviewContent::Empty,
)
} else {
PreviewContent::Empty
}, },
icon: entry.icon, icon: entry.icon,
..Default::default() partial_offset: None,
total_lines,
}); });
self.cache.insert(entry.clone(), preview.clone()); self.cache.insert(entry.clone(), preview.clone());
preview preview

View File

@ -1,4 +1,3 @@
use color_eyre::Result;
use parking_lot::Mutex; use parking_lot::Mutex;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use std::collections::HashSet; use std::collections::HashSet;
@ -9,6 +8,8 @@ use std::sync::{
atomic::{AtomicU8, Ordering}, atomic::{AtomicU8, Ordering},
Arc, Arc,
}; };
use television_utils::files::{read_into_lines_capped, ReadResult};
use television_utils::syntax::HighlightedLines;
use syntect::{highlighting::Theme, parsing::SyntaxSet}; use syntect::{highlighting::Theme, parsing::SyntaxSet};
use tracing::{debug, warn}; use tracing::{debug, warn};
@ -17,7 +18,7 @@ use super::cache::PreviewCache;
use crate::previewers::{meta, Preview, PreviewContent}; use crate::previewers::{meta, Preview, PreviewContent};
use television_channels::entry; use television_channels::entry;
use television_utils::{ use television_utils::{
files::{get_file_size, FileType}, files::FileType,
strings::preprocess_line, strings::preprocess_line,
syntax::{self, load_highlighting_assets, HighlightingAssetsExt}, syntax::{self, load_highlighting_assets, HighlightingAssetsExt},
}; };
@ -28,7 +29,6 @@ pub struct FilePreviewer {
pub syntax_set: Arc<SyntaxSet>, pub syntax_set: Arc<SyntaxSet>,
pub syntax_theme: Arc<Theme>, pub syntax_theme: Arc<Theme>,
concurrent_preview_tasks: Arc<AtomicU8>, concurrent_preview_tasks: Arc<AtomicU8>,
last_previewed: Arc<Mutex<Arc<Preview>>>,
in_flight_previews: Arc<Mutex<FxHashSet<String>>>, in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
} }
@ -43,10 +43,6 @@ impl FilePreviewerConfig {
} }
} }
/// The maximum file size that we will try to preview.
/// 4 MB
const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 3; const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 3;
const BAT_THEME_ENV_VAR: &str = "BAT_THEME"; const BAT_THEME_ENV_VAR: &str = "BAT_THEME";
@ -72,30 +68,41 @@ impl FilePreviewer {
syntax_set: Arc::new(syntax_set), syntax_set: Arc::new(syntax_set),
syntax_theme: Arc::new(theme), syntax_theme: Arc::new(theme),
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)), concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
last_previewed: Arc::new(Mutex::new(Arc::new(
Preview::default().stale(),
))),
in_flight_previews: Arc::new(Mutex::new(HashSet::with_hasher( in_flight_previews: Arc::new(Mutex::new(HashSet::with_hasher(
FxBuildHasher, FxBuildHasher,
))), ))),
} }
} }
/// Get a preview for a file entry. pub fn cached(&self, entry: &entry::Entry) -> Option<Arc<Preview>> {
/// self.cache.lock().get(&entry.name)
/// # Panics }
/// Panics if seeking to the start of the file fails.
pub fn preview(&mut self, entry: &entry::Entry) -> Arc<Preview> {
// do we have a preview in cache for that entry?
if let Some(preview) = self.cache.lock().get(&entry.name) {
return preview;
}
debug!("Preview cache miss for {:?}", entry.name);
// are we already computing a preview in the background for that entry? pub fn preview(&mut self, entry: &entry::Entry) -> Option<Arc<Preview>> {
if let Some(preview) = self.cached(entry) {
debug!("Preview cache hit for {:?}", entry.name);
if preview.partial_offset.is_some() {
// preview is partial, spawn a task to compute the next chunk
// and return the partial preview
debug!("Spawning partial preview task for {:?}", entry.name);
self.handle_preview_request(entry, Some(preview.clone()));
}
Some(preview)
} else {
// preview is not in cache, spawn a task to compute the preview
debug!("Preview cache miss for {:?}", entry.name);
self.handle_preview_request(entry, None);
None
}
}
pub fn handle_preview_request(
&mut self,
entry: &entry::Entry,
partial_preview: Option<Arc<Preview>>,
) {
if self.in_flight_previews.lock().contains(&entry.name) { if self.in_flight_previews.lock().contains(&entry.name) {
debug!("Preview already in flight for {:?}", entry.name); debug!("Preview already in flight for {:?}", entry.name);
return self.last_previewed.lock().clone();
} }
if self.concurrent_preview_tasks.load(Ordering::Relaxed) if self.concurrent_preview_tasks.load(Ordering::Relaxed)
@ -109,22 +116,19 @@ impl FilePreviewer {
let syntax_set = self.syntax_set.clone(); let syntax_set = self.syntax_set.clone();
let syntax_theme = self.syntax_theme.clone(); let syntax_theme = self.syntax_theme.clone();
let concurrent_tasks = self.concurrent_preview_tasks.clone(); let concurrent_tasks = self.concurrent_preview_tasks.clone();
let last_previewed = self.last_previewed.clone();
let in_flight_previews = self.in_flight_previews.clone(); let in_flight_previews = self.in_flight_previews.clone();
tokio::spawn(async move { tokio::spawn(async move {
try_preview( try_preview(
&entry_c, &entry_c,
partial_preview,
&cache, &cache,
&syntax_set, &syntax_set,
&syntax_theme, &syntax_theme,
&concurrent_tasks, &concurrent_tasks,
&last_previewed,
&in_flight_previews, &in_flight_previews,
); );
}); });
} }
self.last_previewed.lock().clone()
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -133,41 +137,98 @@ impl FilePreviewer {
} }
} }
/// The size of the buffer used to read the file in bytes.
/// This ends up being the max size of partial previews.
const PARTIAL_BUFREAD_SIZE: usize = 16 * 1024;
pub fn try_preview( pub fn try_preview(
entry: &entry::Entry, entry: &entry::Entry,
partial_preview: Option<Arc<Preview>>,
cache: &Arc<Mutex<PreviewCache>>, cache: &Arc<Mutex<PreviewCache>>,
syntax_set: &Arc<SyntaxSet>, syntax_set: &Arc<SyntaxSet>,
syntax_theme: &Arc<Theme>, syntax_theme: &Arc<Theme>,
concurrent_tasks: &Arc<AtomicU8>, concurrent_tasks: &Arc<AtomicU8>,
last_previewed: &Arc<Mutex<Arc<Preview>>>,
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>, in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
) { ) {
debug!("Computing preview for {:?}", entry.name); debug!("Computing preview for {:?}", entry.name);
let path = PathBuf::from(&entry.name); let path = PathBuf::from(&entry.name);
// check file size
if get_file_size(&path).map_or(false, |s| s > MAX_FILE_SIZE) {
debug!("File too large: {:?}", entry.name);
let preview = meta::file_too_large(&entry.name);
cache.lock().insert(entry.name.clone(), &preview);
}
if matches!(FileType::from(&path), FileType::Text) { // if we're dealing with a partial preview, no need to re-check for textual content
if partial_preview.is_some()
|| matches!(FileType::from(&path), FileType::Text)
{
debug!("File is text-based: {:?}", entry.name); debug!("File is text-based: {:?}", entry.name);
match File::open(path) { match File::open(path) {
Ok(file) => { Ok(mut file) => {
// if we're dealing with a partial preview, seek to the provided offset
// and use the previous state to compute the next chunk of the preview
let cached_lines = if let Some(p) = partial_preview {
if let PreviewContent::SyntectHighlightedText(hl) =
&p.content
{
let _ = file.seek(std::io::SeekFrom::Start(
// this is always Some in this case
p.partial_offset.unwrap() as u64,
));
Some(hl.clone())
} else {
None
}
} else {
None
};
// compute the highlighted version in the background // compute the highlighted version in the background
let mut reader = BufReader::new(file); match read_into_lines_capped(file, PARTIAL_BUFREAD_SIZE) {
reader.seek(std::io::SeekFrom::Start(0)).unwrap(); ReadResult::Full(lines) => {
let preview = compute_highlighted_text_preview( if let Some(content) = compute_highlighted_text_preview(
entry, entry,
reader, &lines
syntax_set, .iter()
syntax_theme, .map(|l| preprocess_line(l).0 + "\n")
); .collect::<Vec<_>>(),
cache.lock().insert(entry.name.clone(), &preview); syntax_set,
in_flight_previews.lock().remove(&entry.name); syntax_theme,
let mut tp = last_previewed.lock(); cached_lines,
*tp = preview.stale().into(); ) {
let total_lines = content.total_lines();
let preview = Arc::new(Preview::new(
entry.name.clone(),
content,
entry.icon,
None,
total_lines,
));
cache.lock().insert(entry.name.clone(), &preview);
}
}
ReadResult::Partial(p) => {
if let Some(content) = compute_highlighted_text_preview(
entry,
&p.lines
.iter()
.map(|l| preprocess_line(l).0 + "\n")
.collect::<Vec<_>>(),
syntax_set,
syntax_theme,
cached_lines,
) {
let total_lines = content.total_lines();
let preview = Arc::new(Preview::new(
entry.name.clone(),
content,
entry.icon,
Some(p.bytes_read),
total_lines,
));
cache.lock().insert(entry.name.clone(), &preview);
}
}
ReadResult::Error(e) => {
warn!("Error reading file: {:?}", e);
let p = meta::not_supported(&entry.name);
cache.lock().insert(entry.name.clone(), &p);
}
}
} }
Err(e) => { Err(e) => {
warn!("Error opening file: {:?}", e); warn!("Error opening file: {:?}", e);
@ -181,44 +242,34 @@ pub fn try_preview(
cache.lock().insert(entry.name.clone(), &preview); cache.lock().insert(entry.name.clone(), &preview);
} }
concurrent_tasks.fetch_sub(1, Ordering::Relaxed); concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
in_flight_previews.lock().remove(&entry.name);
} }
fn compute_highlighted_text_preview( fn compute_highlighted_text_preview(
entry: &entry::Entry, entry: &entry::Entry,
reader: BufReader<File>, lines: &[String],
syntax_set: &SyntaxSet, syntax_set: &SyntaxSet,
syntax_theme: &Theme, syntax_theme: &Theme,
) -> Arc<Preview> { previous_lines: Option<HighlightedLines>,
) -> Option<PreviewContent> {
debug!( debug!(
"Computing highlights in the background for {:?}", "Computing highlights in the background for {:?}",
entry.name entry.name
); );
let lines: Vec<String> = reader
.lines()
.map_while(Result::ok)
// we need to add a newline here because sublime syntaxes expect one
// to be present at the end of each line
.map(|line| preprocess_line(&line).0 + "\n")
.collect();
match syntax::compute_highlights_for_path( match syntax::compute_highlights_incremental(
&PathBuf::from(&entry.name), &PathBuf::from(&entry.name),
lines, lines,
syntax_set, syntax_set,
syntax_theme, syntax_theme,
previous_lines,
) { ) {
Ok(highlighted_lines) => { Ok(highlighted_lines) => {
debug!("Successfully computed highlights for {:?}", entry.name); Some(PreviewContent::SyntectHighlightedText(highlighted_lines))
Arc::new(Preview::new(
entry.name.clone(),
PreviewContent::SyntectHighlightedText(highlighted_lines),
entry.icon,
false,
))
} }
Err(e) => { Err(e) => {
warn!("Error computing highlights: {:?}", e); warn!("Error computing highlights: {:?}", e);
meta::not_supported(&entry.name) None
} }
} }
} }
@ -244,10 +295,12 @@ fn plain_text_preview(title: &str, reader: BufReader<&File>) -> Arc<Preview> {
break; break;
} }
} }
let total_lines = u16::try_from(lines.len()).unwrap_or(u16::MAX);
Arc::new(Preview::new( Arc::new(Preview::new(
title.to_string(), title.to_string(),
PreviewContent::PlainText(lines), PreviewContent::PlainText(lines),
None, None,
false, None,
total_lines,
)) ))
} }

View File

@ -6,7 +6,8 @@ pub fn not_supported(title: &str) -> Arc<Preview> {
title.to_string(), title.to_string(),
PreviewContent::NotSupported, PreviewContent::NotSupported,
None, None,
false, None,
1,
)) ))
} }
@ -15,7 +16,8 @@ pub fn file_too_large(title: &str) -> Arc<Preview> {
title.to_string(), title.to_string(),
PreviewContent::FileTooLarge, PreviewContent::FileTooLarge,
None, None,
false, None,
1,
)) ))
} }
@ -25,6 +27,17 @@ pub fn loading(title: &str) -> Arc<Preview> {
title.to_string(), title.to_string(),
PreviewContent::Loading, PreviewContent::Loading,
None, None,
false, None,
1,
))
}
pub fn timeout(title: &str) -> Arc<Preview> {
Arc::new(Preview::new(
title.to_string(),
PreviewContent::Timeout,
None,
None,
1,
)) ))
} }

View File

@ -23,6 +23,7 @@ color-eyre = { workspace = true }
syntect = { workspace = true } syntect = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
devicons = { workspace = true }
[lints] [lints]
workspace = true workspace = true

View File

@ -1,15 +1,41 @@
use devicons::FileIcon;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::sync::Arc; use std::sync::Arc;
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
use television_utils::cache::RingSet; use television_utils::cache::RingSet;
const DEFAULT_RENDERED_PREVIEW_CACHE_SIZE: usize = 25; const DEFAULT_RENDERED_PREVIEW_CACHE_SIZE: usize = 10;
#[derive(Clone, Debug)]
pub struct CachedPreview<'a> {
pub key: String,
pub icon: Option<FileIcon>,
pub title: String,
pub paragraph: Arc<Paragraph<'a>>,
}
impl<'a> CachedPreview<'a> {
pub fn new(
key: String,
icon: Option<FileIcon>,
title: String,
paragraph: Arc<Paragraph<'a>>,
) -> Self {
CachedPreview {
key,
icon,
title,
paragraph,
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct RenderedPreviewCache<'a> { pub struct RenderedPreviewCache<'a> {
previews: FxHashMap<String, Arc<Paragraph<'a>>>, previews: FxHashMap<String, CachedPreview<'a>>,
ring_set: RingSet<String>, ring_set: RingSet<String>,
pub last_preview: Option<CachedPreview<'a>>,
} }
impl<'a> RenderedPreviewCache<'a> { impl<'a> RenderedPreviewCache<'a> {
@ -17,15 +43,29 @@ impl<'a> RenderedPreviewCache<'a> {
RenderedPreviewCache { RenderedPreviewCache {
previews: FxHashMap::default(), previews: FxHashMap::default(),
ring_set: RingSet::with_capacity(capacity), ring_set: RingSet::with_capacity(capacity),
last_preview: None,
} }
} }
pub fn get(&self, key: &str) -> Option<Arc<Paragraph<'a>>> { pub fn get(&self, key: &str) -> Option<CachedPreview<'a>> {
self.previews.get(key).cloned() self.previews.get(key).cloned()
} }
pub fn insert(&mut self, key: String, preview: &Arc<Paragraph<'a>>) { pub fn insert(
self.previews.insert(key.clone(), preview.clone()); &mut self,
key: String,
icon: Option<FileIcon>,
title: &str,
paragraph: &Arc<Paragraph<'a>>,
) {
let cached_preview = CachedPreview::new(
key.clone(),
icon,
title.to_string(),
paragraph.clone(),
);
self.last_preview = Some(cached_preview.clone());
self.previews.insert(key.clone(), cached_preview);
if let Some(oldest_key) = self.ring_set.push(key) { if let Some(oldest_key) = self.ring_set.push(key) {
self.previews.remove(&oldest_key); self.previews.remove(&oldest_key);
} }

View File

@ -3,6 +3,7 @@ use crate::{
colors::{Colorscheme, PreviewColorscheme}, colors::{Colorscheme, PreviewColorscheme},
}; };
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use devicons::FileIcon;
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap}; use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
use ratatui::Frame; use ratatui::Frame;
use ratatui::{ use ratatui::{
@ -15,7 +16,8 @@ use television_channels::entry::Entry;
use television_previewers::{ use television_previewers::{
ansi::IntoText, ansi::IntoText,
previewers::{ previewers::{
Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG, Preview, PreviewContent, FILE_TOO_LARGE_MSG, LOADING_MSG,
PREVIEW_NOT_SUPPORTED_MSG, TIMEOUT_MSG,
}, },
}; };
use television_utils::strings::{ use television_utils::strings::{
@ -28,14 +30,20 @@ const FILL_CHAR_SLANTED: char = '';
const FILL_CHAR_EMPTY: char = ' '; const FILL_CHAR_EMPTY: char = ' ';
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn build_preview_paragraph( pub fn build_preview_paragraph<'a>(
preview_block: Block<'_>,
inner: Rect, inner: Rect,
preview_content: PreviewContent, preview_content: PreviewContent,
target_line: Option<u16>, target_line: Option<u16>,
preview_scroll: u16, preview_scroll: u16,
colorscheme: Colorscheme, colorscheme: Colorscheme,
) -> Paragraph<'_> { ) -> Paragraph<'a> {
let preview_block =
Block::default().style(Style::default()).padding(Padding {
top: 0,
right: 1,
bottom: 0,
left: 1,
});
match preview_content { match preview_content {
PreviewContent::AnsiText(text) => { PreviewContent::AnsiText(text) => {
build_ansi_text_paragraph(text, preview_block, preview_scroll) build_ansi_text_paragraph(text, preview_block, preview_scroll)
@ -56,7 +64,7 @@ pub fn build_preview_paragraph(
} }
PreviewContent::SyntectHighlightedText(highlighted_lines) => { PreviewContent::SyntectHighlightedText(highlighted_lines) => {
build_syntect_highlighted_paragraph( build_syntect_highlighted_paragraph(
highlighted_lines, highlighted_lines.lines,
preview_block, preview_block,
target_line, target_line,
preview_scroll, preview_scroll,
@ -65,7 +73,7 @@ pub fn build_preview_paragraph(
} }
// meta // meta
PreviewContent::Loading => { PreviewContent::Loading => {
build_meta_preview_paragraph(inner, "Loading...", FILL_CHAR_EMPTY) build_meta_preview_paragraph(inner, LOADING_MSG, FILL_CHAR_EMPTY)
.block(preview_block) .block(preview_block)
.alignment(Alignment::Left) .alignment(Alignment::Left)
.style(Style::default().add_modifier(Modifier::ITALIC)) .style(Style::default().add_modifier(Modifier::ITALIC))
@ -86,28 +94,48 @@ pub fn build_preview_paragraph(
.block(preview_block) .block(preview_block)
.alignment(Alignment::Left) .alignment(Alignment::Left)
.style(Style::default().add_modifier(Modifier::ITALIC)), .style(Style::default().add_modifier(Modifier::ITALIC)),
PreviewContent::Timeout => {
build_meta_preview_paragraph(inner, TIMEOUT_MSG, FILL_CHAR_EMPTY)
}
.block(preview_block)
.alignment(Alignment::Left)
.style(Style::default().add_modifier(Modifier::ITALIC)),
PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)), PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)),
} }
} }
const ANSI_BEFORE_CONTEXT_SIZE: u16 = 10;
const ANSI_CONTEXT_SIZE: usize = 150;
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn build_ansi_text_paragraph( fn build_ansi_text_paragraph(
text: String, text: String,
preview_block: Block, preview_block: Block,
preview_scroll: u16, preview_scroll: u16,
) -> Paragraph { ) -> Paragraph {
let text = replace_non_printable( let lines = text.lines();
text.as_bytes(), let skip =
&ReplaceNonPrintableConfig { preview_scroll.saturating_sub(ANSI_BEFORE_CONTEXT_SIZE) as usize;
replace_line_feed: false, let context = lines
replace_control_characters: false, .skip(skip)
..Default::default() .take(ANSI_CONTEXT_SIZE)
}, .collect::<Vec<_>>()
) .join("\n");
.0
.into_text() let mut text = "\n".repeat(skip);
.unwrap(); text.push_str(
Paragraph::new(text) &replace_non_printable(
context.as_bytes(),
&ReplaceNonPrintableConfig {
replace_line_feed: false,
replace_control_characters: false,
..Default::default()
},
)
.0,
);
Paragraph::new(text.into_text().unwrap())
.block(preview_block) .block(preview_block)
.scroll((preview_scroll, 0)) .scroll((preview_scroll, 0))
} }
@ -244,20 +272,18 @@ pub fn build_meta_preview_paragraph<'a>(
Paragraph::new(Text::from(lines)) Paragraph::new(Text::from(lines))
} }
#[allow(clippy::too_many_arguments)] fn draw_content_outer_block(
pub fn draw_preview_content_block(
f: &mut Frame, f: &mut Frame,
rect: Rect, rect: Rect,
entry: &Entry,
preview: &Arc<Preview>,
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
preview_scroll: u16,
use_nerd_font_icons: bool,
colorscheme: &Colorscheme, colorscheme: &Colorscheme,
) -> Result<()> { icon: Option<FileIcon>,
title: &str,
use_nerd_font_icons: bool,
) -> Result<Rect> {
let mut preview_title_spans = vec![Span::from(" ")]; let mut preview_title_spans = vec![Span::from(" ")];
if preview.icon.is_some() && use_nerd_font_icons { // optional icon
let icon = preview.icon.as_ref().unwrap(); if icon.is_some() && use_nerd_font_icons {
let icon = icon.as_ref().unwrap();
preview_title_spans.push(Span::styled( preview_title_spans.push(Span::styled(
{ {
let mut icon_str = String::from(icon.icon); let mut icon_str = String::from(icon.icon);
@ -267,10 +293,11 @@ pub fn draw_preview_content_block(
Style::default().fg(Color::from_str(icon.color)?), Style::default().fg(Color::from_str(icon.color)?),
)); ));
} }
// preview title
preview_title_spans.push(Span::styled( preview_title_spans.push(Span::styled(
shrink_with_ellipsis( shrink_with_ellipsis(
&replace_non_printable( &replace_non_printable(
preview.title.as_bytes(), title.as_bytes(),
&ReplaceNonPrintableConfig::default(), &ReplaceNonPrintableConfig::default(),
) )
.0, .0,
@ -279,6 +306,8 @@ pub fn draw_preview_content_block(
Style::default().fg(colorscheme.preview.title_fg).bold(), Style::default().fg(colorscheme.preview.title_fg).bold(),
)); ));
preview_title_spans.push(Span::from(" ")); preview_title_spans.push(Span::from(" "));
// build the preview block
let preview_outer_block = Block::default() let preview_outer_block = Block::default()
.title_top( .title_top(
Line::from(preview_title_spans) Line::from(preview_title_spans)
@ -294,47 +323,111 @@ pub fn draw_preview_content_block(
) )
.padding(Padding::new(0, 1, 1, 0)); .padding(Padding::new(0, 1, 1, 0));
let preview_inner_block =
Block::default().style(Style::default()).padding(Padding {
top: 0,
right: 1,
bottom: 0,
left: 1,
});
let inner = preview_outer_block.inner(rect); let inner = preview_outer_block.inner(rect);
f.render_widget(preview_outer_block, rect); f.render_widget(preview_outer_block, rect);
Ok(inner)
}
let target_line = entry.line_number.map(|l| u16::try_from(l).unwrap_or(0)); #[allow(clippy::too_many_arguments)]
let cache_key = compute_cache_key(entry); pub fn draw_preview_content_block(
f: &mut Frame,
rect: Rect,
entry: &Entry,
preview: &Option<Arc<Preview>>,
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
preview_scroll: u16,
use_nerd_font_icons: bool,
colorscheme: &Colorscheme,
) -> Result<()> {
if let Some(preview) = preview {
let inner = draw_content_outer_block(
f,
rect,
colorscheme,
preview.icon,
&preview.title,
use_nerd_font_icons,
)?;
// Check if the rendered preview content is already in the cache // check if the rendered preview content is already in the cache
if let Some(preview_paragraph) = let cache_key = compute_cache_key(entry);
rendered_preview_cache.lock().unwrap().get(&cache_key) if let Some(rp) =
{ rendered_preview_cache.lock().unwrap().get(&cache_key)
let p = preview_paragraph.as_ref().clone(); {
f.render_widget(p.scroll((preview_scroll, 0)), inner); // we got a hit, render the cached preview content
let p = rp.paragraph.as_ref().clone();
f.render_widget(p.scroll((preview_scroll, 0)), inner);
return Ok(());
}
// render the preview content and cache it
let rp = build_preview_paragraph(
//preview_inner_block,
inner,
preview.content.clone(),
entry.line_number.map(|l| u16::try_from(l).unwrap_or(0)),
preview_scroll,
colorscheme.clone(),
);
// only cache the preview content if it's not a partial preview
// and the preview title matches the entry name
if preview.partial_offset.is_none() && preview.title == entry.name {
rendered_preview_cache.lock().unwrap().insert(
cache_key,
preview.icon,
&preview.title,
&Arc::new(rp.clone()),
);
}
f.render_widget(rp.scroll((preview_scroll, 0)), inner);
return Ok(()); return Ok(());
} }
// If not, render the preview content and cache it if not empty // else if last_preview exists
let c_scheme = colorscheme.clone(); if let Some(last_preview) =
let rp = build_preview_paragraph( &rendered_preview_cache.lock().unwrap().last_preview
preview_inner_block, {
inner, let inner = draw_content_outer_block(
preview.content.clone(), f,
target_line, rect,
preview_scroll, colorscheme,
c_scheme, last_preview.icon,
); &last_preview.title,
if !preview.stale { use_nerd_font_icons,
rendered_preview_cache )?;
.lock()
.unwrap() f.render_widget(
.insert(cache_key, &Arc::new(rp.clone())); last_preview
.paragraph
.as_ref()
.clone()
.scroll((preview_scroll, 0)),
inner,
);
return Ok(());
} }
f.render_widget( // otherwise render empty preview
Arc::new(rp).as_ref().clone().scroll((preview_scroll, 0)), let inner = draw_content_outer_block(
inner, f,
); rect,
colorscheme,
None,
"",
use_nerd_font_icons,
)?;
let preview_outer_block = Block::default()
.title_top(Line::from(Span::styled(
" Preview ",
Style::default().fg(colorscheme.preview.title_fg),
)))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(colorscheme.general.border_fg))
.style(
Style::default()
.bg(colorscheme.general.background.unwrap_or_default()),
)
.padding(Padding::new(0, 1, 1, 0));
f.render_widget(preview_outer_block, inner);
Ok(()) Ok(())
} }

View File

@ -20,10 +20,11 @@ directories = { workspace = true }
syntect = { workspace = true } syntect = { workspace = true }
unicode-width = { workspace = true } unicode-width = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
tokio = { workspace = true }
ignore = "0.4.23" ignore = "0.4.23"
bat = { version = "0.24.0", default-features = false, features = [ bat = { version = "0.24.0", default-features = false, features = [
"regex-onig", "regex-fancy",
] } ] }
gag = "1.0.0" gag = "1.0.0"

View File

@ -48,6 +48,17 @@ pub struct RingSet<T> {
capacity: usize, capacity: usize,
} }
const DEFAULT_CAPACITY: usize = 20;
impl<T> Default for RingSet<T>
where
T: Eq + std::hash::Hash + Clone + std::fmt::Debug,
{
fn default() -> Self {
RingSet::with_capacity(DEFAULT_CAPACITY)
}
}
impl<T> RingSet<T> impl<T> RingSet<T>
where where
T: Eq + std::hash::Hash + Clone + std::fmt::Debug, T: Eq + std::hash::Hash + Clone + std::fmt::Debug,
@ -97,6 +108,11 @@ where
pub fn contains(&self, key: &T) -> bool { pub fn contains(&self, key: &T) -> bool {
self.known_keys.contains(key) self.known_keys.contains(key)
} }
/// Returns an iterator that goes from the back to the front of the buffer.
pub fn back_to_front(&self) -> impl Iterator<Item = T> {
self.ring_buffer.clone().into_iter().rev()
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,6 +1,8 @@
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use std::fmt::Debug; use std::fmt::Debug;
use std::fs::File; use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@ -14,6 +16,51 @@ use crate::strings::{
}; };
use crate::threads::default_num_threads; use crate::threads::default_num_threads;
pub struct PartialReadResult {
pub lines: Vec<String>,
pub bytes_read: usize,
}
pub enum ReadResult {
Partial(PartialReadResult),
Full(Vec<String>),
Error(String),
}
pub fn read_into_lines_capped<R>(r: R, max_bytes: usize) -> ReadResult
where
R: Read,
{
let mut buf_reader = BufReader::new(r);
let mut line = String::new();
let mut lines = Vec::new();
let mut bytes_read = 0;
loop {
line.clear();
match buf_reader.read_line(&mut line) {
Ok(0) => break,
Ok(_) => {
if bytes_read > max_bytes {
break;
}
lines.push(line.trim_end().to_string());
bytes_read += line.len();
}
Err(e) => {
warn!("Error reading file: {:?}", e);
return ReadResult::Error(format!("{e:?}"));
}
}
}
if bytes_read > max_bytes {
ReadResult::Partial(PartialReadResult { lines, bytes_read })
} else {
ReadResult::Full(lines)
}
}
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into(); pub static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into();
} }

View File

@ -224,7 +224,7 @@ pub fn replace_non_printable(
input: &[u8], input: &[u8],
config: &ReplaceNonPrintableConfig, config: &ReplaceNonPrintableConfig,
) -> (String, Vec<i16>) { ) -> (String, Vec<i16>) {
let mut output = String::new(); let mut output = String::with_capacity(input.len());
let mut offsets = Vec::new(); let mut offsets = Vec::new();
let mut cumulative_offset: i16 = 0; let mut cumulative_offset: i16 = 0;

View File

@ -1,31 +1,95 @@
use bat::assets::HighlightingAssets; use bat::assets::HighlightingAssets;
use color_eyre::Result;
use gag::Gag; use gag::Gag;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, Theme}; use syntect::highlighting::{
use syntect::parsing::SyntaxSet; HighlightIterator, HighlightState, Highlighter, Style, Theme,
};
use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
use tracing::warn; use tracing::warn;
#[derive(Debug, Clone)]
pub struct HighlightingState {
parse_state: ParseState,
highlight_state: HighlightState,
}
impl HighlightingState {
pub fn new(
parse_state: ParseState,
highlight_state: HighlightState,
) -> Self {
Self {
parse_state,
highlight_state,
}
}
}
struct LineHighlighter<'a> {
highlighter: Highlighter<'a>,
pub parse_state: ParseState,
pub highlight_state: HighlightState,
}
impl<'a> LineHighlighter<'a> {
pub fn new(
syntax: &SyntaxReference,
theme: &'a Theme,
) -> LineHighlighter<'a> {
let highlighter = Highlighter::new(theme);
let highlight_state =
HighlightState::new(&highlighter, ScopeStack::new());
Self {
highlighter,
parse_state: ParseState::new(syntax),
highlight_state,
}
}
pub fn from_state(
state: HighlightingState,
theme: &'a Theme,
) -> LineHighlighter<'a> {
Self {
highlighter: Highlighter::new(theme),
parse_state: state.parse_state,
highlight_state: state.highlight_state,
}
}
/// Highlights a line of a file
pub fn highlight_line<'b>(
&mut self,
line: &'b str,
syntax_set: &SyntaxSet,
) -> Result<Vec<(Style, &'b str)>, syntect::Error> {
let ops = self.parse_state.parse_line(line, syntax_set)?;
let iter = HighlightIterator::new(
&mut self.highlight_state,
&ops[..],
line,
&self.highlighter,
);
Ok(iter.collect())
}
}
#[deprecated(
note = "Use `compute_highlights_incremental` instead, which also returns the state"
)]
pub fn compute_highlights_for_path( pub fn compute_highlights_for_path(
file_path: &Path, file_path: &Path,
lines: Vec<String>, lines: &[String],
syntax_set: &SyntaxSet, syntax_set: &SyntaxSet,
syntax_theme: &Theme, syntax_theme: &Theme,
) -> color_eyre::Result<Vec<Vec<(Style, String)>>> { ) -> Result<Vec<Vec<(Style, String)>>> {
let syntax = let syntax = set_syntax_set(syntax_set, file_path);
syntax_set
.find_syntax_for_file(file_path)?
.unwrap_or_else(|| {
warn!(
"No syntax found for {:?}, defaulting to plain text",
file_path
);
syntax_set.find_syntax_plain_text()
});
let mut highlighter = HighlightLines::new(syntax, syntax_theme); let mut highlighter = HighlightLines::new(syntax, syntax_theme);
let mut highlighted_lines = Vec::new(); let mut highlighted_lines = Vec::new();
for line in lines { for line in lines {
let hl_regions = highlighter.highlight_line(&line, syntax_set)?; let hl_regions = highlighter.highlight_line(line, syntax_set)?;
highlighted_lines.push( highlighted_lines.push(
hl_regions hl_regions
.iter() .iter()
@ -36,13 +100,86 @@ pub fn compute_highlights_for_path(
Ok(highlighted_lines) Ok(highlighted_lines)
} }
fn set_syntax_set<'a>(
syntax_set: &'a SyntaxSet,
file_path: &Path,
) -> &'a SyntaxReference {
syntax_set
.find_syntax_for_file(file_path)
.unwrap_or(None)
.unwrap_or_else(|| {
warn!(
"No syntax found for {:?}, defaulting to plain text",
file_path
);
syntax_set.find_syntax_plain_text()
})
}
#[derive(Debug, Clone)]
pub struct HighlightedLines {
pub lines: Vec<Vec<(Style, String)>>,
pub state: Option<HighlightingState>,
}
impl HighlightedLines {
pub fn new(
lines: Vec<Vec<(Style, String)>>,
state: Option<HighlightingState>,
) -> Self {
Self { lines, state }
}
}
pub fn compute_highlights_incremental(
file_path: &Path,
lines: &[String],
syntax_set: &SyntaxSet,
syntax_theme: &Theme,
cached_lines: Option<HighlightedLines>,
) -> Result<HighlightedLines> {
let mut highlighted_lines: Vec<_>;
let mut highlighter: LineHighlighter;
if let Some(HighlightedLines {
lines: c_lines,
state: Some(s),
}) = cached_lines
{
highlighter = LineHighlighter::from_state(s, syntax_theme);
highlighted_lines = c_lines;
} else {
let syntax = set_syntax_set(syntax_set, file_path);
highlighter = LineHighlighter::new(syntax, syntax_theme);
highlighted_lines = Vec::new();
};
for line in lines {
let hl_regions = highlighter.highlight_line(line, syntax_set)?;
highlighted_lines.push(
hl_regions
.iter()
.map(|(style, text)| (*style, (*text).to_string()))
.collect(),
);
}
Ok(HighlightedLines::new(
highlighted_lines,
Some(HighlightingState::new(
highlighter.parse_state.clone(),
highlighter.highlight_state.clone(),
)),
))
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn compute_highlights_for_line<'a>( pub fn compute_highlights_for_line<'a>(
line: &'a str, line: &'a str,
syntax_set: &SyntaxSet, syntax_set: &SyntaxSet,
syntax_theme: &Theme, syntax_theme: &Theme,
file_path: &str, file_path: &str,
) -> color_eyre::Result<Vec<(Style, &'a str)>> { ) -> Result<Vec<(Style, &'a str)>> {
let syntax = syntax_set.find_syntax_for_file(file_path)?; let syntax = syntax_set.find_syntax_for_file(file_path)?;
match syntax { match syntax {
None => { None => {

View File

@ -560,20 +560,26 @@ impl Television {
&& !matches!(selected_entry.preview_type, PreviewType::None) && !matches!(selected_entry.preview_type, PreviewType::None)
{ {
// preview content // preview content
let preview = self.previewer.preview(&selected_entry); let maybe_preview = self.previewer.preview(&selected_entry);
self.current_preview_total_lines = preview.total_lines();
// initialize preview scroll let _ = self.previewer.preview(&selected_entry);
self.maybe_init_preview_scroll(
selected_entry if let Some(preview) = &maybe_preview {
.line_number self.current_preview_total_lines = preview.total_lines;
.map(|l| u16::try_from(l).unwrap_or(0)), // initialize preview scroll
layout.preview_window.unwrap().height, self.maybe_init_preview_scroll(
); selected_entry
.line_number
.map(|l| u16::try_from(l).unwrap_or(0)),
layout.preview_window.unwrap().height,
);
}
draw_preview_content_block( draw_preview_content_block(
f, f,
layout.preview_window.unwrap(), layout.preview_window.unwrap(),
&selected_entry, &selected_entry,
&preview, &maybe_preview,
&self.rendered_preview_cache, &self.rendered_preview_cache,
self.preview_scroll.unwrap_or(0), self.preview_scroll.unwrap_or(0),
self.config.ui.use_nerd_font_icons, self.config.ui.use_nerd_font_icons,