mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 03:25:23 +00:00
perf(preview): add partial preview rendering and buffer preview requests
This commit is contained in:
parent
c227b2a201
commit
de60437b2f
51
Cargo.lock
generated
51
Cargo.lock
generated
@ -223,6 +223,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@ -949,6 +964,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "faster-hex"
|
||||
version = "0.9.0"
|
||||
@ -2210,28 +2235,6 @@ version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "oorandom"
|
||||
version = "11.1.4"
|
||||
@ -2979,10 +2982,10 @@ checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags 1.3.2",
|
||||
"fancy-regex",
|
||||
"flate2",
|
||||
"fnv",
|
||||
"once_cell",
|
||||
"onig",
|
||||
"plist",
|
||||
"regex-syntax 0.8.5",
|
||||
"serde",
|
||||
@ -3093,6 +3096,7 @@ name = "television-screen"
|
||||
version = "0.0.21"
|
||||
dependencies = [
|
||||
"color-eyre",
|
||||
"devicons",
|
||||
"ratatui",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
@ -3115,6 +3119,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"rustc-hash",
|
||||
"syntect",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"unicode-width 0.2.0",
|
||||
"winapi-util",
|
||||
|
@ -56,13 +56,14 @@ readme = "README.md"
|
||||
|
||||
[workspace.dependencies]
|
||||
directories = "5.0.1"
|
||||
devicons = "0.6.11"
|
||||
color-eyre = "0.6.3"
|
||||
lazy_static = "1.5.0"
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
|
||||
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"
|
||||
clap = { version = "4.5.20", features = ["derive", "cargo", "string"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
@ -104,7 +105,7 @@ copypasta = "0.10.1"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5.1"
|
||||
devicons = "0.6.11"
|
||||
devicons = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
bench = false
|
||||
|
@ -20,6 +20,7 @@ television-derive = { path = "../television-derive", version = "0.0.21" }
|
||||
tracing = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
devicons = { workspace = true }
|
||||
directories = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
@ -27,7 +28,6 @@ lazy_static = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
devicons = "0.6.11"
|
||||
ignore = "0.4.23"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
regex = "1.11.1"
|
||||
|
@ -17,6 +17,7 @@ television-channels = { path = "../television-channels", version = "0.0.21" }
|
||||
television-utils = { path = "../television-utils", version = "0.0.21" }
|
||||
|
||||
syntect = { workspace = true }
|
||||
devicons = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
@ -24,7 +25,6 @@ lazy_static = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
parking_lot = "0.12.3"
|
||||
devicons = "0.6.11"
|
||||
regex = "1.11.1"
|
||||
nom = "7.1"
|
||||
tui = { version = "0.29", default-features = false, package = "ratatui" }
|
||||
|
@ -19,23 +19,44 @@ pub use env::EnvVarPreviewer;
|
||||
pub use env::EnvVarPreviewerConfig;
|
||||
pub use files::FilePreviewer;
|
||||
pub use files::FilePreviewerConfig;
|
||||
use syntect::highlighting::Style;
|
||||
use television_utils::cache::RingSet;
|
||||
use television_utils::syntax::HighlightedLines;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PreviewContent {
|
||||
Empty,
|
||||
FileTooLarge,
|
||||
SyntectHighlightedText(Vec<Vec<(Style, String)>>),
|
||||
SyntectHighlightedText(HighlightedLines),
|
||||
Loading,
|
||||
Timeout,
|
||||
NotSupported,
|
||||
PlainText(Vec<String>),
|
||||
PlainTextWrapped(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 =
|
||||
"Preview for this file type is not supported";
|
||||
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.
|
||||
///
|
||||
@ -47,7 +68,10 @@ pub struct Preview {
|
||||
pub title: String,
|
||||
pub content: PreviewContent,
|
||||
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 {
|
||||
@ -56,7 +80,8 @@ impl Default for Preview {
|
||||
title: String::new(),
|
||||
content: PreviewContent::Empty,
|
||||
icon: None,
|
||||
stale: false,
|
||||
partial_offset: None,
|
||||
total_lines: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,27 +91,22 @@ impl Preview {
|
||||
title: String,
|
||||
content: PreviewContent,
|
||||
icon: Option<FileIcon>,
|
||||
stale: bool,
|
||||
partial_offset: Option<usize>,
|
||||
total_lines: u16,
|
||||
) -> Self {
|
||||
Preview {
|
||||
title,
|
||||
content,
|
||||
icon,
|
||||
stale,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stale(&self) -> Self {
|
||||
Preview {
|
||||
stale: true,
|
||||
..self.clone()
|
||||
partial_offset,
|
||||
total_lines,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_lines(&self) -> u16 {
|
||||
match &self.content {
|
||||
PreviewContent::SyntectHighlightedText(lines) => {
|
||||
lines.len().try_into().unwrap_or(u16::MAX)
|
||||
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)
|
||||
@ -105,6 +125,7 @@ pub struct Previewer {
|
||||
file: FilePreviewer,
|
||||
env_var: EnvVarPreviewer,
|
||||
command: CommandPreviewer,
|
||||
requests: RingSet<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@ -132,6 +153,8 @@ impl PreviewerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
const REQUEST_STACK_SIZE: usize = 20;
|
||||
|
||||
impl Previewer {
|
||||
pub fn new(config: Option<PreviewerConfig>) -> Self {
|
||||
let config = config.unwrap_or_default();
|
||||
@ -140,19 +163,45 @@ impl Previewer {
|
||||
file: FilePreviewer::new(Some(config.file)),
|
||||
env_var: EnvVarPreviewer::new(Some(config.env_var)),
|
||||
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 {
|
||||
PreviewType::Basic => self.basic.preview(entry),
|
||||
PreviewType::EnvVar => self.env_var.preview(entry),
|
||||
PreviewType::Basic => Some(self.basic.preview(entry)),
|
||||
PreviewType::EnvVar => Some(self.env_var.preview(entry)),
|
||||
PreviewType::Files => self.file.preview(entry),
|
||||
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) {
|
||||
self.basic = BasicPreviewer::new(Some(config.basic));
|
||||
self.file = FilePreviewer::new(Some(config.file));
|
||||
|
@ -23,7 +23,8 @@ impl BasicPreviewer {
|
||||
title: entry.name.clone(),
|
||||
content: PreviewContent::PlainTextWrapped(entry.name.clone()),
|
||||
icon: entry.icon,
|
||||
..Default::default()
|
||||
partial_offset: None,
|
||||
total_lines: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ pub struct CommandPreviewer {
|
||||
cache: Arc<Mutex<PreviewCache>>,
|
||||
config: CommandPreviewerConfig,
|
||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||
last_previewed: Arc<Mutex<Arc<Preview>>>,
|
||||
in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
|
||||
}
|
||||
|
||||
@ -53,54 +52,65 @@ impl CommandPreviewer {
|
||||
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||
config,
|
||||
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())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached(&self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||
self.cache.lock().get(&entry.name)
|
||||
}
|
||||
|
||||
pub fn preview(
|
||||
&mut self,
|
||||
entry: &Entry,
|
||||
command: &PreviewCommand,
|
||||
) -> Arc<Preview> {
|
||||
// do we have a preview in cache for that entry?
|
||||
if let Some(preview) = self.cache.lock().get(&entry.name) {
|
||||
return preview.clone();
|
||||
) -> Option<Arc<Preview>> {
|
||||
if let Some(preview) = self.cached(entry) {
|
||||
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, 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) {
|
||||
debug!("Preview already in flight for {:?}", entry.name);
|
||||
return self.last_previewed.lock().clone();
|
||||
return;
|
||||
}
|
||||
|
||||
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||
< MAX_CONCURRENT_PREVIEW_TASKS
|
||||
{
|
||||
self.in_flight_previews.lock().insert(entry.name.clone());
|
||||
self.concurrent_preview_tasks
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let cache = self.cache.clone();
|
||||
let entry_c = entry.clone();
|
||||
let concurrent_tasks = self.concurrent_preview_tasks.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 {
|
||||
try_preview(
|
||||
&command,
|
||||
&entry_c,
|
||||
&cache,
|
||||
&concurrent_tasks,
|
||||
&last_previewed,
|
||||
&in_flight_previews,
|
||||
);
|
||||
});
|
||||
} 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,
|
||||
cache: &Arc<Mutex<PreviewCache>>,
|
||||
concurrent_tasks: &Arc<AtomicU8>,
|
||||
last_previewed: &Arc<Mutex<Arc<Preview>>>,
|
||||
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
|
||||
) {
|
||||
debug!("Computing preview for {:?}", entry.name);
|
||||
let command = format_command(command, entry);
|
||||
debug!("Formatted preview command: {:?}", command);
|
||||
|
||||
let output = shell_command()
|
||||
let child = shell_command()
|
||||
.arg(&command)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
if output.status.success() {
|
||||
let content = String::from_utf8_lossy(&output.stdout);
|
||||
if child.status.success() {
|
||||
let content = String::from_utf8_lossy(&child.stdout);
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::AnsiText(content.to_string()),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
));
|
||||
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
let mut tp = last_previewed.lock();
|
||||
*tp = preview.stale().into();
|
||||
} else {
|
||||
let content = String::from_utf8_lossy(&output.stderr);
|
||||
let content = String::from_utf8_lossy(&child.stderr);
|
||||
let preview = Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::AnsiText(content.to_string()),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
u16::try_from(content.lines().count()).unwrap_or(u16::MAX),
|
||||
));
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
|
||||
concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
|
||||
in_flight_previews.lock().remove(&entry.name);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -26,17 +26,22 @@ impl EnvVarPreviewer {
|
||||
if let Some(preview) = self.cache.get(entry) {
|
||||
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 {
|
||||
title: entry.name.clone(),
|
||||
content: if let Some(preview) = &entry.value {
|
||||
PreviewContent::PlainTextWrapped(
|
||||
maybe_add_newline_after_colon(preview, &entry.name),
|
||||
)
|
||||
} else {
|
||||
PreviewContent::Empty
|
||||
content: match content {
|
||||
Some(content) => PreviewContent::PlainTextWrapped(content),
|
||||
None => PreviewContent::Empty,
|
||||
},
|
||||
icon: entry.icon,
|
||||
..Default::default()
|
||||
partial_offset: None,
|
||||
total_lines,
|
||||
});
|
||||
self.cache.insert(entry.clone(), preview.clone());
|
||||
preview
|
||||
|
@ -1,4 +1,3 @@
|
||||
use color_eyre::Result;
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::collections::HashSet;
|
||||
@ -9,6 +8,8 @@ use std::sync::{
|
||||
atomic::{AtomicU8, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use television_utils::files::{read_into_lines_capped, ReadResult};
|
||||
use television_utils::syntax::HighlightedLines;
|
||||
|
||||
use syntect::{highlighting::Theme, parsing::SyntaxSet};
|
||||
use tracing::{debug, warn};
|
||||
@ -17,7 +18,7 @@ use super::cache::PreviewCache;
|
||||
use crate::previewers::{meta, Preview, PreviewContent};
|
||||
use television_channels::entry;
|
||||
use television_utils::{
|
||||
files::{get_file_size, FileType},
|
||||
files::FileType,
|
||||
strings::preprocess_line,
|
||||
syntax::{self, load_highlighting_assets, HighlightingAssetsExt},
|
||||
};
|
||||
@ -28,7 +29,6 @@ pub struct FilePreviewer {
|
||||
pub syntax_set: Arc<SyntaxSet>,
|
||||
pub syntax_theme: Arc<Theme>,
|
||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||
last_previewed: Arc<Mutex<Arc<Preview>>>,
|
||||
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 BAT_THEME_ENV_VAR: &str = "BAT_THEME";
|
||||
@ -72,30 +68,41 @@ impl FilePreviewer {
|
||||
syntax_set: Arc::new(syntax_set),
|
||||
syntax_theme: Arc::new(theme),
|
||||
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(
|
||||
FxBuildHasher,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a preview for a file entry.
|
||||
///
|
||||
/// # 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);
|
||||
pub fn cached(&self, entry: &entry::Entry) -> Option<Arc<Preview>> {
|
||||
self.cache.lock().get(&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) {
|
||||
debug!("Preview already in flight for {:?}", entry.name);
|
||||
return self.last_previewed.lock().clone();
|
||||
}
|
||||
|
||||
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||
@ -109,22 +116,19 @@ impl FilePreviewer {
|
||||
let syntax_set = self.syntax_set.clone();
|
||||
let syntax_theme = self.syntax_theme.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();
|
||||
tokio::spawn(async move {
|
||||
try_preview(
|
||||
&entry_c,
|
||||
partial_preview,
|
||||
&cache,
|
||||
&syntax_set,
|
||||
&syntax_theme,
|
||||
&concurrent_tasks,
|
||||
&last_previewed,
|
||||
&in_flight_previews,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
self.last_previewed.lock().clone()
|
||||
}
|
||||
|
||||
#[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(
|
||||
entry: &entry::Entry,
|
||||
partial_preview: Option<Arc<Preview>>,
|
||||
cache: &Arc<Mutex<PreviewCache>>,
|
||||
syntax_set: &Arc<SyntaxSet>,
|
||||
syntax_theme: &Arc<Theme>,
|
||||
concurrent_tasks: &Arc<AtomicU8>,
|
||||
last_previewed: &Arc<Mutex<Arc<Preview>>>,
|
||||
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
|
||||
) {
|
||||
debug!("Computing preview for {:?}", 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);
|
||||
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
|
||||
let mut reader = BufReader::new(file);
|
||||
reader.seek(std::io::SeekFrom::Start(0)).unwrap();
|
||||
let preview = compute_highlighted_text_preview(
|
||||
entry,
|
||||
reader,
|
||||
syntax_set,
|
||||
syntax_theme,
|
||||
);
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
in_flight_previews.lock().remove(&entry.name);
|
||||
let mut tp = last_previewed.lock();
|
||||
*tp = preview.stale().into();
|
||||
match read_into_lines_capped(file, PARTIAL_BUFREAD_SIZE) {
|
||||
ReadResult::Full(lines) => {
|
||||
if let Some(content) = compute_highlighted_text_preview(
|
||||
entry,
|
||||
&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,
|
||||
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) => {
|
||||
warn!("Error opening file: {:?}", e);
|
||||
@ -181,44 +242,34 @@ pub fn try_preview(
|
||||
cache.lock().insert(entry.name.clone(), &preview);
|
||||
}
|
||||
concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
|
||||
in_flight_previews.lock().remove(&entry.name);
|
||||
}
|
||||
|
||||
fn compute_highlighted_text_preview(
|
||||
entry: &entry::Entry,
|
||||
reader: BufReader<File>,
|
||||
lines: &[String],
|
||||
syntax_set: &SyntaxSet,
|
||||
syntax_theme: &Theme,
|
||||
) -> Arc<Preview> {
|
||||
previous_lines: Option<HighlightedLines>,
|
||||
) -> Option<PreviewContent> {
|
||||
debug!(
|
||||
"Computing highlights in the background for {:?}",
|
||||
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),
|
||||
lines,
|
||||
syntax_set,
|
||||
syntax_theme,
|
||||
previous_lines,
|
||||
) {
|
||||
Ok(highlighted_lines) => {
|
||||
debug!("Successfully computed highlights for {:?}", entry.name);
|
||||
Arc::new(Preview::new(
|
||||
entry.name.clone(),
|
||||
PreviewContent::SyntectHighlightedText(highlighted_lines),
|
||||
entry.icon,
|
||||
false,
|
||||
))
|
||||
Some(PreviewContent::SyntectHighlightedText(highlighted_lines))
|
||||
}
|
||||
Err(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;
|
||||
}
|
||||
}
|
||||
let total_lines = u16::try_from(lines.len()).unwrap_or(u16::MAX);
|
||||
Arc::new(Preview::new(
|
||||
title.to_string(),
|
||||
PreviewContent::PlainText(lines),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
total_lines,
|
||||
))
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ pub fn not_supported(title: &str) -> Arc<Preview> {
|
||||
title.to_string(),
|
||||
PreviewContent::NotSupported,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
1,
|
||||
))
|
||||
}
|
||||
|
||||
@ -15,7 +16,8 @@ pub fn file_too_large(title: &str) -> Arc<Preview> {
|
||||
title.to_string(),
|
||||
PreviewContent::FileTooLarge,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
1,
|
||||
))
|
||||
}
|
||||
|
||||
@ -25,6 +27,17 @@ pub fn loading(title: &str) -> Arc<Preview> {
|
||||
title.to_string(),
|
||||
PreviewContent::Loading,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
1,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn timeout(title: &str) -> Arc<Preview> {
|
||||
Arc::new(Preview::new(
|
||||
title.to_string(),
|
||||
PreviewContent::Timeout,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
))
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ color-eyre = { workspace = true }
|
||||
syntect = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
devicons = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -1,15 +1,41 @@
|
||||
use devicons::FileIcon;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ratatui::widgets::Paragraph;
|
||||
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)]
|
||||
pub struct RenderedPreviewCache<'a> {
|
||||
previews: FxHashMap<String, Arc<Paragraph<'a>>>,
|
||||
previews: FxHashMap<String, CachedPreview<'a>>,
|
||||
ring_set: RingSet<String>,
|
||||
pub last_preview: Option<CachedPreview<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> RenderedPreviewCache<'a> {
|
||||
@ -17,15 +43,29 @@ impl<'a> RenderedPreviewCache<'a> {
|
||||
RenderedPreviewCache {
|
||||
previews: FxHashMap::default(),
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: String, preview: &Arc<Paragraph<'a>>) {
|
||||
self.previews.insert(key.clone(), preview.clone());
|
||||
pub fn insert(
|
||||
&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) {
|
||||
self.previews.remove(&oldest_key);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use crate::{
|
||||
colors::{Colorscheme, PreviewColorscheme},
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use devicons::FileIcon;
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
use ratatui::{
|
||||
@ -15,7 +16,8 @@ use television_channels::entry::Entry;
|
||||
use television_previewers::{
|
||||
ansi::IntoText,
|
||||
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::{
|
||||
@ -28,14 +30,20 @@ const FILL_CHAR_SLANTED: char = '╱';
|
||||
const FILL_CHAR_EMPTY: char = ' ';
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn build_preview_paragraph(
|
||||
preview_block: Block<'_>,
|
||||
pub fn build_preview_paragraph<'a>(
|
||||
inner: Rect,
|
||||
preview_content: PreviewContent,
|
||||
target_line: Option<u16>,
|
||||
preview_scroll: u16,
|
||||
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 {
|
||||
PreviewContent::AnsiText(text) => {
|
||||
build_ansi_text_paragraph(text, preview_block, preview_scroll)
|
||||
@ -56,7 +64,7 @@ pub fn build_preview_paragraph(
|
||||
}
|
||||
PreviewContent::SyntectHighlightedText(highlighted_lines) => {
|
||||
build_syntect_highlighted_paragraph(
|
||||
highlighted_lines,
|
||||
highlighted_lines.lines,
|
||||
preview_block,
|
||||
target_line,
|
||||
preview_scroll,
|
||||
@ -65,7 +73,7 @@ pub fn build_preview_paragraph(
|
||||
}
|
||||
// meta
|
||||
PreviewContent::Loading => {
|
||||
build_meta_preview_paragraph(inner, "Loading...", FILL_CHAR_EMPTY)
|
||||
build_meta_preview_paragraph(inner, LOADING_MSG, FILL_CHAR_EMPTY)
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
@ -86,28 +94,48 @@ pub fn build_preview_paragraph(
|
||||
.block(preview_block)
|
||||
.alignment(Alignment::Left)
|
||||
.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)),
|
||||
}
|
||||
}
|
||||
|
||||
const ANSI_BEFORE_CONTEXT_SIZE: u16 = 10;
|
||||
const ANSI_CONTEXT_SIZE: usize = 150;
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn build_ansi_text_paragraph(
|
||||
text: String,
|
||||
preview_block: Block,
|
||||
preview_scroll: u16,
|
||||
) -> Paragraph {
|
||||
let text = replace_non_printable(
|
||||
text.as_bytes(),
|
||||
&ReplaceNonPrintableConfig {
|
||||
replace_line_feed: false,
|
||||
replace_control_characters: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.0
|
||||
.into_text()
|
||||
.unwrap();
|
||||
Paragraph::new(text)
|
||||
let lines = text.lines();
|
||||
let skip =
|
||||
preview_scroll.saturating_sub(ANSI_BEFORE_CONTEXT_SIZE) as usize;
|
||||
let context = lines
|
||||
.skip(skip)
|
||||
.take(ANSI_CONTEXT_SIZE)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let mut text = "\n".repeat(skip);
|
||||
text.push_str(
|
||||
&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)
|
||||
.scroll((preview_scroll, 0))
|
||||
}
|
||||
@ -244,20 +272,18 @@ pub fn build_meta_preview_paragraph<'a>(
|
||||
Paragraph::new(Text::from(lines))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn draw_preview_content_block(
|
||||
fn draw_content_outer_block(
|
||||
f: &mut Frame,
|
||||
rect: Rect,
|
||||
entry: &Entry,
|
||||
preview: &Arc<Preview>,
|
||||
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
|
||||
preview_scroll: u16,
|
||||
use_nerd_font_icons: bool,
|
||||
colorscheme: &Colorscheme,
|
||||
) -> Result<()> {
|
||||
icon: Option<FileIcon>,
|
||||
title: &str,
|
||||
use_nerd_font_icons: bool,
|
||||
) -> Result<Rect> {
|
||||
let mut preview_title_spans = vec![Span::from(" ")];
|
||||
if preview.icon.is_some() && use_nerd_font_icons {
|
||||
let icon = preview.icon.as_ref().unwrap();
|
||||
// optional icon
|
||||
if icon.is_some() && use_nerd_font_icons {
|
||||
let icon = icon.as_ref().unwrap();
|
||||
preview_title_spans.push(Span::styled(
|
||||
{
|
||||
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)?),
|
||||
));
|
||||
}
|
||||
// preview title
|
||||
preview_title_spans.push(Span::styled(
|
||||
shrink_with_ellipsis(
|
||||
&replace_non_printable(
|
||||
preview.title.as_bytes(),
|
||||
title.as_bytes(),
|
||||
&ReplaceNonPrintableConfig::default(),
|
||||
)
|
||||
.0,
|
||||
@ -279,6 +306,8 @@ pub fn draw_preview_content_block(
|
||||
Style::default().fg(colorscheme.preview.title_fg).bold(),
|
||||
));
|
||||
preview_title_spans.push(Span::from(" "));
|
||||
|
||||
// build the preview block
|
||||
let preview_outer_block = Block::default()
|
||||
.title_top(
|
||||
Line::from(preview_title_spans)
|
||||
@ -294,47 +323,111 @@ pub fn draw_preview_content_block(
|
||||
)
|
||||
.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);
|
||||
f.render_widget(preview_outer_block, rect);
|
||||
Ok(inner)
|
||||
}
|
||||
|
||||
let target_line = entry.line_number.map(|l| u16::try_from(l).unwrap_or(0));
|
||||
let cache_key = compute_cache_key(entry);
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
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
|
||||
if let Some(preview_paragraph) =
|
||||
rendered_preview_cache.lock().unwrap().get(&cache_key)
|
||||
{
|
||||
let p = preview_paragraph.as_ref().clone();
|
||||
f.render_widget(p.scroll((preview_scroll, 0)), inner);
|
||||
// check if the rendered preview content is already in the cache
|
||||
let cache_key = compute_cache_key(entry);
|
||||
if let Some(rp) =
|
||||
rendered_preview_cache.lock().unwrap().get(&cache_key)
|
||||
{
|
||||
// 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(());
|
||||
}
|
||||
// If not, render the preview content and cache it if not empty
|
||||
let c_scheme = colorscheme.clone();
|
||||
let rp = build_preview_paragraph(
|
||||
preview_inner_block,
|
||||
inner,
|
||||
preview.content.clone(),
|
||||
target_line,
|
||||
preview_scroll,
|
||||
c_scheme,
|
||||
);
|
||||
if !preview.stale {
|
||||
rendered_preview_cache
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(cache_key, &Arc::new(rp.clone()));
|
||||
// else if last_preview exists
|
||||
if let Some(last_preview) =
|
||||
&rendered_preview_cache.lock().unwrap().last_preview
|
||||
{
|
||||
let inner = draw_content_outer_block(
|
||||
f,
|
||||
rect,
|
||||
colorscheme,
|
||||
last_preview.icon,
|
||||
&last_preview.title,
|
||||
use_nerd_font_icons,
|
||||
)?;
|
||||
|
||||
f.render_widget(
|
||||
last_preview
|
||||
.paragraph
|
||||
.as_ref()
|
||||
.clone()
|
||||
.scroll((preview_scroll, 0)),
|
||||
inner,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
f.render_widget(
|
||||
Arc::new(rp).as_ref().clone().scroll((preview_scroll, 0)),
|
||||
inner,
|
||||
);
|
||||
// otherwise render empty preview
|
||||
let inner = draw_content_outer_block(
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -20,10 +20,11 @@ directories = { workspace = true }
|
||||
syntect = { workspace = true }
|
||||
unicode-width = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
ignore = "0.4.23"
|
||||
bat = { version = "0.24.0", default-features = false, features = [
|
||||
"regex-onig",
|
||||
"regex-fancy",
|
||||
] }
|
||||
gag = "1.0.0"
|
||||
|
||||
|
@ -48,6 +48,17 @@ pub struct RingSet<T> {
|
||||
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>
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone + std::fmt::Debug,
|
||||
@ -97,6 +108,11 @@ where
|
||||
pub fn contains(&self, key: &T) -> bool {
|
||||
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)]
|
||||
|
@ -1,6 +1,8 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::fmt::Debug;
|
||||
use std::fs::File;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@ -14,6 +16,51 @@ use crate::strings::{
|
||||
};
|
||||
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! {
|
||||
pub static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into();
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ pub fn replace_non_printable(
|
||||
input: &[u8],
|
||||
config: &ReplaceNonPrintableConfig,
|
||||
) -> (String, Vec<i16>) {
|
||||
let mut output = String::new();
|
||||
let mut output = String::with_capacity(input.len());
|
||||
let mut offsets = Vec::new();
|
||||
let mut cumulative_offset: i16 = 0;
|
||||
|
||||
|
@ -1,31 +1,95 @@
|
||||
use bat::assets::HighlightingAssets;
|
||||
use color_eyre::Result;
|
||||
use gag::Gag;
|
||||
use std::path::{Path, PathBuf};
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::{Style, Theme};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::highlighting::{
|
||||
HighlightIterator, HighlightState, Highlighter, Style, Theme,
|
||||
};
|
||||
use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
|
||||
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(
|
||||
file_path: &Path,
|
||||
lines: Vec<String>,
|
||||
lines: &[String],
|
||||
syntax_set: &SyntaxSet,
|
||||
syntax_theme: &Theme,
|
||||
) -> color_eyre::Result<Vec<Vec<(Style, String)>>> {
|
||||
let syntax =
|
||||
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()
|
||||
});
|
||||
) -> Result<Vec<Vec<(Style, String)>>> {
|
||||
let syntax = set_syntax_set(syntax_set, file_path);
|
||||
let mut highlighter = HighlightLines::new(syntax, syntax_theme);
|
||||
let mut highlighted_lines = Vec::new();
|
||||
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(
|
||||
hl_regions
|
||||
.iter()
|
||||
@ -36,13 +100,86 @@ pub fn compute_highlights_for_path(
|
||||
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)]
|
||||
pub fn compute_highlights_for_line<'a>(
|
||||
line: &'a str,
|
||||
syntax_set: &SyntaxSet,
|
||||
syntax_theme: &Theme,
|
||||
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)?;
|
||||
match syntax {
|
||||
None => {
|
||||
|
@ -560,20 +560,26 @@ impl Television {
|
||||
&& !matches!(selected_entry.preview_type, PreviewType::None)
|
||||
{
|
||||
// preview content
|
||||
let preview = self.previewer.preview(&selected_entry);
|
||||
self.current_preview_total_lines = preview.total_lines();
|
||||
// initialize preview scroll
|
||||
self.maybe_init_preview_scroll(
|
||||
selected_entry
|
||||
.line_number
|
||||
.map(|l| u16::try_from(l).unwrap_or(0)),
|
||||
layout.preview_window.unwrap().height,
|
||||
);
|
||||
let maybe_preview = self.previewer.preview(&selected_entry);
|
||||
|
||||
let _ = self.previewer.preview(&selected_entry);
|
||||
|
||||
if let Some(preview) = &maybe_preview {
|
||||
self.current_preview_total_lines = preview.total_lines;
|
||||
// initialize preview scroll
|
||||
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(
|
||||
f,
|
||||
layout.preview_window.unwrap(),
|
||||
&selected_entry,
|
||||
&preview,
|
||||
&maybe_preview,
|
||||
&self.rendered_preview_cache,
|
||||
self.preview_scroll.unwrap_or(0),
|
||||
self.config.ui.use_nerd_font_icons,
|
||||
|
Loading…
x
Reference in New Issue
Block a user