Skip to content

Commit

Permalink
preload raw file content preview while computing highlights
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpasmantier committed Sep 30, 2024
1 parent 85cf6ef commit d2b0fe9
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 70 deletions.
2 changes: 1 addition & 1 deletion src/components/previewers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub enum PreviewContent {
Image(Box<dyn StatefulProtocol>),
Loading,
NotSupported,
PlainText(String),
PlainText(Vec<String>),
PlainTextWrapped(String),
}

Expand Down
166 changes: 113 additions & 53 deletions src/components/previewers/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ use image::ImageReader;
use image::Rgb;
use ratatui_image::picker::Picker;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use syntect::easy::HighlightLines;
use tokio::sync::Mutex;

use syntect::highlighting::{Style, ThemeSet};
Expand All @@ -14,7 +19,7 @@ use tracing::{debug, info, warn};

use crate::components::finders;
use crate::components::previewers::{Preview, PreviewContent};
use crate::components::strings::{replace_nonprintable, truncate_at_char_boundary};
use crate::components::strings::{replace_nonprintable, slice_at_char_boundary};

pub struct FilePreviewer {
cache: Arc<Mutex<HashMap<String, Arc<Preview>>>>,
Expand Down Expand Up @@ -44,20 +49,20 @@ impl FilePreviewer {
const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;

pub async fn preview(&mut self, entry: &finders::Entry) -> Arc<Preview> {
let path = Path::new(&entry.name);
let path_buf = PathBuf::from(&entry.name);
// check if we have that preview in the cache
if let Some(preview) = self.cache.lock().await.get(&entry.name) {
return preview.clone();
}
if let Ok(metadata) = std::fs::metadata(path) {
if let Ok(metadata) = std::fs::metadata(&path_buf) {
if metadata.len() > Self::MAX_FILE_SIZE {
return Arc::new(file_too_large(&entry.name));
}
} else {
warn!("Error getting metadata for {:?}", entry);
}
let cache = self.cache.clone();
let is_image = match infer::get_from_path(path) {
let is_image = match infer::get_from_path(&path_buf) {
Ok(Some(t)) => t.mime_type().contains("image"),
_ => false,
};
Expand Down Expand Up @@ -90,36 +95,85 @@ impl FilePreviewer {
.insert(entry_c.name.clone(), preview.clone());
}
});
return Arc::new(Preview {
title: entry.name.clone(),
content: PreviewContent::Loading,
target_line: 0,
});
} else {
// TODO: add a check for text files to avoid trying to parse binary files
// text
let preview_content = match File::open(&path_buf) {
Ok(file) => {
let reader = BufReader::new(file);
let mut lines = Vec::new();
let mut err = false;

// read through the file and preprocess the lines
for maybe_line in reader.lines() {
match maybe_line {
Ok(line) => lines.push(preprocess_line(&line)),
Err(e) => {
warn!("Error reading file: {:?}", e);
err = true;
break;
}
}
}
if err {
PreviewContent::NotSupported
} else {
PreviewContent::PlainText(lines)
}
}
Err(e) => {
warn!("Error opening file: {:?}", e);
PreviewContent::NotSupported
}
};
let temp_preview = Arc::new(Preview {
title: entry.name.clone(),
content: preview_content.clone(),
target_line: entry.line_number.unwrap_or(0) as u16,
});
self.cache
.lock()
.await
.insert(entry.name.clone(), temp_preview.clone());
let cache = self.cache.clone();
let syntax_set = self.syntax_set.clone();
let syntax_theme = self.syntax_theme.clone();
let entry_c = entry.clone();
tokio::spawn(async move {
debug!("Computing highlights for {:?}", entry_c.name);
let preview = match compute_highlights(&entry_c.name, &syntax_set, &syntax_theme) {
Ok(highlighted_lines) => Preview {
title: entry_c.name.clone(),
content: PreviewContent::HighlightedText(highlighted_lines),
target_line: entry_c.line_number.unwrap_or(0) as u16,
},
Err(e) => {
warn!("Error computing highlights: {:?}", e);
preview_not_supported(&entry_c.name)
}
};
cache
.lock()
.await
.insert(entry_c.name.clone(), Arc::new(preview.clone()));
});
match preview_content {
PreviewContent::PlainText(lines) => {
tokio::spawn(async move {
debug!("Computing highlights for {:?}", entry_c.name);
let preview = match compute_highlights(
&path_buf,
lines,
&syntax_set,
&syntax_theme,
) {
Ok(highlighted_lines) => Preview {
title: entry_c.name.clone(),
content: PreviewContent::HighlightedText(highlighted_lines),
target_line: entry_c.line_number.unwrap_or(0) as u16,
},
Err(e) => {
warn!("Error computing highlights: {:?}", e);
preview_not_supported(&entry_c.name)
}
};
cache
.lock()
.await
.insert(entry_c.name.clone(), Arc::new(preview));
});
}
_ => {}
}
return temp_preview;
}
return Arc::new(Preview {
title: entry.name.clone(),
content: PreviewContent::Loading,
target_line: 0,
});
}
}

Expand All @@ -141,37 +195,43 @@ fn file_too_large(title: &str) -> Preview {

const MAX_LINE_LENGTH: usize = 500;

fn compute_highlights(
file_path: &str,
fn preprocess_line(line: &str) -> String {
replace_nonprintable(
{
if line.len() > MAX_LINE_LENGTH {
slice_at_char_boundary(line, MAX_LINE_LENGTH)
} else {
line
}
}
.trim_end_matches(|c| c == '\r' || c == '\n' || c == '\0')
.as_bytes(),
2,
)
}

fn compute_highlights<'a>(
file_path: &Path,
lines: Vec<String>,
syntax_set: &SyntaxSet,
syntax_theme: &Theme,
) -> Result<Vec<Vec<(Style, String)>>> {
let mut highlighter = HighlightFile::new(file_path, &syntax_set, &syntax_theme)?;
let mut line = String::new();
let syntax = syntax_set
.find_syntax_for_file(file_path)?
.unwrap_or_else(|| {
warn!("No syntax found for {:?}", file_path);
syntax_set.find_syntax_plain_text()
});
let mut highlighter = HighlightLines::new(syntax, syntax_theme);
let mut highlighted_lines = Vec::new();
while let Ok(bytes_read) = highlighter.reader.read_line(&mut line) {
if bytes_read == 0 {
break;
}
if line.len() > MAX_LINE_LENGTH {
line = truncate_at_char_boundary(&line, MAX_LINE_LENGTH).to_string();
}
let processed_line = replace_nonprintable(
line.trim_end_matches(|c| c == '\r' || c == '\n' || c == '\0')
.as_bytes(),
2,
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(),
);
let line_regions = highlighter
.highlight_lines
.highlight_line(&processed_line, &syntax_set)?;

let mut cloned_regions = Vec::new();
for region in line_regions.iter().take(processed_line.len() - 1) {
cloned_regions.push((region.0, region.1.to_owned()));
}

highlighted_lines.push(cloned_regions);
line.clear();
}
Ok(highlighted_lines)
}
2 changes: 1 addition & 1 deletion src/components/strings.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use lazy_static::lazy_static;
use std::fmt::Write;

pub fn truncate_at_char_boundary(s: &str, byte_index: usize) -> &str {
pub fn slice_at_char_boundary(s: &str, byte_index: usize) -> &str {
let mut char_index = byte_index;
while !s.is_char_boundary(char_index) {
char_index -= 1;
Expand Down
38 changes: 23 additions & 15 deletions src/components/television.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,11 +678,15 @@ impl Television {
match &preview.content {
PreviewContent::PlainText(content) => {
let mut lines = Vec::new();
for line in content.lines() {
lines.push(Line::styled(
line.to_string(),
Style::default().fg(DEFAULT_RESULT_PREVIEW_FG),
));
for line in content.iter() {
lines.push(Line::from(vec![
build_line_number_span(lines.len() + 1),
Span::styled(" │ ", Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()),
Span::styled(
line.to_string(),
Style::default().fg(DEFAULT_RESULT_PREVIEW_FG),
),
]));
}
let text = Text::from(lines);
Paragraph::new(text)
Expand Down Expand Up @@ -815,6 +819,13 @@ impl InputActionHandler for Input {
}
}

fn build_line_number_span<'a>(line_number: usize) -> Span<'a> {
Span::styled(
format!("{:5} ", line_number),
Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG),
)
}

fn compute_paragraph_from_highlighted_lines(
highlighted_lines: &Vec<Vec<(syntect::highlighting::Style, String)>>,
line_specifier: Option<usize>,
Expand All @@ -823,16 +834,13 @@ fn compute_paragraph_from_highlighted_lines(
.iter()
.enumerate()
.map(|(i, l)| {
let line_number = Span::styled(
format!("{:5} ", i + 1),
Style::default().fg(
if line_specifier.is_some() && i == line_specifier.unwrap() - 1 {
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
} else {
DEFAULT_PREVIEW_GUTTER_FG
},
),
);
let line_number = build_line_number_span(i + 1).style(Style::default().fg(
if line_specifier.is_some() && i == line_specifier.unwrap() - 1 {
DEFAULT_PREVIEW_GUTTER_SELECTED_FG
} else {
DEFAULT_PREVIEW_GUTTER_FG
},
));
Line::from_iter(
std::iter::once(line_number)
.chain(std::iter::once(Span::styled(
Expand Down

0 comments on commit d2b0fe9

Please sign in to comment.